First commit

This commit is contained in:
2026-04-13 09:11:52 +00:00
commit f515fae4e3
6 changed files with 337 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
# Screens
Suche nach Screenshots im `screens/`-Verzeichnis des aktuellen Arbeitsverzeichnisses (`process.cwd()/screens`).
## Vorgehen
1. Prüfe ob `./screens/` existiert — wenn nicht, darauf hinweisen und ggf. den Server starten
2. Liste alle Dateien in `./screens/` auf (sortiert nach Änderungszeit, neueste zuerst)
3. Zeige Dateinamen, Größe und Datum übersichtlich an
4. Wenn der Nutzer nach einer bestimmten Datei fragt, suche per Glob oder Grep in `./screens/`
## Befehle
```bash
# Alle Screenshots auflisten (neueste zuerst)
ls -lt ./screens/
# Nur Bilddateien
ls -lt ./screens/*.{png,jpg,jpeg,gif,webp,bmp} 2>/dev/null
# Nach Name suchen
ls ./screens/*<suchbegriff>*
```
## Hinweise
- Dateien werden durch den Screenshot-Upload-Server in `./screens/` gespeichert
- Server starten: `server.sh start` (aus dem Projektverzeichnis)
- Wenn `./screens/` nicht existiert, wurde noch kein Upload durchgeführt oder der Server läuft nicht

41
CLAUDE.md Normal file
View File

@@ -0,0 +1,41 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
A minimal, self-contained image upload server written in vanilla Node.js (ES modules). No external dependencies — only Node.js built-ins (`http`, `fs`, `path`, `url`).
## Running the Server
```bash
# Direct
node server.mjs
# Via daemon manager
./server.sh start
./server.sh stop
./server.sh restart
./server.sh status
```
- Listens on `0.0.0.0:8765` (hardcoded in `server.mjs`)
- Logs to `/tmp/server_mjs.log`; PID tracked at `/tmp/server_mjs.pid`
## Architecture
The entire application lives in two files:
**`server.mjs`** — Three logical sections:
1. **Embedded HTML UI** (lines 985): Full self-contained frontend (HTML/CSS/JS) as a string constant — no separate asset files. Dark theme, drag-and-drop, German labels.
2. **`parseMultipart()`** (lines 87110): Custom binary multipart/form-data parser; no external library.
3. **HTTP server** (lines 112145): Two routes — `GET /` serves the UI, `POST /upload` saves files to the directory where `server.mjs` lives.
**`server.sh`** — Daemon management (start/stop/restart/status). Note: the path inside the script points to `/home/joachim/git/ai-coding-kit/screens/server.mjs`, not the local copy — update if deploying from this directory.
## Key Conventions
- **Save location**: Files are written to the same directory as `server.mjs` via `fileURLToPath(import.meta.url)`.
- **Filename sanitization**: `path.basename()` prevents path traversal; regex `/[^a-zA-Z0-9._\- ()äöüÄÖÜß]/g`replaces disallowed characters with underscores. German characters are intentionally allowed.
- **Synchronous writes**: `fs.writeFileSync()` is used deliberately for simplicity.
- **UI is embedded**: Keep HTML/CSS/JS inside the string constant in `server.mjs` — there are no separate asset files by design.

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# Screenshot-Server
Ein minimaler Upload-Server für Screenshots und Bilder, geschrieben in vanilla Node.js ohne externe Abhängigkeiten.
## Funktionsweise
Der Server stellt eine Web-Oberfläche bereit, über die Bilder per Drag & Drop oder Dateiauswahl hochgeladen werden können. Die Dateien werden direkt in das Verzeichnis gespeichert, in dem `server.mjs` liegt.
- **Port:** 8765
- **Erreichbar unter:** `http://<IP>:8765`
- **Unterstützte Dateitypen:** Bilder (`image/*`)
- **Dateinamen:** Sonderzeichen werden bereinigt; Umlaute (äöüÄÖÜß) sind erlaubt
## Voraussetzungen
- Node.js (ES-Module-Unterstützung, d.h. Node 14+)
- Kein `npm install` notwendig
## Starten
### Direkt
```bash
node server.mjs
```
### Als Hintergrunddienst (über `server.sh`)
```bash
./server.sh start # Starten
./server.sh stop # Stoppen
./server.sh restart # Neustart
./server.sh status # Status anzeigen
```
Logs werden nach `/tmp/server_mjs.log` geschrieben.
> **Hinweis:** In `server.sh` ist der Pfad zur `server.mjs` hardcodiert. Bei abweichendem Speicherort muss `APP_CMD` in Zeile 3 angepasst werden.

BIN
screens/Screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

151
server.mjs Normal file
View File

@@ -0,0 +1,151 @@
import http from "http";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const DIR = path.join(process.cwd(), "screens");
if (!fs.existsSync(DIR)) fs.mkdirSync(DIR, { recursive: true });
const PORT = 8765;
const HTML = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>📸 Screens Upload</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #fff; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.card { background: #111; border: 1px solid #222; border-radius: 16px; padding: 40px; width: 100%; max-width: 480px; }
h1 { font-size: 1.4rem; margin-bottom: 8px; }
p { color: #666; font-size: 0.9rem; margin-bottom: 24px; }
.drop-zone { border: 2px dashed #333; border-radius: 12px; padding: 40px; text-align: center; cursor: pointer; transition: all 0.2s; }
.drop-zone:hover, .drop-zone.over { border-color: #fff; background: #1a1a1a; }
.drop-zone input { display: none; }
.drop-zone label { cursor: pointer; font-size: 2rem; display: block; margin-bottom: 8px; }
.drop-zone span { color: #555; font-size: 0.85rem; }
.btn { display: block; width: 100%; margin-top: 16px; padding: 12px; background: #fff; color: #000; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn:hover { background: #ddd; }
.status { margin-top: 16px; padding: 12px; border-radius: 8px; font-size: 0.85rem; display: none; }
.status.ok { background: #0d2b0d; color: #4ade80; border: 1px solid #166534; display: block; }
.status.err { background: #2b0d0d; color: #f87171; border: 1px solid #991b1b; display: block; }
.files { margin-top: 8px; font-size: 0.8rem; color: #888; }
</style>
</head>
<body>
<div class="card">
<h1>📸 Screens Upload</h1>
<p>Screenshots direkt in den <code>screens/</code> Ordner im aktuellen Verzeichnis hochladen.</p>
<div class="drop-zone" id="zone">
<label for="file">🖼️</label>
<input type="file" id="file" multiple accept="image/*">
<span>Drag & Drop oder klicken zum Auswählen</span>
<div class="files" id="filelist"></div>
</div>
<button class="btn" onclick="upload()">Hochladen</button>
<div class="status" id="status"></div>
</div>
<script>
const zone = document.getElementById("zone");
const fileInput = document.getElementById("file");
const filelist = document.getElementById("filelist");
const status = document.getElementById("status");
fileInput.addEventListener("change", updateList);
zone.addEventListener("dragover", e => { e.preventDefault(); zone.classList.add("over"); });
zone.addEventListener("dragleave", () => zone.classList.remove("over"));
zone.addEventListener("drop", e => {
e.preventDefault(); zone.classList.remove("over");
fileInput.files = e.dataTransfer.files;
updateList();
});
function updateList() {
const names = [...fileInput.files].map(f => f.name).join(", ");
filelist.textContent = names ? "📎 " + names : "";
}
async function upload() {
if (!fileInput.files.length) { showStatus("Keine Datei ausgewählt.", "err"); return; }
const form = new FormData();
for (const f of fileInput.files) form.append("file", f);
try {
const res = await fetch("/upload", { method: "POST", body: form });
const text = await res.text();
if (res.ok) { showStatus("✅ " + text, "ok"); fileInput.value = ""; filelist.textContent = ""; }
else showStatus("❌ " + text, "err");
} catch(e) { showStatus("❌ Verbindungsfehler", "err"); }
}
function showStatus(msg, type) {
status.textContent = msg;
status.className = "status " + type;
}
</script>
</body>
</html>`;
function parseMultipart(body, boundary) {
const files = [];
const sep = Buffer.from("--" + boundary);
const parts = [];
let start = body.indexOf(sep) + sep.length + 2;
while (start < body.length) {
const end = body.indexOf(sep, start);
if (end === -1) break;
parts.push(body.slice(start, end - 2));
start = end + sep.length + 2;
}
for (const part of parts) {
const headerEnd = part.indexOf("\r\n\r\n");
if (headerEnd === -1) continue;
const header = part.slice(0, headerEnd).toString();
const content = part.slice(headerEnd + 4);
const nameMatch = header.match(/name="([^"]+)"/);
const fileMatch = header.match(/filename="([^"]+)"/);
if (nameMatch && fileMatch) {
files.push({ name: fileMatch[1], data: content });
}
}
return files;
}
const server = http.createServer((req, res) => {
if (req.method === "GET" && req.url === "/") {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return res.end(HTML);
}
if (req.method === "POST" && req.url === "/upload") {
const ct = req.headers["content-type"] || "";
const match = ct.match(/boundary=(.+)/);
if (!match) {
res.writeHead(400); return res.end("Kein Boundary");
}
const boundary = match[1];
const chunks = [];
req.on("data", c => chunks.push(c));
req.on("end", () => {
const body = Buffer.concat(chunks);
const files = parseMultipart(body, boundary);
if (!files.length) { res.writeHead(400); return res.end("Keine Datei gefunden"); }
const saved = [];
for (const file of files) {
const safeName = path.basename(file.name).replace(/[^a-zA-Z0-9._\- ()äöüÄÖÜß]/g, "_");
const dest = path.join(DIR, safeName);
fs.writeFileSync(dest, file.data);
saved.push(safeName);
console.log(`✅ Gespeichert: ${safeName}`);
}
res.writeHead(200); res.end(saved.join(", ") + " gespeichert.");
});
return;
}
res.writeHead(404); res.end("Not found");
});
server.listen(PORT, "0.0.0.0", () => {
console.log(`📸 Screens Upload Server läuft auf http://0.0.0.0:${PORT}`);
console.log(`📁 Speicherort: ${DIR}`);
});

78
server.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_CMD="node $SCRIPT_DIR/server.mjs"
PID_FILE="/tmp/server_mjs.pid"
start() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p $PID > /dev/null 2>&1; then
echo "Service läuft bereits (PID: $PID)"
exit 1
else
echo "Alte PID-Datei gefunden, wird entfernt"
rm -f "$PID_FILE"
fi
fi
echo "Starte Service..."
nohup $APP_CMD > /tmp/server_mjs.log 2>&1 &
echo $! > "$PID_FILE"
echo "Gestartet mit PID $(cat $PID_FILE)"
}
stop() {
if [ ! -f "$PID_FILE" ]; then
echo "Keine PID-Datei gefunden läuft der Service?"
exit 1
fi
PID=$(cat "$PID_FILE")
if ps -p $PID > /dev/null 2>&1; then
echo "Stoppe Service (PID: $PID)..."
kill $PID
rm -f "$PID_FILE"
echo "Gestoppt"
else
echo "Prozess läuft nicht mehr, entferne PID-Datei"
rm -f "$PID_FILE"
fi
}
status() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p $PID > /dev/null 2>&1; then
echo "Service läuft (PID: $PID)"
else
echo "PID-Datei vorhanden, aber Prozess läuft nicht"
exit 1
fi
else
echo "Service gestoppt"
exit 3
fi
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
sleep 1
start
;;
status)
status
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac