From e11259756802c847ed0ac510dff036436f119f36 Mon Sep 17 00:00:00 2001 From: DeeCee03 Date: Tue, 20 Oct 2026 14:31:57 +0200 Subject: [PATCH] Dashboard fuer Kollegen auf Cloudflare Pages veroeffentlichen.sh baut mit werkzeuge/pages_bauen.py den Ordner .pages und laedt ihn nach hivegrid-marketing.pages.dev. Die Middleware in werkzeuge/pages/ uebernimmt Passwort, nur lesen und die Auskuenfte von serve.py (Dokumente, Stand, Suche). Der Tunnel bleibt als Notloesung. Anleitung in .docs/Freigabe.md, Hinweis fuer Agents in .claude/CLAUDE.md. Co-Authored-By: Claude Opus 5.5 --- .claude/CLAUDE.md | 7 ++- .docs/Freigabe.md | 88 +++++++++++++++++++------------ .gitignore | 10 ++++ veroeffentlichen.sh | 8 +++ werkzeuge/pages/_middleware.js | 93 +++++++++++++++++++++++++++++++++ werkzeuge/pages_bauen.py | 94 ++++++++++++++++++++++++++++++++++ 6 files changed, 265 insertions(+), 35 deletions(-) create mode 100755 veroeffentlichen.sh create mode 100644 werkzeuge/pages/_middleware.js create mode 100644 werkzeuge/pages_bauen.py diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 22121ee..ff2ef4f 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,5 +1,8 @@ # HiveGrid Marketing -Das Dashboard lässt sich für Kollegen freigeben, über einen Tunnel mit Passwort und nur zum Lesen. Wenn Dejan darum bittet, die Freigabe oder eine Session für die Kollegen zu starten: nach `.docs/Freigabe.md` vorgehen. Dort stehen auch das Passwort und wie man prüft und beendet. +Das Dashboard ist für Kollegen freigegeben unter https://hivegrid-marketing.pages.dev, mit Passwort und nur zum Lesen. +**Nach jedem „commit und push“ im Hub auch `./veroeffentlichen.sh` ausführen**, damit die Kollegen den neuen Stand +sehen. Wie man hochlädt und prüft, das Passwort und die Notlösung per Tunnel: `.docs/Freigabe.md`. -Interne Dateien, die Kollegen nicht sehen sollen, gehören in einen Ordner mit Punkt am Anfang (`.docs/`). Alles andere im Hub ist über die Freigabe sichtbar. +Interne Dateien, die Kollegen nicht sehen sollen, gehören in einen Ordner mit Punkt am Anfang (`.docs/`). Alles andere, +was in git steht, ist über die Freigabe sichtbar. diff --git a/.docs/Freigabe.md b/.docs/Freigabe.md index 241f659..1fed7a2 100644 --- a/.docs/Freigabe.md +++ b/.docs/Freigabe.md @@ -1,63 +1,85 @@ # Freigabe für Kollegen -**Stand:** 19.10.2026. Das Dashboard lässt sich über einen Cloudflare-Tunnel für Kollegen freigeben, mit Passwort und nur zum Lesen. Diese Datei sieht von außen niemand: Die Freigabe liefert nichts aus, was mit einem Punkt beginnt. +**Stand:** 20.10.2026. Das Dashboard ist für Kollegen freigegeben, mit Passwort und nur zum Lesen. Diese Datei sieht von +außen niemand: Beide Wege liefern nichts aus, was mit einem Punkt beginnt. + +Es gibt zwei Wege. **Standard ist Cloudflare Pages**, der läuft ohne Dejans Laptop. Der Tunnel ist nur noch die +Notlösung, zum Beispiel wenn ein Stand sofort und ohne Hochladen sichtbar sein soll. --- ## Zugang -- **Passwort:** `attiJia1GXxGeIKg`, der Name ist egal. -- Maßgeblich ist die Datei `.freigabe-passwort` (nicht in git). Wird sie gelöscht, erzeugt der nächste Start ein neues Passwort. Dann hier nachtragen. -- **Die Adresse ist bei jedem Start neu,** zum Beispiel `https://dock-texture-dna-reason.trycloudflare.com`. Kollegen brauchen also nach jedem Neustart den neuen Link. +- **Adresse:** https://hivegrid-marketing.pages.dev +- **Passwort:** `attiJia1GXxGeIKg`, der Name ist egal. Gleiches Passwort für Pages und Tunnel. +- Maßgeblich ist die Datei `.freigabe-passwort` (nicht in git). Bei Pages liegt es zusätzlich als Secret `PASSWORT` im + Projekt. Wer es ändert, muss beides ändern und neu hochladen (siehe unten). --- -## Für Agents: Freigabe starten +## Weg 1: Cloudflare Pages (Standard) -Wenn Dejan sagt „starte die Freigabe“, „Session für die Kollegen“ oder Ähnliches: +Das Projekt `hivegrid-marketing` liegt im Cloudflare-Konto `dejancosic2003@gmail.com`. Es zeigt den Stand vom letzten +Hochladen, nicht live. -1. **Läuft sie schon?** `curl -s 127.0.0.1:20241/quicktunnel`. Kommt ein `hostname` zurück, läuft sie. Dann nur Adresse und Passwort nennen, nichts neu starten. Ist Port 4330 belegt (`lsof -iTCP:4330 -sTCP:LISTEN`), aber kein Tunnel da, zuerst beenden (siehe unten). -2. **Starten:** `./freigabe.sh` im Hub-Ordner im Hintergrund laufen lassen. Achtung, der Ordnername endet mit einem Leerzeichen: `"/Users/dejancosic/Documents/GitHub/HiveGrid Marketing "`. -3. **Adresse holen:** ein paar Sekunden warten, dann `curl -s 127.0.0.1:20241/quicktunnel`. In der Ausgabe von `freigabe.sh` steht vorher schon „Requesting new quick Tunnel on trycloudflare.com...“. Das ist noch nicht die Adresse. -4. **Prüfen,** nach weiteren paar Sekunden, bis der neue Name auflöst: - - `curl -s -o /dev/null -w '%{http_code}' https:///` muss `401` sein - - `curl -s -o /dev/null -w '%{http_code}' -u x:$(cat .freigabe-passwort) https:///` muss `200` sein - - `.../.git/config` mit Passwort muss `404` sein -5. **Dejan Adresse und Passwort nennen.** +### Für Agents: neuen Stand hochladen -**Wichtig:** Eine Freigabe, die ein Agent startet, endet mit dessen Sitzung. Soll sie länger laufen, startet Dejan selbst in einem Terminal: +**Immer nach „commit und push“ im Hub** auch hochladen, damit die Kollegen den neuen Stand sehen: ``` -cd "/Users/dejancosic/Documents/GitHub/HiveGrid Marketing " && ./freigabe.sh +cd "/Users/dejancosic/Documents/GitHub/HiveGrid Marketing " && ./veroeffentlichen.sh ``` -**Beenden:** die Hintergrund-Aufgabe stoppen oder Ctrl+C im Terminal. Wenn etwas hängen bleibt: +- Hochgeladen wird, was git kennt, im Stand der Arbeitskopie. **Neue Dateien erst committen**, sonst fehlen sie. +- Am Ende steht „Deployment complete!“. Die neue Version braucht danach bis zu einer Minute, bis sie überall ankommt. + In dieser Zeit kann die Adresse noch die alte Version zeigen. +- **Prüfen:** + - `curl -s -o /dev/null -w '%{http_code}' https://hivegrid-marketing.pages.dev/` muss `401` sein + - mit `-u x:$(cat .freigabe-passwort)` muss es `200` sein +- **Nicht angemeldet?** Dann meldet `wrangler` einen Fehler zur Anmeldung. `npx --yes wrangler@4 login` öffnet ein + Browserfenster, dort muss Dejan auf „Allow“ klicken. +- `--force` im Skript ist nötig: Neuere wrangler-Versionen wollen Pages sonst als Worker anlegen und scheitern am + Ordnernamen mit Leerzeichen. -``` -pkill -f "cloudflared tunnel --no-autoupdate --url http://127.0.0.1:4330"; pkill -f freigabe.py -``` +### Was hochgeladen wird + +- Alle Dateien, die git kennt, außer: alles mit Punkt am Anfang (`.git`, `.docs`, `.claude`), die Server-Skripte oben im + Hub (`serve.py`, `freigabe.*`, `veroeffentlichen.sh`) und Dateien über 25 MB. Derzeit fehlt deshalb nur + `produktionsvorgaben/gestaltung/outro-e120/E120-Alpha.mov`. +- Dazu in `pages-daten/` die Dokumentenliste, der Stand und der Suchindex. Die erzeugt `werkzeuge/pages_bauen.py`. +- `werkzeuge/pages/_middleware.js` läuft bei Cloudflare vor jeder Anfrage: Passwort, nur lesen, Suche, „/“ zeigt das + Dashboard. --- -## Was gesperrt ist +## Weg 2: Tunnel vom Laptop (Notlösung) + +Zeigt den Stand live von Dejans Laptop. Endet, sobald der Laptop schläft, die Sitzung oder das Terminal zu ist. + +1. **Läuft er schon?** `curl -s 127.0.0.1:20241/quicktunnel`. Kommt ein `hostname`, läuft er. Dann nur Adresse und + Passwort nennen. +2. **Starten:** `./freigabe.sh` im Hub-Ordner im Hintergrund. Der Ordnername endet mit einem Leerzeichen. +3. **Adresse holen:** ein paar Sekunden warten, dann `curl -s 127.0.0.1:20241/quicktunnel`. Die Adresse ist bei jedem + Start neu. +4. **Prüfen:** ohne Passwort `401`, mit Passwort `200`, `.../.git/config` mit Passwort `404`. +5. **Beenden:** Hintergrund-Aufgabe stoppen oder Ctrl+C. Wenn etwas hängt: + `pkill -f "cloudflared tunnel --no-autoupdate --url http://127.0.0.1:4330"; pkill -f freigabe.py` + +`freigabe.py` ist derselbe Server wie `serve.py`, auf Port 4330, nur 127.0.0.1, mit Passwort und nur zum Lesen. +`freigabe.sh` startet dazu `caffeinate` und `cloudflared tunnel --url http://127.0.0.1:4330`. + +--- + +## Was gesperrt ist (beide Wege) - **Ohne Passwort** alles (401). -- **Alles mit Punkt am Anfang** (404): `.git`, `.docs`, `.claude`, `.freigabe-passwort`. Interne Dateien, die Kollegen nicht sehen sollen, gehören deshalb in einen solchen Ordner, zum Beispiel hierher. -- **Ordnerlisten** (404). +- **Alles mit Punkt am Anfang** (404). Interne Dateien, die Kollegen nicht sehen sollen, gehören deshalb in einen solchen + Ordner, zum Beispiel hierher. - **Status setzen, Export, Öffnen** (403). Das Dashboard zeigt dann unten „In der Freigabe lässt sich nichts ändern.“ -Alles andere im Hub ist für Kollegen sichtbar, auch Dokumente und Daten, die nicht in git stehen. +Alles andere im Hub ist für Kollegen sichtbar. Beim Tunnel auch Dateien, die nicht in git stehen. ## Was bei Kollegen nicht geht - PostGrid, das läuft nur auf Dejans Mac. - Das Online-Deck und andere claude.ai-Links, die sind privat. - ---- - -## Technik - -- `freigabe.py`: derselbe Server wie `serve.py`, aber auf Port 4330, nur 127.0.0.1, mit Passwort und nur zum Lesen. -- `freigabe.sh`: startet `freigabe.py`, dazu `caffeinate` (der Mac schläft nicht ein, mit zugeklapptem Deckel trotzdem) und `cloudflared tunnel --url http://127.0.0.1:4330`. -- `cloudflared` ist über Homebrew installiert (19.10.2026, Version 2026.10.1). Ein Quick Tunnel braucht kein Konto, hat aber keine Garantie auf Erreichbarkeit. -- `127.0.0.1:20241` ist die Statusseite von `cloudflared`. Ist der Port belegt, nimmt es den nächsten bis 20245. diff --git a/.gitignore b/.gitignore index efb0749..84b87c8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,13 @@ webinar/Webinar-Entwurf.zip # Passwort der Freigabe für Kollegen (freigabe.py) .freigabe-passwort + +# Gebauter Stand für Cloudflare Pages (veroeffentlichen.sh) +.pages/ + +# wrangler files +.wrangler +.dev.vars* +!.dev.vars.example +.env* +!.env.example diff --git a/veroeffentlichen.sh b/veroeffentlichen.sh new file mode 100755 index 0000000..ea689ba --- /dev/null +++ b/veroeffentlichen.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Lädt das Dashboard zu Cloudflare Pages hoch, für die Kollegen: mit Passwort, +# nur lesen, ohne dass Dejans Laptop laufen muss. Hochgeladen wird, was git +# kennt, im Stand der Arbeitskopie. Neue Dateien also vorher committen. +# Anleitung und Adresse in .docs/Freigabe.md. +cd "$(dirname "$0")" || exit 1 +python3 werkzeuge/pages_bauen.py || exit 1 +cd .pages && exec npx --yes wrangler@4 pages deploy public --project-name hivegrid-marketing --branch main --commit-dirty=true --force diff --git a/werkzeuge/pages/_middleware.js b/werkzeuge/pages/_middleware.js new file mode 100644 index 0000000..51c6bc1 --- /dev/null +++ b/werkzeuge/pages/_middleware.js @@ -0,0 +1,93 @@ +// Cloudflare Pages: das Dashboard für Kollegen, ohne Dejans Laptop. +// +// Läuft vor jeder Anfrage und macht, was serve.py und freigabe.py lokal tun: +// - jede Anfrage braucht das Passwort (Pages-Secret PASSWORT), der Name ist egal +// - nur lesen: Status setzen, Export und Öffnen antworten mit 403 +// - "/" zeigt das Dashboard +// - /api/dokumente, /api/stand und /api/suche kommen aus festen Dateien in +// pages-daten/, die werkzeuge/pages_bauen.py beim Hochladen erzeugt +// +// Hochgeladen wird mit ./veroeffentlichen.sh, Anleitung in .docs/Freigabe.md. + +const NUR_LESEN = { fehler: "In der Freigabe lässt sich nichts ändern." }; + +export async function onRequest({ request, env, next }) { + if (!erlaubt(request, env.PASSWORT)) { + return new Response("Passwort nötig", { + status: 401, + headers: { "WWW-Authenticate": 'Basic realm="HiveGrid Marketing", charset="UTF-8"' }, + }); + } + const url = new URL(request.url); + if (request.method !== "GET" && request.method !== "HEAD") return json(403, NUR_LESEN); + + if (url.pathname === "/" || url.pathname === "/index.html") return asset(env, url, "/Dashboard/"); + if (url.pathname === "/api/dokumente") return asset(env, url, "/pages-daten/dokumente.json"); + if (url.pathname === "/api/stand") return asset(env, url, "/pages-daten/stand.json"); + if (url.pathname === "/api/suche") return suche(env, url); + + const antwort = await next(); + // Markdown als Text mit Umlauten, so wie serve.py es ausliefert. + if (url.pathname.endsWith(".md") && antwort.ok) { + const neu = new Response(antwort.body, antwort); + neu.headers.set("Content-Type", "text/markdown; charset=utf-8"); + return neu; + } + return antwort; +} + +function erlaubt(request, passwort) { + if (!passwort) return false; + const kopf = request.headers.get("Authorization") || ""; + if (!kopf.startsWith("Basic ")) return false; + let pw = ""; + try { + const roh = Uint8Array.from(atob(kopf.slice(6)), (z) => z.charCodeAt(0)); + const text = new TextDecoder().decode(roh); + pw = text.slice(text.indexOf(":") + 1); + } catch { + return false; + } + // Immer alle Zeichen vergleichen, damit die Antwortzeit nichts verrät. + let unterschied = pw.length ^ passwort.length; + for (let i = 0; i < passwort.length; i++) unterschied |= (pw.charCodeAt(i) || 0) ^ passwort.charCodeAt(i); + return unterschied === 0; +} + +async function asset(env, url, pfad) { + const antwort = await env.ASSETS.fetch(new URL(pfad, url)); + const neu = new Response(antwort.body, antwort); + neu.headers.set("Cache-Control", "no-store"); + return neu; +} + +// Wie _suche in serve.py: Zeile für Zeile, ohne Groß- und Kleinschreibung, +// mit der letzten Überschrift davor, höchstens 300 Treffer. +async function suche(env, url) { + const q = (url.searchParams.get("q") || "").trim(); + if (q.length < 2) return json(200, []); + const index = await (await env.ASSETS.fetch(new URL("/pages-daten/suche.json", url))).json(); + const gesucht = q.toLowerCase(); + const treffer = []; + for (const dok of index) { + let ueberschrift = ""; + for (let i = 0; i < dok.zeilen.length; i++) { + const zeile = dok.zeilen[i]; + if (zeile.startsWith("#")) ueberschrift = zeile.replace(/^#+/, "").trim(); + const pos = zeile.toLowerCase().indexOf(gesucht); + if (pos < 0) continue; + const a = Math.max(0, pos - 70); + const text = (a ? "…" : "") + zeile.slice(a, pos + q.length + 90).trim(); + treffer.push({ pfad: dok.pfad, titel: dok.titel, zeile: i + 1, ueberschrift, text }); + if (treffer.length >= 300) return json(200, treffer); + } + } + return json(200, treffer); +} + +function json(status, daten) { + return new Response(JSON.stringify(daten), { + status, + headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }, + }); +} diff --git a/werkzeuge/pages_bauen.py b/werkzeuge/pages_bauen.py new file mode 100644 index 0000000..fa2012d --- /dev/null +++ b/werkzeuge/pages_bauen.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Baut den Ordner .pages/ für Cloudflare Pages. + + .pages/public/ alle Dateien, die git kennt, im Stand der Arbeitskopie + .pages/functions/ die Middleware aus werkzeuge/pages/ (Passwort, nur lesen, API) + +Weggelassen wird, was mit einem Punkt beginnt (.git, .docs, .claude), die +Server-Skripte oben im Hub und alles über 25 MB, mehr nimmt Pages nicht an. + +Dazu kommen in public/pages-daten/ die Auskünfte, die lokal serve.py +liefert: Dokumentenliste, Stand und der Index für die Suche. Gleiche Auswahl +der Dokumente wie in serve.py, aber nur die, die auch hochgeladen werden. + +Aufgerufen von ./veroeffentlichen.sh. +""" + +import json +import shutil +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +import serve # noqa: E402 + +HUB = serve.HUB +ZIEL = HUB / ".pages" +GRENZE = 25 * 1024 * 1024 +SERVER = {"serve.py", "serve.sh", "freigabe.py", "freigabe.sh", "veroeffentlichen.sh"} + +NICHT_GEFUNDEN = """ +Nicht gefunden + +

Nicht gefunden

Zum Dashboard

+""" + + +def bekannte_dateien(): + roh = subprocess.run(["git", "ls-files", "-z"], cwd=HUB, capture_output=True, check=True).stdout + for rel in filter(None, roh.decode("utf-8").split("\0")): + rel = serve.nfc(rel) + teile = rel.split("/") + if any(t.startswith(".") for t in teile) or "node_modules" in teile or rel in SERVER: + continue + pfad = HUB / rel + if not pfad.is_file(): + continue # in der Arbeitskopie gelöscht + if pfad.stat().st_size > GRENZE: + print("Zu groß für Pages, fehlt dort: %s" % rel) + continue + yield rel, pfad + + +def main(): + if ZIEL.exists(): + shutil.rmtree(ZIEL) + oeffentlich = ZIEL / "public" + hochgeladen = set() + for rel, pfad in bekannte_dateien(): + ziel = oeffentlich / rel + ziel.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(pfad, ziel) + hochgeladen.add(rel) + + dokumente, index = [], [] + for p in serve.markdown_dateien(): + rel = serve.nfc(p.relative_to(HUB).as_posix()) + if rel not in hochgeladen: + continue + titel = serve.titel_von(p) + dokumente.append({"pfad": rel, "titel": titel, + "ordner": str(Path(rel).parent) if "/" in rel else "", + "zeit": serve.mtime(p)}) + index.append({"pfad": rel, "titel": titel, "zeilen": p.read_text(encoding="utf-8").split("\n")}) + daten = max([serve.mtime(p) for p in serve.DATEN.glob("*.json")] or [0.0]) + neueste = max([d["zeit"] for d in dokumente] or [0.0]) + stand = {"daten": daten, "dokumente": neueste, "aenderung": max(daten, neueste)} + + auskunft = oeffentlich / "pages-daten" + auskunft.mkdir(parents=True) + for name, inhalt in (("dokumente", dokumente), ("stand", stand), ("suche", index)): + (auskunft / (name + ".json")).write_text(json.dumps(inhalt, ensure_ascii=False), encoding="utf-8") + # Ohne 404.html hält Pages die Seite für eine Single-Page-App und liefert + # für fehlende Dateien die Startseite statt eines Fehlers. + (oeffentlich / "404.html").write_text(NICHT_GEFUNDEN, encoding="utf-8") + + funktionen = ZIEL / "functions" + funktionen.mkdir(parents=True) + shutil.copy2(HUB / "werkzeuge" / "pages" / "_middleware.js", funktionen / "_middleware.js") + print("Gebaut: %d Dateien, %d Dokumente in %s" % (len(hochgeladen), len(dokumente), ZIEL)) + + +if __name__ == "__main__": + main()