Files
ApiCore/docs/superpowers/plans/2026-10-14-t5-optik.md
T
Nick SzaunigandClaude Opus 5.5 cdd30cf3ee feat: platform backbone and field capture complete (checkpoint)
Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed:
- webhooks with delivery table, retries and stable delivery id
- calendar handlers; a finished task removes its Outlook event
- tasks, automations, inspections, varroa, treatments, feedings,
  harvests, colonies, wintering, stock and bottling record domain events
  in the same transaction; legacy post-commit calls removed
- row locks through one helper (lockRowForUpdate, lockRowsForShare,
  lockRowsForUpdate)
- job framework with JobRun, cli exit codes, cleanup, task templates,
  daily evaluation, hourly app-backup and the rekey job
- system API and page "Verwaltung > System" with retry
- @nestjs/schedule removed, guard tests against in-process schedulers

Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed:
- inspection photo offline without metadata
- bulk capture for treatments, feedings and varroa (backend + frontend)
- offline stand mode with service worker, offline pages and packs
- offline session, cold start and logout on shared devices

Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md.
Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema
drift; frontend lint, typecheck, 913 tests, build. Not rolled out.

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

86 KiB
Raw Blame History

T5 Optik mit eigener Handschrift Implementation Plan

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–A10, ≈ 4,75 PT, 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: keine neue Animation. Rad, Lebenslinie, Waben erscheinen sofort. Erlaubt bleibt nur die honey-check-Bewegung beim Abhaken. prefers-reduced-motion und data-motion bleiben wirksam.
  • 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 A10 A8, A9

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):
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
// 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-variants (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:
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

// 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:
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

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

// 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

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:
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

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:
// 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

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.
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

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

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

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.

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:
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

// 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:
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

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:
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)

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

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):
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
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:
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

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

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) Kopf

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.