Ihre Stile hingen als <style> im Shadow DOM, und Markup faellt unter die CSP der Gastgeber-App: Jede fremde App musste deshalb style-src 'unsafe-inline' erlauben, sonst stand die HoneyBar schwarz auf schwarz da. Jetzt kommen sie als konstruiertes Stylesheet ueber die CSSOM herein, die keine CSP kennt. Damit braucht die App gar keine style-src-Erlaubnis mehr -- auch keine fuer die HoneyOS-Domain, was der geplante Weg ueber eine zweite Datei und <link> gekostet haette, samt zweiter Anfrage. Der Strich zum Wiederausklappen hat seinen eigenen Shadow-Root und geht denselben Weg. Der Rueckfall fuer alte Browser laeuft nach innerHTML, sonst raeumt das Markup ihn gleich wieder weg. Gemessen an einer nachgebauten App mit style-src 'self': Leiste bernstein, 32 px, Strich mit Verlauf, keine CSP-Meldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
775 lines
36 KiB
Markdown
775 lines
36 KiB
Markdown
# HoneyOS
|
||
|
||
Ein Retro-Schreibtisch vor den eigenen Apps. Amber-CRT, Boot-Log, zwei Betriebsarten,
|
||
Anmeldung über den eigenen OIDC-Anbieter — und dieselbe HoneyBar zum Einhängen in die Apps.
|
||
|
||
**Was es ist:** eine Oberfläche davor. Jede App bleibt ihre eigene PWA auf ihrer eigenen
|
||
Domain mit ihrer eigenen Anmeldung. HoneyOS ist der Startpunkt, der Umschalter und die
|
||
gemeinsame HoneyBar — **kein Rahmen, der die Apps einsperrt** (kein iframe).
|
||
|
||
**Wo der Zustand liegt:** im Server. Die App-Liste gilt für alle, die Einstellungen gehören
|
||
je Nutzer — deshalb sieht dein HoneyOS auf jedem Gerät gleich aus, an dem du angemeldet bist.
|
||
Der Browser hält dieselben Einstellungen nur als Vorschuss, damit die Röhre beim Laden nicht
|
||
in der falschen Farbe aufflackert.
|
||
|
||
## Betrieb
|
||
|
||
```bash
|
||
mkdir -p /srv/honeyos && chown 1000:1000 /srv/honeyos
|
||
cp .env.example .env # PUBLIC_URL, SESSION_SECRET, OIDC-Zugang
|
||
docker login gitea.honeyy.duckdns.org
|
||
docker compose pull && docker compose up -d
|
||
```
|
||
|
||
Zu sichern ist genau eine Datei: `${HONEYOS_DATA_DIR}/honeyos.json`. Darin stehen die Apps
|
||
und die Einstellungen je Nutzer. Sichere sie zusammen mit der `.env` — ohne
|
||
`HONEYOS_SESSION_SECRET` müssen sich alle neu anmelden, mehr passiert aber nicht.
|
||
|
||
`/healthz` antwortet mit `ok`. Hinter deinem Reverse Proxy braucht es nichts Besonderes,
|
||
außer dass `X-Forwarded-Proto: https` durchkommt — sonst setzt HoneyOS das Sitzungscookie
|
||
ohne `Secure`.
|
||
|
||
## Anmeldung — zwei Wege, einer davon reicht
|
||
|
||
**A · Ein Proxy davor macht die Tür** (Pangolin, Authelia, …). Dann in der `.env`:
|
||
|
||
```dotenv
|
||
HONEYOS_PROXY_AUTH=true
|
||
# HONEYOS_USER_HEADER=Remote-User # optional, trennt die Einstellungen je Nutzer
|
||
```
|
||
|
||
HoneyOS fragt dann nicht noch einmal — kein zweiter Anmeldeschirm. **Der
|
||
Container darf in diesem Betrieb nur über den Proxy erreichbar sein**, denn er
|
||
prüft selbst nichts mehr.
|
||
|
||
**B · HoneyOS meldet selbst an**, mit eigenem Anmeldeschirm über deinen
|
||
OIDC-Anbieter. In PocketID einen Client anlegen und als Rückrufadresse **exakt**
|
||
`<PUBLIC_URL>/api/auth/callback` eintragen. HoneyOS schickt PKCE mit, braucht
|
||
aber trotzdem ein Client-Geheimnis — also **keinen „public client"**.
|
||
|
||
```dotenv
|
||
HONEYOS_SESSION_SECRET=… # openssl rand -hex 32
|
||
HONEYOS_OIDC_ISSUER=https://id.example.com
|
||
HONEYOS_OIDC_CLIENT_ID=honeyos
|
||
HONEYOS_OIDC_CLIENT_SECRET=…
|
||
HONEYOS_BUTTON_LABEL=Anmelden mit PocketID
|
||
```
|
||
|
||
Wer nicht angemeldet ist, sieht nur den Anmeldeschirm — keine App-Liste, keine
|
||
Namen. Die Sitzung ist ein signiertes Cookie ohne Gegenstück im Server: ein
|
||
Neustart wirft niemanden hinaus. Sie hält 30 Tage.
|
||
|
||
**Weder A noch B** heißt: offen für jeden. Sagt beim Start deutlich, dass das
|
||
nur zum Entwickeln taugt.
|
||
|
||
## Wenn HoneyOS auf einer anderen Site liegt als die Apps
|
||
|
||
Der Browser behandelt `honeyos.duckdns.org` und `cloud.honeyy.duckdns.org` als
|
||
**fremde Sites** — `duckdns.org` steht auf der Public-Suffix-Liste, die
|
||
gemeinsame Endung zählt also nicht. Folge: Bei der Anfrage aus einer App heraus
|
||
schickt der Browser die HoneyOS-Sitzung **nicht** mit.
|
||
|
||
Damit die HoneyBar trotzdem eine App-Liste bekommt:
|
||
|
||
```dotenv
|
||
HONEYOS_APPS_KEY=… # openssl rand -hex 16
|
||
```
|
||
|
||
und derselbe Wert im Skript-Tag der App:
|
||
|
||
```html
|
||
<script defer src="https://honeyos.duckdns.org/honeybar/honeybar.js"
|
||
data-app="aether" data-key="…"></script>
|
||
```
|
||
|
||
Der Schlüssel steht damit in der App, die ohnehin nur Berechtigte sehen. Ohne
|
||
ihn antwortet `/api/apps` mit 401 — auch im Proxy-Betrieb, gerade weil dieser
|
||
Pfad im Proxy von der Anmeldung befreit sein muss.
|
||
|
||
Zweite Schranke, unabhängig davon: HoneyOS gibt die Liste nur an Ursprünge
|
||
heraus, die **selbst als App eingetragen** sind. Eine fremde Seite scheitert
|
||
also an CORS, selbst mit richtigem Schlüssel.
|
||
|
||
**Erst die Sitzung, dann der Schlüssel.** Bringt die Anfrage ein gültiges
|
||
HoneyOS-Cookie mit, antwortet der Server mit den Einstellungen **dieses**
|
||
Nutzers; der Schlüssel wird dann gar nicht erst angesehen. Er ist die Krücke
|
||
für den Fall, dass das Cookie nicht ankommen kann — und darf eine echte
|
||
Anmeldung nicht überstimmen, denn er kennt keinen Nutzer und landet beim
|
||
Standardnutzer.
|
||
|
||
Ob das Cookie ankommt, entscheidet die **Site**, nicht die Adresse: Weil
|
||
`duckdns.org` auf der Public-Suffix-Liste steht, sind `honeyos.duckdns.org` und
|
||
`cloud.honeyos.duckdns.org` **dieselbe** Site (Cookie fließt),
|
||
`honeyy.duckdns.org` dagegen eine fremde (Cookie fließt nicht).
|
||
|
||
| | Cookie | Folge |
|
||
|---|---|---|
|
||
| App unter derselben Site | kommt an | eigene Röhre, eigenes Wetter, `data-key` überflüssig |
|
||
| App auf fremder Site | bleibt weg | Schlüssel nötig, HoneyBar trägt die Farben des Standardnutzers |
|
||
|
||
Zweiteres gilt auch für den Anmeldestand auf den Kacheln des Schreibtischs: Der
|
||
fragt über die Domaingrenze und bleibt dort leer.
|
||
|
||
## Apps anlegen
|
||
|
||
**In der Oberfläche**, nicht in der `.env`: *Einstellungen → Programme* oder die Kachel
|
||
„+ app hinzufügen". Pro App:
|
||
|
||
| Feld | wofür |
|
||
|---|---|
|
||
| ID | klein, ohne Leerzeichen — muss zu `data-app` der HoneyBar passen |
|
||
| Name, Untertitel | Beschriftung auf Kachel und Symbol |
|
||
| Adresse | wohin der Start springt |
|
||
| Symbol | cloud · note · box · chip · folder · gear · globe · disk |
|
||
|
||
**Die Reihenfolge machst du mit den Pfeilen** ▲▼ in der Kopfzeile jeder Zeile — sie gilt
|
||
überall gleich: Streifen, Schublade, Kacheln, Schreibtischsymbole, Startmenü. Neue Apps
|
||
hängen sich hinten an.
|
||
|
||
Gespeichert wird die Liste als Ganzes; der Server prüft sie noch einmal und antwortet mit
|
||
dem, was er wirklich behalten hat. Eine Adresse ohne `https://` kommt als Fehlermeldung
|
||
zurück, nicht als kaputter Eintrag.
|
||
|
||
## Die Schublade
|
||
|
||
Der Pfeil rechts in beiden Leisten klappt alle Apps als Raster auf: **Symbol,
|
||
Name, Untertitel.** Drei Spalten, am Handy dieselben drei.
|
||
|
||
Warum nicht nur Symbole? Weil acht geteilte Pixelglyphen nicht identifizieren —
|
||
zwei Apps können dieselbe Wolke tragen, und bei Homelab-Diensten ist das der
|
||
Normalfall. Dazu der Unterschied der beiden Orte: Den Streifen oben sieht man
|
||
im Vorbeigehen, da reicht ein Blick und die Symbole sind abschaltbar. Die
|
||
Schublade macht man auf, **weil man sucht** — Suchen heißt Lesen.
|
||
|
||
Der Untertitel aus der App-Liste steht damit endlich dort, wo er nützt: Er
|
||
beantwortet die Frage, die ein generisches Symbol offenlässt. Ist er leer,
|
||
fällt die Zeile weg.
|
||
|
||
## Ein Satz Symbole für alles
|
||
|
||
Die Pixelraster stehen **an genau einer Stelle**: `web/glyphs.json`, 12×12
|
||
Zeichen je Symbol, ein `#` ist ein Pixel. Der Schreibtisch holt die Datei
|
||
selbst; die HoneyBar in fremden Apps bekommt die Raster **mit der App-Liste
|
||
mitgeliefert** — sie soll eine einzelne Datei ohne zweite Anfrage bleiben, und
|
||
`/api/apps` fragt sie ohnehin. Wer dort ein Symbol ergänzt, kann es sofort im
|
||
Programm-Editor auswählen; abgetippt wird nichts mehr.
|
||
|
||
Dass das nötig war, hatte einen Anlass: Die Wettersymbole standen einmal in
|
||
`os.js` **und** in `honeybar.js`. Sie liefen auseinander, und niemand merkte es,
|
||
bis das Wetter in der einen Leiste anders aussah als in der anderen.
|
||
|
||
**Wo die Symbole stehen:**
|
||
|
||
| | Symbol |
|
||
|---|---|
|
||
| App-Streifen in beiden Leisten | nach Schalter *Einstellungen → HoneyBar* |
|
||
| Aufklappmenü beider Leisten | immer |
|
||
| Kacheln, Schreibtischsymbole, Startmenü | immer |
|
||
|
||
Der Schalter gilt für **beide** Leisten zugleich — es ist dieselbe Leiste, nur
|
||
an zwei Orten. Er liegt wie alles andere im Server, wirkt also auf jedem Gerät
|
||
und in jeder App, ohne dass eine App neu gebaut wird.
|
||
|
||
## Zwei Betriebsarten, ein Datenstand
|
||
|
||
| | Terminal-Modus | Desktop-Modus |
|
||
|---|---|---|
|
||
| Apps | Kacheln nebeneinander | Symbole auf der Fläche, Doppelklick öffnet |
|
||
| Fenster | Dialog in der Mitte | verschiebbar, minimierbar |
|
||
| unten | Statuszeile mit Tastenhinweisen | Taskleiste mit Startmenü |
|
||
|
||
Umschalten mit **`M`**, über *Einstellungen → Betriebsart* oder im Startmenü. Gleich bleiben
|
||
in beiden: Menüleiste, Palette, Einstellungen, App-Liste.
|
||
|
||
**Tasten:** `M` Betriebsart · `S` Einstellungen · `?` Hilfe ·
|
||
`Esc` schließt Startmenü, oberstes Fenster oder Dialog — in dieser Reihenfolge.
|
||
|
||
## Das Wappen
|
||
|
||
Links in beiden Leisten sitzt eine Pixelbiene — seitlich, mit braunen Streifen,
|
||
schwarzen Beinen und Flügeln mit Kerbe. Sie ersetzt das `■`, das dort seit dem
|
||
ersten Tag stand.
|
||
|
||
**Sie steht an genau einer Stelle:** `tools/biene.py`, als 16 × 16 Zeichen, ein
|
||
Zeichen je Pixel. Daraus entstehen alle Fassungen:
|
||
|
||
```
|
||
python3 tools/biene.py
|
||
```
|
||
|
||
| Ausgabe | wohin |
|
||
|---|---|
|
||
| `web/img/biene.svg` | Favicon und Anmeldebildschirm |
|
||
| `web/js/biene.js` | Menüleiste des Schreibtischs (`BIENE_SVG`), System-Fenster und Startlauf (`BIENE_ASCII`) |
|
||
| `web/img/biene-256.png`, `-32.png` | Doku und Vorschau |
|
||
|
||
In die HoneyBar ist dieselbe SVG-Zeile **einkopiert** — sie ist eine einzelne
|
||
Datei ohne zweite Anfrage. Ein Test (`TestWappenIstUeberallDasselbe`) hält die
|
||
drei Kopien zusammen; wer das Raster ändert, lässt das Skript laufen und
|
||
kopiert die Zeile neu.
|
||
|
||
**Warum SVG und nicht PNG?** Die HoneyBar läuft in *fremden* Apps. Ein `<img>`
|
||
holte dort eine Datei von HoneyOS, und ob das erlaubt ist, entscheidet die CSP
|
||
der fremden App (`img-src`). Markup kann niemand blockieren, es braucht keine
|
||
zweite Anfrage, und es ist mit 2,6 kB kleiner als das PNG.
|
||
|
||
**Warum bleibt sie gelb?** In der grünen und der eisblauen Röhre wechselt alles
|
||
die Farbe mit — die Biene nicht. Ein Zeichen, das die Farbe wechselt, ist ein
|
||
Symbol wie *wolke*; eines, das seine Farbe behält, ist ein Wappen. Der eine
|
||
warme Fleck liest sich als „das hier ist das System selbst".
|
||
|
||
**Überall dasselbe Zeichen:** Leiste, Tab und Anmeldebildschirm zeigen dieselbe
|
||
Datei, nur in anderer Größe. Eine 3/4-Ansicht (ein gerechneter Würfel in
|
||
2:1-Schrägsicht) steckt als `schraeg()` in `tools/biene.py`, wird aber nirgends
|
||
ausgeliefert — sie ist unter 64 px unbrauchbar, und ein zweites Wappen für
|
||
große Flächen wäre ein zweites Wappen.
|
||
|
||
### Und aus Schriftzeichen
|
||
|
||
Im System-Fenster und über dem Startlauf steht dieselbe Biene aus `#`, `:` und
|
||
`-`. Beides sind Textflächen — ein Bild darin wäre ein Fremdkörper, wie ein
|
||
Foto in einer Tabelle. So wird aus dem System-Fenster ein **neofetch**.
|
||
|
||
Zwei Dinge stecken in der Umsetzung, die man nur durch Ausprobieren findet: Ein
|
||
Zeichen ist etwa halb so breit wie hoch, ein Pixel muss also **zwei Zeichen**
|
||
breit sein. Und Blockzeichen (`█ ▓ ▒`) sehen im Editor perfekt aus, hinterlassen
|
||
im Browser aber feine Fugen zwischen den Zellen — der Körper wird zur Waffel.
|
||
Gewöhnliche Schriftzeichen behaupten gar nicht erst, eine Fläche zu sein.
|
||
|
||
Dass ausgerechnet die **Seitenansicht** als Text funktioniert, ist kein Zufall:
|
||
Ihre Streifen laufen senkrecht, also bleibt jede Textzeile ein
|
||
zusammenhängendes Stück Körper. Von vorn zerfällt die Biene in Streifen und
|
||
liest sich wie ein Strichcode.
|
||
|
||
## Widgets
|
||
|
||
Ein Widget ist eine **Beschreibung**, kein Kasten in der `index.html`:
|
||
|
||
```js
|
||
widgetAnmelden({
|
||
id: "wetter", titel: "wetter",
|
||
wo: ["terminal", "desktop"], // wo es erscheinen darf
|
||
sichtbar: () => true, // eigene Bedingung, optional
|
||
laden: async () => …, // false = es gibt nichts zu zeigen
|
||
leer: "verbergen", // oder ein Satz, der dann dasteht
|
||
inhalt: () => "<html>",
|
||
takt: 10 * 60 * 1000, // Nachladen, optional
|
||
})
|
||
```
|
||
|
||
Den Rest macht `web/js/widgets/rahmen.js`: Im Terminal-Modus wird daraus ein
|
||
Fenster in der linken Spalte, im Desktop-Modus ein Schild oben rechts. Er kennt
|
||
vier Zustände — **lädt · fertig · nichts da · Fehler** — und hält den
|
||
Nachladetakt.
|
||
|
||
**Ein Widget, das seinen Server nicht erreicht, sagt das in seinem eigenen
|
||
Rahmen.** Es verschwindet nicht (das sähe aus wie abgeschaltet) und reißt weder
|
||
den Schreibtisch noch die anderen Widgets mit. Wo „nichts da" schlicht heißt,
|
||
dass es nichts zu zeigen gibt, kann ein Widget mit `leer: "verbergen"` ganz
|
||
wegbleiben — so macht es das Wetter ohne eingestellten Ort.
|
||
|
||
Eine neue Anzeige ist damit eine Datei in `web/js/widgets/` und ein Import in
|
||
`os.js`. Weder das Raster noch die Seite werden dafür angefasst.
|
||
|
||
### Reihenfolge und An/Aus liegen beim Nutzer
|
||
|
||
Unter *Einstellungen → Widgets* steht jedes Widget mit einem Schalter und zwei
|
||
Pfeilen. Beides zusammen steht in **einer** Einstellung, und die liegt wie alle
|
||
anderen beim Konto auf dem Server — also auf jedem Gerät gleich:
|
||
|
||
```
|
||
widgets = "benutzer,notiz,-system,wetter,bild"
|
||
```
|
||
|
||
Das Minus heißt aus. Ein Feld statt zwei, weil beides zusammengehört: Wer ein
|
||
Widget ausschaltet, soll seinen Platz behalten, wenn er es wieder einschaltet.
|
||
Die Liste ist ein Wunsch, kein Gesetz — unbekannte Namen fallen weg, neue
|
||
Widgets hängen sich hinten an und sind an. Ein Widget, das erst später
|
||
eingeschaltet wird, holt seine Daten dann nach.
|
||
|
||
Pfeile statt Ziehen: Ziehen wäre hübscher, löst am Handy per Berührung aber
|
||
nicht aus (dieselbe Entscheidung wie bei der Reihenfolge der Apps).
|
||
|
||
## Notiz und Bild
|
||
|
||
Zwei Widgets, die nichts abfragen, sondern etwas *sind*. Beides liegt **je
|
||
Nutzer auf dem Server** — die Notiz als Text in `honeyos.json`, das Bild als
|
||
Datei in `bilder/` daneben (oder als fremde Adresse, nie beides).
|
||
|
||
### Die Notiz zeigt sich sofort gesetzt
|
||
|
||
Der Text ist **immer** formatiert zu sehen; roh ist nur die eine Zeile, in der
|
||
die Schreibmarke steht. Man tippt also in einer Aufzählung und sieht ringsum
|
||
schon Überschriften, Häkchen und Punkte — statt `- [ ] Milch` zu schreiben und
|
||
sich vorzustellen, wie das gleich aussieht.
|
||
|
||
Die Werkzeugleiste (Aufgabe · Aufzählung · Nummer · Überschrift · fett · Linie)
|
||
erscheint **erst beim Schreiben** und wirkt auf genau diese Zeile — ohne offene
|
||
Zeile hätten ihre Knöpfe nichts anzufassen, und eine Notiz, die man nur liest,
|
||
soll wie ein Text aussehen und nicht wie ein Formular. Enter führt eine Liste fort und nummeriert weiter;
|
||
Enter auf einer leeren Aufzählungszeile beendet sie. Backspace am Zeilenanfang
|
||
nimmt erst den Aufzählungspunkt, dann erst die Zeile. Häkchen lassen sich
|
||
anklicken, ohne in den Text zu springen — das ist der häufigste Griff an einer
|
||
Liste. Für größere Umbauten gibt es den Knopf **quelltext**: dann doch das ganze
|
||
Feld, roh.
|
||
|
||
**Warum nicht `contenteditable`?** Weil man aus dem, was der Browser beim Tippen
|
||
und Einfügen an HTML erzeugt, den Quelltext *zurückraten* müsste — und ein
|
||
Fehler dabei kostet Text. Hier bleibt der Quelltext die Wahrheit und wird nie
|
||
zurückgerechnet: Die Anzeige entsteht aus ihm, geändert wird immer nur eine
|
||
Zeile. Der Preis ist, dass man nicht über mehrere Zeilen hinweg markieren kann;
|
||
dafür ist der Quelltext-Knopf da.
|
||
|
||
Das kleine Markdown ist von Hand geschrieben und absichtlich klein:
|
||
Überschriften (`#`, `##`, `###`), Häkchen (`- [ ]`, `- [x]`), Aufzählungen,
|
||
nummerierte Listen, `---`, `**fett**`, `*kursiv*`, `` `code` ``. Keine
|
||
Bibliothek — HoneyOS hat keinen Bauschritt, und für sechs Regeln lohnt kein
|
||
Nachladen. Escapt wird **vor** dem Formatieren: Eine Notiz, die `<script>`
|
||
ausführt, wäre eine Falle für den Tag, an dem man etwas hineinkopiert.
|
||
|
||
**Die Notiz ist keine zweite Notiz-App.** InkNote gibt es schon: kein Titel,
|
||
keine Liste von Notizen, keine Ordner, keine Suche. Genau ein Text für das, was
|
||
nicht länger als einen Tag lebt. Gespeichert wird verzögert (und beim Verlassen
|
||
der Seite sofort); zwei offene Geräte überschreiben einander, der letzte Stand
|
||
gewinnt.
|
||
|
||
### Das Bild
|
||
|
||
Genau eines je Nutzer, hochgeladen oder verlinkt; ein neues ersetzt das alte
|
||
samt Datei. **Eingestellt wird es unter *Einstellungen → Bild*, nicht im Widget**
|
||
— wie beim Wetter, dessen Ort auch nicht in der Wetterkachel gesucht wird. Ein
|
||
Schild auf dem Schreibtisch, das zu drei Vierteln aus Knöpfen besteht, zeigt vor
|
||
allem sich selbst. Erlaubt sind PNG, JPEG, WebP und GIF — der Typ wird aus den
|
||
**Bytes** bestimmt, nicht aus dem, was der Browser behauptet. SVG ist bewusst
|
||
nicht dabei: Es kann Skripte enthalten. Ausgeliefert wird mit einem
|
||
Versionsanteil in der Adresse, sonst bliebe beim Austausch das alte Bild im
|
||
Zwischenspeicher hängen.
|
||
|
||
## Wetter
|
||
|
||
Ein Pixelsymbol mit der Temperatur — wahlweise auf dem Schreibtisch, in der
|
||
HoneyBar, in beidem oder nirgends. Zwei Orte, zwei Schalter an zwei Stellen:
|
||
Auf dem Schreibtisch ist das Wetter ein Widget wie jedes andere und steht in
|
||
der Widget-Liste; für die Leiste gibt es einen eigenen Schalter unter
|
||
*Einstellungen → HoneyBar*. Wer es beim Arbeiten in einer App sehen will, will
|
||
es nicht zwangsläufig auch auf einer Seite, die er selten aufruft.
|
||
|
||
Die Stadt wird unter *Einstellungen → Wetter* einmal gesucht und **mit ihren Koordinaten** gespeichert;
|
||
sonst müsste jede Abfrage den Ort erneut suchen — zwei Anfragen nach draußen
|
||
statt einer. Ohne Stadt bleibt das Wetter aus.
|
||
|
||
**Der Server holt es, nicht der Browser.** Die HoneyBar steht auf jeder Seite
|
||
jeder App: Fragte sie selbst beim Wetterdienst an, ginge bei jedem Seitenaufruf
|
||
aus jeder App eine Anfrage samt IP dorthin, und die CSP jeder App müsste den
|
||
fremden Ursprung erlauben. So sieht der Dienst einen Server. Die Antwort gilt
|
||
zehn Minuten — das Wetter ändert sich langsamer, als Seiten neu geladen werden.
|
||
|
||
Verwendet wird [Open-Meteo](https://open-meteo.com): kein Zugangsschlüssel, kein
|
||
Konto, nichts einzurichten. Ist der Dienst nicht erreichbar oder das Gerät
|
||
offline, verschwindet das Symbol — lieber keins als eine Zahl von gestern.
|
||
|
||
Ändern lässt sich die Stadt nur auf dem Schreibtisch; ein Klick auf das Symbol
|
||
in der HoneyBar führt dorthin. Die HoneyBar liest über den Schlüssel und darf nichts
|
||
schreiben.
|
||
|
||
## Die HoneyBar in deine Apps bringen
|
||
|
||
Es gibt zwei Wege, und welcher gilt, hängt an einer einzigen Frage: **Kannst du
|
||
den Quelltext der App ändern?**
|
||
|
||
| | Weg 1 — selbst geschrieben | Weg 2 — fertig bezogen |
|
||
|---|---|---|
|
||
| Beispiele | Aether, InkNote | BentoPDF, Uptime Kuma, alles aus einem Image |
|
||
| Einbau | die App lädt die HoneyBar selbst | ein nginx davor setzt sie ein |
|
||
| Umstellen | eine Zeile in der `.env` | zwei Zeilen in der Proxy-Tabelle |
|
||
| Layout | passt sich an (`--honeybar-h`) | bleibt, wie die App es macht |
|
||
|
||
Beide Wege ergeben dieselbe HoneyBar; die App-Liste, die Farben und das Wetter
|
||
kommen ohnehin aus HoneyOS.
|
||
|
||
### Weg 1 — Apps, die du selbst schreibst
|
||
|
||
Eine Zeile im HTML der App:
|
||
|
||
```html
|
||
<script defer src="https://honeyos.duckdns.org/honeybar/honeybar.js"
|
||
data-app="aether"></script>
|
||
```
|
||
|
||
`defer` ist nicht optional: ohne wartet der Seitenaufbau der App auf das Skript.
|
||
Ist HoneyOS langsam oder aus, soll die HoneyBar den Start von Aether nicht bremsen.
|
||
|
||
**Aether und InkNote machen es einen Schritt anders — und das ist die bessere
|
||
Art:** Sie laden die HoneyBar zur *Laufzeit*, die Adresse kommt aus ihrer eigenen
|
||
`.env` (`HONEYBAR_ORIGIN`) über einen kleinen Endpunkt der App. Der Grund ist
|
||
Erfahrung: Ein fertiges Abbild wird gezogen, nicht gebaut — was beim Bauen ins
|
||
HTML geschrieben wird, lässt sich hinterher nicht mehr ändern. Und der Service
|
||
Worker einer PWA hebt ganze Antworten auf, samt Kopfzeilen; eine ins HTML
|
||
eingefrorene Adresse überlebt dort jede Konfigurationsänderung.
|
||
|
||
`data-app` ist die ID aus der App-Liste. Die Liste holt sich die HoneyBar selbst von
|
||
`/api/apps` — mit deiner HoneyOS-Sitzung, weil alle Adressen unter derselben Domain liegen.
|
||
**Freigegeben sind nur die Ursprünge, die selbst als App eingetragen sind**: die App-Liste
|
||
ist zugleich ihre eigene CORS-Freigabe.
|
||
|
||
Sie rendert in einem **Shadow DOM**: Tailwind (Aether) und die InkNote-Palette kommen nicht
|
||
hinein, ihre Stile nicht hinaus. Sie schiebt den Seiteninhalt um 32 px nach unten
|
||
(`data-push="none"` legt sie stattdessen darüber). Rechnet die App mit voller Höhe:
|
||
|
||
```css
|
||
.app-shell { height: calc(100vh - var(--honeybar-h, 0px)); }
|
||
```
|
||
|
||
Vorschau ohne Einbau: `/honeybar/demo.html`.
|
||
|
||
### Weg 2 — fertige Apps: ein HoneyBar-Proxy davor
|
||
|
||
Für Werkzeuge, die man **nicht selbst geschrieben hat**, gibt es keinen
|
||
`<script>`-Tag zum Ändern — den setzt ein kleiner nginx ein, während er die
|
||
Seite durchreicht:
|
||
|
||
```
|
||
außen ──► Pangolin ──► honeybar (nginx) ──► app:8080
|
||
TLS, Auth setzt eine Zeile ein, reicht sonst nur durch
|
||
```
|
||
|
||
Ein Reverse Proxy hinter dem Reverse Proxy also, mit klarer Arbeitsteilung. Getestet mit nginx gegen eine App mit gzip und eigener CSP:
|
||
|
||
```nginx
|
||
location / {
|
||
proxy_pass http://127.0.0.1:8181;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
|
||
# 1. Unkomprimiert anfordern — in einem gzip-Strom ist nichts zu ersetzen.
|
||
proxy_set_header Accept-Encoding "";
|
||
|
||
# 2. Die CSP der App ersetzen, nicht ergänzen. Zwei CSP-Kopfzeilen bedeuten
|
||
# nicht „mehr erlaubt", sondern „beide müssen zustimmen".
|
||
proxy_hide_header Content-Security-Policy;
|
||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://honeyos.example.com; connect-src 'self' https://honeyos.example.com; style-src 'self'; img-src 'self' data:" always;
|
||
|
||
# 3. Nur HTML anfassen; JSON, Bilder und Downloads bleiben unberührt.
|
||
sub_filter_types text/html;
|
||
sub_filter_once on;
|
||
sub_filter '</head>' '<script defer src="https://honeyos.example.com/honeybar/honeybar.js" data-app="werkzeug"></script></head>';
|
||
}
|
||
```
|
||
|
||
Dazu muss der Ursprung dieser App **in HoneyOS als App eingetragen** sein — die
|
||
Liste ist zugleich die CORS-Freigabe. **Auf den Ursprung genau**: `localhost` und
|
||
`127.0.0.1` sind für den Browser zwei verschiedene, ebenso `http` und `https`
|
||
oder ein abweichender Port. Passt er nicht, erscheint die HoneyBar mit leerer
|
||
App-Liste und in der Konsole steht ein CORS-Fehler.
|
||
|
||
**Kein `style-src 'unsafe-inline'` nötig** (seit 11.09.2026). Die HoneyBar
|
||
bringt ihre Stile zwar mit, hängt sie aber als konstruiertes Stylesheet
|
||
(`adoptedStyleSheets`) in ihren Shadow DOM statt als `<style>`-Element. Nur
|
||
Markup fällt unter die CSP, über die CSSOM gesetzte Stile nicht — die App
|
||
braucht für die HoneyBar also weder `'unsafe-inline'` noch eine Freigabe für
|
||
die HoneyOS-Domain. Vorher stand die HoneyBar unter einer strengen CSP schwarz
|
||
auf schwarz da. Gemessen an einer nachgebauten App mit `style-src 'self'`:
|
||
Leiste bernstein, 32 px, keine CSP-Meldung.
|
||
|
||
Browser ohne konstruierbare Stylesheets (vor Safari 16.4, Firefox 101) fallen
|
||
auf das `<style>`-Element zurück — dort gilt der alte Stand, also `unsafe-inline`
|
||
oder eine Leiste ohne Farbe.
|
||
|
||
**Apps mit COEP** (alles mit WASM oder SharedArrayBuffer — BentoPDF etwa)
|
||
lehnen fremde Dateien ab, die sich nicht zur Einbettung bekennen. HoneyOS
|
||
liefert die HoneyBar deshalb mit `Cross-Origin-Resource-Policy: cross-origin`
|
||
aus. Ohne das erscheint dort keine HoneyBar, und in der Konsole steht nur
|
||
`ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep`.
|
||
Das COEP der App bleibt dabei unangetastet — ihre eigenen Fähigkeiten also auch.
|
||
|
||
**Zwei Bauarten.** Die zweite ist die bessere, sobald die App eine eigene CSP
|
||
schickt — und das tun die meisten:
|
||
|
||
| | Skript von HoneyOS | Skript über die eigene Adresse |
|
||
|---|---|---|
|
||
| CSP der App | muss ersetzt werden | **unangetastet** (`'self'` genügt) |
|
||
| CORS | Ursprung muss eingetragen sein | entfällt |
|
||
| COEP der App | braucht `Cross-Origin-Resource-Policy` | entfällt |
|
||
| `HONEYOS_APPS_KEY` | steht im Quelltext der Seite | **bleibt auf dem Server** |
|
||
| Aufwand | eine `sub_filter`-Zeile | drei `location`-Blöcke |
|
||
|
||
Bei der zweiten reicht der HoneyBar-Proxy auch `/honeybar/` und die zwei Abfragen der
|
||
HoneyBar durch, unter *seiner* Adresse. Für den Browser ist damit alles derselbe
|
||
Ursprung, und die drei Hürden fallen auf einmal weg. Durchgereicht werden dabei
|
||
**nur** `/api/apps` und `/api/weather`, und nur lesend (`limit_except GET`) —
|
||
ein pauschales `/api/` hinge sonst die ganze HoneyOS-Schnittstelle unter die
|
||
Adresse der App.
|
||
|
||
#### Was du an der Compose der App änderst
|
||
|
||
Vorher — BentoPDF, wie es aus der Anleitung des Herstellers kommt:
|
||
|
||
```yaml
|
||
services:
|
||
bentopdf:
|
||
image: ghcr.io/alam00000/bentopdf-simple:latest
|
||
container_name: bentopdf
|
||
restart: unless-stopped
|
||
ports:
|
||
- '8888:8080'
|
||
```
|
||
|
||
Nachher:
|
||
|
||
```yaml
|
||
services:
|
||
bentopdf:
|
||
image: ghcr.io/alam00000/bentopdf-simple:latest
|
||
container_name: bentopdf
|
||
restart: unless-stopped
|
||
networks: [honeybar] # statt ports:
|
||
|
||
networks:
|
||
honeybar:
|
||
external: true # legt der HoneyBar-Proxy-Stapel an
|
||
```
|
||
|
||
Zwei Änderungen, mehr nicht:
|
||
|
||
1. **`ports:` fällt weg.** Die App braucht kein Loch nach außen mehr — der
|
||
HoneyBar-Proxy spricht sie im Docker-Netz unter ihrem Namen an (`bentopdf:8080`;
|
||
die `8080` ist der Port *im* Behälter und bleibt, die `8888` war nur die
|
||
Klinke an der Außenseite). Ginge Pangolin weiter direkt auf die App, wäre der
|
||
HoneyBar-Proxy umgangen und die HoneyBar fehlte. Zum Nachsehen darf ein `ports:`
|
||
ruhig stehen bleiben — dann hast du zwei Wege: direkt ohne HoneyBar, über
|
||
Pangolin mit.
|
||
2. **Das Netz `honeybar` dazu.** Es entsteht im HoneyBar-Proxy-Stapel; hier wird es
|
||
nur als `external` mitbenutzt. Deshalb jenen Stapel zuerst starten.
|
||
|
||
Dazu noch zwei Zeilen im HoneyBar-Proxy (`$ziel` und `$appid`) und in Pangolin die
|
||
Ressource von der App auf `honeybar:80` umstellen. Fertig.
|
||
|
||
#### Was es kostet
|
||
|
||
Nicht viel, aber nicht nichts:
|
||
|
||
- **Ein Ausfallpunkt mehr.** Ist der HoneyBar-Proxy weg, sind die Apps dahinter weg —
|
||
nicht nur ihre HoneyBar. `restart: unless-stopped` ist deshalb Pflicht; ein
|
||
nginx, der nur durchreicht, fällt praktisch nie aus.
|
||
- **Eine Stelle mehr zum Pflegen.** Je App zwei Zeilen, und beim Umbenennen
|
||
einer Adresse muss man daran denken.
|
||
- **Ein Sprung mehr im Netz.** Im selben Docker-Netz nicht messbar.
|
||
- **Nicht jede App muss dahinter.** Was ohne HoneyBar laufen soll, zeigt in
|
||
Pangolin weiter direkt auf die App. Das entscheidest du je App.
|
||
|
||
**Fertige Beispiele:**
|
||
|
||
- [`beispiele/bentopdf/`](beispiele/bentopdf/) — ein Dienst, alles über die
|
||
eigene Adresse. Geprüft mit `ghcr.io/alam00000/bentopdf-simple`, das eine
|
||
strenge CSP und COEP mitbringt: HoneyBar, Apps und Wetter erscheinen, ohne dass
|
||
an dessen Kopfzeilen etwas geändert wird.
|
||
- [`beispiele/mehrere-apps/`](beispiele/mehrere-apps/) — **HoneyOS und ein
|
||
HoneyBar-Proxy für beliebig viele Dienste in einem Stapel.** Zwei Tabellen oben in
|
||
der Datei sagen, welcher Name zu welchem Behälter gehört und welche App das
|
||
für HoneyOS ist; eine neue App sind zwei Zeilen. Die HoneyBar hebt die aktuelle
|
||
App hervor, sobald `$appid` der `ID` in der App-Liste entspricht — genau wie
|
||
in Aether und InkNote. Am Host erkennt nginx, welcher
|
||
Dienst gemeint ist; eine neue App sind zwei Zeilen in den Tabellen oben in
|
||
`honeybar.conf`. Das ist der Aufbau, den man haben will, sobald es mehr als ein
|
||
Nicht ein Behälter je App, und nicht HoneyOS getrennt davon gepflegt.
|
||
|
||
Das Netz `honeybar` legt dieser Stapel selbst an, unter festem Namen. Die
|
||
fremden Dienste treten ihm bei (`networks: [default, honeybar]` und
|
||
`honeybar: {external: true}`); ihre eigenen `ports:` können dann ganz weg,
|
||
erreichbar ist nur noch der HoneyBar-Proxy. Deshalb diesen Stapel zuerst starten. Ein Host, der in der Tabelle fehlt, bekommt **404** statt einer
|
||
geratenen Weiterleitung — sonst wird aus einem Tippfehler in Pangolin eine
|
||
stille Umleitung irgendwohin. WebSocket-Verbindungen (Uptime Kuma, Dashboards,
|
||
alles mit Live-Anzeige) gehen durch, dafür sorgt `upgrade.conf`.
|
||
|
||
**Nur Pangolin davor?** Traefik kann Antwortkörper nicht von sich aus ändern;
|
||
dafür bräuchte es ein Plugin. Der kürzere Weg ist ein kleiner nginx *zwischen*
|
||
Pangolin und der App, der nichts tut außer einsetzen:
|
||
|
||
```yaml
|
||
honeybar:
|
||
image: nginx:alpine
|
||
restart: unless-stopped
|
||
volumes: [./honeybar.conf:/etc/nginx/conf.d/default.conf:ro]
|
||
# Pangolin zeigt jetzt hierher statt direkt auf die App.
|
||
```
|
||
|
||
Geprüft mit BentoPDF: Es schickt keine CSP, also genügen `proxy_pass`,
|
||
`Accept-Encoding ""` und die `sub_filter`-Zeile — die CSP-Zeilen oben braucht
|
||
nur, wessen App selbst eine schickt.
|
||
|
||
**Was dabei zu erwarten ist:**
|
||
|
||
- **Das Layout der fremden App kennt die HoneyBar nicht.** Rechnet sie mit
|
||
`100vh`, wird die Seite um 32 px zu hoch (Bildlaufleiste) — ihr CSS lässt
|
||
sich nicht anpassen wie das eigene. Dann hilft `data-push="none"`: Die HoneyBar
|
||
legt sich darüber, statt zu schieben. Verdeckt dafür die oberste Zeile.
|
||
- **Apps mit eigener Kopfleiste** sehen doppelt belegt aus. Geschmackssache.
|
||
- **Anmeldeseiten, iframes, Downloads** bekommen die HoneyBar ebenfalls, soweit
|
||
sie HTML sind. `sub_filter` unterscheidet das nicht.
|
||
- **Andere Proxys:** Caddy braucht ein Zusatzmodul (`replace-response`),
|
||
Traefik ein Plugin für den Antwortkörper. nginx kann es ohne Erweiterung.
|
||
|
||
## Das Systemmenü im Logo
|
||
|
||
Das `■ HONEYOS` links ist ein Menü — auf dem Schreibtisch wie in jeder App:
|
||
|
||
| in der HoneyBar (fremde App) | auf dem Schreibtisch |
|
||
|---|---|
|
||
| Schreibtisch | Über HoneyOS |
|
||
| HoneyBar einklappen | Einstellungen |
|
||
| — | Terminal-/Desktop-Modus |
|
||
|
||
Rechts daneben klappt ein Pfeil die App-Liste auf — in beiden Leisten gleich.
|
||
Das Zahnrad, das dort früher stand, ist weg: Auf dem Schreibtisch stehen die
|
||
Einstellungen links im Systemmenü; in einer fremden App gibt es sie nicht,
|
||
sondern nur den Weg dorthin.
|
||
|
||
**In den Apps ist „Schreibtisch" ein Link, keine Handlung** — und das ist eine
|
||
Grenze, keine Bequemlichkeit. Einstellungen schreiben, und dafür braucht der
|
||
Server die HoneyOS-Sitzung: Aus einer fremden Site schickt der Browser sie nicht
|
||
mit, und selbst unter einer Domain liegt das CSRF-Prüfwort in einem Cookie, das
|
||
nur HoneyOS' eigene Seiten lesen dürfen.
|
||
|
||
Deshalb stand dort lange ein zweiter Punkt „Einstellungen …", der auf dieselbe
|
||
Seite führte, nur mit geöffnetem Feld. **Er ist weg** (01.09.2026): Zwei Türen
|
||
in denselben Raum sind keine zwei Wege. Wer etwas einstellen will, ist damit
|
||
ohnehin auf dem Schreibtisch, und dort steht das Feld einen Klick — oder `[S]` —
|
||
entfernt.
|
||
|
||
**Abmelden steht in den Einstellungen, ganz unten — und sonst nirgends.** Nicht
|
||
in der HoneyBar fremder Apps, nicht im Systemmenü, nicht im Startmenü. Es ist
|
||
die einzige Schaltfläche, die eine Sitzung beendet statt etwas einzustellen, und
|
||
sie gehört dorthin, wo man sich angemeldet hat. Wer es aus einer App heraus
|
||
braucht, geht über „Einstellungen …" — ein Klick mehr, dafür kann kein
|
||
eingebettetes Bild einer fremden Seite eine Abmeldung auslösen. Der Punkt
|
||
erscheint nur, wenn HoneyOS selbst anmeldet; hinter einem Proxy oder im offenen
|
||
Betrieb fehlt er.
|
||
|
||
## Wenn die HoneyBar veraltet aussieht
|
||
|
||
Die HoneyBar wird bei Weg 2 über den **eigenen Ursprung der App** ausgeliefert
|
||
(`/honeybar/honeybar.js`). Das ist der Kniff, der CSP, CORS und COEP auf einmal
|
||
erledigt — es setzt sie aber zugleich dem Zwischenspeicher dieser App aus, und
|
||
dem ihres Service Workers, falls sie einen hat. Eine alte HoneyBar dort sieht
|
||
aus wie ein Fehler in HoneyOS: Ein neuer Menüpunkt fehlt, obwohl der
|
||
Schreibtisch ihn längst hat.
|
||
|
||
HoneyOS liefert seine Dateien deshalb mit `Cache-Control: no-store` aus. Bleibt
|
||
trotzdem etwas hängen, hilft die Probe von Hand:
|
||
|
||
```bash
|
||
curl -s https://<app-adresse>/honeybar/honeybar.js | grep -c sysmenu
|
||
```
|
||
|
||
Kommt dort **0**, ist die ausgelieferte Datei alt — dann läuft HoneyOS noch auf
|
||
einem älteren Abbild (`docker compose pull && up -d`). Kommt **2**, liegt es am
|
||
Browser: einmal hart neu laden (Strg+Umschalt+R), und bei einer installierten
|
||
PWA deren Daten löschen.
|
||
|
||
**Am schnellsten zu sehen ohne Werkzeug:** Stehen im App-Streifen der HoneyBar
|
||
Symbole neben den Namen? Dann ist sie aktuell.
|
||
|
||
## Wer angemeldet ist
|
||
|
||
Steht auf dem Schreibtisch, als eigenes Widget: Bild, Name und Begrüßung, aus
|
||
der HoneyOS-Anmeldung. In den Apps steht es **nicht** — die HoneyBar dort trägt
|
||
genau dasselbe wie die Menüleiste hier, sonst merkt man beim Wechsel, dass es
|
||
zwei verschiedene HoneyBars sind.
|
||
|
||
Früher konnte jede App einen „Wer-bin-ich"-Pfad und einen Token-Schlüssel
|
||
mitbringen; die HoneyBar fragte damit die App nach dem Anzeigenamen. Beides ist
|
||
seit dem 29.08.2026 **entfernt**. Es brachte allein einen Namen, wirkte je nach
|
||
Domain unterschiedlich, und der Token-Schlüssel hatte eine scharfe Kante: Zeigte
|
||
das Wer-bin-ich-Feld auf eine fremde Adresse, ging der Ausweis der App dorthin.
|
||
Wer die Felder in seiner `honeyos.json` noch stehen hat, braucht nichts zu tun —
|
||
sie werden beim nächsten Speichern still verworfen.
|
||
|
||
|
||
## Wie das Projekt liegt
|
||
|
||
```
|
||
main.go Start, Routen, Handler
|
||
internal/
|
||
config/ was aus der Umgebung kommt
|
||
store/ die eine JSON-Datei: Apps und Nutzer
|
||
auth/ wer fragt da? (OIDC, Proxy, offen)
|
||
weather/ Wetter holen und zehn Minuten behalten
|
||
web/
|
||
index.html
|
||
css/style.css
|
||
glyphs.json die Pixelraster — die einzige Quelle, beide Leisten
|
||
js/core.js Zustand, Server, Einstellungen, Kleinkram
|
||
js/glyphs.js lädt die Raster und baut SVGs daraus
|
||
js/os.js die beiden Ansichten, Fenster, Dialoge
|
||
js/widgets/ ein Widget je Datei
|
||
honeybar/ die HoneyBar für fremde Seiten — bewusst eine Datei
|
||
```
|
||
|
||
Die HoneyBar bleibt absichtlich **eine** Datei ohne Importe: Sie wird von
|
||
fremden Seiten mit einer Zeile eingebunden, da darf nichts nachladen.
|
||
|
||
## Entwickeln
|
||
|
||
```bash
|
||
docker compose -f docker-compose.dev.yml up -d --build # → http://localhost:8088
|
||
go test ./... # Anmeldeablauf, CSRF, Prüfungen
|
||
```
|
||
|
||
Der Dev-Betrieb läuft **ohne Anmeldung**. Die Oberfläche steckt per `go:embed` im Binary —
|
||
nach Änderungen an `web/` also neu bauen, ein Reload allein genügt nicht.
|
||
|
||
```
|
||
main.go Konfiguration, Routen, statische Auslieferung
|
||
auth.go OIDC mit PKCE, Sitzungscookie, CSRF
|
||
store.go die JSON-Datei: Apps, Nutzer, Einstellungen
|
||
auth_test.go falscher OIDC-Anbieter — der ganze Anmeldeweg im Test
|
||
web/ index.html · style.css · os.js · honeybar/ · looks/
|
||
```
|
||
|
||
Die Tests fahren einen echten OIDC-Anbieter mit eigenem Schlüssel hoch und gehen den Weg
|
||
einmal ganz durch. Wenn die grün sind, liegt ein Anmeldeproblem am Anbieter oder an der
|
||
Konfiguration, nicht am Code.
|
||
|
||
## Bauen und schieben
|
||
|
||
```bash
|
||
./build.sh # Tag kommt aus `const Version` in main.go, dazu :latest
|
||
./build.sh 1.0-test # abweichender Tag, falls nötig
|
||
```
|
||
|
||
**Die Version steht an genau einer Stelle:** `const Version` in `main.go`.
|
||
`build.sh` liest sie von dort, die Oberfläche holt sie über `/api/public`. Vorher
|
||
stand sie zusätzlich in `os.js` — dann hiess das Image 0.4 und der Schreibtisch
|
||
zeigte 0.2.
|
||
|
||
**Wann sie steigt (Marcs Regel, 25.08.2026): nicht bei jeder Kleinigkeit.**
|
||
Sonst steht in zwei Wochen 0.30 da, und die Zahl sagt nichts mehr — eine
|
||
Versionsnummer soll unterscheidbare Stände benennen, nicht Arbeitsschritte
|
||
zählen.
|
||
|
||
- **Steigt:** neue Fähigkeit, geänderte Bedienung, neuer Pfad oder neue
|
||
Einstellung — alles, wofür man wissen will, ob der Server es schon kann.
|
||
- **Steigt nicht:** Fehlerbehebung an frisch Gebautem, Feinschliff am Aussehen,
|
||
Text, Dokumentation, Aufräumen.
|
||
|
||
Das Abbild wird trotzdem gebaut und geschoben — nur unter demselben Tag und
|
||
`:latest`. Wer wissen will, was genau läuft, schaut in `git log`; dafür ist die
|
||
Versionsnummer zu grob.
|
||
|
||
## Grenzen, die man kennen sollte
|
||
|
||
- **Die Lampe an der Kachel ist kein Health-Check.** Bei einer fremden Domain darf der
|
||
Browser nur sehen, *dass* geantwortet wurde. Ein 500er sähe aus wie „online".
|
||
- **Die App-Liste ist für alle gleich.** Wer angemeldet ist, darf sie ändern — das ist für
|
||
eine Handvoll Konten gewollt und wäre für mehr Leute zu wenig.
|
||
- **Ein Angemeldeter kann jede Adresse eintragen.** Der Server prüft nur, dass es eine
|
||
http(s)-Adresse ist. Das ist zugleich die CORS-Freigabe der HoneyBar, also nicht
|
||
gedankenlos etwas Fremdes eintragen.
|
||
|
||
## Was als Nächstes ansteht
|
||
|
||
1. **CORS in Aether** für `os.…`, damit der Desktop den Anmeldestand sehen kann.
|
||
2. **Überall abmelden** in der HoneyBar — braucht pro App den CSRF-Weg zum Logout.
|
||
3. **Fenster mit Inhalt** statt nur Verweisen: „letzte Dateien", „letzte Notizen".
|