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:
1 parent
64da152d53
commit
e112597568
6 files changed
+265
-35
No files matched your search
+5
-2
@@ -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
@@ -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
@@ -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
|
||||
Executable
+8
@@ -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
|
||||
@@ -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" },
|
||||
});
|
||||
}
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user