Neue Token neben den bestehenden (Schrift auf Hauptfarbe und Warnung, Zustands-, Bereichs- und Königinnen-Töne, Fokus- und Rahmenmaße, Wabenstriche), zwei vollständige Sonnen-Sätze (hell/dunkel) mit größerer Schrift, 3-px-Fokus und ohne Schatten, Holo-Sonne für die Beute-Bühne und .holo-stage-Fokusring, Druck immer im hellen Satz, forced-colors. Wächter misst alle 21 Paare der Spec-Tabelle 4.4 in vier Sätzen (OKLCH → sRGB → WCAG, Tönungen alpha-gemischt), prüft den sRGB-Farbraum und erzwingt das Umschalten in A4. Sichtbar ändert sich nur ein minimal entsättigter Holo-Kantenton (vorher außerhalb sRGB). Refs #26 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1180 lines
105 KiB
Markdown
1180 lines
105 KiB
Markdown
# 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)
|
||
|
||
- (A1-Review) `dark:` ist jetzt `:where(.dark, .dark *)` (Spezifität 0). Beim Codemod `dark:bg-x` nie mit un-präfixiertem `hover:bg-y` paaren: Hover gewinnt dann im Dunkeln. A4 setzt außerdem `VALUES_SWITCHED = true` (ein Wächter-Test schlägt sonst an).
|
||
|
||
**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)
|
||
|
||
- (A1-Review) `.holo-stage` muss auf die Wurzeln von `components/hive/hive-card.tsx`, des Lade-Fallbacks in `app/(app)/colonies/[id]/hive/page.tsx`, `components/hive/editor/hive-editor.tsx` (Zeile ~316) und `editor/editor-notice.tsx` (Zeile ~16); sonst greift der Bühnen-Fokusring (`--holo-edge-strong`) nicht.
|
||
|
||
**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.
|