14 KiB
phase, verified, status, score, behavior_unverified, overrides_applied, behavior_unverified_items, human_verification
| phase | verified | status | score | behavior_unverified | overrides_applied | behavior_unverified_items | human_verification | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260929-if2 | 2026-09-29T12:20:00Z | human_needed | 8/9 must-haves verified | 1 | 0 |
|
|
Quick 260929-if2: Reminder widget "Erinnerungen" Verification Report
Phase Goal: Reminder widget with notification in desktop app, browser and optionally by e-mail; one-time, no advance warning, after due "Erledigt"/"Spaeter erinnern" (10 min / 1 h / tomorrow); personal with RLS; e-mail exactly once per due occurrence. Verified: 2026-09-29 Status: human_needed Re-verification: No, initial verification
Note on commits: verified against the re-created commits 325c5dd, 709b41a, 6879c75 (HEAD, three commits above cd1f8f6; nothing pushed, git log origin/main..HEAD shows exactly these). No source files were modified by this verification. The only untracked path is the task directory. During verification an unrelated test reminder ("Kaffee holen") appeared in the live DB, presumably from the orchestrator's browser check; it was not touched.
Goal Achievement
Observable Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | User adds widget, creates reminder (date/time/title/description, local time), sees only own open reminders sorted by due time (D-05) | VERIFIED | reminder-widget.tsx sorts by dueAt, lists via listReminders; reminders.service.ts list() uses forTenant(prisma, tenantId, userId) + where {tenantId,userId}, orderBy dueAt asc; registered in registry, page.tsx (registerWidget('reminder', ReminderWidget)), WIDGET_TYPES in shared; widget test green; live GET as testuser returns 200 array |
| 2 | At due time an open tab shows a Web Notification once granted; permission asked only from widget at first creation, never on page load (D-04) | VERIFIED (unit-level; live browser in human items) | reminder-notify.ts: requestBrowserPermissionOnce (flag in localStorage, no-op in Tauri/when not 'default'); called first in submit handler only when !reminder (create); ReminderNotifier mounted in app-shell.tsx:48; remindersToNotify only dueAt <= now (D-02) within 24 h; widget/notifier/notify tests green (410 web tests in scope, 1069 full) |
| 3 | Desktop app shows native OS notification also while window hidden in tray, via plugin, callable only from stored server origin | PRESENT_BEHAVIOR_UNVERIFIED | Code present and wired: showReminderNotification invokes plugin:notification|notify with {options:{title,body}}; grant_server_notifications called in setup() before first navigate and in save_server_url; server_origin_pattern escapes host, self-checks with RemoteUrlPattern; SERVER_NOTIFICATION_PERMISSIONS pinned to 3 ids; capability .local(false).window("main"); plugin registered (lib.rs:867). cargo: 57 passed, 12 server_origin_*; fmt ok. add_capability in tauri 2.11.3 appends (checked source), so repeated calls do not overwrite. Actual toast delivery / runtime acceptance is not exercised by any test, so routed to human (Windows) |
| 4 | Every client shows each (id, dueAt) at most once: tabs share local claim, desktop and browser each notify once | VERIFIED (unit-level) | claimNotification (localStorage record, key ${id}|${dueAt}, 7-day prune) under withNotifyLock (Web Locks); key changes after snooze; notifier test: one notification across several ticks, again after dueAt change. Separate webview storage means desktop and browser each notify once by design. Multi-tab live check in human items |
| 5 | Due reminder stays highlighted with "Erledigt" (removes) and "Spaeter erinnern" (+10 min, +1 h, tomorrow same time); snooze sets new dueAt so notifications and e-mail fire again (D-01, D-03) | VERIFIED | Widget: due rows (dueAt <= now, 10 s tick) get border-status-warn, badge, Erledigt (deleteReminder), snooze options via snoozeTarget; reminder-time.ts snoozeTarget (now+10m, now+1h, original time + calendar days until future); service snooze writes {dueAt, emailSentAt: null, emailAttempts: 0}; no recurrence field/UI anywhere in schema/DTO; time/widget/service tests green |
| 6 | Upcoming reminders editable/deletable; editing a due one is 409; snoozing a not-due one is 409 | VERIFIED | service.update 409 when dueAt <= now; snooze 409 when dueAt > now; controller has PATCH/POST snooze/DELETE below static email-status; route-order spec in controller spec (14 tests green); widget shows edit/delete only on non-due rows |
| 7 | With e-mail on, server sends exactly one mail per due occurrence via tenant SMTP (Europe/Berlin), no client needed, several instances safe (atomic claim); toggle disabled with explanation when SMTP missing / no e-mail | VERIFIED (unit-level; real transport in human items) | reminder-mail.scheduler.ts: single forSystem call, scalar select, candidate filter (emailEnabled, emailSentAt null, attempts<3, due within 24 h), claim updateMany with dueAt equality and emailSentAt: null, count===1 check before send, release only on transport failure with own timestamp, skip keeps claim, running reentrancy guard, 30 s addInterval. MailService.sendReminderEmail: CR/LF stripped subject, Europe/Berlin de-DE + " Uhr", text only, returns bool. Scheduler spec: 16 tests incl. two instances one mail, 3-attempt cap, snooze re-arm. Toggle: emailAvailable/emailHint in form modal; email-status endpoint live: {"smtpConfigured":true,"hasEmail":true}. Summary reports a live DB run with 3 attempts then stop |
| 8 | Foreign reminder id always 404 (never 403); Reminder has tenant+user RLS policy and system read policy; rls-coverage and rls-access-inventory green; classification doc re-measured | VERIFIED | loadOwn throws NotFoundException for unknown/foreign; live DELETE on unknown id returns 404; DB: relrowsecurity and relforcerowsecurity both true; policies tenant_isolation_policy (ALL, tenant AND user dim) and system_read_policy (SELECT, is_system_context()); migrate diff --exit-code "No difference detected"; rls-coverage and rls-access-inventory pass; FORSYSTEM_ALLOWED_CALL_SITES has reminder-mail.scheduler.ts, 1; doc updated in all three commits (numbers not independently re-counted) |
| 9 | All API/web tests green, type-check and lint green, Biome warnings web <= 55 and api <= 82, cargo test/fmt/clippy green | VERIFIED (clippy not re-run) | API full: 91 files / 1570 tests pass; web full: 108 files / 1069 tests pass; turbo run type-check lint --force: 9/9 successful; Biome web 55, api 82 (exactly baseline); cargo test 57 pass, fmt ok. Clippy -D warnings not re-run by me (summary claims clean) |
Score: 8/9 truths verified (1 present, behavior-unverified)
Required Artifacts
| Artifact | Status | Details |
|---|---|---|
apps/api/prisma/migrations/20260929140000_reminder/migration.sql |
VERIFIED | Table, indexes, FK cascade, ENABLE+FORCE RLS, both policies; applied locally, no schema drift |
apps/api/src/reminders/reminders.service.ts / .controller.ts |
VERIFIED | Owner-scoped CRUD/snooze/email-status, all through forTenant(..., tenantId, userId); REMINDER_SELECT excludes tenantId/userId/emailSentAt/emailAttempts |
apps/api/src/reminders/reminder-mail.scheduler.ts |
VERIFIED | Registered in RemindersModule providers; module registered in app.module.ts |
apps/web/src/lib/reminder-notify.ts |
VERIFIED | Tauri vs browser branch, one-time permission, dedup with Web Locks |
apps/web/src/components/reminders/reminder-notifier.tsx |
VERIFIED | Mounted in app-shell.tsx |
apps/web/src/components/dashboard/widgets/reminder-widget.tsx (+ form modal) |
VERIFIED | Wired via registry, page.tsx, widget-wrapper FRAME_HEADER_TYPES, icon |
apps/desktop/src-tauri/src/lib.rs |
VERIFIED (code), see truth 3 | server_origin_pattern, grant_server_notifications, 12 tests |
Key Link Verification
| From | To | Status | Details |
|---|---|---|---|
app-shell.tsx |
ReminderNotifier |
WIRED | line 48 |
reminder-notify.ts |
plugin notification | WIRED | invoke('plugin:notification|notify', { options }) |
lib.rs |
Tauri runtime authority | WIRED | add_capability in grant_server_notifications, called in setup() and save_server_url |
| scheduler | MailService.sendReminderEmail |
WIRED | claim then send, release on false |
snooze |
emailSentAt/emailAttempts reset |
WIRED | data: { dueAt, emailSentAt: null, emailAttempts: 0 } |
Data-Flow Trace (Level 4)
Widget and notifier data come from listReminders() -> GET /reminders -> Prisma query (live check returned real rows). FLOWING. No hardcoded/static fallbacks.
Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|---|---|---|---|
| API reminders/mail/prisma/dashboard specs | vitest run src/reminders src/mail src/prisma src/dashboard |
17 files, 267 passed | PASS |
| Web reminder specs | vitest run src/lib/reminder src/components/reminders src/components/dashboard src/messages |
30 files, 410 passed | PASS |
| Full suites | api / web vitest run |
1570 / 1069 passed | PASS |
| cargo | cargo test --lib; server_origin_ filter |
57 passed; 12 passed | PASS |
| Schema drift | prisma migrate diff --exit-code |
exit 0 | PASS |
| RLS in DB | pg_policies, pg_class |
both policies present, RLS+FORCE on | PASS |
| Live API | login, GET /reminders, GET /reminders/email-status, DELETE unknown id | 200, 200, 200, 404 | PASS |
| Type-check + lint, Biome | turbo run type-check lint --force; biome lint |
9/9; 55 / 82 warnings | PASS |
Probe Execution
No probes declared. SKIPPED.
Requirements Coverage
QUICK-260929-if2 (all decisions D-01..D-05, E-01..E-09) implemented as described; no REQUIREMENTS.md mapping.
Anti-Patterns Found
None. No TBD/FIXME/XXX/TODO in the new files; no stubs; no debt markers. Working tree clean apart from the task directory.
Notes / minor observations (non-blocking)
- Deviation 3 (badge uses
bg-status-warn/20instead of the plan's solid fill) is documented and justified by contrast; flagged for a dark-mode visual check. - Summary deviation 6 mentions the trailer as "Claude Sonnet 5.5"; the re-created commits carry "Claude Opus 5.5 (1M context)". Immaterial to the goal.
claimNotificationclaims before showing: a browser whose permission is still 'default' at due time loses that occurrence's notification (still visible in the widget, highlighted). Consistent with the plan ("blocked notifications only show in the widget").- Fingerprint fields (
covered_files/covered_digest) were not generated because the fingerprint verb was not run in this environment.
Human Verification Required
See frontmatter human_verification and behavior_unverified_items. Summary: (1) Windows toast in the tray, runtime capability accepted by Tauri, plus desktop+browser once-each; (2) live browser notification, permission prompt timing and two-tab dedup; (3) dark-mode look of the "Faellig" badge; (4) real SMTP flow (mailhog is not running locally).
Gaps Summary
No gaps. All code-verifiable must-haves hold in the codebase and the automated gates reproduce the summary's numbers (tests, type-check, lint, Biome baseline, migrate diff, RLS state in the live DB). The single behavior-dependent truth that cannot be proven without a GUI is the actual desktop toast through the runtime Tauri capability; it is left PRESENT_BEHAVIOR_UNVERIFIED and routed to the Windows check, so the status is human_needed, not passed.
Verified: 2026-09-29 Verifier: Claude (gsd-verifier)