diff --git a/docs/README.md b/docs/README.md index abd3ed8f5..66cd7782c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,8 @@ Internal documentation for engineers and agents working in the Compass repo. - Event shape or recurrence behavior: [Event Domain Model](./architecture/event-domain-model.md), [Common Change Recipes](./development/common-change-recipes.md#change-recurring-event-behavior) - Event caching, reads, or optimistic writes: [Event Caching](./frontend/event-caching.md) - Dragging/resizing events on the week grid: [Week Drag Interaction](./frontend/week-drag-interaction.md) +- Keyboard shortcuts, event jump, or Hardcore Mode: [Shortcuts](./acceptance/shortcuts.md), [Feature File Map](./development/feature-file-map.md#keyboard-shortcuts) +- Welcome, Shortcut Showcase, or onboarding checklist: [Feature File Map](./development/feature-file-map.md#welcome-showcase-and-checklist), [Frontend Runtime Flow](./frontend/frontend-runtime-flow.md#welcome-showcase-and-checklist) - Breakpoints, sidebar collapse, or layout at different viewport sizes: [Responsive Layout](./frontend/responsive-layout.md) - Local-first or storage behavior: [Offline Storage And Migrations](./features/offline-storage-and-migrations.md) - Backend routes and API behavior: [Backend Route Map](./backend/README.md), [Backend Request Flow](./backend/backend-request-flow.md), [Backend Error Handling](./backend/backend-error-handling.md) diff --git a/docs/acceptance/shortcuts.md b/docs/acceptance/shortcuts.md index 32b027538..85bf9c5b2 100644 --- a/docs/acceptance/shortcuts.md +++ b/docs/acceptance/shortcuts.md @@ -16,11 +16,13 @@ Use this guide to validate: - navigating between views with the keyboard (D, W) - navigating between days in Day view (J, K, T) - navigating between weeks in Week view (J, K, T) -- opening and using the command palette (Cmd+K) +- opening and using the command palette (Cmd+K), including undo/redo rows - creating events with keyboard shortcuts (C, A in both Day and Week view) - editing events with the same keys in Day and Week (Delete, Shift+arrows, draft arrows) +- focusing events with arrow keys (chronological in Day, spatial in Week) +- toggling event-jump chips (`S`) and Hardcore Mode (`H`) - toggling the sidebar (]) -- undoing with the keyboard (Cmd+Z / Ctrl+Z) +- undoing / redoing with the keyboard (Cmd+Z / Cmd+Shift+Z) - confirming that shortcuts do not fire while typing in inputs Do not use this guide to validate: @@ -50,19 +52,20 @@ Helpful notes: | `W` | Global | Navigate to Week view | | `Cmd+K` / `Ctrl+K` | Global | Open command palette | | `]` | Global | Toggle sidebar | +| `?` | Global | Toggle shortcuts legend | | `Cmd+Z` / `Ctrl+Z` | Global | Undo last event action | | `Cmd+Shift+Z` / `Ctrl+Shift+Z` | Global | Redo last undone event action | +| `H` | Global | Toggle Hardcore Mode | | `J` | Day view | Previous day | | `K` | Day view | Next day | | `T` | Day view | Go to today | | `I` | Day view | Focus sidebar | | `U` | Day view | Focus first calendar event | -| `S` | Day view | Toggle event jump keys | -| `H` | Day view | Toggle Hardcore Mode | +| `S` | Day view | Toggle event jump keys | | `C` | Day view | Create timed event | | `A` | Day view | Create all-day event | | `Delete` | Day view | Delete focused event | -| `ArrowUp` / `ArrowDown` | Day view | Focus previous/next event | +| `ArrowUp` / `ArrowDown` / `ArrowLeft` / `ArrowRight` | Day view | Focus previous/next event chronologically | | `Arrow keys` | Day view | Move open draft event | | `Enter` | Day view | Open focused event | | `E` then `T` | Day view | Edit focused event title | @@ -83,10 +86,10 @@ Helpful notes: | `A` | Week view | Create all-day event | | `I` | Week view | Focus sidebar | | `U` | Week view | Focus first calendar event | -| `S` | Week view | Toggle event jump keys | -| `H` | Week view | Toggle Hardcore Mode | +| `S` | Week view | Toggle event jump keys | | `Delete` | Week view | Delete focused event | -| `ArrowUp` / `ArrowDown` | Week view | Focus previous/next event | +| `ArrowUp` / `ArrowDown` | Week view | Focus previous/next event on the same day | +| `ArrowLeft` / `ArrowRight` | Week view | Focus time-nearest event on previous/next non-empty day | | `Arrow keys` | Week view | Move open draft event | | `Enter` | Week view | Open focused event | | `E` then `T` | Week view | Edit focused event title | @@ -180,16 +183,17 @@ Pressing Cmd+K opens the command palette from any view, including while a text i 2. Press Cmd+K (or Ctrl+K on Windows). 3. Observe the palette contents. 4. Use the search/filter to type "event". -5. Select "Create Event" from the palette. +5. Select "Create event" from the palette. 6. Press Cmd+K again and then Escape. ### Expected Results - The command palette opens immediately. -- Items include: Create Event, Create All-Day Event, Go to Today, Log Out. +- Items include: Create event, Create all-day event, Go to Today, Toggle Hardcore Mode, Practice shortcuts, Show welcome guide, Undo last change, Redo last change, Log Out. +- Undo / Redo rows show their keycaps and stay disabled when there is no history. - Google Calendar connection status and actions appear in the sidebar, not the command palette. - Typing filters the list. -- Selecting "Create Event" opens the event creation form. +- Selecting "Create event" opens the event creation form. - Pressing Escape closes the palette without taking action. - Cmd+K works even when a text input elsewhere has focus. @@ -360,7 +364,33 @@ Pressing `S` shows event-jump chips. Week view chips use day prefixes (`SU`/`M`/ --- -## Scenario 13: Shortcuts Do Not Fire While Typing In Inputs +## Scenario 13: H Enters Hardcore Mode + +### UX + +Bare `H` toggles Hardcore Mode (keyboard-only). While active, pointer clicks are blocked (scroll and hover still work) so the user practices keyboard navigation. A persistent “Hardcore Mode · Esc” indicator shows how to exit. Mode is not persisted across refresh. Shift alone does not enter this mode. + +### Steps + +1. Navigate to `/week` with at least one event visible. +2. Press `H` (not a chord). +3. Try clicking an event with the mouse. +4. Use `U` / arrows / `Enter` to open an event with the keyboard. +5. Press `Esc` (with no modal/form open) or press `H` again. + +### Expected Results + +- A “Hardcore Mode · Esc” indicator appears. +- Clicks do not open events or focus controls; the indicator may pulse on a blocked click. +- Clicks inside onboarding UI (`[data-onboarding-ui]`) still work. +- Keyboard shortcuts continue to work. +- If a modal, floating layer, or event form is open, `Esc` dismisses that owner first; a later `Esc` exits Hardcore Mode. +- Exiting clears the indicator. Reloading the page also clears the mode. +- Event-jump chips (`S`) clear when Hardcore enters. Shift alone / Shift-Shift do not toggle Hardcore Mode. + +--- + +## Scenario 14: Shortcuts Do Not Fire While Typing In Inputs ### UX @@ -389,7 +419,7 @@ If time is limited, run these checks before shipping shortcut-related changes: 1. `D`, `W` navigate to the correct views from any starting view. 2. `J` and `K` navigate days in Day view and weeks in Week view. 3. `T` returns to today from any offset in both Day and Week view. -4. Cmd+K opens the command palette; Escape closes it without action. +4. Cmd+K opens the command palette; Escape closes it without action; Undo/Redo rows are present. 5. `C` opens a timed event form and `A` an all-day event form, in both Day and Week view. 6. `]` toggles the sidebar in both Week and Day view. 7. Delete removes a focused event in Day and Week view and shows an undo toast. @@ -397,7 +427,8 @@ If time is limited, run these checks before shipping shortcut-related changes: 9. No shortcuts fire inside a focused text input except Cmd+K. 10. Shift+ArrowLeft/Right move a focused event by one day in both Day and Week view. 11. Arrow keys reposition an open draft in both Day and Week view. -12. With a focused event and no draft open, ArrowUp/ArrowDown move focus to the previous/next event chronologically. +12. With a focused event and no draft open: in Week view ArrowUp/ArrowDown stay on the same day and ArrowLeft/Right jump to the time-nearest event on the previous/next non-empty day; in Day view all four arrows move chronological focus. 13. Cmd+D / Ctrl+D duplicates a focused event in Day and Week view. 14. With a focused event, `E` then `T` opens the form with the title focused; `E` then `A` / `C` jump to account / color; bare `E` alone does nothing. -15. Pressing `S` shows event jump chips; a day letter + digit focuses that event; Shift+Tab does not show chips. `H` toggles Hardcore Mode. +15. Pressing `S` shows event jump chips; a day letter + digit focuses that event; Shift+Tab does not show chips. +16. `H` enters Hardcore Mode (clicks blocked, indicator visible); Esc or another `H` exits. Shift-Shift does not. diff --git a/docs/development/common-change-recipes.md b/docs/development/common-change-recipes.md index 50b3b1b36..532f6ee35 100644 --- a/docs/development/common-change-recipes.md +++ b/docs/development/common-change-recipes.md @@ -69,6 +69,39 @@ consistent. 2. Verify auth-state implications in `packages/web/src/auth/compass/session/SessionProvider.tsx` and auth-state helpers. 3. Test both never-authenticated and previously-authenticated behavior. +## Add Or Change A Keyboard Shortcut + +1. Add or update the entry in `packages/web/src/shortcuts/shortcuts.registry.ts` + (id, keys, label, section, optional `when`). The `?` legend and overlay + sections read from this registry — do not hard-code a second label list. +2. If the Shortcut Showcase teaches the binding, also update + `packages/web/src/shortcuts/keymap.ts`. `keymap.test.ts` fails if the + registry row and keymap disagree. +3. Register the key in the owning hook: + - global / shell: `packages/web/src/shortcuts/useGlobalShortcuts.ts` + - week: `packages/web/src/views/Week/hooks/shortcuts/useWeekViewShortcuts.ts` + (+ behavior in `useWeekShortcutOwner.ts`) + - day: `packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts` + - shared grid edit/focus: `packages/web/src/grid/shortcuts/` + - event jump (`S`): `packages/web/src/shortcuts/shift-hint/` + - Hardcore Mode (`H`): `packages/web/src/shortcuts/keyboard-only/` +4. Respect `app-lock`, `escape-ownership`, and “do not fire while typing in + inputs” (use the existing `useAppShortcut` helpers; do not attach bare + `window` listeners for new app shortcuts). Bare-letter shortcuts (`S`, `H`, + `E`…) must yield to an armed edit sequence. +5. Add or update registry/data tests and the owning hook tests. Update + [Shortcuts acceptance](../acceptance/shortcuts.md) when user-visible + behavior changes. + +Common pitfalls: + +- **Legend-only change** — updating the registry without a handler (or the + reverse) ships a lie in the `?` overlay. +- **Showcase drift** — remapping a taught shortcut in the handler but not + `keymap.ts` leaves the practice calendar teaching the old keycaps. +- **App-lock blind spots** — shortcuts that fire while Auth/Settings/Welcome + own the UI feel broken; follow the helpers that already gate on lock state. + ## Change A Shared Hotkey Dialog (Day + Week) Use this for overlays mounted in both `WeekView` and `DayViewContent` (for example Dedication). diff --git a/docs/development/feature-file-map.md b/docs/development/feature-file-map.md index cc55a0462..a15a6a214 100644 --- a/docs/development/feature-file-map.md +++ b/docs/development/feature-file-map.md @@ -38,8 +38,12 @@ Use this document to find the first files to inspect for common Compass changes. - Day view route and content: `packages/web/src/views/Day/view` - Day view header (includes sidebar toggle control): `packages/web/src/views/Day/components/Header/Header.tsx` -- Day keyboard shortcuts: `packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts` -- Sidebar toggle (`]`): `packages/web/src/views/Week/hooks/shortcuts/useGlobalShortcuts.ts` +- Day keyboard shortcuts (thin key registration): `packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts` +- Week keyboard shortcuts (thin key registration): `packages/web/src/views/Week/hooks/shortcuts/useWeekViewShortcuts.ts` +- Week shortcut owner (draft create/nav/focus + bus): `packages/web/src/views/Week/hooks/shortcuts/useWeekShortcutOwner.ts` +- Shared grid edit/focus shortcuts: `packages/web/src/grid/shortcuts/useGridEventEditShortcuts.ts`, `focus-adjacent-grid-event.ts` +- Day column set when Google is connected (hides local Compass column): `packages/web/src/views/Day/components/Calendar/dayCalendarColumns.util.ts` +- All-day event color wash on day columns: `packages/web/src/grid/utils/allDayColumnTint.util.ts` - Day view hooks: `packages/web/src/views/Day/hooks` - Week view: `packages/web/src/views/Week` - Responsive layout controller (auto-collapse on breakpoint crossings): `packages/web/src/components/AuthenticatedLayout/useResponsiveLayout.ts` @@ -48,11 +52,40 @@ Use this document to find the first files to inspect for common Compass changes. - week view: `packages/web/src/views/Week/WeekView.tsx` - day view: `packages/web/src/views/Day/view/DayViewContent.tsx` +## Keyboard Shortcuts + +Authoritative legend data and taught bindings live under +`packages/web/src/shortcuts`. View owners register keys; do not duplicate +labels outside the registry. + +- Registry (source of truth for `?` legend): `packages/web/src/shortcuts/shortcuts.registry.ts` +- Overlay sections derived from the registry: `packages/web/src/shortcuts/data/shortcuts.data.ts` +- Taught bindings (handlers + Shortcut Showcase keycaps): `packages/web/src/shortcuts/keymap.ts` +- Global shell shortcuts (sidebar `]`, palette, settings, navigation): `packages/web/src/shortcuts/useGlobalShortcuts.ts` +- Event-jump chips (`S`): `packages/web/src/shortcuts/shift-hint/` +- Hardcore Mode (`H`; clicks inert until Esc / `H` / refresh): `packages/web/src/shortcuts/keyboard-only/` +- Escape ownership (modals/form before lower handlers): `packages/web/src/shortcuts/escape-ownership.ts` +- App lock (suppress shortcuts while a modal owns the UI): `packages/web/src/shortcuts/app-lock.ts` +- Mount point for global + Hardcore Mode hooks: `packages/web/src/components/RootShell/RootShell.tsx` +- Acceptance runbook: [Shortcuts](../acceptance/shortcuts.md) + +## Welcome, Showcase, And Checklist + +Anonymous calendar onboarding is three stacked surfaces, all mounted from +`RootShell` except on `/life` (see `isLifePathname`). + +- Welcome modal (**Start Now** / Escape both hand off to the showcase): `packages/web/src/components/WelcomeModal/WelcomeModal.tsx` +- Shortcut Showcase (takeover practice calendar): `packages/web/src/components/ShortcutShowcase/` (`showcase.steps.ts` is the taught-step order) +- Post-showcase checklist over real events: `packages/web/src/components/OnboardingChecklist/` +- Replay from the command palette (“Practice shortcuts”, “Show welcome guide”): `packages/web/src/components/CommandPalette/navigation.cmd.constants.ts` +- Legacy tour seen-flag is honored once so established users are not ambushed: `packages/web/src/components/ShortcutShowcase/showcase.storage.ts` + ## Sidebar - Shared sidebar shell: `packages/web/src/components/Sidebar/Sidebar.tsx` - Month picker: `packages/web/src/components/Sidebar/MonthPicker/MonthPicker.tsx` -- Account identity/sync indicator: `packages/web/src/components/Sidebar/CalendarList/CalendarListHeader.tsx` +- Shared account sync-status + CTA labels: `packages/web/src/components/Sidebar/CalendarList/useAccountHeaderStatus.ts` +- Account identity/sync indicator: `packages/web/src/components/Sidebar/CalendarList/CalendarListHeader.tsx`, `AccountSectionHeader.tsx` - Sidebar actions and shortcuts overlay: `packages/web/src/components/Sidebar/SidebarActions/SidebarActions.tsx`, `packages/web/src/components/Sidebar/ShortcutsOverlay/ShortcutsOverlay.tsx` - Week mount point: `packages/web/src/views/Week/WeekView.tsx` - Day mount point: `packages/web/src/views/Day/view/DayViewContent.tsx` @@ -88,6 +121,8 @@ see [Google Sync And SSE Flow](../features/google-sync-and-sse-flow.md) for the full picture. - Service entrypoint + internal routes: `packages/sync/src/app.ts`, `packages/sync/src/server/` +- Failed-job self-heal sweep: `packages/sync/src/domain/failed-job-requeue.service.ts` (wired from `packages/sync/src/app.ts`) +- Operator CLI for exhausted jobs: [CLI](./cli.md#manage-exhausted-sync-jobs) - Diagnostics / retention / principal purge: `packages/sync/src/server/diagnostic.routes.ts`, `packages/sync/src/domain/connection-retention.service.ts`, `packages/sync/src/domain/principal-purge.service.ts` - Sync DB backup/restore CLI: `packages/scripts/src/commands/sync-backup.ts`, `sync-restore.ts` diff --git a/docs/features/google-sync-and-sse-flow.md b/docs/features/google-sync-and-sse-flow.md index 6158e00cc..9d54fafed 100644 --- a/docs/features/google-sync-and-sse-flow.md +++ b/docs/features/google-sync-and-sse-flow.md @@ -200,6 +200,29 @@ remember to click **Refresh calendar**. 4. User initiates re-consent via the OAuth flow (proxied to Sync). 5. Sync completes the OAuth exchange; the backend's next metadata fetch/change-feed poll picks up the reconnected state. +## Failed Job Self-Heal (Sync Operator Path) + +Sync workers mark a job `failed` after its per-attempt retry ladder is spent. +Nothing else requeues that row unless the **failed-job self-heal** sweep runs +(`failedJobRequeue` in `packages/sync/src/app.ts`, logic in +`packages/sync/src/domain/failed-job-requeue.service.ts`): + +1. After a ~30 minute cooldown, the sweep requeues cooled-down failed jobs with + a fresh attempt budget (up to `FAILED_JOB_MAX_REQUEUES`, currently 3). +2. Jobs that keep failing past that budget are **exhausted** and need an + operator — see [manage-failed-jobs](../development/cli.md#manage-exhausted-sync-jobs). +3. Exhausted jobs whose connection already has a durable provider read-failure + marker (`lastReadFailureAt`, for example Google `notACalendarUser`) are + **auto-cleared** so their coalescing key no longer blocks rediscovery / + reconnect enqueue. Health already surfaces those provider errors; keeping + the failed row only adds log noise. + +Watch sync logs for: + +- `Sync self-heal sweep requeued N failed job(s)` +- `Sync self-heal sweep cleared N exhausted job(s) blocked by durable provider read failure` +- `Sync self-heal sweep: N failed job(s) exhausted their requeue budget and need operator attention` + ## Rules Of Thumb For Changes - New realtime behavior usually needs changes in `core` diff --git a/docs/frontend/frontend-runtime-flow.md b/docs/frontend/frontend-runtime-flow.md index 4f9961954..d8d31f55d 100644 --- a/docs/frontend/frontend-runtime-flow.md +++ b/docs/frontend/frontend-runtime-flow.md @@ -52,6 +52,47 @@ Important behavior: This is the shell for the main desktop app experience. +## Welcome, Showcase, And Checklist + +Files: + +- `packages/web/src/components/RootShell/RootShell.tsx` +- `packages/web/src/components/WelcomeModal/WelcomeModal.tsx` +- `packages/web/src/components/ShortcutShowcase/` +- `packages/web/src/components/OnboardingChecklist/` + +`RootShell` mounts the welcome modal, Shortcut Showcase, onboarding +checklist, global navigation / calendar-shell shortcuts, and Hardcore Mode. +Those calendar-onboarding overlays are skipped on `/life`. + +Welcome → showcase → checklist contract: + +- unauthenticated users who have not seen welcome get `WelcomeModal` +- **Start Now** and backdrop / Escape both start the Shortcut Showcase (neither + path dumps the user onto a blank calendar) +- Log In / Sign Up hand off to auth; a pending showcase offer can still run + after signup (`showcase.storage.ts`) +- showcase step order lives in `showcase.steps.ts`; taught keycaps come from + `packages/web/src/shortcuts/keymap.ts` +- after the showcase, `OnboardingChecklist` is practice missions on the real + calendar (not an app-lock modal) +- command palette can reopen practice (“Practice shortcuts”) or the welcome + guide (“Show welcome guide”) +- users who already finished or skipped the retired guided tour are treated as + having seen the showcase so it does not ambush them + +Hardcore Mode (`H`, also mounted from `RootShell`): + +- blocks pointer clicks while active; scroll and hover remain +- clicks inside `[data-onboarding-ui]` still work so the showcase/checklist + stay usable +- exits via Escape when nothing higher owns Escape, another `H`, or refresh +- entering Hardcore clears event-jump chips so there is not a second Esc owner + +See [Shortcuts](../acceptance/shortcuts.md) for acceptance coverage and +[Feature File Map](../development/feature-file-map.md#keyboard-shortcuts) for +file pointers. + ## Session Runtime File: diff --git a/docs/self-hosting/monitoring.md b/docs/self-hosting/monitoring.md index 31b788ce0..39da650f1 100644 --- a/docs/self-hosting/monitoring.md +++ b/docs/self-hosting/monitoring.md @@ -40,6 +40,7 @@ What to watch, in sync container logs (`./compass logs sync`): - **`Sync scheduler draining, reconciling, renewing channels, retaining, and reporting health`** — logged once at startup when active; confirms the job worker, the reconcile sweep, and subscription renewal are all running. If you only see `Sync retention + health snapshot started (passive / unconfigured)` instead, Sync isn't doing calendar work — check `google.clientId`/`google.clientSecret` are set and `sync.execution: active`. - **`Sync reconcile sweep enqueued N pull(s)`** — logs roughly every 10 minutes when there's stale work to catch up on. This is the fallback for missed push notifications; every connected calendar converges through it even if Google's webhook never arrives. It's normal for this to log `0` most of the time on a healthy install (nothing missed). - **`Sync job {kind} ({id}) dropped: {reason}`** (warn level) — a job settled without completing (e.g. the connection's Google access was revoked). The affected user needs to reconnect; Compass surfaces this in the UI as a "Reconnect Google Calendar" prompt automatically. +- **`Sync self-heal sweep … exhausted … need operator attention`** (error level) — a job burned its retry ladder and the automatic requeue budget. Inventory and clear/requeue with `bun run cli manage-failed-jobs` ([CLI](../development/cli.md#manage-exhausted-sync-jobs)). Durable provider refusals (connection already stamped with a read-failure marker) are auto-cleared by the same sweep so they do not page forever. Unlike the older backend-only sync engine, Sync manages its own push-notification channel renewal internally — there's no separate cron job or maintenance endpoint to wire up.