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
+461 -180

No files matched your search

+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,
});
+22 -27
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,30 +16,24 @@ 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() {
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>`;
return `
<div class="uw">
${bild}
<div class="uw-text">
<b class="uw-name">${state.cfg.user}</b>
<span class="uw-gruss">${greeting()}</span>
<span class="uw-host">${state.cfg.user}@${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;
}
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>`;
return `
<div class="uw">
${bild}
<div class="uw-text">
<b class="uw-name">${escapeHtml(state.cfg.user)}</b>
<span class="uw-gruss">${greeting()}</span>
<span class="uw-host">${escapeHtml(state.cfg.user)}@${escapeHtml(state.cfg.hostname)}</span>
</div>
</div>`;
},
});
+62 -44
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,52 +8,63 @@ 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() {
if (!state.settings.weatherCity) {
state.weather = null;
renderWeather();
return;
}
try {
state.weather = await api("/api/weather");
} catch {
state.weather = null;
}
renderWeather();
}
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
function weatherHtml(w) {
return `
<div class="wx">
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
<div class="wx-text">
<b class="wx-temp">${Math.round(w.grad)}°</b>
<span class="wx-desc">${w.text}</span>
<span class="wx-city">${w.ort}</span>
</div>
</div>`;
}
async laden() {
if (!state.settings.weatherCity) {
state.weather = null;
return false;
}
try {
state.weather = await api("/api/weather");
} 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;
}
// 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;
},
export function renderWeather() {
const w = state.weather;
const tile = $("wxTile");
const desk = $("wxDesk");
inhalt() {
const w = state.weather;
return `
<div class="wx">
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
<div class="wx-text">
<b class="wx-temp">${Math.round(w.grad)}°</b>
<span class="wx-desc">${w.text}</span>
<span class="wx-city">${w.ort}</span>
</div>
</div>`;
},
});
/**
* 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();
}