Widget-Rahmen: Widgets sind Beschreibungen, keine Kaesten in der Seite

widgetAnmelden({ id, titel, wo, sichtbar, laden, leer, inhalt, takt }) —
der Rahmen macht daraus ein Fenster in der Spalte (Terminal) oder ein
Schild (Desktop), kennt lädt/fertig/nichts da/Fehler und haelt den
Nachladetakt. Benutzer, Wetter und Systemzustand sind umgestellt; aus
index.html sind vier feste Kaesten verschwunden und aus dem Raster die
Einzelplaetze.

Zwei Dinge, die erst dabei sichtbar wurden:

- Der Server meldete 'kein Ort eingestellt' und 'Dienst nicht
  erreichbar' beide als 204. Ein Ausfall sah damit aus wie ein
  abgeschaltetes Widget. Ausfall ist jetzt 502, mit Test.
- Die Reihenfolge haengt nicht mehr an der Modulauswertung: Seit
  glyphs.js seine Raster mit await auf oberster Ebene laedt, laufen
  Module ohne Symbole vorher durch — der Systemzustand stand deshalb
  ueber dem Benutzer. widgetsOrdnen() macht die Reihenfolge zur Angabe,
  und genau dort haengt spaeter die im Server gespeicherte ein.
This commit is contained in:
marc committed 2026-08-30 03:12:00 +02:00
1 parent 981a2331df
commit b7cc06b7c1
12 files changed
+435 -154

No files matched your search

+70 -43
View File
@@ -165,51 +165,36 @@ ist am 29.08.2026 weggefallen: Aether und InkNote liegen längst unter
erst nach der Sitzung. **Die HoneyBar weiß dort jetzt, wer schaut.** Was bleibt,
ist die Frage, ob es sich bei einem Nutzer lohnt.
### 1.4 HoneyOS selbst als PWA
### 1.4 Widget-Rahmen — ✅ gebaut (30.08.2026)
Heute ist HoneyOS eine Website: Es hat `theme-color`, aber kein Manifest, kein
Symbol und keinen Service Worker — installierbar ist es damit nicht. Am Handy
landet man also im Browser mit Adressleiste, während Aether und InkNote als
eigene Apps danebenliegen. Das ist genau der Bruch, den das Ganze eigentlich
verhindern soll: Der Schreibtisch sollte sich am wenigsten nach Website
anfühlen.
Ein Widget ist jetzt eine **Beschreibung**, kein Stück HTML im Dokument:
Was dazugehört, klein bis groß:
```js
widgetAnmelden({
id, titel,
wo: ["terminal", "desktop"], // wo es erscheinen darf
sichtbar: () => …, // Schalter aus → gar nicht erst da
laden: async () => false|void, // false = es gibt nichts zu zeigen
leer: "verbergen" | "Text", // was dann passiert
inhalt: () => "<html>",
takt: 600000, // Nachladen, optional
})
```
1. **Manifest** — Name, Startadresse, `display: standalone`, Hintergrund- und
Themenfarbe. Die Farben sollten der eingestellten Röhre folgen; sie liegen
im Server, das Manifest kann also erzeugt statt fest hinterlegt werden
(Aether macht das beim Symbol schon so).
2. **Symbol** — ein Pixelglyph in 192 und 512 px, im Amber-Look. Passt zum
Rest und ist ohne fremde Werkzeuge zu machen.
3. **Service Worker** — nötig für die Installierbarkeit. Hier ist zu
entscheiden, was er überhaupt soll (siehe unten).
Der Rahmen (`web/js/widgets/rahmen.js`) macht daraus im Terminal-Modus ein
Fenster in der Widgetspalte und im Desktop-Modus ein Schild, kennt die 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**
und reißt weder den Schreibtisch noch die anderen Widgets mit.
**Die eigentliche Frage ist der Offline-Fall.** HoneyOS zeigt fremde Apps und
später deren Daten — ohne Netz gibt es davon nichts. Ein Schreibtisch, der
offline mit leeren Kacheln dasteht, ist schlimmer als ein ehrlicher Hinweis.
Sinnvoll wäre: Oberfläche und App-Liste im Cache, damit man die Symbole sieht
und antippen kann, und ein deutlicher Vermerk „ohne Verbindung". Widgets
bleiben leer, statt Altes von gestern als aktuell auszugeben.
Umgestellt sind Benutzer, Wetter und der Systemzustand. Aus der `index.html`
sind damit vier fest verdrahtete Kästen verschwunden, und aus dem Raster die
Einzelplätze — vorher hätte das dritte Widget beides wieder angefasst.
**Aus dem Aether-Debakel gelernt und hier von Anfang an zu beachten:** Ein
Eintrag im Cache ist eine ganze Antwort **samt Kopfzeilen**. Wird die Startseite
vorab abgelegt, friert damit auch ihre CSP ein — und eine Änderung an der
Konfiguration erreicht den Browser nie wieder. Also Navigationen Netz zuerst,
Cache nur als Rückfall.
**Reihenfolge:** Erst umziehen (siehe unten), dann installierbar machen. Ein
Ursprungswechsel macht aus einer installierten PWA eine fremde App — wer
vorher installiert, installiert zweimal.
### 1.5 Widget-Rahmen
Ein Fenstertyp, der einen Inhalt holt, ihn anzeigt und sich benimmt, wenn es
schiefgeht: lädt gerade, nichts da, nicht angemeldet, Fehler. Einmal richtig
gebaut, kostet jedes weitere Widget danach fast nichts.
Muss in **beiden Betriebsarten** funktionieren — im Desktop als Fenster, im
Terminal als Kachel.
**Was dabei offen gelassen wurde:** die dritte Form. Widgets sollen später auch
als Aufklappfeld in der HoneyBar leben können, deshalb erzeugt `inhalt()` nur
den Kern; die Verpackung entsteht im Rahmen. Und die Reihenfolge ist eine
Liste — genau das, was „Widgets verschieben" braucht.
## 2 · Erste Widgets
@@ -424,7 +409,7 @@ schon gebaut) oder am Handy Pfeiltasten statt Ziehen anbieten. Nicht vergessen:
Ohne das ist die Funktion am Handy schlicht nicht da.
Voraussetzung ist ein Widget-Verzeichnis — heute stehen Benutzer und Wetter
fest im HTML. Sinnvoll zusammen mit dem Widget-Rahmen (1.5) zu bauen, nicht
fest im HTML. Sinnvoll zusammen mit dem Widget-Rahmen (1.4) zu bauen, nicht
davor.
### Widgets in der HoneyBar selbst
@@ -434,9 +419,9 @@ ist aber die Seite, die man am seltensten offen hat — die HoneyBar dagegen ist
überall. Naheliegend also: ein Knopf in der HoneyBar klappt dasselbe Widget als
kleines Feld auf, ohne die App zu verlassen.
Technisch ist das dieselbe Sache wie 1.5, nur ein zweites Zuhause für denselben
Technisch ist das dieselbe Sache wie 1.4, nur ein zweites Zuhause für denselben
Inhalt: Der Rahmen muss dann drei Formen können — Fenster, Kachel, Aufklappfeld.
Das ist beim **Bauen** von 1.5 zu bedenken, hinterher wird es teuer.
Das ist beim **Bauen** von 1.4 zu bedenken, hinterher wird es teuer.
Für Widgets, die nur sich selbst brauchen (Wetter, Bild, Uhr), ist das
geschenkt — sie fragen HoneyOS, nicht die App.
@@ -445,6 +430,48 @@ geschenkt — sie fragen HoneyOS, nicht die App.
## 3 · Ausbau
### 3.x HoneyOS selbst als PWA — zurückgestellt (30.08.2026)
Heute ist HoneyOS eine Website: Es hat `theme-color`, aber kein Manifest, kein
Symbol und keinen Service Worker — installierbar ist es damit nicht. Am Handy
landet man also im Browser mit Adressleiste, während Aether und InkNote als
eigene Apps danebenliegen. Das ist genau der Bruch, den das Ganze eigentlich
verhindern soll: Der Schreibtisch sollte sich am wenigsten nach Website
anfühlen.
Was dazugehört, klein bis groß:
1. **Manifest** — Name, Startadresse, `display: standalone`, Hintergrund- und
Themenfarbe. Die Farben sollten der eingestellten Röhre folgen; sie liegen
im Server, das Manifest kann also erzeugt statt fest hinterlegt werden
(Aether macht das beim Symbol schon so).
2. **Symbol** — ein Pixelglyph in 192 und 512 px, im Amber-Look. Passt zum
Rest und ist ohne fremde Werkzeuge zu machen.
3. **Service Worker** — nötig für die Installierbarkeit. Hier ist zu
entscheiden, was er überhaupt soll (siehe unten).
**Die eigentliche Frage ist der Offline-Fall.** HoneyOS zeigt fremde Apps und
später deren Daten — ohne Netz gibt es davon nichts. Ein Schreibtisch, der
offline mit leeren Kacheln dasteht, ist schlimmer als ein ehrlicher Hinweis.
Sinnvoll wäre: Oberfläche und App-Liste im Cache, damit man die Symbole sieht
und antippen kann, und ein deutlicher Vermerk „ohne Verbindung". Widgets
bleiben leer, statt Altes von gestern als aktuell auszugeben.
**Aus dem Aether-Debakel gelernt und hier von Anfang an zu beachten:** Ein
Eintrag im Cache ist eine ganze Antwort **samt Kopfzeilen**. Wird die Startseite
vorab abgelegt, friert damit auch ihre CSP ein — und eine Änderung an der
Konfiguration erreicht den Browser nie wieder. Also Navigationen Netz zuerst,
Cache nur als Rückfall.
**Zurückgestellt (Marc, 30.08.2026): erst die Hauptfunktionen, dann das
Gehäuse.** Die PWA ist mehr Gefühl als Funktion — sie macht nichts möglich, was
heute nicht ginge, während der Widget-Rahmen darunter alles Weitere trägt.
**Reihenfolge:** Erst umziehen (siehe unten), dann installierbar machen. Ein
Ursprungswechsel macht aus einer installierten PWA eine fremde App — wer
vorher installiert, installiert zweimal.
- Welche Widgets wo liegen, **pro Nutzer im Server** merken — die Einstellungen
liegen ohnehin schon dort, das ist eine Erweiterung, kein Neubau
- Widgets im Terminal-Modus
+30
View File
@@ -184,6 +184,36 @@ 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.
## 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: () => state.settings.weatherDesk === "on",
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.
## Wetter
Ein Pixelsymbol mit der Temperatur — wahlweise auf dem Schreibtisch, in der
+14
View File
@@ -15,8 +15,22 @@ import (
// Open-Meteo an — das wäre langsam, unzuverlässig und benutzte einen fremden
// Dienst für unsere Testläufe.
func FakeFuerTest(t *testing.T, code int, grad float64) *Dienst {
return fake(t, code, grad, false)
}
// KaputtFuerTest stellt einen Dienst, der nicht antwortet — für den Fall, den
// die Oberfläche seit dem Widget-Rahmen anders behandelt als „nichts da".
func KaputtFuerTest(t *testing.T) *Dienst {
return fake(t, 0, 0, true)
}
func fake(t *testing.T, code int, grad float64, kaputt bool) *Dienst {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if kaputt {
w.WriteHeader(http.StatusInternalServerError)
return
}
if strings.Contains(r.URL.Path, "search") {
json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{
{"name": "Köln", "country": "Deutschland", "admin1": "Nordrhein-Westfalen",
+6 -1
View File
@@ -380,7 +380,12 @@ func (s *Server) handleWeather(w http.ResponseWriter, r *http.Request, sub strin
wetter, err := s.wetter.Jetzt(r.Context(), set["weatherCity"], lat, lon)
if err != nil {
log.Printf("wetter: %v", err)
w.WriteHeader(http.StatusNoContent)
// 502, nicht 204: Ein Ort ist eingestellt, es ist also etwas zu zeigen —
// der Dienst ist nur gerade nicht da. Die Oberflaeche unterscheidet das
// seit dem Widget-Rahmen: „nichts eingestellt" verschwindet still,
// „nicht erreichbar" sagt es. Beides als 204 zu melden hiess, dass ein
// Ausfall aussah wie ein abgeschaltetes Widget.
writeJSON(w, http.StatusBadGateway, map[string]string{"error": "Wetterdienst nicht erreichbar"})
return
}
writeJSON(w, http.StatusOK, wetter)
+25 -16
View File
@@ -284,14 +284,11 @@ body {
grid-template-columns: 320px minmax(0, 1fr);
align-items: start;
}
/* Ohne feste Plätze verteilt das Raster die drei Bereiche der Reihe nach —
das Wetter landete dann neben „system" und schob die Kacheln in die
schmale linke Spalte. Links stehen die Anzeigen untereinander, rechts
gehören die Apps, über die volle Höhe. */
.win-user { grid-column: 1; grid-row: 1; }
.win-status { grid-column: 1; grid-row: 2; }
.win-weather { grid-column: 1; grid-row: 3; }
.tiles { grid-column: 2; grid-row: 1 / span 3; }
/* Zwei Bereiche, keine Einzelplätze mehr: Links die Widgetspalte, rechts
die Apps. Vorher hatte jedes Widget seinen festen Platz im Raster — beim
dritten hätte man das Raster wieder anfassen müssen. */
.widgetspalte { grid-column: 1; }
.tiles { grid-column: 2; }
}
/* Fenster */
@@ -1028,9 +1025,7 @@ body {
white-space: nowrap;
}
/* Terminal-Modus: ein Fenster wie die anderen. Schmal gehalten — über die
volle Breite gezogen sähe eine Temperatur nach Bekanntmachung aus. */
.win-weather { max-width: 320px; align-self: start; }
/* Schreibtisch-Modus: schwebendes Schild, rechts unter der Menüleiste. Nicht
@@ -1101,16 +1096,30 @@ body {
.uw-name { color: var(--p); font-size: 15px; }
.uw-gruss { color: var(--p-mid); }
.uw-host { color: var(--p-dim); font-size: 12px; }
.win-user { max-width: 320px; align-self: start; }
/* Die Spalte selbst hält die Breite — vorher trug sie jedes Widget einzeln,
und ein neues ohne diese Zeile stand quer. */
.widgetspalte {
display: flex;
flex-direction: column;
gap: clamp(14px, 3vw, 28px);
align-self: start;
min-width: 0;
}
@media (min-width: 860px) {
.widgetspalte { max-width: 320px; }
}
/* Die vier Zustände, die der Rahmen kennt. „lädt" und „nicht erreichbar"
stehen im selben Kasten wie der Inhalt — ein Widget, das bei einem Fehler
verschwindet, sieht aus wie eines, das man nie eingeschaltet hat. */
.w-zustand { color: var(--p-dim); margin: 0; font-size: 12.5px; }
.w-fehler { color: var(--p-mid); }
/* Einspaltig (Handy, schmales Fenster) sollen die Widgets so breit sein wie das
Systemfenster. Drei verschieden breite Kästen untereinander sehen aus wie ein
Fehler, nicht wie Absicht. Muss hinter den Regeln stehen, die es aufhebt —
eine Medienabfrage erhöht die Spezifität nicht. */
@media (max-width: 859px) {
.win-weather,
.win-user { max-width: none; }
}
/* Die Schilder auf dem Schreibtisch stapeln sich oben rechts. */
.deskwidgets {
+6 -29
View File
@@ -47,30 +47,10 @@
</header>
<main class="stage" id="stage">
<!-- Statusfenster -->
<section class="win win-status">
<div class="win-bar"><span class="win-title">system</span><span class="win-dots">&#9679;&#9679;&#9679;</span></div>
<div class="win-body">
<dl class="statlist" id="statList"></dl>
</div>
</section>
<!-- Wer angemeldet ist. Stand früher in der Menüleiste — dort ist
es aber der einzige Unterschied zur HoneyBar in den Apps
gewesen, und der Schreibtisch hat für so etwas Platz. -->
<section class="win win-user" id="userTile" hidden>
<div class="win-bar"><span class="win-title">angemeldet</span><span class="win-dots">&#9679;&#9679;&#9679;</span></div>
<div class="win-body user-body"></div>
</section>
<!-- Wetter. Steht in beiden Betriebsarten zur Verfügung, wird aber
nur gezeigt, wenn ein Ort eingestellt und das Widget an ist. -->
<section class="win win-weather" id="wxTile" hidden>
<div class="win-bar"><span class="win-title">wetter</span><span class="win-dots">&#9679;&#9679;&#9679;</span></div>
<div class="win-body wx-body"></div>
</section>
<!-- App-Kacheln -->
<!-- Links die Widgets untereinander, rechts die Apps. Was hier steht,
entscheidet der Widget-Rahmen (js/widgets/rahmen.js) — im
Dokument steht nur der Platz dafür. -->
<div class="widgetspalte" id="widgetSpalte"></div>
<section class="tiles" id="tiles" aria-label="Apps starten"></section>
</main>
@@ -78,11 +58,8 @@
<div class="deskspace" id="deskspace" hidden>
<div class="desk-icons" id="deskIcons"></div>
<div class="windows" id="windows"></div>
<!-- Dasselbe Wetter, im Schreibtisch-Modus als kleines Feld oben rechts -->
<div class="deskwidgets">
<div class="deskwidget" id="userDesk" hidden></div>
<div class="deskwidget" id="wxDesk" hidden></div>
</div>
<!-- Dieselben Widgets, im Schreibtisch-Modus als Schilder oben rechts -->
<div class="deskwidgets" id="deskwidgets"></div>
</div>
<footer class="taskbar" id="taskbar" hidden>
+25 -20
View File
@@ -4,8 +4,16 @@ import {
SS_BOOTED, state,
} from "./core.js";
import { GLYPHS, glyphSvg } from "./glyphs.js";
import { renderUser } from "./widgets/user.js";
import { loadWeather, renderWeather } from "./widgets/weather.js";
import { widgetsLaden, widgetsOrdnen, widgetsStarten, widgetsZeichnen } from "./widgets/rahmen.js";
import "./widgets/user.js";
import { statsHtml } from "./widgets/system.js";
import { loadWeather, renderWeatherBar } from "./widgets/weather.js";
// Die Reihenfolge steht hier und nicht in den Importzeilen darüber: Module
// werden nicht in deren Reihenfolge ausgewertet, sobald eines auf oberster
// Ebene wartet (glyphs.js holt seine Raster mit await). Bis die Reihenfolge
// aus dem Server kommt, ist das ihre Stelle.
widgetsOrdnen(["benutzer", "system", "wetter"]);
/*
* HoneyOS — Desktop-Logik.
@@ -116,8 +124,11 @@ function renderShell() {
// Nicht abgewartet: Das Wetter kommt von außen und darf den Aufbau des
// Schreibtischs nicht verzögern. Danach alle 10 Minuten, im selben Takt,
// in dem der Server seinen Zwischenspeicher erneuert.
loadWeather();
setInterval(loadWeather, 10 * 60 * 1000);
// Nicht abgewartet: Die Widgets holen ihre Daten von außen und dürfen den
// Aufbau des Schreibtischs nicht verzögern. Den Takt fürs Nachladen kennt
// jedes Widget selbst.
widgetsLaden().then(renderWeatherBar);
widgetsStarten();
}
function renderMenubarApps() {
@@ -163,9 +174,7 @@ function renderMode() {
closeAllWindows();
renderTiles();
}
renderStats();
renderUser();
renderWeather();
widgetsZeichnen();
paintProbes();
}
@@ -188,18 +197,13 @@ function launch(app) {
/** Die Kennzahlen stehen in beiden Ansichten — deshalb über die Klasse in
* alle vorhandenen Listen schreiben, nicht in eine bestimmte id. */
/** Zeichnet den Systemzustand überall neu, wo er gerade steht: als Widget in
* der Spalte und, im Desktop-Modus, im Fenster aus dem Startmenü. */
function renderStats() {
const up = Object.values(state.probes).filter((s) => s === "up").length;
const rows = [
["apps", `${state.cfg.apps.length}`],
["erreichbar", `${up}/${state.cfg.apps.length}`],
["modus", state.settings.mode],
["röhre", state.settings.phosphor],
["version", `v${state.cfg.version}`],
];
const html = rows.map(([k, v]) => `<dt>${k}</dt><dd>${escapeHtml(v)}</dd>`).join("");
document.querySelectorAll(".statlist").forEach((dl) => (dl.innerHTML = html));
widgetsZeichnen();
document.querySelectorAll("#windows .statlist").forEach((dl) => {
dl.outerHTML = statsHtml();
});
}
function tickClock() {
@@ -490,8 +494,9 @@ function openSystemWindow() {
id: "system",
title: "system",
w: 320,
body: `<p class="greet"></p><dl class="statlist"></dl>`,
onMount: renderStats,
// Derselbe Inhalt wie das Widget im Terminal-Modus — eine Quelle, damit
// nicht zwei Listen mit denselben Zahlen auseinanderlaufen.
body: statsHtml(),
});
}
+151
View File
@@ -0,0 +1,151 @@
/* ── Der Widget-Rahmen ─────────────────────────────────────────────────────
*
* Ein Widget ist hier eine **Beschreibung**, kein Stück HTML im Dokument.
* Vorher standen Benutzer und Wetter fest in index.html, jedes mit eigener
* Zeigen-und-Verstecken-Logik in seiner Datei — beim dritten Widget hätte man
* dieselben zwanzig Zeilen ein drittes Mal geschrieben.
*
* Der Rahmen kümmert sich um das, was jedes Widget braucht und keines für sich
* lösen sollte:
*
* · beide Betriebsarten — Fenster im Terminal, Schild auf dem Schreibtisch
* · die vier Zustände: lädt · fertig · nichts da · Fehler
* · Reihenfolge und Nachladen im Takt
*
* Ein Widget beschreibt sich so:
*
* {
* id, titel,
* wo: ["terminal", "desktop"] // wo es erscheinen darf
* sichtbar: () => true // Schalter aus → gar nicht erst da
* laden: async () => false|void // false = es gibt nichts zu zeigen
* leer: "verbergen" | "Text" // was dann passiert
* inhalt: () => "<html>"
* takt: 600000 // ms, optional
* }
*
* Absichtlich offen gelassen: eine dritte Form. Die Widgets sollen später auch
* als Aufklappfeld in der HoneyBar leben können — deshalb erzeugt `inhalt()`
* nur den Kern, und die Verpackung entsteht hier.
*/
import { $, state } from "../core.js";
const REGISTER = [];
/** id → "laedt" | "fertig" | "leer" | "fehler" */
const zustand = {};
const uhren = {};
export function widgetAnmelden(def) {
REGISTER.push(def);
zustand[def.id] = def.laden ? "laedt" : "fertig";
}
/** Nur zum Nachsehen von außen — die Liste selbst gehört dem Rahmen. */
export function widgetListe() {
return REGISTER.map((w) => w.id);
}
/**
* Legt die Reihenfolge fest. Was nicht genannt ist, hängt sich hinten an.
*
* Warum überhaupt? Weil die Reihenfolge sonst davon abhinge, in welcher
* Reihenfolge die Module ausgewertet werden — und das ist **nicht** die
* Reihenfolge der Importzeilen: `glyphs.js` lädt seine Raster mit `await` auf
* oberster Ebene, also warten alle Module, die es brauchen, während die
* anderen schon durchlaufen. Der Systemzustand braucht keine Symbole und stand
* deshalb vor dem Benutzer, obwohl er darunter importiert wird.
*
* Hier ist die Reihenfolge damit eine Angabe statt einer Nebenwirkung — und
* genau die Stelle, an der später die im Server gespeicherte Reihenfolge
* einhaken kann.
*/
export function widgetsOrdnen(ids) {
const platz = (w) => {
const i = ids.indexOf(w.id);
return i < 0 ? ids.length : i;
};
REGISTER.sort((a, b) => platz(a) - platz(b));
}
/**
* Lädt ein Widget (oder alle) und zeichnet neu.
*
* Fehler bleiben hier: Ein Widget, das seinen Server nicht erreicht, sagt das
* in seinem eigenen Rahmen — es darf weder den Schreibtisch aufhalten noch die
* anderen Widgets mitreißen.
*/
export async function widgetsLaden(nurId) {
const dran = REGISTER.filter((w) => w.laden && (!nurId || w.id === nurId));
await Promise.all(
dran.map(async (w) => {
zustand[w.id] = "laedt";
widgetsZeichnen();
try {
zustand[w.id] = (await w.laden()) === false ? "leer" : "fertig";
} catch {
zustand[w.id] = "fehler";
}
})
);
widgetsZeichnen();
}
/** Startet die Nachlade-Takte. Einmal beim Aufbau des Schreibtischs. */
export function widgetsStarten() {
REGISTER.forEach((w) => {
if (!w.takt || uhren[w.id]) return;
uhren[w.id] = setInterval(() => widgetsLaden(w.id), w.takt);
});
}
function zustandHtml(w) {
switch (zustand[w.id]) {
case "laedt":
return `<p class="w-zustand">lädt …</p>`;
case "fehler":
return `<p class="w-zustand w-fehler">nicht erreichbar</p>`;
case "leer":
return `<p class="w-zustand">${w.leer}</p>`;
default:
return w.inhalt();
}
}
/**
* Zeichnet alle Widgets in die Ansicht, die gerade gilt.
*
* Beide Container werden bei jedem Durchgang neu gefüllt statt einzeln
* nachgeführt: Bei einer Handvoll Widgets ist das billiger als Buchführung
* darüber, was sich geändert hat — und es kann nicht auseinanderlaufen.
*/
export function widgetsZeichnen() {
const desktop = state.settings.mode === "desktop";
const modus = desktop ? "desktop" : "terminal";
const spalte = $("widgetSpalte");
const schilder = $("deskwidgets");
if (!spalte || !schilder) return;
const dran = REGISTER.filter((w) => {
if (w.wo && !w.wo.includes(modus)) return false;
if (w.sichtbar && !w.sichtbar()) return false;
// „nichts da" heißt bei manchen Widgets: gar nicht erst hinstellen. Eine
// leere Wetterkachel wäre schlechter als keine.
return !(zustand[w.id] === "leer" && w.leer === "verbergen");
});
spalte.innerHTML = desktop
? ""
: dran
.map(
(w) => `
<section class="win win-widget" data-widget="${w.id}">
<div class="win-bar"><span class="win-title">${w.titel}</span><span class="win-dots">&#9679;&#9679;&#9679;</span></div>
<div class="win-body">${zustandHtml(w)}</div>
</section>`
)
.join("");
schilder.innerHTML = desktop
? dran.map((w) => `<div class="deskwidget" data-widget="${w.id}">${zustandHtml(w)}</div>`).join("")
: "";
}
+29
View File
@@ -0,0 +1,29 @@
import { escapeHtml, state } from "../core.js";
import { widgetAnmelden } from "./rahmen.js";
/* ── Systemzustand ─────────────────────────────────────────────────────────
*
* Nur im Terminal-Modus ein Widget. Auf dem Schreibtisch gibt es dieselben
* Zahlen als Fenster über das Startmenü — ein Schild, das dauerhaft „modus:
* desktop" behauptet, während man im Desktop-Modus steht, sagt niemandem etwas.
*/
export function statsHtml() {
const up = Object.values(state.probes).filter((s) => s === "up").length;
const rows = [
["apps", `${state.cfg.apps.length}`],
["erreichbar", `${up}/${state.cfg.apps.length}`],
["modus", state.settings.mode],
["röhre", state.settings.phosphor],
["version", `v${state.cfg.version}`],
];
return `<dl class="statlist">${rows
.map(([k, v]) => `<dt>${k}</dt><dd>${escapeHtml(v)}</dd>`)
.join("")}</dl>`;
}
widgetAnmelden({
id: "system",
titel: "system",
wo: ["terminal"],
inhalt: statsHtml,
});
+12 -17
View File
@@ -1,5 +1,6 @@
import { $, escapeAttr, state } from "../core.js";
import { escapeAttr, escapeHtml, state } from "../core.js";
import { glyphSvg } from "../glyphs.js";
import { widgetAnmelden } from "./rahmen.js";
function greeting() {
const h = new Date().getHours();
@@ -15,8 +16,13 @@ function greeting() {
* die HoneyBar in den Apps nicht hat — beide sollen aber gleich aussehen, sonst
* merkt man beim Wechsel, dass es zwei verschiedene HoneyBars sind. Auf dem
* Schreibtisch ist ohnehin Platz dafür, und die Begrüßung gehört dazu.
*
* Kein `laden`: Der Name steht schon im Zustand, seit die Seite ihn geholt hat.
*/
function userHtml() {
widgetAnmelden({
id: "benutzer",
titel: "angemeldet",
inhalt() {
const bild = state.me && state.me.picture
? `<img class="uw-bild" src="${escapeAttr(state.me.picture)}" alt="" />`
: `<span class="uw-bild uw-leer">${glyphSvg("chip")}</span>`;
@@ -24,21 +30,10 @@ function userHtml() {
<div class="uw">
${bild}
<div class="uw-text">
<b class="uw-name">${state.cfg.user}</b>
<b class="uw-name">${escapeHtml(state.cfg.user)}</b>
<span class="uw-gruss">${greeting()}</span>
<span class="uw-host">${state.cfg.user}@${state.cfg.hostname}</span>
<span class="uw-host">${escapeHtml(state.cfg.user)}@${escapeHtml(state.cfg.hostname)}</span>
</div>
</div>`;
}
export function renderUser() {
const tile = $("userTile");
const desk = $("userDesk");
const desktopMode = state.settings.mode === "desktop";
const html = userHtml();
tile.hidden = desktopMode;
desk.hidden = !desktopMode;
tile.querySelector(".user-body").innerHTML = html;
desk.innerHTML = html;
}
},
});
+46 -28
View File
@@ -1,5 +1,6 @@
import { $, api, state } from "../core.js";
import { api, $, state } from "../core.js";
import { glyphSvg } from "../glyphs.js";
import { widgetAnmelden, widgetsLaden } from "./rahmen.js";
/* ── Wetter ────────────────────────────────────────────────────────────────
*
@@ -7,21 +8,38 @@ import { glyphSvg } from "../glyphs.js";
* was er hergibt. Kein Ort eingestellt oder Dienst nicht erreichbar: 204, und
* das Widget bleibt weg. Eine Zahl von gestern wäre schlechter als keine.
*/
export async function loadWeather() {
widgetAnmelden({
id: "wetter",
titel: "wetter",
// Der eigene Schalter — unabhängig von dem für die HoneyBar.
sichtbar: () => state.settings.weatherDesk === "on",
leer: "verbergen",
takt: 10 * 60 * 1000, // derselbe Takt, in dem der Server seinen Speicher erneuert
async laden() {
if (!state.settings.weatherCity) {
state.weather = null;
renderWeather();
return;
return false;
}
try {
state.weather = await api("/api/weather");
} catch {
} catch (e) {
// Auch im Fehlerfall den alten Stand wegwerfen: Das Widget zeigt dann
// „nicht erreichbar", und in der Leiste verschwindet das Symbol. Eine
// Temperatur von vorhin, die aussieht wie von jetzt, wäre schlechter.
state.weather = null;
throw e;
}
renderWeather();
}
// 204: kein Ort, kein Dienst — der Aufrufhelfer gibt dann nichts zurück.
if (!state.weather || !state.weather.glyph) {
state.weather = null;
return false;
}
return true;
},
function weatherHtml(w) {
inhalt() {
const w = state.weather;
return `
<div class="wx">
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
@@ -31,28 +49,22 @@ function weatherHtml(w) {
<span class="wx-city">${w.ort}</span>
</div>
</div>`;
}
},
});
export function renderWeather() {
const w = state.weather;
const tile = $("wxTile");
const desk = $("wxDesk");
/**
* Und in der HoneyBar. Die Menüleiste hier ist zwar anderer Code als
* honeybar.js in den Apps, aber für den, der davorsitzt, dieselbe HoneyBar —
* ein Schalter, der nur woanders wirkt, wäre nicht zu erklären.
*
* Sie hängt am selben geladenen Stand, hat aber ihren eigenen Schalter und
* gehört nicht in den Widget-Rahmen: Sie ist kein Widget, sondern ein Teil der
* Leiste.
*/
export function renderWeatherBar() {
const bar = $("mbWx");
const desktopMode = state.settings.mode === "desktop";
// Das Widget: Fenster im Terminal-Modus, Schild auf dem Schreibtisch.
const alsWidget = state.settings.weatherDesk === "on" && w;
tile.hidden = !alsWidget || desktopMode;
desk.hidden = !alsWidget || !desktopMode;
if (alsWidget) {
const html = weatherHtml(w);
tile.querySelector(".wx-body").innerHTML = html;
desk.innerHTML = html;
}
// Und in der HoneyBar. Die Menüleiste hier ist zwar anderer Code als
// honeybar.js in den Apps, aber für den, der davorsitzt, dieselbe HoneyBar —
// ein Schalter, der nur woanders wirkt, wäre nicht zu erklären.
if (!bar) return;
const w = state.weather;
const inBar = state.settings.weatherBar === "on" && w;
bar.hidden = !inBar;
if (inBar) {
@@ -60,3 +72,9 @@ export function renderWeather() {
bar.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
}
}
/** Beides zusammen: Widget und Leiste hängen am selben Abruf. */
export async function loadWeather() {
await widgetsLaden("wetter");
renderWeatherBar();
}
+21
View File
@@ -46,6 +46,27 @@ func TestMitOrtKommtDasWetter(t *testing.T) {
}
}
// Ist ein Ort eingestellt und der Dienst fällt aus, ist das etwas anderes als
// „nichts eingestellt": Es gibt etwas zu zeigen, es kommt nur gerade nicht. Die
// Oberfläche unterscheidet das — 204 verschwindet still, 502 sagt „nicht
// erreichbar". Beides als 204 zu melden hiess: Ein Ausfall sah aus wie ein
// abgeschaltetes Widget.
func TestAusfallIstNichtDasselbeWieNichts(t *testing.T) {
s := newTestServer(t, "")
s.wetter = weather.KaputtFuerTest(t)
if err := s.store.SetSettings(store.LocalSub, map[string]string{
"weatherCity": "Köln", "weatherLat": "50.938", "weatherLon": "6.96",
}); err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/api/weather", nil))
if rec.Code != http.StatusBadGateway {
t.Fatalf("bei Ausfall erwartet 502, bekam %d", rec.Code)
}
}
// Der Streifen in der Leiste heisst .apps und blendet seine rechten 18 Pixel
// mit einer Maske aus. Bekommt ein zweites Element dieselbe Klasse, erbt es die
// Maske — die Schublade sah rechts aus, als loese sie sich auf, und in der