CluPilotCloud/CLAUDE.md

5.0 KiB
Raw Blame History

CluPilot — verbindliche Regeln (Repo-Teil)

Die Vollfassung der Regeln R1R17 liegt beim Nutzer, nicht im Repo. Die Kurzfassung steht in docs/handoffs/2026-07-25-clupilot-state-handoff.md §9. Bei Konflikt: STOP & fragen.

Diese Datei hält die Regeln fest, die aus konkreten Fehlern im laufenden Betrieb entstanden sind — sie sind nicht verhandelbar und werden per Test erzwungen.


R18 — Icon-Größe und Zeilenumbruch

Ein Icon steht neben seinem Text, nie darüber, und nie größer als bestellt.

Verboten:

  1. Icon zwingt den Text auf eine zweite Zeile. Ein Navigationseintrag, ein Button, ein Tabellen-Action ist einzeilig. Zwei Zeilen sind nur erlaubt, wenn der Text selbst bewusst zweizeilig gesetzt ist (Label + Unterzeile) — dann steht das Icon links davon, nicht darüber.
  2. Icon größer als die Zeile, in der es sitzt. Standard ist size-5 (20px) in der Navigation, size-4 (16px) in Buttons, Tabellen und Fließtext. Größer nur, wenn es als eigenständiges Element gemeint ist (Leerzustand, Statusplakette).

Warum das zweimal schiefging

  • Zeilenumbruch. Tailwinds Preflight setzt svg { display: block }. Ein Icon in einem inline-Elternteil schiebt den folgenden Text damit auf die nächste Zeile. Genau so wurde aus dem Eintrag „Zugangsdaten" ein doppelt so hoher Kasten mit Schloss oben und Wort darunter.
  • Größe. .size-4 und .size-5 haben dieselbe Spezifität, also entscheidet die Reihenfolge im Stylesheet — und Tailwind gibt .size-4 vor .size-5 aus. Eine Komponente, die size-5 bedingungslos mitmergt, überstimmt damit jedes class="size-4" am Aufrufort. Alle so geschriebenen Icons liefen still auf 20px.

Wie es jetzt gebaut ist

  • resources/views/components/ui/icon.blade.php setzt seine Standardgröße nur, wenn der Aufrufort keine size-/w-/h--Klasse mitgibt, und rendert inline-block shrink-0 align-middle statt des Preflight-block.
  • resources/views/components/ui/nav-item.blade.php legt Label und Icon in eine eigene Flex-Zeile, damit ein Icon auch im falschen Slot daneben landet.
  • Icons in <x-ui.nav-item> gehören in <x-slot:icon>, nicht in den Default-Slot.

Erzwungen durch

tests/Feature/IconLayoutTest.php — Größe des Aufruforts gewinnt, Icon bleibt inline-block, Nav-Eintrag bleibt einzeilig aus beiden Slots, und kein Blade-File im Repo darf ein Icon am Icon-Slot vorbeischmuggeln.


R19 — Zeitzone: gespeichert in UTC, angezeigt auf der Wanduhr

Jede Zeit, die ein Mensch liest, geht vorher durch ->local().

Verboten:

  1. Einen gespeicherten Zeitstempel direkt formatieren. $model->created_at->isoFormat(…) liefert UTC. Richtig ist $model->created_at->local()->isoFormat(…).
  2. ->timezone(config('app.timezone')). Das ist die Speicherzone und bleibt UTC — der Aufruf sieht aus wie eine Umrechnung und ist keine.
  3. Ein datetime-local-Feld nur in eine Richtung behandeln. Rein und raus gehören zusammen: LocalTime::toField() und LocalTime::fromField(). Ein Feld hat keine Zeitzone; es sind die Ziffern, die jemand auf der eigenen Uhr abliest.

Ausgenommen: diffForHumans() ist relativ und in jeder Zone gleich.

Warum das durchgerutscht ist

Die Konsole kündigte eine Aktualisierung „spätestens um 15:21" an, während die Uhr 17:21 zeigte. Zwei der vierzehn betroffenen Ansichten sahen sogar behandelt aus — sie riefen ->timezone(config('app.timezone')), was sich liest wie „in Ortszeit umrechnen" und, weil diese Zone UTC ist, nichts tut. Eine Attrappe ist schlimmer als gar kein Aufruf: sie hält den Nächsten vom Nachsehen ab.

Schlimmer als die Anzeigen waren zwei Formulare: Wartungsfenster und Paketversionen füllten ihre Felder mit UTC und lasen sie als UTC zurück. Ein eingetragenes „21:00" wurde zu 23:00 Ortszeit.

Und die Tests deckten es nicht auf, weil sie den erwarteten Wert mit demselben falschen Aufruf bildeten. Ein Test, der die Implementierung nachrechnet, prüft nichts.

Wie es jetzt gebaut ist

  • config('app.display_timezone') (APP_DISPLAY_TIMEZONE, Vorgabe Europe/Vienna) getrennt von app.timezone, das UTC bleibt.
  • ->local() als Carbon-Makro in AppServiceProvider. Es kopiert vor dem Umstellen: Illuminate\Support\Carbon ist mutabel, sonst schriebe das bloße Anzeigen das Modellattribut um. Beide Klassen bekommen denselben Rumpf — Carbon führt Makros in einer globalen Tabelle, die zweite Registrierung ersetzt die erste für alle.
  • App\Support\LocalTime hält beide Feldrichtungen nebeneinander, damit niemand eine ändert, ohne die andere zu sehen.

Erzwungen durch

tests/Feature/DisplayTimezoneTest.php — kein Blade und kein Livewire-Bauteil darf absolut formatieren ohne ->local(), die UTC-Attrappe ist verboten, Speicherzone bleibt UTC, Sommer- und Winterzeit werden geprüft, ->local() verändert das Original nicht, und der Feld-Round-Trip kommt als derselbe Zeitpunkt zurück.