docs: T5 Optik in ARCHITECTURE §15, Annahmen 119–124, TASKS, Spec-Abweichungen (T5b B8)

- ARCHITECTURE: neuer Abschnitt 15 (Tokens und vier Farbsätze,
  Darstellung, Baukasten und Wächter, Bewegung und Budgets, Beute in Sonne,
  Wabe und Kartenmarker, Lebenslinie, Bienenjahr-Rad, Manifest/Icons/SW),
  §11.7 Königinnen mit automatischem Endstatus, Annahmen 119–124.
- TASKS: Abschnitt T5 mit erledigten und offenen Punkten.
- Spec: 18 markierte Abweichungen bei der Umsetzung mit Grund.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-19 17:31:48 +02:00
1 parent bb3f79e55f
commit 4ba6dea45a
3 files changed
+146 -6

No files matched your search

+76 -3
View File
@@ -74,6 +74,8 @@ Zentrale Schnellaktion "+ Neu" (Abschnitt 22): permanent sichtbarer Floating-But
**Navigation aus einer Quelle:** `app/(app)/nav-config.ts` ist die einzige Quelle für Seitenleiste, Bottom-Nav, „Mehr“-Sheet und „+ Neu“; der Berechtigungsfilter gilt überall gleich. Neue Seiten (auch „Verwaltung > System“) werden nur dort eingetragen. Auf allen `new`-Seiten führt ein Zurück-Link zur Liste, bei einem Entwurf mit Rückfrage. „Mehr“ und „+ Neu“ teilen sich die Sheet-Hülle `components/nav/nav-sheet.tsx`.
**Bereiche und Motiv (T5):** `src/lib/areas.ts` (`AREAS`) ist die einzige Quelle für Icon und Farbton je Bereich. Navigation, „+ Neu“, Seitenköpfe (`PageHeader`, `DetailHeader`, `EntityCard` mit Prop `area`) und Leerzustände lesen daraus; ein Icon steht immer für genau einen Bereich. Die Wabenzelle ist das einzige grafische Motiv: `AreaIcon`, `BrandMark`, Waben-Standkarte, Kartenmarker und die Kappe am Zeiger des Bienenjahr-Rads. Farbverläufe gibt es nicht mehr. Einzelheiten: Abschnitt 15.
**Offline-Seiten:** Ohne Netz zeigen `/offline` (Start: geladene Stände), `/offline/apiary?id=…` und `/offline/colony?id=…` die Daten aus den Offline-Paketen (IndexedDB). QR- und NFC-Deep-Links (`/apiaries/<id>`, `/colonies/<id>`) weicht der Service Worker ohne Netz auf diese Seiten aus; der Weg zurück („Mit Netz öffnen“) übernimmt `from` nur bei gleicher Herkunft (same-origin). Formulare lesen Völker und Königinnen aus dem Paket, wenn die API nicht antwortet (`withPackFallback`, `networkMode: "always"`).
**Beute offline (Abschnitt 13.1):** Das Paket (`offline-pack`) trägt optional `hives` (Aufbau je Volk) und `hiveCatalog` (aktive Systeme plus die von den Aufbauten genutzten); ein älteres Paket ohne diese Felder sagt „unbekannt“, nicht „kein Aufbau“ (`hiveFromPacks`, `lib/hive/pack.ts`). Änderungen ohne Netz landen im IndexedDB-Speicher `pending-hive-changes` (DB-Version 5, `queueHiveChange`). `drainTimeline` (`lib/offline/sync.ts`) sendet diese Einträge und die Durchsichten, die eine `hiveChange` tragen, gemeinsam nach `queuedAt`, nacheinander. Netzfehler und 5xx stoppen den Lauf; jede andere Ablehnung (`HIVE_CONFLICT`, 403, 400) wird am Eintrag gespeichert, der Eintrag bleibt in der Warteschlange und hält die späteren Einträge desselben Volks zurück, andere Völker laufen weiter. **Überholschutz:** Solange für ein Volk eine Beute-Änderung wartet (auch eine abgelehnte), reiht `useHiveMutation` jede neue Kartenänderung hinter sie ein, statt online zu senden (`hasQueuedHiveChange`). Die Karte zeigt die wartenden Operationen auf dem geladenen Stand; eine abgelehnte Änderung zeigt `HivePendingNote` als Hinweis mit Weg in die Warteschlange (Annahme 102).
@@ -241,6 +243,12 @@ Fachliche Annahmen, getroffen weil das Lastenheft an der Stelle keine eindeutige
116. **Der Text einer Sprachnotiz darf zu Anthropic (Drittland)**: Der Inhaber hat den API-Schlüssel angelegt (2026-10-16); das gilt als Freigabe von Spec-Frage 3. Modell `claude-sonnet-5-5` mit `effort: low` (Frage 1); die Cloud-Erkennung des Browsers bleibt ohne Einwilligung aus (Frage 2). Eine Region ist nicht gesetzt (`inference_geo` bleibt aus), es gilt die Voreinstellung des Anthropic-Arbeitsbereichs. Kosten bei falscher Annahme: Fehlt die Freigabe, bleibt der Worker ohne das Secret `apicore-anthropic`, die Notizen bleiben `TRANSCRIBED` und sind als Text nutzbar; Datenschutzerklärung und Verzeichnis müssen die Übermittlung vor dem Einschalten nennen. Abschnitt 14.7.
117. **Die Grenzen der Vorschläge folgen dem Formular, nicht der Spec**: Volksstärke bis 40 und fünf Futterstufen, wie `CreateInspectionDto` und das Formular; für `broodFrames` ohne Obergrenze im Formular gilt im Sprachweg 40. Kosten bei falscher Annahme: Die Auswertung schlägt Werte vor, die der Autor der Spec nicht erwartet hat, oder verwirft zulässige (sie landen als Zahl in `dropped`); die Grenzen stehen an einer Stelle (`core/inspection-field-specs.ts`). Abschnitt 14.4.
118. **Meldungen zu Sprachnotizen gehören in die Kategorie „Vorschläge“**: „Sprachnotiz ist fertig“ und „konnte nicht ausgewertet werden“ haben die Kategorie `SUGGESTION`, weil sie zu Vorschlägen führen. Kosten bei falscher Annahme: Wer die Kategorie „Vorschläge“ in Telegram oder der App abschaltet, bekommt auch diese Meldungen nicht mehr; die Notiz bleibt unter `/voice` sichtbar. Abschnitt 14.5.
119. **Die Darstellung gilt je Gerät, nicht je Konto**: Farbschema (hell, dunkel, wie Gerät), Kontrast (normal, Sonne) und Bewegungsstufe (Voll, Dezent, Aus) liegen in `localStorage` (`apicore.theme`, `apicore.contrast`, `apicore.motion`), nicht im Benutzerkonto. Ohne gespeicherte Wahl gilt hell, auch auf einem Gerät im Dunkelmodus; `prefers-contrast: more` schaltet Sonne vor, solange nichts gewählt ist. Ein Inline-Skript im `<head>` (`AppearanceScript`) setzt die Werte vor dem ersten Bild. Es braucht `script-src 'unsafe-inline'`, das Next.js ohnehin verlangt (Abschnitt 9); wird die CSP verschärft, bekommt das Skript einen Hash (sein Inhalt ist fest, ohne Nutzerdaten). Ist der Speicher gesperrt, gilt die Wahl nur für den Besuch. Abmelden lässt die Werte stehen. Kosten bei falscher Annahme: Wer auf einem zweiten Gerät arbeitet, stellt die Darstellung dort neu ein. Ein Konto-Feld bräuchte Migration, Endpunkt und einen Abgleich nach dem Login, und das erste Bild käme trotzdem vor den Kontodaten. Abschnitt 15.2.
120. **Ein Baukasten, durch Tests erzwungen**: Auswahlgruppe (`ChoiceGroup`), Checkbox und Schalter (`Checkbox`, `CheckboxField`, `Switch`), Reiter (`Tabs`, `ActiveHistoryTabs`, `TabPicker`), Plus/Minus (`NumberStepper`), Bereichs-Icon (`AreaIcon`) und Markenzeichen (`BrandMark`) gibt es je genau einmal. `src/styles/baukasten-guard.spec.ts` durchsucht `src` und schlägt bei jedem Eigenbau fehl (Regeln in Abschnitt 15.3). Jede Ausnahme steht mit Grund in der Liste, und ein Test prüft, dass sie noch gebraucht wird. Ausnahmen heute: der eigene Fokus-Pfad der Wabenzelle (`honeycomb-cell.tsx`), der Fokus nach einem Skript-Sprung in `print-labels-card.tsx` und `social-media-tab.tsx`, die Rollen in `choice-group.tsx` und `aria-pressed` in `ui/button.tsx` und an den Kalendertagen. Kosten bei falscher Annahme: Ein Sonderfall, der wirklich anders aussehen muss, braucht einen Eintrag mit Grund im Wächter statt einer stillen Abweichung; das kostet einen Review-Schritt, verhindert aber Kopien. Abschnitt 15.3.
121. **Die offenen Fragen der T5-Spec gelten vorläufig mit Antwort A**: Hauptknopf in hellem Honig mit dunkler Tinte (Frage 1), Zellen der Waben-Standkarte nach Volkscode (Frage 2), hell als Standard auf einem neuen Gerät (Frage 3), ruhige Bewegung in der App mit der Beute als einziger Bühne (Frage 4). Der Inhaber hat sie noch nicht bestätigt. Kosten bei falscher Annahme: je Frage eine Token- oder Konstantenänderung (`--primary*` in `globals.css` zusammen mit `contrast.spec.ts`, `sortCells` in `lib/honeycomb/cell-state.ts`, `DEFAULT_THEME` in `lib/appearance/appearance-core.ts`, Bewegungs-Tokens in `lib/motion/tokens.ts`). Nur Frage 2 B (echte Reihenfolge am Stand) bräuchte ein neues Feld `Colony.standSlot` mit Migration (etwa 1 PT). Spec §16.
122. **Die Stärke-Kurve der Lebenslinie beginnt am 17.10.2026**: Seit T3 zählt `Inspection.colonyStrength` besetzte Waben, vorher war es eine Skala. Das Frontend mit dem Waben-Stepper ging am 16.10.2026 um 22:28 live (sha-cda3e02); der erste volle Tag ist der 17.10. (`STRENGTH_IN_FRAMES_SINCE` in `lib/colonies/lifeline.ts`). Ob ein Wert davor liegt, entscheidet `createdAt` der Durchsicht, ersatzweise ihr Datum. Ältere Werte stehen nur in der Tabelle mit der Einheit „Skala (alt)“. In Produktion gibt es zwischen 10.10. und 19.10. keine Durchsicht, also keinen Wert auf der Grenze. Kosten bei falscher Annahme: Die Kurve mischt Skala und Waben, oder echte Wabenwerte fehlen in der Kurve; die Konstante wird geändert, kein Datenumbau. Abschnitt 15.7.
123. **Die Bienenstände-Liste zeigt die Wabe nur für die ersten 20 Stände**: Jede Karte lädt ihre Übersicht einzeln (`GET /apiaries/:id/colony-overview`, derselbe Cache-Schlüssel und derselbe Paket-Rückfall wie die Stand-Seite). Ab dem 21. Stand zeigt die Karte nur Name und Ort (`MAX_COMB_CARDS`). Einen Sammel-Endpunkt gibt es nicht. Kosten bei falscher Annahme: Beim Öffnen der Liste gehen bis zu 20 Anfragen raus, und ein großer Betrieb sieht spätere Stände ohne Wabe. Abhilfe ist ein Sammel-Endpunkt für die Übersicht mehrerer Stände (Folgepunkt in TASKS.md). Abschnitt 15.6.
124. **Das Bienenjahr-Rad zeigt den Abschnitt der meisten Stände**: Bezugsstand ist der Abschnitt, in dem die meisten aktiven Stände sind; bei Gleichstand gilt der im Bienenjahr weiter fortgeschrittene. Abweichende Stände nennt eine Zeile unter dem Rad und die Tabelle. Liefert das Backend keinen `seasonCalendar` (älterer Stand), gelten `SEASON_DEFAULTS` (`lib/season/season-defaults.ts`); ein Test hält sie gleich mit `phenology.config.ts` und `season-autopilot.config.ts` des Backends. Kosten bei falscher Annahme: Wer Stände in sehr verschiedenen Lagen hat, sieht im Zentrum den Mehrheitsabschnitt; ein früherer Stand steht nur in der Zeile darunter. Der Fehler geht zur sicheren Seite: lieber eine Durchsicht zu früh als einen Schwarm verpasst. Abschnitt 15.8.
## 7. Kubernetes
@@ -606,7 +614,7 @@ Als „letzte Behandlung“ zählt das späteste Ende (`endDate ?? date`); eine
- Aktion: `CAPTURE` (Formular-Link; auch Ersatz, wenn `tasks:create` fehlt und die Absicht ein Formular hat), `ACCEPT` (nur mit `tasks:create`), `CLEANUP`, `OPEN_TASK` (`/tasks`). Ohne mögliche Aktion entfällt der Schritt. `withdrawalUntil` (nur mit `treatments:read`) und `inspectionDueOn` (nur mit `inspections:read`, `null` bei abgeschalteter Regel oder in der Winterruhe) sind Hinweise, keine Schritte. Das Erfassen einer Durchsicht erledigt Ketten-Vorschläge (`chains.queen-cells`) und führt Regel-Vorschläge nach (Annahme 12).
- `GET /apiaries/:id/colony-overview` trägt je Volk zusätzlich `occupiedFrames`, `inspectionDueOn`, `segment` (oder `null`) und `nextStepTitle`; der Offline-Pack enthält `nextSteps` je Volk.
**Oberfläche.** Volksseite: „Als Nächstes“ (bis 3 Schritte mit Gründen, Fälligkeitschip, Aktion, Verwerfen; „Wartezeit bis TT.MM.“ als Hinweis; Fehlerzustand mit Wiederholen). Startseite „Heute“: der dringlichste Schritt, Karten „Durchsicht fällig – <Stand>“ mit Saison-Chip und „Runde starten“, Chips „Vorschläge <n>“ (nur mit `tasks:read`) und „<Stand>: <Saison>“; bei Fehler, offline oder ohne Recht greift die bisherige Aufgabenlogik. Stand-Seite: je Volk „Durchsicht fällig in 2 Tagen“ und „Als Nächstes: …“. Offline zeigt die Volksseite die Schritte aus dem Paket nur lesend (ohne Übernehmen, Verwerfen, Aufräumen). Aufräumen öffnet das Sheet aus Abschnitt 11.7; Übernehmen aus Schritten zeigt „Aufgabe angelegt“.
**Oberfläche.** Volksseite: „Als Nächstes“ (bis 3 Schritte mit Gründen, Fälligkeitschip, Aktion, Verwerfen; „Wartezeit bis TT.MM.“ als Hinweis; Fehlerzustand mit Wiederholen). Startseite „Heute“: der dringlichste Schritt, Karten „Durchsicht fällig – <Stand>“ mit Saison-Chip und „Runde starten“, der Chip „Vorschläge <n>“ (nur mit `tasks:read`); die Saison je Stand steht seit T5 im Bienenjahr-Rad (Abschnitt 15.8), die Chip-Zeile „<Stand>: <Saison>“ ist entfallen; bei Fehler, offline oder ohne Recht greift die bisherige Aufgabenlogik. Stand-Seite: je Volk „Durchsicht fällig in 2 Tagen“ und „Als Nächstes: …“. Offline zeigt die Volksseite die Schritte aus dem Paket nur lesend (ohne Übernehmen, Verwerfen, Aufräumen). Aufräumen öffnet das Sheet aus Abschnitt 11.7; Übernehmen aus Schritten zeigt „Aufgabe angelegt“.
### 11.6 Biotechnik-Ketten
@@ -618,7 +626,7 @@ Drei Ketten schlagen die nächsten Prüfschritte vor (Handler `chains.*`, Tabell
### 11.7 Volk beendet und Aufräumen
Jeder Weg, auf dem ein Volk endet (Auflösen, Vereinigen, Bearbeiten auf `SOLD`/`DECEASED`, Auswinterungsverlust mit `markColonyDeceased`), beendet in derselben Transaktion die Königin-Zuordnung über `releaseQueenIn` (`src/colonies/colony-lifecycle.ts`): Die offene `QueenColonyHistory` bekommt `endDate`, `Colony.currentQueenId` wird `null`, der Status der Königin ändert sich nicht. Das Enddatum ist der Berliner Tag der Änderung (bei der Auswinterung das Datum der Prüfung). Beim Bearbeiten greift das nur beim Übergang von lebend nach beendet. Ein Wächter (`colony-end-releases-queen.spec.ts`, TypeScript-Compiler-API) fordert, dass jede Stelle, die einen beendeten Status schreibt, vorher `releaseQueenIn(` aufruft. `ColonyEnded` trägt `{ status, endedOn, releasedQueenId, via }` (`via`: `DISSOLVE`, `MERGE`, `EDIT`, `WINTER_CHECK`); ältere Ereignisse ohne diese Felder fallen auf den Berliner Tag des Ereignisses und `EDIT` zurück.
Jeder Weg, auf dem ein Volk endet (Auflösen, Vereinigen, Bearbeiten auf `SOLD`/`DECEASED`, Auswinterungsverlust mit `markColonyDeceased`), beendet in derselben Transaktion die Königin-Zuordnung über `releaseQueenIn` (`src/colonies/colony-lifecycle.ts`): Die offene `QueenColonyHistory` bekommt `endDate`, `Colony.currentQueenId` wird `null`. Danach setzt `QueensService.retireIn` in derselben Transaktion den Endstatus der Königin (Entscheidung des Inhabers 2026-10-19): Auflösen und Vereinigen setzen `REPLACED`, die Auswinterung mit Verlust setzt `DECEASED`, und eine Königin, die von einer neuen verdrängt wird, wird ebenfalls `REPLACED`. `retireIn` ändert nur einen verfügbaren Status (`ACTIVE`, `RESERVE`, `MATED`, `UNMATED`) und läuft über `updateStatusIn` mit Archivdatum (`deactivatedAt`) und Audit. Beim Bearbeiten auf `SOLD` oder `DECEASED` bleibt der Status der Königin; dort fragt das Aufräumen nach (unten). Ein Statuswechsel auf `LOST`, `DECEASED`, `REPLACED` oder `SOLD` setzt `deactivatedAt`, ein korrigierter Status nimmt es wieder weg (Migration `20261021100000_queen_deactivated_backfill` für den Bestand). Das Enddatum ist der Berliner Tag der Änderung (bei der Auswinterung das Datum der Prüfung). Beim Bearbeiten greift das nur beim Übergang von lebend nach beendet. Ein Wächter (`colony-end-releases-queen.spec.ts`, TypeScript-Compiler-API) fordert, dass jede Stelle, die einen beendeten Status schreibt, vorher `releaseQueenIn(` aufruft. `ColonyEnded` trägt `{ status, endedOn, releasedQueenId, via }` (`via`: `DISSOLVE`, `MERGE`, `EDIT`, `WINTER_CHECK`); ältere Ereignisse ohne diese Felder fallen auf den Berliner Tag des Ereignisses und `EDIT` zurück.
Der Handler `suggestions.colony-ended` setzt offene Vorschläge des Volkes auf `OBSOLETE` und legt den Aufräum-Vorschlag an (Regel `COLONY_ENDED_CLEANUP`; Quelle `CHAIN`, Absicht `CLEANUP`, Art `DO`, Priorität `NORMAL`, `colonies:read`). Anker `ended-<endedOn>-<Ereignis-ID>`, Beleg `{ colonyId, status, endedOn, via, openTaskIds, queenId }`; die Königin zählt nur, solange sie `ACTIVE` ist. Wird ein Volk reaktiviert (`reactivate`), werden seine offenen Aufräum-Vorschläge `OBSOLETE`; endet es am selben Tag erneut, entsteht ein neuer.
@@ -1041,7 +1049,7 @@ Formfehler (Form der Anfrage, zukünftiger Tag, Tag vor `INIT`, mehr als 100 Ope
- **Karte.** `components/hive/hive-card.tsx` auf der Volksseite (`mode` `view` oder `inspection`). Die Bühne (`HiveVisualSlot`) zeigt immer das SVG (`HiveSvg`, aus `lib/hive/layout.ts`), die 3D-Szene liegt darüber. Daneben stehen die Gliederung (`HiveOutline`, Baum aller Teile, per Tastatur bedienbar), Fakten und Fokusleiste. Teile und Rähmchen öffnen Sheets (`part-sheet`, `frame-sheet`); jede Änderung geht über `useHiveMutation` als `POST changes` mit eigenem `Idempotency-Key` je Versuch, einen Versand nach dem anderen, optimistisch auf dem Cache. Das Sheet zeigt bei `HIVE_CONFLICT` den aktuellen Stand aus der Antwort (`HiveConflictSheet`). Die Historie (`HiveHistory`) zeigt Änderungen und den Stand eines Tages (`GET at`).
- **Editor** `/colonies/[id]/hive` („Umbauen“): Mehrere Schritte werden lokal als Entwurf gesammelt (`useHiveDraft`: Schritte, Rückgängig und Wiederholen, `snapshot` des Ausgangsstands, Annahme 104) und mit einer Änderung gespeichert. Bis dahin berührt der Entwurf den Server nicht; Verlassen der Seite mit offenen Schritten fragt nach. Der Editor lädt den Aufbau neu, sobald das Fenster den Fokus bekommt, und alle 30 s, solange ein Entwurf besteht (`REFRESH_INTERVAL_MS`). Umbauen geht nur mit Netz.
- **Durchsicht.** `inspections/new` hat den Abschnitt „Beute“ (`hive-section.tsx`): die Karte im Modus `inspection` sammelt Operationen als Entwurf, sie gehen als `hiveChange` mit der Durchsicht in `POST /inspections` und in der Warteschlange mit dem Eintrag (Abschnitt 2). Die Sheets stoppen das `submit` an ihrem Formular (`stopPropagation`; sie liegen in einem Portal, doch React reicht Ereignisse im Komponentenbaum weiter), sonst würde „Speichern“ im Sheet die Durchsicht speichern; neue Formulare in Sheets müssen dasselbe tun. Zusätzlich stoppt der Abschnitt selbst `submit` an seinem Wrapper (`hive-section.tsx`).
- **3D-Szene.** `components/hive/hive-scene` (three, react-three-fiber, drei, postprocessing) wird nur über `next/dynamic` mit `ssr: false` geladen, sobald die Karte sichtbar wird (`IntersectionObserver`); andere Seiten laden nichts davon. Budget für den Chunk: höchstens 320 kB gzip (Spec 9.7); gemessen zuletzt etwa 296 kB. Die Szene misst die Bildrate (`quality.ts`): unter 45 fps fällt sie auf die Stufe `low` (kein Spiegelboden, kein MSAA, keine Tiefenunschärfe, keine Partikel), die Tiefenunschärfe läuft erst ab 55 fps. Bewegung folgt der Stufe `full`, `subtle` oder `off` (`useEffectiveMotionLevel`: Flüge, Überblendung 400, 150 oder 0 ms). **Rückfall auf das SVG** (`shouldUse3D`, `lib/hive/webgl.ts`): kein WebGL, erzwungene Farben, oder Bewegung `off` bei höchstens 2 Prozessorkernen; ein verlorener WebGL-Kontext baut die Szene einmal neu auf, der zweite lässt es beim SVG. Das SVG ist darunter immer gerendert und bleibt bedienbar, bis das erste 3D-Bild steht; im Druck entfällt die 3D-Ebene.
- **3D-Szene.** `components/hive/hive-scene` (three, react-three-fiber, drei, postprocessing) wird nur über `next/dynamic` mit `ssr: false` geladen, sobald die Karte sichtbar wird (`IntersectionObserver`); andere Seiten laden nichts davon. Budget für den Chunk: höchstens 320 kB gzip (Spec 9.7); gemessen zuletzt etwa 296 kB. Die Szene misst die Bildrate (`quality.ts`): unter 45 fps fällt sie auf die Stufe `low` (kein Spiegelboden, kein MSAA, keine Tiefenunschärfe, keine Partikel), die Tiefenunschärfe läuft erst ab 55 fps. Bewegung folgt der Stufe `full`, `subtle` oder `off` (`useEffectiveMotionLevel`: Flüge, Überblendung 400, 150 oder 0 ms). **Rückfall auf das SVG** (`shouldUse3D`, `lib/hive/webgl.ts`): kein WebGL, erzwungene Farben, oder Bewegung `off` bei höchstens 2 Prozessorkernen; ein verlorener WebGL-Kontext baut die Szene einmal neu auf, der zweite lässt es beim SVG. Das SVG ist darunter immer gerendert und bleibt bedienbar, bis das erste 3D-Bild steht; im Druck entfällt die 3D-Ebene. In der Sonne zeichnet die Szene Weiß auf Schwarz ohne Farbton (Abschnitt 15.5).
## 14. Sprache (T7 „Freihand“)
@@ -1145,3 +1153,68 @@ Reihenfolge, jeder Schritt mit Freigabe:
4. **Anthropic.** Der Inhaber legt das Secret `apicore-anthropic` (Schlüssel `ANTHROPIC_API_KEY`) an und verschiebt den Schlüssel aus `apicore-backend-secret`, wo er über `envFrom` auch API- und Job-Pods erreichte; er setzt in der Anthropic-Konsole ein Ausgabenlimit (Vorschlag 10 $ im Monat). Mit 60 Aufrufen am Tag (`VOICE_DAILY_EXTRACTIONS`) bleibt der Verbrauch dahinter.
5. **Messung (Task 24)** auf dem Zielknoten: Speicherspitze des Whisper-Containers gegen 1280Mi (inklusive der Speicher-Volumes), Zeit und CPU-Drosselung, Trefferquote der Fachwörter (Ziel ≥ 90 %, sonst `medium` oder längere Fachwortliste), einmal `npm run eval:voice -- --live --yes`.
6. **Freigabe (Task 25).** `VOICE_RECORDING_ENABLED` auf `"true"` und Frontend ausrollen. Rückweg: Schalter auf `"false"` und die Komponente `voice` abschalten; offene Notizen laufen über die Fristen ab, es gehen nur flüchtige Daten verloren.
## 15. Optik und Baukasten (T5)
Die App soll draußen am Stand lesbar sein: in der Sonne, mit Handschuhen, mit einer Hand. Spec: `docs/superpowers/specs/2026-10-13-t5-optik-design.md`, Plan: `docs/superpowers/plans/2026-10-14-t5-optik.md`. T5a („Sonnentauglich und Baukasten“, Tasks A1–A12) ist seit 2026-10-19 produktiv. T5b („Handschrift“, Tasks B1–B8) bringt die Grafiken mit der Wabenzelle. Bis auf einen additiven Zusatz an `GET /autopilot/today` (Abschnitt 15.8) ist T5 reines Frontend. Annahmen 119–124.
### 15.1 Tokens und vier Farbsätze
- **Nur Tokens.** Farben stehen nur als CSS-Variablen in `src/app/globals.css`. Es gibt vier Sätze: hell (`:root`), dunkel (`.dark`), Sonne hell (`:root[data-contrast="sun"]`) und Sonne dunkel (`.dark[data-contrast="sun"]`). Sonne dunkel wiederholt jeden Wert von Sonne hell, damit die Kaskade nichts mischt. Der Druck nutzt immer den hellen Satz (`@media print`).
- **Tinte und Fläche.** Der Hauptknopf ist Honig (`--primary`) mit dunkler Tinte (`--primary-foreground`). Für Text, Links, Icons und den Fokusring auf hellen Flächen gibt es `--primary-ink`; `text-primary` ist verboten. Jede Zustandsfarbe hat drei Tokens: Fläche (`--condition-x`, `--warning`), Text auf der Fläche (`-foreground`) und Text oder Icon auf normalen Flächen und Tönungen (`-ink`). Dazu kommen Bereichstöne (`--area-*`), Zeichenfarben der Königin (`--queen-*`), Fokus (`--focus-width`, `--focus-offset`), Rahmen der Eingaben (`--control-border-width`) und die drei Randstufen der Wabe (`--comb-stroke-1` bis `-3`).
- **Sonne.** Reinweiße Fläche, fast schwarze Tinte, Rahmen in Tinte, keine Tönung, kein Schatten, keine Unschärfe. Der Fokusring ist 3 px breit, Eingaberahmen 2 px, Nebentext 16 px statt 14 px.
- **Wächter.** `src/styles/contrast.spec.ts` liest `globals.css` (über `src/styles/css-tokens.ts`) und misst jedes Paar der Spec-Tabelle 4.4 in allen vier Sätzen, dazu Tabs, Menüeinträge, Fokus um eine gewählte Honig-Option und die Beute-Bühne. Er prüft auch, dass jeder Wert im sRGB-Raum liegt und dass kein Satz ein Token auslässt. Tokenwerte ändern sich nur zusammen mit diesem Test.
- **Varianten.** `dark:` (als `:where(.dark, .dark *)`), `sun:`, `pointer-fine:`, `pointer-coarse:` und `motion-full:`. Klassen mit `dark:` gibt es im Code nicht; die Tokens tragen den Modus.
- **Schrift und Größen.** Fließtext, Beschriftungen, Auswahl-Knöpfe, Listentitel und Knopftext haben 16 px, Nebentext 14 px. `text-xs` ist 13 px und nur für Achsen, Legenden und Fußzeilen erlaubt. Ziele sind auf Touch-Geräten mindestens 44 px groß; das kompakte Maß (32/36 px) gibt es nur mit `pointer-fine:`, nie mit `sm:`. Icon-Knöpfe bekommen auf Touch eine Trefferfläche von 44 px über ein Pseudo-Element.
- **Fokus.** Eine Regel für alles: `:where(:focus-visible)` zeichnet einen durchgehenden Ring in `--ring` (= `--primary-ink`) mit 2 px Abstand (Sonne 3 px). Komponenten löschen ihn nicht. Firefox bekommt dieselbe Regel über `:-moz-focusring`. Menüeinträge zeigen den Fokus mit einem inneren Ring neben der Hervorhebung.
### 15.2 Darstellung umschalten
- **Ohne Flackern.** `components/appearance-script.tsx` rendert ein blockierendes Inline-Skript in den `<head>`. Es liest die drei Werte (Annahme 119) und setzt vor dem ersten Bild `class="dark"`, `data-contrast` und `data-motion` auf `<html>` sowie die `theme-color`-Metas (`#f9f4ea`, `#1a120d`, `#ffffff`, `#040302`). Das Skript entsteht aus denselben Konstanten wie `resolveAppearance()` in `lib/appearance/appearance-core.ts`; ein Test vergleicht beide über alle Kombinationen. `next-themes` ist entfernt.
- **Hook.** `useAppearance()` (`lib/appearance/appearance.ts`) liest und setzt die Werte. Er hört auf Systemeinstellungen (dunkel, `prefers-contrast: more`), andere Tabs und das eigene Ereignis `apicore:appearance`. `setMotion` löst weiter `MOTION_EVENT` aus, damit `useEffectiveMotionLevel()` sofort folgt. Der Toaster folgt dem Hook.
- **Bedienung.** Profil > Darstellung (`profile/appearance-section.tsx`): drei Auswahlgruppen als Karten mit dem Satz „Gilt nur für dieses Gerät.“. Im Benutzermenü schaltet „Sonnenmodus“ mit einem Tipp, danach ein Toast. Der Wechsel wirkt sofort und ohne Übergang.
- **Hinweis.** `components/appearance-notice.tsx` zeigt auf Start einmal je Gerät „Neu: Sonnenmodus für draußen – im Benutzermenü.“ mit „Ausprobieren“ und „Schließen“ (`apicore.notice.sun`). Beide Knöpfe setzen den Fokus auf die Überschrift der Seite. Ist die Sonne schon an, bleibt der Hinweis weg.
### 15.3 Baukasten und Wächter
- **Bausteine.** `ChoiceGroup` (`components/form/choice-group.tsx`, `appearance` `buttons` oder `cards`, gewählt mit Honig, 2 px Tinten-Rahmen und Haken; „Nicht geprüft“ gestrichelt), `Checkbox`, `Switch` und `CheckboxField` (`ui/checkbox.tsx`, `ui/switch.tsx`, `form/checkbox-field.tsx`; Formen `row`, `tile`, `task`; Enter schaltet nicht und sendet kein Formular ab, die Leertaste schaltet), `Tabs` mit der einen Variante `segment` (`ui/tabs.tsx`; Pfeiltasten bewegen den Fokus, Enter oder Leertaste wählt), `ActiveHistoryTabs` (wählt beim Fokus, Props `toolbar`, `toolbarAlign`, `heading`), `TabPicker` (unter 640 px eine Auswahlliste statt der Leiste), `NumberStepper` für ganzzahlige Zählwerte am Stand, `Button pressed` für Umschalter in Werkzeugleisten. `ToggleChip`, `SwitchRow` und `TabSwitcher` sind gelöscht.
- **Wächter.** `src/styles/baukasten-guard.spec.ts` (mit `guard-files.spec-helper.ts` und `touch-target-guard.spec.ts`) durchsucht `src/**/*.{ts,tsx}` und `globals.css`. Er schlägt fehl bei: eigenen Rollen oder Typen für Radio, Checkbox, Schalter und Reiter; `aria-pressed` außerhalb von `Button pressed` und Kalender; `ring-ring/50`, `outline-none` und eigenem Fokus-Rahmen; `text-primary` ohne `-ink` oder `-foreground`; `dark:`; Farbverläufen und `text-white`; Schrift unter 13 px; `sm:h-`, `sm:min-h-`, `sm:size-` und `md:text-sm`; Icon-Knöpfen nebeneinander ohne Abstand auf Touch; Listen von `CheckboxField` mit weniger als 4 px Abstand; und den Bewegungsregeln (15.4). Jede Regel hat einen Selbsttest mit Treffer und Nicht-Treffer. Ausnahmen: Annahme 120.
- **Bereiche.** `AREAS` (37 Bereiche) mit Icon, Ton und Bezeichnung; `AreaIcon` zeichnet das Icon in einer Wabenzelle im Bereichston, `BrandMark` das Markenzeichen. `lib/areas.spec.ts` prüft, dass jedes Icon genau einem Bereich gehört und jeder Eintrag in Navigation und „+ Neu“ sein Icon aus `AREAS` hat. Die Absichts-Icons der Vorschläge (`INTENT_ICONS` in `lib/suggestions/labels.ts`) sind eine eigene Tabelle, weil eine Absicht kein Bereich ist.
### 15.4 Bewegung
- **Stufen.** Es gilt die Bewegungsstufe je Gerät: „Voll“ (`full`), „Dezent“ (`subtle`, auch bei „Voll“ plus `prefers-reduced-motion: reduce`) und „Aus“ (`off`). Bei „Dezent“ bleiben nur Überblendungen, bei „Aus“ ist alles sofort. In JavaScript ist `useEffectiveMotionLevel()` (`lib/motion/motion-preference.ts`) die einzige Quelle, in CSS die Variante `motion-full:` für jede Verschiebung, Skalierung und Drehung.
- **Tokens.** `lib/motion/tokens.ts` und die Spiegel in `globals.css`: `--motion-fade` 150 ms, `--motion-enter` 200 ms, `--motion-settle` 350 ms, `--motion-moment` 420 ms, `--motion-shift` 8 px, `--ease-out-expo`, `--ease-in`, `--ease-spring` (aus der Feder der Beute, Steifigkeit 210, Dämpfung 21). `tokens.spec.ts` hält CSS, TypeScript und die Feder der Beute gleich. Kein DOM-Übergang ist länger als 420 ms; Ausnahmen sind die Beute-Bühne und das Wegfalten der Heute-Zeile (720 ms, gespeichert ist sofort).
- **Werkzeuge.** Seiten, Sheets, Dialoge, Menüs und Toasts laufen mit CSS (Base UI `data-starting-style`/`data-ending-style`). `motion` lädt nur für entfernte und umsortierte Listenzeilen: `components/app-motion.tsx` lädt im `(app)`-Layout das Modul `components/motion/motion-runtime.tsx` nach (`LazyMotion strict`, `domAnimation`, eigenes FLIP mit der Feder), `components/motion/presence-list.tsx` nutzt es (Heute-Liste, Warteschlange, Sprachnotizen). Bis das Modul da ist, ist die Liste ohne Bewegung.
- **Nie animiert.** Datenwerte (Zahlen, Ampeln, Balken, Kurven, Zellfüllungen, der Zeiger des Rads), Fehler und Warnungen, das Feld während der Eingabe, der Fokusring und der Wechsel der Darstellung. Bewegt werden nur `transform` und `opacity`, für Zustände auch Farben; `transition-all` ist verboten.
- **Wächter und Budgets.** Die Regelgruppe „Bewegung“ im Baukasten-Wächter prüft `motion-safe:` vor Bewegung, Puls und Drehen nur hinter `motion-full:`, `transition-all`, Breite und Höhe, Dauer über 420 ms, Hover mit Verschiebung, Übergänge nur aus den Tokens und die erlaubten Importe aus `motion/react`. Dateien mit Datenwerten (Wabe, Karte, Lebenslinie, Rad, Varroa-Diagramm) dürfen gar keinen Übergang haben. Der Image-Build prüft zwei Budgets: `npm run check:motion-budget` (alle `motion`-Chunks zusammen höchstens 30 kB gzip, zuletzt 28,1 kB, in keinem ersten Laden) und `npm run check:3d-budget` (höchstens 320 kB, zuletzt 301,3 kB).
### 15.5 Beute-Bühne in der Sonne
- Die Bühne ist eine eigene, immer dunkle Fläche mit eigenen Tokens (`--holo-*`); nur hier gibt es Glas, Leuchten und Ruhebewegung. `.holo-stage` setzt `--ring` auf `--holo-edge-strong`, weil der Ring der Seite auf der Bühne unter 3:1 läge; Karten auf der Bühne (`.stage-card`) bekommen den Ring der Seite zurück.
- `useHoloTheme()` (`hive-scene/theme.ts`) liest die Tokens neu, sobald sich Farbschema oder Kontrast ändern (Ereignis `apicore:appearance` und ein `MutationObserver` auf `<html>`), nie je Bild. Bei gleichen Werten bleibt das Theme-Objekt, und kein Material wird neu gebaut.
- In der Sonne zeichnet die Szene Weiß auf Schwarz ohne Farbton: kein Bloom, keine Vignette, kein Dunst, keine Partikel, kein Spiegelboden, kein Raster. Kanten sind 2 px breit (3 px bei Hover, Auswahl und aktivem Ablageplatz). Rähmchen-Arten unterscheiden sich durch Schraffuren statt durch Farbe. Etiketten haben 16 px auf fester Fläche, Glas hat keine Unschärfe.
### 15.6 Wabe: Standkarte und Kartenmarker
- **Modell.** `lib/honeycomb/cell-state.ts` (`cellModelOf`, `rimStepOf`, `summarizeColonies`, `sortCells`) und `lib/honeycomb/layout.ts` (`combLayout`, `columnsFor`) sind reine Funktionen. Die Füllung kommt aus `colonyStatusLight` (T3/T4). Der Rand zeigt die Durchsicht in drei Stufen: frisch, bald fällig (heute bis in 3 Tagen) und fällig oder nie durchgesehen (gestrichelt, mit Uhr). Ohne `inspectionDueOn` (altes Paket) gelten 10 und 14 Tage. Ohne `inspections:read` hat die Zelle einen neutralen Rand ohne Aussage. Der Rand ist in der Tinte der Füllung gezeichnet (`--condition-*-foreground`).
- **Ansicht.** `components/honeycomb/apiary-comb.tsx` zeigt Zusammenfassung, Wabe, Legende, „Braucht Blick“ und die aufklappbare Liste „Alle n Völker als Liste“ (die T3-Zeilenliste als Tabellen-Rückfall). Sie dient der Stand-Seite, der Offline-Standseite (aus dem Paket) und, über `apiary-comb-card.tsx`, der Liste `/apiaries` (Annahme 123). Jede Zelle ist ein Link zum Volk mit vollem Namen („B3 – Achtung: …“). Nur die Sechseckfläche nimmt Zeiger an (`comb.css`), damit sich die Treffer verzahnter Reihen nicht überlappen. Den Fokus zeichnet ein eigener Sechseckpfad (einzige Ausnahme von `outline-none`). Die Spaltenzahl misst `useContainerWidth` vor dem ersten Bild.
- **Karte.** `lib/honeycomb/mini-comb-svg.ts` liefert je Stand einen SVG-String (48 × 52 px): Rosette aus bis zu 7 Zellen, das schlechteste Volk in der Mitte, Kreuz und Ausrufezeichen für Rot und Gelb, bei mehr als 7 Völkern ein Zählchip. `components/apiary-map.tsx` setzt ihn als `divIcon` mit `aria-label` („Stand Nordhang: 7 Völker, 1 kritisch, 1 Achtung“). Enter oder Tippen öffnet das Popup mit Zusammenfassung und „Stand öffnen“ (bekommt den Fokus). Die Seite `/apiaries/map` lädt die Übersichten mit `useQueries` je Stand mit Koordinaten (derselbe Schlüssel wie die Stand-Seite) und zeigt darunter die Liste „Stände auf der Karte“ sowie ohne Netz einen Hinweis; beide gehen auch, wenn Leaflet nicht lädt. `src/styles/leaflet-theme.css` bringt Popup, Zoomknöpfe und Schrift auf die Tokens und dunkelt die Kacheln im Dunkelmodus ab. Die PNG-Marker sind gelöscht. Zoom- und Marker-Animationen laufen nur bei „Voll“; das Verschieben per Pfeiltaste animiert Leaflet immer (Folgepunkt in TASKS.md).
### 15.7 Lebenslinie
- **Modell.** `buildLifeline(input, today, options)` (`lib/colonies/lifeline.ts`) ist eine reine Funktion. `useColonyLifeline(colonyId)` sammelt die Daten über dieselben Schlüssel wie die Karten der Volksseite, also ohne Doppel-Anfragen: Volk, Historie, Durchsichten, Varroa, Behandlungen, Fütterungen, Schwarmkontrollen, höchstens 5 Königinnen und `next-steps`. Fehlt ein Leserecht (403), fällt die Spur still weg. Offline zeigt die Karte einen Hinweis.
- **Ausschnitt.** 12 Monate zurück plus 21 Tage voraus; „Ganzes Leben zeigen“ zeigt die ganze Lebenszeit. Die Stärke-Kurve beginnt bei `STRENGTH_IN_FRAMES_SINCE` (Annahme 122). Eine Lücke über 45 Tage unterbricht die Linie. Milben zählen nur aus dem natürlichen Milbenfall; die Ampel kommt aus `rating.light` der Messung.
- **Ansicht.** `components/colonies/colony-lifeline.tsx` (Karte und Zustände), `lifeline-svg.tsx` (ein SVG in der gemessenen Breite, alle Beschriftungen als HTML), `lifeline-table.tsx` (Tabelle mit derselben Zeitspanne, immer im DOM, per `hidden` versteckt). Die Zukunftszone ist in Zeilen geteilt (22 px je Schritt, höchstens 3). Die Milben-Skala reicht bis zum Doppelten der höchsten Grenze. Ereignis-Icons, die näher als 20 px beieinander liegen, werden ein Icon mit Zähler. Die Karte steht auf der Volksseite in Zeile 2 über den Karten zu Durchsichten, Varroa usw.; höchstens 600 SVG-Knoten für drei Jahre, keine Animation.
### 15.8 Bienenjahr-Rad
- **Backend-Zusatz.** `GET /autopilot/today` trägt je Stand zusätzlich `segmentSince`, `reportedOn` und `note` (`NO_COORDINATES`, `WEATHER_GAPS`, `WEATHER_STALE`) und auf oberster Ebene `seasonCalendar` (`calendarFallback` mit 8 Phasen, `swarmSeason`, `winterVarroaWindow`) aus den Regeln `PHENOLOGY` und `SEASON_AUTOPILOT`, auch bei abgeschalteter Regel. Die Antwort ist nur erweitert; Recht weiter `colonies:read`, keine Migration.
- **Modell.** `buildYearWheel(input, today)` (`lib/season/year-wheel.ts`) liefert sechs Abschnitte als lückenlosen Ring ab dem aktuellen, die Arbeitsfenster (Tracht, Varroa Sommer, Einwinterung, Varroa Winter) dieses und des nächsten Bienenjahrs, Aufgabenpunkte je Woche (bis 51 Wochen voraus) und den Bezugsstand mit seinen Abweichungen (Annahme 124). Ohne `segmentSince` oder `seasonCalendar` greift der Kalender aus `SEASON_DEFAULTS`.
- **Ansicht.** `components/season/year-wheel.tsx` auf dem Dashboard als letzter Abschnitt „Bienenjahr“: SVG 320 × 320 (skaliert darunter mit), Monatsinitialen und Zentrum als HTML, Zentrum mit höchstens drei einzeiligen Zeilen über Container-Queries, darunter Legende, „Jetzt … · Danach …“ und eine Zeile für Wetter-Hinweis und abweichende Stände. „Als Tabelle“ öffnet drei Tabellen (Abschnitte, Arbeitsfenster, Aufgaben je Woche). Das Rad nutzt die Abfragen des Dashboards (`["tasks", "dashboard"]`, `autopilotTodayKey`), keine neue Anfrage. Die Kacheln „Überblick“ und die Abfrage `/dashboard/summary` sind aus dem Frontend entfernt (der Endpunkt bleibt), ebenso die Chip-Zeile „Saison je Stand“ in „Heute“.
### 15.9 Manifest, Icons, Service Worker
- Master ist `public/brand/apicore-mark.svg` (dieselben Pfade wie `BrandMark`). `npm run icons` (`scripts/generate-icons.mjs`, `sharp` als devDependency) erzeugt daraus `public/icons/*` (192/512 „any“ und „maskable“, 512 „monochrome“, drei Shortcut-Icons), `src/app/apple-icon.png`, `src/app/favicon.ico` (16/32/48) und `public/icon.svg`. Zwei Läufe ergeben dieselben Bytes. Die Dateien liegen im Repo.
- `src/app/manifest.ts`: Hintergrund- und Theme-Farbe = heller Seitenhintergrund (`#f9f4ea`), Shortcuts Durchsicht, Varroa und Heute. Der Proxy-Matcher nimmt `/icons/*` und `/apple-icon.png` von den Kopfzeilen aus.
- Der Service Worker heißt `apicore-shell-v5` und frischt die Icons bei jedem Aufwärmen auf. Android tauscht das Homescreen-Icon von selbst (das kann Stunden dauern); iOS erst, wenn die App entfernt und neu hinzugefügt wird.