feat(wetter): Wetter auf dem Schreibtisch und in der Leiste

Zwei Schalter, unabhaengig voneinander: Schreibtisch, Leiste, beides oder
keins. Die Stadt wird einmal gesucht und mit ihren Koordinaten gespeichert,
sonst suchte jede Abfrage den Ort erneut.

Geholt wird serverseitig und zehn Minuten zwischengespeichert. Der Grund ist
nicht Sparsamkeit: Die Leiste steht auf jeder Seite jeder App, und fragte sie
selbst an, ginge bei jedem Seitenaufruf eine Anfrage samt IP an einen Fremden —
und die CSP jeder App muesste ihn erlauben.

Kein Ort eingestellt oder Dienst nicht erreichbar: 204, und das Symbol bleibt
weg. Eine Zahl von gestern waere schlechter als keine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-25 13:12:09 +02:00
1 parent 52b11efe06
commit 4e38ccdec1
10 files changed
+823 -6

No files matched your search

+71
View File
@@ -51,6 +51,8 @@
// nur an Ursprünge, die selbst als App eingetragen sind.
const appsUrl = d("apps", script ? new URL("../api/apps", script.src).href : "");
const appsKey = d("key", "");
// Dieselbe Herkunft wie die App-Liste: Wo /api/apps liegt, liegt /api/weather.
const wetterUrl = appsUrl ? appsUrl.replace(/\/apps(\?|$)/, "/weather$1") : "";
let apps = DEFAULT_APPS;
let settings = null;
@@ -67,6 +69,33 @@
const SAFE = "env(safe-area-inset-top, 0px)";
const HOEHE = "calc(" + BAR_H + "px + " + SAFE + ")";
/* Wettersymbole als Pixelraster, dieselben Namen wie in weather.go.
Sie stehen auch in os.js — bis die geteilte Symbolquelle (1.3) gebaut ist,
ist das die kleinere Sünde: Die Leiste soll eine einzelne Datei bleiben,
die eine fremde App mit einer Zeile einbindet. */
const WX = {
sonne: "............;.....##.....;..#..##..#..;...#....#...;....####....;..##....##..;..##....##..;....####....;...#....#...;..#..##..#..;.....##.....;............",
heiter: "............;...##.......;..####..#...;...##..###..;......#####.;...####...#.;..#....##.#.;.##........#;#..........#;.##########.;............;............",
wolke: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;#..........#;#..........#;.##########.;............;............;............",
nebel: "............;....####....;..##....##..;.##.......#.;#..........#;.##########.;............;.#########..;............;..#########.;............;.#########..",
regen: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;.##########.;............;..#..#..#...;.#..#..#....;..#..#..#...;.#..#..#....",
schnee: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;.##########.;............;..#...#...#.;.#.#.#.#.#..;..#...#...#.;............",
gewitter: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;.##########.;......##....;.....##.....;....####....;......##....;.....##.....",
};
function wxSvg(name) {
const grid = (WX[name] || WX.wolke).split(";");
let d = "";
grid.forEach((row, y) => {
let run = 0;
for (let x = 0; x <= row.length; x++) {
if (row[x] === "#") run++;
else if (run) { d += `<rect x="${x - run}" y="${y}" width="${run}" height="1"/>`; run = 0; }
}
});
return `<svg viewBox="0 0 12 12" fill="currentColor" aria-hidden="true">${d}</svg>`;
}
const host = document.createElement("honey-bar");
host.style.cssText = [
"position:fixed", "top:0", "left:0", "right:0",
@@ -127,6 +156,16 @@
.user { color: var(--p); max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user[hidden] { display: none; }
.clock { color: var(--p-mid); font-variant-numeric: tabular-nums; }
.wx {
display: flex; align-items: center; gap: 5px; color: var(--p-mid);
border-color: transparent; padding: 2px 4px;
}
.wx[hidden] { display: none; }
.wx:hover { color: var(--p); border-color: var(--p-dim); }
.wx svg { width: 14px; height: 14px; display: block; }
/* Am Handy nur das Symbol: 32 px Höhe und die Apps daneben lassen der
Temperatur keinen Platz mehr. */
@media (max-width: 700px) { .wx .grad { display: none; } }
.menu-btn { border-color: var(--p-dim); line-height: 1; padding: 3px 7px; }
.menu-btn:hover { background: var(--p-faint); }
.menu {
@@ -151,6 +190,7 @@
<nav class="apps" id="apps"></nav>
<div class="right">
<span class="user" id="user" hidden></span>
<button class="wx" id="wx" hidden title="Wetter"></button>
<span class="clock" id="clock"></span>
<button class="menu-btn" id="menuBtn" aria-haspopup="true" aria-expanded="false">&#9662;</button>
</div>
@@ -215,6 +255,36 @@
});
}
/**
* Das Wetter neben der Uhr — nur wenn in HoneyOS eingeschaltet und ein Ort
* eingestellt ist. Der Server antwortet sonst mit 204, und dann bleibt hier
* schlicht nichts stehen: lieber kein Symbol als eines ohne Aussage.
*/
async function wetter() {
if (!settings || settings.weatherBar !== "on") return;
let w;
try {
if (!wetterUrl) return;
const res = await fetch(wetterUrl, {
cache: "no-store",
credentials: "include",
headers: appsKey ? { "X-HoneyOS-Key": appsKey } : {},
});
if (res.status !== 200) return;
w = await res.json();
} catch {
return; // offline oder HoneyOS aus — die App geht das nichts an
}
const el = root.getElementById("wx");
el.innerHTML = wxSvg(w.glyph) + `<span class="grad">${Math.round(w.grad)}°</span>`;
el.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
el.hidden = false;
// Ein Klick führt dorthin, wo der Ort eingestellt wird. Ändern lässt er
// sich hier nicht: Die Leiste liest über den Schlüssel und darf nichts
// schreiben — und über eine Site-Grenze wüsste sie ohnehin nicht, für wen.
el.onclick = () => window.open(homeUrl, "_blank", "noopener");
}
/**
* Zeigt an, wer angemeldet ist. Die Leiste steckt in der App selbst, der
* Aufruf geht also an denselben Ursprung — kein CORS, keine Freigabe nötig.
@@ -411,6 +481,7 @@
else document.addEventListener("DOMContentLoaded", mount, { once: true });
showUser();
wetter();
}
start();