Wie Einstellungen gespeichert und überlagert werden
Stand 11.09.2026. Christians Auftrag (F29): „Überleg du dir lieber, wie die verschiedenen Einstellungen am effizientesten gespeichert und überlagert werden können." Dazu seine Bedingungen aus N30/N31: jede Funktion ist ein Plugin und für Gruppen, Nutzer oder global abschaltbar; Defaults setzen Devs; Bereichsfarben als ein JSON je Nutzer, je Wert und insgesamt zurücksetzbar; Theme immer als Ganzes; Einfach/Erweitert; Bewegung in drei Stärken plus Barrierefreiheit; Telefon und PC verhalten sich verschieden.
Das ist ein Entwurf. Gebaut wird nichts davon, bevor er nicht Ja gesagt hat.
Hinweis: Stand 21.09.2026 (A03). §2 und §3 sind gebaut: Die Auflösung steht in
@shared/handlers/einstellungen/schichtung, überlagert pfadweise, liestnullals „zurück auf die Schicht darunter" und gibt die Herkunft je Pfad mit — genau die Karte aus §6. Der Server liefert sie inherkunftan jeder Einstellung. Was noch fehlt: der Schirm dazu (§6, §7), die Geräteschicht als fünfter Beitrag an die Auflösung (sie liegt weiter im Client über dem Serverwert), und das Schreiben je Pfad —setzePfadsteht bereit, die Farbeditoren schreiben noch das ganze Objekt zurück.
Stand 24.09.2026 (A03, Punkt 3, 5 und 6). Der Schirm zu §6 steht, das Schreiben je Pfad auch (§3, „Geschrieben wird je Pfad"), und §1, §5, §7 und §8 sind auf den gebauten Stand gebracht. Offen ist nur noch die Geräteschicht als Beitrag an die Auflösung (A03, Punkt 4).
Hinweis: Nachtrag 17.09.2026 (N76, Grundsatz 72). Dieses Dokument beschreibt die Mechanik. Seit dem 17.09. gibt es dazu eine Pflicht: nichts, was nur im Code zu ändern ist. Ein Wert, den ein Mensch plausibel anders wollen könnte, wird ein Schlüssel hier — keine Konstante in einem Modul. Das Warum steht in philosophie.md §12.
1. Was es schon gibt
personal.preferences (user_id, schluessel varchar(64), wert jsonb)— eine Zeile je Nutzer und Schlüssel.personal.preference_defaults— die globale Vorgabe je Schlüssel.- Die eingebaute Vorgabe in
packages/shared/src/config/preferenceKeys.ts(PREFERENCES), der Server weist unbekannte Schlüssel ab. auth.groups,auth.user_groups— Gruppen existieren, tragen aber keine Einstellungen.auth.feature_grants(was ein Konto darf) undauth.user_feature_settings.enabled(was es will).
Drei Ebenen also — eigen, global, eingebaut — und die Skill- und Interessenfarben liegen in vier eigenen Schlüsseln (skill.colors, skill.icons, interest.colors, interest.icons).
So stand es am 11.09.2026. Heute sind es die sechs Schichten aus §2: Die Gruppentabelle kam mit Migration 33 und ist seit A29, Stufe 1, still; die Rollentabelle kam mit Migration 78 (A09); die Geräteschicht liegt im Browser. Die vier Farbschlüssel sind mit A28 in darstellung aufgegangen (§3).
2. Sechs Schichten, eine Auflösung
6 Gerät localStorage denotato.geraet · die zwölf Schlüssel mit Reichweite Gerät (N259)
5 Nutzer personal.preferences
4 Rolle personal.preference_role_defaults ← A09, 21.09.2026
3 Gruppe personal.preference_group_defaults (still seit A29, Stufe 1)
2 Instanz personal.preference_defaults (Devs/Admins)
1 Eingebaut im Plugin, im QuelltextOben gewinnt. Eine Einstellung wird von unten nach oben aufgelöst; die höchste Schicht, die einen Wert hat, gilt. Neu sind zwei Dinge: die Rolle (weil eine Vorgabe die Frage „wie hätte es jemand gern, der diese Arbeit tut" beantwortet) und das Gerät (weil F16 sagt, das Telefon zeigt an und der PC konfiguriert — dieselbe Person will am Telefon kompakt und am Schreibtisch luftig, und das ist keine Nutzer-, sondern eine Geräteentscheidung).
Die Geräteschicht seit A03, Punkt 4 (24.09.2026, N259). Sie trägt jeden der zwölf Schlüssel, die schichtErlaubt(…, 'geraet') zulässt — bis dahin nur Szene, Dichte, Bewegung und Rütteln, und die legte die Gestaltung selbst über das Konto, während die übrigen sieben die Schicht gar nicht lasen. Im Speicher liegt denotato.geraet als { werte: { <Schlüssel>: <Wert> }, szeneBis }; die alte Form mit festen Feldern (szene, dichte, bewegung, haptik) wird weiter gelesen und beim ersten Schreiben überführt. Was die Prüfung des Schlüssels nicht besteht, wird weder gelesen noch geschrieben. Darübergelegt wird zentral, in wert, herkunft und herkunftPfad des PreferencesContext; die Herkunft heißt dann geraet (schichtung.ts). Setzen geht über useGeraet() (context/Geraetekontext.tsx), auf der Einstellungsseite über den Umschalter „Ändern für" (N260, §6). Die Sprache der Oberfläche hat zwar Reichweite Gerät, bleibt aber, wie sie ist (N40): Sie hat ihren eigenen Weg über die Sprachschicht.
Warum die Rolle über und nicht an Stelle der Gruppe steht. Sie hat die Gruppe abgelöst: Eine Gruppe ist eine Menge Menschen, eine Rolle eine Menge Fähigkeiten, und eine Vorgabe gehört zu Letzterem. Wer heute zwei Menschen dieselbe Vorgabe geben will, muss sie in eine Gruppe stecken — und gibt ihnen nebenbei deren Fähigkeiten. Herausgenommen wird die Gruppenschicht trotzdem erst in einem zweiten Schritt (A29), und bis dahin liegt die Rolle darüber: Läge sie darunter, schlüge eine alte Gruppenzeile eine frisch gesetzte Rollenvorgabe, und ein Verwalter sähe seine Änderung ohne Fehlermeldung nicht wirken. Geschrieben wird seit A09 ohnehin nur noch in die Rolle — auch der alte Endpunkt /api/account/preferences/group tut das und hält die Gruppenzeile nur noch als Spiegel mit.
Seit dem 23.09.2026 ist die Gruppenschicht still (A29, Stufe 1, N155). Die Auflösung liest sie nicht mehr, nichts schreibt mehr hinein, und die Wege group und groups sind fort (N175); die Verwaltung zeigt statt der Gruppen die Rollen. Ein Mitglied erbt die Vorgaben seiner Gruppe über die Rolle zu ihr — jede neue Gruppe bringt diese Rolle beim Anlegen mit, und Wanderung 103 hat sie für die Gruppen nachgeholt, die noch ohne dastanden (N186). Die Tabelle selbst fällt erst mit Stufe 2; bis dahin bleibt gruppe auch in der Aufzählung der Schichten und in den Reichweiten unten stehen, schreiben lässt sich dort nichts mehr.
Nicht jeder Schlüssel erreicht jede Schicht. Jeder Schlüssel trägt seine Reichweite:
| Schlüssel (Beispiel) | darf gesetzt werden von |
|---|---|
plugin.<id>.aktiv | Instanz, Gruppe, Rolle, Nutzer |
theme.szene, dichte, bewegung.stufe, kontrast.stufe | Instanz, Gruppe, Rolle, Nutzer, Gerät |
darstellung | Instanz, Nutzer |
theme.folgtTageszeit, theme.szeneMorgen, …Tag, …Abend, …Nacht (N132) | Instanz, Gruppe, Rolle, Nutzer — nicht Gerät, siehe §4 |
theme.abo (N132) | nur Nutzer |
rang.gezeigt (E22, N353) | nur Nutzer |
leiste.angeheftet (E25, N405) | nur Nutzer |
theme.eigen (F17, N471, erweitert) | Nutzer |
toast.mode, dev.* | Instanz, Nutzer — nur Stufe Entwickler |
Ein Wert in einer Schicht, die der Schlüssel nicht erreicht, wird beim Schreiben abgewiesen, nicht beim Lesen ignoriert.
3. Objekt-Schlüssel werden pfadweise überlagert
Das ist der Kern für „ein JSON je Nutzer, je Wert zurücksetzbar":
// Eingebaut (im Plugin)
{ "bereiche": { "study": { "farbe": "#4d9bff", "zeichen": "kappe", "form": "blatt" } } }
// Nutzer
{ "bereiche": { "study": { "farbe": "#00b8a9" } } }
// Ergebnis
{ "bereiche": { "study": { "farbe": "#00b8a9", "zeichen": "kappe", "form": "blatt" } } }Regeln, drei Stück:
- Objekte verschmelzen pfadweise, Skalare und Listen ersetzen.
nullheißt „zurück auf die Schicht darunter" — nicht „leer". Werbereiche.study.farbe = nullschreibt, sieht wieder die Vorgabe. Das ist das Zurücksetzen je Wert.- Die Zeile löschen heißt alles zurücksetzen.
DELETE FROM personal.preferences WHERE schluessel = 'darstellung'— und alle Bereiche, Skills, Gebiete und Auswahlwerte stehen wieder auf Vorgabe.
Damit wandern skill.colors, skill.icons, interest.colors, interest.icons in einen Schlüssel:
"darstellung": {
"bereiche": { "<plugin-id>": { "farbe", "zeichen", "form" } },
"skills": { "<skill-id>": { "farbe", "zeichen" } },
"gebiete": { "<gebiet-id>": { "farbe", "zeichen" } },
"werte": { "<feld>": { "<wert>": { "farbe", "zeichen" } } } // F18
}Eine Migration, ein Ort, ein Zurücksetzen. Die vier alten Schlüssel bleiben ein Release lang lesbar und werden beim ersten Schreiben übernommen.
Erste Stufe gebaut (A04, 21.09.2026). migration77.sql trägt den Altbestand zusätzlich unter darstellung.skills und darstellung.gebiete ein; die vier alten Zeilen bleiben stehen, damit ein Rücksprung auf den Stand von gestern nichts verliert. Gelesen wird über mitAltbestand (@shared/handlers/einstellungen/darstellungUmzug): der Altbestand liegt unten, darstellung gewinnt Feld für Feld — und ein ausdrücklich gespeichertes null gewinnt auch. Stünde der Altbestand darüber, käme eine zurückgesetzte Skillfarbe wieder hoch, und „zurücksetzen" täte nichts. Geschrieben wird nur noch darstellung. Die zweite Stufe — die vier aus PREFERENCES nehmen, die Zeilen räumen, darstellungUmzug.ts nach archiv/ — ist A28.
Zweite Stufe gebaut (A28, 24.09.2026, N246). Die vier stehen nicht mehr in PREFERENCES; die API weist sie mit setting_unknown_error ab. useSkilldarstellung und useInteressendarstellung lesen nur noch darstellung, mitAltbestand liegt mit darstellungUmzug.ts in archiv/2026-09-24-darstellung-altbestand/, und istGesetzt steht in @shared/handlers/einstellungen/darstellung. migration127.sql wiederholt den Umzug aus 77 und löscht danach die alten Zeilen in allen Schichten — auf dem Server von Hand, nach einem pg_dump.
Geschrieben wird je Pfad (A03, Punkt 3, 24.09.2026). Die Farbeditoren bauten ihren Schreibwert bis dahin aus dem wirksamen Wert, also aus allen Schichten übereinander, und schrieben ihn als Ganzes in die eigene Schicht. Wer eine Skillfarbe setzte, übernahm so jede Farbe aus Rolle und Instanz als eigene, und spätere Vorgaben kamen bei ihm nie mehr an; wer eine Vorgabe für alle setzte, trug seine eigenen Farben mit in die Instanz. Jetzt schreibt schreibeInSchicht (schichtung.ts) auf dem Wert einer Schicht — aus layers der Antwort, nutzer oder instanz —, und null löscht dort die Stelle samt leerer Fächer, statt ein null stehen zu lassen. Innerhalb einer Schicht ist beides gleich viel wert (Regel 2), aber ein stehengebliebenes null unter einem Fach, das darunter fehlt, hätte das ganze Fach zur eigenen Herkunft gemacht. Bleibt nichts übrig, wird die Zeile gelöscht (Regel 3).
Dazu gehört eine Regel der Auflösung, die bis dahin fehlte: Fehlt ein Fach darunter ganz, wird trotzdem pfadweise überlagert, als läge dort ein leeres. Die eingebaute Darstellung ist {}; ohne diese Regel stand eine einzige eigene Skillfarbe als skills: nutzer in der Karte, und „zurücksetzen" stand an jedem Skill. Ersetzt wird nur, wo darunter wirklich etwas anderes als ein Fach liegt (Regel 1).
4. Das Theme ist ein Wert, nicht zwanzig
F17: immer als Ganzes. Also ein Schlüssel theme.szene mit einem Namen (nacht, sonnentag, daemmerung, bewoelkt, gewitter, aurora, wueste, …) — jede Szene ist im Quelltext ein vollständiger Token-Satz. Kein Nutzer stellt eine einzelne Farbe des Themes um.
Wer doch will (Erweitert): theme.eigen ist eine eigene Szene, abgeleitet von einer der zwölf (N471) — {"eltern": "nacht", "grundton": "#2a3b5c", "himmel": {"abend": ["#…", "#…", "#…"]}}. Die Elternszene gibt Basis, Atmosphäre und Tönung; grundton und die Himmelstöne je Tageszeit sind wahlweise, null heißt „wie die Elternszene“. Die Szene wird daraus gerechnet (theme/eigen.ts), und was die Schwellen aus §17 des Guides reißen würde, wird gedämpft statt abgewiesen. theme.szene = "eigen" wählt sie. Auch das ist ein Ganzes: der Schlüssel trägt alle drei Felder auf einmal, und die Nutzerschicht ist die einzige.
Dazu: theme.folgtWetter (F21, Vorgabe aus), theme.himmel (F9: aus | kopfband | ueberall), theme.metall liest sich nicht aus einer Einstellung, sondern aus der Mitgliedschaft (Frei → Violett, Premium → Gold, N31/F6) — was ein Konto ist, ist keine Präferenz.
Seit N132 (22.09.2026) wählt die Szene sich nach der Tageszeit. theme.folgtTageszeit (Vorgabe an) schaltet auf vier Schlüssel um — theme.szeneMorgen (Vorgabe daemmerung), theme.szeneTag (sonnentag), theme.szeneAbend (herbstabend), theme.szeneNacht (nacht); theme.szene gilt dann nicht. Die Grenzen kommen aus theme/tageszeit.ts: nach Sonnenauf- und -untergang am eigenen Ort, ohne Ort nach der Uhr (5, 10, 17, 21 Uhr). Welche Szene gezeichnet wird, entscheidet in dieser Reihenfolge:
- eine Szene am Gerät ohne Ablauf (
geraet.szene,szeneBis: null) — die Geräteschicht aus §2, wie bisher über allem; - eine Handwahl (
geraet.szenemitszeneBis) — gesetzt von der Leiste oder der Schnellsuche, solange die Tageszeit gilt, und gültig bis zum nächsten Abschnittswechsel (Christians Entscheidung); - mit Tageszeit die Szene des laufenden Abschnitts, ohne sie
theme.szene.
Die vier Abschnittsschlüssel reichen deshalb nicht bis zum Gerät: Die Geräteschicht der Szene ist eine einzige, und vier weitere Gerätewerte daneben wären eine Schicht, die niemand setzen kann. Eine abgelaufene Handwahl gilt ab ihrem Ablauf nicht mehr, und die Gestaltung löscht sie dann auch aus dem Speicher des Geräts (Gestaltung.tsx) — damit nach einem Neuladen am nächsten Tag keine Zeile stehen bleibt, die niemand mehr meint.
Das Wetter ersetzt die Szene nicht mehr, es legt sich darauf (N132). theme.folgtWetter heißt am Schirm „Wetter auf der Szene" und wirkt als Filter auf Himmel und Grund (theme/wetterszene.ts); die Neutralen und der Text bleiben die der Szene.
theme.abo (N132) — auto oder eine Stufe (frei, premium, admin, dev). Die Stufe ist Knopf und Anwendungsfarbe. Nur der Nutzer setzt sie; geklemmt wird beim Zeichnen, nicht beim Schreiben: Eine Wahl über der eigenen Stufe — etwa nach einem entzogenen Recht — fällt auf die eigene zurück. Beim Impersonieren gilt immer auto. Was ein Konto ist, bleibt damit keine Präferenz; nur, welche der darunterliegenden Stufen es zeigt.
rang.gezeigt (E22, N353) — der Schlüssel der Rolle, deren Rang am Namen steht, oder leer. Leer heißt: der erste in der Reihenfolge der Materialien (ordneRaenge), denn wann ein Rang vergeben wurde, weiß die Datenbank nicht. Wie beim Abo-Theme ist die Wahl keine Aussage darüber, was ein Konto ist: Die Ränge kommen aus den Rollen, und ein gewählter Rang, den das Konto nicht mehr trägt, fällt beim Zeichnen auf den ersten zurück.
leiste.angeheftet (E25, N405) — die Seiten, die der Stern in der Adresszeile angeheftet hat: eine Liste aus Weg (Adresse samt Suchteil) und Namen, vorgegeben leer. Christian wählte am 28.09.2026, dass sie am Konto liegt und nicht im Browser — angeheftet ist auf jedem Gerät angeheftet. Die Prüfung ist dieselbe beim Lesen und beim Schreiben (istAngeheftetliste): nur Wege innerhalb von Denotato (ein / am Anfang, kein //, kein Rückstrich, also auch kein Schema), jeder Weg einmal, ein Name mit Inhalt, Weg und Name je höchstens 300 Zeichen, höchstens ANGEHEFTET_HOECHSTENS (zwölf) Einträge. Die Grenze ist fest, weil die Schnellsuche die ganze Liste über allen anderen Treffern zeigt; mehr wäre keine Abkürzung mehr.
theme.stilpaket (E-261016-admin-schalter, 17.10.2026) — das Stilpaket, ein Name aus STILPAKETE in shared/src/config/gestaltung.ts, eingebaute Vorgabe legacy; das Warum steht in stilpakete-architektur.md §3. Die Reichweite ist Instanz, Rolle, Nutzer und Gerät, aber jede mit eigener Grenze: Instanz und Rolle setzt die Verwaltung im Abschnitt „Gestaltung“ (setzeGlobal, setzeRolle), mit jedem gebauten Paket. Die Nutzerschicht nimmt nur klar, ruhig und marker an (pruefeNutzer); legacy und das Prüfpaket weist der Server dort mit SETTING_RESERVED_ERROR ab. Die Geräteschicht schreiben nur /ansehen --pakete und die Vorschau der Verwaltung; die Einstellungsseite bietet den Schlüssel unter „nur dieses Gerät“ nicht an (amGeraetEinstellbar). Ergibt die Vorgabe aus eingebaut, Instanz und Rolle legacy, überstimmt sie die Kontowahl (vorgabeUeberstimmt): So schaltet die Verwaltung jederzeit alle auf Legacy zurück. Die Geräteschicht liegt weiter über allem.
5. Plugins bringen ihre Einstellungen mit
Ein Plugin ist ein Verzeichnis mit einer Deklaration:
export const plugin: Plugin = {
id: 'study',
ort: 'lesesaal', // F20: der Raum in der Sternwarte
darstellung: { farbe: '#4d9bff', zeichen: 'kappe', form: 'blatt' },
lautstaerke: 3, // F8/F23
navigation: { label: 'Studium', to: '/study' },
schluessel: [ /* die Einstellungen, die es kennt, mit Schema und Reichweite */ ],
aktivVorgabe: true, // Instanz kann das kippen
};Der Kern kennt kein Plugin beim Namen. plugin.<id>.aktiv wird wie jede Einstellung aufgelöst (eingebaut → Instanz → Rolle → Nutzer; die Gruppe ist still, und ein Gerät erreicht der Schlüssel nicht) und mit dem Grant verschnitten: aktiv = gewährt und eingeschaltet. Ist ein Plugin nicht aktiv, existieren seine Schlüssel, seine Navigation, seine Farbe und seine Kacheln nicht — nicht ausgegraut, nicht da.
Gebaut ist das am 27.09.2026 für die Bereiche ohne Merkmal (N363, D33): plugin.schlaf.aktiv, plugin.rueckblick.aktiv und seit NB05 plugin.neuigkeiten.aktiv stehen in PREFERENCES, eingebaut mit der aktivVorgabe der Deklaration, und pluginschalter in plugins/src/rights/server/pluginschalter.ts fragt sie auf dem Server (aus = 403). Ohne Merkmal gibt es keinen Grant, mit dem sie sich verschneiden; aktiv heißt dort allein „eingeschaltet". Die Bereiche mit Merkmal schalten weiter über enabledFeatures (Grundsatz 23); ein plugin.<id>.aktiv für sie gibt es nicht.
Das ist der Teil von Christians Philosophie („fast alles theoretisch verzichtbar"), der Architektur ist und nicht Geschmack: Wenn Bereich seine Farbe aus dem Plugin-Register holt und nicht aus dem Theme, dann bricht nichts, wenn ein Plugin fehlt.
6. Einmal auflösen, Herkunft mitgeben
Der Server löst bei jedem Lesen einmal alle Schlüssel auf (leseEinstellungen in preferencesController.ts, über loeseAuf) und liefert unter /api/account/preferences (PREFERENCES) eine Liste, je Schlüssel eine Zeile mit Wert und Herkunft:
[
{ "key": "dichte", "value": "kompakt", "origin": "global",
"fallback": "kompakt", "herkunft": { "*": "instanz" }, "layers": {…} },
{ "key": "table.editor", "value": "popup", "origin": "rolle", "role": "Tutoren",
"fallback": "popup", "herkunft": { "*": "rolle" }, "layers": {…} },
{ "key": "darstellung", "value": {…}, "origin": "eigen", "fallback": {…},
"herkunft": { "bereiche.study.farbe": "nutzer", "*": "eingebaut" }, "layers": {…} }
]originist die grobe Auskunft: die höchste Schicht irgendwo im Schlüssel, in den Namen der Oberfläche —eigen,rolle,global,eingebaut. Beirollesteht der Name der Rolle inrole.herkunftist die feine: eine spärliche Karte Pfad → Schicht mit'*'für alles Übrige (§3), in den Namen der Schichten. Übersetzt wird an einer Stelle,HERKUNFT_VON_SCHICHTinschichtung.ts— seit A03, Punkt 1, liest auch der Browser sie.fallbackist, was nach einem Zurücksetzen gälte: der Wert ohne die eigene Schicht.layersnennt die Werte je Schicht für die Verwaltung.
Einen Zwischenspeicher gibt es nicht. Hier stand bis zum 24.09.2026, das aufgelöste Objekt liege im Sitzungsspeicher des Servers und werde bei jedem Schreiben ungültig; einen solchen Speicher gab es nie (A03, Vermessung (d), Punkt 3). Jedes Lesen stellt drei Abfragen — Nutzer, Instanz, Rolle; bis A29, Stufe 1, waren es mit der Gruppe vier. Wer nach Grundsatz 68 misst, misst diese drei. Die Geräteschicht löst der Client selbst darüber — sie erreicht den Server nie.
Was die Einstellungsseite daraus macht (A03, Punkt 2; N196)
Die Herkunft ist kein Luxus: Ohne sie rät die Oberfläche, und der Nutzer weiß nicht, warum etwas so aussieht. Christian hat am 24.09.2026 die Form gewählt (N196, „Nur Abweichungen"): Eine Zeile ohne Zusatz steht auf der eingebauten Vorgabe. Sonst steht leise neben der Beschriftung, wer den Wert gesetzt hat.
| Wer an dieser Stelle setzt | Was neben der Beschriftung steht |
|---|---|
| niemand — eingebaute Vorgabe | nichts |
| die Instanz | „Vorgabe für alle" |
| eine Rolle | „von Rolle Tutoren", ohne Namen „von einer Rolle" |
| das Konto | „von dir · zurücksetzen" — nur hier ein Knopf |
| dieses Gerät | „dieses Gerät · zurücksetzen" — der Knopf nimmt nur den Gerätewert zurück; steht vor „von dir", weil der Gerätewert der ist, der gilt (N260). Bis zum 24.09.2026 stand bei Dichte, Bewegung und Rütteln stattdessen eine eigene Marke unter der Knopfreihe |
„Ändern für" (N260). Unter dem Umfang steht „Ändern für: mein Konto | nur dieses Gerät". Auf dem Konto schreibt jede Zeile ins Konto und nimmt einen Gerätewert desselben Schlüssels mit. Auf dem Gerät bleiben nur die Zeilen, deren Schlüssel das Gerät erreicht, und sie schreiben nach denotato.geraet; eine Vorgabe für alle gibt es dort nicht. Beim Öffnen steht der Umschalter immer auf dem Konto. Einzelheiten: docs/seiten/settings.md.
Die Schicht zählt, nicht der Wert. Setzt die Instanz denselben Wert wie die eingebaute Vorgabe, steht trotzdem „Vorgabe für alle": Sie hält ihn fest, auch wenn sich die eingebaute Vorgabe einmal ändert. Und wer selbst den Wert der Vorgabe wählt, hat etwas gewählt — „zurücksetzen" heißt dann: wieder der Vorgabe folgen. Dieselbe Zählung steht schon unter den Kategorien: „13 Einstellungen weichen von der Vorgabe ab" zählt die Schlüssel mit eigener Schicht, nicht die mit anderem Wert (Zuruecksetzen.tsx).
„zurücksetzen" steht genau dann da, wenn an dieser Stelle etwas Eigenes steht — eigenGesetzt(schluessel, pfad) im Kontext, dieselbe Frage wie zuruecksetzbar(karte, 'nutzer', pfad) in schichtung.ts. Nicht schon, wenn origin „eigen" sagt: Bei darstellung hieße das „eigen" für jeden Bereich, sobald eine einzige Farbe gewählt ist. Der Knopf nimmt genau diesen einen Wert zurück (setze(schluessel, null)); eine Vorgabe der Rolle oder der Instanz nimmt nur die Verwaltung zurück.
Wie sie aussieht. Die Marke ist xs, normal gewichtet, in Nebentextfarbe (foreground.tertiary, 3:1, Guide §17) — leiser als die Beschriftung, neben der sie steht. „zurücksetzen" ist ein Knopf, der wie ein Wort aussieht, und trägt deshalb, was jeder Textknopf dieser Seite trägt („Mehr zeigen" unter den Gruppen): die Anwendungsfarbe (Guide §3.1 — Knöpfe und Links zeigen, was Denotato tut) und eine Unterstreichung, die immer steht, weil es am Telefon kein Überfahren gibt. Leise bleibt die Marke trotzdem, weil nur dieses eine Wort Farbe hat. Der Vorleser hört den Namen der Zeile mit — „Inhaltsbreite zurücksetzen" —, und der Name enthält, was dasteht (WCAG 2.5.3). Gebaut ist das in settings/client/Herkunftsmarke.tsx; MitHerkunft hängt die Marke an eine Beschriftung, deren Text zugleich der Name des Knopfes ist, damit beide nicht auseinanderlaufen.
Wo die Beschriftung selbst klein und leise ist — über den Feldern von Zeit und Anfang —, sehen Beschriftung und Marke gleich aus: „Uhrzeit · von dir · zurücksetzen". Getrennt sind sie dort durch den Punkt und, bei „von dir", durch das farbige „zurücksetzen". Eine eigene Form für diesen Fall, etwa kursiv, hat die Marke bewusst nicht: Sie sähe sonst an zwei Stellen der Seite verschieden aus (Sichtprüfer, 24.09.2026).
Ein Wort, kein Zeichen (N210). N134 gibt jedem Merkmal Zeichen und Farbe zusätzlich zum Wort. Die Marke bekommt keines, und das ist entschieden, keine Lücke. Was N134 schützt, ist erfüllt: Sie sagt alles im Wort, und Farbe trägt nichts allein. Sie ist auch kein Merkmal, das man in einer Liste überfliegt, sondern ein leiser Nachsatz (N196). Und ein Zeichen kostete Breite: Auf 360 Punkten brächen mehr Beschriftungen um, und der frei gehaltene Platz aus A40 (N207) würde größer.
Die Trefferfläche wächst, die Zeile nicht. Unter pointer: coarse ist „zurücksetzen" 40 Punkte hoch (§16.1), mit negativen Rändern, die dem Kasten genau das nehmen, was er über eine Textzeile (1lh) hinaus hat. Die Zeile bleibt so hoch wie ohne Marke; die Fläche reicht über sie hinaus, und wo sie auf etwas trifft, das später im Dokument steht — die Knopfreihe darunter —, liegt das darüber und bekommt den Tipp. Ein unsichtbares ::after mit position: absolute wäre der bekanntere Weg und hier der falsche: Es läge über allem, was keine eigene Stellung hat, und nähme der Knopfreihe darunter ihre Oberkante. Die Gerätemarke hat dieselbe Fläche; sichtbar ist dort nur der Rahmen darin. Im ersten Bau vom 24.09.2026 wuchs die Zeile mit — die Knopfreihe sprang beim Wählen um 23 Punkte, unter dem Daumen weg (docs/kontext/fallstricke.md).
Auf etwas Früheres darf die Fläche nicht reichen, denn dort läge sie selbst oben. Eine Marke auf eigener Zeile hält deshalb unter pointer: coarse Abstand zur Reihe darüber, genau so viel, wie die Fläche über die Zeile hinausreicht (calc((40px - 1lh) / 2), rund elf Punkte). Mit nur xs lag die Fläche 7,5 Punkte über der Unterkante der Knöpfe darüber: Wer „Kalender" an der Unterkante traf, setzte „Wo Denotato aufgeht" zurück, ebenso unter der Sprache, der festen Szene und, um 3 Punkte, unter den Abo-Mustern. Die Gerätemarke hält denselben Abstand auch nach unten, als Innenrand, weil ein Außenrand mit dem der nächsten Zeile zusammenfiele: Unter ihr kann eine Beschriftung mit „zurücksetzen" stehen, dessen Fläche nach oben reicht. Hinter einer Beschriftung genügt der Abstand, den die Seite über jeder Beschriftung lässt. Gemessen am 24.09.2026 mit elementFromPoint, alle Marken gesetzt, auch mit fester Szene und Werten am Gerät, auf 390, 375, 360 und 834 Punkten Breite: Keine Fläche liegt über etwas Früherem. Wer eine Marke enger an etwas Bedienbares darüber rückt, misst das neu.
Wo sie steht. Zwei Stellungen, als Eigenschaft stellung:
hinter(Vorgabe): hinter einer Beschriftung, im selben Satz, mit einem „·" davor. Ohne Trenner las sich „Wo eine Tabellenzeile bearbeitet wird von Rolle Tutoren" als ein Satz. Der Punkt ist für den Vorleser verborgen; die Leerzeichen darum sind echt, sonst läse er „Tabletvon dir".zeile: eine eigene Zeile, ohne Trenner — unter einer Knopfreihe oder Liste ohne Beschriftung (Sprache, feste Szene, Wo Denotato aufgeht, die Haken des Tagesbriefings), unter den Abo-Mustern und unter der Auswahl jedes Szenenabschnitts. Hinter dem letzten Knopf gehörte sie sichtbar zu ihm: „Material · von dir", während „Aufgaben" gewählt war.
Was beim Wählen erscheint, erscheint unter der Reihe, die man antippt, nicht darüber. Die Szenenmarke stand bis zum 24.09.2026 neben dem Beginn; am Telefon brach sie unter die Uhrzeit um, also zwischen Beschriftung und Auswahl, und die eben angetippte Auswahl rutschte 16 Punkte nach unten. Unter der Uhrzeit las sich „von dir" außerdem wie deren Herkunft — die rechnet Denotato aus dem Sonnenstand. Die Gerätemarke stand hinter der Beschriftung; beim ersten Tipp verschwand sie, weil jeder Knopf der Reihe den Gerätewert zurücknimmt, die zweizeilige Beschriftung schrumpfte, und die Knopfreihe sprang nach oben. Beide stehen jetzt unter ihrer Reihe; beim Erscheinen und Verschwinden rücken nur die Zeilen darunter.
Eine Beschriftung hält den Platz für ihre Marke frei (N207, A40). Die Marke erscheint mit dem ersten eigenen Wert, also genau dann, wenn der Daumen auf der Knopfreihe darunter liegt. Wo sie die Beschriftung zweizeilig machte, sprang die Reihe nach unten, unter dem Daumen weg — auf 360 Punkten Breite, der Breite vieler Android-Telefone, bei Zeilennummern im Dokumenteneditor und Für wen die Meldungen geschrieben sind um 17 Punkte, bei Kurzes Rütteln bei Rückmeldungen um 20, dort auch auf 375. Christian hat entschieden, dass die Beschriftung den Platz für „· von dir · zurücksetzen" von Anfang an unsichtbar frei hält, nur in der Stellung hinter. Wo alles in eine Zeile passt, ändert sich nichts; wo nicht, steht ohne eigenen Wert eine leere zweite Zeile unter der Beschriftung, und die Reihe steht in jedem Zustand an derselben Stelle.
So ist es gebaut (Herkunftsmarke.tsx, Stapel):
- Der Trenner „ · " bleibt Text im Fluss der Beschriftung. Dort darf die Zeile umbrechen, und der Punkt bleibt am Ende der ersten Zeile.
- Dahinter steht ein Stapel,
display: inline-grid. Die sichtbare Marke und ein unsichtbarer Platzhalter teilen dieselbe Zelle (grid-area: 1 / 1); der Stapel ist so breit wie das breitere von beiden und bricht als Ganzes um.max-width: 100%lässt einen Rollennamen, der länger ist als die Zeile, im Stapel umbrechen. - Der Platzhalter ist erzeugter Text,
::beforemitcontent: attr(data-platz), kein Knoten. Als Knoten stünde „von dir" an jeder Beschriftung — für die Suche im Browser, fürs Kopieren und für jeden, dertextContentliest.visibility: hiddennimmt ihn aus dem Bild, dem Baum der Barrierefreiheit und auselementFromPoint; dazu kommt ein leerer Ersatztext (content: … / ''). Vorgelesen wird, was N196 sagt, nicht mehr. - Ein Punkt Zugabe (
padding-inline-end: 1px): Der Platzhalter ist ein Text am Stück, die sichtbare Marke drei Kästen — Wort, Punkt, Knopf. An den Grenzen kann die Unterschneidung sie um Bruchteile breiter machen, und genau auf dieser Breite bräche sie anders um. Mit der Zugabe ist der Platzhalter immer der breitere. - Ist nichts zu sagen, steht eine stille Marke da: ganz unsichtbar,
aria-hidden, der Trenner ebenfalls erzeugter Text, im Stapel nur der Platzhalter. Im Baum steht dann kein einziges Wort. stummsagt nichts und hält den Platz — für Dichte, Bewegung und Rütteln, solange ihr Wert am Gerät steht. Jeder Knopf der Reihe nimmt den Gerätewert zurück, und die Marke, die dann erscheint, schiebt die Reihe nicht.- Der Text des Platzhalters kommt aus
t(), er ist also in jeder Sprache der, den das Konto dort selbst bekommen kann.
Gemessen am 24.09.2026 mit elementFromPoint und den Höhen aller fünfzehn Beschriftungen, alle Marken gesetzt und wieder zurückgenommen, auf 360, 375, 390, 834 und 1440 Punkten: Keine Beschriftung ändert ihre Höhe, und keine Fläche liegt über etwas Früherem. Grenze, die bleibt: Steht eine Rolle mit einem Namen da, der länger ist als der Platzhalter, wird die Beschriftung kürzer, wenn das Konto selbst wählt — der Platzhalter kennt nur den eigenen Text. Wer eine Beschriftung mit Marke in eine Spalte stellt, deren Breite vom Inhalt abhängt (etwa max-content), bekommt den Platz dort mit: Die Spalte wird so breit, als stünde die Marke schon da.
Eine dritte Stellung, frei in einem Behälter mit eigenem Abstand, gab es bis zum 24.09.2026. Ihre beiden Stellen, die Szenenliste und die Hinweiszeile unter den Abo-Mustern, stehen jetzt als Zeile.
Wo eine Beschriftung über einer Auswahl steht (Zeit und Anfang), stehen die Felder untereinander. Nebeneinander zog der Kopf mit Marke die Auswahl auf seine Breite und schob das Nachbarfeld am Telefon in eine eigene Zeile, näher an das Feld darüber als an seine eigene Auswahl.
Ausnahmen.
- Die Stufenwahl oben auf der Seite trägt keine Marke. Sie steht über den Kategorien und entscheidet, was sie enthalten; ein „zurücksetzen" dort ließe Zeilen verschwinden, während man auf sie schaut.
- Zeilen, die einen Pfad in einem Objekt zeigen — die Farben und Zeichen der Bereiche, Skills und Gebiete unter
darstellung— bekommen ihre Marke erst mit A03, Punkt 3: Solange die Farbeditoren das ganze Objekt zurückschreiben, stünde an jeder Stelle „von dir". - Die Handwahl der Szene bleibt ihr eigener Knopf „Zurücknehmen" (N132): Sie ist die Geräteschicht von
theme.szeneund sagt das schon. - Die Oberflächensprache trägt die Marke der Serverschichten. Die Sprache am Gerät gilt nur, solange das Konto nichts sagt (N40), und bekommt deshalb kein eigenes Wort.
Bis zum 24.09.2026 stand an genau zwei Zeilen (Inhaltsbreite, Zeilennummern) das Wort „Vorgabe", und hier stand, neben jedem Wert stehe ein Wort. Beides ist mit N196 fort.
7. Stufen und Barrierefreiheit
einstellungen.stufe:einfach | erweitert | entwickler. Jeder Schlüssel trägt seine Mindeststufe. Einfach zeigt etwa fünfzehn Dinge: Szene, Dichte, Serif, Bewegung, Sprache-Add-on, Bereiche an/aus. Der Schalter steht oben auf der Einstellungsseite (F28).bewegung.stufe:voll | mittel | wenig | aus. F24 unterscheidet drei Anlässe (Laden stark, Menüwechsel mittel, Interaktion wenig); die Stufe skaliert alle drei.prefers-reduced-motionergibtaus, solange auf diesem Gerät nichts anderes gewählt ist. Es setzt dabei nichts: Es ist ein Rückfall in einem Ausdruck (bewegungWirksamintheme/Gestaltung.tsx), der den Nutzerwert schlägt, gegen einen Wert indenotato.geraetverliert und bei jedem Laden neu entsteht. Die Barrierefreiheits-Einstellung ist also keine eigene, sondern wirkt wie die Geräteschicht dieses Schlüssels — eine Schicht mit Namen wird sie mit A03, Punkt 4.kontrast.stufe:system | normal | mehr, Vorgabesystem(RL19, N431), beim Nutzer und am Gerät.systemfolgtprefers-contrast: moreund hört auf den Wechsel mitten in der Sitzung;mehrsetzttheme.kontrast(Style-Guide §17).haptik:an | aus, Geräteschicht, nur auf Geräten mit Vibration sichtbar (F26).
8. Was das an Arbeit ist
Die Schätzung vom 11.09.2026. Gebaut wurden es zwei Tabellen statt einer — die Gruppentabelle (Migration 33) und die Rollentabelle (Migration 78, A09), von denen nur noch die zweite gelesen wird —, und der Endpunkt heißt /api/account/preferences. Der Rest steht wie geplant:
Eine Tabelle (preference_group_defaults), eine pfadweise Auflösung mit null-Regel (~80 Zeilen, mit Nachweis in verify:einstellungen), die Herkunft in der Antwort von /api/preferences, eine Migration der vier Darstellungsschlüssel, ein Plugin-Register mit Deklarationen für die elf Bereiche, und die Einstellungsseite mit Stufe und Herkunft. Mittel bis groß — aber es ist die Grundlage, auf der Bereichsfarben, Szenen, Zimmer und Plugins alle stehen. Wer die Farben vorher baut, baut sie zweimal.
9. Zahlen, die je Instanz gelten — die Instanzschicht (A15, 21.09.2026)
Die sechs Schichten oben beantworten „wie hätte dieser Mensch es gern". Für einen Teil der Zahlen ist das die falsche Frage. Wie viele Fehlversuche eine Adresse hat, bevor die Bremse greift, entscheidet niemand für sich selbst — das entscheidet der, der die Instanz betreibt, und es gilt für alle.
Solche Zahlen standen bis zum 21.09.2026 als Zahlen im Quelltext. Ändern hieß bauen und neu starten, und das ist genau der Zustand, den Grundsatz 72 nicht will.
Drei Teile, und jeder an seinem Ort:
@shared/config/instanzwerte.ts die Akte — reine Daten
@shared/handlers/einstellungen/instanzschicht.ts die Auflösung — synchron
packages/backend/src/config/instanzquelle.ts die Quelle — admin.instance_valuesDie Akte beschreibt jede Zahl vollständig: Vorgabe, Einheit (anzahl | faktor | sekunden | minuten), kleinster und größter erlaubter Wert, der Zweck in einem Satz, der Ort, an dem sie gelesen wird, und — wenn es einen gibt — der Name des Umgebungsschalters. Sie enthält keinen Zugriff auf irgendetwas; deshalb darf sie in shared liegen und von jedem Paket gelesen werden.
Die Auflösung ist die kurze:
3 Umgebung AUTH_… — die Notbremse des Betreibers
2 Instanz admin.instance_values
1 Vorgabe die Zahl in der AkteWarum die Umgebung hier oben steht und nicht wie sonst unten: Sie ist der Griff, den jemand anfasst, wenn die Anwendung sich falsch verhält. Ließe sich ein gesetzter Schalter aus der Datenbank heraus überstimmen, wäre er als Notbremse nichts wert — wer die Datenbank hat, hat dann auch die Bremse.
Warum es eine eigene Schicht ist und nicht die Instanzschicht aus §2. Eine Bremse wird innerhalb einer Anfrage gelesen, oft mehrmals, und sie darf dafür nicht warten. instanzwert() gibt eine Zahl zurück, kein Versprechen. Möglich ist das, weil die Werte in einem Zwischenspeicher liegen, den die Quelle im Takt (instanz.auffrischung, Vorgabe 60 Sekunden) neu füllt. Der Preis steht ehrlich dabei: eine Änderung wirkt bis zu eine Minute später, nicht sofort. Für Bremswerte ist das richtig herum — sie sollen nicht auf die Millisekunde stimmen, sondern ohne Neustart änderbar sein.
Ohne Quelle ändert sich nichts. Hängt niemand eine Quelle ein — im Test, in einem Skript, in einem Paket ohne Datenbank —, gilt bitgenau die Vorgabe aus der Akte, also das, was vorher als Zahl im Code stand. Die Schicht ist nachrüstbar, nicht einschaltpflichtig.
Was der Nachweis dazu tut. npm run verify:instanzwerte wird rot, sobald eine dieser Zahlen wieder als Zahl an ihrem Ort auftaucht, sobald ein Schlüssel woanders gelesen wird als dort, wo die Akte ihn verortet, oder sobald der Ort selbst wieder process.env anfasst. Und er hält sich an die Rechnung aus A15: Er verlässt sich auf keine Vorgabe, sondern setzt den Zustand selbst — eigene Quelle, eine Zeile in der Tabelle, auffrischen, nachrechnen, abräumen.
Was fehlt. Ein Schirm. Geändert wird heute in der Tabelle, mit dem Besitzer der Datenbank: Die Anwendung (denotato_app) darf sie seit Wanderung 141 nur lesen, und verify:adminrechte hält das fest (F96). Ein Schirm dafür braucht also zuerst eine Entscheidung, wer dort schreibt. Die Akte trägt für einen Admin-Schirm schon alles, was er anzeigen müsste — Zweck, Einheit, Spanne —, und die Spanne ist keine Zierde: pruefeInstanzwert() weist ab, was außerhalb liegt, statt es stillschweigend zurechtzubiegen.
10. Anordnungen und Räume — die Form aus OE03 und ihre ersten Schlüssel (NV08 Schnitt 2, 28.09.2026)
Wo ein Admin oder ein Konto eine Reihe ordnen und Einzelnes ausblenden darf, gilt seit dem 28.09.2026 eine Form (OE03, vorgezogen für die Arbeitsräume): @shared/handlers/einstellungen/anordnung.ts.
{ "folge": ["kennung", "…"], "ausgeblendet": ["kennung", "…"] } // beides darf fehlenangeordnet(eingebaut, kennung, anordnung) setzt vier Regeln durch: leer heißt „wie eingebaut"; die Folge ordnet, was sie nennt (ein Schirm schreibt sie vollständig, wie zusammengefuehrt() für die Kacheln); ein neues Element steht an seinem eingebauten Platz hinter seinem eingebauten Vorgänger, nicht am Ende; eine unbekannte Kennung wird übergangen, nicht gelöscht — kommt der Bereich zurück, steht er wieder an seinem Platz. Ausgeblendet sticht die Folge. Was hereinkommt, entscheiden Rechte und Schalter vorher; die Anordnung gewährt nichts (N149). istAnordnung prüft die Form beim Speichern, alsAnordnung liest nachsichtig. dashboard.kacheln läuft noch über geordnet() im Frontend; der Umzug auf diese Form bleibt bei OE03.
Die ersten drei Schlüssel darauf gehören zu den Arbeitsräumen (NV06, N388, N390; Räume selbst in @shared/config/raeume.ts, Vorgabe der Zuordnung als Feld raeume jeder Deklaration, Auflösung in frontend/src/bereiche/raeume.ts):
| Schlüssel | Reichweite | Eingebaut | Inhalt |
|---|---|---|---|
raeume | Instanz, Nutzer | {} | folge/ausgeblendet der Räume; titel und zeichen je Raum; bereiche.<raum> als Anordnung der Bereiche und Werkzeuge; aktionen.<raum> als Anordnung der Schnellaktionen in der unteren Reihe am Telefon (N419; Vorgabe je Aktion in schnellaktionen/aktionen.ts, Ausgeblendetes wandert hinter den Raumknopf, nichts verschwindet); ein Raum in folge ohne Deklaration wird zum eigenen Raum, sobald titel ihn benennt — außer seine Kennung heißt wie ein Weg (BELEGTE_WEGSTUECKE; so ein Titel wird abgewiesen, ein gespeicherter Raum übergangen, N461) |
raum | Instanz, Nutzer | studium | in der Instanz der Raum für neue Konten, beim Nutzer der zuletzt benutzte — die gewöhnliche Auflösung ergibt „zuletzt benutzter, sonst Vorgabe des Admins" ohne eigenen Merker |
verwandt | Instanz, Nutzer | {} | je Bereich oder Werkzeug eine Anordnung (oder null) der Nachbarn im Abschnitt „Verwandt“ am Schirmende (NV07, N460, N467); die Vorgabe stammt aus dem Feld verwandt der Deklarationen, gelesen in beide Richtungen; istAnordnungJeKennung prüft beim Speichern; gestellt später im Oberflächeneditor (OE01) |
oberflaeche.reiter | Instanz | {} | je Folge von Reitern eine Anordnung (oder null): die Abschnitte unter der Kennung des Schirms (study), die Ansichten eines Abschnitts unter <schirm>/<abschnitt> (OE10, N500); unter seitenleiste die festen Plätze der Seitenleiste (NV-261007-seitenleiste, Vorgabe in bereiche/festePlaetze.ts), unter leiste/konto die Einträge des Kontomenüs und unter leiste/adresse die Knöpfe links vom Feld der Adresszeile (OE06; Abmelden und Einstellungen tragen ein Schloss im Code und bleiben sichtbar); fehlt eine Folge, gilt die eingebaute; gestellt im Schirm selbst über „Bearbeiten“ im Kopf, die drei Folgen der Leiste über „Leiste bearbeiten“ im Kontomenü, als Entwurf mit settings.global_default (N398); istAnordnungJeKennung prüft beim Speichern |
raeume ist ein Objekt und wird pfadweise überlagert (§3): Wer einen Raum umbenennt, erbt Folge und Zuordnung von der Schicht darunter. Ein Raum ohne sichtbare Mitglieder erscheint nicht; ein Bereich, den das Konto nicht hat, steht in keinem Raum, auch wenn bereiche.<raum>.folge ihn nennt (Nachweis raeume.test.ts, „ein Raum gewährt kein Recht"). Gezeichnet ist das seit Schnitt 3 (28.09.2026): Die Leiste zeigt einen Raum mit Umschalter, die Adresse trägt ihn seit NV10 als Präfix (/alltag/tasks), und die Leiste schreibt raum, sobald eine Adresse einen anderen Raum nennt als gespeichert (00-rahmen.md §1, „Welcher Raum gezeigt wird"). Gestellt wird raeume in der Kategorie „Bereiche" der Einstellungen (NV05, noch nicht gebaut) und später im Oberflächeneditor (OE01).