Files
HoneyOS/web/js/core.js
T
marcandClaude Opus 5 52deffa30e refactor: aufgeraeumt — Pakete hinten, Widgets vorne
Go: store, auth, weather und config sind eigene Pakete unter internal/. Vorher
lagen fuenf Dateien in einem Topf, und jede kannte jede.

Web: os.js hatte 1425 Zeilen und alles darin. Jetzt core.js (Zustand, Server,
Einstellungen), glyphs.js (die Pixelraster), js/widgets/ mit einem Widget je
Datei, os.js nur noch die Ansichten. Stylesheet nach css/.

Die HoneyBar bleibt eine einzelne Datei ohne Importe — sie wird von fremden
Seiten mit einer Zeile eingebunden, da darf nichts nachladen.

Nebenbei: Das gebaute Binary lag mit 9,8 MB im Repo und ist jetzt ignoriert.
Und der Test gegen eine zweite Versionsnummer geht nicht mehr auf os.js, sondern
durch alle .js — sonst haette ihn dieser Umbau still ausgehebelt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:51:21 +02:00

121 lines
4.2 KiB
JavaScript

/* Der gemeinsame Unterbau: Zustand, Server, Einstellungen, Kleinkram.
*
* Alles, was mehr als eine Datei braucht, steht hier — und nur das. Die
* Ansichten (os.js) und die Widgets holen es sich von hier, statt sich
* gegenseitig zu kennen.
*/
export const LS_SETTINGS = "honeyos.settings";
export const SS_BOOTED = "honeyos.booted";
export const DEFAULT_SETTINGS = {
mode: "terminal", // terminal | desktop
phosphor: "amber", // amber | green | ice
scanlines: "on",
flicker: "on",
glow: "on",
openIn: "tab", // tab | same
// Wetter. Zwei Schalter statt einem: Der Schreibtisch und die HoneyBar sind
// zwei verschiedene Orte, und wer das Wetter beim Arbeiten in einer App will,
// will es nicht zwingend auch auf einer Seite, die er selten aufruft.
weatherBar: "off",
weatherDesk: "off",
weatherCity: "",
weatherLat: "",
weatherLon: "",
};
export const state = {
cfg: { user: "", hostname: "honeyos", version: "…", apps: [] },
me: null, // {sub, name, picture} sobald angemeldet
weather: null, // zuletzt geholtes Wetter, oder null
oidc: false,
settings: { ...DEFAULT_SETTINGS },
probes: {}, // appId -> "up" | "down" | "unknown"
wins: {}, // id -> {el, task, min}
zTop: 10,
};
export const $ = (id) => document.getElementById(id);
export const isTouch = () => window.matchMedia("(pointer: coarse)").matches;
/* ── Server ────────────────────────────────────────────────────────────────
* Ein winziger Aufrufhelfer. Schreibende Aufrufe brauchen die CSRF-Kopfzeile:
* ihr Wert steht in einem absichtlich lesbaren Cookie, das nur bekommt, wer
* die Seite wirklich geöffnet hat.
*/
/** Der Schlüssel für die App-Liste steht als Meta-Angabe in der Seite — der
* Server legt ihn dort hinein, wenn einer eingerichtet ist. */
export const APPS_KEY = document.querySelector('meta[name="honeyos-key"]')?.content || "";
export async function api(path, opts = {}) {
const headers = { ...(opts.headers || {}) };
if (APPS_KEY) headers["X-HoneyOS-Key"] = APPS_KEY;
if (opts.method && opts.method !== "GET") {
headers["Content-Type"] = "application/json";
const tok = cookie("honeyos_csrf");
if (tok) headers["X-CSRF-Token"] = tok;
}
const res = await fetch(path, { credentials: "same-origin", cache: "no-store", ...opts, headers });
if (res.status === 204) return null;
const body = await res.json().catch(() => null);
if (!res.ok) throw new Error((body && body.error) || `${res.status} ${res.statusText}`);
return body;
}
export function cookie(name) {
return document.cookie
.split(";")
.map((c) => c.trim().split("="))
.find(([k]) => k === name)?.[1] || "";
}
export function loadLocalSettings() {
state.settings = { ...DEFAULT_SETTINGS, ...(readJson(LS_SETTINGS) || {}) };
}
export function readJson(key) {
try {
return JSON.parse(localStorage.getItem(key) || "null");
} catch {
return null;
}
}
let syncTimer = null;
export function saveSettings() {
try {
localStorage.setItem(LS_SETTINGS, JSON.stringify(state.settings));
} catch {
/* Privatmodus: dann gilt es nur für diese Sitzung — der Server merkt es trotzdem */
}
applySettings();
// Zum Server gebündelt: wer dreimal hintereinander die Röhre wechselt,
// soll nicht dreimal schreiben lassen.
clearTimeout(syncTimer);
syncTimer = setTimeout(() => {
api("/api/settings", { method: "PUT", body: JSON.stringify(state.settings) }).catch((e) =>
console.warn("Einstellungen nicht gespeichert:", e.message)
);
}, 600);
}
export function escapeHtml(s) {
return String(s).replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
}
export function escapeAttr(s) {
return escapeHtml(s).replace(/"/g, "&quot;");
}
/* Die Einstellungen schlagen als Datenattribute auf die Seite durch; das CSS
hängt daran. Steht hier, weil saveSettings() es sofort anwenden muss. */
export function applySettings() {
const r = document.documentElement;
r.dataset.phosphor = state.settings.phosphor;
r.dataset.scanlines = state.settings.scanlines;
r.dataset.flicker = state.settings.flicker;
r.dataset.glow = state.settings.glow;
r.dataset.mode = state.settings.mode;
}