From f515fae4e30941b4dd1dee8f9dea263bc3ee9631 Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Mon, 13 Apr 2026 09:11:52 +0000 Subject: [PATCH] First commit --- .claude/commands/screens.md | 29 +++++++ CLAUDE.md | 41 ++++++++++ README.md | 38 +++++++++ screens/Screen.png | Bin 0 -> 35408 bytes server.mjs | 151 ++++++++++++++++++++++++++++++++++++ server.sh | 78 +++++++++++++++++++ 6 files changed, 337 insertions(+) create mode 100644 .claude/commands/screens.md create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 screens/Screen.png create mode 100644 server.mjs create mode 100755 server.sh diff --git a/.claude/commands/screens.md b/.claude/commands/screens.md new file mode 100644 index 0000000..aabc934 --- /dev/null +++ b/.claude/commands/screens.md @@ -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/** +``` + +## 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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8565442 --- /dev/null +++ b/CLAUDE.md @@ -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 9–85): 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 87–110): Custom binary multipart/form-data parser; no external library. +3. **HTTP server** (lines 112–145): 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..38307ca --- /dev/null +++ b/README.md @@ -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://: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. diff --git a/screens/Screen.png b/screens/Screen.png new file mode 100644 index 0000000000000000000000000000000000000000..1ff963e1d201d0866cec873613ebabf28abf956f GIT binary patch literal 35408 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≦PpgrqdwFMhEPO+SjZvrc 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 M?kGy-Nf^HPUl_dL-v9sr literal 0 HcmV?d00001 diff --git a/server.mjs b/server.mjs new file mode 100644 index 0000000..b39615b --- /dev/null +++ b/server.mjs @@ -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 = ` + + + + + 📸 Screens Upload + + + +
+

📸 Screens Upload

+

Screenshots direkt in den screens/ Ordner im aktuellen Verzeichnis hochladen.

+
+ + + Drag & Drop oder klicken zum Auswählen +
+
+ +
+
+ + +`; + +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}`); +}); diff --git a/server.sh b/server.sh new file mode 100755 index 0000000..106d737 --- /dev/null +++ b/server.sh @@ -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