Marcs Vorschlag, und er ist der bessere: "+ kategorie" setzt eine Ueberschriftenzeile, und was darunter steht, gehoert dazu, bis die naechste kommt. Die Ueberschrift hat dieselben Pfeile wie eine App, laesst sich umbenennen und mit x aufloesen. Damit faellt die Sonderregel von gestern weg -- "am Rand der Gruppe kategorisiert der Pfeil um, statt zu tauschen". Sie war die Beschreibung eines Modells; jetzt ist es das Modell: Schiebt man eine App ueber eine Ueberschrift, ist sie in der anderen Gruppe, und schiebt man eine Ueberschrift, wandert die Grenze. Nichts davon braucht Code. Vor allem loest es den echten Mangel der ersten Fassung: Eine Kategorie umbenennen war ein Feld je App, jetzt ist es eine Zeile. Gespeichert wird weiter als Feld an der App. Waere die Ueberschrift ein Eintrag in der App-Liste, enthielte diese Zeilen ohne ID und ohne Adresse, und jeder Leser muesste das wissen -- Kacheln, Schublade, Pruefung auf Erreichbarkeit und vor allem die HoneyBar in fremden Apps. Der Preis: Eine Ueberschrift ohne Apps darunter ueberlebt das Speichern nicht. Eine Kategorie besteht durch ihre Apps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
812 lines
37 KiB
Markdown
812 lines
37 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.
|
||
|
||
## Kategorien
|
||
|
||
Apps lassen sich gruppieren — *Apps*, *Dienste*, *Werkzeuge*, was du willst:
|
||
|
||
```
|
||
APPS
|
||
AETHER INKNOTE BENTOPDF
|
||
DIENSTE
|
||
GITEA POCKETID
|
||
```
|
||
|
||
**Angelegt wird eine Kategorie als Zeile in der Liste**, mit „+ kategorie". Was darunter
|
||
steht, gehört dazu, bis die nächste Überschrift kommt — wie in einem Dokument. Die
|
||
Überschrift ist eine Zeile wie jede andere: Sie hat dieselben Pfeile ▲▼, lässt sich
|
||
umbenennen und mit × auflösen. **Ein Sonderfall zum Umhängen existiert nicht:** Schiebst du
|
||
eine App über eine Überschrift, ist sie in der anderen Gruppe, und schiebst du eine
|
||
Überschrift, wandert die Grenze.
|
||
|
||
Was **oberhalb der ersten** Überschrift steht, hat keine Kategorie und erscheint ohne
|
||
Überschrift ganz oben. Wer keine anlegt, sieht alles wie zuvor.
|
||
|
||
**Eine Kategorie besteht nur durch ihre Apps.** Gespeichert wird sie als Feld an jeder App,
|
||
nicht als eigener Eintrag — sonst enthielte die App-Liste Zeilen ohne ID und ohne Adresse,
|
||
und jeder Leser müsste das wissen: Kacheln, Schublade, Erreichbarkeitsprüfung und vor allem
|
||
die HoneyBar in fremden Apps. Zwei Folgen davon, die man kennen sollte:
|
||
|
||
- Eine Überschrift **ohne Apps darunter überlebt das Speichern nicht.** Beim Bearbeiten
|
||
bleibt sie stehen, danach ist sie weg.
|
||
- Eine Kategorie **umbenennen ist trotzdem eine Zeile**, nicht ein Feld je App — der Editor
|
||
schreibt das Feld beim Speichern an allen Apps der Gruppe.
|
||
|
||
Überschriften erscheinen dort, wo man **sucht** — Kacheln, Schublade beider Leisten,
|
||
Programme-Fenster. Nicht im App-Streifen der Leiste (32 Pixel, kein Platz) und nicht bei
|
||
den Schreibtischsymbolen (ein Schreibtisch ist flach; Ordner wären ein eigenes Thema).
|
||
|
||
**Die Reihenfolge legt der Server fest**, nicht die Oberfläche: `SetApps` sammelt Apps
|
||
derselben Kategorie zusammen, beim Schreiben wie beim Lesen. Sortierte jeder Leser für
|
||
sich, sähe dieselbe Liste an zwei Stellen verschieden aus — genau das passierte, als die
|
||
HoneyBar ihre Überschriften beim Durchlaufen setzte und eine App ohne Kategorie am Ende
|
||
unter der letzten Überschrift landete.
|
||
|
||
## 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".
|