Files
Nick SzaunigandClaude Opus 5.5 1806255211 docs: T5 Bewegungs-Sprache (Spec §17) und Plan-Review 2026-10-16
Spec: Zweck, Stufen Voll/Dezent/Aus über useEffectiveMotionLevel,
Tokens aus dem, was Beute und CSS heute nutzen (eine Quelle in
lib/motion/tokens.ts), Regeln je Muster, was nie animiert,
Performance-Regeln, Abnahme; neue Inhaber-Frage 4.
Plan: 22 Korrekturen gegen den aktuellen Code (u. a. 31 statt 17
Checkbox-Stellen, fehlende AREAS, Lebenslinie unter der Beute-Karte,
Fokusring auf der Holo-Bühne), neue Tasks A11 (Bewegungs-Sprache
durchsetzen) und A12 (Beute-Bühne und Sonne-Modus).

Refs #26

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 14:17:03 +02:00

1176 lines
105 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T5 Optik mit eigener Handschrift Implementation Plan
**Status (2026-10-16):** Noch nicht begonnen. Plan-Review erledigt (Abschnitt „Review 2026-10-16“ direkt unten), Bewegungs-Sprache steht in Spec §17 und ist als Task A11 eingeplant. Offen beim Inhaber: Spec §16 Fragen 1–3 (vorläufig A/A/A) und die neue Frage 4 (Bewegungs-Sprache, Empfehlung A). Issue: [#26](https://git.encry.de/Nick.Rosenloecher/ApiCore/issues/26).
## Review 2026-10-16
Geprüft gegen den Code nach Beute L1–L5, T4a/T4b, T6 und T7 L1/L2/L3/L5. Die Korrekturen gelten vor dem jeweiligen Task-Text. Wo sie ihm widersprechen, gilt die Korrektur.
**Allgemein**
- G1 (Voraussetzungen): T4a **und T4b** sind gemergt (TASKS.md). T5b läuft damit ohne Kalender-Rückfall.
- `PHASE_SOURCE_LABELS` liegt in `src/lib/phenology/labels.ts`; die lokale Ersatzdefinition entfällt.
- `SeasonChip` liegt in `src/components/autopilot/season-chip.tsx`, `fetchAutopilotToday` in `src/lib/autopilot/api.ts`, `INTENT_ICONS` in `src/components/autopilot/next-steps-card.tsx`.
- G2 (Global Constraints, Zeile „Bewegung“): ersetzt durch Spec §17. Ab sofort gilt: `motion-full:` statt `motion-safe:` für jede Verschiebung, Tokens aus `src/lib/motion/tokens.ts`, nichts Datenhaftes bewegt sich. Rad, Lebenslinie und Waben bleiben ohne Animation.
- G3 (Bestand `motion`): Installiert ist `motion` **13.4.6**, nicht 12.x. `maath` ist da, `framer-motion` nur transitiv. Kein Quelltext importiert `motion/react` (Beute §9.8 sah es für die DOM-Teile vor, gebaut wurde CSS). Task A11 bringt die ersten DOM-Nutzer. Ohne A11 wäre die Abhängigkeit ungenutzt.
- G4 (Aufwand): T5a wächst von ≈ 4,75 auf ≈ 6,4 PT. Gründe: A7 +0,4 (31 statt 17 Checkbox-Stellen), neue Tasks A11 (0,75) und A12 (0,5). T5b bleibt bei ≈ 5,25 PT.
- G5 (Wellen T5a): A11 läuft in Welle 9 nach A8. Sie teilt `globals.css` mit A1/A4/A5, `today-panel.tsx` mit A7 und `ui/*` mit A5. A12 läuft in Welle 9 parallel; sie fasst nur `components/hive/**` an, `globals.css` nicht, weil die Holo-Sonne-Werte schon in A1 kommen. A10 rückt in Welle 10 und wartet auf A8, A9, A11 und A12.
**Je Task**
- A1:
- `--holo-*` (in `:root`/`.dark` seit Beute L3) bekommt Werte in beiden Sonne-Sätzen: Bühne `oklch(0 0 0)`, Kanten und Etikett-Text `oklch(1 0 0)`, Etikett-Fläche `oklch(0.12 0 0)`.
- Neu ist der Rahmen `.holo-stage { --ring: var(--holo-edge-strong) }` (Spec §17.8).
- `contrast.spec.ts` bekommt zwei Zeilen: `holo-label-fg`/`holo-label-bg` ≥ 7 und `holo-edge-strong`/`holo-bg` ≥ 3, in allen vier Sätzen.
- Grund: `--primary-ink` auf `--holo-bg` ≈ 2,8:1.
- A2:
- `motion-preference.ts` hat seit der Beute `useEffectiveMotionLevel` und `MOTION_EVENT`. Nutzer sind `hive-canvas.tsx`, `hive-visual-slot.tsx` und `today/use-task-actions.ts`. Die API bleibt; `useAppearance().setMotion` löst `MOTION_EVENT` aus.
- Neu ist das Ereignis `apicore:appearance` bei jedem Wechsel von Schema oder Kontrast; A12 hört darauf.
- Test dazu: `it('a motion change from the appearance section reaches useEffectiveMotionLevel in the same tab', ...)`.
- Die Stufe heißt in der Oberfläche „Dezent“.
- A3:
- Verlauf-Verbraucher sind **33 Dateien**, nicht 31. `text-white` steht zusätzlich in `components/hive/editor/editor-stage-overlay.tsx` (auf der Bühne → `text-holo-label-fg`).
- `AREAS` fehlen Bereiche aus T6/T7/Beute, die in `nav-config.ts`/`quick-actions.ts` stehen. Neue Schlüssel:
- `compliance` (`ShieldCheck`, betrieb)
- `medicines` (`Pill`, erfassung)
- `voice` (`Mic`, erfassung)
- `hive-systems` (`Box`, neutral)
- `system` (`Activity`, neutral)
- `areas.spec` prüft danach wieder jeden Nav- und Quick-Action-Eintrag.
- Neue Icon-Abweichungen, die korrigiert werden müssen:
- Arzneimittel-Packungen nutzen `Package` (= Abfüllungen) in `medicines/[id]/page.tsx` und `medicines/lots/[id]/page.tsx`.
- Beutensysteme nutzen `Boxes` (= Rohstoffe) in `settings/hive-systems/page.tsx`, die Navigation dagegen `Box`.
- `settings/voice/page.tsx` nutzt `ShieldCheck` (= Prüfbereitschaft).
- Das Dashboard trägt seit T6 `ComplianceLine` und `StandNote`; beide bleiben unverändert.
- A4:
- Die Codemod-Trefferzahlen vor dem Lauf neu zählen (`grep`), die Zahlen im Task sind vom 14.10.
- `dark:` steht weiter in genau 8 `ui/*`-Dateien.
- Die Beute-Bühne (`bg-holo-*`) bleibt unberührt: Sie ist in hell und dunkel absichtlich dunkel.
- A5:
- `sm:h-`/`sm:min-h-` stehen in **11 Dateien**, nicht in 4.
- Der Fokus-Codemod trifft 5 Stellen in `components/hive/**`. Dort entfällt der eigene Ring, und der Ring kommt aus `.holo-stage` (A1).
- `.honey-check` hat `outline: none` plus eigenen Schatten-Ring in `globals.css`. Er nimmt die globale Regel, die Ausnahme entfällt.
- A6:
- `NumberStepper` hat seit Beute L2 `step` mit Dezimalschritten; die Umstellungen nutzen nur ganze Schritte.
- Neue Radio-Eigenbauten → `ChoiceGroup`: `compliance/notices/create-notice-dialog.tsx` (`type="radio"`) und `components/phenology/phenology-report-sheet.tsx` (`type="radio"`).
- Neues `aria-pressed` in `components/hive/hive-card.tsx` („Aufbau als Liste“) → `Button pressed`.
- Ergänze die Dateien in der Files-Liste und im Testbefehl (`"src/app/(app)/compliance" src/components/phenology src/components/hive`).
- A7: Heute sind es **31 Stellen in 27 Dateien** (13 `ToggleChip`, 13 native, 4 Base UI, 1 `today-panel`).
- `ToggleChip`: 13 Stellen in 11 Dateien, neu dazugekommen:
- `compliance/notices/create-notice-dialog.tsx`
- `compliance/restrictions/create-restriction-dialog.tsx`
- `compliance/settings/page.tsx` ×3
- `medicines/medicine-form-dialog.tsx`
- `profile/telegram-link-card.tsx`
- `settings/automation/automation-rules-tab.tsx`
- `settings/automation/rule-forms/rule-form-parts.tsx`
- Natives `type="checkbox"`, neu: `settings/hive-systems/system-dialog.tsx`, `settings/voice/page.tsx`, `components/colonies/colony-takeover-section.tsx`.
- `components/form/switch-row.tsx` (`role="switch"`, Nutzer `profile/notification-settings-card.tsx`, `telegram-link-card.tsx`) geht in `CheckboxField` auf. Dafür bekommt `CheckboxField` das Prop `role?: "checkbox" | "switch"`, Umsetzung mit Base UI `Switch`. Danach wird `SwitchRow` gelöscht. Die Wächter-Regel erlaubt `role="switch"` nur in `ui/checkbox.tsx`/`ui/switch.tsx`.
- `today-panel.tsx` (`task`) kommt zuletzt, weil A11 dieselbe Datei danach anfasst.
- Aufwand 0,9 PT.
- A8:
- `ActiveHistoryTabs` hat jetzt **10 Nutzer**, neu ist `medicines/page.tsx`; sie kommt in die Files-Liste.
- `TabSwitcher` hat unverändert 2 Nutzer.
- A9:
- `SHELL_CACHE` ist weiter `apicore-shell-v4` → v5 gilt.
- `SW_ROUTES_VERSION` haben Beute L5 und T7 erhöht. „Bleibt unverändert“ heißt: A9 erhöht sie nicht noch einmal.
- `/inspections/new`, `/varroa/new`, `/voice/new` stehen in `SHELL_ROUTES`. Ein vierter Shortcut „Sprachnotiz“ (`/voice/new`) ist möglich, aber nicht Spec; nur nach Freigabe.
- A10:
- `src/app/(app)/dashboard/page.spec.tsx` gibt es schon → Modify, nicht Create.
- Die Screenshot-Matrix bekommt die Seiten aus T6, T7 und Beute:
- Volk mit Beute-Karte (Bühne, Fokus, Sheet)
- `/colonies/[id]/hive` (Umbauen)
- `/voice` und `/voice/new` (Aufnahme-Punkt)
- `/settings/voice`
- `/offline`, `/offline/apiary` und `/offline/colony`
- das Warteschlangen-Sheet (`components/offline/pending-queue-sheet.tsx`) und das Offline-Band
- `/compliance`, `/medicines`
- Die Matrix läuft je in Voll, Dezent und Aus, zusätzlich mit `prefers-reduced-motion: reduce` auf Start, Volk und `/voice/new`.
- A11 **neu**: Bewegungs-Sprache durchsetzen (Task-Text unten nach A10).
- A12 **neu**: Beute-Bühne an Sonne und Darstellungswechsel anschließen (Task-Text unten nach A11).
- B1: Die Offline-Standseite `src/app/(app)/offline/apiary/page.tsx` zeigt die Wabe aus dem Paket mit demselben `ApiaryComb` (Modify ergänzen, Test `it('the offline apiary page shows the comb from the pack', ...)`). `apiary-colonies-card.tsx` nutzt `inspectionDueOn` schon. Die Tests für den Zweig ohne T4 bleiben für alte Pakete.
- B3: Die Keys stimmen (`varroa-measurements`, `swarm-control-checks`, `next-steps`, `history`). `["colonies", id, "hive"]` gibt es neu. Beute-Änderungen als Ereignisse sind laut Beute §9.10 nicht Teil von T5; die Lebenslinie lässt sie aus.
- B4: Die Volksseite hat seit Beute L2 ein neues Raster:
- oben `NextStepsCard` über die volle Breite
- Zeile 1: Stammdaten und Beute, gleich hoch, ohne `items-start`
- Zeile 2: Aktionen | Karten
- „Desktop rechte Spalte oben“ ist jetzt die Beute. Die Lebenslinie kommt in Zeile 2 als erstes Kind der 2-Spalten-Fläche, über dem `xl:grid-cols-2`-Raster.
- Reihenfolge im DOM: Stammdaten, Beute, Aktionen, Lebenslinie, Karten.
- Test `it('the colony page puts the lifeline under „Als Nächstes“', ...)` → `it('the lifeline opens row 2 above the record cards and leaves row 1 to Stammdaten and Beute', ...)`.
- B5: Unverändert gültig. `AutopilotToday.seasons` hat weiter weder `segmentSince`, `reportedOn` und `note` noch `seasonCalendar`. Die Dateien sind `src/autopilot/next-steps.service.ts`, `src/autopilot/dto/next-steps-response.dto.ts` und `test/next-steps.e2e-spec.ts`.
- B6: `season-defaults.spec.ts` liest nicht `default-rules.ts`, sondern `ApiCore-Backend/src/automation/rule-config/phenology.config.ts` (`calendarFallback`, Schwarmzeit) und `season-autopilot.config.ts` (`winterVarroaWindow`).
- B7: Das Dashboard hat heute noch Honigbanner und `StatTile`; A3 und B7 sind also weiter nötig. `ComplianceLine` und `StandNote` bleiben über „Bienenjahr“ an ihrem Platz.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** ApiCore ist draußen am Stand lesbar und bedienbar (Sonne, Handschuhe, eine Hand) und erkennbar eine Imker-App: vier geprüfte Farbsätze (hell, dunkel, Sonne hell, Sonne dunkel), ein durchgesetzter Baukasten und die Wabenzelle als einziges Motiv (Waben-Standkarte, Mini-Waben-Marker, Lebenslinie, Bienenjahr-Rad, Markenzeichen).
**Architecture:** Farben stehen nur als Tokens in `globals.css`; ein Wächter-Test misst jedes Paar aus Spec 4.4 in allen vier Sätzen. Ein Inline-Skript im `<head>` setzt Farbschema, Kontrast und Bewegung vor dem ersten Bild (`next-themes` entfällt). Die Umstellung läuft in der Reihenfolge der Spec (Tokens additiv, Darstellung, Bereichs-Icons, Farben, Fokus, Bausteine), sodass jede Seite nach jedem Task lesbar bleibt; ein Baukasten-Wächter hält das Ergebnis. Grafiken sind handgeschriebenes SVG über reine Modellfunktionen (`cell-state`, `combLayout`, `buildLifeline`, `buildYearWheel`), die Ansichten sind dünn und liefern zu jeder Grafik eine Tabelle.
**Tech Stack:** Next.js 16 App Router, React 19, Tailwind 4 (`@theme inline`, `@custom-variant`), shadcn/Base UI, TanStack Query 5, react-leaflet/Leaflet 1.9, Vitest 4 + RTL, `sharp` (neue devDependency, nur für das Icon-Skript). Backend-Zusatz in NestJS 11/Jest 30 (Task B5). Keine neue Laufzeit-Bibliothek.
**Spec:** `docs/superpowers/specs/2026-10-13-t5-optik-design.md` (bindend). Bausteine aus `docs/superpowers/plans/2026-10-13-t3-erfassung-am-stand.md` und `docs/superpowers/plans/2026-10-13-t4-logik-ketten.md`.
**Lieferungen:** T5a „Sonnentauglich und Baukasten“ (Tasks A1–A12, ≈ 6,4 PT nach Review 2026-10-16, hängt nur an T3) und T5b „Handschrift“ (Tasks B1–B8, ≈ 5,25 PT, nach T4a; ohne T4 greift der Kalender-Rückfall).
**Vorläufige Inhaber-Entscheidungen (Empfehlungen, vor der Ausführung noch überstimmbar):** Hauptknopf = heller Honig mit dunkler Tinte (Offene Frage 1, A, 9,4:1). Zellen der Waben-Standkarte nach Volkscode (Frage 2, A). Standard-Farbschema auf neuem Gerät = Hell (Frage 3, A). Kippt eine Antwort, ändert sich nur das Token bzw. eine Konstante (`DEFAULT_THEME` in Task A2, `--primary*` in Task A4, `sortCells` in Task B1); `contrast.spec.ts` hält jede Variante.
---
## Global Constraints
**Voraussetzungen (nicht neu bauen, genau so nutzen)**
- T3 ist gemergt: Tasks 7 (`nav-config.ts`, `quick-actions.ts`, Sheets), 8 (`NumberStepper`, `RatingScale` mit `RATING_TONE_CLASS`, `rating-contrast.spec.ts`), 11 (Varroa-Trend als handgeschriebenes SVG), 14 (`colonyStatusLight`, `StatusLightBadge`, `ApiaryColoniesCard`, `useColonyOverview`/`colonyOverviewKey`), 15 (Offline-Paket, `withPackFallback`, Service Worker mit `SHELL_ROUTES`).
- T5b braucht T4a: Tasks 1 (`rule-config`), 15 (`ColonyOverviewRow.inspectionDueOn`, `NextStepsService.today`, `AutopilotToday`), 17, 18 (`SeasonChip`, `SEGMENT_LABELS`, Heute-Karten), 19 (`rating`/`band` je Messung). T4b (Task 31 `PHASE_SOURCE_LABELS`) ist optional; fehlt es, definiert T5b die drei Quellen-Texte lokal ('gemeldet', 'geschätzt', 'Kalender').
- Alle Pfade relativ zu `ApiCore-Frontend/`, sofern nicht anders angegeben; Backend-Pfade relativ zu `ApiCore-Backend/`.
- Vor jedem Next.js-Code die passende Anleitung unter `node_modules/next/dist/docs/` lesen (`AGENTS.md`); für T5 vor allem `01-app/02-guides/preventing-flash-before-hydration.md`, `progressive-web-apps.md`, `01-app/03-api-reference/03-file-conventions/01-metadata/manifest.md`, `app-icons.md`.
- Grafiken: den `dataviz`-Skill laden (wie T3/T4). Gestaltung: `impeccable` führt; keine neue Laufzeit-Bibliothek, `next-themes` fliegt raus.
**Verhalten und Barrierefreiheit**
- WCAG 2.2 AA. Text ≥ 4,5:1, Bedienrahmen, Fokus und Grafik ≥ 3:1; Fließtext und Hauptknopf Ziel ≥ 7:1.
- Touch-Ziele ≥ 44 px, Sheet-Zeilen 56 px, benachbarte Ziele ≥ 4 px Abstand. Kompaktmaß (32/36 px) nur mit `pointer-fine:`, nie mit `sm:`.
- Grundschrift 16 px (`text-base`) für Fließtext, Labels, Auswahl-Knöpfe, Listentitel, Knopftext, Eingabe; Nebentext 14 px (`text-sm`); `text-xs` = 13 px nur für Achsen, Legenden, Fußzeilen; in Sonne 16/14 px. Überschriften Space Grotesk, nie in Knöpfen oder Labels.
- Ein Fokusstil: 2 px durchgehender Ring in `--ring`, 2 px Abstand (Sonne 3 px), als Basisregel auf `:where(:focus-visible)`. Einzige Ausnahme: `honeycomb-cell.tsx` (zweiter Sechseckpfad, Spec 6.6).
- Zustand nie nur über Farbe: Icon plus Wort, Rand-Stufen über Strichstärke und Strichelung. Jede Grafik hat `<title>`, `<desc>` und eine aufklappbare Tabelle aus demselben Modell.
- Bewegung (geändert 2026-10-16): Spec §17 ist bindend. Stufen über `useEffectiveMotionLevel()` und die CSS-Variante `motion-full:` (nie `motion-safe:` für Verschiebung). Tokens kommen aus `src/lib/motion/tokens.ts` und `--motion-*`. Bewegt werden nur `transform` und `opacity`, Datenwerte, Fehlertexte und Eingaben nie. `motion` nur über `LazyMotion`/`m.*` im `(app)`-Layout. Rad, Lebenslinie und Waben erscheinen weiter sofort. Durchgesetzt wird das in Task A11, der Wächter prüft es.
- Deutsche Oberfläche, kurze Sätze, ein Hauptknopf (Honig) je Ansicht, ADHS-freundlich: wenig kognitive Last. Code-Bezeichner englisch.
- Verboten (Wächter prüft): Weiß auf Honig, `text-primary` als Schriftfarbe (nur `text-primary-ink`), `ring-ring/50`, `outline-none`, Farbverlauf als Hintergrund, `text-white`, `dark:`-Klassen, Text < 13 px, Zustand nur über Farbe, eigene `role="radio|checkbox|tab"`-Bauten außerhalb des Baukastens. Ein Bereichston ist nie eine Zustandsfarbe.
- Wiederverwenden statt kopieren: `StatusLightBadge`, `colonyStatusLight`, `ChoiceGroup`, `NumberStepper`, `Field`, `QueryState`, `PageHeader`/`DetailHeader`/`EntityCard`, `useColonyOverview`, Query-Keys der bestehenden Karten (keine Doppel-Requests).
**Tokens (exakte Werte, OKLCH ist die Quelle, alle im sRGB-Gamut)**
| Token | Hell | Dunkel |
|---|---|---|
| `--background` | `oklch(0.968 0.014 85)` | `oklch(0.19 0.016 55)` |
| `--foreground` | `oklch(0.21 0.03 55)` | `oklch(0.95 0.018 80)` |
| `--card`, `--popover`, `--sidebar` | `oklch(0.995 0.004 90)` | `oklch(0.235 0.018 55)` |
| `--muted` | `oklch(0.925 0.018 82)` | `oklch(0.28 0.018 55)` |
| `--muted-foreground` | `oklch(0.44 0.03 60)` | `oklch(0.76 0.025 75)` |
| `--primary` | `oklch(0.80 0.145 78)` | `oklch(0.80 0.14 78)` |
| `--primary-foreground` | `oklch(0.21 0.035 55)` | `oklch(0.19 0.03 55)` |
| `--primary-ink` neu | `oklch(0.47 0.115 55)` | `oklch(0.82 0.13 80)` |
| `--ring` | = `--primary-ink` | = `--primary-ink` |
| `--secondary` / `-foreground` | `oklch(0.44 0.07 145)` / `oklch(0.98 0.015 100)` | `oklch(0.66 0.08 145)` / `oklch(0.16 0.02 145)` |
| `--accent` / `-foreground` | `oklch(0.935 0.03 78)` / `oklch(0.25 0.045 50)` | `oklch(0.31 0.03 60)` / `oklch(0.95 0.018 80)` |
| `--destructive` | `oklch(0.50 0.17 28)` | `oklch(0.70 0.15 28)` |
| `--destructive-foreground` neu | `oklch(0.985 0.005 30)` | `oklch(0.17 0.03 30)` |
| `--border` (nur Trennlinien) | `oklch(0.86 0.022 78)` | `oklch(0.34 0.02 60)` |
| `--input` (Rahmen jedes Bedienelements) | `oklch(0.60 0.035 65)` | `oklch(0.56 0.025 70)` |
| `--warning` / `-foreground` | `oklch(0.79 0.15 80)` / `oklch(0.23 0.04 60)` | `oklch(0.80 0.14 80)` / `oklch(0.20 0.03 60)` |
| `--warning-ink` neu | `oklch(0.50 0.11 65)` | `oklch(0.82 0.13 82)` |
| `--success` / `-foreground` | `oklch(0.46 0.08 145)` / `oklch(0.98 0.015 100)` | `oklch(0.70 0.09 145)` / `oklch(0.16 0.02 145)` |
| `--condition-excellent` / `-foreground` / `-ink` | `0.62 0.13 150` / `0.15 0.04 150` / `0.40 0.09 150` | `0.68 0.12 150` / `0.17 0.04 150` / `0.80 0.11 150` |
| `--condition-good` | `0.645 0.13 125` / `0.17 0.04 125` / `0.41 0.09 125` | `0.72 0.12 125` / `0.18 0.04 125` / `0.83 0.11 125` |
| `--condition-average` | `0.665 0.125 88` / `0.19 0.04 90` / `0.44 0.09 85` | `0.77 0.13 90` / `0.20 0.04 90` / `0.86 0.11 90` |
| `--condition-weak` | `0.665 0.14 58` / `0.18 0.04 55` / `0.46 0.12 52` | `0.74 0.13 60` / `0.19 0.04 55` / `0.82 0.11 60` |
| `--condition-critical` | `0.62 0.17 30` / `0.15 0.04 30` / `0.47 0.16 28` | `0.68 0.16 28` / `0.16 0.04 30` / `0.78 0.11 28` |
| `--chart-1` … `--chart-5` | `0.56 0.13 55`, `0.46 0.08 145`, `0.60 0.12 85`, `0.50 0.17 28`, `0.52 0.08 250` | `0.78 0.13 70`, `0.70 0.09 145`, `0.82 0.12 90`, `0.70 0.15 28`, `0.72 0.08 250` |
| `--area-betrieb` / `-erfassung` / `-arbeit` / `-ernte` / `-neutral` neu | `0.46 0.08 150`, `0.50 0.12 60`, `0.48 0.08 250`, `0.52 0.11 75`, `0.45 0.02 60` | `0.74 0.09 150`, `0.80 0.12 70`, `0.76 0.07 250`, `0.80 0.10 80`, `0.76 0.02 70` |
| `--queen-white/-yellow/-red/-green/-blue` neu | `0.97 0.005 90`, `0.86 0.17 95`, `0.58 0.20 27`, `0.58 0.15 145`, `0.52 0.15 255` | gleich, außer rot `0.62 0.20 27`, grün `0.66 0.15 145`, blau `0.62 0.15 255` |
| `--radius`, `--shadow-soft`, `--shadow-elevated` | unverändert | unverändert |
(Condition-Zeilen: Fläche / Text auf Fläche / Tinte, jeweils `oklch(L C H)`.) Entfällt: `--gradient-honey/-sage/-sky`. Sätze in dieser Reihenfolge in `globals.css`: `:root`, `.dark`, `:root[data-contrast="sun"]`, `.dark[data-contrast="sun"]`; `@custom-variant dark (&:where(.dark, .dark *));` dazu `sun`, `pointer-fine`, `pointer-coarse`. `@media print` setzt den hellen Satz; `@media (forced-colors: active)` nutzt `CanvasText`-Striche.
**Sonne (überschreibt nur diese Tokens; Rest erbt vom Grundsatz)**
| Token | Sonne hell | Sonne dunkel |
|---|---|---|
| `--background`, `--card`, `--popover`, `--sidebar` | `oklch(1 0 0)` | `oklch(0.10 0.005 55)` |
| `--foreground`, `--primary-foreground`, `--ring` | `oklch(0.13 0.02 55)` | `--foreground`/`--ring` `oklch(0.99 0.005 85)`, `--primary-foreground` wie dunkel |
| `--muted` | `oklch(0.95 0.01 80)` | `oklch(0.20 0.01 55)` |
| `--muted-foreground` | `oklch(0.32 0.03 60)` | `oklch(0.88 0.02 80)` |
| `--border` | `oklch(0.40 0.03 60)` | `oklch(0.70 0.02 70)` |
| `--input` | `oklch(0.20 0.03 55)` | `oklch(0.92 0.01 80)` |
| `--primary-ink` | `oklch(0.38 0.10 52)` | wie dunkel |
Regeln unter `[data-contrast="sun"]`: `--text-sm: 1rem`, `--text-xs: 0.875rem`, `--focus-width: 3px`, `--control-border-width: 2px`, `--shadow-*: none`, Kopfzeile undurchsichtig (`backdrop-filter: none`), Tönungen `bg-*/15` werden über die Hilfsklasse `.tint` zu `bg-card` mit 2-px-Rahmen in der Tinte, Wabenränder 3/5/7 px statt 2/4/6 px, keine Honig-Tönung unter der Stärkekurve.
**Gemessene Kontraste (Soll, `contrast.spec.ts` prüft jede Zeile mit ihrer Grenze)**
| Paar | hell | dunkel | Sonne hell | Sonne dunkel | Grenze |
|---|---|---|---|---|---|
| Text auf Hauptknopf (`--primary-foreground` / `--primary`) | 9,38 | 9,77 | 10,61 | 9,77 | 7 |
| Grundtext / Seite | 16,23 | 15,98 | 20,15 | 20,01 | 7 |
| Grundtext / Karte | 17,56 | 14,45 | 20,15 | 20,01 | 7 |
| Nebentext / Seite | 7,13 | 8,60 | 12,77 | 14,33 | 4,5 |
| Nebentext / `--muted` | 6,27 | 6,81 | 11,03 | 12,62 | 4,5 |
| Link (`--primary-ink`) / Seite | 6,48 | 10,48 | 10,37 | 11,66 | 4,5 |
| Eingaberahmen (`--input`) / Karte | 3,92 | 3,58 | 18,19 | 16,25 | 3 |
| Eingaberahmen / Seite | 3,62 | 3,96 | 18,19 | 16,25 | 3 |
| Fokusring / Karte (mit Abstand) | 7,01 | 9,47 | 20,15 | 20,01 | 3 |
| Fokusring / Seite | 6,48 | 10,48 | 20,15 | 20,01 | 3 |
| Warn-Icon/-Text (`--warning-ink`) / Karte | 6,09 | 9,49 | 6,18 | 11,68 | 4,5 |
| Warn-Text / Warn-Tönung | 5,53 | 6,92 | 5,60 | 9,37 | 4,5 |
| Fehlertext / Karte | 6,45 | 5,87 | 6,54 | 7,23 | 4,5 |
| Fehlertext / Fehler-Tönung | 5,50 | 5,06 | 5,57 | 6,58 | 4,5 |
| Salbei-Knopf | 7,11 | 6,43 | 7,11 | 6,43 | 4,5 |
| Ampel-Abzeichen (Tinte / Tönung), schlechteste Stufe | 6,06 | 6,45 | 6,14 | 8,49 | 4,5 |
| Text auf Ampelfläche, schlechteste Stufe | 5,02 | 6,32 | 5,02 | 6,32 | 4,5 |
| Ampelfläche / Karte, schlechteste | 3,03 | 5,41 | 3,07 | 6,66 | 3 |
| `chart-1…5` / Karte, schlechteste | 3,93 | 5,87 | 3,99 | 7,23 | 3 |
| Bereichston / Karte, schlechtester | 5,54 | 7,52 | 5,54 | 7,52 | 3 |
| Königinnenfarbe mit Tinten-Ring / Karte (Ring) | 17,56 | 14,45 | 20,15 | 20,01 | 3 |
Die Honigfläche hat gegen die Karte nur 1,87:1; das ist zulässig, weil ein Knopf über seinen Text erkannt wird und gewählte Zustände Rahmen plus Haken tragen.
**Arbeitsregeln für Ausführende**
- Agents committen nie. Der Orchestrator prüft und committet pro Task einmal; Pushes gebündelt (CI nicht zuspammen).
- Standard-Verifikation **V** (Befehle in `ApiCore-Frontend/`): `npm run lint && npm run typecheck && npx vitest run <Pfade> && npm run build`. In parallelen Wellen baut nur der Orchestrator (`.next` ist nicht parallelfähig); Agents lassen `npm run build` dort aus und melden es.
- Standard-Browserprüfung **VB** (nur UI-Tasks): gegen `npm run build && npm run start` mit Demo-Daten, per Chrome DevTools (`emulate`, `resize_page`, `take_screenshot`): Desktop 1366 px und 390 px; Tastatur (Tab-Reihenfolge, sichtbarer Fokus, Pfeiltasten wo zutreffend); `prefers-reduced-motion: reduce`; Modi hell, dunkel, Sonne hell, Sonne dunkel (ab Task A2 schaltbar, dunkel ab A4 ohne Flag); die betroffenen Seiten sind je Task genannt. Befunde als Liste im Bericht, Bilder nicht im Repo.
- `src/styles/baukasten-guard.spec.ts` (angelegt in Task A5) wächst task-weise: jeder Task trägt die Regel ein, die er erfüllt, samt Ausnahmen mit Begründung.
---
## Review Focus
1. **Flackern und blockierter Speicher:** Der erste Frame darf nie im falschen Modus erscheinen; ohne `localStorage` (Privatmodus) muss die App hell starten statt zu brechen.
- Task A2 `the inline script sets class, data-contrast and data-motion before first paint`
- Task A2 `blocked storage falls back to the default`
2. **Unlesbarer Dunkelmodus und Ampel nur über Farbe:** Abzeichen, Warnungen und Zellen müssen in allen vier Sätzen ≥ 4,5:1 (Text) bzw. ≥ 3:1 (Fläche) haben und Icon plus Wort tragen.
- Task A4 `contrast table 4.4 holds in all four sets`
- Task A4 `every condition tone has surface, text-on-surface and ink in all four sets`
- Task B1 `a cell always shows icon and code, never colour alone`
3. **Fehlende Rechte, Offline und alte Backends:** Ohne `inspections:read`, mit 403 je Spur, ohne T4-Felder oder ohne `seasonCalendar` darf nichts brechen und nichts Falsches behaupten.
- Task B1 `without inspections:read the rim is neutral and says nothing about inspections`
- Task B3 `a 403 on a track drops the track without an error`
- Task B6 `without seasonCalendar the wheel uses the season defaults`
4. **Extreme Daten:** 0, 1 und über 100 Völker; zwanzig und mehr Stände; sehr lange Codes; Lücke von über 45 Tagen; Messwerte vor dem Einheitenwechsel; Schaltjahr und Jahreswechsel im Rad.
- Task B1 `an empty apiary shows the empty hint and no comb`
- Task B1 `100 cells render with one shared symbol`
- Task B3 `values before STRENGTH_IN_FRAMES_SINCE are not in the curve but in the table`
- Task B6 `winter rest over the year change is two arcs`
5. **Migration reißt Seiten ein:** Entfernte Props (`gradient`), gelöschte Bausteine (`ToggleChip`, `TabSwitcher`) und verlorene Fokus-/Größenklassen dürfen keine Seite unlesbar oder unbedienbar zurücklassen.
- Task A3 `no file passes a gradient prop any more`
- Task A5 `no component removes the outline without the global rule`
- Task A10 `baukasten guard passes with the documented exceptions only`
- Task A9 `every manifest icon exists with the declared size`
6. **Tastatur und Forced Colors in eigenen Bedienelementen** (Wabenzelle, Tabs, Auswahlgruppe, Marker): Fokus sichtbar, Pfeiltasten, Namen mit sichtbarem Text am Anfang.
- Task B1 `focus draws the ring on the outer hexagon path`
- Task A8 `arrow keys switch the tab and the panel is labelled by it`
---
## Abhängigkeiten und Parallelisierung
**T5a** (nur T3; Tasks A1–A5 laufen in einem kurzen Fenster ohne andere offene Frontend-Tasks, weil sie viele Dateien mechanisch berühren):
| Welle | Tasks (parallel, verschiedene Dateien) | wartet auf |
|---|---|---|
| 1 | A1 | T3 |
| 2 | A2 | A1 |
| 3 | A3 | A1, A2 (gleiche Layout-Dateien) |
| 4 | A4 | A3 (Verlauf-Verbraucher müssen vorher weg sein) |
| 5 | A5, A9 | A5 → A4; A9 → A3 (`BrandMark`-Geometrie) |
| 6 | A6 | A5 |
| 7 | A7 | A6 (`winter-check-dialog`, `label-designer`, `swarm-control-edit-dialog` gemeinsam) |
| 8 | A8 | A7 (`tasks/page.tsx`, `settings/automation/*` gemeinsam) |
| 9 | A11, A12 | A11 → A8 (`today-panel.tsx`, `ui/*`, `globals.css`); A12 → A1, A2 |
| 10 | A10 | A8, A9, A11, A12 |
**T5b** (nach T4a-Frontend; ohne T4 mit Kalender-Rückfall lauffähig, aber dieselben Dateien wie T4 Tasks 17/18/20, darum danach):
| Welle | Tasks (parallel, verschiedene Dateien) | wartet auf |
|---|---|---|
| 1 | B1, B3, B5 | T5a (A10), T4a |
| 2 | B2, B4, B6 | B2 → B1 (`summarizeColonies`); B4 → B3; B6 → B5 (Vertrag) |
| 3 | B7 | B6 |
| 4 | B8 | B2, B4, B7 |
Geteilte Dateien, deshalb je Welle nur ein Task:
- `src/app/globals.css`: A1 (W1), A2 nur falls nötig nicht, A4 (W4), A5 (W5); T5b ändert sie nicht (Waben-CSS liegt in `src/styles/comb.css`, Leaflet-CSS in `src/styles/leaflet-theme.css`, jeweils aus der Komponente importiert).
- `src/app/layout.tsx`, `src/app/providers.tsx`, `src/app/(app)/layout.tsx`, `src/app/(app)/nav-config.ts`, `src/app/(app)/quick-actions.ts`, `src/app/(app)/sidebar.tsx`, `src/app/(app)/bottom-nav.tsx`: A2 (W2), A3 (W3), A9 nur `layout.tsx` Metadaten nicht (liegt in `manifest.ts`), A10 nur Dashboard.
- `src/app/(app)/dashboard/page.tsx`: A3 (W3, Banner raus), A10 (W9, Hinweis-Karte), B7 (W3 T5b). T4 Task 18 (Welle 6 von T4) ändert dieselbe Datei: wer später dran ist, rebased; kein Parallellauf.
- `src/components/ui/*`: A4 (W4, `dark:` raus), A5 (W5), A7 (checkbox), A8 (tabs).
- `src/components/form/choice-group.tsx`, `number-stepper.tsx`, `rating-scale.tsx`: nur A6 (W6) bzw. A4 (Farbklassen, W4).
- `src/app/(app)/winter/winter-check-dialog.tsx`, `settings/label-templates/designer/label-designer.tsx`, `colonies/[id]/swarm-control-edit-dialog.tsx`: A6 (W6), A7 (W7).
- `src/app/(app)/tasks/page.tsx`, `settings/automation/*`: A7 (W7), A8 (W8).
- `src/styles/baukasten-guard.spec.ts`: A5, A6, A7, A8 (je eigene Welle), A10.
- `package.json`/`package-lock.json`: A2 (W2, `next-themes` raus), A9 (W5, `sharp` rein).
- `public/sw.js`, `src/proxy.ts`: nur A9.
- `src/lib/honeycomb/*`: B1 (`cell-state`, `layout`), B2 (`mini-comb-svg`, je eigene Datei).
- `src/app/(app)/apiaries/page.tsx`, `apiaries/[id]/*`: B1 (W1); `apiaries/map/page.tsx`, `src/components/apiary-map.tsx`: B2.
- `src/app/(app)/colonies/[id]/page.tsx`: B4 (W2). `src/lib/autopilot/types.ts`: B6 (W2).
- `ARCHITECTURE.md`, `TASKS.md`: nur der Orchestrator (Texte liefert B8).
---
## Tasks
## Lieferung T5a – Sonnentauglich und Baukasten
### Task A1: Tokens additiv und Kontrast-Wächter (Spec 10.8 Schritt 1)
**Files**
- Create: `src/styles/css-tokens.ts` (+ `css-tokens.spec.ts`), `src/styles/contrast.spec.ts`
- Modify: `src/app/globals.css`
**Interfaces**
- Produces (nur für Tests):
```ts
export type TokenSet = "light" | "dark" | "sun-light" | "sun-dark";
export function tokenSet(css: string, set: TokenSet): Record<string, string>; // Sonne-Sätze liegen auf ihrem Grundsatz
export function oklchOf(value: string): [number, number, number];
export function contrastOf(a: [number, number, number], b: [number, number, number]): number;
export function blend(fg: [number, number, number], alpha: number, bg: [number, number, number]): [number, number, number];
export function inGamut(color: [number, number, number]): boolean;
```
- `contrast.spec.ts` exportiert `VALUES_SWITCHED` (Konstante, hier `false`); Task A4 setzt sie auf `true`.
Neue Tokens (`-ink`, `area-*`, `queen-*`, `--destructive-foreground`, `--primary-ink`, `--warning-ink`, `--condition-*-ink`) und die beiden Sonne-Sätze tragen hier schon ihre Endwerte; bestehende Werte bleiben unverändert, damit nichts bricht.
- [ ] **Schritt 1: Failing Tests**
```ts
// css-tokens.spec.ts
it('reads oklch values with and without alpha', ...);
it('contrast of black on white is 21', ...);
it('blends a 15 % tint over its background before measuring', ...);
it('sun-light layers over light and sun-dark over dark', ...);
// contrast.spec.ts
it('every token in @theme inline exists in light and dark', ...);
it('all tokens are inside the sRGB gamut', ...); // alle vier Sätze, auch Bestand
it.each(AREA_TONES)('area tone %s reaches 3:1 on the card in every set', ...);
it('queen marking colours with the foreground ring reach 3:1 on the card', ...);
it('the focus ring token equals --primary-ink in light and dark', ...);
it('sun sets define every token listed in the sun table', ...);
describe.skipIf(!VALUES_SWITCHED)('table 4.4', () => {
it.each(ROWS)('$id reaches its limit and the measured ratio in $set', ...); // ROWS = Zeilen aus „Gemessene Kontraste“, |gemessen − Soll| ≤ 0,05
});
```
- [ ] **Schritt 2:** `npx vitest run src/styles` → FAIL (Module und Tokens fehlen).
- [ ] **Schritt 3: Implementieren.** `css-tokens.ts` übernimmt die OKLCH-Rechnung aus `src/lib/forms/rating-contrast.spec.ts` (Ottosson), ergänzt um Alpha-Mischung und Gamut-Prüfung; `globals.css`: neue Tokens in `:root` und `.dark` mit den Tabellenwerten, `@theme inline` mit `--color-primary-ink`, `-warning-ink`, `-destructive-foreground`, `-condition-*-ink`, `-area-*`, `-queen-*`, die zwei Sonne-Sätze samt Regeln aus der Sonne-Tabelle, `@custom-variant`s (`dark` mit `:where`, `sun`, `pointer-fine`, `pointer-coarse`), `.tint`, `@media print`, `@media (forced-colors: active)`. `--gradient-*` und alle alten Werte bleiben bis Task A4. `rating-contrast.spec.ts` bleibt bis A4 bestehen.
- [ ] **Schritt 4:** `npx vitest run src/styles src/lib/forms` → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src/styles src/lib/forms src/components`; VB nur ein Blick auf Start in hell (muss unverändert aussehen). Kein Commit.
---
### Task A2: Darstellung umschalten, ohne zu flackern (Spec 5, Schritt 2)
**Files**
- Create: `src/lib/appearance/appearance.ts` (+ Spec), `src/components/appearance-script.tsx` (+ Spec), `src/app/(app)/profile/appearance-section.tsx` (+ Spec)
- Modify: `src/app/layout.tsx`, `src/app/providers.tsx` (+ `providers.spec.tsx`), `src/components/ui/sonner.tsx`, `src/lib/motion/motion-preference.ts`, `src/app/(app)/profile/page.tsx` (+ Spec), `src/app/(app)/layout.tsx` (+ `layout.spec.tsx`), `package.json`, `package-lock.json`
- Delete: `src/app/(app)/profile/motion-section.tsx`
**Interfaces**
- Produces:
```ts
export const APPEARANCE_KEYS: { theme: "apicore.theme"; contrast: "apicore.contrast"; motion: "apicore.motion" };
export type ThemeChoice = "light" | "dark" | "system";
export type ContrastChoice = "normal" | "sun";
export const DEFAULT_THEME: ThemeChoice; // "light" (Offene Frage 3, A)
export function resolveAppearance(stored: { theme?: string | null; contrast?: string | null; motion?: string | null },
env: { prefersDark: boolean; prefersMore: boolean; darkEnabled: boolean }): { dark: boolean; sun: boolean; motion: MotionLevel };
export function useAppearance(): { theme: ThemeChoice; contrast: ContrastChoice; motion: MotionLevel; resolvedTheme: "light" | "dark";
sunActive: boolean; setTheme(v: ThemeChoice): void; setContrast(v: ContrastChoice): void; setMotion(v: MotionLevel): void };
export function appearanceScript(darkEnabled: boolean): string; // IIFE-Quelltext aus APPEARANCE_KEYS und resolveAppearance
export function AppearanceScript(): JSX.Element; // <script dangerouslySetInnerHTML> im <head>
```
- `motion-preference.ts` behält API (`readMotionLevel`, `applyMotionLevel`, `useMotionPreference`, `effectiveMotionLevel`); der `useEffect` in `providers.tsx` entfällt.
- Entwicklungs-Flag `NEXT_PUBLIC_APPEARANCE_DARK=1`: ohne Flag bleiben „Dunkel“ und „Wie Gerät“ ausgeblendet und ein gespeichertes `dark` wird nicht angewendet (Dark ist bis Task A4 mit alten Werten unlesbar). Task A4 entfernt das Flag.
- [ ] **Schritt 1: Failing Tests**
```ts
// appearance.spec.ts
it('defaults to light', ...);
it('system follows matchMedia and reacts to a change', ...);
it('prefers-contrast: more without a stored value selects sun', ...);
it('blocked storage falls back to the default', ...);
it('a storage event from another tab is picked up', ...);
it('dark is ignored without the dev flag', ...);
// appearance-script.spec.ts
it('uses the same storage keys as the hook', ...);
it('the inline script sets class, data-contrast and data-motion before first paint', ...); // Skript in jsdom auswerten
it('sets the theme-color meta to the page colour of the active set', ...); // #f9f4ea / #1a120d / #ffffff / #040302
// appearance-section.spec.tsx
it('choosing Sonne sets data-contrast on the root and stores it', ...);
it('says that the setting only applies to this device', ...);
it('hides Dunkel without the dev flag', ...);
// layout.spec.tsx
it('the user menu offers Sonnenmodus as menuitemcheckbox and toasts Sonnenmodus an', ...);
// providers.spec.tsx
it('the toaster follows resolvedTheme and next-themes is not imported', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/lib/appearance src/components/appearance-script.spec.tsx "src/app/(app)/profile" "src/app/(app)/layout.spec.tsx" src/app/providers.spec.tsx` → FAIL.
- [ ] **Schritt 3: Implementieren.** Muster aus `preventing-flash-before-hydration.md`; `<html suppressHydrationWarning>` und `<AppearanceScript />` im `<head>`; `viewport.themeColor` mit den zwei `media`-Einträgen (`#f9f4ea` / `#1a120d`). `useAppearance` schreibt `localStorage` (in `try/catch`) und das Attribut, hört auf `matchMedia` (nur solange „Wie Gerät“ bzw. nichts gewählt) und auf `storage`. Profil-Karte „Darstellung“ mit drei `ChoiceGroup`-Feldern (Texte aus Spec 5.3) und „Gilt nur für dieses Gerät.“; Benutzermenü bekommt oben `DropdownMenuCheckboxItem` „Sonnenmodus“ (44 px, Sonnen-Icon) mit Toast „Sonnenmodus an“/„aus“. Abmelden löscht die Einstellungen nicht. `next-themes` aus `package.json` entfernen, `sonner.tsx` liest `useAppearance().resolvedTheme`. Login und `/herkunft/[lot]` folgen automatisch (Skript im Root-Layout).
- [ ] **Schritt 4:** Befehl aus Schritt 2 → PASS.
- [ ] **Schritt 5: Verifikation** – V mit denselben Pfaden; VB: Profil > Darstellung, Start, Login; Sonne an/aus mit einem Tipp im Benutzermenü, Seite neu laden (kein Aufblitzen des falschen Modus, auch mit gedrosselter CPU), `prefers-contrast: more` per `emulate` schaltet Sonne vor. Kein Commit.
---
### Task A3: `AREAS`, `AreaIcon`, `BrandMark`, Verläufe aus den Komponenten (Spec 11, 12.1; vor dem Werte-Wechsel)
Reihenfolge-Abweichung zur Spec 10.8: Dieser Task läuft **vor** dem Werte-Wechsel (Task A4), weil A4 die `--gradient-*`-Tokens entfernt und deren 22 Verbraucher dann bereits weg sein müssen.
**Files**
- Create: `src/lib/areas.ts` (+ `areas.spec.ts`), `src/components/area-icon.tsx` (+ Spec), `src/components/brand-mark.tsx` (+ Spec)
- Modify: `src/app/(app)/nav-config.ts` (+ Spec), `src/app/(app)/quick-actions.ts`, `src/app/(app)/sidebar.tsx`, `src/app/(app)/layout.tsx`, `src/components/page-header.tsx`, `src/components/detail-header.tsx` (+ Spec), `src/components/entity-card.tsx`, `src/app/(app)/dashboard/page.tsx`, die Login-Seite unter `src/app/(auth)/`, `src/app/herkunft/[lot]/page.tsx`, alle Seiten mit `gradient=`/`var(--gradient-` (`grep -rlE "gradient=|--gradient-" src`, 31 Dateien), die Icon-Abweichungen (Ernte-Detail `Scissors`, Schleuderung neu `FlaskConical`, Lager nur `Warehouse`, Inventar/Etikettenvorlagen/Königinnen/Kalender/Audit-Log/Profil aus `AREAS`), falls gemergt `INTENT_ICONS` (T4 Task 17)
- Modify (Guard): `src/styles/baukasten-guard.spec.ts` entsteht erst in Task A5; die Regel „kein `gradient`“ trägt Task A10 nach.
**Interfaces**
- Produces:
```ts
export type AreaKey = "dashboard" | "search" | "apiaries" | "colonies" | "queens" | "winter" | "phenology" | "inspections" | "varroa"
| "swarm-control" | "treatments" | "feedings" | "tasks" | "calendar" | "suggestions" | "notifications" | "harvests" | "extractions"
| "honey-batches" | "bottling" | "products" | "raw-materials" | "production-batches" | "storage" | "equipment" | "statistics"
| "users-roles" | "automation" | "label-templates" | "backup" | "audit-log" | "profile";
export type AreaTone = "betrieb" | "erfassung" | "arbeit" | "ernte" | "neutral";
export interface Area { key: AreaKey; label: string; icon: LucideIcon; tone: AreaTone; href?: string }
export const AREAS: Readonly<Record<AreaKey, Area>>; // Tabelle aus Spec 11, Icons exakt wie dort
export function AreaIcon(props: { area: AreaKey; size?: "md" | "sm" }): JSX.Element; // Sechseck 40/32 px, Ton 14 % getönt, 1 px Rand im Ton; Sonne: --card, 2 px Rand
export function BrandMark(props: { size?: 24 | 32; withWordmark?: boolean }): JSX.Element;
// PageHeader({ area, title, description?, actions? }), DetailHeader/EntityCard: Prop `area?: AreaKey` statt `icon` + `gradient`
```
- Der Ton wird nur in `AreaIcon`, dem „+ Neu“-Sheet, den Ereignis-Icons der Lebenslinie (B4) und den Absichts-Icons der Vorschläge gezeigt; Navigationslisten bleiben neutral (Icon `--muted-foreground`, aktiv Honig).
- [ ] **Schritt 1: Failing Tests**
```ts
it('every NAV_GROUPS entry and QUICK_ACTION maps to an AREAS key with the same icon', ...);
it('no area tone is a status colour', ...);
it('AreaIcon draws the icon inside a hexagon tinted with the area tone and is aria-hidden', ...);
it('PageHeader renders the AreaIcon of its area and has no gradient prop', ...); // plus @ts-expect-error auf gradient
it('BrandMark renders the capped cell and the ApiCore wordmark', ...);
it('harvest detail uses Scissors, extraction create FlaskConical and storage only Warehouse', ...);
it('the dashboard header has no honey banner and no white text', ...);
it('no file passes a gradient prop any more', ...); // liest src/**/*.tsx
```
- [ ] **Schritt 2:** `npx vitest run src/lib/areas.spec.ts src/components "src/app/(app)/nav-config.spec.ts"` → FAIL.
- [ ] **Schritt 3: Implementieren.** `BrandMark` zeichnet die verdeckelte Wabenzelle (Sechseck mit spitzer Oberseite in `--primary` mit 6 % Rand in `--foreground`, darin kleineres Sechseck `oklch(0.88 0.11 85)` leicht nach oben); dieselbe Geometrie nutzt Task A9 für das Master-SVG (die Pfade stehen in `src/components/brand-mark.tsx` als exportierte Konstante `BRAND_CELL_PATHS`). Alle `gradient`-Props und `style={{ background: "var(--gradient-…)" }}` verschwinden; die Dashboard-Kopfzeile wird `h1` in Tinte auf dem Seitenhintergrund (Rad und Kacheln-Ersatz folgen erst in B7). Die Zählkacheln bleiben bis B7 bestehen, bekommen aber nur noch `AreaIcon`.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src`; VB: Start, Bienenstände, Völker, Königinnen, Kalender, Inventar, Login, Herkunft, Seitenleiste und Bottom-Nav (Icons neutral, aktiver Eintrag lesbar). Kein Commit.
---
### Task A4: Farb-Codemod und Tokenwerte (Spec 10.8 Schritte 3 und 4, Spec 4.2)
**Files**
- Create: `scripts/codemod-t5-colors.mjs` (+ `scripts/codemod-t5-colors.spec.mjs`)
- Modify: `src/app/globals.css` (Werte aus der Token-Tabelle, `--gradient-*` entfernen), `src/styles/contrast.spec.ts` (`VALUES_SWITCHED = true`), `src/components/ui/*` (alle `dark:`-Klassen raus; `button.tsx` Standard ohne Verlauf), `src/lib/colonies/labels.ts` (`COLONY_CONDITION_TONE_CLASS`), `src/components/colonies/status-light-badge.tsx`, `src/components/form/rating-scale.tsx`, `src/lib/appearance/appearance.ts` (Flag entfällt), `appearance-section.tsx`, alle Treffer des Codemods (`grep -rlE "text-primary\b|text-warning\b|border-primary\b" src`, 22 + 6 Dateien) und die acht Dateien mit `dark:`
- Delete: `src/lib/forms/rating-contrast.spec.ts` (aufgegangen in `contrast.spec.ts`)
**Interfaces**
- Produces: `export function transform(source: string): string` (rein, idempotent) in `scripts/codemod-t5-colors.mjs`; CLI `node scripts/codemod-t5-colors.mjs [--check] <glob…>`.
- Regeln: `text-primary` → `text-primary-ink`; `text-warning` → `text-warning-ink`; `text-condition-x-foreground` in Abzeichen-Klassen → `text-condition-x-ink`; `border-primary` in Zeilen mit Auswahlzustand (`aria-pressed`, `aria-checked`, `data-checked`, `selected`) → `border-foreground`. `bg-primary`, `text-primary-foreground`, `bg-primary/…` bleiben.
- Gleiche Reihenfolge einhalten: erst Codemod (Schritt 3), Ergebnis per Diff prüfen, V grün; dann Werte umstellen.
- [ ] **Schritt 1: Failing Tests**
```js
// codemod-t5-colors.spec.mjs
it('text-primary becomes text-primary-ink', ...);
it('text-primary-foreground, bg-primary and text-primary/80 stay unchanged', ...);
it('text-warning becomes text-warning-ink but text-warning-foreground stays', ...);
it('badge classes with text-condition-good-foreground become -ink', ...);
it('border-primary on a selection state becomes border-foreground', ...);
it('is idempotent', ...);
// contrast.spec.ts (jetzt hart)
it('table 4.4 holds in all four sets', ...); // VALUES_SWITCHED = true
it('every condition tone has surface, text-on-surface and ink in all four sets', ...);
it('no --gradient-* token remains', ...);
// weitere
it('the default button uses solid primary with primary-foreground and no gradient', ...);
it('dark is selectable without a flag', ...);
it('ui components contain no dark: classes', ...);
```
- [ ] **Schritt 2:** `npx vitest run scripts src/styles src/components/ui src/lib/appearance` → FAIL.
- [ ] **Schritt 3: Implementieren.** (a) Codemod schreiben, ausführen, `git diff` auf Fehltreffer prüfen. (b) Werte aus der Token-Tabelle einsetzen (hell, dunkel, `--sidebar*`, `--chart-*`), `--primary-foreground` Standard-Knopf ohne Verlauf, `ui/*` ohne `dark:` (der Modus ist nur ein anderer Tokensatz). Einzige `dark:`-Ausnahme: Leaflet-Kachelfilter, nur in CSS (B2). (c) `NEXT_PUBLIC_APPEARANCE_DARK` aus Code und `.env`-Beispielen entfernen.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src`; VB in **allen vier Modi** auf Start, Bienenstände, Stand, Volk, Durchsicht neu, Varroa neu, Profil, Login, Herkunft: Ampel-Abzeichen, Warnungen, Links, Hauptknopf (dunkle Tinte auf Honig), Eingaberahmen sichtbar. Kein Commit.
---
### Task A5: Fokus, Größen, Schrift (Spec 4.5, 4.6, Schritt 5)
**Files**
- Create: `scripts/codemod-t5-focus.mjs` (+ Spec), `src/styles/baukasten-guard.spec.ts`
- Modify: `src/app/globals.css` (`--text-xs: 0.8125rem`, Basisregel `:where(:focus-visible)`, `--focus-width/-offset`, Trefferfläche `pointer-coarse`), `src/components/ui/button.tsx`, `input.tsx`, `select.tsx`, `textarea.tsx`, `tabs.tsx`, `checkbox.tsx` (Fokus), `src/components/form/field.tsx`, `number-stepper.tsx`, `choice-group.tsx`, `src/components/empty-state.tsx`, `src/components/detail-header.tsx`, alle Treffer von `ring-ring/50`, `outline-none` (≈ 20 Dateien), `sm:h-` (4 Dateien), `md:text-sm` in Input/Textarea
**Interfaces**
- Produces: `transform(source)` im Fokus-Codemod entfernt `outline-none`, `focus-visible:ring-3`, `focus-visible:ring-ring/50`, `focus-visible:border-ring`, `has-[:focus-visible]:outline-*` und wandelt `sm:h-*`/`sm:min-h-*` in `pointer-fine:h-*` um; bei einem inneren Fokuselement (Label-Zeile mit verstecktem Input) bleibt `has-[:focus-visible]:outline`-Ersatz am äußeren Element.
- Guard-Regelformat (alle folgenden Tasks tragen hier ihre Regel ein): `{ id: string; pattern: RegExp; scope: "src/**/*.tsx"; allow: { file: string; reason: string }[] }`. Regeln dieses Tasks: kein `ring-ring/50`, kein `outline-none` (Ausnahme `honeycomb-cell.tsx`, Grund in B1 eintragen), kein `sm:h-`, kein `md:text-sm` in `ui/input.tsx`/`ui/textarea.tsx`.
- Größen: `Button`/`Input` `size="default"` 44 px (`min-h-11`), kompakt nur mit `pointer-fine:`; `icon`/`icon-sm` bekommen bei `pointer-coarse` eine 44-px-Trefferfläche über `after:`; `Field`-Label, `ChoiceGroup`-Option, `CheckboxField`-Label, Button `touch` (statt `text-[0.95rem]`), `Input`/`Textarea`, Listentitel, `DetailHeader`-Untertitel und `EmptyState`-Beschreibung auf 16 px (`text-base`).
- [ ] **Schritt 1: Failing Tests**
```ts
it('Button default is 44 px high and compact only with pointer-fine', ...);
it('icon buttons expose a 44 px hit area under pointer-coarse', ...);
it('Input and Textarea use text-base without md:text-sm', ...);
it('NumberStepper buttons keep min-h-11 and drop their own ring classes', ...);
it('the base layer draws the focus ring with --focus-width and --focus-offset', ...); // liest globals.css
it('--text-xs is 0.8125rem', ...);
it('no component removes the outline without the global rule', ...); // Guard-Regeln dieses Tasks
it('the focus codemod is idempotent and keeps pointer-fine classes', ...); // scripts/codemod-t5-focus.spec.mjs
```
- [ ] **Schritt 2:** `npx vitest run scripts src/styles src/components` → FAIL.
- [ ] **Schritt 3: Implementieren.** Codemod ausführen, Diff prüfen; Fehlerzustand (`aria-invalid`): Rahmen 2 px `--destructive`, Ring bleibt `--ring`. Bestehendes `scroll-padding` unverändert lassen.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src`; VB: Tab durch Durchsicht neu, Profil, Bienenstände-Filter; Fokus sichtbar auf Karte und Seite in allen vier Modi, Tablet 820 px mit Touch-Emulation hat 44-px-Knöpfe (`pointer-coarse`), Desktop-Maus bleibt kompakt. Kein Commit.
---
### Task A6: Auswahlgruppe und Plus/Minus (Spec 10.2, 10.5)
**Files**
- Modify: `src/components/form/choice-group.tsx` (+ Spec), `src/components/form/rating-scale.tsx` (+ Spec), `src/components/ui/button.tsx` (Prop `pressed`), `src/app/(app)/winter/winter-check-dialog.tsx` (+ Spec), `src/app/(app)/settings/label-templates/designer/label-designer.tsx` (+ Spec), `src/app/(app)/profile/appearance-section.tsx`, `src/app/(app)/colonies/[id]/varroa-edit-dialog.tsx`, `src/app/(app)/swarm-control/new/page.tsx`, `src/app/(app)/colonies/[id]/swarm-control-edit-dialog.tsx`, `src/components/stock/stock-card.tsx` (+ Spec), die Etikettendruck-Startposition (`grep -rn "startPosition\|Startposition" src`), `src/styles/baukasten-guard.spec.ts`
- Unverändert: Kalender-Tageswahl (`calendar/page.tsx`, bekommt nur Fokus und Auswahl-Optik, Guard-Ausnahme)
**Interfaces**
- Produces:
```ts
export interface ChoiceOption<T extends ChoiceValue> { /* wie bisher */ icon?: LucideIcon }
// ChoiceGroupProps += appearance?: "buttons" | "cards"; // "cards": Label 16 px + Beschreibung 14 px, volle Breite, min-h-14
// ChoiceOption += description?: string (nur "cards")
// Button += pressed?: boolean -> aria-pressed (Toolbar-Umschalter, kein Radio)
```
- Optik: nicht gewählt `--card` + 1 px `--input`; gewählt `--primary` + `--primary-foreground` halbfett + 2 px `--foreground` + Haken-Icon vor dem Label; „Nicht geprüft“ (`null`) `--muted` + 2 px gestrichelt `--foreground` + Strich-Icon; Skalen behalten `selectedClassName`, bekommen Rahmen und Haken vor der Zahl („✓ 4“). Tastatur unverändert (ein Tab-Halt, Pfeile, Home, End).
- Umstellung `NumberStepper` (beim Bau je Feld die Regel 10.5 bestätigen: ganzzahlig, meist < 100, am Stand erfasst; Abweichungen im Bericht): besetzte Wabengassen (`winter-seams`), Milbenzahl im Varroa-Bearbeiten-Dialog, Weiselzellen in Schwarmkontrolle neu und bearbeiten, Entnahme und gezählter Bestand in `stock-card`, Startposition im Etikettendruck. Bleibt Eingabefeld: Wartezeit, Schlupfjahr, Mengen in Gramm, MFA-Codes, Spalten/Zeilen, Regel- und Vorlagenwerte.
- Umstellung `ChoiceGroup`: Ausgang des Winters (mit `icon`, `appearance="cards"`), Linien-Ausrichtung im Etiketten-Designer, Bewegungsstufe, Farbschema und Kontrast in `appearance-section.tsx` (`appearance="cards"`).
- [ ] **Schritt 1: Failing Tests**
```tsx
it('the chosen option has aria-checked true, the tick icon and the ink border', ...);
it('„Nicht geprüft“ chosen shows the dash icon and a dashed border', ...);
it('appearance cards shows the description and is at least 56 px high', ...);
it('an option icon renders before the label', ...);
it('arrow keys, Home and End move the selection as before', ...);
it('RatingScale shows the tick before the chosen number and keeps its tone', ...);
it('Button pressed sets aria-pressed', ...);
it('winter seams use NumberStepper', ...);
it('the varroa edit dialog counts mites with NumberStepper', ...);
it('swarm control new and edit count queen cells with NumberStepper', ...);
it('stock card withdraw and counted stock use NumberStepper', ...);
it('label print start position uses NumberStepper', ...);
it('the label designer line alignment is a ChoiceGroup', ...);
// Guard
it('no role radio, radiogroup or type radio outside choice-group.tsx', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/components/form src/components/stock src/styles "src/app/(app)/winter" "src/app/(app)/profile" "src/app/(app)/settings/label-templates" "src/app/(app)/colonies" "src/app/(app)/swarm-control"` → FAIL.
- [ ] **Schritt 3: Implementieren** wie oben.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit denselben Pfaden; VB: Winterkontrolle, Profil > Darstellung, Varroa-Bearbeiten, Schwarmkontrolle neu, Etikettendruck; Brutwaben mit dem Stepper von 0 auf 6 (Handschuh-Größe 44 px) in allen vier Modi. Kein Commit.
---
### Task A7: Checkbox-Baustein und alle 17 Stellen (Spec 10.3)
**Files**
- Create: `src/components/form/checkbox-field.tsx` (+ Spec)
- Modify: `src/components/ui/checkbox.tsx` (+ Spec), `src/components/form/colony-multi-select.tsx`, `src/components/harvest-select-field.tsx`, `src/components/today/today-panel.tsx` (+ `honey-check` in `globals.css` ist Tag-gebunden: Animation bleibt), `src/app/(app)/extractions/new/page.tsx`, `harvests/new/page.tsx`, `harvests/[id]/harvest-edit-dialog.tsx`, `swarm-control/new/page.tsx`, `colonies/[id]/swarm-control-edit-dialog.tsx`, `tasks/page.tsx`, `winter/winter-check-dialog.tsx`, `settings/automation/calendar-sync-tab.tsx`, `create-webhook-dialog.tsx`, `edit-webhook-dialog.tsx`, `edit-task-template-dialog.tsx`, `settings/label-templates/designer/label-designer.tsx`, `settings/users-roles/permission-matrix.tsx`, `src/styles/baukasten-guard.spec.ts`
- Delete: `src/components/form/toggle-chip.tsx` (nach der Umstellung seiner vier Nutzer)
**Interfaces**
- Produces:
```ts
// ui/checkbox.tsx (Base UI): 20 px, 2 px --input, Radius 6; gewählt --primary + 2 px --foreground + Haken 14 px (Strichstärke 3, --primary-foreground); indeterminate = Strich; disabled 50 % mit Hinweistext am Feld
export interface CheckboxFieldProps { label: string; description?: string; checked: boolean | "indeterminate";
onCheckedChange(next: boolean): void; disabled?: boolean; disabledHint?: string;
appearance?: "row" | "tile" | "task"; size?: "default" | "sheet" }
export function CheckboxField(props: CheckboxFieldProps): JSX.Element;
// row: ganze Zeile klickbar, min-h-11 (sheet: min-h-14), 16 px; tile: umrandete Kachel (gewählt: Honig-Tönung + 2 px Tinte; Sonne: --card + Tinten-Rand); task: Abhak-Zelle der Heute-Liste mit der honey-check-Bewegung auf dem Base-UI-Primitiv
```
- Reihenfolge: Bausteine, dann Formulare, dann Einstellungen, zuletzt `today-panel` (`task`). Native `type="checkbox"` (7 Dateien) → `CheckboxField appearance="row"`; `ToggleChip` (4 Nutzer) → `appearance="tile"`; Base-UI-Checkbox-Nutzer (3) → neue Optik.
- [ ] **Schritt 1: Failing Tests**
```tsx
it('the whole row toggles and the name comes from the label', ...);
it('the row is at least 44 px high (min-h-11) and in a sheet 56 px (min-h-14)', ...);
it('tile and task have role checkbox from the primitive', ...);
it('checked shows the tick icon and a 2 px foreground border', ...);
it('indeterminate shows the dash', ...);
it('disabled shows its hint text', ...);
it('today panel ticking a task keeps the honey-check moment only with full motion', ...);
it('the harvest dialog, webhook dialogs and task template dialog use CheckboxField', ...);
// Guard
it('no role checkbox or type checkbox outside ui/checkbox.tsx', ...);
it('ToggleChip is gone', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/components src/styles "src/app/(app)"` → FAIL.
- [ ] **Schritt 3: Implementieren** wie oben; `ui/checkbox` ist der einzige Ort mit `role="checkbox"`.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src`; VB: Heute (abhaken, Tastatur Leertaste, Bewegung nur bei „Voll“), Ernte neu, Webhook anlegen, Rollenmatrix, Etiketten-Designer in allen vier Modi. Kein Commit.
---
### Task A8: Tabs-Baustein (Spec 10.4)
**Files**
- Modify: `src/components/ui/tabs.tsx` (+ neue Spec), `src/components/active-history-tabs.tsx` (+ Spec), `src/app/(app)/apiaries/page.tsx`, `colonies/page.tsx`, `honey-batches/page.tsx`, `products/page.tsx`, `queens/page.tsx`, `storage/page.tsx`, `tasks/page.tsx`, `statistics/page.tsx`, `settings/automation/page.tsx`, `settings/automation/task-templates-tab.tsx`, `settings/users-roles/page.tsx`, `settings/users-roles/roles-tab.tsx`, `src/styles/baukasten-guard.spec.ts`
- Delete: `src/components/tab-switcher.tsx`
**Interfaces**
- Produces: `Tabs`/`TabsList`/`TabsTrigger`/`TabsContent` auf Base UI, Variante `segment` (Standard; `line` entfällt): Leiste `--muted` mit 4 px Innenabstand, Reiter 44 px (`pointer-fine:h-9`), 16 px, aktiv `--card` + halbfett + 3 px Strich `--primary-ink`.
```ts
export interface ActiveHistoryTabsProps { value: EntityLifecycleTab; onValueChange(v: EntityLifecycleTab): void;
activeCount?: number; historyCount?: number; children: { active: ReactNode; history: ReactNode } }
export function ActiveHistoryTabs(props: ActiveHistoryTabsProps): JSX.Element; // Vorlage auf Tabs, je Panel die Liste
```
- Guard-Regeln: kein `role="tablist"`/`role="tab"` außerhalb `ui/tabs.tsx`; `aria-pressed` nur in `ui/button.tsx` und `calendar/page.tsx`.
- [ ] **Schritt 1: Failing Tests**
```tsx
it('arrow keys switch the tab and the panel is labelled by it', ...);
it('the active tab has the surface, bold text and the 3 px ink bar', ...);
it('ActiveHistoryTabs shows the counts and renders the matching panel', ...);
it('the statistics page uses the segment variant', ...);
it('both settings pages use Tabs directly', ...);
it('TabSwitcher is gone', ...);
// Guard
it('no tablist or tab role outside ui/tabs.tsx', ...);
it('aria-pressed only in ui/button.tsx and the calendar page', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/components src/styles "src/app/(app)"` → FAIL.
- [ ] **Schritt 3: Implementieren;** die neun Listenseiten legen ihre Liste in das Panel, die Query-Logik bleibt.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src`; VB: Bienenstände Aktiv/Historie, Aufgaben, Statistik, Automatisierung, Benutzer & Rollen: Pfeiltasten, Fokus, 390 px (Leiste bricht um statt zu überlaufen), vier Modi. Kein Commit.
---
### Task A9: Manifest, Icons, Service Worker (Spec 12)
**Files**
- Create: `public/brand/apicore-mark.svg`, `scripts/generate-icons.mjs` (idempotent), erzeugte Dateien `public/icons/icon-192.png`, `icon-512.png`, `maskable-192.png`, `maskable-512.png`, `monochrome-512.png`, `shortcut-inspection-96.png`, `shortcut-varroa-96.png`, `shortcut-today-96.png`, `src/app/apple-icon.png`, `src/app/favicon.ico` (ersetzt), `src/app/manifest.spec.ts`, `public/sw-icons.spec.ts` bzw. Ergänzung in `src/lib/offline/sw.spec.ts`
- Modify: `public/icon.svg`, `src/app/manifest.ts`, `public/sw.js` (`SHELL_CACHE = "apicore-shell-v5"`, neue Icons in den Shell-Cache), `src/proxy.ts` (`matcher` nimmt `/icons/` und `apple-icon.png` aus), `package.json` (`sharp` als devDependency, Skript `"icons": "node scripts/generate-icons.mjs"`)
- Delete: `src/app/apple-icon.tsx`
**Interfaces**
- `manifest.ts` exakt wie Spec 12.3: `id "/"`, `lang "de"`, `scope "/"`, `background_color` und `theme_color` `#f9f4ea`, Icons (any 192/512, maskable 192/512, monochrome 512, `icon.svg` any), Shortcuts „Durchsicht erfassen“ (`/inspections/new`), „Varroa messen“ (`/varroa/new`), „Heute“ (`/dashboard`) mit den 96-px-Icons. `public/sw-routes.js` bleibt unverändert (die Shortcut-Routen stehen schon in `SHELL_ROUTES`), `SW_ROUTES_VERSION` bleibt.
- App-Icon: Zelle auf Tinte `oklch(0.24 0.035 55)` (#2c1b0f); maskable randlos, Zelle höchstens 66 % der Kante; Monochrom weiß auf transparent; `favicon.ico` mit 16/32/48 px. `sharp` schreibt kein ICO: das Skript baut den ICO-Container von Hand (Header + PNG-Einträge), keine weitere Abhängigkeit. Die Master-Geometrie entspricht `BRAND_CELL_PATHS` aus Task A3. Die erzeugten Dateien werden committet, das Docker-Image hängt nicht vom Skript ab.
- [ ] **Schritt 1: Failing Tests**
```ts
it('every manifest icon exists under public with the declared size', ...); // PNG-IHDR lesen
it('declares any and maskable icons at 192 and 512 and one monochrome icon', ...);
it('background_color and theme_color equal the light --background', ...); // gegen globals.css
it('every shortcut url is in SHELL_ROUTES', ...);
it('the service worker cache version is apicore-shell-v5', ...);
it('favicon.ico holds 16, 32 and 48 px entries', ...);
it('the proxy matcher excludes icons and apple-icon.png', ...);
it('running the icon script twice leaves the files byte-identical', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/app/manifest.spec.ts src/lib/offline` → FAIL.
- [ ] **Schritt 3: Implementieren;** Anleitungen `manifest.md`, `app-icons.md` vorher lesen; danach `npm run icons` einmal ausführen und die erzeugten Dateien prüfen (Ansicht des Lochs der maskable-Zone).
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src/app src/lib/offline`; VB: Chrome DevTools > Application > Manifest (Icons, maskable-Vorschau, Shortcuts) und Service Worker nach Update (neuer Cache `apicore-shell-v5`, App startet offline). Kein Commit.
---
### Task A10: Wächter vollständig, Hinweis „Sonnenmodus“, Sichtprüfung T5a (Spec 10.7, 15.4)
**Files**
- Modify: `src/styles/baukasten-guard.spec.ts`, `src/app/(app)/dashboard/page.tsx`, `src/app/(app)/dashboard/page.spec.tsx` (neu)
- Create: `src/components/appearance-notice.tsx` (+ Spec)
**Interfaces**
- Guard-Regeln, die noch fehlen: kein `text-primary` ohne `-foreground`/`-ink` (Regex `text-primary(?![\w-])` bzw. nur `-foreground`/`-ink`-Suffix erlaubt), kein `gradient-` und kein `text-white` (Ausnahme `herkunft/[lot]/page.tsx` nur für das Foto-Overlay, falls nötig), kein `dark:` (Ausnahme `apiary-map.tsx` entfällt, weil der Kachelfilter in CSS liegt), keine Schriftgröße unter 13 px (`text-[10px]`, `text-[11px]`, `text-[12px]`), `PageHeader`/`DetailHeader`/`EntityCard` ohne Prop `gradient`. Der Test liest `src/**/*.tsx` ohne `*.spec.tsx`; jede Ausnahme steht mit Grund in der Liste.
- `AppearanceNotice`: einmalig auf Start „Neu: Sonnenmodus für draußen – im Benutzermenü.“ mit „Ausprobieren“ (schaltet Sonne) und „Schließen“; gemerkt je Gerät (`apicore.notice.sun`, `try/catch`).
- [ ] **Schritt 1: Failing Tests**
```tsx
it('baukasten guard passes with the documented exceptions only', ...);
it('every guard exception has a reason', ...);
it('the notice shows once, Ausprobieren enables sun and Schließen remembers it', ...);
it('the notice works when storage is blocked', ...);
it('the dashboard shows no white text on honey', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/styles src/components/appearance-notice.spec.tsx "src/app/(app)/dashboard"` → FAIL.
- [ ] **Schritt 3: Implementieren.** Danach Sichtprüfung T5a: Screenshot-Matrix (Start, Stand, Bienenstände, Volk, Durchsicht neu, Varroa neu, Profil > Darstellung, Login, Herkunft × vier Modi × 390/820/1366 px; `forced-colors: active` und `prefers-reduced-motion: reduce` je einmal auf Start und Durchsicht neu), Lighthouse Barrierefreiheit ≥ 95 auf Start, Volk, Durchsicht neu, Profil in hell und Sonne, `impeccable detect --json` über die geänderten Dateien (0 Befunde). Befunde als Liste an den Orchestrator; bei Tokenänderungen `contrast.spec.ts` mit anpassen. Der Draußen-Test mit dem Inhaber (Spec 13.4) ist keine Agent-Aufgabe und läuft nach dem Rollout.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src` (volle Suite) und `npm run build`; VB wie in Schritt 3. Kein Commit.
---
### Task A11: Bewegungs-Sprache durchsetzen (Spec §17, neu im Review 2026-10-16)
Vorher laden:
- `animate` bzw. `ecc:motion-foundations` für Konzept und Tokens
- `motion` (Docs „Reduce bundle size“, `LazyMotion`, `layout`, `AnimatePresence`)
- danach `fixing-motion-performance`
Skill-Routing: erst Konzept, dann CSS, dann `motion`. GSAP nicht.
**Files**
- Create:
- `src/lib/motion/tokens.ts` (+ `tokens.spec.ts`)
- `src/components/app-motion.tsx` (+ Spec): `LazyMotion` mit `strict`, Features asynchron, `MotionConfig` je Stufe
- `src/lib/motion/motion-features.ts`: `export default domMax`, nur per dynamischem Import
- `scripts/codemod-t5-motion.mjs` (+ Spec)
- Modify:
- `src/app/globals.css`:
- `--motion-fade`, `--motion-enter`, `--motion-settle`, `--motion-moment`, `--ease-in`, `--ease-spring`, `--motion-shift`
- `--animate-page-in` auf `--motion-enter`
- `next-step-in` ohne `filter: blur`
- `.today-row`-Faltung entfernen
- `src/app/(app)/layout.tsx` (`<AppMotion>` um den Inhalt)
- `src/app/(app)/page-transition.tsx`
- `src/components/ui/sheet.tsx`, `dialog.tsx`, `dropdown-menu.tsx`, `select.tsx` (Dauern und Kurven aus Tokens, Austritt `--ease-in`)
- `src/components/ui/button.tsx` (`motion-full:active:scale-[0.97]`, kein `transition-all`)
- `src/components/ui/badge.tsx`, `card.tsx`, `input.tsx`, `textarea.tsx`, `tabs.tsx` (`transition-all` raus; Karte ohne Hover-Heben)
- `src/app/(app)/sidebar.tsx` (ohne `group-hover:scale-110`, ohne `transition-all`)
- `src/components/stat-bar-list.tsx` (Balken ohne Übergang)
- `src/components/today/today-panel.tsx` und `today/use-task-actions.ts`: Wegfalten über `AnimatePresence` und `m.li layout` mit `SPRING_FLIGHT`; `EXIT_DELAY_MS` bleibt
- `src/components/offline/pending-queue-sheet.tsx` und `src/app/(app)/voice/page.tsx`: Entfernen über dasselbe Muster
- alle `motion-safe:`-Treffer mit Verschiebung, Drehung, `animate-pulse` oder `animate-spin` (Codemod → `motion-full:`). Heute betroffen:
- `rule-card-shell.tsx`, `suggestions/page.tsx`, `restriction-override.tsx`, `form-section.tsx`, `suggestion-group.tsx`
- `hive-card.tsx`, `colonies/[id]/hive/page.tsx`, `voice-capture-card.tsx`, `voice-recorder.tsx`
- `label-compliance-panel.tsx` (`motion-reduce:animate-none` → `motion-full:animate-spin`)
- Beute als Verbraucher derselben Quelle:
- `src/components/hive/hive-scene/quality.ts` (`SPRING_STIFFNESS`/`SPRING_DAMPING` aus `tokens.ts`)
- `use-hive-motion.ts` (`SUBTLE_FADE_SECONDS` aus `tokens.ts`)
- `hive-visual-slot.tsx` (`FADE_MS.subtle` = `--motion-fade`)
- `src/styles/baukasten-guard.spec.ts`: Regelgruppe „Bewegung“ (Spec §17.7)
- Unverändert: three.js-Szene (`use-animated-node.ts`, `model-rig.tsx`); `.honey-check` (nur Fokus-Ausnahme aus A5).
**Interfaces**
```ts
export const MOTION = { fadeMs: 150, enterMs: 200, settleMs: 350, momentMs: 420, shiftPx: 8 } as const;
export const SPRING_FLIGHT = { type: "spring", stiffness: 210, damping: 21, mass: 1 } as const; // = Beute-Feder
export const SUBTLE_FADE_SECONDS: number; // MOTION.fadeMs / 1000
export function springEasing(points?: number): string; // CSS linear(...) aus springStep; erzeugt --ease-spring
export function transitionFor(level: MotionLevel, kind: "enter" | "exit" | "fade" | "layout"): Transition; // off → { duration: 0 }
export function scrollBehaviorFor(level: MotionLevel, distancePx: number, viewportPx: number): ScrollBehavior; // smooth nur bei full und < 1 Bildschirm
export function AppMotion(props: { children: ReactNode }): JSX.Element; // full: reducedMotion "never"; subtle: "always"; off: transition duration 0
```
- [ ] **Schritt 1: Failing Tests**
```ts
// tokens.spec.ts
it('css motion variables equal the TS tokens', ...); // liest globals.css
it('the flight spring equals the hive spring in quality.ts', ...);
it('--ease-spring samples stay within 0.02 of springStep', ...);
it('transitionFor off has zero duration and subtle has no spatial part', ...);
it('scrollBehaviorFor is smooth only at full and below one viewport', ...);
// app-motion.spec.tsx
it('sets reducedMotion per level and follows a level change in the same tab', ...);
// today-panel.spec.tsx (neu, gibt es noch nicht)
it('a ticked task leaves via layout at full and instantly at off', ...);
it('a ticked task never collapses grid-template-rows', ...);
// guard (baukasten-guard.spec.ts, Gruppe Bewegung)
it('no motion-safe: on translate, scale, rotate, slide, zoom or animate classes', ...);
it('no transition-all and no duration above 420 ms outside components/hive', ...);
it('hover translate or scale only inside components/hive', ...);
it('motion/react is used only for LazyMotion, MotionConfig, AnimatePresence and hooks; elements come from motion/react-m', ...);
it('globals.css keyframes and transitions do not animate width, height, grid-template-rows, filter or box-shadow', ...);
it('stat bars and other data values have no transition', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/lib/motion src/components/app-motion.spec.tsx src/components/today src/styles scripts` → FAIL.
- [ ] **Schritt 3: Implementieren.**
- Codemod (`motion-safe:` → `motion-full:` nur für Verschiebung, Pulsieren und Drehen; Farbübergänge bleiben ohne Variante) ausführen und den Diff prüfen.
- Dann die Tokens setzen, `AppMotion` einbauen und die Listen umstellen.
- Skelett-Verzögerung 150 ms und Spinner ab 300 ms mit Wort, wo `QueryState`/Knöpfe das heute nicht tun (Spec §17.4).
- Toast bei „Dezent“ ohne Gleiten (Sichtprüfung; falls nötig CSS an `[data-sonner-toaster]`).
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src scripts`; Bundle-Messung `motion`-Chunk ≤ 30 kB gzip, nicht im First Load von Login und Herkunft (Build-Ausgabe).
- VB:
- jedes Muster aus Spec §17.4 auf Start (Abhaken), Aufgaben, Warteschlangen-Sheet, Volk (Sheet, Beute), `/voice` (Entfernen), `/voice/new` (Aufnahme-Punkt)
- je Voll, Dezent, Aus und `prefers-reduced-motion: reduce`, 390 px und 1366 px
- Performance-Trace bei 4-facher CPU-Drosselung (390 px):
- Sheet auf/zu, Abhaken mit Wegfalten, Seitenwechsel
- kein Frame > 32 ms, kein Layout-Eintrag während der Bewegung außer der `layout`-Messung
- Danach `review-animations` über den Diff.
- Kein Commit.
---
### Task A12: Beute-Bühne an Sonne und Darstellungswechsel anschließen (Spec §17.8, Beute-Spec §9.3, neu im Review 2026-10-16)
**Files**
- Modify:
- `src/components/hive/hive-scene/theme.ts` (Tokens bei `apicore:appearance` neu lesen statt einmalig)
- `hive-canvas.tsx` (Bloom aus in Sonne)
- `materials.ts` und `frame-textures.ts` (Sonne: Kanten 2 px reinweiß; Rähmchen-Arten als Schraffur je Art statt Tönung)
- `hive-labels.tsx` und `src/components/hive/hive-glass.ts` (Sonne: Etiketten 16 px, kein `backdrop-blur`)
- `src/components/hive/hive-card.tsx` und `src/app/(app)/colonies/[id]/hive/page.tsx` (Bühnen-Wurzel bekommt `.holo-stage`)
- Specs dazu (`hive-scene-content.spec.tsx`, `frame-textures.spec.ts`)
- Consumes: Holo-Sonne-Tokens und `.holo-stage` (A1), Ereignis `apicore:appearance` und `useAppearance().sunActive` (A2).
**Interfaces**
- `readHoloTheme(): HoloTheme` liest bei jedem Aufruf neu. `useHoloTheme()` abonniert `apicore:appearance` und ruft `invalidate()` auf.
- `frameLookFor(type, { sun }): { color: Color | null; hatch: HatchKind | null }`.
- [ ] **Schritt 1: Failing Tests**
```ts
it('the scene re-reads the holo tokens when the appearance changes', ...);
it('sun turns bloom off and draws white edges', ...);
it('in sun every frame type has its own hatch and no tint', ...);
it('labels are 16 px and without backdrop blur in sun', ...);
it('focus inside the stage uses the holo ring token', ...); // .holo-stage setzt --ring
```
- [ ] **Schritt 2:** `npx vitest run src/components/hive` → FAIL.
- [ ] **Schritt 3: Implementieren.**
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src/components/hive`. VB:
- Volk und `/colonies/[id]/hive` in hell, dunkel, Sonne hell und Sonne dunkel
- Sonne live umschalten: Die Szene zieht ohne Neuladen mit
- Tastatur-Fokus auf Glas-Knöpfen sichtbar
- Budget des 3D-Chunks unverändert (CI-Prüfung)
Kein Commit.
---
## Lieferung T5b – Handschrift
### Task B1: Waben-Standkarte (Spec 6)
**Files**
- Create: `src/lib/honeycomb/cell-state.ts` (+ Spec), `src/lib/honeycomb/layout.ts` (+ Spec), `src/components/honeycomb/honeycomb-cell.tsx`, `comb-symbols.tsx`, `comb-legend.tsx`, `apiary-comb.tsx` (+ Spec), `apiary-comb-card.tsx`, `use-container-width.ts`, `src/styles/comb.css`
- Modify: `src/app/(app)/apiaries/[id]/apiary-colonies-card.tsx` (+ Spec; die T3/T4-Zeilenliste bleibt als aufklappbarer Rückfall), `src/app/(app)/apiaries/page.tsx` (+ Spec), `src/styles/baukasten-guard.spec.ts` (Ausnahme `honeycomb-cell.tsx` für `outline-none` mit Grund „zweiter Sechseckpfad zeichnet den Ring, Spec 6.6“)
**Interfaces**
- Consumes: `useColonyOverview(apiaryId)`, `colonyOverviewKey`, `colonyStatusLight(row, { today, inspectionsVisible })`, `StatusLightBadge`-Icons (`CheckCircle2`, `AlertTriangle`, `XCircle`), `todayLocalIsoDate`, `daysSince`, `startRound`; ab T4 `ColonyOverviewRow.inspectionDueOn?`, `segment`, `nextStepTitle` (optional typisiert, damit der Code vor T4 läuft).
- Produces:
```ts
export type RimStep = 1 | 2 | 3 | "neutral";
export const INSPECTION_STALE_DAYS: 14; // aus T3, unverändert genutzt; SOON_DAYS = 3
export function rimStepOf(row: ColonyOverviewRow, today: string, inspectionsVisible: boolean): RimStep;
export interface CellModel { colonyId: string; code: string; light: StatusLight; reasons: string[]; rim: RimStep; daysSince: number | null; winterRest: boolean; name: string }
export function cellModelOf(row: ColonyOverviewRow, ctx: { today: string; inspectionsVisible: boolean }): CellModel;
export function summarizeColonies(models: readonly CellModel[]): { counts: Record<StatusLight, number>; dueCount: number; sentence: string };
export function sortCells(models: readonly CellModel[]): CellModel[]; // natürlich nach Code (V-2 vor V-10); Offene Frage 2, A
export function columnsFor(containerWidth: number, cell: number): number; // floor((W − (b+4)/2)/(b+4)), 3..10
export function combLayout(count: number, columns: number, cell: number): { x: number; y: number; row: number }[]; // jede zweite Reihe um (b+4)/2 versetzt, um ein Viertel der Höhe verzahnt, 4 px Abstand
export function ApiaryComb(props: { apiary: { id: string; name: string }; cell?: number }): JSX.Element;
export function ApiaryCombCard(props: { apiary: Apiary }): JSX.Element; // Bienenstände-Liste, Zellen 56/48 px
```
- Zelle: Sechseck mit spitzer Oberseite, Breite 64 px (`pointer-coarse`) bzw. 56 px (`pointer-fine`), Liste 56/48 px; Füllung Grün `--condition-good`, Gelb `--condition-average`, Rot `--condition-critical`, Text/Icon `-foreground`; oben Zustands-Icon 16 px, darunter Code (16 px halbfett, > 6 Zeichen mit Auslassung), unten rechts Zähler bei > 1 Grund (13 px). Rand in `--foreground`, innen: Stufe 1 2 px, Stufe 2 4 px, Stufe 3 6 px gestrichelt (6 4) mit Uhr-Icon (Sonne 3/5/7 px); `neutral` 1 px `--input`. Regeln der Stufen: ab T4 (mit `inspectionDueOn`) 1 = Fälligkeit > 3 Tage entfernt oder Winterruhe (`null`), 2 = heute bis in 3 Tagen, 3 = überschritten oder nie; vor T4: ≤ 10 Tage, 11–14, > 14 oder nie.
- Name (`aria-label`): „B3 – Achtung: Futter schwach. Letzte Durchsicht vor 16 Tagen.“ (ab T4 „Durchsicht seit 2 Tagen fällig.“, Winterruhe „keine Durchsicht nötig“). `<ul aria-label="Völker am Stand {Name}">` mit `<li><a>` je Zelle; das Sechseck ist ein `aria-hidden`-SVG mit `<use href="#comb-hex">`, Text und Icon liegen als HTML darüber; Fokus zeichnet ein zweiter Sechseckpfad 3 px außerhalb (`.comb-cell:focus-visible .comb-focus`); Hover nur bei `pointer-fine` (2 px `--primary-ink`, ohne Größenänderung).
- Stand-Seite: Karte „Völker (n)“ mit Satz „5 Gut · 1 Achtung · 1 Kritisch · 2 Durchsichten fällig“, Wabe, Legende (eine Zeile, 13 px), Abschnitt „Braucht Blick“ (nur Gelb/Rot mit Gründen; sonst „Alle Völker sind in Ordnung.“), Knopf „Alle n Völker als Liste“ (`aria-expanded`, klappt die T3-Zeilenliste auf). Hauptknopf der Seite bleibt „Runde starten“. Bienenstände-Liste: jede aktive Stand-Karte mit `AreaIcon`, Name (Link), ab T4 `SeasonChip`, Zusammenfassung, Zellen; `useQueries` höchstens 20 Stände, darüber nur Zusammenfassung; Legende einmal oben; Tab „Historie“ ohne Wabe.
- Keine Animation; ein `<symbol id="comb-hex">` je Seite; Budget ≤ 5 kB gzip.
- [ ] **Schritt 1: Failing Tests**
```ts
// cell-state.spec.ts
it.each([[10,1],[11,2],[14,2],[15,3]])('before T4: %i days since the last inspection is rim step %i', ...);
it('before T4: never inspected is step 3', ...);
it.each([[4,1],[3,2],[0,2],[-1,3]])('with T4: due in %i days is step %i', ...);
it('with T4: inspectionDueOn null (winter rest) is step 1 and named as such', ...);
it('without inspections:read the rim is neutral and says nothing about inspections', ...);
it('the cell name starts with the code and names light, reasons and days', ...);
it('a cell always shows icon and code, never colour alone', ...); // cellModelOf liefert Icon-Schlüssel je Ampel
it('summarizeColonies counts lights and due inspections', ...);
// layout.spec.ts
it('columns follow the container width between 3 and 10', ...);
it('every second row is offset and interlocked by a quarter of the height', ...);
it('cells are ordered naturally by code (V-2 before V-10)', ...);
// apiary-comb.spec.tsx
it('renders a list of named links in code order', ...);
it('shows the legend and „Braucht Blick“ only with yellow and red colonies', ...);
it('„Alle Völker als Liste“ opens the T3 list (aria-expanded)', ...);
it('an empty apiary shows the empty hint and no comb', ...);
it('100 cells render with one shared symbol', ...);
it('works offline from the pack', ...);
it('focus draws the ring on the outer hexagon path', ...);
// apiaries/page.spec.tsx
it('the apiaries list shows one comb card per active apiary and only the summary beyond 20', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/lib/honeycomb src/components/honeycomb "src/app/(app)/apiaries"` → FAIL.
- [ ] **Schritt 3: Implementieren.** `cell-state`/`layout` rein und ohne React; `ApiaryComb` misst mit `useContainerWidth` (`ResizeObserver`), `useMemo` für `combLayout`.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit denselben Pfaden; VB: Stand mit 7 Völkern und Stand ohne Völker, Bienenstände-Liste; vom Stand aus „welches Volk ist Kritisch, welches lange nicht durchgesehen“ auf 50 cm Abstand in Sonne hell; Tab-Reihenfolge Zeile für Zeile; `forced-colors: active` (Stufen über Strichstärke erkennbar); React-Profiler-Messung 100 Zellen bei 4-facher CPU-Drosselung < 30 ms Scripting. Kein Commit.
---
### Task B2: Mini-Waben-Marker auf der Karte (Spec 7)
**Files**
- Create: `src/lib/honeycomb/mini-comb-svg.ts` (+ Spec), `src/styles/leaflet-theme.css`, `src/components/apiary-map.spec.tsx`
- Modify: `src/components/apiary-map.tsx`, `src/app/(app)/apiaries/map/page.tsx` (+ Spec falls vorhanden)
- Delete: `public/leaflet/marker-icon.png`, `marker-icon-2x.png`, `marker-shadow.png`
**Interfaces**
- Consumes: `cellModelOf`, `summarizeColonies` (B1), `useQueries` über `colonyOverviewKey`, react-leaflet `Marker`/`Popup` (die Spec spricht von `L.marker`; im Bestand sind es die react-leaflet-Komponenten, `L.divIcon` geht über die Prop `icon`).
- Produces:
```ts
export interface MiniCombSummary { lights: readonly StatusLight[]; total: number }
export function miniCombSvg(summary: MiniCombSummary): string; // 7-Zellen-Rosette in Sechseck-Hintergrund, nur var(--…)-Farben
export function markerLabel(name: string, summary: MiniCombSummary): string; // „Stand Nordhang: 7 Völker, 1 kritisch, 1 Achtung“
```
- Marker: `L.divIcon({ html, className: "stand-marker", iconSize: [48, 52], iconAnchor: [24, 26], popupAnchor: [0, -24] })`; Hintergrund-Sechseck (Radius 24) `--card` mit 2 px `--foreground`; schlechtestes Volk in der Mitte; freie Plätze als Wachs-Umriss (`--muted`, Rand `--input`); > 7 Völker: 7 schlechteste plus Zählchip (13 px fett); Standname als Chip ab Zoom 9; vor dem Laden eine einzelne Wachs-Zelle. `keyboard: true`, `title`, `aria-label` am Marker-Element nach dem Hinzufügen. Popup im Token-Stil (`.leaflet-popup-content-wrapper`, `-tip` mit `--popover` und Tinten-Rand, Schließknopf 44 px per Pseudo-Element) mit Name, Zusammenfassung wie 6.2 und Knopf „Stand öffnen“ (`size="touch"`, Umriss). Dunkel: `.dark .leaflet-tile-pane { filter: brightness(0.8) contrast(1.1) }`; Sonne: Marker-Rand 3 px. Unter der Karte die Liste „Stände auf der Karte“ (Name als Link, Zusammenfassung, „2 Durchsichten fällig“); offline der Hinweis „Die Karte braucht Internet. Die Stände stehen unten als Liste.“ (`role="status"`). Leaflet bleibt per `dynamic(..., { ssr: false })`.
- [ ] **Schritt 1: Failing Tests**
```ts
it('the worst colony sits in the middle', ...);
it('free places are outlines', ...);
it('more than seven colonies add a count chip', ...);
it('uses only var(--…) colours and no hex', ...);
it('markerLabel names colonies, critical and attention counts', ...);
// apiary-map.spec.tsx (Leaflet gemockt)
it('a marker carries an aria-label and opens the popup with „Stand öffnen“', ...);
it('lists the apiaries below the map as the alternative', ...);
it('shows the offline hint when the browser is offline', ...);
it('shows a single wax cell before the overview has loaded', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/lib/honeycomb src/components/apiary-map.spec.tsx "src/app/(app)/apiaries/map"` → FAIL.
- [ ] **Schritt 3: Implementieren;** SVG-String je Stand gemerkt nach den Ampel-Zählern.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit denselben Pfaden; VB: Karte in allen vier Modi (Popup lesbar, Kachelfilter dunkel), Tastatur (Marker per Tab, Enter öffnet), 390 px, Netz aus (Hinweis und Liste). Kein Commit.
---
### Task B3: Lebenslinie – Modell und Daten (Spec 8.1)
**Files**
- Create: `src/lib/colonies/lifeline.ts` (+ Spec), `src/lib/colonies/use-colony-lifeline.ts` (+ Spec)
**Interfaces**
- Consumes (Keys des Bestands, keine Doppel-Requests): `["colonies", id]`, `["colonies", id, "history"]`, `["colonies", id, "inspections"]`, `["colonies", id, "varroa-measurements"]` (die Spec nennt `…, "varroa"`; im Bestand heißt der Key so), `["colonies", id, "treatments"]`, `["colonies", id, "feedings"]`, `["colonies", id, "swarm-control-checks"]`, `["queens", queenId]` (höchstens 5), `["varroa", "threshold"]`, `["tasks", …]`; ab T4 `["colonies", id, "next-steps"]` und `rating.light`/`rating.band.redAbove` je Messung.
- Produces:
```ts
export const STRENGTH_IN_FRAMES_SINCE: string; // Rollout-Datum von T3 Task 9 (YYYY-MM-DD); der Orchestrator liefert den Wert aus dem Deploy-Protokoll
export interface LifelineOptions { strengthInFramesSince?: string; showAll?: boolean } // Standard: 12 Monate + 21 Tage Zukunft
export interface LifelineModel { colonyCode: string; startDate: string; from: string; to: string;
strength?: { points: { date: string; frames: number }[]; breaks: string[]; max: number };
mites?: { bars: { from: string; to: string; perDay: number; light: StatusLight; redAbove?: number; label?: string }[]; threshold?: number };
treatments: { from: string; to: string; product: string }[];
queen?: { spans: { from: string; to: string | null; code: string; color: QueenColor | null; colorWord: string; hatchYear: number | null }[] };
events: { date: string; kind: AreaKey; count: number }[];
future: { date: string; title: string }[]; // höchstens 3: T4 next-steps, sonst offene Aufgaben ≤ 21 Tage
withdrawalUntil: string | null; missing: ("strength" | "mites" | "treatments" | "queen")[];
hasData: boolean; desc: string; table: { date: string; event: string; value: string }[] }
export function buildLifeline(input: LifelineInput, today: string, options?: LifelineOptions): LifelineModel;
export function useColonyLifeline(colonyId: string): { model: LifelineModel | undefined; isPending: boolean; isError: boolean; refetch(): void };
```
- Regeln: Stärke = `Inspection.colonyStrength` ab `strengthInFramesSince` (Achse 0 bis `max(20, aufgerundet auf 5)`, bei T4-Cap 40), ältere Werte nur in der Tabelle mit „Skala (alt)“; Lücke > 45 Tage bricht die Linie; Milben nur `NATURAL_MITE_FALL` mit `calculatedValue`, Balken über die echte Liegezeit (1–7 Tage, mindestens 4 px); `rating.light` (T4) hat Vorrang vor `varroaLight` (T3); 403 auf `inspections:read`/`varroa:read`/`treatments:read`/`queens:read` lässt die Spur still entfallen (`missing`); unbekannte Zeichenfarbe → `color: null` (gestrichelt). `<desc>` höchstens 4 Sätze wie Spec 8.4.
- [ ] **Schritt 1: Failing Tests** (`lifeline.spec.ts`, `use-colony-lifeline.spec.ts`)
```ts
it('values before STRENGTH_IN_FRAMES_SINCE are not in the curve but in the table', ...); // Option injiziert
it('a gap over 45 days breaks the line', ...);
it('varroa bars span the real exposure time', ...);
it('rating.light (T4) wins over varroaLight (T3)', ...);
it('queen spans come from queenHistory and the colour from markingColor', ...);
it('an unknown marking colour is dashed and named', ...);
it('treatments become spans', ...);
it('the future shows T4 next steps, otherwise open tasks, at most 3', ...);
it('a 403 on a track drops the track without an error', ...);
it('the default window is the last 12 months plus 21 days, showAll widens it', ...);
it('desc has at most four sentences and names the last values', ...);
it('the table comes from the same model and covers the same span', ...);
it('useColonyLifeline reuses the page query keys and issues no duplicate request', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/lib/colonies` → FAIL.
- [ ] **Schritt 3: Implementieren;** reine Funktion, nur Modell (keine React-Komponente).
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – `npm run lint && npm run typecheck && npx vitest run src/lib/colonies && npm run build`; Rechenzeit `buildLifeline` bei 150 Durchsichten < 50 ms (4-fach gedrosselt, Messung im Bericht). Kein Commit.
---
### Task B4: Lebenslinie – Ansicht auf der Volksseite (Spec 8.2–8.5)
**Files**
- Create: `src/components/colonies/colony-lifeline.tsx` (+ Spec), `lifeline-svg.tsx`, `lifeline-table.tsx`
- Modify: `src/app/(app)/colonies/[id]/page.tsx` (+ `page.spec.tsx`; Karte direkt unter „Als Nächstes“, Desktop rechte Spalte oben)
**Interfaces**
- Consumes: `useColonyLifeline`, `LifelineModel` (B3), `AreaIcon`-Töne (A3), `QueryState`.
- Produces: `export function ColonyLifeline(props: { colonyId: string }): JSX.Element;`
- Aufbau (handgeschriebenes SVG, Vorbild `colony-varroa.tsx`): Zeitmaß 1,6 px je Tag; Spalten Monat 44 px, Königin 16 px (Schiene 4 px in Zeichenfarbe mit 1-px-Tintenrand, Punkt Radius 6 mit 2-px-Ring `--foreground`), Ereignisse 24 px (16-px-Icons in 20-px-Kreis aus `AREAS`, ein Icon mit Zähler je Tag), Stärke (Linie 2 px `--foreground`, Punkte Radius 4, Fläche `--primary` 18 %, in Sonne ohne Fläche, Raster gepunktet), Milben 64 px (Desktop 96): Balken in Ampelfarbe, Wert als Text bei Gelb, Rot und der letzten Messung, Schwelle gestrichelt (vor T4) bzw. Strich bei `band.redAbove` (ab T4); Behandlungen schraffiert (`--chart-4` 30 %, 45°). Heute-Linie 2 px beschriftet „heute“, darüber die gestrichelte Zukunftszone mit hohlen Sechsecken und Titel/Datum; Wartezeit als Satz „Wartezeit bis 20.08.“ unter der Grafik; Wertbeschriftung nur letzter, höchster, niedrigster Wert und Punkte mit ≥ 24 px Abstand. Zwei Knöpfe: „Ganzes Leben zeigen“/„Letzte 12 Monate“ (`aria-pressed`) und „Als Tabelle“ (`aria-expanded`, Tabelle Datum | Ereignis | Wert). `<figure>` mit `<svg role="img" aria-labelledby>`, `<title>` „Lebenslinie {Code}“, `<desc>` aus dem Modell, Zeichenfarbe zusätzlich als Wort. Leer: „Noch keine Durchsicht. Die Lebenslinie wächst mit jeder Durchsicht.“ mit Umriss-Knopf „Durchsicht erfassen“; Laden: Skelett in voller Höhe; Fehler: `QueryState` mit „Erneut versuchen“. Forced Colors: `CanvasText`. Höchstens 600 Knoten, ≤ 8 kB gzip, keine Animation. Der Hauptknopf der Seite bleibt „Durchsicht erfassen“.
- [ ] **Schritt 1: Failing Tests**
```tsx
it('renders a figure with the title and the description', ...);
it('„Als Tabelle“ opens the same entries as the graphic', ...);
it('„Ganzes Leben zeigen“ widens the window and flips its label', ...);
it('shows the empty state with the outline button when nothing was recorded', ...);
it('without varroa:read there is no mite column', ...);
it('a long gap leaves the line broken and says „keine Durchsicht“', ...);
it('the colony page puts the lifeline under „Als Nächstes“', ...);
it('shows a skeleton while loading and a retry on error', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/components/colonies "src/app/(app)/colonies"` → FAIL.
- [ ] **Schritt 3: Implementieren.**
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit denselben Pfaden; VB: Volk mit viel Verlauf, neues Volk ohne Daten, Volk mit Königinnenwechsel und Behandlung; 390 px (Stärke-Spalte ≥ 160 px), vier Modi, Forced Colors, Bildschirmleser-Reihenfolge (Titel, Beschreibung, Tabelle). Kein Commit.
---
### Task B5: Backend-Zusatz an `GET /autopilot/today` (Spec 9.1)
**Files** (in `ApiCore-Backend/`)
- Modify: `src/autopilot/next-steps.service.ts` (+ Spec), der Typ `AutopilotToday` und sein Swagger-DTO (Datei aus T4 Task 15, `grep -rn "AutopilotToday" src`), `test/next-steps.e2e-spec.ts`
- Keine Migration, keine neue Abhängigkeit, kein neues Recht (`colonies:read` wie bisher, keine Geheimnisse).
**Interfaces**
- Consumes: `SeasonService.statesFor` (hat `segmentSince`, `reportedOn`, `note` schon), `readRule(PHENOLOGY)`, `readRule(SEASON_AUTOPILOT)` aus `src/automation/rule-config/`.
- Produces (additiv, alte Frontends ignorieren die Felder):
```ts
interface AutopilotToday {
// … unverändert aus T4 Task 15 …
seasons: { apiaryId: string; apiaryName: string; segment: SeasonSegmentKey; phase: PhenologyPhaseKey | null; phaseSource: PhaseSourceKey | null;
segmentSince: string; reportedOn: string | null; note: 'NO_COORDINATES' | 'WEATHER_GAPS' | 'WEATHER_STALE' | null }[];
seasonCalendar: { calendarFallback: Record<PhenologyPhaseKey, string>; // 'MM-DD'
swarmSeason: { from: PhenologyPhaseKey; until: PhenologyPhaseKey };
winterVarroaWindow: { from: string; to: string } };
}
```
- [ ] **Schritt 1: Failing Tests**
```ts
it('GET /autopilot/today carries seasonCalendar and segmentSince per apiary', ...); // e2e
it('seasonCalendar follows a changed PHENOLOGY config', ...); // e2e
it('a user without colonies:read still gets 403 and no calendar', ...); // e2e
it('today() maps reportedOn and note from the season state', ...); // unit
```
- [ ] **Schritt 2:** in `ApiCore-Backend/`: `npx jest src/autopilot` und `npx jest --config ./test/jest-e2e.json test/next-steps.e2e-spec.ts --runInBand` → FAIL.
- [ ] **Schritt 3: Implementieren** in `NextStepsService.today`.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – `npm run typecheck && npx eslint "{src,test}/**/*.ts" && npm test -- --ci`; die e2e-Datei einzeln; Swagger zeigt die neuen Felder. Kein Commit.
---
### Task B6: Bienenjahr-Rad – Modell und Saison-Standards (Spec 9.1)
**Files**
- Create: `src/lib/season/season-defaults.ts` (+ `season-defaults.spec.ts`), `src/lib/season/year-wheel.ts` (+ Spec)
- Modify: `src/lib/autopilot/types.ts` (optionale neue Felder wie B5)
**Interfaces**
- Consumes: `AutopilotToday` (B5, Felder optional), `SEGMENT_LABELS` (T4 Task 18), offene Aufgaben aus `["tasks", "dashboard"]`.
- Produces:
```ts
export const SEASON_DEFAULTS: { calendarFallback: Record<PhenologyPhaseKey, string>; swarmSeason: { from: PhenologyPhaseKey; until: PhenologyPhaseKey };
winterVarroaWindow: { from: string; to: string } }; // Salweide 03-20, Kirsche 04-15, Apfel 04-28, Holunder 05-30, Linde 06-20, Trachtende 08-01, Herbst 09-15, Winter 11-15; Schwarmzeit Kirsche bis Linde; Winter-Varroa 11-15 bis 12-20
export interface YearWheelModel { year: number; today: string;
segments: { key: SeasonSegmentKey; from: string; to: string; source: "REPORT" | "GTS" | "CALENDAR"; current: boolean; estimated: boolean }[];
windows: { key: "TRACHT" | "VARROA_SUMMER" | "EINWINTERUNG" | "VARROA_WINTER"; from: string; to: string }[];
taskWeeks: { weekStart: string; count: number }[]; overdueCount: number;
reference: { apiaryId: string; apiaryName: string } | null; deviations: { apiaryName: string; segment: SeasonSegmentKey }[] }
export function buildYearWheel(input: { today: AutopilotToday | undefined; tasks: { dueDate: string | null; status: string }[] }, today: string): YearWheelModel;
export function angleOf(date: string): number; // 360° × (Tag im Jahr − 1) / Tage im Jahr − 90°, Schaltjahr berücksichtigt
export function arcsOf(from: string, to: string): { startAngle: number; endAngle: number }[]; // Winterruhe über den Jahreswechsel = zwei Bögen
```
- Regeln: Fenster Tracht = Salweide bis Trachtende, Varroa Sommer = Trachtende bis Herbst, Einwinterung = Herbst bis Winter, Varroa Winter = `winterVarroaWindow`; lückenlos und überlappungsfrei. Aktueller Abschnitt ab `segmentSince`; vergangene/künftige Grenzen sind Kalenderwerte mit `estimated = true`. Kalenderdatum vorbei, Abschnitt noch nicht erreicht → nächster Abschnitt „ab morgen, noch offen“. Bezugsstand = Abschnitt der meisten aktiven Stände, bei Gleichstand der fortgeschrittenere (T4 7.3); Abweichungen nennt `deviations`. Aufgaben: ein Punkt je Kalenderwoche, Überfällige gesammelt (`overdueCount`), Vorschläge zählt das Rad nicht. Fehlt `seasonCalendar` (älteres Backend), gilt `SEASON_DEFAULTS`.
- `season-defaults.spec.ts` liest `ApiCore-Backend/src/automation/rule-config/default-rules.ts` per `readFileSync` (Datei aus T4 Task 1, darum liegt der Test hier und nicht in T5a) und vergleicht die `MM-DD`-Werte.
- [ ] **Schritt 1: Failing Tests**
```ts
it('defaults equal the T4 PHENOLOGY and SEASON_AUTOPILOT defaults', ...);
it.each([["2028-01-01", -90], ...])('angleOf %s in a leap year', ...); // 01.01., 01.07., 31.12. im Schaltjahr
it('winter rest over the year change is two arcs', ...);
it('the current segment starts at segmentSince (cherry reported on 05.04. gives swarm season from 05.04.)', ...);
it('a late bloom makes the next segment „ab morgen, noch offen“', ...);
it('windows are gapless and do not overlap', ...);
it('the reference apiary is the majority, a tie takes the more advanced segment', ...);
it('deviations name the apiaries that differ', ...);
it('tasks are counted per week and overdue ones are collected', ...);
it('without seasonCalendar the wheel uses the season defaults', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/lib/season` → FAIL.
- [ ] **Schritt 3: Implementieren.**
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – `npm run lint && npm run typecheck && npx vitest run src/lib/season src/lib/autopilot && npm run build`. Kein Commit.
---
### Task B7: Bienenjahr-Rad – Ansicht und Dashboard-Umbau (Spec 9.2–9.5)
**Files**
- Create: `src/components/season/year-wheel.tsx` (+ Spec), `year-wheel-table.tsx`
- Modify: `src/app/(app)/dashboard/page.tsx`, `src/app/(app)/dashboard/page.spec.tsx` (aus A10), `src/lib/dashboard/types.ts` falls Kachel-Typen entfallen
**Interfaces**
- Consumes: `buildYearWheel`, `angleOf`, `arcsOf` (B6), `fetchAutopilotToday` (T4), `SEGMENT_LABELS`, `["tasks", "dashboard"]`.
- Produces: `export function YearWheel(): JSX.Element;` (Karte „Bienenjahr“ als `h2`, Textlink „Zum Kalender“ mit 44 px Trefferfläche, „Als Tabelle“ mit `aria-expanded`).
- Aufbau: 320 × 320 px (skaliert unter 328 px mit, Text ≥ 13 px); Januar oben, im Uhrzeigersinn; Monatsinitialen r = 150, Außenring r = 124 / 26 px (Segmente mit 2 px Lücke in `--card`, Kontur 1,5 px `--input`, aktuelles Segment `--primary` + 2 px `--foreground`, geschätzte Grenzen gestrichelt, keine Beschriftung im Ring), Innenring r = 96 / 10 px (Tracht `--chart-3` voll, Varroa `--chart-4` mit 45°-Schraffur in zwei Fenstern, Einwinterung `--chart-5` voll), Aufgabenpunkte r = 78 (Radius 3 / 4,5 / 6 px bei 1 / 2–3 / ≥ 4, `--foreground`; überfällig ein Punkt `--condition-critical` mit 2-px-Tintenring 4° vor dem Zeiger), Zentrum r < 62 mit drei Zeilen (Abschnitt 22 px Space Grotesk halbfett; „seit {Datum} · {Quelle}“ 14 px, bei `note` zusätzlich „Wetterdaten lückenhaft“/„ohne Koordinaten“; laufendes Fenster mit Ende oder die Abweichung „Südhang: noch Frühjahr“ bzw. „2 Stände weichen ab – siehe Tabelle“), Zeiger von r = 66 bis 140, 3 px `--foreground`, Kappe Wabenzelle (Honig mit Tintenrand), springt und dreht nie. Darunter Legende (13 px) und „Jetzt … · Danach …“. Tabellen: Abschnitt | von | bis | Quelle (je Stand eine Spalte bei Abweichung), Fenster | von | bis, Zeile „Aufgaben je Woche“ für 4 Wochen. `<svg role="img">` mit `<title>` „Bienenjahr {Jahr}“ und `<desc>` wie Spec 9.4, keine interaktiven Teile im SVG, ≈ 150 Knoten, ≤ 6 kB gzip, keine Animation.
- Dashboard (oben nach unten): Kopf (`h1` in Tinte, Datumszeile), „Als Nächstes“ (einziger Honigknopf), „Außerdem heute“, T4-Karte „Durchsicht fällig“, „Im Blick behalten“, „Bienenjahr“. Die Kacheln „Überblick“ (`StatTile`) entfallen ersatzlos, ebenso die T4-Chip-Zeile „Saison je Stand“ (zieht in Zentrum und Tabelle; `SeasonChip` bleibt auf der Stand-Seite).
- [ ] **Schritt 1: Failing Tests**
```tsx
it('the centre shows the segment, the source and the running window', ...);
it('„Als Tabelle“ opens the segment and window tables with the weekly task row', ...);
it('„Zum Kalender“ links to the calendar', ...);
it('names deviating apiaries in the third centre line', ...);
it('the dashboard has no count tiles and no white text on honey', ...);
it('the dashboard no longer shows the season chip row', ...);
it('the dashboard still works without T4 data', ...);
it('the pointer is a fixed element without animation', ...);
```
- [ ] **Schritt 2:** `npx vitest run src/components/season "src/app/(app)/dashboard"` → FAIL.
- [ ] **Schritt 3: Implementieren;** `useMemo` für das Modell.
- [ ] **Schritt 4:** → PASS.
- [ ] **Schritt 5: Verifikation** – V mit denselben Pfaden; VB: Start in vier Modi, 390 px (Rad skaliert, Text ≥ 13 px), Forced Colors, Dezember (Winterruhe über den Jahreswechsel, per Datums-Override im Test-Build), „im Rad sagen, welcher Abschnitt gerade ist“ auf 50 cm. Kein Commit.
---
### Task B8: Sichtprüfung T5b und Doku-Texte (Spec 13.4, 15.6)
**Files**
- Keine Code-Änderung außer Befund-Korrekturen in den Dateien der Tasks B1–B7; Texte für `ARCHITECTURE.md`/`TASKS.md` gehen im Bericht an den Orchestrator.
**Interfaces**
- Liefert dem Orchestrator: (1) die Screenshot-Matrix (Start, Stand mit Wabe, Bienenstände, Karte, Volk mit Lebenslinie, Durchsicht neu, Varroa neu, Profil > Darstellung, Login, Herkunft × hell/dunkel/Sonne hell/Sonne dunkel × 390/820 px (Touch-Emulation)/1366 px; Forced Colors und `prefers-reduced-motion: reduce` je einmal auf Start, Stand, Volk), (2) Lighthouse Barrierefreiheit ≥ 95 auf Start, Stand, Volk, Durchsicht neu, Profil in hell und Sonne, (3) `impeccable detect --json` mit 0 Befunden, (4) Doku-Texte: ARCHITECTURE neue Annahmen „Darstellung je Gerät“ (hell, dunkel, Sonne; Inline-Skript; keine Kontodaten) und „Ein Baukasten“ (Bausteine, Wächter, Ausnahmen) unter der nächsten freien Nummer, §2 `AREAS` als einzige Quelle und Wabenzelle als einziges Motiv, TASKS.md Abschnitt T5 mit Haken, Hinweis für das Changelog (iOS-Icon erst nach Entfernen und neu Hinzufügen), Empfehlung `DESIGN.md` per `/impeccable document`.
- Der Draußen-Test (Spec 13.4: Hauptknopf finden, kritisches und lange nicht durchgesehenes Volk auf 50 cm nennen, Brutwaben 0 auf 6, Sonnenmodus einschalten, Abschnitt im Rad nennen) ist Sache des Inhabers in der ersten Woche; ein Fehlschlag führt zu Token- oder Größenänderung (mit `contrast.spec.ts`), nicht zu neuer Bedienung.
- [ ] **Schritt 1: Failing Test** – falls die Prüfung eine Regression zeigt, zuerst ein Test dafür: `it('<beobachtete Regression>', ...)` in der betroffenen Spec; sonst entfällt der Schritt mit Vermerk „keine Befunde“.
- [ ] **Schritt 2:** `npx vitest run src` → die Befund-Tests FAIL (falls vorhanden).
- [ ] **Schritt 3: Prüfen und Befunde beheben** wie beschrieben.
- [ ] **Schritt 4:** `npx vitest run src` → PASS.
- [ ] **Schritt 5: Verifikation** – `npm run lint && npm run typecheck && npx vitest run src && npm run build` (gesamte Suite). Kein Commit.
---
## Abdeckung der Spec
| Spec-Punkt | Tasks |
|---|---|
| 1 Ziel und Technik: vier geprüfte Sätze, Wächter, Inline-Skript, SVG, `next-themes` raus | A1, A2, A4 |
| 1 Entscheidungen: Hauptknopf Honig + Tinte, `--primary-ink`, Auswahl-Optik, Ampel-Tokens, Sonne, Grundschrift, Kompaktmaße, Fokus, ein Motiv | A1, A4, A5, A6 |
| 2.1 Waben-Standkarte | B1 |
| 2.1 Mini-Waben-Marker | B2 |
| 2.1 Lebenslinie | B3, B4 |
| 2.1 Bienenjahr-Rad ersetzt Kacheln | B5, B6, B7 |
| 2.1 Kontrast, Grundschrift, Kontrastmodus, Dark Mode | A1, A4, A5 |
| 2.1 Manifest, PNG- und maskable Icons | A9 |
| 2.1 Bausteine Auswahlgruppe, Checkbox, Tabs, Plus/Minus | A6, A7, A8 |
| 2.1 Bereichs-Icons und -Farben | A3 |
| 2.1 Fokusstil | A5 |
| 2.2 Befunde (Kontraste, Fokus, Abzeichen, Warn-Badge, Navigation, Icons, `chart-3`, Tablet-Größen, Toaster, `data-motion`, Tabs, Verläufe, Icon-Abweichungen, Manifest, Leaflet-Popups, `dark:`) | A1–A5, A3, A9, B2 |
| 2.4 Abgrenzung (Wächter ersetzt `rating-contrast.spec`, `StatusLightBadge`, `INTENT_ICONS`, `SeasonChip`, `PHASE_SOURCE_LABELS`) | A3, A4, B1, B7 |
| 3 Gestaltungsgrundsätze und Verbotsliste | Global Constraints, A10 (Wächter) |
| 4.1–4.4 Tokens, Sonne, gemessene Kontraste | A1, A4 |
| 4.5 Schrift und Größen | A5 |
| 4.6 Fokusstil | A5, B1 (Ausnahme Zelle) |
| 5 Darstellung umschalten (Skript, Hook, Profil, Schnellschalter, Toaster, `theme-color`) | A2 |
| 5.4 Seiten außerhalb der App, Druck hell | A1 (print), A2, A3 |
| 6 Waben-Standkarte (Daten, Aufbau, Zelle, Bedienung, Barrierefreiheit, Fokus, Leistung) | B1 |
| 7 Mini-Waben (Marker, Popup, Rückfall, Dunkel/Sonne, Leistung) | B2 |
| 8 Lebenslinie (Daten, Aufbau, Platzierung, Barrierefreiheit, Leistung) | B3, B4 |
| 9.1 Daten und Backend-Zusatz | B5, B6 |
| 9.2–9.5 Rad, Dashboard nach T5, Barrierefreiheit, Leistung | B7 |
| 10.1–10.4 Bausteine (Auswahlgruppe, Checkbox, Tabs) | A6, A7, A8 |
| 10.5 Plus/Minus | A6 |
| 10.6 Fokus für eigene Bedienelemente | A5, A6, A7, B1 |
| 10.7 Wächter-Regeln | A5, A6, A7, A8, A10 |
| 10.8 Migrationsreihenfolge (mit Abweichung: A3 vor A4) | Abhängigkeiten, A1–A10 |
| 11 `AREAS`/`AreaIcon` | A3 |
| 12 Manifest und Icons, Service-Worker-Version | A9 |
| 13.1 Wächter (contrast, baukasten-guard, season-defaults, manifest) | A1, A4, A5–A10, B6, A9 |
| 13.2 Unit-Tests | A2, A3, B1, B2, B3, B6 |
| 13.3 Komponenten-Tests und Backend-e2e | A2, A6–A8, B1, B2, B4, B5, B7 |
| 13.4 Sichtprüfung, Lighthouse, Impeccable, Draußen-Test | A10, B8 |
| 14 Aufwand, Zwei Lieferungen | Kopf, Abhängigkeiten |
| 15 Rollout (Reihenfolge, Service Worker, Nutzerhinweis, Doku) | Abhängigkeiten, A9, A10, B8 |
| 16 Offene Fragen (vorläufig A/A/A, Frage 4 Bewegungs-Sprache neu, Empfehlung A) | Kopf, Status |
| 17 Bewegungs-Sprache (Stufen, Tokens, Muster, Nie-Liste, Leistung, Abnahme) | A11; Beute-Bühne 17.8: A1, A12 |
---
## Self-Review
**Spec-Lücken und Widersprüche, hier aufgelöst (bitte beim Review bestätigen)**
- **Migrationsreihenfolge 10.8:** Schritt 4 entfernt die `--gradient-*`-Tokens vor Schritt 6, der deren 22+ Verbraucher (Köpfe, Karten, Dashboard-Banner, Markenzeichen) ablöst. Gelöst: `AREAS`/`AreaIcon`/`BrandMark` (Task A3) laufen vor dem Werte-Wechsel (Task A4); `AreaIcon` braucht nur die in A1 additiv angelegten Bereichs-Tokens.
- **Wächter „ab Schritt 4 hart“ gegen Anlage in Schritt 1:** Gelöst über `VALUES_SWITCHED` in `contrast.spec.ts` (A1 `false`, A4 `true`): Neue-Token-Zeilen sind sofort hart, die Tabelle 4.4 erst mit den neuen Werten.
- **Darstellung (Schritt 2) braucht `ChoiceGroup appearance="cards"` (Schritt 7):** Gelöst: A2 baut die Karte mit der bestehenden `ChoiceGroup`, A6 stellt sie auf `cards` um.
- **Spec 8.1 nennt den Varroa-Key `["colonies", id, "varroa"]`:** im Bestand heißt er `["colonies", id, "varroa-measurements"]` (Schwarmkontrollen `…"swarm-control-checks"`); B3 nutzt die Bestands-Keys, damit keine Doppel-Requests entstehen.
- **Spec 6.1 nennt den Key `["apiaries", id, "colony-overview"]`:** gelöst über `colonyOverviewKey`/`useColonyOverview` (T3), kein zweiter Schlüssel.
- **Spec 12.2 `favicon.ico` (16/32/48) mit `sharp`:** `sharp` schreibt kein ICO. Gelöst: das Icon-Skript baut den ICO-Container selbst (PNG-Einträge), keine weitere Abhängigkeit (A9).
- **Spec 7.1 spricht von `L.marker`:** der Bestand nutzt react-leaflet; gelöst über `divIcon` in der `icon`-Prop und `aria-label` am Marker-Element nach `add` (B2).
- **`season-defaults.spec` liest Backend-Dateien aus T4 Task 1:** existiert vor T4 nicht. Gelöst: der Test gehört zu T5b (B6), nicht zu T5a.
- **`STRENGTH_IN_FRAMES_SINCE` ist beim Planen unbekannt:** gelöst als injizierbare Option von `buildLifeline` (Tests unabhängig), der Konstantenwert kommt vom Orchestrator aus dem T3-Deploy-Protokoll (B3).
- **Service Worker:** Cache-Version im Bestand ist `apicore-shell-v4`, die Spec nennt `v5` als Ziel; A9 hebt auf `v5`. `sw-routes.js` bleibt unverändert (Shortcut-Routen stehen schon in `SHELL_ROUTES`), `SW_ROUTES_VERSION` ändert sich nicht.
- **Dashboard-Datei gleichzeitig in T5a, T5b und T4 Task 18:** gelöst über Wellen (A3, A10, B7) und die Regel „wer später dran ist, rebased“; T5b startet erst nach T4a.
- **Entwicklungs-Flag `NEXT_PUBLIC_APPEARANCE_DARK`:** wird beim Build eingebettet; reicht, weil A2–A3 nie ausgerollt werden, bevor A4 das Flag entfernt (A2–A4 gehören in dasselbe Fenster).
- **PT-Summen:** T5a ≈ 4,75 PT (A1–A9 je 0,5, A10 0,25), T5b ≈ 5,25 PT (1,25 + 0,5 + 0,75 + 0,75 + 0,25 + 0,5 + 0,75 + 0,5); einzelne Tasks (A7, B1) liegen am oberen Rand.
**Geprüft**
- Jeder Spec-Abschnitt hat einen Task (Tabelle oben). Kein Task enthält „TBD“; Rümpfe stehen nur dort, wo Signatur und Test sie nicht festlegen (Formeln, Geometrie, Layout-Maße).
- Namen über die Tasks gleich: `AreaKey`/`AREAS`/`AreaIcon`/`BrandMark`/`BRAND_CELL_PATHS` (A3, A9, B4), `useAppearance`/`APPEARANCE_KEYS`/`DEFAULT_THEME` (A2, A4, A6, A10), `ChoiceOption.icon`/`appearance` (A6, A2), `CheckboxField` (A7), `ActiveHistoryTabs`-Props `onValueChange` (A8), `rimStepOf`/`cellModelOf`/`summarizeColonies`/`combLayout`/`columnsFor` (B1, B2), `miniCombSvg`/`markerLabel` (B2), `LifelineModel`/`buildLifeline`/`useColonyLifeline` (B3, B4), `AutopilotToday.seasonCalendar`/`segmentSince` (B5, B6), `YearWheelModel`/`buildYearWheel`/`angleOf`/`arcsOf` (B6, B7), `VALUES_SWITCHED` (A1, A4).
- Jeder Task schließt mit Verifikation (V, bei UI-Tasks VB mit Desktop und 390 px, Tastaturfokus, `prefers-reduced-motion` und den Modi hell/dunkel/Sonne) und „Kein Commit“; Wächter-Regeln wandern mit dem Task, der sie erfüllt.
- Geteilte Dateien je Welle höchstens einmal (Liste unter „Abhängigkeiten und Parallelisierung“); `globals.css` und Layout-/Navigationsdateien sind strikt serialisiert; T5b fasst `globals.css` nicht an.
- Proportion: Code steht nur als Signaturen, Testnamen mit Kern-Assertions und gepinnten Spec-Werten; Geometrie und Texte verweisen auf die bindende Spec.