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 -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,
});
+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();
}