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:
1 parent
981a2331df
commit
b7cc06b7c1
12 files changed
+434
-153
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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">●●●</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">●●●</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">●●●</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
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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">●●●</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("")
|
||||
: "";
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
+45
-27
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user