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 <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5.5 committed 2026-10-20 14:31:57 +02:00
1 parent 64da152d53
commit e112597568
6 files changed
+265 -35

No files matched your search

+5 -2
View File
@@ -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.
+55 -33
View File
@@ -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://<adresse>/` muss `401` sein
- `curl -s -o /dev/null -w '%{http_code}' -u x:$(cat .freigabe-passwort) https://<adresse>/` 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.
+10
View File
@@ -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
+8
View File
@@ -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
+93
View File
@@ -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" },
});
}
+94
View File
@@ -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 = """<!doctype html>
<html lang="de"><meta charset="utf-8"><title>Nicht gefunden</title>
<body style="font-family: system-ui, sans-serif; padding: 40px">
<h1>Nicht gefunden</h1><p><a href="/">Zum Dashboard</a></p></body></html>
"""
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()