HA Widgets — Eigene Home-Assistant-Apps mit Vue 3 (Teil 8: Claude Code richtig briefen)

- Veröffentlicht unter Makerspace von

Hinweis: Der Beitrag wurde mit KI-Unterstützung (Claude Code) verfasst und manuell nachbearbeitet.

HA Widgets — alle Teile: Teil 1: Überblick · Teil 2: Wie fängt man an? · Teil 3: Architektur · Teil 4: Architektur-Checks · Teil 5: HA-Infrastruktur · Teil 6: LCARS-Design · Teil 7: Phone und Desktop · Teil 8: Claude Code richtig briefen

Die ganze Serie wurde mit Claude Code geschrieben — Code, Refactoring, Architektur, Tests, sogar die Artikel. Zeit, einmal die Werkzeugseite anzuschauen: Wie startet man ein Projekt sinnvoll? Was gehört in eine CLAUDE.md? Und wo sind die Grenzen?


Der erste Prompt ist nicht der wichtige

Versuchung: „Schreib mir eine Vue-3-App, die einen Vorratskatalog verwaltet, mit Material-Dark-Theme und HA-Integration." Funktioniert — Claude legt los und produziert Code. Allerdings nichts, was man am Ende wirklich haben will.

Der bessere Einstieg ist umgekehrt: erst Kontext aufbauen, dann konkret werden.

  • Was ist das Ziel?
  • Wo läuft das? (HA per Docker, statische Dateien per SCP, iFrame im Dashboard)
  • Womit will ich bauen? (Vue 3 + TypeScript + Vite)
  • Wie sind die Vorlieben? (strikte Typisierung, kein any, kein fetch in Views)

Diesen Kontext kann man nicht in einen einzelnen Prompt quetschen. Er entsteht im Gespräch — und wandert dann in eine CLAUDE.md.


CLAUDE.md als Regelheft

CLAUDE.md liegt im Projektwurzelverzeichnis und wird von Claude Code bei jedem Start automatisch geladen. Sie ist das einzige Dokument, das in jeder Session vorhanden ist — und damit der natürliche Ort für alles, was nicht im Code steht, aber zu jeder Änderung gehört.

Was darin steht: Coding-Regeln, Sprachkonventionen, Prüf-Befehle, Ordnerstruktur. Was nicht darin steht: alles Instabile (aktueller Arbeitsstand, offene TODOs, „vorhin haben wir noch X gemacht") — das gehört in andere Kanäle.

Sprachregel. Aus der globalen ~/.claude/CLAUDE.md:

## Sprache im Code

- Code-Kommentare, Identifier, Log-Messages, Strings in der App/UI:
  immer auf Englisch.
- Kommunikation mit dem Nutzer im Chat: auf Deutsch.

Ohne diese Regel mischt sich Deutsch in den Code, weil die Konversation auf Deutsch läuft. Mit ihr bleibt der Code anschlussfähig — falls jemand ihn doch mal in die Hand bekommt.

Architektur-Regeln. Auszüge aus der Projekt-CLAUDE.md:

1. Kein `any` — strikte TypeScript-Typisierung überall, `shared/types.ts`
   als zentrale Quelle (einzige Ausnahme: catch (e: any))
2. Trennung Logik / UI — kein fetch() in Views, kein callAction() direkt,
   keine hardcodierten Entity-IDs im Template
3. Shared-first — wenn Logik in zwei Dateien auftaucht → sofort in shared/
4. Standardisierung — gleiche Patterns für gleiche Probleme

Diese Regeln klingen wie Allgemeinplätze. Sie werden interessant, weil Claude sie nicht von selbst einhält. Im Default schreibt jedes Modell den Code, der am wenigsten Reibung erzeugt — any ist bequemer als ein präziser Typ, ein fetch im View ist näher dran als ein neues Composable. Die Regeln verschieben den Default.

Prüf-Befehle. Damit Claude nach Änderungen weiß, was zu prüfen ist:

cd tablet/app  && npx tsc --noEmit
cd phone/app   && npx tsc --noEmit
cd desktop/app && npx tsc --noEmit

npm run lint                          # alle drei Apps + shared
python tools/check_vue_imports.py     # Vue-Imports + Deklarationsreihenfolge
npx jscpd --min-lines 6 --min-tokens 50 ...   # bei Refactorings

Ohne diese Liste fragt Claude nach jeder Änderung „soll ich was prüfen?" — oder schlimmer, prüft gar nicht. Mit der Liste passiert es automatisch nach jeder substanziellen Änderung.


Pair-Programming: Vorschlag, Verstehen, Entscheiden

Der entscheidende Schritt zwischen Vorschlag und Code ist nicht „akzeptieren" oder „ablehnen", sondern verstehen.

Ein Beispiel: Claude schlägt vor, eine Liste mit Object.entries(katalog.value).find(...) zu durchsuchen. Funktional korrekt. Aber: hatten wir nicht schon ein Composable, das genau diese Suche macht? Ein Blick in shared/ — ja, gibt es. Vorschlag ablehnen, Composable nutzen.

Das ist keine besondere Disziplin, sondern normales Code-Review. Der Unterschied: hier ist der Reviewer auch der Auftraggeber, und der Auftragnehmer ist nicht beleidigt, wenn der Vorschlag zurückgeht. Iteration kostet Zeit und Tokens, aber kann Nerven sparen.

Was sich in der Praxis bewährt hat:

  • Architektur vor Code. Erst besprechen wo etwas hingehört (Composable? View? shared/?), dann implementieren lassen.
  • Kleine Schritte. Eine Funktion zur Zeit. Große Diffs sind schwer zu reviewen, und Claude hat es nicht eilig.
  • Bei jedem any nachhaken. Es gibt fast nie einen guten Grund — und wenn doch, sollte er im Code stehen.
  • Bei jedem neuen fetch() in einer View nachhaken. Das ist die häufigste Regel-Verletzung. Composable raus, Aufruf umziehen.

Was Claude gut macht — und was nicht

Gut funktioniert:

  • Refactoring nach Muster (eine Funktion in fünf Views ändern, Imports anpassen, Typen mitziehen)
  • Boilerplate (Composable-Skeleton, neue View nach existierendem Vorbild)
  • Konsistenz halten (gleiches Namensschema, gleiche Reihenfolge in props/emits)
  • Architektur-Diskussion (Vorschläge, Gegenvorschläge, Trade-offs aufzeigen)

Weniger gut:

  • Den ersten Wurf gleich richtig haben — der zweite oder dritte ist es oft erst
  • Bugs, die nicht im Code stehen — fehlende Argumente, fehlende Klammer, fehlender Watch
  • Echtes Testen der Apps ersetzen

Zum letzten Punkt zwei Beispiele aus dem Projekt.


Bug 1: Der TokenDialog, den niemand aufrief

Eines Tages hat ESLint mit no-unused-vars einen ungenutzten Import in einer View gemeldet — TokenDialog. Erwartet hätte ich: „Ah, alter Import, vergessen zu löschen." Tatsächlich war es umgekehrt: Claude hatte die Komponente importiert, registriert, alles vorbereitet — und vergessen, sie ins Template einzubauen. Der Dialog wurde nie angezeigt, niemand hätte das gemerkt. Bis ESLint sich beschwerte.

In Teil 3 hatte ich das schon kurz erwähnt — hier als Lehre: lint-Regeln sind nicht Pedanterie, sie fangen Fehler, die niemand sucht.


Bug 2: Das PointerEvent im Abteilung-Feld

Beim Anlegen eines neuen Vorrats-Items über den „+ Hinzufügen"-Button stand im Edit-Dialog plötzlich der String [object PointerEvent] im Feld „Abteilung". Beim manuellen Testen aufgefallen — sonst nirgends.

Ursache: eine fehlende Klammer im Template.

<button class="md-btn" @click="openNew">+ Hinzufügen</button>

Vue reicht bei @click="openNew" ohne Klammern den Click-Event als ersten Parameter durch. Die Signatur von openNew war:

function openNew(katName?: string): void {
  editForm.value = {
    ...emptyForm,
    kat:       katName ?? '',
    abteilung: katName ?? '',
  }
  ...
}

Der PointerEvent landete also im katName-Parameter, wurde per ?? durchgereicht und beim Schreiben in kat und abteilung zu "[object PointerEvent]" gestringified.

Korrektur: explizit ohne Argument aufrufen.

<button class="md-btn" @click="openNew()">+ Hinzufügen</button>

Was hier interessant ist: keiner der Architektur-Checks fängt das.

  • tsc --noEmit sieht nichts Falsches. Der Parameter ist string | undefined, der ??-Operator ist syntaktisch sauber, und Vues Template-Typprüfung schaut Event-Handler-Signaturen nicht streng an.
  • ESLint hat dafür keine Regel.
  • check_vue_imports.py schaut nur auf Imports.

Was den Bug aufgedeckt hat: das echte Bedienen der App. Beim Anklicken des Buttons stand der Mist im Feld.

Die Lehre: Architektur-Checks aus Teil 4 schließen viele Lücken — fehlende Imports, ungenutzte Variablen, Template-Duplikate, falsche Typen. Aber das eigene Klicken durch alle Funktionen bleibt unersetzlich. Nicht jeder Fehler steht im Code; manche entstehen erst beim Ausführen.


Rückblick auf die Serie

Acht Teile, drei Apps, ein gemeinsames shared/-Paket. Die Erkenntnisse aus dem Prozess:

  • YAML hat seine Grenzen. Eine Vue-3-SPA als iFrame in HA einzubinden ist erstaunlich einfach — und schenkt einem die volle Freiheit über Layout, Logik und Design.
  • Shared-first lohnt sich. Wer das von Anfang an mitdenkt, spart später viel Refactoring. Nachträglich (useVorratEinkaufBase) geht auch noch.
  • tsc und ESLint sind die halbe Miete. Die andere Hälfte sind eigene Checks (check_vue_imports.py, jscpd) — und manuelles Testen.
  • HA Custom Component statt Webhooks. Wer mehrere Datenpunkte braucht, baut sich besser einmal eine eigene wsCommand-API, als 15 Webhooks zu pflegen.
  • LCARS, Material 3 und Tabellen-UI können dieselbe Datenbasis teilen, solange Logik und UI sauber getrennt sind.
  • Claude Code als Pair-Programmer ist mehr wert als „Code-Generator". Vorschlag → Verstehen → Entscheiden bleibt der Modus — auch wenn der Vorschlag von einer KI kommt.

Würde ich das nochmal so machen? Ja. Mit ein paar Dingen früher: Custom Component statt erst Webhooks, shared/ direkt am Anfang, tsc und lint von Tag eins. Aber das ist die Natur eines Projekts, das wächst — mit Claudes Unterstützung sind auch größere Refactoring machbar.


Fazit

  • Erst Kontext, dann Code. Der erste Prompt ist nicht der wichtige — die CLAUDE.md ist es.
  • CLAUDE.md als Regelheft: Coding-Regeln, Sprachkonventionen, Prüf-Befehle. Nichts Instabiles.
  • Pair-Programming: Vorschlag → Verstehen → Entscheiden. Iteration ist kostengünstig, blindes Übernehmen womöglich teurer.
  • Stärken von Claude: Refactoring nach Muster, Boilerplate, Konsistenz, Architektur-Diskussion.
  • Schwächen: erste Würfe oft nicht final; Bugs durch fehlende Argumente/Klammern/Watcher; manuelles Bedienen kann es nicht ersetzen.
  • TokenDialog-Bug durch no-unused-vars aufgedeckt — lint-Regeln fangen, was niemand sucht.
  • PointerEvent-Bug durch fehlende Klammer in @click="openNew" — kein Check fand ihn, nur manuelles Klicken.
  • Architektur-Checks schließen viele Lücken — aber nicht alle. Das echte Bedienen der App bleibt unersetzlich.