# 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** `/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 ``` 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 `` 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: () => "", 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 ` ``` `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 `'; } ``` 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 `