Files
tessera-ctl/.planning/quick/260916-hiv-kalenderquellen-formular-url-platzhalter/260916-hiv-PLAN.md
T

180 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: quick-260916-hiv
plan: 01
type: execute
wave: 1
depends_on: []
autonomous: true
requirements: [QUICK-260916-HIV]
files_modified:
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
- apps/web/src/components/settings/calendar-source-form.tsx
- apps/web/src/components/settings/calendar-source-form.test.tsx
- CHANGELOG.md
estimate:
tokens: 45000
raw_tokens: 45000
tasks: 3
confidence: low
must_haves:
truths:
- "Im Formular fuer Kalenderquellen zeigt das Feld „Adresse (URL)“ je nach gewaehltem Typ ein passendes Beispiel als Platzhalter: Exchange + EWS → `https://mail.firma.de/EWS/Exchange.asmx`, Exchange + Graph → `https://graph.microsoft.com/v1.0`, CalDAV → `https://caldav.firma.de/dav/`, ICS → `https://…/kalender.ics`; solange kein Typ gewaehlt ist, weiterhin `https://`."
- "Bei Exchange + EWS steht unter dem Adressfeld ein kleiner grauer Hinweis (`mt-1 text-xs text-muted-foreground`, de/en), dass die vollstaendige EWS-Adresse inkl. /EWS/Exchange.asmx noetig ist; bei Graph, CalDAV und ICS erscheint er nicht. Zeigt das Feld einen Fehler, steht der Hinweis unterhalb des Fehlers."
- "Beide Sprachdateien tragen dieselben fuenf neuen Schluessel unter `widgets.calendar` (Namespace von `useTranslations('widgets')`), Platzhalter-Werte in de und en identisch, Beispiel-Domain `firma.de`, keine kundenspezifische Domain."
- "`CHANGELOG.md` nennt die Aenderung unter `## Unveröffentlicht` → `### Geändert` in Alltagssprache."
- "Type-Check und alle Web-Tests bleiben gruen (Basislinie: 49 Testdateien / 309 Tests, plus die neue Testdatei)."
artifacts:
- "apps/web/src/messages/de.json — 5 neue Schluessel `widgets.calendar.formFieldUrlPlaceholderEws|Graph|Caldav|Ics` + `formFieldUrlHintEws`"
- "apps/web/src/messages/en.json — dieselben 5 Schluessel"
- "apps/web/src/components/settings/calendar-source-form.tsx — typabhaengiger Platzhalter + EWS-Hinweis"
- "apps/web/src/components/settings/calendar-source-form.test.tsx — neuer Komponententest"
- "CHANGELOG.md — Eintrag unter Unveröffentlicht / Geändert"
key_links:
- "`t('calendar.formFieldUrlPlaceholder*')` / `t('calendar.formFieldUrlHintEws')` in der Form ↔ `widgets.calendar.*` in de.json/en.json (Namespace `widgets` kommt aus `useTranslations('widgets')`, Zeile 63)"
- "Hinweis-Sichtbarkeit haengt an `isExchange && exchangeMode === 'ews'` — derselbe Zustand, der auch den EWS-Platzhalter waehlt"
- "`umlaut-guard.spec.ts` erzwingt identische Schluesselmengen in de.json und en.json — fehlt ein Schluessel in einer Datei, wird der Test rot"
---
<objective>
Das Formular fuer Kalenderquellen (`apps/web/src/components/settings/calendar-source-form.tsx`) zeigt im Adressfeld heute nur den festen Platzhalter `https://`. Kuenftig zeigt es je nach gewaehltem Typ (CalDAV / ICS / Exchange-Graph / Exchange-EWS) eine passende Beispieladresse und blendet bei Exchange-EWS einen grauen Hinweis ein, dass die vollstaendige Adresse inkl. `/EWS/Exchange.asmx` noetig ist — der Servername allein reicht nicht.
Purpose: Bei EWS scheiterte die Verbindung, wenn Anwender nur den Servernamen eintrugen. Ein sprechendes Beispiel und ein Hinweis verhindern das, ohne dass jemand die Anleitung lesen muss.
Output: fuenf neue Uebersetzungsschluessel (de/en), die angepasste Komponente, ein neuer Komponententest, ein Changelog-Eintrag.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@apps/web/src/components/settings/calendar-source-form.tsx
@apps/web/src/components/settings/widget-settings-panel.test.tsx
@apps/web/src/messages/umlaut-guard.spec.ts
Gemessene Fakten zur Planungszeit (2026-09-16, Arbeitsbaum sauber auf `main` @ 2a820b6):
- `t` in der Form ist `useTranslations('widgets')` (Zeile 63); alle `calendar.formField*`-Schluessel liegen im JSON unter `widgets.calendar` (de.json/en.json Zeilen 205-236). `"formFieldUrl"` steht in beiden Dateien in Zeile 220, danach folgt `"formFieldUsername"`.
- Muster fuer Beispiel-URLs: `emailAlerts.hostPlaceholderExchange` (de.json:983 `https://mail.firma.de/EWS/Exchange.asmx`; en.json:983 weicht dort mit `company.com` ab — fuer DIESEN Auftrag sind die Platzhalter laut Vorgabe in beiden Sprachen identisch).
- Es gibt keinen Test fuer `calendar-source-form.tsx`; `widget-settings-panel.test.tsx` liefert das Mock-Muster (echte `de.json` ueber `vi.mock('next-intl', …)`, Namespace-Verkettung `ns.key`). Vitest: jsdom, `globals: true`, Setup `src/test/setup.ts`, Alias `@` → `src`.
- `umlaut-guard.spec.ts` prueft (a) keine Ersatzschreibung aus `UMLAUT_REPLACEMENTS`, (b) jedes `ae/oe/ue/ss`-Wort in de.json muss auf `UMLAUT_ALLOWLIST` stehen, (c) identische Schluesselmengen de/en. Von den neuen Texten ist nur `Adresse` verdaechtig und bereits allowlisted — `umlaut-dictionary.ts` bleibt unangetastet.
- `## Unveröffentlicht` in `CHANGELOG.md` (Zeile 5) ist leer; direkt darunter folgt `## 1.1.0 – 2026-09-16`. Bestehende Eintraege beginnen mit einem Bereichsnamen wie „Kalender-Einstellungen: …“.
- Basislinie: `pnpm --filter @tessera/web type-check` Exit 0 (3 s); `pnpm --filter @tessera/web exec vitest run` → 49 Testdateien / 309 Tests gruen; Umlaut-Waechter 3/3 gruen.
- `biome check` ist KEIN Gate: die Wurzel-`biome.json` scheitert unabhaengig von dieser Datei am unbekannten Schluessel `organizeImports` (vorbestehend, nicht Teil dieses Auftrags — `biome.json` nicht anfassen).
- Paketname ist `@tessera/web` (nicht `web`) — Filter immer `--filter @tessera/web`.
- Kein Docker-Bau, kein Deploy, kein Testserver in diesem Auftrag (Deploy macht der User selbst).
</context>
<tasks>
<task type="auto">
<name>Task 1: Fuenf Uebersetzungsschluessel in de.json und en.json</name>
<files>apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
<action>
In BEIDEN Dateien direkt nach der Zeile `"formFieldUrl": …` (Zeile 220, Block `widgets.calendar`) fuenf neue Zeilen einfuegen — gleiche Reihenfolge, gleiche Einrueckung (6 Leerzeichen), jede Zeile mit Komma, weil `"formFieldUsername"` folgt:
1. `formFieldUrlPlaceholderEws` — Wert `https://mail.firma.de/EWS/Exchange.asmx`
2. `formFieldUrlPlaceholderGraph` — Wert `https://graph.microsoft.com/v1.0`
3. `formFieldUrlPlaceholderCaldav` — Wert `https://caldav.firma.de/dav/`
4. `formFieldUrlPlaceholderIcs` — Wert `https://…/kalender.ics` (echtes Auslassungszeichen U+2026, wie bei `formSaving` im selben Block)
5. `formFieldUrlHintEws` — de: `Vollständige EWS-Adresse inkl. /EWS/Exchange.asmx eintragen – nur der Servername reicht nicht.` / en: `Enter the full EWS address including /EWS/Exchange.asmx – the server name alone is not enough.` (Gedankenstrich U+2013 wie in `formFieldDomainHint`).
Die vier Platzhalter sind in de.json und en.json IDENTISCH (Beispiel-Adressen, Vorgabe des Users). Nur der Hinweis ist uebersetzt. Beispiel-Domain ist ausschliesslich `firma.de` bzw. `graph.microsoft.com` — keine kundenspezifische Domain (Tessera ist ein Mehrfirmen-Produkt). Keine anderen Schluessel anfassen, `umlaut-dictionary.ts` nicht aendern (`Adresse` ist bereits allowlisted, sonst enthalten die Texte kein `ae/oe/ue/ss`-Wort). JSON muss gueltig bleiben (echte Umlaute direkt als UTF-8, wie im Bestand).
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl; set -e; for f in apps/web/src/messages/de.json apps/web/src/messages/en.json; do for k in formFieldUrlPlaceholderEws formFieldUrlPlaceholderGraph formFieldUrlPlaceholderCaldav formFieldUrlPlaceholderIcs formFieldUrlHintEws; do test "$(grep -c "\"$k\"" "$f")" -eq 1; done; done; node -e "const de=require('./apps/web/src/messages/de.json').widgets.calendar, en=require('./apps/web/src/messages/en.json').widgets.calendar; for (const k of ['formFieldUrlPlaceholderEws','formFieldUrlPlaceholderGraph','formFieldUrlPlaceholderCaldav','formFieldUrlPlaceholderIcs']) { if (de[k]!==en[k]) throw new Error('de/en differ: '+k); if (!/^https:\/\//.test(de[k])) throw new Error('not https: '+k); } if (de.formFieldUrlPlaceholderEws!=='https://mail.firma.de/EWS/Exchange.asmx') throw new Error('EWS placeholder'); if (de.formFieldUrlPlaceholderGraph!=='https://graph.microsoft.com/v1.0') throw new Error('Graph placeholder'); if (de.formFieldUrlPlaceholderCaldav!=='https://caldav.firma.de/dav/') throw new Error('CalDAV placeholder'); if (!de.formFieldUrlPlaceholderIcs.endsWith('/kalender.ics')) throw new Error('ICS placeholder'); if (!de.formFieldUrlHintEws.includes('/EWS/Exchange.asmx') || !en.formFieldUrlHintEws.includes('/EWS/Exchange.asmx')) throw new Error('hint'); if (de.formFieldUrlHintEws===en.formFieldUrlHintEws) throw new Error('hint not translated'); const keys=Object.keys(de); const i=keys.indexOf('formFieldUrl'); if (keys[i+1]!=='formFieldUrlPlaceholderEws' || keys[i+5]!=='formFieldUrlHintEws') throw new Error('order'); console.log('I18N_OK')"; ! grep -q 'ctl\.de' apps/web/src/messages/de.json apps/web/src/messages/en.json; pnpm --filter @tessera/web exec vitest run src/messages/umlaut-guard.spec.ts</automated>
</verify>
<done>Beide Sprachdateien enthalten die fuenf Schluessel genau einmal, direkt hinter `formFieldUrl`, mit den vorgegebenen Werten (Platzhalter identisch, Hinweis uebersetzt, keine kundenspezifische Domain); `umlaut-guard.spec.ts` bleibt 3/3 gruen (Schluesselparitaet de/en, keine Ersatzschreibung).</done>
</task>
<!-- planner-discipline-allow: placeholder={urlPlaceholder} -->
<!-- planner-discipline-allow: data-testid="source-url-hint-ews" -->
<!-- planner-discipline-allow: text-muted-foreground -->
<!-- Die drei Literale oben sind POSITIV-Gates (-eq 1 / -ge 1): sie muessen nach Task 2 in der Komponente stehen. Das einzige Negativ-Gate (-eq 0) gilt dem alten festen placeholder-Attributwert, der in keiner Action zitiert wird. -->
<task type="auto" tdd="true">
<name>Task 2: Typabhaengiger Platzhalter + EWS-Hinweis in der Komponente, mit Komponententest</name>
<files>apps/web/src/components/settings/calendar-source-form.tsx, apps/web/src/components/settings/calendar-source-form.test.tsx</files>
<behavior>
Neue Testdatei `calendar-source-form.test.tsx` (Muster: `widget-settings-panel.test.tsx` — `vi.mock('next-intl', …)` mit Lookup in der echten `de.json` und Namespace-Verkettung `ns.key`; zusaetzlich `vi.mock('@/lib/calendar-api', () => ({ testSourceConfig: vi.fn(), testSource: vi.fn() }))`, damit kein echter Aufruf passiert; `afterEach(cleanup)`; Erwartungstexte aus `de.widgets.calendar`). Render `<CalendarSourceForm onSave={vi.fn()} onCancel={vi.fn()} />`; Elemente: URL-Feld `screen.getByLabelText(/Adresse \(URL\)/)`, Typ `screen.getByLabelText(/^Typ/)`, Exchange-Anbindung `screen.getByLabelText(/Exchange-Anbindung/)`; Umschalten per `fireEvent.change(el, { target: { value } })`.
- Test 1: Ohne gewaehlten Typ hat das URL-Feld den Platzhalter `https://` und es gibt kein Element mit `data-testid="source-url-hint-ews"`.
- Test 2: Typ `caldav` → Platzhalter = `formFieldUrlPlaceholderCaldav`; kein Hinweis.
- Test 3: Typ `ics` → Platzhalter = `formFieldUrlPlaceholderIcs`; kein Hinweis.
- Test 4: Typ `exchange` (Standardmodus `graph`) → Platzhalter = `formFieldUrlPlaceholderGraph`; kein Hinweis.
- Test 5: Typ `exchange` + Modus `ews` → Platzhalter = `formFieldUrlPlaceholderEws`; Hinweis vorhanden, Text = `formFieldUrlHintEws`, `className` enthaelt `text-muted-foreground`. Zurueck auf `graph` → Hinweis weg, Platzhalter wieder Graph.
- Test 6: Typ `exchange` + Modus `ews` + Eingabe `http://mail.firma.de/EWS/Exchange.asmx` (http statt https) → Fehlertext `formUrlErrorHttps` UND Hinweis sind beide sichtbar, und der Hinweis steht im DOM NACH dem Fehler (`fehler.compareDocumentPosition(hinweis) & Node.DOCUMENT_POSITION_FOLLOWING` ist truthy).
</behavior>
<action>
Erst die Testdatei schreiben und rot sehen (Tests 2-6 schlagen fehl, weil Platzhalter fest und Hinweis nicht vorhanden), dann die Komponente anpassen:
1. Nach `const isExchange = type === 'exchange';` (Zeile 81) zwei reine Ableitungen ohne State ergaenzen (kein `setState` im Render — siehe Kommentar Zeile 100-102): `const isEws = isExchange && exchangeMode === 'ews';` und `const urlPlaceholder`, das per Verzweigung liefert: bei `isExchange` → `isEws ? t('calendar.formFieldUrlPlaceholderEws') : t('calendar.formFieldUrlPlaceholderGraph')`; bei `type === 'caldav'` → `t('calendar.formFieldUrlPlaceholderCaldav')`; bei `type === 'ics'` → `t('calendar.formFieldUrlPlaceholderIcs')`; sonst (kein Typ gewaehlt) der bisherige Festwert `https://`. Kein `useMemo` noetig.
2. Im URL-Input (`id="source-url"`, Zeile 247-264) das feste `placeholder`-Attribut (Zeile 251) auf `placeholder={urlPlaceholder}` umstellen. Sonst nichts am Input aendern (Validierung, Klassen, Handler bleiben).
3. Direkt NACH dem bestehenden Fehlerabsatz `{urlError && (<p className="mt-1 text-xs text-destructive">…</p>)}` (Zeile 265-267) einen zweiten bedingten Absatz einfuegen: `{isEws && (<p data-testid="source-url-hint-ews" className="mt-1 text-xs text-muted-foreground">{t('calendar.formFieldUrlHintEws')}</p>)}`. Entscheidung (von den zwei erlaubten Varianten): der Hinweis ist bei EWS IMMER sichtbar und steht bei einem Fehler UNTER dem Fehler — so hilft er auch dann, wenn die Eingabe gerade abgelehnt wird.
4. Den Doku-Kommentar der Komponente (Zeile 48-56) um einen Satz ergaenzen: Platzhalter des URL-Feldes typabhaengig, EWS-Hinweis unter dem Feld (Quick 260916-hiv). Keine Schluesselnamen im Kommentar aufzaehlen und den alten Attributwert nicht im Kommentar zitieren.
Keine weiteren Aenderungen: `EXCHANGE_MODES`, `SOURCE_TYPES`, `handleTest`, `handleSubmit`, Payload bleiben unveraendert. Keine neuen Pakete.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl; set -e; F=apps/web/src/components/settings/calendar-source-form.tsx; test "$(grep -c 'placeholder="https://"' "$F")" -eq 0; test "$(grep -c 'placeholder={urlPlaceholder}' "$F")" -eq 1; for k in formFieldUrlPlaceholderEws formFieldUrlPlaceholderGraph formFieldUrlPlaceholderCaldav formFieldUrlPlaceholderIcs formFieldUrlHintEws; do test "$(grep -c "calendar.$k" "$F")" -ge 1; done; test "$(grep -c 'data-testid="source-url-hint-ews"' "$F")" -eq 1; test "$(grep -c 'text-muted-foreground' "$F")" -ge 1; test -f apps/web/src/components/settings/calendar-source-form.test.tsx; pnpm --filter @tessera/web exec vitest run src/components/settings/calendar-source-form.test.tsx; pnpm --filter @tessera/web type-check</automated>
</verify>
<done>Der neue Test (6 Faelle) ist gruen, Type-Check Exit 0; die Komponente liest den Platzhalter aus `urlPlaceholder` (kein fester Wert mehr im Attribut), rendert den grauen Hinweis nur bei Exchange + EWS und dort unterhalb eines eventuellen Fehlers.</done>
</task>
<task type="auto">
<name>Task 3: Changelog-Eintrag unter Unveröffentlicht / Geändert</name>
<files>CHANGELOG.md</files>
<action>
Unter `## Unveröffentlicht` (Zeile 5, derzeit leer — direkt darunter folgt `## 1.1.0 – 2026-09-16`) einfuegen: Leerzeile, `### Geändert`, Leerzeile, genau einen Listenpunkt, Leerzeile vor `## 1.1.0`. Listenpunkt wortgleich:
`- Kalender-Einstellungen: Das Feld „Adresse (URL)“ im Formular für Kalenderquellen zeigt jetzt je nach Typ ein passendes Beispiel (z. B. `https://mail.firma.de/EWS/Exchange.asmx` für Exchange EWS) und bei Exchange EWS einen Hinweis, dass die vollständige Adresse nötig ist – der Servername allein reicht nicht.`
Stil wie die Bestandseintraege: Alltagssprache, echte Umlaute, typografische Anfuehrungszeichen „…“, keine Dateinamen, keine Commit-Kuerzel. Abschnitte `## 1.1.0` und `## 1.0.0` unveraendert lassen. Die Seite „Was ist neu“ zeigt diesen Abschnitt auf der Beta automatisch, sobald er einen Listenpunkt hat (`filterChangelogForChannel` blendet nur leere Abschnitte aus) — dort ist nichts zu tun.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl; set -e; SEC="$(awk '/^## Unveröffentlicht$/{f=1;next} /^## /{if(f)exit} f' CHANGELOG.md)"; test "$(printf '%s\n' "$SEC" | grep -c '^### Geändert$')" -eq 1; test "$(printf '%s\n' "$SEC" | grep -c '^- Kalender-Einstellungen: Das Feld „Adresse (URL)“')" -eq 1; test "$(printf '%s\n' "$SEC" | grep -c 'Exchange.asmx')" -eq 1; test "$(printf '%s\n' "$SEC" | grep -c '^- ')" -eq 1; test "$(grep -c '^## Unveröffentlicht$' CHANGELOG.md)" -eq 1; test "$(grep -c '^## 1.1.0 – 2026-09-16$' CHANGELOG.md)" -eq 1; test "$(grep -c '^## 1.0.0 – 2026-09-15$' CHANGELOG.md)" -eq 1; pnpm --filter @tessera/web exec vitest run src/lib/changelog.test.ts</automated>
</verify>
<done>`## Unveröffentlicht` enthaelt genau eine Untergruppe `### Geändert` mit genau einem Listenpunkt zum Kalenderquellen-Formular; die Versionsabschnitte 1.1.0 und 1.0.0 sind unveraendert; `changelog.test.ts` bleibt gruen.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser-UI → i18n-Text | Platzhalter und Hinweis sind statische Uebersetzungsstrings; sie werden als React-Textknoten gerendert (automatisch escaped), nicht als HTML. Keine Nutzereingabe fliesst in Platzhalter oder Hinweis. |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-HIV-01 | Information Disclosure | Platzhalter-Werte in de.json/en.json | low | mitigate | Nur neutrale Beispiel-Domains (`firma.de`, `graph.microsoft.com`); Gate in Task 1 verbietet eine kundenspezifische Domain in beiden Sprachdateien. |
| T-HIV-02 | Tampering | Hinweis-Text im DOM | low | accept | Reiner Uebersetzungsstring ueber `t()`, als Textknoten gerendert — kein `dangerouslySetInnerHTML`, keine Interpolation von Nutzereingaben. |
| T-HIV-SC | Tampering | npm-Installationen | low | accept | Dieser Plan installiert keine Pakete (kein `pnpm add`); Lockfile bleibt unveraendert. |
</threat_model>
<verification>
Nach allen drei Tasks, vom Repo-Wurzelverzeichnis:
1. `pnpm --filter @tessera/web type-check` → Exit 0.
2. `pnpm --filter @tessera/web exec vitest run` → 50 Testdateien gruen (49 Bestand + `calendar-source-form.test.tsx`), mindestens 315 Tests (309 + 6), keine Fehlschlaege.
3. `git diff --stat` beruehrt genau die fuenf Dateien aus `files_modified` (plus SUMMARY/Planungsdateien) — kein `biome.json`, kein `umlaut-dictionary.ts`, kein `pnpm-lock.yaml`.
4. Kein Docker-Bau und kein Deploy in diesem Auftrag; die Browser-Pruefung auf der Beta macht der User nach dem naechsten Pull.
</verification>
<success_criteria>
- Adressfeld zeigt je Typ das vorgegebene Beispiel als Platzhalter (EWS / Graph / CalDAV / ICS), ohne Typ weiterhin `https://`.
- Grauer EWS-Hinweis erscheint nur bei Exchange + EWS, unterhalb eines eventuellen Fehlers, Text aus `widgets.calendar.formFieldUrlHintEws` (de/en).
- de.json und en.json tragen dieselben fuenf Schluessel; Umlaut-Waechter gruen; keine kundenspezifische Domain.
- Neuer Komponententest mit 6 Faellen gruen; Type-Check gruen; Gesamt-Testlauf gruen.
- CHANGELOG.md: `## Unveröffentlicht` → `### Geändert` mit genau einem Eintrag zum Kalenderquellen-Formular.
</success_criteria>
<output>
Create `.planning/quick/260916-hiv-kalenderquellen-formular-url-platzhalter/260916-hiv-SUMMARY.md` when done
</output>