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