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

19 KiB
Raw Blame History

phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
phase plan type wave depends_on autonomous requirements files_modified estimate must_haves
quick-260916-hiv 01 execute 1
true
QUICK-260916-HIV
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
tokens raw_tokens tasks confidence
45000 45000 3 low
truths artifacts key_links
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).
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
`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
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.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_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).
Task 1: Fuenf Uebersetzungsschluessel in de.json und en.json apps/web/src/messages/de.json, apps/web/src/messages/en.json 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). 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 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).

Task 2: Typabhaengiger Platzhalter + EWS-Hinweis in der Komponente, mit Komponententest apps/web/src/components/settings/calendar-source-form.tsx, apps/web/src/components/settings/calendar-source-form.test.tsx 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 ``; 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). 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. 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 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.

Task 3: Changelog-Eintrag unter Unveröffentlicht / Geändert CHANGELOG.md 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. 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 ## 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.

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

<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>
Create `.planning/quick/260916-hiv-kalenderquellen-formular-url-platzhalter/260916-hiv-SUMMARY.md` when done