First commit
This commit is contained in:
29
.claude/commands/screens.md
Normal file
29
.claude/commands/screens.md
Normal 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
41
CLAUDE.md
Normal 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 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.
|
||||||
38
README.md
Normal file
38
README.md
Normal 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
BIN
screens/Screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
151
server.mjs
Normal file
151
server.mjs
Normal 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
78
server.sh
Executable 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
|
||||||
Reference in New Issue
Block a user