diff --git a/IDEAS.md b/IDEAS.md index 550d493..51d6433 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -14,19 +14,21 @@ wirklich hinschaut. Ohne diese Stücke ist alles danach nicht möglich. Reihenfolge nach dem Verhältnis von Aufwand zu spürbarer Wirkung. -### 1.1 Die Leiste trägt deine Röhre +### 1.1 Die Leiste trägt deine Röhre — ✅ erledigt (24.08.2026) -**Das größte Gefühl für den kleinsten Eingriff.** Heute ist `honeybar.js` fest -bernsteinfarben — auch wenn dein Schreibtisch auf grün steht. Genau dieser Bruch -lässt die Leiste wie etwas Angeklebtes wirken statt wie Teil eines Systems. +Die Leiste liest ihre Farben nicht mehr fest, sondern aus derselben Antwort, mit +der sie die App-Liste holt. Röhre in HoneyOS auf grün → die Leiste in Aether ist +grün, ohne dass Aether angefasst wird. `glow` und `scanlines` gelten dort ebenso. -Die Einstellungen liegen seit 24.08. serverseitig. Liest die Leiste sie mit, gilt: -Röhre in InkNote auf grün stellen → Schreibtisch, Aether und jede weitere App -ziehen mit, weil es dieselbe Einstellung an derselben Stelle ist. +**Ging entgegen der ersten Einschätzung auch über Site-Grenzen hinweg.** Ich +hatte notiert, das scheitere daran, dass der Browser die Sitzung nicht mitschickt +— stimmt, aber der Schlüssel für die App-Liste löst es: dieselbe Anfrage bringt +das Aussehen gleich mit. Deshalb liefert `/api/apps` jetzt `{apps, settings}` +statt nur einer Liste. -Zu tun: die fest eingetragenen Farbwerte im Shadow DOM durch dieselben -`--p*`-Variablen ersetzen, die `style.css` schon benutzt, und beim Start -`/api/me` mitnehmen (holt die Leiste ohnehin schon für den Namen). +Offen bleibt: Bei mehreren Nutzern trägt die Leiste die Farbe des +Standardnutzers, weil ihr über die Site-Grenze niemand sagen kann, wer da +gerade schaut. ### 1.2 Systemmenü im Logo @@ -120,6 +122,16 @@ wirklich draufschaust, lohnt der Rest. Wenn nicht, war es ein Abend. ### Wie sichtbar darf die Leiste sein? +**Teilweise beantwortet:** „ausblenden" heißt seit dem 24.08. „einklappen" — es +bleibt ein 3 px hoher Strich am oberen Rand stehen, ein Klick holt die Leiste +zurück. Vorher war das eine Einbahnstraße: wer einmal ausgeblendet hatte, kam +nur über die Entwicklerkonsole wieder heran. `?honeybar=1` an der Adresse +funktioniert als Notausgang. + +Damit ist der schmale Streifen technisch schon da. Offen bleibt, ob er am Handy +der **Standard** sein soll. + + **Der Zwiespalt:** Die Zeile steht im HTML der App, also ist die Leiste bei *jedem* Aufruf da — auch wenn man die Cloud einfach direkt öffnet und mit HoneyOS gerade nichts zu tun hat. 32 px, dauerhaft, in einer App, die man auch diff --git a/auth_test.go b/auth_test.go index fb09f05..9c947e9 100644 --- a/auth_test.go +++ b/auth_test.go @@ -540,3 +540,33 @@ func TestSchlüsselGiltAuchHinterDemProxy(t *testing.T) { t.Errorf("richtiger Schlüssel: %d, erwartet 200", code) } } + +// Die Leiste stellt genau eine Anfrage — sie muss Liste und Aussehen bringen. +func TestAppListeBringtDasAussehenMit(t *testing.T) { + s := proxyServer(t, Config{ProxyAuth: true, AppsKey: "geheim123"}) + if err := s.store.SetSettings(localSub, map[string]string{"phosphor": "green", "glow": "off"}); err != nil { + t.Fatal(err) + } + if err := s.store.SetApps([]App{{ID: "aether", Name: "AETHER", URL: "https://cloud.example.test"}}); err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("GET", "/api/apps", nil) + req.Header.Set("X-HoneyOS-Key", "geheim123") + rec := httptest.NewRecorder() + s.routes().ServeHTTP(rec, req) + + var got struct { + Apps []App + Settings map[string]string + } + if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { + t.Fatalf("Antwort nicht lesbar: %v — %s", err, rec.Body) + } + if len(got.Apps) != 1 || got.Apps[0].ID != "aether" { + t.Errorf("Liste fehlt: %+v", got.Apps) + } + if got.Settings["phosphor"] != "green" || got.Settings["glow"] != "off" { + t.Errorf("Aussehen fehlt: %+v", got.Settings) + } +} diff --git a/main.go b/main.go index 91382b4..dbaabbe 100644 --- a/main.go +++ b/main.go @@ -317,11 +317,22 @@ func (s *Server) handleSetSettings(w http.ResponseWriter, r *http.Request, sub s w.WriteHeader(http.StatusNoContent) } -func (s *Server) handleApps(w http.ResponseWriter, r *http.Request, _ string) { - writeJSON(w, http.StatusOK, s.store.Apps()) +// handleApps liefert die Liste **und** das Aussehen. Beides zusammen, weil die +// Leiste in einer fremden App genau eine Anfrage stellen kann: Sie liegt oft auf +// einer anderen Site, kommt also nur mit dem Schlüssel an Daten und hat keine +// Sitzung, aus der sich der Rest ergäbe. +// +// Welche Einstellungen? Die des Angemeldeten, wenn es einen gibt — sonst die des +// Standardnutzers. Bei mehreren Nutzern trägt die Leiste damit die Farbe des +// Standardnutzers; für eine Person stimmt es immer. +func (s *Server) handleApps(w http.ResponseWriter, r *http.Request, sub string) { + writeJSON(w, http.StatusOK, map[string]any{ + "apps": s.store.Apps(), + "settings": s.store.User(sub).Settings, + }) } -func (s *Server) handleSetApps(w http.ResponseWriter, r *http.Request, _ string) { +func (s *Server) handleSetApps(w http.ResponseWriter, r *http.Request, sub string) { var in []App if err := decode(r, &in); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) @@ -331,7 +342,7 @@ func (s *Server) handleSetApps(w http.ResponseWriter, r *http.Request, _ string) writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } - writeJSON(w, http.StatusOK, s.store.Apps()) + s.handleApps(w, r, sub) } /* ── Kleinkram ───────────────────────────────────────────────────────────── */ diff --git a/web/honeybar/honeybar.js b/web/honeybar/honeybar.js index dbdf695..f54c63a 100644 --- a/web/honeybar/honeybar.js +++ b/web/honeybar/honeybar.js @@ -53,6 +53,7 @@ const appsKey = d("key", ""); let apps = DEFAULT_APPS; + let settings = null; /* ── Aufbau ───────────────────────────────────────────────────────────── */ @@ -69,55 +70,66 @@ root.innerHTML = ` @@ -136,9 +148,35 @@
wechseln zu

- + `; + /* ── Aussehen ───────────────────────────────────────────────────────────── + * Dieselben drei Röhren wie in style.css. Sie hier zu wiederholen ist die + * bewusste Entscheidung: die Leiste soll ohne zweite Datei auskommen, damit + * sie in jeder fremden App mit einer Zeile funktioniert. + */ + const ROEHREN = { + amber: { p: "#ffb641", mid: "#c8842a", dim: "#6b4718", faint: "#2a1c0c", bg: "#140d07", glow: "255,182,65" }, + green: { p: "#5cff8f", mid: "#2fa85c", dim: "#17512c", faint: "#08200f", bg: "#071206", glow: "92,255,143" }, + ice: { p: "#cfe8ff", mid: "#7fa6c4", dim: "#395366", faint: "#12202a", bg: "#0a1118", glow: "207,232,255" }, + }; + + function applySettings(s) { + if (!s) return; + const t = ROEHREN[s.phosphor] || ROEHREN.amber; + const bar = root.querySelector(".bar"); + if (!bar) return; + const set = (k, v) => bar.parentNode.host.style.setProperty(k, v); + set("--p", t.p); + set("--p-mid", t.mid); + set("--p-dim", t.dim); + set("--p-faint", t.faint); + set("--bg", t.bg); + set("--glow", s.glow === "off" ? "none" : `0 0 5px rgba(${t.glow},.4)`); + set("--scan", s.scanlines === "off" ? "transparent" : "rgba(0,0,0,.13)"); + } + /* ── Verhalten ────────────────────────────────────────────────────────── */ function render() { @@ -219,6 +257,7 @@ } render(); + applySettings(settings); clock(); setInterval(clock, 20000); @@ -250,9 +289,57 @@ /* dann eben nur für diesen Aufruf */ } unmount(); + mountNub(); }); } + /** + * Eingeklappt bleibt ein 3 px hoher Strich am oberen Rand stehen. Er nimmt + * praktisch keinen Platz, liegt über dem Inhalt statt ihn zu schieben — und + * ist der Weg zurück. Ganz verschwinden darf die Leiste nicht: wer sie einmal + * weggeklickt hat, käme sonst nur noch über die Entwicklerkonsole an sie heran. + */ + let nub = null; + + function mountNub() { + if (nub) return; + nub = document.createElement("honey-bar-nub"); + nub.style.cssText = [ + "position:fixed", "top:0", "left:0", "right:0", "height:3px", + "z-index:2147483000", "cursor:pointer", "display:block", + ].join(";"); + nub.title = "HoneyOS-Leiste einblenden"; + const r = nub.attachShadow({ mode: "open" }); + r.innerHTML = ` + +
`; + + nub.addEventListener("click", () => { + forget(); + nub.remove(); + nub = null; + mount(); + showUser(); + }); + document.body.appendChild(nub); + } + + function forget() { + try { + localStorage.removeItem(LS_HIDDEN); + } catch { + /* dann gilt es nur für diesen Aufruf */ + } + } + function unmount() { host.remove(); if (pushMode === "body") { @@ -269,7 +356,16 @@ } catch { /* kein Speicher, Leiste bleibt sichtbar */ } - if (hidden) return; + // ?honeybar=1 holt sie zurück, falls der Strich einmal übersehen wird. + if (hidden && new URLSearchParams(location.search).get("honeybar") === "1") { + hidden = false; + forget(); + } + if (hidden) { + if (document.body) mountNub(); + else document.addEventListener("DOMContentLoaded", mountNub, { once: true }); + return; + } if (appsUrl) { try { @@ -281,10 +377,11 @@ headers: appsKey ? { "X-HoneyOS-Key": appsKey } : {}, }); if (res.ok) { - const list = await res.json(); - if (Array.isArray(list) && list.length) { - apps = [{ id: "honeyos", name: "DESKTOP", url: homeUrl }].concat(list); + const data = await res.json(); + if (Array.isArray(data.apps) && data.apps.length) { + apps = [{ id: "honeyos", name: "DESKTOP", url: homeUrl }].concat(data.apps); } + settings = data.settings || null; } } catch { /* nicht angemeldet, kein CORS, kein Netz → eingebaute Liste; diff --git a/web/os.js b/web/os.js index ca3e7ee..7c2f898 100644 --- a/web/os.js +++ b/web/os.js @@ -222,7 +222,7 @@ async function loadFromServer() { // Der Server hat das letzte Wort; localStorage war nur der Vorschuss. state.settings = { ...DEFAULT_SETTINGS, ...(readJson(LS_SETTINGS) || {}), ...(me.settings || {}) }; - state.cfg.apps = await api("/api/apps"); + state.cfg.apps = (await api("/api/apps")).apps; } /** Einstellungen vorab aus localStorage, damit die Röhre nicht erst weiß @@ -1000,7 +1000,7 @@ function openApps() { msg.textContent = "speichere …"; try { const clean = draft.map(({ __neu, ...rest }) => rest); - state.cfg.apps = await api("/api/apps", { method: "PUT", body: JSON.stringify(clean) }); + state.cfg.apps = (await api("/api/apps", { method: "PUT", body: JSON.stringify(clean) })).apps; draft = state.cfg.apps.map((a) => ({ ...a })); msg.textContent = `${state.cfg.apps.length} gespeichert.`; render(root);