Skip to content

fix(db): resolve startup crash from duplicate migration and invalid SQLite syntax - #104

Closed
rghamilton3 wants to merge 17 commits into
developfrom
fix/migration-crash-beta12
Closed

rghamilton3 wants to merge 17 commits into
developfrom
fix/migration-crash-beta12

Conversation

@rghamilton3

Copy link
Copy Markdown
Contributor

Summary

  • Rename 013_workout_note_tags.sql → 014_workout_note_tags.sql to resolve duplicate migration version number conflict with 013_drop_display_visible.sql
  • Remove IF EXISTS from ALTER TABLE ... DROP COLUMN in migration 013 -- SQLite does not support this syntax variant, causing a startup panic

Both bugs combined caused the app to crash immediately on launch with no visible error. Shipped as v1.0.0-beta.12.

Test plan

  • bun run test -- 2554/2554 passing
  • bun run lint -- clean
  • bun run build -- clean
  • bun tauri dev -- app starts and DB initializes correctly

rghamilton3 and others added 16 commits April 4, 2026 22:34
* fix: add Authorization header to Supabase reachability check

The PostgREST root endpoint requires both `apikey` and `Authorization`
headers. Missing the latter caused a 401 during setup, showing
"Cannot reach server" even with valid credentials.

* fix: auto-validate and surface env var connection failures on setup page

When VITE_SUPABASE_URL and VITE_SUPABASE_PUB_KEY are set but invalid,
the setup page now auto-validates on mount and shows the specific error
(e.g. "Server responded with 401") instead of silently presenting a
blank form. Also adds a banner explaining that environment variables
were detected but failed.

* fix: use Supabase client for connection validation instead of raw fetch

Raw fetch with apikey/Authorization headers only works with legacy anon
keys. New publishable keys (sb_publishable_xxx) require the Supabase
client library which handles auth headers internally. This switch
supports both key formats transparently.

* fix: resolve duplicate migration timestamp collision

Two migrations shared timestamp 20260329000001, causing a primary key
violation in supabase_migrations. Renumbered create_sharing_tables to
20260329000002 and expand_rls_for_sharing to 20260329000003.

* chore: update Claude Code configuration and skills

* feat: complete sync pull path with dynamic upsert and force-pull (Step 9) (#28)

Implement the stubbed pull/upsert path so remote changes from Supabase
are actually written to local SQLite, completing bidirectional sync.

- Fix broken timestamp parsing in upsert_row (ISO 8601 strings were
  silently parsed as 0, causing all remote updates to be discarded)
- Implement dynamic column upsert via pragma_table_info with type
  coercion (timestamps, booleans, JSONB) between Supabase and SQLite
- Add pull_table/pull_all for paginated force-pull via Supabase REST API
- Wire sync_force_pull command with proper state transitions
- Add auto-pull on first sign-in (detect last_pull_at == 0)
- Add 28 new tests (49 total), all passing

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: events & packing lists (Step 13.5) (#30)

* feat(events): add events & packing lists feature (Step 13.5)

Introduces EVENT as a new session category with full support for event
metadata (date, location, URL, requirements) and a categorized packing
checklist with drag-and-drop reorder, optimistic toggle, progress
tracking, and countdown notifications.

Database:
- Migration adds EVENT to category CHECK constraints on session_templates
  and scheduled_sessions; adds event_metadata JSONB to session_templates
  and workout_logs; creates event_items table with polymorphic FK
  (template XOR log), RLS policies, partial indices, and updated_at trigger

Domain:
- eventRequirementSchema, eventMetadataSchema, eventItemSchema (Zod)
- EVENT added to SessionType enum; eventMetadata optional on templates
  and logs; EventItemRow added to database.types.ts

Data layer:
- toEventItem/fromEventItem mappers; eventMetadata parsing in
  toSessionTemplate (JSON.parse) and toWorkoutLog (direct JSONB)
- DataAdapter interface: getEventItems, saveEventItem, updateEventItem,
  deleteEventItem, toggleEventItemPacked (accepts target bool, no race),
  reorderEventItems (Promise.all), cloneSessionTemplate (isPacked reset)
- SupabaseAdapter implements all methods; getSessionTemplateFull fetches
  eventItems for EVENT templates; updateSessionTemplateFull uses 3-way
  reconciliation to preserve item UUIDs and isPacked state
- TauriAdapter stubs (event support deferred, W-8)

Hooks:
- useEventItems, useCreateEventItem, useUpdateEventItem,
  useDeleteEventItem, useToggleEventItemPacked (optimistic),
  useReorderEventItems (optimistic), useNextUpcomingEvent

UI:
- event-builder/: EventTemplateForm (Sheet overlay, create/edit),
  EventDetail (metadata + map link + packing), PackingList (DnD,
  collapsible categories, error feedback), PackingItem (checkbox toggle),
  EventProgressBar (ember on surface-steel, ARIA progressbar),
  EventCard (library), EventCountdownBadge (Today screen, TODAY edge
  case), RequirementEditor, EventItemEditor, shared styles.ts
- Library: EventCard for EVENT templates, NEW EVENT button
- log/$workoutId: interactive EventDetail for event logs
- history/$workoutId: read-only EventDetail for event history
- Today screen: countdown badge when next event within 30 days
- Program builder: flag icon + ember accent for EVENT sessions,
  event date displayed in program-preview grid and detail sections

Native (Rust):
- event_reminders channel registered in notification.rs
- EventReminderState: background Tokio task (60s poll), checks
  configured intervals (7d/3d/1d), quiet hours, packing progress in
  notification body, deterministic notification ID
- schedule_event_reminders / cancel_event_reminders Tauri commands
- Clippy fixes: RemindedState type alias, needless_borrow in lib.rs,
  manual_range_contains in session_reminder.rs
- Lock contention fix: single mutex guard across event loop in tick()

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 002-events-packing-lists

* docs(planning): mark 002-events-packing-lists complete

Updates Steps.md status to Complete, session.md with full wave
execution log and fixes applied, and implementation-plan.md.

Feature: 002-events-packing-lists

* chore(rust): apply cargo fmt across codebase

* chore: add mise config and cortex hooks

* fix(events): address PR #30 review findings

- Fix timezone bug in combineDateTime: store local date strings instead
  of converting to UTC, preventing event dates from shifting by a day
- Add eventUrl validation in form validate() before save
- Switch sequential item persistence to Promise.allSettled with error
  collection for partial-failure reporting
- Fix DST-unsafe daysUntil calculation using date-only comparison
- Make TauriAdapter getEventItems throw consistently with write stubs
- Add "TODAY" special case in Rust event reminder notification title
- Eliminate double hydration of requirements/items in form init
- Add EventItem mapper roundtrip tests and splitDateTime/combineDateTime
  edge case tests (19 new tests)

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(groups): coach program assignment

Adds the ability for a group coach to assign one of their programs to a
member. The program is atomically transferred to the member's ownership
with source tagged as COACH_ASSIGNED and the coach recorded as created_by,
enabling the member to filter out programs they received vs created.

Includes the two-step AssignProgramDialog UI and integrates it into
MemberCard. Both the Tauri SQLite command and the Supabase SECURITY
DEFINER RPC perform the same three authorization checks (caller is COACH,
target is MEMBER, program owned by caller) before executing the transfer.

* fix(sync): resolve incremental push breakage and missing error state

- push.rs: timestamp_millis() -> timestamp() so last_push_at is stored
  in seconds, matching the updated_at column it filters against; the
  millis value was always larger than any updated_at, causing zero rows
  to be found on every push after the first
- sync.rs: sync_force_push now transitions to SyncState::Error on
  failure, matching the sync_force_pull pattern; previously a failed
  push left the engine in Idle, hiding the error from the frontend

* docs(planning): add spec, tech, and steps for coach program assignment

* feat(chat): add chat data layer + QA fixes (#33)

* feat(chat): add chat data layer (Step 21)

Builds the full persistence layer for the chat domain across Supabase,
SQLite, Rust, and TypeScript. No UI -- pure data plumbing for Steps 22-25.

- Supabase migration: conversations, conversation_participants, messages,
  media_attachments with RLS (CH-1), append-only messages (CH-7), and
  direct conversation uniqueness via trigger (CH-2)
- SQLite migration: mirrors Postgres with sync_status on messages for
  offline queueing (pending -> synced/failed state machine)
- Rust: ConversationRow/MessageRow/etc structs, 10 Tauri commands (CRUD
  for conversations, messages, participants, media), chat tables in
  SYNCABLE_TABLES for push/pull sync
- TypeScript: Zod schemas (Conversation, Message, MediaAttachment,
  WorkoutSnapshot), DB row types, bidirectional mappers, 13-method
  ChatAdapter facet on DataAdapter interface, Supabase and Tauri
  adapter implementations, mock adapter updated

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011
Feature: 004-Chat-Data-Layer

* docs(planning): add spec and steps for chat data layer (Step 21)

Feature 004 planning artifacts: Spec.md covering requirements,
testable assertions, and invariants (CH-1, CH-2, CH-4, CH-7);
Steps.md with 4-wave execution plan for db-engineer, rust-engineer,
domain-engineer, and quality-engineer.

Feature: 004-Chat-Data-Layer

* fix(qa): address all QA feedback -- sync resilience, lint, and stale tests

Rust sync hardening:
- sync/mod.rs: replace `return` with `continue` so queue flush failure no longer kills the sync loop permanently; transition to Error state when initial pull fails rather than silently proceeding to push
- sync/pull.rs: add SYNCABLE_TABLES allowlist guard in upsert_row and delete_row
- sync/push.rs: add SYNCABLE_TABLES allowlist guard in push_table
- sync/queue.rs: normalize enqueue timestamp to seconds (was millis, inconsistent with push/pull)

Frontend lint:
- packing-list.tsx: replace setState-in-useEffect anti-pattern with useMemo derivation for mutation error feedback
- event-template-form.tsx: extract splitDateTime/combineDateTime to date-time-utils.ts to fix Fast Refresh violation (non-component exports alongside component)

Tests (31 stale failures fixed):
- Update 5 test files with correct mixed-case text after CSS uppercase refactor
- Rewrite mobile-nav.test.tsx: add useAuth mock, update to 5+1 nav structure, correct icons and routes

* fix(review): resolve PR #33 review findings for chat data layer

Critical fixes (C-1 through C-5):
- Add missing INSERT policy on conversations table (C-1)
- Filter user's own messages from getUnreadCounts in Supabase adapter (C-2)
- Change z.url() to z.string().url() for media attachment URLs (C-3)
- Return Err instead of Ok(()) from SYNCABLE_TABLES allowlist guards (C-4)
- Add missing UPDATE policy on media_attachments table (C-5)

Important fixes (I-2, I-3):
- Add participant check before message insert in send_message (I-2)
- Validate direct conversations require exactly 2 participants (I-3)

Tasks created (S013-S017) for: data mapper tests, dedicated
get_messages_since command, updatedAt mapping fix, unread counts
resilience, and sync guard test coverage.

Suggestions S-1 through S-6 tracked in Ideas backlog.

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

* feat(chat): implement review remediation tasks S013-S017

S013: Add 36 data mapper unit tests for chat entities
  - Conversation, ConversationParticipant, Message, MediaAttachment
  - Round-trip, null handling, and Zod validation tests

S014: Add dedicated get_messages_since Rust command
  - SQL-level WHERE created_at > ? filter replaces client-side filtering
  - Eliminates silent truncation at 1000 message limit

S015: Fix toConversationParticipant updatedAt mapping
  - Use last_read_at ?? joined_at instead of always joined_at

S016: Improve getUnreadCounts error resilience
  - Single conversation failure no longer aborts all counts
  - Returns partial results with console.warn for failed queries

S017: Remove #[cfg(not(test))] from sync allowlist guards
  - Tests now exercise the same allowlist path as production
  - Updated test fixtures to use real SYNCABLE_TABLES entries

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: Supabase Realtime, chat data layer, events & coach assignment (#34)

* docs(planning): add spec, tech, and steps for supabase realtime integration

4-phase blueprint for Step 22 (Supabase Realtime Integration).
Establishes architecture for Broadcast channel management, cursor-based
message pagination, RealtimeManager module, and all chat TanStack Query hooks.

ADR-005: cursor-based getMessages replaces offset-based (no existing callers).

* feat(chat): add Supabase Realtime broadcast channels and chat hooks

Wire up Supabase Realtime Broadcast for live message delivery and typing
indicators. Introduces the RealtimeManager singleton, cursor-based message
pagination, platform-agnostic foreground detection, and all TanStack Query
hooks needed by the Chat UI (Step 23).

New files:
- src/lib/realtime-schemas.ts     -- Zod schemas for broadcast payloads
- src/lib/realtime-manager.ts     -- channel lifecycle, typing state, catch-up
- src/lib/foreground-detector.ts  -- visibilitychange / Tauri focus detection
- src/hooks/use-chat.ts           -- useConversations, useMessages (infinite),
                                     useSendMessage (optimistic), useRealtimeMessages, +6 more
- src/components/chat-realtime-listener.tsx -- app-level cache update listener

Modified:
- data-adapter.ts / supabase-adapter.ts / tauri-adapter.ts / chat.rs
  Replace offset-based getMessages with cursor-based (before?: ISO timestamp)
- main.tsx: mount ChatRealtimeListener alongside SyncListener
- auth.tsx: call resetRealtimeManager() on SIGNED_OUT

Tasks: S001, S002, S003, S004, S005, S006, S007

* fix: resolve QA lint and code quality issues across frontend and Rust

Fix clippy errors in test code (manual_range_contains, approx_constant),
ESLint no-constant-binary-expression in utils test, and three code quality
issues in realtime-manager: validate typing payloads with Zod schema,
replace fragile onTyping callback chain with Set-based listener pattern,
and explicitly discard handleForeground Promise.

* fix(chat): address PR #34 review findings for Supabase Realtime

Multi-listener pattern (addMessageListener/addTypingListener) replacing
single-callback ownership, broadcast try/catch in useSendMessage,
channel lifecycle ownership fix, auth integration via useAuth hook,
and Wave 5 polish: named constants, toMessageFromBroadcast mapper,
simplified clearAllTyping, foreground-detector error logging and
async/await rewrite, broadcastTyping .catch(), MessagePaginationOptions
interface, user_name max(200) constraint.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(chat): complete chat UI with messaging and conversations (#35)

* docs(planning): add spec, tech, and steps for chat UI (Step 23)

* feat(chat): build complete chat UI (Step 23)

Routes, conversation list, virtualized message detail, compose bar,
typing indicators, new conversation flow, group management, and
client-side blocking. Navigation updated -- COMMS replaces Social
on mobile, added after Vault on desktop sidebar with unread badge.

Conversation type extended with participantUserIds so ConversationDetail
can resolve the other user's display name and blocking state for direct
conversations. Supabase adapter fetches participants alongside
conversations; Tauri adapter was already receiving them and now uses them.

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 006-Chat-UI

* docs(planning): add quick plan for conversation participant IDs

Feature: 006-Chat-UI

* fix: update mobile-nav tests for Comms tab and fix Rust formatting

Update test assertions for Social→Comms rename, add useUnreadCounts
mock, and fix rustfmt formatting in event_reminder tests.

* fix(chat): resolve PR #35 review findings

Critical fixes:
- C1: Contact search filter was a no-op; now passes search prop to
  ContactRow which renders null when display name does not match
- C2: Sender name in group bubbles showed raw UUID; resolved via
  useUserProfile inside MessageBubble
- C3: Send failure gave zero feedback; added sendError state and
  inline "Failed to send" indicator on ComposeBar
- C4: Conversation creation failure was silent; added error state
  and inline message in NewConversationSheet
- C5: MessageList had no error/loading state; added spinner and
  error fallback from useMessages isLoading/isError

Important fixes:
- I1: Typing broadcast used user.email; now uses displayName via
  useUserProfile
- I2/I3: useBlockedUsers storage key scoped to userId; added
  console.error/warn on localStorage corruption
- I4: Optimistic message had senderId undefined; now uses current
  user ID from useAuth
- I5: LeaveDialog failure was silent; added inline error message
- I6: ParticipantSheet add failure was silent; shows error including
  Tauri offline message
- I7: Removed rounded-full from avatar containers and unread dots
- I8: Added comment clarifying group threshold (2 selected + server
  adds self = 3 total participants)

Suggestions:
- S3: Removed build-artifact comment from conversation-list.tsx
- S9: Fixed error copy from "pull to retry" to "try again"

Tasks: C1, C2, C3, C4, C5, I1, I2, I3, I4, I5, I6, I7, I8, S3, S9
Feature: 006-Chat-UI

* docs(planning): resolve PR #35 review and add test/polish tasks

Marks 0003-pr35-chat-ui-review.md as fully resolved (5 critical,
8 important, 2 suggestions fixed inline; 6 tasks created; 5
suggestions dismissed with rationale).

Adds Wave 7 tasks to Steps.md:
- S015-T: deriveItems unit tests
- S016-T: useBlockedUsers unit tests
- S017-T: formatTypingText unit tests
- S018-T: relativeTime + getInitials unit tests
- S019: Migrate useBlockedUsers to Zustand store
- S020: Wire archive action in conversation header

Feature: 006-Chat-UI

* refactor(chat): extract utils, add blocked-users store, verify review

Extract pure functions (relativeTime, getInitials, formatTypingText,
deriveItems) into chat-utils.ts and add tests. Migrate useBlockedUsers
to Zustand store with user-scoped persistence. Mark PR #35 review as
verified with follow-up tracked in GitHub issues #43-#51.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: add link preview metadata with OG tags, Twitter Cards, and useMetadata hook (#53)

Add comprehensive link preview support so shared URLs display rich previews
on social platforms and messaging apps. Includes a useMetadata hook for
dynamic per-route title and description updates, used on share pages.

* fix(auth): prevent PKCE code race condition on OAuth callback (#54)

Supabase JS v2.100.1 defaults to detectSessionInUrl: true with PKCE
flow, causing _initialize() to auto-exchange the auth code before the
/auth/callback route handler can. The second exchange attempt fails
(code already consumed), redirecting users to /sign-in?reason=oauth_error.

Disable detectSessionInUrl since we have a dedicated callback route
that handles PKCE code exchange manually.

* fix(auth): enable PKCE flow for OAuth sign-in (#55)

Without explicit flowType: 'pkce', the Supabase client uses implicit
flow, returning tokens as hash fragments (#access_token=...) that the
/auth/callback route cannot read (it expects ?code= query params).

Setting flowType: 'pkce' makes Supabase return a PKCE authorization
code as a query parameter that the callback can exchange for a session.

* fix(db): resolve RLS infinite recursion on group_members and conversation_participants (#56)

The group_members and conversation_participants SELECT policies
self-referenced their own tables, triggering PostgreSQL error 42P17
(infinite recursion). This cascaded 500s to every table whose sharing
policies join group_members (workout_logs, session_templates, etc.).

Creates SECURITY DEFINER helpers (is_group_member, is_group_coach,
is_conversation_participant) that bypass RLS for inner membership
lookups and replaces all six affected policies.

Also removes the PWA manifest (manifest.webmanifest) and its link tag
since Vercel deployment protection returns 401 on the unauthenticated
manifest fetch.

* feat(builder): program builder UI with responsive week grid (#58)

* feat(builder): program builder UI with responsive week grid

Implements the complete program builder and session builder UIs.

Program builder:
- Sidebar layout (desktop) / stacked layout (mobile) at /builder route
- Drag-and-drop block reordering with @dnd-kit
- Week grid defaults to 5-day Mon-Fri view with a toggle to show all 7
  days, improving readability at constrained content widths (~94px/cell
  at 1024px narrowed to nothing legible; 5-day gives ~132px/cell min)
- Session slots reduced from 80px to 56px min-height with type-colored
  left borders (STRENGTH/CONDITIONING/SE/MIXED/EVENT)
- Mobile block editor with vertical day-row tap targets
- Session picker bottom sheet and copy-week dialog
- Program preview overlay with per-session exercise/load tables

Session builder:
- Set scheme editor with per-type form variants
- Activity group editor and session template form

Infrastructure:
- Collapsible shadcn/ui component added
- Removed boilerplate App.tsx/App.css
- Expanded index.css with builder-specific design tokens

* fix(builder): declutter block header with overflow menu

Replace inline action buttons (move up/down, delete) with a dropdown
overflow menu in both desktop and mobile block editors. This prevents
touch target overlap on narrow viewports and isolates the destructive
delete action behind a two-tap interaction.

* feat(builder): guard destructive actions with confirmation and undo

Block and week deletions now require confirmation via AlertDialog,
showing impact ("This will remove N weeks and M sessions.") before
proceeding. Session removal fires immediately but shows a 5-second
undo toast via sonner, letting users recover accidental taps without
blocking the fast path.

- Add alert-dialog.tsx shadcn primitive (Radix AlertDialog)
- Add confirm-delete-dialog.tsx reusable builder component
- Wire block delete in BlockEditor and MobileBlockCard to dialog
- Wire week delete in WeekGrid and MobileWeekSection to dialog
- Add undo toast to SessionSlot and MobileDayRow session removal
- Mount Toaster in main.tsx (bottom-center)

* feat(builder): replace full-screen preview with inline week detail

Remove the disconnected ProgramPreview overlay from the builder flow.
Exercise details now toggle inline per-week via a visibility icon,
keeping the edit-preview feedback loop tight. Save button moves to
the header. Shared formatting helpers extracted to session-detail.tsx.
Also fixes Collapsible asChild bug in block-editor and mobile-block-editor.

* fix(builder): remove interstitial from session template create flow

Tapping "Create new template" in the session picker now opens the
inline form immediately, skipping the Library recommendation callout.
The Library option is preserved as a subtle link ("Want more options?
Create in the Library") below the form for users who need it.

Removes showCreateCallout state and the two-step reveal pattern.

* refactor(builder): consolidate session constants and polish UI details

- Extract SESSION_TINT and SESSION_TYPE_BADGE from session-slot,
  mobile-block-editor, and program-preview into constants.ts
- Remove duplicate @keyframes block-enter and slot-fill in index.css
- Bump section-level labels (WEEK N, day columns, EXERCISE/SETS/LOAD)
  from text-[11px] font-medium to text-xs font-semibold for visual
  hierarchy over metadata badges which stay at text-[11px] font-medium
- Upgrade session picker SheetTitle to text-sm font-display
- Add empty state copy when zero blocks exist in BlockList and
  MobileBlockEditor
- Clarify copy-week dialog: description now states sessions will be
  replaced, and per-week "(N existing)" label gains warning colour

* feat(builder): replace collapsed save error with structured inline validation

Validation errors now appear at the relevant field rather than as a single
joined string below the save button. Program name errors render under the
name input, block name and week errors render on each block card (visible
even when collapsed), and server/auth errors remain global in the header.

* feat(builder): add recently-used templates and reduce week grid noise

Surface the 5 most recently assigned session templates at the top of the
picker sheet so users with large template libraries reach common picks
faster. Adds lastAssignedAt tracking across both Tauri/SQLite and
Supabase backends.

Reduce visual noise in the week grid: restyle empty slots as muted
"Rest" labels instead of prominent calls-to-action, bring the 5/7-day
toggle to mobile, and collapse identical weeks within a block to a
compact summary row (with manual expand).

* refactor(builder): extract session-detail helpers to fix Fast Refresh lint errors

* feat: display broadcast infrastructure (Step 27) (#59)

* feat(chat): video, image, and file sharing (#57)

feat(chat): video, image, and file sharing (Feature 007)

Full-stack media sharing: Cloudflare Stream video uploads with TUS, image/file uploads via Supabase Storage, real-time transcoding status via webhook, signed URL caching, and inline media rendering in chat.

Review: 4 critical + 10 important findings resolved and verified. 12 enhancements implemented. 1,305 tests passing.

* docs: add plan for Step 27 -- display broadcast infrastructure

Spec.md, Tech.md, and Steps.md for Feature 008. Covers DisplaySnapshot
Zod schema, publisher module, store integration, display_visible toggle,
and Supabase Realtime Broadcast channel plumbing. 14 tasks across 4 waves
with 3 agents.

* feat: add display_visible column to user profiles

SQLite and Supabase migrations for display broadcast toggle,
Tauri model and command updates to support the new field.

* feat: add DisplaySnapshot domain type and display publisher

DisplaySnapshot type with Zod schema, display-publisher for
broadcasting workout state, data-mapper and store updates.

* feat: add PushToDisplayButton and display broadcast hook

useDisplayBroadcast hook, PushToDisplayButton component,
workout log and profile route integrations.

* test: add display snapshot and publisher tests

Unit tests for DisplaySnapshot schema, display-publisher,
display-snapshot builder, and updated data-mapper/adapter tests.

* docs: update Steps.md and implementation plan for display broadcast

* fix(review): resolve PR #59 review findings

Fixes: C1 (RLS auth), C2 (image signed URLs), C3 (snapshot try-catch),
C4 (Cloudflare subdomain), I1-I9 (error handling, schema tightening,
z.url() revert, publisher readiness), M1/M3/M5/M6 (env guards,
request parsing, query error logging, JSDoc)
Tasks: S024-T, S025-T added for media-provider and timeout tests
Comments: 9 comment issues resolved

Review: Context/Reviews/0005-pr59-display-broadcast-review.md

* test(media): add unit tests for CloudflareStreamProvider and transcoding timeout

S024-T: media-provider tests covering signed URL TTL cache hit/miss,
null client error, Zod validation of malformed responses, and cache
deduplication (13 tests).

S025-T: use-media-upload tests covering 5-minute transcoding timeout
marks processing attachment as failed, unaffected ready attachments,
query cache invalidation, and timer cleanup on unmount (4 tests).

Tasks: S024-T, S025-T
Feature: 007-video-image-sharing

* docs(review): mark PR #59 review as verified

* feat(edge-functions): deploy CF video edge functions with Vault secrets and local JWT signing

Deploys three Supabase Edge Functions for Cloudflare Stream video support
(Step 25). All Cloudflare credentials read from Supabase Vault using
CF_* secret names (CF_ACCOUNT_ID, CF_STREAM_TOKEN, CF_WEBHOOK_SECRET,
CF_SIGNING_KEY_PEM, CF_SIGNING_KEY_ID, CF_CUSTOMER_SUBDOMAIN).

chat-media-signed-url: replaces Cloudflare token API round-trip with
local RS256 JWT signing via CF_SIGNING_KEY_PEM + CF_SIGNING_KEY_ID.
No outbound HTTP call needed per playback request.
chat-media-upload-url: reads CF_ACCOUNT_ID + CF_STREAM_TOKEN from Vault.
chat-media-webhook: reads CF_WEBHOOK_SECRET + CF_ACCOUNT_ID from Vault.

Adds get_secret() SQL migration granting service_role-only read access
to vault.decrypted_secrets. Fixes duplicate migration timestamp
20260403000002 -> 20260403000003 for session_template_last_assigned.

Updates mock-supabase test util to support keyed RPC results
(get_secret:SECRET_NAME) for parameter-specific vault mocking.
Removes CLOUDFLARE_ACCOUNT_ID from STANDARD_ENV test fixture.

* chore: update app icons and branding assets

Replace default Tauri icons with Ardent Forge branding across all
platforms (Android, iOS, Windows, macOS). Update app display name
in Android strings.xml and tauri.conf.json.

* fix(db): rename migration 008 and auto-recreate DB on checksum mismatch

Rename session_template_last_assigned to migration 009 to make room
for 008_add_display_visible. Add debug-only auto-recreate logic so
migration checksum mismatches during development delete and rebuild
the local database instead of crashing.

* fix(auth): use Chrome Custom Tabs for Google OAuth on Android

Replace window.open with tauri-plugin-opener's inAppBrowser mode to
launch Google OAuth in Chrome Custom Tabs instead of an embedded
WebView. Google blocks OAuth in WebViews under their "Use secure
browsers" policy. Add opener plugin registration and URL scope
permissions for mobile capability.

* chore: update Claude agent configs and mise tooling

* feat(display): display route with board and focused views (#60)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* fix(display): address QA findings from PR #60 review (#61)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* feat(discovery): add discovery client and server URL flow (#63)

* docs(planning): add Feature 009 plan for discovery client + server URL

Creates the full 4-phase planning artifacts for Refactor A of the onboarding
redesign: server URL input with auto-discovery replaces raw Supabase credential
entry as the primary setup flow.

Tasks: S001, S001-T, S002, S003, S003-T, S004, S004-D
Feature: 009-discovery-client-server-url
ADR-006

* feat(setup): add discovery client and server URL flow

Replace raw Supabase credential inputs on the setup screen with a
primary server address field that auto-discovers backend credentials
via GET /.well-known/ardent-forge.json. Manual Supabase URL and key
entry moves into a collapsible "Manual configuration" section as a
fallback.

- Add discoverInstance() in src/lib/discovery.ts with URL normalization,
  8-second AbortController timeout, and typed DiscoveryResult discriminated
  union (NETWORK_ERROR, NOT_FOUND, INVALID_RESPONSE)
- Add 11 Vitest unit tests covering happy path, URL normalization variants,
  and all error branches
- Refactor setup.tsx: server URL primary input, discovery + validation
  state machine, "or" divider, collapsible manual section, disabled QR
  placeholder for future Refactor B
- Add /.well-known/ardent-forge.json handle block to Caddyfile with
  {$SITE_URL} and {$ANON_KEY} env var interpolation
- Add ANON_KEY to caddy service environment in docker-compose.yml
- Document discovery endpoint for static host and Docker deployments
  in docs/self-hosting.md; update mobile app configuration section

Tasks: S001, S001-T, S002, S003, S003-T, S004
Feature: 009-discovery-client-server-url
ADR: ADR-006

* docs(planning): mark Feature 009 Steps.md as complete

* fix(discovery): harden URL validation, use Zod, deduplicate connect logic

- Validate URL protocol via new URL() parse; reject non-http/https schemes
  (file://, ftp://, etc.) before any fetch is made
- Replace manual typeof field checks with a Zod schema (DiscoverySchema)
  to align with project convention and improve type safety
- Extract shared validateAndSave() helper in setup.tsx, eliminating
  duplicated validate-save-init-navigate block between handleConnect
  and handleDiscoverAndConnect
- Add 4 new tests: file:// rejection, ftp:// rejection, empty string,
  whitespace-only input

Feature: 009-discovery-client-server-url

* fix(review): resolve PR review findings for discovery feature

Fixes (P6-001..008): add logging to all error paths, capture error
params in catch blocks, correct error type semantics (INVALID_INPUT
for validation, INVALID_RESPONSE for JSON parse), fix JSDoc, add
aria-expanded to disclosure toggle, add double-submission guard,
fix comment scope.

Refactor: unify 4 parallel state variables into a single SetupState
discriminated union with 7 phases, eliminating impossible states.
Also wraps handlers in try-catch (P6-009..011) and fixes success
state ordering as part of the refactor.

Rules: create .claude/rules/error-handling.md codifying error type
semantics and catch block conventions (P6-017, P6-018).

Tasks: S005..S007-T added to Steps.md for schema tightening and
test coverage gaps (P6-012..015).

Review: Context/Reviews/0006-discovery-client-server-url-2026-04-04.md

* fix(discovery): tighten schema validators and add missing test coverage

S005: Use z.string().url() for supabase_url, z.string().min(1) for
supabase_publishable_key and version in DiscoverySchema. Validation
is now strictest at the system boundary.

S005-T: Add test for missing supabase_publishable_key field.
S006-T: Add test for 500 HTTP response (non-404 error code).
S007-T: Add test for URL with path component being stripped.

Review: P6-012, P6-013, P6-014, P6-015

* docs(review): mark discovery feature review as verified

* feat(display): idle mode with Edge Function broadcast (#62)

* docs(planning): add feature 009 idle mode spec, tech plan, and steps

Full 4-phase blueprint for Step 29 (Idle Mode + Edge Function).

- Spec.md: 8 must-haves, 17 testable assertions, all open questions
  resolved (24h clock default, /display?clock=12h override, max 3
  sessions, silently hide schedule when Edge Function unreachable)
- Tech.md: 8 architecture decisions -- idle_snapshot event type
  extension, Realtime HTTP broadcast API, Supabase cron schedule,
  program hierarchy JOIN query, server_time drift correction, CSS-only
  transitions, TanStack Router search param validation
- Steps.md: 10 tasks across 4 phases, frontend-specialist +
  backend-engineer + quality-engineer, parallel Phase 1 (types + Edge
  Function) and Phase 2 (ClockDisplay + IdleView + transition wrapper)

* feat(display): implement idle mode with Edge Function broadcast

Adds idle mode to the /display route -- the ambient state shown when no
workouts are active. A server-side Edge Function queries today's remaining
scheduled sessions every 60 seconds and broadcasts them to the display
channel so the TV always shows useful information.

- IdleSnapshot Zod schema + idle_snapshot event type in DisplayEventType
- ClockDisplay component (8rem Space Grotesk, server-time drift correction)
- IdleView component (clock, date, sessions list, next-up badge, footer)
- useDisplayMode hook deriving idle/board/focused from session map
- useIdleSnapshot hook subscribing to display Broadcast channel
- DisplayModeTransition wrapper (300ms fade for idle↔board, 400ms zoom for board↔focused)
- /display route composed end-to-end with all hooks and components
- display-idle-snapshot Edge Function with cron every 60 seconds
- get_display_idle_sessions RPC (service_role only, filters completed sessions)
- /display bypasses config guard in __root.tsx (public, no-auth route)
- docs/self-hosting.md: Remote Display section

Tasks: S001, S001-T, S002, S003, S003-T, S004, S005, S005-T, S006, S006-T, S007, S008, S009, S009-D
Feature: 009-idle-mode

* docs(planning): mark feature 009 idle mode as complete

Feature: 009-idle-mode

* fix(display): address QA findings from idle mode review

Frontend:
- Refactor useDisplayMode to use setState-during-render pattern (React
  "storing info from previous renders") instead of refs-in-useMemo,
  fixing react-hooks/refs ESLint violations and the ref-as-dep stability bug
- Add NaN guard and 60s periodic re-sync to ClockDisplay offset to handle
  NTP corrections and DST transitions on long-running displays
- Wire real Realtime channel status to connectionStatus in display route,
  replacing the hardcoded 'connected' placeholder
- Fix useIdleSnapshot cleanup to safely void removeChannel promise; add
  dev-mode console.warn on safeParse failures for easier debugging
- Fix IdleView session key to use display_name+session_name instead of
  array index
- Remove redundant cleanup() calls in display test afterEach blocks

Backend:
- Add day_of_week IS NOT NULL guard to get_display_idle_sessions RPC to
  prevent silent exclusion of sessions with unset day_of_week
- Add composite index idx_workout_logs_session_user_date to prevent full
  table scan on the cron-driven workout_logs LEFT JOIN
- Replace CREATE OR REPLACE with DROP IF EXISTS + CREATE for atomic
  GRANT application
- Remove non-null assertions on sessions[0] in Edge Function
- Add LIMIT 3 comment and verify_jwt rationale comment in config
- Improve Edge Function error messages to include error codes and response
  status for easier production debugging

Feature: 009-idle-mode

* fix(display): resolve PR review findings from 0006-idle-mode-review

Fixes: clock drift bug (P6-001), null client silence (P6-002),
lying connection status (P6-003), broad path bypass (P6-004),
duplicate channel (P6-005), missing CLOSED handling (P6-006),
production-silent parse errors (P6-007), duplicate DisplayMode type
(P6-008), unsafe RPC cast (P6-009), redundant comments (P6-010)

Tasks: S011-S014 added to Steps.md for missing test coverage
Rules: .min(1) on display-facing strings, Tech.md D4 corrected

Review: Context/Reviews/0006-idle-mode-review.md

* test(display): add review remediation tests S011-S014

S011: useIdleSnapshot hook tests (9 tests) -- valid/invalid payload,
null client, cleanup, connection status callback for all channel states
S012: DisplayModeTransition tests (8 tests) -- transition class mapping
for all mode pairs including duration assertions
S013: IdleSnapshot schema edge cases (7 tests) -- invalid session_type,
empty arrays, invalid ISO dates, .min(1) enforcement
S014: ClockDisplay correction test strengthened to assert exact time value

Review: Context/Reviews/0006-idle-mode-review.md

* docs(reviews): mark 0006-idle-mode-review as verified

All 16 findings confirmed resolved by review-verify agent.

Feature: 009-idle-mode

* fix(display): add onIdleSnapshot to subscriber test mock handlers

Fixes TS build error from merge integration -- the test's mock handlers
were missing the new onIdleSnapshot callback added to DisplayEventHandlers.

* chore(harness): update skills, hooks, and Claude Code settings

* docs: update hosting PRD and add onboarding refactoring plans

* feat(onboarding): QR code gen + scan for server setup (#64)

* docs(planning): add Feature 010 QR code gen + scan blueprint

Spec, Tech, and Steps for Refactor B of the onboarding redesign
(QR code generation in Settings + scan/paste on setup screen).
Covers invite link format, qrcode.react config, barcode scanner
plugin setup, and isTauri()-based platform branching. Updates
onboarding-refactoring-plans.md with researched plugin specifics.

Feature: 010-qr-code-gen-scan

* feat(onboarding): add QR code server sharing and scan on setup

Adds QR-based server sharing to reduce friction for onboarding new
users to a self-hosted Ardent Forge instance.

- Settings backend section now shows a "Share this server" area with
  a QRCodeSVG encoding `ardentforge://connect?url=...&key=...` and a
  "Copy invite link" button, guarded by hasConfig()
- Setup screen QR button activates native camera scan via
  @tauri-apps/plugin-barcode-scanner (Tauri/Android) or toggles an
  inline paste field (browser); both paths parse the invite link and
  auto-trigger connection validation
- New src/lib/invite-link.ts provides buildInviteLink() and
  parseInviteLink() as a pure shared utility
- Barcode scanner plugin registered under #[cfg(mobile)] and
  barcode-scanner:default added to mobile capabilities

Tasks: S001, S002, S003, S004, S005, S006
Feature: 010-qr-code-gen-scan

* fix(qr-scan): address QA findings from code review

- Add .catch() to unawaited processInviteLink() calls in paste/keydown
  handlers to prevent silent unhandled promise rejections
- Replace duplicate dynamic import of cancel with cancelRef to avoid
  redundant module resolution on scan overlay dismiss
- Change perms === 'denied' guard to perms !== 'granted' for defensive
  permission handling on non-standard permission states
- Move tauri-plugin-barcode-scanner to target-specific mobile dep in
  Cargo.toml to avoid enlarging desktop binary
- Remove unreachable pathname fallback branch from parseInviteLink
- Add comment to buildInviteLink documenting publishable key intent
- Rename misleading test and add true malformed-URL test coverage
- Apply rustfmt formatting to db.rs

Feature: 010-qr-code-gen-scan

* fix(qr-scan): resolve P7 review findings and add component tests

Fix bare catch blocks in handleCopy and connection-validator, add
try/catch to processInviteLink to prevent UI getting stuck, handle
unhandled promise from handleScan onClick, add user-facing error
toasts for clipboard failures, fix buildInviteLink to use SCHEME/HOST
constants, downgrade parse error log to silent null return, add
aria-label to paste field, improve QR scan failure toast message.

Add component tests for processInviteLink valid/invalid flows and
backend-settings share section conditional rendering and copy button.

Tasks: S006-T1, S006-T2
Feature: 010-qr-code-gen-scan
Review: Context/Reviews/0007-pr64-qr-code-gen-scan-review.md

* docs(planning): resolve P7 review and add S006-T1/T2 tasks to Steps.md

Mark all 14 findings in the QR code gen/scan review as resolved.
Add S006-T1 (processInviteLink component tests) and S006-T2
(backend-settings share section tests) to Feature 010 Steps.md.

Feature: 010-qr-code-gen-scan

* docs(review): mark P7 review verified by review-verify agent

* refactor(review-resolve): simplify bulk triage to bulk accept

Renames bulk triage to bulk accept to better reflect the workflow --
the capture category already IS the suggested action. Removes the
requirement for a reason on discard (individual and bulk).

* chore(harness): add gitignore entries and reformat hooks

Adds .claude/backups/, .claude/worktrees/, .serena/, and
hooks/skill-activation/.state/ to .gitignore. Reformats all hook
TypeScript files to double-quote style for consistency.

* feat(builders): progressive disclosure and contextual help (Step 14.5)

* docs(planning): add Step 14.5 feature plan and user flow docs

Documents the progressive disclosure and contextual help plan
for the template builder and program builder (Step 14.5).
Adds Flow 7 builder progressive disclosure and contextual help
system diagrams to 10-user-flows.md.

Feature: 010-Progressive-Disclosure
Tasks: S001–S014-T

* feat(builders): progressive disclosure and contextual help (Step 14.5)

Adds context-aware field visibility, staged group creation, set scheme
filtering, and inline help to the template and program builders.

New primitives and utilities:
- HelpTrigger component (Popover desktop / Drawer mobile via useMediaQuery)
- shadcn Popover and Drawer with Iron & Ember styling
- useMediaQuery hook (SSR-safe, listener cleanup)
- CollapsedFieldsRow for expandable hidden field groups
- visibility-maps.ts: data-driven lookup tables for all field visibility rules
- help-content.ts: GROUP_TYPE_HELP, BLOCK_TYPE_HELP, SOURCE_HELP constants
- text-label-large and text-body-small typography utilities in index.css

Template builder changes:
- Category-aware Scoring/Time Cap visibility with CollapsedFieldsRow
- Activity group staged reveal: Stage 1 (type selector only), Stage 2 (type-
  dependent rest fields + exercises)
- Set scheme type list filtered by session category; "Show all types" escape
- Load type options filtered by set scheme type; null = hide entire section
- Session picker gains "Create new event" inline creation flow

Program builder changes:
- Block type HelpTrigger with one-liner summary (desktop + mobile)
- Source badge HelpTrigger with all 7 sources

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013
Feature: 010-Progressive-Disclosure

* fix(builders): resolve QA findings from progressive disclosure review

Fixes two blocking hook violations and seven warnings across the builder
components introduced in Step 14.5.

Errors fixed:
- Move useCallback above early return in LoadSpecEditor to satisfy
  Rules of Hooks (conditional hook call)
- Rewrite useMediaQuery with useSyncExternalStore to eliminate
  synchronous setState in effect and double render

Warnings fixed:
- Remove eslint-disable suppression in LoadSpecEditor; expand useEffect
  deps to [allowedLoads, availableTypes, value.type, onChange]
- Replace div[role=button] nested inside <button> in MobileDayRow with
  sibling <button> elements; fix event type to MouseEvent | KeyboardEvent
- Add useId/aria-controls pairing to CollapsedFieldsRow for screen
  reader disclosure association
- Dynamic aria-label on HelpTrigger (Help: ${title} instead of static)
- Extract BLOCK_TYPE_STYLES to constants.ts; remove duplication across
  block-editor and mobile-block-editor
- Remove unused placement prop from HelpTrigger and all four call sites
- Fix PopoverTitle to render <h2> matching its ComponentProps<'h2'> type

Suggestions addressed:
- Sync ladderText in DescendingRepsFields when repLadder resets externally
- Use touchLastAssigned.mutate (stable ref) in useCallback deps
- Add clientId to ActivityGroupData and ActivityData; generate via
  crypto.randomUUID() at creation; use as React key instead of index
- Add aria-expanded/aria-controls to ProgramForm details toggle

Feature: 010-Progressive-Disclosure

* fix(builders): resolve PR review findings P7

Fixes: P7-001 (silent userId guard), P7-002 (JSX triplication),
P7-003 (redundant satisfies), P7-004 (unstable useEffect deps),
P7-005 (block type help duplication), P7-007 (missing JSDoc),
P7-008 (HelpEntry interface), P7-009 (non-empty tuple),
P7-010 (scheme map typing), P7-011 (constants typing)
Rules: P7-013, P7-014 added to typescript-conventions.md;
P7-015 added to error-handling.md
Deferred: P7-012 (ErrorBoundary strategy) to backlog
Discarded: P7-006 (superseded by P7-002 extraction)

Review: Context/Reviews/0007-pr65-progressive-disclosure-review.md

* fix(builders): resolve review-verify type errors and mark review passed

Fix 3 pre-existing TS errors from P7 review fixes: collapsedLabels
cast to non-empty tuple, allowedLoads.includes widened for
LoadSpec['type'], defaultTypes.includes widened for SetSchemeType.
Review file verified and checkbox marked.

* feat: implement Android CI/CD pipeline (Feature 011) (#67)

* docs(ci-cd): add full blueprint for Android CI/CD pipeline

4-phase planning for feature 011: GitHub Actions workflows covering PR
validation (lint, typecheck, unit tests, E2E via Playwright + local
Supabase, Android debug APK build), tagged release pipeline (signed AAB,
Supabase migrations gate, GitHub Release, Play Store internal track
upload), and supporting setup documentation.

Feature: 011-ci-cd-pipeline
ADR-007: GitHub Actions chosen as CI/CD platform

* ci(github-actions): implement complete Android CI/CD pipeline

Adds two GitHub Actions workflows from scratch:

- ci.yml: PR validation with three parallel jobs -- lint/typecheck/unit
  tests, Playwright E2E against local Supabase, and Android debug APK
  build with full Rust/NDK toolchain and caching
- release.yml: tag-triggered release pipeline -- signed AAB build and
  Supabase migrations run in parallel, gated publish job creates GitHub
  Release and uploads to Play Store internal track

Supporting changes:
- Add Playwright (@playwright/test) with smoke tests and test:e2e script
- Add signingConfigs block to build.gradle.kts reading from env vars
- Add .github/BRANCH_PROTECTION.md and .github/README.md
- Add CI/Release workflow badges to README.md

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009
Feature: 011-ci-cd-pipeline

* fix(android-ci): harden pipeline per PR #67 review findings

- Guard signingConfig assignment with keystorePath null check
- Validate KEYSTORE_BASE64 before decode; check decoded file size
- Pass all secrets through env: blocks (not inline shell expansion)
- Add secret validation steps to build-release, migrate, publish jobs
- Reorder publish job: Play Store upload before GitHub Release
- Capture Supabase status once; validate non-empty/non-null values
- Add if-no-files-found: ignore to Playwright report upload
- Fix test:e2e script to use bunx playwright test
- Add webServer timeout: 30_000, stdout/stderr: pipe to playwright.config.ts

Tasks: P8-001, P8-002, P8-003, P8-004, P8-005, P8-006, P8-007, P8-008, P8-015, P8-016

* docs(ci-cd): apply post-review documentation corrections

- Tech.md: Four->Two workflows, remove armv7 target, fix port 4173,
  update signing snippet to null-guard pattern
- Steps.md: add VITE_ prefix to env var names; add S010 secret validation
- ADR-007: status Proposed->Accepted
- Add PR #67 review capture (16/16 findings resolved and verified)
- Update review-resolve skill

Tasks: P8-009, P8-010, P8-011, P8-012, P8-013, P8-014, P8-015

* feat(deep-link): handle ardentforge://connect deep links (#66)

* docs(planning): add Feature 011 deep link handler planning artifacts

Full 4-phase plan for ardentforge://connect deep link handling.
Covers Tauri dispatch extension, browser /connect route, setup
screen pre-population, and transient store for settings handoff.

Feature: 011-deep-link-handler
ADR-007: transient-store-deep-link-state

* feat(deep-link): handle ardentforge://connect deep links

Implements the connect deep link handler so tapping an invite link
opens the app directly to the correct screen with credentials
pre-populated.

- Register ardentforge://connect host in Tauri deep-link config
- Add handleConnectLink() with 4-way branch: invalid, unconfigured,
  same instance, different instance
- Dispatch connect vs auth in onOpenUrl listener (OAuth flow untouched)
- Add /connect browser route as fallback for web users
- Exempt /connect from root route guard
- Setup screen accepts url/key search params for auto-population
- BackendSettings consumes pending connect Zustand store on mount
- Unit tests for all 5 handleConnectLink() branches (5/5 passing)

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010
Feature: 011-deep-link-handler
ADR: ADR-007 (transient Zustand store for cross-route deep link state)

* fix(deep-link): address QA findings from review

- Add desktop deep-link scheme config + permissions to default capability
- Fix bare catch blocks in invite-link.ts and auth.tsx (error-handling convention)
- Refactor setState-in-effect in backend-settings.tsx to useRef guard
- Add HTTPS validation for url param in parseInviteLink
- Accept navigate callback in handleConnectLink (avoids full-page reload in router context)
- Update OAuth tests to mock @tauri-apps/plugin-opener instead of window.open
- Add test for getConfig() returning null with hasConfig true
- Add test for non-HTTPS url param rejection
- Fix setup.test.tsx mock to provide Route.useSearch()

Feature: 011-deep-link-handler

* fix(deep-link): address PR-66 review findings

- Wrap handleConnectLink in try-catch with toast on error (P8-001)
- Isolate connect-link error path from OAuth error handler in
  auth.tsx (P8-002)
- Replace render-phase setState with useRef + useState initializers
  in backend-settings.tsx (P8-003)
- Replace unsafe `as string` casts with typeof guards in
  validateSearch for /connect and /setup routes (P8-004)
- Add toast feedback when auto-validate fails in /setup (P8-005)
- Replace startsWith check with new URL() + protocol/hostname
  validation in parseInviteLink (P8-006)
- Fix bare catch blocks in config-store.ts to capture and log
  errors (P8-007)
- Add store-boundary validation to usePendingConnect.setPending
  (P8-009)
- Add state-management.md coding rule for Zustand conventions
- Add review capture with verification (0008-pr66)
- Add test step S011 to Steps.md (P8-008)

* test(deep-link): add /connect route and auth connect-dispatch tests

- Add connect.test.tsx covering search param validation, pending
  store integration, and redirect behavior
- Update auth-oauth tests for connect deep-link error isolation

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* fix(ci): upgrade supabase/setup-cli from v1 to v2

v1 runs on deprecated Node 20 and installs an older CLI that rejects
newer config keys (oauth_server, web3, cron, etc.), breaking the
migrate job on tagged releases.

* fix(ci): pin supabase CLI to v2.84.4 to match local version

setup-cli@v1 defaults to CLI v2.20.3 which rejects newer config keys.
Pinning to 2.84.4 matches local dev environment. setup-cli v2 is
merged but not yet tagged as a release.

* fix(supabase): remove invalid cron key from config.toml

The [functions.display-idle-snapshot.cron] section is not a valid
config.toml option. Edge function cron scheduling is configured via
pg_cron in SQL, not in config.toml. This was causing supabase link
to fail in CI.

* fix(sync): add HTTP status check and empty update_set guard in pull path (#70)

* fix(sync): add HTTP status check and empty update_set guard in pull path

pull_table deserialized Supabase responses without checking the HTTP
status code, which could silently swallow 4xx/5xx errors or produce
confusing deserialization failures. Added a status check matching the
pattern already used in push.rs.

upsert_row produced invalid SQL ("... ON CONFLICT(id) DO UPDATE SET ")
when the only matching column was `id`. Now falls back to INSERT OR
IGNORE for that edge case.

Closes #68, closes #69

* fix(lint): resolve unused var, explicit any, and unused import errors

- Prefix unused catch parameter with underscore in formatter hook
- Add eslint-disable for necessary `any` cast in test helper
- Remove unused `buildRequest` import from webhook test

* fix(lint): use bare catch block for unused error parameter

* fix(data-mapper): handle JSONB columns already parsed by Supabase PostgREST

Supabase returns JSONB columns as objects, not strings. Calling JSON.parse()
on an object stringifies it to "[object Object]" first, causing a parse error
on session_templates fields (rest_between_groups, time_cap, event_metadata),
activity_groups fields (rest_between_rounds, rest_between_activities), and
activities (set_scheme).

Adds parseJsonOrValue() helper that passes objects through directly and only
calls JSON.parse() on string values, keeping backward compatibility with the
Tauri/SQLite adapter which returns these columns as JSON strings.

* chore: batch enhancement issues cleanup (#71)

* chore: add Zod refinements to chat domain types

Add validation refinements that catch invalid data at parse time:
- conversation.ts: prevent groupId on direct conversations, enforce leftAt > joinedAt
- message.ts: prevent senderId on system messages
- media.ts: add .nonnegative() to durationSeconds and fileSizeBytes

Closes #40, closes #47

* chore: polish realtime manager (simplify clearAllTyping, fix silent catch)

- Simplify clearAllTyping to delegate to clearTypingForConversation helper
- Add error logging to broadcastTyping .catch per error-handling conventions

Closes #42, closes #49

* feat(events): wire EventCountdownBadge navigation to dedicated event route

- Create /events/$templateId route with minimal event detail page
- Update EventCountdownBadge to navigate to new route instead of /library
- Remove _templateId unused-var rename and TODO comment

Closes #31

* perf: batch getUnreadCounts queries with Promise.allSettled

Replace sequential N+1 COUNT queries with concurrent execution via
Promise.allSettled, preserving partial-results error handling.

Closes #39, closes #46

* perf: optimize chat query patterns (CH-2 trigger, findDirectConversation)

- Add error context with [tauri-adapter] prefix to findDirectConversation loop
- Create materialized direct_conversation_pairs lookup table with unique index
- Replace O(N) trigger scan with O(1) index-backed uniqueness check
- Add cleanup trigger for participant departures

Closes #41, closes #48

* docs: add quick plan for enhancement issues batch

* fix(review): resolve PR #71 review findings

Fixes (7): RLS on direct_conversation_pairs, ISO datetime comparison,
findDirectConversation all-fail throw, getUnreadCounts error enrichment,
comment accuracy (N+1, migration …
* fix: add Authorization header to Supabase reachability check

The PostgREST root endpoint requires both `apikey` and `Authorization`
headers. Missing the latter caused a 401 during setup, showing
"Cannot reach server" even with valid credentials.

* fix: auto-validate and surface env var connection failures on setup page

When VITE_SUPABASE_URL and VITE_SUPABASE_PUB_KEY are set but invalid,
the setup page now auto-validates on mount and shows the specific error
(e.g. "Server responded with 401") instead of silently presenting a
blank form. Also adds a banner explaining that environment variables
were detected but failed.

* fix: use Supabase client for connection validation instead of raw fetch

Raw fetch with apikey/Authorization headers only works with legacy anon
keys. New publishable keys (sb_publishable_xxx) require the Supabase
client library which handles auth headers internally. This switch
supports both key formats transparently.

* fix: resolve duplicate migration timestamp collision

Two migrations shared timestamp 20260329000001, causing a primary key
violation in supabase_migrations. Renumbered create_sharing_tables to
20260329000002 and expand_rls_for_sharing to 20260329000003.

* chore: update Claude Code configuration and skills

* feat: complete sync pull path with dynamic upsert and force-pull (Step 9) (#28)

Implement the stubbed pull/upsert path so remote changes from Supabase
are actually written to local SQLite, completing bidirectional sync.

- Fix broken timestamp parsing in upsert_row (ISO 8601 strings were
  silently parsed as 0, causing all remote updates to be discarded)
- Implement dynamic column upsert via pragma_table_info with type
  coercion (timestamps, booleans, JSONB) between Supabase and SQLite
- Add pull_table/pull_all for paginated force-pull via Supabase REST API
- Wire sync_force_pull command with proper state transitions
- Add auto-pull on first sign-in (detect last_pull_at == 0)
- Add 28 new tests (49 total), all passing

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: events & packing lists (Step 13.5) (#30)

* feat(events): add events & packing lists feature (Step 13.5)

Introduces EVENT as a new session category with full support for event
metadata (date, location, URL, requirements) and a categorized packing
checklist with drag-and-drop reorder, optimistic toggle, progress
tracking, and countdown notifications.

Database:
- Migration adds EVENT to category CHECK constraints on session_templates
  and scheduled_sessions; adds event_metadata JSONB to session_templates
  and workout_logs; creates event_items table with polymorphic FK
  (template XOR log), RLS policies, partial indices, and updated_at trigger

Domain:
- eventRequirementSchema, eventMetadataSchema, eventItemSchema (Zod)
- EVENT added to SessionType enum; eventMetadata optional on templates
  and logs; EventItemRow added to database.types.ts

Data layer:
- toEventItem/fromEventItem mappers; eventMetadata parsing in
  toSessionTemplate (JSON.parse) and toWorkoutLog (direct JSONB)
- DataAdapter interface: getEventItems, saveEventItem, updateEventItem,
  deleteEventItem, toggleEventItemPacked (accepts target bool, no race),
  reorderEventItems (Promise.all), cloneSessionTemplate (isPacked reset)
- SupabaseAdapter implements all methods; getSessionTemplateFull fetches
  eventItems for EVENT templates; updateSessionTemplateFull uses 3-way
  reconciliation to preserve item UUIDs and isPacked state
- TauriAdapter stubs (event support deferred, W-8)

Hooks:
- useEventItems, useCreateEventItem, useUpdateEventItem,
  useDeleteEventItem, useToggleEventItemPacked (optimistic),
  useReorderEventItems (optimistic), useNextUpcomingEvent

UI:
- event-builder/: EventTemplateForm (Sheet overlay, create/edit),
  EventDetail (metadata + map link + packing), PackingList (DnD,
  collapsible categories, error feedback), PackingItem (checkbox toggle),
  EventProgressBar (ember on surface-steel, ARIA progressbar),
  EventCard (library), EventCountdownBadge (Today screen, TODAY edge
  case), RequirementEditor, EventItemEditor, shared styles.ts
- Library: EventCard for EVENT templates, NEW EVENT button
- log/$workoutId: interactive EventDetail for event logs
- history/$workoutId: read-only EventDetail for event history
- Today screen: countdown badge when next event within 30 days
- Program builder: flag icon + ember accent for EVENT sessions,
  event date displayed in program-preview grid and detail sections

Native (Rust):
- event_reminders channel registered in notification.rs
- EventReminderState: background Tokio task (60s poll), checks
  configured intervals (7d/3d/1d), quiet hours, packing progress in
  notification body, deterministic notification ID
- schedule_event_reminders / cancel_event_reminders Tauri commands
- Clippy fixes: RemindedState type alias, needless_borrow in lib.rs,
  manual_range_contains in session_reminder.rs
- Lock contention fix: single mutex guard across event loop in tick()

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 002-events-packing-lists

* docs(planning): mark 002-events-packing-lists complete

Updates Steps.md status to Complete, session.md with full wave
execution log and fixes applied, and implementation-plan.md.

Feature: 002-events-packing-lists

* chore(rust): apply cargo fmt across codebase

* chore: add mise config and cortex hooks

* fix(events): address PR #30 review findings

- Fix timezone bug in combineDateTime: store local date strings instead
  of converting to UTC, preventing event dates from shifting by a day
- Add eventUrl validation in form validate() before save
- Switch sequential item persistence to Promise.allSettled with error
  collection for partial-failure reporting
- Fix DST-unsafe daysUntil calculation using date-only comparison
- Make TauriAdapter getEventItems throw consistently with write stubs
- Add "TODAY" special case in Rust event reminder notification title
- Eliminate double hydration of requirements/items in form init
- Add EventItem mapper roundtrip tests and splitDateTime/combineDateTime
  edge case tests (19 new tests)

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(groups): coach program assignment

Adds the ability for a group coach to assign one of their programs to a
member. The program is atomically transferred to the member's ownership
with source tagged as COACH_ASSIGNED and the coach recorded as created_by,
enabling the member to filter out programs they received vs created.

Includes the two-step AssignProgramDialog UI and integrates it into
MemberCard. Both the Tauri SQLite command and the Supabase SECURITY
DEFINER RPC perform the same three authorization checks (caller is COACH,
target is MEMBER, program owned by caller) before executing the transfer.

* fix(sync): resolve incremental push breakage and missing error state

- push.rs: timestamp_millis() -> timestamp() so last_push_at is stored
  in seconds, matching the updated_at column it filters against; the
  millis value was always larger than any updated_at, causing zero rows
  to be found on every push after the first
- sync.rs: sync_force_push now transitions to SyncState::Error on
  failure, matching the sync_force_pull pattern; previously a failed
  push left the engine in Idle, hiding the error from the frontend

* docs(planning): add spec, tech, and steps for coach program assignment

* feat(chat): add chat data layer + QA fixes (#33)

* feat(chat): add chat data layer (Step 21)

Builds the full persistence layer for the chat domain across Supabase,
SQLite, Rust, and TypeScript. No UI -- pure data plumbing for Steps 22-25.

- Supabase migration: conversations, conversation_participants, messages,
  media_attachments with RLS (CH-1), append-only messages (CH-7), and
  direct conversation uniqueness via trigger (CH-2)
- SQLite migration: mirrors Postgres with sync_status on messages for
  offline queueing (pending -> synced/failed state machine)
- Rust: ConversationRow/MessageRow/etc structs, 10 Tauri commands (CRUD
  for conversations, messages, participants, media), chat tables in
  SYNCABLE_TABLES for push/pull sync
- TypeScript: Zod schemas (Conversation, Message, MediaAttachment,
  WorkoutSnapshot), DB row types, bidirectional mappers, 13-method
  ChatAdapter facet on DataAdapter interface, Supabase and Tauri
  adapter implementations, mock adapter updated

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011
Feature: 004-Chat-Data-Layer

* docs(planning): add spec and steps for chat data layer (Step 21)

Feature 004 planning artifacts: Spec.md covering requirements,
testable assertions, and invariants (CH-1, CH-2, CH-4, CH-7);
Steps.md with 4-wave execution plan for db-engineer, rust-engineer,
domain-engineer, and quality-engineer.

Feature: 004-Chat-Data-Layer

* fix(qa): address all QA feedback -- sync resilience, lint, and stale tests

Rust sync hardening:
- sync/mod.rs: replace `return` with `continue` so queue flush failure no longer kills the sync loop permanently; transition to Error state when initial pull fails rather than silently proceeding to push
- sync/pull.rs: add SYNCABLE_TABLES allowlist guard in upsert_row and delete_row
- sync/push.rs: add SYNCABLE_TABLES allowlist guard in push_table
- sync/queue.rs: normalize enqueue timestamp to seconds (was millis, inconsistent with push/pull)

Frontend lint:
- packing-list.tsx: replace setState-in-useEffect anti-pattern with useMemo derivation for mutation error feedback
- event-template-form.tsx: extract splitDateTime/combineDateTime to date-time-utils.ts to fix Fast Refresh violation (non-component exports alongside component)

Tests (31 stale failures fixed):
- Update 5 test files with correct mixed-case text after CSS uppercase refactor
- Rewrite mobile-nav.test.tsx: add useAuth mock, update to 5+1 nav structure, correct icons and routes

* fix(review): resolve PR #33 review findings for chat data layer

Critical fixes (C-1 through C-5):
- Add missing INSERT policy on conversations table (C-1)
- Filter user's own messages from getUnreadCounts in Supabase adapter (C-2)
- Change z.url() to z.string().url() for media attachment URLs (C-3)
- Return Err instead of Ok(()) from SYNCABLE_TABLES allowlist guards (C-4)
- Add missing UPDATE policy on media_attachments table (C-5)

Important fixes (I-2, I-3):
- Add participant check before message insert in send_message (I-2)
- Validate direct conversations require exactly 2 participants (I-3)

Tasks created (S013-S017) for: data mapper tests, dedicated
get_messages_since command, updatedAt mapping fix, unread counts
resilience, and sync guard test coverage.

Suggestions S-1 through S-6 tracked in Ideas backlog.

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

* feat(chat): implement review remediation tasks S013-S017

S013: Add 36 data mapper unit tests for chat entities
  - Conversation, ConversationParticipant, Message, MediaAttachment
  - Round-trip, null handling, and Zod validation tests

S014: Add dedicated get_messages_since Rust command
  - SQL-level WHERE created_at > ? filter replaces client-side filtering
  - Eliminates silent truncation at 1000 message limit

S015: Fix toConversationParticipant updatedAt mapping
  - Use last_read_at ?? joined_at instead of always joined_at

S016: Improve getUnreadCounts error resilience
  - Single conversation failure no longer aborts all counts
  - Returns partial results with console.warn for failed queries

S017: Remove #[cfg(not(test))] from sync allowlist guards
  - Tests now exercise the same allowlist path as production
  - Updated test fixtures to use real SYNCABLE_TABLES entries

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: Supabase Realtime, chat data layer, events & coach assignment (#34)

* docs(planning): add spec, tech, and steps for supabase realtime integration

4-phase blueprint for Step 22 (Supabase Realtime Integration).
Establishes architecture for Broadcast channel management, cursor-based
message pagination, RealtimeManager module, and all chat TanStack Query hooks.

ADR-005: cursor-based getMessages replaces offset-based (no existing callers).

* feat(chat): add Supabase Realtime broadcast channels and chat hooks

Wire up Supabase Realtime Broadcast for live message delivery and typing
indicators. Introduces the RealtimeManager singleton, cursor-based message
pagination, platform-agnostic foreground detection, and all TanStack Query
hooks needed by the Chat UI (Step 23).

New files:
- src/lib/realtime-schemas.ts     -- Zod schemas for broadcast payloads
- src/lib/realtime-manager.ts     -- channel lifecycle, typing state, catch-up
- src/lib/foreground-detector.ts  -- visibilitychange / Tauri focus detection
- src/hooks/use-chat.ts           -- useConversations, useMessages (infinite),
                                     useSendMessage (optimistic), useRealtimeMessages, +6 more
- src/components/chat-realtime-listener.tsx -- app-level cache update listener

Modified:
- data-adapter.ts / supabase-adapter.ts / tauri-adapter.ts / chat.rs
  Replace offset-based getMessages with cursor-based (before?: ISO timestamp)
- main.tsx: mount ChatRealtimeListener alongside SyncListener
- auth.tsx: call resetRealtimeManager() on SIGNED_OUT

Tasks: S001, S002, S003, S004, S005, S006, S007

* fix: resolve QA lint and code quality issues across frontend and Rust

Fix clippy errors in test code (manual_range_contains, approx_constant),
ESLint no-constant-binary-expression in utils test, and three code quality
issues in realtime-manager: validate typing payloads with Zod schema,
replace fragile onTyping callback chain with Set-based listener pattern,
and explicitly discard handleForeground Promise.

* fix(chat): address PR #34 review findings for Supabase Realtime

Multi-listener pattern (addMessageListener/addTypingListener) replacing
single-callback ownership, broadcast try/catch in useSendMessage,
channel lifecycle ownership fix, auth integration via useAuth hook,
and Wave 5 polish: named constants, toMessageFromBroadcast mapper,
simplified clearAllTyping, foreground-detector error logging and
async/await rewrite, broadcastTyping .catch(), MessagePaginationOptions
interface, user_name max(200) constraint.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(chat): complete chat UI with messaging and conversations (#35)

* docs(planning): add spec, tech, and steps for chat UI (Step 23)

* feat(chat): build complete chat UI (Step 23)

Routes, conversation list, virtualized message detail, compose bar,
typing indicators, new conversation flow, group management, and
client-side blocking. Navigation updated -- COMMS replaces Social
on mobile, added after Vault on desktop sidebar with unread badge.

Conversation type extended with participantUserIds so ConversationDetail
can resolve the other user's display name and blocking state for direct
conversations. Supabase adapter fetches participants alongside
conversations; Tauri adapter was already receiving them and now uses them.

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 006-Chat-UI

* docs(planning): add quick plan for conversation participant IDs

Feature: 006-Chat-UI

* fix: update mobile-nav tests for Comms tab and fix Rust formatting

Update test assertions for Social→Comms rename, add useUnreadCounts
mock, and fix rustfmt formatting in event_reminder tests.

* fix(chat): resolve PR #35 review findings

Critical fixes:
- C1: Contact search filter was a no-op; now passes search prop to
  ContactRow which renders null when display name does not match
- C2: Sender name in group bubbles showed raw UUID; resolved via
  useUserProfile inside MessageBubble
- C3: Send failure gave zero feedback; added sendError state and
  inline "Failed to send" indicator on ComposeBar
- C4: Conversation creation failure was silent; added error state
  and inline message in NewConversationSheet
- C5: MessageList had no error/loading state; added spinner and
  error fallback from useMessages isLoading/isError

Important fixes:
- I1: Typing broadcast used user.email; now uses displayName via
  useUserProfile
- I2/I3: useBlockedUsers storage key scoped to userId; added
  console.error/warn on localStorage corruption
- I4: Optimistic message had senderId undefined; now uses current
  user ID from useAuth
- I5: LeaveDialog failure was silent; added inline error message
- I6: ParticipantSheet add failure was silent; shows error including
  Tauri offline message
- I7: Removed rounded-full from avatar containers and unread dots
- I8: Added comment clarifying group threshold (2 selected + server
  adds self = 3 total participants)

Suggestions:
- S3: Removed build-artifact comment from conversation-list.tsx
- S9: Fixed error copy from "pull to retry" to "try again"

Tasks: C1, C2, C3, C4, C5, I1, I2, I3, I4, I5, I6, I7, I8, S3, S9
Feature: 006-Chat-UI

* docs(planning): resolve PR #35 review and add test/polish tasks

Marks 0003-pr35-chat-ui-review.md as fully resolved (5 critical,
8 important, 2 suggestions fixed inline; 6 tasks created; 5
suggestions dismissed with rationale).

Adds Wave 7 tasks to Steps.md:
- S015-T: deriveItems unit tests
- S016-T: useBlockedUsers unit tests
- S017-T: formatTypingText unit tests
- S018-T: relativeTime + getInitials unit tests
- S019: Migrate useBlockedUsers to Zustand store
- S020: Wire archive action in conversation header

Feature: 006-Chat-UI

* refactor(chat): extract utils, add blocked-users store, verify review

Extract pure functions (relativeTime, getInitials, formatTypingText,
deriveItems) into chat-utils.ts and add tests. Migrate useBlockedUsers
to Zustand store with user-scoped persistence. Mark PR #35 review as
verified with follow-up tracked in GitHub issues #43-#51.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: add link preview metadata with OG tags, Twitter Cards, and useMetadata hook (#53)

Add comprehensive link preview support so shared URLs display rich previews
on social platforms and messaging apps. Includes a useMetadata hook for
dynamic per-route title and description updates, used on share pages.

* fix(auth): prevent PKCE code race condition on OAuth callback (#54)

Supabase JS v2.100.1 defaults to detectSessionInUrl: true with PKCE
flow, causing _initialize() to auto-exchange the auth code before the
/auth/callback route handler can. The second exchange attempt fails
(code already consumed), redirecting users to /sign-in?reason=oauth_error.

Disable detectSessionInUrl since we have a dedicated callback route
that handles PKCE code exchange manually.

* fix(auth): enable PKCE flow for OAuth sign-in (#55)

Without explicit flowType: 'pkce', the Supabase client uses implicit
flow, returning tokens as hash fragments (#access_token=...) that the
/auth/callback route cannot read (it expects ?code= query params).

Setting flowType: 'pkce' makes Supabase return a PKCE authorization
code as a query parameter that the callback can exchange for a session.

* fix(db): resolve RLS infinite recursion on group_members and conversation_participants (#56)

The group_members and conversation_participants SELECT policies
self-referenced their own tables, triggering PostgreSQL error 42P17
(infinite recursion). This cascaded 500s to every table whose sharing
policies join group_members (workout_logs, session_templates, etc.).

Creates SECURITY DEFINER helpers (is_group_member, is_group_coach,
is_conversation_participant) that bypass RLS for inner membership
lookups and replaces all six affected policies.

Also removes the PWA manifest (manifest.webmanifest) and its link tag
since Vercel deployment protection returns 401 on the unauthenticated
manifest fetch.

* feat(builder): program builder UI with responsive week grid (#58)

* feat(builder): program builder UI with responsive week grid

Implements the complete program builder and session builder UIs.

Program builder:
- Sidebar layout (desktop) / stacked layout (mobile) at /builder route
- Drag-and-drop block reordering with @dnd-kit
- Week grid defaults to 5-day Mon-Fri view with a toggle to show all 7
  days, improving readability at constrained content widths (~94px/cell
  at 1024px narrowed to nothing legible; 5-day gives ~132px/cell min)
- Session slots reduced from 80px to 56px min-height with type-colored
  left borders (STRENGTH/CONDITIONING/SE/MIXED/EVENT)
- Mobile block editor with vertical day-row tap targets
- Session picker bottom sheet and copy-week dialog
- Program preview overlay with per-session exercise/load tables

Session builder:
- Set scheme editor with per-type form variants
- Activity group editor and session template form

Infrastructure:
- Collapsible shadcn/ui component added
- Removed boilerplate App.tsx/App.css
- Expanded index.css with builder-specific design tokens

* fix(builder): declutter block header with overflow menu

Replace inline action buttons (move up/down, delete) with a dropdown
overflow menu in both desktop and mobile block editors. This prevents
touch target overlap on narrow viewports and isolates the destructive
delete action behind a two-tap interaction.

* feat(builder): guard destructive actions with confirmation and undo

Block and week deletions now require confirmation via AlertDialog,
showing impact ("This will remove N weeks and M sessions.") before
proceeding. Session removal fires immediately but shows a 5-second
undo toast via sonner, letting users recover accidental taps without
blocking the fast path.

- Add alert-dialog.tsx shadcn primitive (Radix AlertDialog)
- Add confirm-delete-dialog.tsx reusable builder component
- Wire block delete in BlockEditor and MobileBlockCard to dialog
- Wire week delete in WeekGrid and MobileWeekSection to dialog
- Add undo toast to SessionSlot and MobileDayRow session removal
- Mount Toaster in main.tsx (bottom-center)

* feat(builder): replace full-screen preview with inline week detail

Remove the disconnected ProgramPreview overlay from the builder flow.
Exercise details now toggle inline per-week via a visibility icon,
keeping the edit-preview feedback loop tight. Save button moves to
the header. Shared formatting helpers extracted to session-detail.tsx.
Also fixes Collapsible asChild bug in block-editor and mobile-block-editor.

* fix(builder): remove interstitial from session template create flow

Tapping "Create new template" in the session picker now opens the
inline form immediately, skipping the Library recommendation callout.
The Library option is preserved as a subtle link ("Want more options?
Create in the Library") below the form for users who need it.

Removes showCreateCallout state and the two-step reveal pattern.

* refactor(builder): consolidate session constants and polish UI details

- Extract SESSION_TINT and SESSION_TYPE_BADGE from session-slot,
  mobile-block-editor, and program-preview into constants.ts
- Remove duplicate @keyframes block-enter and slot-fill in index.css
- Bump section-level labels (WEEK N, day columns, EXERCISE/SETS/LOAD)
  from text-[11px] font-medium to text-xs font-semibold for visual
  hierarchy over metadata badges which stay at text-[11px] font-medium
- Upgrade session picker SheetTitle to text-sm font-display
- Add empty state copy when zero blocks exist in BlockList and
  MobileBlockEditor
- Clarify copy-week dialog: description now states sessions will be
  replaced, and per-week "(N existing)" label gains warning colour

* feat(builder): replace collapsed save error with structured inline validation

Validation errors now appear at the relevant field rather than as a single
joined string below the save button. Program name errors render under the
name input, block name and week errors render on each block card (visible
even when collapsed), and server/auth errors remain global in the header.

* feat(builder): add recently-used templates and reduce week grid noise

Surface the 5 most recently assigned session templates at the top of the
picker sheet so users with large template libraries reach common picks
faster. Adds lastAssignedAt tracking across both Tauri/SQLite and
Supabase backends.

Reduce visual noise in the week grid: restyle empty slots as muted
"Rest" labels instead of prominent calls-to-action, bring the 5/7-day
toggle to mobile, and collapse identical weeks within a block to a
compact summary row (with manual expand).

* refactor(builder): extract session-detail helpers to fix Fast Refresh lint errors

* feat: display broadcast infrastructure (Step 27) (#59)

* feat(chat): video, image, and file sharing (#57)

feat(chat): video, image, and file sharing (Feature 007)

Full-stack media sharing: Cloudflare Stream video uploads with TUS, image/file uploads via Supabase Storage, real-time transcoding status via webhook, signed URL caching, and inline media rendering in chat.

Review: 4 critical + 10 important findings resolved and verified. 12 enhancements implemented. 1,305 tests passing.

* docs: add plan for Step 27 -- display broadcast infrastructure

Spec.md, Tech.md, and Steps.md for Feature 008. Covers DisplaySnapshot
Zod schema, publisher module, store integration, display_visible toggle,
and Supabase Realtime Broadcast channel plumbing. 14 tasks across 4 waves
with 3 agents.

* feat: add display_visible column to user profiles

SQLite and Supabase migrations for display broadcast toggle,
Tauri model and command updates to support the new field.

* feat: add DisplaySnapshot domain type and display publisher

DisplaySnapshot type with Zod schema, display-publisher for
broadcasting workout state, data-mapper and store updates.

* feat: add PushToDisplayButton and display broadcast hook

useDisplayBroadcast hook, PushToDisplayButton component,
workout log and profile route integrations.

* test: add display snapshot and publisher tests

Unit tests for DisplaySnapshot schema, display-publisher,
display-snapshot builder, and updated data-mapper/adapter tests.

* docs: update Steps.md and implementation plan for display broadcast

* fix(review): resolve PR #59 review findings

Fixes: C1 (RLS auth), C2 (image signed URLs), C3 (snapshot try-catch),
C4 (Cloudflare subdomain), I1-I9 (error handling, schema tightening,
z.url() revert, publisher readiness), M1/M3/M5/M6 (env guards,
request parsing, query error logging, JSDoc)
Tasks: S024-T, S025-T added for media-provider and timeout tests
Comments: 9 comment issues resolved

Review: Context/Reviews/0005-pr59-display-broadcast-review.md

* test(media): add unit tests for CloudflareStreamProvider and transcoding timeout

S024-T: media-provider tests covering signed URL TTL cache hit/miss,
null client error, Zod validation of malformed responses, and cache
deduplication (13 tests).

S025-T: use-media-upload tests covering 5-minute transcoding timeout
marks processing attachment as failed, unaffected ready attachments,
query cache invalidation, and timer cleanup on unmount (4 tests).

Tasks: S024-T, S025-T
Feature: 007-video-image-sharing

* docs(review): mark PR #59 review as verified

* feat(edge-functions): deploy CF video edge functions with Vault secrets and local JWT signing

Deploys three Supabase Edge Functions for Cloudflare Stream video support
(Step 25). All Cloudflare credentials read from Supabase Vault using
CF_* secret names (CF_ACCOUNT_ID, CF_STREAM_TOKEN, CF_WEBHOOK_SECRET,
CF_SIGNING_KEY_PEM, CF_SIGNING_KEY_ID, CF_CUSTOMER_SUBDOMAIN).

chat-media-signed-url: replaces Cloudflare token API round-trip with
local RS256 JWT signing via CF_SIGNING_KEY_PEM + CF_SIGNING_KEY_ID.
No outbound HTTP call needed per playback request.
chat-media-upload-url: reads CF_ACCOUNT_ID + CF_STREAM_TOKEN from Vault.
chat-media-webhook: reads CF_WEBHOOK_SECRET + CF_ACCOUNT_ID from Vault.

Adds get_secret() SQL migration granting service_role-only read access
to vault.decrypted_secrets. Fixes duplicate migration timestamp
20260403000002 -> 20260403000003 for session_template_last_assigned.

Updates mock-supabase test util to support keyed RPC results
(get_secret:SECRET_NAME) for parameter-specific vault mocking.
Removes CLOUDFLARE_ACCOUNT_ID from STANDARD_ENV test fixture.

* chore: update app icons and branding assets

Replace default Tauri icons with Ardent Forge branding across all
platforms (Android, iOS, Windows, macOS). Update app display name
in Android strings.xml and tauri.conf.json.

* fix(db): rename migration 008 and auto-recreate DB on checksum mismatch

Rename session_template_last_assigned to migration 009 to make room
for 008_add_display_visible. Add debug-only auto-recreate logic so
migration checksum mismatches during development delete and rebuild
the local database instead of crashing.

* fix(auth): use Chrome Custom Tabs for Google OAuth on Android

Replace window.open with tauri-plugin-opener's inAppBrowser mode to
launch Google OAuth in Chrome Custom Tabs instead of an embedded
WebView. Google blocks OAuth in WebViews under their "Use secure
browsers" policy. Add opener plugin registration and URL scope
permissions for mobile capability.

* chore: update Claude agent configs and mise tooling

* feat(display): display route with board and focused views (#60)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* fix(display): address QA findings from PR #60 review (#61)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* feat(discovery): add discovery client and server URL flow (#63)

* docs(planning): add Feature 009 plan for discovery client + server URL

Creates the full 4-phase planning artifacts for Refactor A of the onboarding
redesign: server URL input with auto-discovery replaces raw Supabase credential
entry as the primary setup flow.

Tasks: S001, S001-T, S002, S003, S003-T, S004, S004-D
Feature: 009-discovery-client-server-url
ADR-006

* feat(setup): add discovery client and server URL flow

Replace raw Supabase credential inputs on the setup screen with a
primary server address field that auto-discovers backend credentials
via GET /.well-known/ardent-forge.json. Manual Supabase URL and key
entry moves into a collapsible "Manual configuration" section as a
fallback.

- Add discoverInstance() in src/lib/discovery.ts with URL normalization,
  8-second AbortController timeout, and typed DiscoveryResult discriminated
  union (NETWORK_ERROR, NOT_FOUND, INVALID_RESPONSE)
- Add 11 Vitest unit tests covering happy path, URL normalization variants,
  and all error branches
- Refactor setup.tsx: server URL primary input, discovery + validation
  state machine, "or" divider, collapsible manual section, disabled QR
  placeholder for future Refactor B
- Add /.well-known/ardent-forge.json handle block to Caddyfile with
  {$SITE_URL} and {$ANON_KEY} env var interpolation
- Add ANON_KEY to caddy service environment in docker-compose.yml
- Document discovery endpoint for static host and Docker deployments
  in docs/self-hosting.md; update mobile app configuration section

Tasks: S001, S001-T, S002, S003, S003-T, S004
Feature: 009-discovery-client-server-url
ADR: ADR-006

* docs(planning): mark Feature 009 Steps.md as complete

* fix(discovery): harden URL validation, use Zod, deduplicate connect logic

- Validate URL protocol via new URL() parse; reject non-http/https schemes
  (file://, ftp://, etc.) before any fetch is made
- Replace manual typeof field checks with a Zod schema (DiscoverySchema)
  to align with project convention and improve type safety
- Extract shared validateAndSave() helper in setup.tsx, eliminating
  duplicated validate-save-init-navigate block between handleConnect
  and handleDiscoverAndConnect
- Add 4 new tests: file:// rejection, ftp:// rejection, empty string,
  whitespace-only input

Feature: 009-discovery-client-server-url

* fix(review): resolve PR review findings for discovery feature

Fixes (P6-001..008): add logging to all error paths, capture error
params in catch blocks, correct error type semantics (INVALID_INPUT
for validation, INVALID_RESPONSE for JSON parse), fix JSDoc, add
aria-expanded to disclosure toggle, add double-submission guard,
fix comment scope.

Refactor: unify 4 parallel state variables into a single SetupState
discriminated union with 7 phases, eliminating impossible states.
Also wraps handlers in try-catch (P6-009..011) and fixes success
state ordering as part of the refactor.

Rules: create .claude/rules/error-handling.md codifying error type
semantics and catch block conventions (P6-017, P6-018).

Tasks: S005..S007-T added to Steps.md for schema tightening and
test coverage gaps (P6-012..015).

Review: Context/Reviews/0006-discovery-client-server-url-2026-04-04.md

* fix(discovery): tighten schema validators and add missing test coverage

S005: Use z.string().url() for supabase_url, z.string().min(1) for
supabase_publishable_key and version in DiscoverySchema. Validation
is now strictest at the system boundary.

S005-T: Add test for missing supabase_publishable_key field.
S006-T: Add test for 500 HTTP response (non-404 error code).
S007-T: Add test for URL with path component being stripped.

Review: P6-012, P6-013, P6-014, P6-015

* docs(review): mark discovery feature review as verified

* feat(display): idle mode with Edge Function broadcast (#62)

* docs(planning): add feature 009 idle mode spec, tech plan, and steps

Full 4-phase blueprint for Step 29 (Idle Mode + Edge Function).

- Spec.md: 8 must-haves, 17 testable assertions, all open questions
  resolved (24h clock default, /display?clock=12h override, max 3
  sessions, silently hide schedule when Edge Function unreachable)
- Tech.md: 8 architecture decisions -- idle_snapshot event type
  extension, Realtime HTTP broadcast API, Supabase cron schedule,
  program hierarchy JOIN query, server_time drift correction, CSS-only
  transitions, TanStack Router search param validation
- Steps.md: 10 tasks across 4 phases, frontend-specialist +
  backend-engineer + quality-engineer, parallel Phase 1 (types + Edge
  Function) and Phase 2 (ClockDisplay + IdleView + transition wrapper)

* feat(display): implement idle mode with Edge Function broadcast

Adds idle mode to the /display route -- the ambient state shown when no
workouts are active. A server-side Edge Function queries today's remaining
scheduled sessions every 60 seconds and broadcasts them to the display
channel so the TV always shows useful information.

- IdleSnapshot Zod schema + idle_snapshot event type in DisplayEventType
- ClockDisplay component (8rem Space Grotesk, server-time drift correction)
- IdleView component (clock, date, sessions list, next-up badge, footer)
- useDisplayMode hook deriving idle/board/focused from session map
- useIdleSnapshot hook subscribing to display Broadcast channel
- DisplayModeTransition wrapper (300ms fade for idle↔board, 400ms zoom for board↔focused)
- /display route composed end-to-end with all hooks and components
- display-idle-snapshot Edge Function with cron every 60 seconds
- get_display_idle_sessions RPC (service_role only, filters completed sessions)
- /display bypasses config guard in __root.tsx (public, no-auth route)
- docs/self-hosting.md: Remote Display section

Tasks: S001, S001-T, S002, S003, S003-T, S004, S005, S005-T, S006, S006-T, S007, S008, S009, S009-D
Feature: 009-idle-mode

* docs(planning): mark feature 009 idle mode as complete

Feature: 009-idle-mode

* fix(display): address QA findings from idle mode review

Frontend:
- Refactor useDisplayMode to use setState-during-render pattern (React
  "storing info from previous renders") instead of refs-in-useMemo,
  fixing react-hooks/refs ESLint violations and the ref-as-dep stability bug
- Add NaN guard and 60s periodic re-sync to ClockDisplay offset to handle
  NTP corrections and DST transitions on long-running displays
- Wire real Realtime channel status to connectionStatus in display route,
  replacing the hardcoded 'connected' placeholder
- Fix useIdleSnapshot cleanup to safely void removeChannel promise; add
  dev-mode console.warn on safeParse failures for easier debugging
- Fix IdleView session key to use display_name+session_name instead of
  array index
- Remove redundant cleanup() calls in display test afterEach blocks

Backend:
- Add day_of_week IS NOT NULL guard to get_display_idle_sessions RPC to
  prevent silent exclusion of sessions with unset day_of_week
- Add composite index idx_workout_logs_session_user_date to prevent full
  table scan on the cron-driven workout_logs LEFT JOIN
- Replace CREATE OR REPLACE with DROP IF EXISTS + CREATE for atomic
  GRANT application
- Remove non-null assertions on sessions[0] in Edge Function
- Add LIMIT 3 comment and verify_jwt rationale comment in config
- Improve Edge Function error messages to include error codes and response
  status for easier production debugging

Feature: 009-idle-mode

* fix(display): resolve PR review findings from 0006-idle-mode-review

Fixes: clock drift bug (P6-001), null client silence (P6-002),
lying connection status (P6-003), broad path bypass (P6-004),
duplicate channel (P6-005), missing CLOSED handling (P6-006),
production-silent parse errors (P6-007), duplicate DisplayMode type
(P6-008), unsafe RPC cast (P6-009), redundant comments (P6-010)

Tasks: S011-S014 added to Steps.md for missing test coverage
Rules: .min(1) on display-facing strings, Tech.md D4 corrected

Review: Context/Reviews/0006-idle-mode-review.md

* test(display): add review remediation tests S011-S014

S011: useIdleSnapshot hook tests (9 tests) -- valid/invalid payload,
null client, cleanup, connection status callback for all channel states
S012: DisplayModeTransition tests (8 tests) -- transition class mapping
for all mode pairs including duration assertions
S013: IdleSnapshot schema edge cases (7 tests) -- invalid session_type,
empty arrays, invalid ISO dates, .min(1) enforcement
S014: ClockDisplay correction test strengthened to assert exact time value

Review: Context/Reviews/0006-idle-mode-review.md

* docs(reviews): mark 0006-idle-mode-review as verified

All 16 findings confirmed resolved by review-verify agent.

Feature: 009-idle-mode

* fix(display): add onIdleSnapshot to subscriber test mock handlers

Fixes TS build error from merge integration -- the test's mock handlers
were missing the new onIdleSnapshot callback added to DisplayEventHandlers.

* chore(harness): update skills, hooks, and Claude Code settings

* docs: update hosting PRD and add onboarding refactoring plans

* feat(onboarding): QR code gen + scan for server setup (#64)

* docs(planning): add Feature 010 QR code gen + scan blueprint

Spec, Tech, and Steps for Refactor B of the onboarding redesign
(QR code generation in Settings + scan/paste on setup screen).
Covers invite link format, qrcode.react config, barcode scanner
plugin setup, and isTauri()-based platform branching. Updates
onboarding-refactoring-plans.md with researched plugin specifics.

Feature: 010-qr-code-gen-scan

* feat(onboarding): add QR code server sharing and scan on setup

Adds QR-based server sharing to reduce friction for onboarding new
users to a self-hosted Ardent Forge instance.

- Settings backend section now shows a "Share this server" area with
  a QRCodeSVG encoding `ardentforge://connect?url=...&key=...` and a
  "Copy invite link" button, guarded by hasConfig()
- Setup screen QR button activates native camera scan via
  @tauri-apps/plugin-barcode-scanner (Tauri/Android) or toggles an
  inline paste field (browser); both paths parse the invite link and
  auto-trigger connection validation
- New src/lib/invite-link.ts provides buildInviteLink() and
  parseInviteLink() as a pure shared utility
- Barcode scanner plugin registered under #[cfg(mobile)] and
  barcode-scanner:default added to mobile capabilities

Tasks: S001, S002, S003, S004, S005, S006
Feature: 010-qr-code-gen-scan

* fix(qr-scan): address QA findings from code review

- Add .catch() to unawaited processInviteLink() calls in paste/keydown
  handlers to prevent silent unhandled promise rejections
- Replace duplicate dynamic import of cancel with cancelRef to avoid
  redundant module resolution on scan overlay dismiss
- Change perms === 'denied' guard to perms !== 'granted' for defensive
  permission handling on non-standard permission states
- Move tauri-plugin-barcode-scanner to target-specific mobile dep in
  Cargo.toml to avoid enlarging desktop binary
- Remove unreachable pathname fallback branch from parseInviteLink
- Add comment to buildInviteLink documenting publishable key intent
- Rename misleading test and add true malformed-URL test coverage
- Apply rustfmt formatting to db.rs

Feature: 010-qr-code-gen-scan

* fix(qr-scan): resolve P7 review findings and add component tests

Fix bare catch blocks in handleCopy and connection-validator, add
try/catch to processInviteLink to prevent UI getting stuck, handle
unhandled promise from handleScan onClick, add user-facing error
toasts for clipboard failures, fix buildInviteLink to use SCHEME/HOST
constants, downgrade parse error log to silent null return, add
aria-label to paste field, improve QR scan failure toast message.

Add component tests for processInviteLink valid/invalid flows and
backend-settings share section conditional rendering and copy button.

Tasks: S006-T1, S006-T2
Feature: 010-qr-code-gen-scan
Review: Context/Reviews/0007-pr64-qr-code-gen-scan-review.md

* docs(planning): resolve P7 review and add S006-T1/T2 tasks to Steps.md

Mark all 14 findings in the QR code gen/scan review as resolved.
Add S006-T1 (processInviteLink component tests) and S006-T2
(backend-settings share section tests) to Feature 010 Steps.md.

Feature: 010-qr-code-gen-scan

* docs(review): mark P7 review verified by review-verify agent

* refactor(review-resolve): simplify bulk triage to bulk accept

Renames bulk triage to bulk accept to better reflect the workflow --
the capture category already IS the suggested action. Removes the
requirement for a reason on discard (individual and bulk).

* chore(harness): add gitignore entries and reformat hooks

Adds .claude/backups/, .claude/worktrees/, .serena/, and
hooks/skill-activation/.state/ to .gitignore. Reformats all hook
TypeScript files to double-quote style for consistency.

* feat(builders): progressive disclosure and contextual help (Step 14.5)

* docs(planning): add Step 14.5 feature plan and user flow docs

Documents the progressive disclosure and contextual help plan
for the template builder and program builder (Step 14.5).
Adds Flow 7 builder progressive disclosure and contextual help
system diagrams to 10-user-flows.md.

Feature: 010-Progressive-Disclosure
Tasks: S001–S014-T

* feat(builders): progressive disclosure and contextual help (Step 14.5)

Adds context-aware field visibility, staged group creation, set scheme
filtering, and inline help to the template and program builders.

New primitives and utilities:
- HelpTrigger component (Popover desktop / Drawer mobile via useMediaQuery)
- shadcn Popover and Drawer with Iron & Ember styling
- useMediaQuery hook (SSR-safe, listener cleanup)
- CollapsedFieldsRow for expandable hidden field groups
- visibility-maps.ts: data-driven lookup tables for all field visibility rules
- help-content.ts: GROUP_TYPE_HELP, BLOCK_TYPE_HELP, SOURCE_HELP constants
- text-label-large and text-body-small typography utilities in index.css

Template builder changes:
- Category-aware Scoring/Time Cap visibility with CollapsedFieldsRow
- Activity group staged reveal: Stage 1 (type selector only), Stage 2 (type-
  dependent rest fields + exercises)
- Set scheme type list filtered by session category; "Show all types" escape
- Load type options filtered by set scheme type; null = hide entire section
- Session picker gains "Create new event" inline creation flow

Program builder changes:
- Block type HelpTrigger with one-liner summary (desktop + mobile)
- Source badge HelpTrigger with all 7 sources

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013
Feature: 010-Progressive-Disclosure

* fix(builders): resolve QA findings from progressive disclosure review

Fixes two blocking hook violations and seven warnings across the builder
components introduced in Step 14.5.

Errors fixed:
- Move useCallback above early return in LoadSpecEditor to satisfy
  Rules of Hooks (conditional hook call)
- Rewrite useMediaQuery with useSyncExternalStore to eliminate
  synchronous setState in effect and double render

Warnings fixed:
- Remove eslint-disable suppression in LoadSpecEditor; expand useEffect
  deps to [allowedLoads, availableTypes, value.type, onChange]
- Replace div[role=button] nested inside <button> in MobileDayRow with
  sibling <button> elements; fix event type to MouseEvent | KeyboardEvent
- Add useId/aria-controls pairing to CollapsedFieldsRow for screen
  reader disclosure association
- Dynamic aria-label on HelpTrigger (Help: ${title} instead of static)
- Extract BLOCK_TYPE_STYLES to constants.ts; remove duplication across
  block-editor and mobile-block-editor
- Remove unused placement prop from HelpTrigger and all four call sites
- Fix PopoverTitle to render <h2> matching its ComponentProps<'h2'> type

Suggestions addressed:
- Sync ladderText in DescendingRepsFields when repLadder resets externally
- Use touchLastAssigned.mutate (stable ref) in useCallback deps
- Add clientId to ActivityGroupData and ActivityData; generate via
  crypto.randomUUID() at creation; use as React key instead of index
- Add aria-expanded/aria-controls to ProgramForm details toggle

Feature: 010-Progressive-Disclosure

* fix(builders): resolve PR review findings P7

Fixes: P7-001 (silent userId guard), P7-002 (JSX triplication),
P7-003 (redundant satisfies), P7-004 (unstable useEffect deps),
P7-005 (block type help duplication), P7-007 (missing JSDoc),
P7-008 (HelpEntry interface), P7-009 (non-empty tuple),
P7-010 (scheme map typing), P7-011 (constants typing)
Rules: P7-013, P7-014 added to typescript-conventions.md;
P7-015 added to error-handling.md
Deferred: P7-012 (ErrorBoundary strategy) to backlog
Discarded: P7-006 (superseded by P7-002 extraction)

Review: Context/Reviews/0007-pr65-progressive-disclosure-review.md

* fix(builders): resolve review-verify type errors and mark review passed

Fix 3 pre-existing TS errors from P7 review fixes: collapsedLabels
cast to non-empty tuple, allowedLoads.includes widened for
LoadSpec['type'], defaultTypes.includes widened for SetSchemeType.
Review file verified and checkbox marked.

* feat: implement Android CI/CD pipeline (Feature 011) (#67)

* docs(ci-cd): add full blueprint for Android CI/CD pipeline

4-phase planning for feature 011: GitHub Actions workflows covering PR
validation (lint, typecheck, unit tests, E2E via Playwright + local
Supabase, Android debug APK build), tagged release pipeline (signed AAB,
Supabase migrations gate, GitHub Release, Play Store internal track
upload), and supporting setup documentation.

Feature: 011-ci-cd-pipeline
ADR-007: GitHub Actions chosen as CI/CD platform

* ci(github-actions): implement complete Android CI/CD pipeline

Adds two GitHub Actions workflows from scratch:

- ci.yml: PR validation with three parallel jobs -- lint/typecheck/unit
  tests, Playwright E2E against local Supabase, and Android debug APK
  build with full Rust/NDK toolchain and caching
- release.yml: tag-triggered release pipeline -- signed AAB build and
  Supabase migrations run in parallel, gated publish job creates GitHub
  Release and uploads to Play Store internal track

Supporting changes:
- Add Playwright (@playwright/test) with smoke tests and test:e2e script
- Add signingConfigs block to build.gradle.kts reading from env vars
- Add .github/BRANCH_PROTECTION.md and .github/README.md
- Add CI/Release workflow badges to README.md

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009
Feature: 011-ci-cd-pipeline

* fix(android-ci): harden pipeline per PR #67 review findings

- Guard signingConfig assignment with keystorePath null check
- Validate KEYSTORE_BASE64 before decode; check decoded file size
- Pass all secrets through env: blocks (not inline shell expansion)
- Add secret validation steps to build-release, migrate, publish jobs
- Reorder publish job: Play Store upload before GitHub Release
- Capture Supabase status once; validate non-empty/non-null values
- Add if-no-files-found: ignore to Playwright report upload
- Fix test:e2e script to use bunx playwright test
- Add webServer timeout: 30_000, stdout/stderr: pipe to playwright.config.ts

Tasks: P8-001, P8-002, P8-003, P8-004, P8-005, P8-006, P8-007, P8-008, P8-015, P8-016

* docs(ci-cd): apply post-review documentation corrections

- Tech.md: Four->Two workflows, remove armv7 target, fix port 4173,
  update signing snippet to null-guard pattern
- Steps.md: add VITE_ prefix to env var names; add S010 secret validation
- ADR-007: status Proposed->Accepted
- Add PR #67 review capture (16/16 findings resolved and verified)
- Update review-resolve skill

Tasks: P8-009, P8-010, P8-011, P8-012, P8-013, P8-014, P8-015

* feat(deep-link): handle ardentforge://connect deep links (#66)

* docs(planning): add Feature 011 deep link handler planning artifacts

Full 4-phase plan for ardentforge://connect deep link handling.
Covers Tauri dispatch extension, browser /connect route, setup
screen pre-population, and transient store for settings handoff.

Feature: 011-deep-link-handler
ADR-007: transient-store-deep-link-state

* feat(deep-link): handle ardentforge://connect deep links

Implements the connect deep link handler so tapping an invite link
opens the app directly to the correct screen with credentials
pre-populated.

- Register ardentforge://connect host in Tauri deep-link config
- Add handleConnectLink() with 4-way branch: invalid, unconfigured,
  same instance, different instance
- Dispatch connect vs auth in onOpenUrl listener (OAuth flow untouched)
- Add /connect browser route as fallback for web users
- Exempt /connect from root route guard
- Setup screen accepts url/key search params for auto-population
- BackendSettings consumes pending connect Zustand store on mount
- Unit tests for all 5 handleConnectLink() branches (5/5 passing)

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010
Feature: 011-deep-link-handler
ADR: ADR-007 (transient Zustand store for cross-route deep link state)

* fix(deep-link): address QA findings from review

- Add desktop deep-link scheme config + permissions to default capability
- Fix bare catch blocks in invite-link.ts and auth.tsx (error-handling convention)
- Refactor setState-in-effect in backend-settings.tsx to useRef guard
- Add HTTPS validation for url param in parseInviteLink
- Accept navigate callback in handleConnectLink (avoids full-page reload in router context)
- Update OAuth tests to mock @tauri-apps/plugin-opener instead of window.open
- Add test for getConfig() returning null with hasConfig true
- Add test for non-HTTPS url param rejection
- Fix setup.test.tsx mock to provide Route.useSearch()

Feature: 011-deep-link-handler

* fix(deep-link): address PR-66 review findings

- Wrap handleConnectLink in try-catch with toast on error (P8-001)
- Isolate connect-link error path from OAuth error handler in
  auth.tsx (P8-002)
- Replace render-phase setState with useRef + useState initializers
  in backend-settings.tsx (P8-003)
- Replace unsafe `as string` casts with typeof guards in
  validateSearch for /connect and /setup routes (P8-004)
- Add toast feedback when auto-validate fails in /setup (P8-005)
- Replace startsWith check with new URL() + protocol/hostname
  validation in parseInviteLink (P8-006)
- Fix bare catch blocks in config-store.ts to capture and log
  errors (P8-007)
- Add store-boundary validation to usePendingConnect.setPending
  (P8-009)
- Add state-management.md coding rule for Zustand conventions
- Add review capture with verification (0008-pr66)
- Add test step S011 to Steps.md (P8-008)

* test(deep-link): add /connect route and auth connect-dispatch tests

- Add connect.test.tsx covering search param validation, pending
  store integration, and redirect behavior
- Update auth-oauth tests for connect deep-link error isolation

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* fix(ci): upgrade supabase/setup-cli from v1 to v2

v1 runs on deprecated Node 20 and installs an older CLI that rejects
newer config keys (oauth_server, web3, cron, etc.), breaking the
migrate job on tagged releases.

* fix(ci): pin supabase CLI to v2.84.4 to match local version

setup-cli@v1 defaults to CLI v2.20.3 which rejects newer config keys.
Pinning to 2.84.4 matches local dev environment. setup-cli v2 is
merged but not yet tagged as a release.

* fix(supabase): remove invalid cron key from config.toml

The [functions.display-idle-snapshot.cron] section is not a valid
config.toml option. Edge function cron scheduling is configured via
pg_cron in SQL, not in config.toml. This was causing supabase link
to fail in CI.

* fix(sync): add HTTP status check and empty update_set guard in pull path (#70)

* fix(sync): add HTTP status check and empty update_set guard in pull path

pull_table deserialized Supabase responses without checking the HTTP
status code, which could silently swallow 4xx/5xx errors or produce
confusing deserialization failures. Added a status check matching the
pattern already used in push.rs.

upsert_row produced invalid SQL ("... ON CONFLICT(id) DO UPDATE SET ")
when the only matching column was `id`. Now falls back to INSERT OR
IGNORE for that edge case.

Closes #68, closes #69

* fix(lint): resolve unused var, explicit any, and unused import errors

- Prefix unused catch parameter with underscore in formatter hook
- Add eslint-disable for necessary `any` cast in test helper
- Remove unused `buildRequest` import from webhook test

* fix(lint): use bare catch block for unused error parameter

* fix(data-mapper): handle JSONB columns already parsed by Supabase PostgREST

Supabase returns JSONB columns as objects, not strings. Calling JSON.parse()
on an object stringifies it to "[object Object]" first, causing a parse error
on session_templates fields (rest_between_groups, time_cap, event_metadata),
activity_groups fields (rest_between_rounds, rest_between_activities), and
activities (set_scheme).

Adds parseJsonOrValue() helper that passes objects through directly and only
calls JSON.parse() on string values, keeping backward compatibility with the
Tauri/SQLite adapter which returns these columns as JSON strings.

* chore: batch enhancement issues cleanup (#71)

* chore: add Zod refinements to chat domain types

Add validation refinements that catch invalid data at parse time:
- conversation.ts: prevent groupId on direct conversations, enforce leftAt > joinedAt
- message.ts: prevent senderId on system messages
- media.ts: add .nonnegative() to durationSeconds and fileSizeBytes

Closes #40, closes #47

* chore: polish realtime manager (simplify clearAllTyping, fix silent catch)

- Simplify clearAllTyping to delegate to clearTypingForConversation helper
- Add error logging to broadcastTyping .catch per error-handling conventions

Closes #42, closes #49

* feat(events): wire EventCountdownBadge navigation to dedicated event route

- Create /events/$templateId route with minimal event detail page
- Update EventCountdownBadge to navigate to new route instead of /library
- Remove _templateId unused-var rename and TODO comment

Closes #31

* perf: batch getUnreadCounts queries with Promise.allSettled

Replace sequential N+1 COUNT queries with concurrent execution via
Promise.allSettled, preserving partial-results error handling.

Closes #39, closes #46

* perf: optimize chat query patterns (CH-2 trigger, findDirectConversation)

- Add error context with [tauri-adapter] prefix to findDirectConversation loop
- Create materialized direct_conversation_pairs lookup table with unique index
- Replace O(N) trigger scan with O(1) index-backed uniqueness check
- Add cleanup trigger for participant departures

Closes #41, closes #48

* docs: add quick plan for enhancement issues batch

* fix(review): resolve PR #71 review findings

Fixes (7): RLS on direct_conversation_pairs, ISO datetime comparison,
findDirectConversation all-fail throw, getUnreadCounts error enrichment,…
…#81)

connect.test.tsx and setup.test.tsx were left behind when the files were
renamed with a "-" prefix. TanStack Router was warning about them on every
build since they don't export a Route.
* fix: add Authorization header to Supabase reachability check

The PostgREST root endpoint requires both `apikey` and `Authorization`
headers. Missing the latter caused a 401 during setup, showing
"Cannot reach server" even with valid credentials.

* fix: auto-validate and surface env var connection failures on setup page

When VITE_SUPABASE_URL and VITE_SUPABASE_PUB_KEY are set but invalid,
the setup page now auto-validates on mount and shows the specific error
(e.g. "Server responded with 401") instead of silently presenting a
blank form. Also adds a banner explaining that environment variables
were detected but failed.

* fix: use Supabase client for connection validation instead of raw fetch

Raw fetch with apikey/Authorization headers only works with legacy anon
keys. New publishable keys (sb_publishable_xxx) require the Supabase
client library which handles auth headers internally. This switch
supports both key formats transparently.

* fix: resolve duplicate migration timestamp collision

Two migrations shared timestamp 20260329000001, causing a primary key
violation in supabase_migrations. Renumbered create_sharing_tables to
20260329000002 and expand_rls_for_sharing to 20260329000003.

* chore: update Claude Code configuration and skills

* feat: complete sync pull path with dynamic upsert and force-pull (Step 9) (#28)

Implement the stubbed pull/upsert path so remote changes from Supabase
are actually written to local SQLite, completing bidirectional sync.

- Fix broken timestamp parsing in upsert_row (ISO 8601 strings were
  silently parsed as 0, causing all remote updates to be discarded)
- Implement dynamic column upsert via pragma_table_info with type
  coercion (timestamps, booleans, JSONB) between Supabase and SQLite
- Add pull_table/pull_all for paginated force-pull via Supabase REST API
- Wire sync_force_pull command with proper state transitions
- Add auto-pull on first sign-in (detect last_pull_at == 0)
- Add 28 new tests (49 total), all passing

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: events & packing lists (Step 13.5) (#30)

* feat(events): add events & packing lists feature (Step 13.5)

Introduces EVENT as a new session category with full support for event
metadata (date, location, URL, requirements) and a categorized packing
checklist with drag-and-drop reorder, optimistic toggle, progress
tracking, and countdown notifications.

Database:
- Migration adds EVENT to category CHECK constraints on session_templates
  and scheduled_sessions; adds event_metadata JSONB to session_templates
  and workout_logs; creates event_items table with polymorphic FK
  (template XOR log), RLS policies, partial indices, and updated_at trigger

Domain:
- eventRequirementSchema, eventMetadataSchema, eventItemSchema (Zod)
- EVENT added to SessionType enum; eventMetadata optional on templates
  and logs; EventItemRow added to database.types.ts

Data layer:
- toEventItem/fromEventItem mappers; eventMetadata parsing in
  toSessionTemplate (JSON.parse) and toWorkoutLog (direct JSONB)
- DataAdapter interface: getEventItems, saveEventItem, updateEventItem,
  deleteEventItem, toggleEventItemPacked (accepts target bool, no race),
  reorderEventItems (Promise.all), cloneSessionTemplate (isPacked reset)
- SupabaseAdapter implements all methods; getSessionTemplateFull fetches
  eventItems for EVENT templates; updateSessionTemplateFull uses 3-way
  reconciliation to preserve item UUIDs and isPacked state
- TauriAdapter stubs (event support deferred, W-8)

Hooks:
- useEventItems, useCreateEventItem, useUpdateEventItem,
  useDeleteEventItem, useToggleEventItemPacked (optimistic),
  useReorderEventItems (optimistic), useNextUpcomingEvent

UI:
- event-builder/: EventTemplateForm (Sheet overlay, create/edit),
  EventDetail (metadata + map link + packing), PackingList (DnD,
  collapsible categories, error feedback), PackingItem (checkbox toggle),
  EventProgressBar (ember on surface-steel, ARIA progressbar),
  EventCard (library), EventCountdownBadge (Today screen, TODAY edge
  case), RequirementEditor, EventItemEditor, shared styles.ts
- Library: EventCard for EVENT templates, NEW EVENT button
- log/$workoutId: interactive EventDetail for event logs
- history/$workoutId: read-only EventDetail for event history
- Today screen: countdown badge when next event within 30 days
- Program builder: flag icon + ember accent for EVENT sessions,
  event date displayed in program-preview grid and detail sections

Native (Rust):
- event_reminders channel registered in notification.rs
- EventReminderState: background Tokio task (60s poll), checks
  configured intervals (7d/3d/1d), quiet hours, packing progress in
  notification body, deterministic notification ID
- schedule_event_reminders / cancel_event_reminders Tauri commands
- Clippy fixes: RemindedState type alias, needless_borrow in lib.rs,
  manual_range_contains in session_reminder.rs
- Lock contention fix: single mutex guard across event loop in tick()

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 002-events-packing-lists

* docs(planning): mark 002-events-packing-lists complete

Updates Steps.md status to Complete, session.md with full wave
execution log and fixes applied, and implementation-plan.md.

Feature: 002-events-packing-lists

* chore(rust): apply cargo fmt across codebase

* chore: add mise config and cortex hooks

* fix(events): address PR #30 review findings

- Fix timezone bug in combineDateTime: store local date strings instead
  of converting to UTC, preventing event dates from shifting by a day
- Add eventUrl validation in form validate() before save
- Switch sequential item persistence to Promise.allSettled with error
  collection for partial-failure reporting
- Fix DST-unsafe daysUntil calculation using date-only comparison
- Make TauriAdapter getEventItems throw consistently with write stubs
- Add "TODAY" special case in Rust event reminder notification title
- Eliminate double hydration of requirements/items in form init
- Add EventItem mapper roundtrip tests and splitDateTime/combineDateTime
  edge case tests (19 new tests)

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(groups): coach program assignment

Adds the ability for a group coach to assign one of their programs to a
member. The program is atomically transferred to the member's ownership
with source tagged as COACH_ASSIGNED and the coach recorded as created_by,
enabling the member to filter out programs they received vs created.

Includes the two-step AssignProgramDialog UI and integrates it into
MemberCard. Both the Tauri SQLite command and the Supabase SECURITY
DEFINER RPC perform the same three authorization checks (caller is COACH,
target is MEMBER, program owned by caller) before executing the transfer.

* fix(sync): resolve incremental push breakage and missing error state

- push.rs: timestamp_millis() -> timestamp() so last_push_at is stored
  in seconds, matching the updated_at column it filters against; the
  millis value was always larger than any updated_at, causing zero rows
  to be found on every push after the first
- sync.rs: sync_force_push now transitions to SyncState::Error on
  failure, matching the sync_force_pull pattern; previously a failed
  push left the engine in Idle, hiding the error from the frontend

* docs(planning): add spec, tech, and steps for coach program assignment

* feat(chat): add chat data layer + QA fixes (#33)

* feat(chat): add chat data layer (Step 21)

Builds the full persistence layer for the chat domain across Supabase,
SQLite, Rust, and TypeScript. No UI -- pure data plumbing for Steps 22-25.

- Supabase migration: conversations, conversation_participants, messages,
  media_attachments with RLS (CH-1), append-only messages (CH-7), and
  direct conversation uniqueness via trigger (CH-2)
- SQLite migration: mirrors Postgres with sync_status on messages for
  offline queueing (pending -> synced/failed state machine)
- Rust: ConversationRow/MessageRow/etc structs, 10 Tauri commands (CRUD
  for conversations, messages, participants, media), chat tables in
  SYNCABLE_TABLES for push/pull sync
- TypeScript: Zod schemas (Conversation, Message, MediaAttachment,
  WorkoutSnapshot), DB row types, bidirectional mappers, 13-method
  ChatAdapter facet on DataAdapter interface, Supabase and Tauri
  adapter implementations, mock adapter updated

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011
Feature: 004-Chat-Data-Layer

* docs(planning): add spec and steps for chat data layer (Step 21)

Feature 004 planning artifacts: Spec.md covering requirements,
testable assertions, and invariants (CH-1, CH-2, CH-4, CH-7);
Steps.md with 4-wave execution plan for db-engineer, rust-engineer,
domain-engineer, and quality-engineer.

Feature: 004-Chat-Data-Layer

* fix(qa): address all QA feedback -- sync resilience, lint, and stale tests

Rust sync hardening:
- sync/mod.rs: replace `return` with `continue` so queue flush failure no longer kills the sync loop permanently; transition to Error state when initial pull fails rather than silently proceeding to push
- sync/pull.rs: add SYNCABLE_TABLES allowlist guard in upsert_row and delete_row
- sync/push.rs: add SYNCABLE_TABLES allowlist guard in push_table
- sync/queue.rs: normalize enqueue timestamp to seconds (was millis, inconsistent with push/pull)

Frontend lint:
- packing-list.tsx: replace setState-in-useEffect anti-pattern with useMemo derivation for mutation error feedback
- event-template-form.tsx: extract splitDateTime/combineDateTime to date-time-utils.ts to fix Fast Refresh violation (non-component exports alongside component)

Tests (31 stale failures fixed):
- Update 5 test files with correct mixed-case text after CSS uppercase refactor
- Rewrite mobile-nav.test.tsx: add useAuth mock, update to 5+1 nav structure, correct icons and routes

* fix(review): resolve PR #33 review findings for chat data layer

Critical fixes (C-1 through C-5):
- Add missing INSERT policy on conversations table (C-1)
- Filter user's own messages from getUnreadCounts in Supabase adapter (C-2)
- Change z.url() to z.string().url() for media attachment URLs (C-3)
- Return Err instead of Ok(()) from SYNCABLE_TABLES allowlist guards (C-4)
- Add missing UPDATE policy on media_attachments table (C-5)

Important fixes (I-2, I-3):
- Add participant check before message insert in send_message (I-2)
- Validate direct conversations require exactly 2 participants (I-3)

Tasks created (S013-S017) for: data mapper tests, dedicated
get_messages_since command, updatedAt mapping fix, unread counts
resilience, and sync guard test coverage.

Suggestions S-1 through S-6 tracked in Ideas backlog.

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

* feat(chat): implement review remediation tasks S013-S017

S013: Add 36 data mapper unit tests for chat entities
  - Conversation, ConversationParticipant, Message, MediaAttachment
  - Round-trip, null handling, and Zod validation tests

S014: Add dedicated get_messages_since Rust command
  - SQL-level WHERE created_at > ? filter replaces client-side filtering
  - Eliminates silent truncation at 1000 message limit

S015: Fix toConversationParticipant updatedAt mapping
  - Use last_read_at ?? joined_at instead of always joined_at

S016: Improve getUnreadCounts error resilience
  - Single conversation failure no longer aborts all counts
  - Returns partial results with console.warn for failed queries

S017: Remove #[cfg(not(test))] from sync allowlist guards
  - Tests now exercise the same allowlist path as production
  - Updated test fixtures to use real SYNCABLE_TABLES entries

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: Supabase Realtime, chat data layer, events & coach assignment (#34)

* docs(planning): add spec, tech, and steps for supabase realtime integration

4-phase blueprint for Step 22 (Supabase Realtime Integration).
Establishes architecture for Broadcast channel management, cursor-based
message pagination, RealtimeManager module, and all chat TanStack Query hooks.

ADR-005: cursor-based getMessages replaces offset-based (no existing callers).

* feat(chat): add Supabase Realtime broadcast channels and chat hooks

Wire up Supabase Realtime Broadcast for live message delivery and typing
indicators. Introduces the RealtimeManager singleton, cursor-based message
pagination, platform-agnostic foreground detection, and all TanStack Query
hooks needed by the Chat UI (Step 23).

New files:
- src/lib/realtime-schemas.ts     -- Zod schemas for broadcast payloads
- src/lib/realtime-manager.ts     -- channel lifecycle, typing state, catch-up
- src/lib/foreground-detector.ts  -- visibilitychange / Tauri focus detection
- src/hooks/use-chat.ts           -- useConversations, useMessages (infinite),
                                     useSendMessage (optimistic), useRealtimeMessages, +6 more
- src/components/chat-realtime-listener.tsx -- app-level cache update listener

Modified:
- data-adapter.ts / supabase-adapter.ts / tauri-adapter.ts / chat.rs
  Replace offset-based getMessages with cursor-based (before?: ISO timestamp)
- main.tsx: mount ChatRealtimeListener alongside SyncListener
- auth.tsx: call resetRealtimeManager() on SIGNED_OUT

Tasks: S001, S002, S003, S004, S005, S006, S007

* fix: resolve QA lint and code quality issues across frontend and Rust

Fix clippy errors in test code (manual_range_contains, approx_constant),
ESLint no-constant-binary-expression in utils test, and three code quality
issues in realtime-manager: validate typing payloads with Zod schema,
replace fragile onTyping callback chain with Set-based listener pattern,
and explicitly discard handleForeground Promise.

* fix(chat): address PR #34 review findings for Supabase Realtime

Multi-listener pattern (addMessageListener/addTypingListener) replacing
single-callback ownership, broadcast try/catch in useSendMessage,
channel lifecycle ownership fix, auth integration via useAuth hook,
and Wave 5 polish: named constants, toMessageFromBroadcast mapper,
simplified clearAllTyping, foreground-detector error logging and
async/await rewrite, broadcastTyping .catch(), MessagePaginationOptions
interface, user_name max(200) constraint.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(chat): complete chat UI with messaging and conversations (#35)

* docs(planning): add spec, tech, and steps for chat UI (Step 23)

* feat(chat): build complete chat UI (Step 23)

Routes, conversation list, virtualized message detail, compose bar,
typing indicators, new conversation flow, group management, and
client-side blocking. Navigation updated -- COMMS replaces Social
on mobile, added after Vault on desktop sidebar with unread badge.

Conversation type extended with participantUserIds so ConversationDetail
can resolve the other user's display name and blocking state for direct
conversations. Supabase adapter fetches participants alongside
conversations; Tauri adapter was already receiving them and now uses them.

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 006-Chat-UI

* docs(planning): add quick plan for conversation participant IDs

Feature: 006-Chat-UI

* fix: update mobile-nav tests for Comms tab and fix Rust formatting

Update test assertions for Social→Comms rename, add useUnreadCounts
mock, and fix rustfmt formatting in event_reminder tests.

* fix(chat): resolve PR #35 review findings

Critical fixes:
- C1: Contact search filter was a no-op; now passes search prop to
  ContactRow which renders null when display name does not match
- C2: Sender name in group bubbles showed raw UUID; resolved via
  useUserProfile inside MessageBubble
- C3: Send failure gave zero feedback; added sendError state and
  inline "Failed to send" indicator on ComposeBar
- C4: Conversation creation failure was silent; added error state
  and inline message in NewConversationSheet
- C5: MessageList had no error/loading state; added spinner and
  error fallback from useMessages isLoading/isError

Important fixes:
- I1: Typing broadcast used user.email; now uses displayName via
  useUserProfile
- I2/I3: useBlockedUsers storage key scoped to userId; added
  console.error/warn on localStorage corruption
- I4: Optimistic message had senderId undefined; now uses current
  user ID from useAuth
- I5: LeaveDialog failure was silent; added inline error message
- I6: ParticipantSheet add failure was silent; shows error including
  Tauri offline message
- I7: Removed rounded-full from avatar containers and unread dots
- I8: Added comment clarifying group threshold (2 selected + server
  adds self = 3 total participants)

Suggestions:
- S3: Removed build-artifact comment from conversation-list.tsx
- S9: Fixed error copy from "pull to retry" to "try again"

Tasks: C1, C2, C3, C4, C5, I1, I2, I3, I4, I5, I6, I7, I8, S3, S9
Feature: 006-Chat-UI

* docs(planning): resolve PR #35 review and add test/polish tasks

Marks 0003-pr35-chat-ui-review.md as fully resolved (5 critical,
8 important, 2 suggestions fixed inline; 6 tasks created; 5
suggestions dismissed with rationale).

Adds Wave 7 tasks to Steps.md:
- S015-T: deriveItems unit tests
- S016-T: useBlockedUsers unit tests
- S017-T: formatTypingText unit tests
- S018-T: relativeTime + getInitials unit tests
- S019: Migrate useBlockedUsers to Zustand store
- S020: Wire archive action in conversation header

Feature: 006-Chat-UI

* refactor(chat): extract utils, add blocked-users store, verify review

Extract pure functions (relativeTime, getInitials, formatTypingText,
deriveItems) into chat-utils.ts and add tests. Migrate useBlockedUsers
to Zustand store with user-scoped persistence. Mark PR #35 review as
verified with follow-up tracked in GitHub issues #43-#51.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: add link preview metadata with OG tags, Twitter Cards, and useMetadata hook (#53)

Add comprehensive link preview support so shared URLs display rich previews
on social platforms and messaging apps. Includes a useMetadata hook for
dynamic per-route title and description updates, used on share pages.

* fix(auth): prevent PKCE code race condition on OAuth callback (#54)

Supabase JS v2.100.1 defaults to detectSessionInUrl: true with PKCE
flow, causing _initialize() to auto-exchange the auth code before the
/auth/callback route handler can. The second exchange attempt fails
(code already consumed), redirecting users to /sign-in?reason=oauth_error.

Disable detectSessionInUrl since we have a dedicated callback route
that handles PKCE code exchange manually.

* fix(auth): enable PKCE flow for OAuth sign-in (#55)

Without explicit flowType: 'pkce', the Supabase client uses implicit
flow, returning tokens as hash fragments (#access_token=...) that the
/auth/callback route cannot read (it expects ?code= query params).

Setting flowType: 'pkce' makes Supabase return a PKCE authorization
code as a query parameter that the callback can exchange for a session.

* fix(db): resolve RLS infinite recursion on group_members and conversation_participants (#56)

The group_members and conversation_participants SELECT policies
self-referenced their own tables, triggering PostgreSQL error 42P17
(infinite recursion). This cascaded 500s to every table whose sharing
policies join group_members (workout_logs, session_templates, etc.).

Creates SECURITY DEFINER helpers (is_group_member, is_group_coach,
is_conversation_participant) that bypass RLS for inner membership
lookups and replaces all six affected policies.

Also removes the PWA manifest (manifest.webmanifest) and its link tag
since Vercel deployment protection returns 401 on the unauthenticated
manifest fetch.

* feat(builder): program builder UI with responsive week grid (#58)

* feat(builder): program builder UI with responsive week grid

Implements the complete program builder and session builder UIs.

Program builder:
- Sidebar layout (desktop) / stacked layout (mobile) at /builder route
- Drag-and-drop block reordering with @dnd-kit
- Week grid defaults to 5-day Mon-Fri view with a toggle to show all 7
  days, improving readability at constrained content widths (~94px/cell
  at 1024px narrowed to nothing legible; 5-day gives ~132px/cell min)
- Session slots reduced from 80px to 56px min-height with type-colored
  left borders (STRENGTH/CONDITIONING/SE/MIXED/EVENT)
- Mobile block editor with vertical day-row tap targets
- Session picker bottom sheet and copy-week dialog
- Program preview overlay with per-session exercise/load tables

Session builder:
- Set scheme editor with per-type form variants
- Activity group editor and session template form

Infrastructure:
- Collapsible shadcn/ui component added
- Removed boilerplate App.tsx/App.css
- Expanded index.css with builder-specific design tokens

* fix(builder): declutter block header with overflow menu

Replace inline action buttons (move up/down, delete) with a dropdown
overflow menu in both desktop and mobile block editors. This prevents
touch target overlap on narrow viewports and isolates the destructive
delete action behind a two-tap interaction.

* feat(builder): guard destructive actions with confirmation and undo

Block and week deletions now require confirmation via AlertDialog,
showing impact ("This will remove N weeks and M sessions.") before
proceeding. Session removal fires immediately but shows a 5-second
undo toast via sonner, letting users recover accidental taps without
blocking the fast path.

- Add alert-dialog.tsx shadcn primitive (Radix AlertDialog)
- Add confirm-delete-dialog.tsx reusable builder component
- Wire block delete in BlockEditor and MobileBlockCard to dialog
- Wire week delete in WeekGrid and MobileWeekSection to dialog
- Add undo toast to SessionSlot and MobileDayRow session removal
- Mount Toaster in main.tsx (bottom-center)

* feat(builder): replace full-screen preview with inline week detail

Remove the disconnected ProgramPreview overlay from the builder flow.
Exercise details now toggle inline per-week via a visibility icon,
keeping the edit-preview feedback loop tight. Save button moves to
the header. Shared formatting helpers extracted to session-detail.tsx.
Also fixes Collapsible asChild bug in block-editor and mobile-block-editor.

* fix(builder): remove interstitial from session template create flow

Tapping "Create new template" in the session picker now opens the
inline form immediately, skipping the Library recommendation callout.
The Library option is preserved as a subtle link ("Want more options?
Create in the Library") below the form for users who need it.

Removes showCreateCallout state and the two-step reveal pattern.

* refactor(builder): consolidate session constants and polish UI details

- Extract SESSION_TINT and SESSION_TYPE_BADGE from session-slot,
  mobile-block-editor, and program-preview into constants.ts
- Remove duplicate @keyframes block-enter and slot-fill in index.css
- Bump section-level labels (WEEK N, day columns, EXERCISE/SETS/LOAD)
  from text-[11px] font-medium to text-xs font-semibold for visual
  hierarchy over metadata badges which stay at text-[11px] font-medium
- Upgrade session picker SheetTitle to text-sm font-display
- Add empty state copy when zero blocks exist in BlockList and
  MobileBlockEditor
- Clarify copy-week dialog: description now states sessions will be
  replaced, and per-week "(N existing)" label gains warning colour

* feat(builder): replace collapsed save error with structured inline validation

Validation errors now appear at the relevant field rather than as a single
joined string below the save button. Program name errors render under the
name input, block name and week errors render on each block card (visible
even when collapsed), and server/auth errors remain global in the header.

* feat(builder): add recently-used templates and reduce week grid noise

Surface the 5 most recently assigned session templates at the top of the
picker sheet so users with large template libraries reach common picks
faster. Adds lastAssignedAt tracking across both Tauri/SQLite and
Supabase backends.

Reduce visual noise in the week grid: restyle empty slots as muted
"Rest" labels instead of prominent calls-to-action, bring the 5/7-day
toggle to mobile, and collapse identical weeks within a block to a
compact summary row (with manual expand).

* refactor(builder): extract session-detail helpers to fix Fast Refresh lint errors

* feat: display broadcast infrastructure (Step 27) (#59)

* feat(chat): video, image, and file sharing (#57)

feat(chat): video, image, and file sharing (Feature 007)

Full-stack media sharing: Cloudflare Stream video uploads with TUS, image/file uploads via Supabase Storage, real-time transcoding status via webhook, signed URL caching, and inline media rendering in chat.

Review: 4 critical + 10 important findings resolved and verified. 12 enhancements implemented. 1,305 tests passing.

* docs: add plan for Step 27 -- display broadcast infrastructure

Spec.md, Tech.md, and Steps.md for Feature 008. Covers DisplaySnapshot
Zod schema, publisher module, store integration, display_visible toggle,
and Supabase Realtime Broadcast channel plumbing. 14 tasks across 4 waves
with 3 agents.

* feat: add display_visible column to user profiles

SQLite and Supabase migrations for display broadcast toggle,
Tauri model and command updates to support the new field.

* feat: add DisplaySnapshot domain type and display publisher

DisplaySnapshot type with Zod schema, display-publisher for
broadcasting workout state, data-mapper and store updates.

* feat: add PushToDisplayButton and display broadcast hook

useDisplayBroadcast hook, PushToDisplayButton component,
workout log and profile route integrations.

* test: add display snapshot and publisher tests

Unit tests for DisplaySnapshot schema, display-publisher,
display-snapshot builder, and updated data-mapper/adapter tests.

* docs: update Steps.md and implementation plan for display broadcast

* fix(review): resolve PR #59 review findings

Fixes: C1 (RLS auth), C2 (image signed URLs), C3 (snapshot try-catch),
C4 (Cloudflare subdomain), I1-I9 (error handling, schema tightening,
z.url() revert, publisher readiness), M1/M3/M5/M6 (env guards,
request parsing, query error logging, JSDoc)
Tasks: S024-T, S025-T added for media-provider and timeout tests
Comments: 9 comment issues resolved

Review: Context/Reviews/0005-pr59-display-broadcast-review.md

* test(media): add unit tests for CloudflareStreamProvider and transcoding timeout

S024-T: media-provider tests covering signed URL TTL cache hit/miss,
null client error, Zod validation of malformed responses, and cache
deduplication (13 tests).

S025-T: use-media-upload tests covering 5-minute transcoding timeout
marks processing attachment as failed, unaffected ready attachments,
query cache invalidation, and timer cleanup on unmount (4 tests).

Tasks: S024-T, S025-T
Feature: 007-video-image-sharing

* docs(review): mark PR #59 review as verified

* feat(edge-functions): deploy CF video edge functions with Vault secrets and local JWT signing

Deploys three Supabase Edge Functions for Cloudflare Stream video support
(Step 25). All Cloudflare credentials read from Supabase Vault using
CF_* secret names (CF_ACCOUNT_ID, CF_STREAM_TOKEN, CF_WEBHOOK_SECRET,
CF_SIGNING_KEY_PEM, CF_SIGNING_KEY_ID, CF_CUSTOMER_SUBDOMAIN).

chat-media-signed-url: replaces Cloudflare token API round-trip with
local RS256 JWT signing via CF_SIGNING_KEY_PEM + CF_SIGNING_KEY_ID.
No outbound HTTP call needed per playback request.
chat-media-upload-url: reads CF_ACCOUNT_ID + CF_STREAM_TOKEN from Vault.
chat-media-webhook: reads CF_WEBHOOK_SECRET + CF_ACCOUNT_ID from Vault.

Adds get_secret() SQL migration granting service_role-only read access
to vault.decrypted_secrets. Fixes duplicate migration timestamp
20260403000002 -> 20260403000003 for session_template_last_assigned.

Updates mock-supabase test util to support keyed RPC results
(get_secret:SECRET_NAME) for parameter-specific vault mocking.
Removes CLOUDFLARE_ACCOUNT_ID from STANDARD_ENV test fixture.

* chore: update app icons and branding assets

Replace default Tauri icons with Ardent Forge branding across all
platforms (Android, iOS, Windows, macOS). Update app display name
in Android strings.xml and tauri.conf.json.

* fix(db): rename migration 008 and auto-recreate DB on checksum mismatch

Rename session_template_last_assigned to migration 009 to make room
for 008_add_display_visible. Add debug-only auto-recreate logic so
migration checksum mismatches during development delete and rebuild
the local database instead of crashing.

* fix(auth): use Chrome Custom Tabs for Google OAuth on Android

Replace window.open with tauri-plugin-opener's inAppBrowser mode to
launch Google OAuth in Chrome Custom Tabs instead of an embedded
WebView. Google blocks OAuth in WebViews under their "Use secure
browsers" policy. Add opener plugin registration and URL scope
permissions for mobile capability.

* chore: update Claude agent configs and mise tooling

* feat(display): display route with board and focused views (#60)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* fix(display): address QA findings from PR #60 review (#61)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* feat(discovery): add discovery client and server URL flow (#63)

* docs(planning): add Feature 009 plan for discovery client + server URL

Creates the full 4-phase planning artifacts for Refactor A of the onboarding
redesign: server URL input with auto-discovery replaces raw Supabase credential
entry as the primary setup flow.

Tasks: S001, S001-T, S002, S003, S003-T, S004, S004-D
Feature: 009-discovery-client-server-url
ADR-006

* feat(setup): add discovery client and server URL flow

Replace raw Supabase credential inputs on the setup screen with a
primary server address field that auto-discovers backend credentials
via GET /.well-known/ardent-forge.json. Manual Supabase URL and key
entry moves into a collapsible "Manual configuration" section as a
fallback.

- Add discoverInstance() in src/lib/discovery.ts with URL normalization,
  8-second AbortController timeout, and typed DiscoveryResult discriminated
  union (NETWORK_ERROR, NOT_FOUND, INVALID_RESPONSE)
- Add 11 Vitest unit tests covering happy path, URL normalization variants,
  and all error branches
- Refactor setup.tsx: server URL primary input, discovery + validation
  state machine, "or" divider, collapsible manual section, disabled QR
  placeholder for future Refactor B
- Add /.well-known/ardent-forge.json handle block to Caddyfile with
  {$SITE_URL} and {$ANON_KEY} env var interpolation
- Add ANON_KEY to caddy service environment in docker-compose.yml
- Document discovery endpoint for static host and Docker deployments
  in docs/self-hosting.md; update mobile app configuration section

Tasks: S001, S001-T, S002, S003, S003-T, S004
Feature: 009-discovery-client-server-url
ADR: ADR-006

* docs(planning): mark Feature 009 Steps.md as complete

* fix(discovery): harden URL validation, use Zod, deduplicate connect logic

- Validate URL protocol via new URL() parse; reject non-http/https schemes
  (file://, ftp://, etc.) before any fetch is made
- Replace manual typeof field checks with a Zod schema (DiscoverySchema)
  to align with project convention and improve type safety
- Extract shared validateAndSave() helper in setup.tsx, eliminating
  duplicated validate-save-init-navigate block between handleConnect
  and handleDiscoverAndConnect
- Add 4 new tests: file:// rejection, ftp:// rejection, empty string,
  whitespace-only input

Feature: 009-discovery-client-server-url

* fix(review): resolve PR review findings for discovery feature

Fixes (P6-001..008): add logging to all error paths, capture error
params in catch blocks, correct error type semantics (INVALID_INPUT
for validation, INVALID_RESPONSE for JSON parse), fix JSDoc, add
aria-expanded to disclosure toggle, add double-submission guard,
fix comment scope.

Refactor: unify 4 parallel state variables into a single SetupState
discriminated union with 7 phases, eliminating impossible states.
Also wraps handlers in try-catch (P6-009..011) and fixes success
state ordering as part of the refactor.

Rules: create .claude/rules/error-handling.md codifying error type
semantics and catch block conventions (P6-017, P6-018).

Tasks: S005..S007-T added to Steps.md for schema tightening and
test coverage gaps (P6-012..015).

Review: Context/Reviews/0006-discovery-client-server-url-2026-04-04.md

* fix(discovery): tighten schema validators and add missing test coverage

S005: Use z.string().url() for supabase_url, z.string().min(1) for
supabase_publishable_key and version in DiscoverySchema. Validation
is now strictest at the system boundary.

S005-T: Add test for missing supabase_publishable_key field.
S006-T: Add test for 500 HTTP response (non-404 error code).
S007-T: Add test for URL with path component being stripped.

Review: P6-012, P6-013, P6-014, P6-015

* docs(review): mark discovery feature review as verified

* feat(display): idle mode with Edge Function broadcast (#62)

* docs(planning): add feature 009 idle mode spec, tech plan, and steps

Full 4-phase blueprint for Step 29 (Idle Mode + Edge Function).

- Spec.md: 8 must-haves, 17 testable assertions, all open questions
  resolved (24h clock default, /display?clock=12h override, max 3
  sessions, silently hide schedule when Edge Function unreachable)
- Tech.md: 8 architecture decisions -- idle_snapshot event type
  extension, Realtime HTTP broadcast API, Supabase cron schedule,
  program hierarchy JOIN query, server_time drift correction, CSS-only
  transitions, TanStack Router search param validation
- Steps.md: 10 tasks across 4 phases, frontend-specialist +
  backend-engineer + quality-engineer, parallel Phase 1 (types + Edge
  Function) and Phase 2 (ClockDisplay + IdleView + transition wrapper)

* feat(display): implement idle mode with Edge Function broadcast

Adds idle mode to the /display route -- the ambient state shown when no
workouts are active. A server-side Edge Function queries today's remaining
scheduled sessions every 60 seconds and broadcasts them to the display
channel so the TV always shows useful information.

- IdleSnapshot Zod schema + idle_snapshot event type in DisplayEventType
- ClockDisplay component (8rem Space Grotesk, server-time drift correction)
- IdleView component (clock, date, sessions list, next-up badge, footer)
- useDisplayMode hook deriving idle/board/focused from session map
- useIdleSnapshot hook subscribing to display Broadcast channel
- DisplayModeTransition wrapper (300ms fade for idle↔board, 400ms zoom for board↔focused)
- /display route composed end-to-end with all hooks and components
- display-idle-snapshot Edge Function with cron every 60 seconds
- get_display_idle_sessions RPC (service_role only, filters completed sessions)
- /display bypasses config guard in __root.tsx (public, no-auth route)
- docs/self-hosting.md: Remote Display section

Tasks: S001, S001-T, S002, S003, S003-T, S004, S005, S005-T, S006, S006-T, S007, S008, S009, S009-D
Feature: 009-idle-mode

* docs(planning): mark feature 009 idle mode as complete

Feature: 009-idle-mode

* fix(display): address QA findings from idle mode review

Frontend:
- Refactor useDisplayMode to use setState-during-render pattern (React
  "storing info from previous renders") instead of refs-in-useMemo,
  fixing react-hooks/refs ESLint violations and the ref-as-dep stability bug
- Add NaN guard and 60s periodic re-sync to ClockDisplay offset to handle
  NTP corrections and DST transitions on long-running displays
- Wire real Realtime channel status to connectionStatus in display route,
  replacing the hardcoded 'connected' placeholder
- Fix useIdleSnapshot cleanup to safely void removeChannel promise; add
  dev-mode console.warn on safeParse failures for easier debugging
- Fix IdleView session key to use display_name+session_name instead of
  array index
- Remove redundant cleanup() calls in display test afterEach blocks

Backend:
- Add day_of_week IS NOT NULL guard to get_display_idle_sessions RPC to
  prevent silent exclusion of sessions with unset day_of_week
- Add composite index idx_workout_logs_session_user_date to prevent full
  table scan on the cron-driven workout_logs LEFT JOIN
- Replace CREATE OR REPLACE with DROP IF EXISTS + CREATE for atomic
  GRANT application
- Remove non-null assertions on sessions[0] in Edge Function
- Add LIMIT 3 comment and verify_jwt rationale comment in config
- Improve Edge Function error messages to include error codes and response
  status for easier production debugging

Feature: 009-idle-mode

* fix(display): resolve PR review findings from 0006-idle-mode-review

Fixes: clock drift bug (P6-001), null client silence (P6-002),
lying connection status (P6-003), broad path bypass (P6-004),
duplicate channel (P6-005), missing CLOSED handling (P6-006),
production-silent parse errors (P6-007), duplicate DisplayMode type
(P6-008), unsafe RPC cast (P6-009), redundant comments (P6-010)

Tasks: S011-S014 added to Steps.md for missing test coverage
Rules: .min(1) on display-facing strings, Tech.md D4 corrected

Review: Context/Reviews/0006-idle-mode-review.md

* test(display): add review remediation tests S011-S014

S011: useIdleSnapshot hook tests (9 tests) -- valid/invalid payload,
null client, cleanup, connection status callback for all channel states
S012: DisplayModeTransition tests (8 tests) -- transition class mapping
for all mode pairs including duration assertions
S013: IdleSnapshot schema edge cases (7 tests) -- invalid session_type,
empty arrays, invalid ISO dates, .min(1) enforcement
S014: ClockDisplay correction test strengthened to assert exact time value

Review: Context/Reviews/0006-idle-mode-review.md

* docs(reviews): mark 0006-idle-mode-review as verified

All 16 findings confirmed resolved by review-verify agent.

Feature: 009-idle-mode

* fix(display): add onIdleSnapshot to subscriber test mock handlers

Fixes TS build error from merge integration -- the test's mock handlers
were missing the new onIdleSnapshot callback added to DisplayEventHandlers.

* chore(harness): update skills, hooks, and Claude Code settings

* docs: update hosting PRD and add onboarding refactoring plans

* feat(onboarding): QR code gen + scan for server setup (#64)

* docs(planning): add Feature 010 QR code gen + scan blueprint

Spec, Tech, and Steps for Refactor B of the onboarding redesign
(QR code generation in Settings + scan/paste on setup screen).
Covers invite link format, qrcode.react config, barcode scanner
plugin setup, and isTauri()-based platform branching. Updates
onboarding-refactoring-plans.md with researched plugin specifics.

Feature: 010-qr-code-gen-scan

* feat(onboarding): add QR code server sharing and scan on setup

Adds QR-based server sharing to reduce friction for onboarding new
users to a self-hosted Ardent Forge instance.

- Settings backend section now shows a "Share this server" area with
  a QRCodeSVG encoding `ardentforge://connect?url=...&key=...` and a
  "Copy invite link" button, guarded by hasConfig()
- Setup screen QR button activates native camera scan via
  @tauri-apps/plugin-barcode-scanner (Tauri/Android) or toggles an
  inline paste field (browser); both paths parse the invite link and
  auto-trigger connection validation
- New src/lib/invite-link.ts provides buildInviteLink() and
  parseInviteLink() as a pure shared utility
- Barcode scanner plugin registered under #[cfg(mobile)] and
  barcode-scanner:default added to mobile capabilities

Tasks: S001, S002, S003, S004, S005, S006
Feature: 010-qr-code-gen-scan

* fix(qr-scan): address QA findings from code review

- Add .catch() to unawaited processInviteLink() calls in paste/keydown
  handlers to prevent silent unhandled promise rejections
- Replace duplicate dynamic import of cancel with cancelRef to avoid
  redundant module resolution on scan overlay dismiss
- Change perms === 'denied' guard to perms !== 'granted' for defensive
  permission handling on non-standard permission states
- Move tauri-plugin-barcode-scanner to target-specific mobile dep in
  Cargo.toml to avoid enlarging desktop binary
- Remove unreachable pathname fallback branch from parseInviteLink
- Add comment to buildInviteLink documenting publishable key intent
- Rename misleading test and add true malformed-URL test coverage
- Apply rustfmt formatting to db.rs

Feature: 010-qr-code-gen-scan

* fix(qr-scan): resolve P7 review findings and add component tests

Fix bare catch blocks in handleCopy and connection-validator, add
try/catch to processInviteLink to prevent UI getting stuck, handle
unhandled promise from handleScan onClick, add user-facing error
toasts for clipboard failures, fix buildInviteLink to use SCHEME/HOST
constants, downgrade parse error log to silent null return, add
aria-label to paste field, improve QR scan failure toast message.

Add component tests for processInviteLink valid/invalid flows and
backend-settings share section conditional rendering and copy button.

Tasks: S006-T1, S006-T2
Feature: 010-qr-code-gen-scan
Review: Context/Reviews/0007-pr64-qr-code-gen-scan-review.md

* docs(planning): resolve P7 review and add S006-T1/T2 tasks to Steps.md

Mark all 14 findings in the QR code gen/scan review as resolved.
Add S006-T1 (processInviteLink component tests) and S006-T2
(backend-settings share section tests) to Feature 010 Steps.md.

Feature: 010-qr-code-gen-scan

* docs(review): mark P7 review verified by review-verify agent

* refactor(review-resolve): simplify bulk triage to bulk accept

Renames bulk triage to bulk accept to better reflect the workflow --
the capture category already IS the suggested action. Removes the
requirement for a reason on discard (individual and bulk).

* chore(harness): add gitignore entries and reformat hooks

Adds .claude/backups/, .claude/worktrees/, .serena/, and
hooks/skill-activation/.state/ to .gitignore. Reformats all hook
TypeScript files to double-quote style for consistency.

* feat(builders): progressive disclosure and contextual help (Step 14.5)

* docs(planning): add Step 14.5 feature plan and user flow docs

Documents the progressive disclosure and contextual help plan
for the template builder and program builder (Step 14.5).
Adds Flow 7 builder progressive disclosure and contextual help
system diagrams to 10-user-flows.md.

Feature: 010-Progressive-Disclosure
Tasks: S001–S014-T

* feat(builders): progressive disclosure and contextual help (Step 14.5)

Adds context-aware field visibility, staged group creation, set scheme
filtering, and inline help to the template and program builders.

New primitives and utilities:
- HelpTrigger component (Popover desktop / Drawer mobile via useMediaQuery)
- shadcn Popover and Drawer with Iron & Ember styling
- useMediaQuery hook (SSR-safe, listener cleanup)
- CollapsedFieldsRow for expandable hidden field groups
- visibility-maps.ts: data-driven lookup tables for all field visibility rules
- help-content.ts: GROUP_TYPE_HELP, BLOCK_TYPE_HELP, SOURCE_HELP constants
- text-label-large and text-body-small typography utilities in index.css

Template builder changes:
- Category-aware Scoring/Time Cap visibility with CollapsedFieldsRow
- Activity group staged reveal: Stage 1 (type selector only), Stage 2 (type-
  dependent rest fields + exercises)
- Set scheme type list filtered by session category; "Show all types" escape
- Load type options filtered by set scheme type; null = hide entire section
- Session picker gains "Create new event" inline creation flow

Program builder changes:
- Block type HelpTrigger with one-liner summary (desktop + mobile)
- Source badge HelpTrigger with all 7 sources

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013
Feature: 010-Progressive-Disclosure

* fix(builders): resolve QA findings from progressive disclosure review

Fixes two blocking hook violations and seven warnings across the builder
components introduced in Step 14.5.

Errors fixed:
- Move useCallback above early return in LoadSpecEditor to satisfy
  Rules of Hooks (conditional hook call)
- Rewrite useMediaQuery with useSyncExternalStore to eliminate
  synchronous setState in effect and double render

Warnings fixed:
- Remove eslint-disable suppression in LoadSpecEditor; expand useEffect
  deps to [allowedLoads, availableTypes, value.type, onChange]
- Replace div[role=button] nested inside <button> in MobileDayRow with
  sibling <button> elements; fix event type to MouseEvent | KeyboardEvent
- Add useId/aria-controls pairing to CollapsedFieldsRow for screen
  reader disclosure association
- Dynamic aria-label on HelpTrigger (Help: ${title} instead of static)
- Extract BLOCK_TYPE_STYLES to constants.ts; remove duplication across
  block-editor and mobile-block-editor
- Remove unused placement prop from HelpTrigger and all four call sites
- Fix PopoverTitle to render <h2> matching its ComponentProps<'h2'> type

Suggestions addressed:
- Sync ladderText in DescendingRepsFields when repLadder resets externally
- Use touchLastAssigned.mutate (stable ref) in useCallback deps
- Add clientId to ActivityGroupData and ActivityData; generate via
  crypto.randomUUID() at creation; use as React key instead of index
- Add aria-expanded/aria-controls to ProgramForm details toggle

Feature: 010-Progressive-Disclosure

* fix(builders): resolve PR review findings P7

Fixes: P7-001 (silent userId guard), P7-002 (JSX triplication),
P7-003 (redundant satisfies), P7-004 (unstable useEffect deps),
P7-005 (block type help duplication), P7-007 (missing JSDoc),
P7-008 (HelpEntry interface), P7-009 (non-empty tuple),
P7-010 (scheme map typing), P7-011 (constants typing)
Rules: P7-013, P7-014 added to typescript-conventions.md;
P7-015 added to error-handling.md
Deferred: P7-012 (ErrorBoundary strategy) to backlog
Discarded: P7-006 (superseded by P7-002 extraction)

Review: Context/Reviews/0007-pr65-progressive-disclosure-review.md

* fix(builders): resolve review-verify type errors and mark review passed

Fix 3 pre-existing TS errors from P7 review fixes: collapsedLabels
cast to non-empty tuple, allowedLoads.includes widened for
LoadSpec['type'], defaultTypes.includes widened for SetSchemeType.
Review file verified and checkbox marked.

* feat: implement Android CI/CD pipeline (Feature 011) (#67)

* docs(ci-cd): add full blueprint for Android CI/CD pipeline

4-phase planning for feature 011: GitHub Actions workflows covering PR
validation (lint, typecheck, unit tests, E2E via Playwright + local
Supabase, Android debug APK build), tagged release pipeline (signed AAB,
Supabase migrations gate, GitHub Release, Play Store internal track
upload), and supporting setup documentation.

Feature: 011-ci-cd-pipeline
ADR-007: GitHub Actions chosen as CI/CD platform

* ci(github-actions): implement complete Android CI/CD pipeline

Adds two GitHub Actions workflows from scratch:

- ci.yml: PR validation with three parallel jobs -- lint/typecheck/unit
  tests, Playwright E2E against local Supabase, and Android debug APK
  build with full Rust/NDK toolchain and caching
- release.yml: tag-triggered release pipeline -- signed AAB build and
  Supabase migrations run in parallel, gated publish job creates GitHub
  Release and uploads to Play Store internal track

Supporting changes:
- Add Playwright (@playwright/test) with smoke tests and test:e2e script
- Add signingConfigs block to build.gradle.kts reading from env vars
- Add .github/BRANCH_PROTECTION.md and .github/README.md
- Add CI/Release workflow badges to README.md

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009
Feature: 011-ci-cd-pipeline

* fix(android-ci): harden pipeline per PR #67 review findings

- Guard signingConfig assignment with keystorePath null check
- Validate KEYSTORE_BASE64 before decode; check decoded file size
- Pass all secrets through env: blocks (not inline shell expansion)
- Add secret validation steps to build-release, migrate, publish jobs
- Reorder publish job: Play Store upload before GitHub Release
- Capture Supabase status once; validate non-empty/non-null values
- Add if-no-files-found: ignore to Playwright report upload
- Fix test:e2e script to use bunx playwright test
- Add webServer timeout: 30_000, stdout/stderr: pipe to playwright.config.ts

Tasks: P8-001, P8-002, P8-003, P8-004, P8-005, P8-006, P8-007, P8-008, P8-015, P8-016

* docs(ci-cd): apply post-review documentation corrections

- Tech.md: Four->Two workflows, remove armv7 target, fix port 4173,
  update signing snippet to null-guard pattern
- Steps.md: add VITE_ prefix to env var names; add S010 secret validation
- ADR-007: status Proposed->Accepted
- Add PR #67 review capture (16/16 findings resolved and verified)
- Update review-resolve skill

Tasks: P8-009, P8-010, P8-011, P8-012, P8-013, P8-014, P8-015

* feat(deep-link): handle ardentforge://connect deep links (#66)

* docs(planning): add Feature 011 deep link handler planning artifacts

Full 4-phase plan for ardentforge://connect deep link handling.
Covers Tauri dispatch extension, browser /connect route, setup
screen pre-population, and transient store for settings handoff.

Feature: 011-deep-link-handler
ADR-007: transient-store-deep-link-state

* feat(deep-link): handle ardentforge://connect deep links

Implements the connect deep link handler so tapping an invite link
opens the app directly to the correct screen with credentials
pre-populated.

- Register ardentforge://connect host in Tauri deep-link config
- Add handleConnectLink() with 4-way branch: invalid, unconfigured,
  same instance, different instance
- Dispatch connect vs auth in onOpenUrl listener (OAuth flow untouched)
- Add /connect browser route as fallback for web users
- Exempt /connect from root route guard
- Setup screen accepts url/key search params for auto-population
- BackendSettings consumes pending connect Zustand store on mount
- Unit tests for all 5 handleConnectLink() branches (5/5 passing)

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010
Feature: 011-deep-link-handler
ADR: ADR-007 (transient Zustand store for cross-route deep link state)

* fix(deep-link): address QA findings from review

- Add desktop deep-link scheme config + permissions to default capability
- Fix bare catch blocks in invite-link.ts and auth.tsx (error-handling convention)
- Refactor setState-in-effect in backend-settings.tsx to useRef guard
- Add HTTPS validation for url param in parseInviteLink
- Accept navigate callback in handleConnectLink (avoids full-page reload in router context)
- Update OAuth tests to mock @tauri-apps/plugin-opener instead of window.open
- Add test for getConfig() returning null with hasConfig true
- Add test for non-HTTPS url param rejection
- Fix setup.test.tsx mock to provide Route.useSearch()

Feature: 011-deep-link-handler

* fix(deep-link): address PR-66 review findings

- Wrap handleConnectLink in try-catch with toast on error (P8-001)
- Isolate connect-link error path from OAuth error handler in
  auth.tsx (P8-002)
- Replace render-phase setState with useRef + useState initializers
  in backend-settings.tsx (P8-003)
- Replace unsafe `as string` casts with typeof guards in
  validateSearch for /connect and /setup routes (P8-004)
- Add toast feedback when auto-validate fails in /setup (P8-005)
- Replace startsWith check with new URL() + protocol/hostname
  validation in parseInviteLink (P8-006)
- Fix bare catch blocks in config-store.ts to capture and log
  errors (P8-007)
- Add store-boundary validation to usePendingConnect.setPending
  (P8-009)
- Add state-management.md coding rule for Zustand conventions
- Add review capture with verification (0008-pr66)
- Add test step S011 to Steps.md (P8-008)

* test(deep-link): add /connect route and auth connect-dispatch tests

- Add connect.test.tsx covering search param validation, pending
  store integration, and redirect behavior
- Update auth-oauth tests for connect deep-link error isolation

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* fix(ci): upgrade supabase/setup-cli from v1 to v2

v1 runs on deprecated Node 20 and installs an older CLI that rejects
newer config keys (oauth_server, web3, cron, etc.), breaking the
migrate job on tagged releases.

* fix(ci): pin supabase CLI to v2.84.4 to match local version

setup-cli@v1 defaults to CLI v2.20.3 which rejects newer config keys.
Pinning to 2.84.4 matches local dev environment. setup-cli v2 is
merged but not yet tagged as a release.

* fix(supabase): remove invalid cron key from config.toml

The [functions.display-idle-snapshot.cron] section is not a valid
config.toml option. Edge function cron scheduling is configured via
pg_cron in SQL, not in config.toml. This was causing supabase link
to fail in CI.

* fix(sync): add HTTP status check and empty update_set guard in pull path (#70)

* fix(sync): add HTTP status check and empty update_set guard in pull path

pull_table deserialized Supabase responses without checking the HTTP
status code, which could silently swallow 4xx/5xx errors or produce
confusing deserialization failures. Added a status check matching the
pattern already used in push.rs.

upsert_row produced invalid SQL ("... ON CONFLICT(id) DO UPDATE SET ")
when the only matching column was `id`. Now falls back to INSERT OR
IGNORE for that edge case.

Closes #68, closes #69

* fix(lint): resolve unused var, explicit any, and unused import errors

- Prefix unused catch parameter with underscore in formatter hook
- Add eslint-disable for necessary `any` cast in test helper
- Remove unused `buildRequest` import from webhook test

* fix(lint): use bare catch block for unused error parameter

* fix(data-mapper): handle JSONB columns already parsed by Supabase PostgREST

Supabase returns JSONB columns as objects, not strings. Calling JSON.parse()
on an object stringifies it to "[object Object]" first, causing a parse error
on session_templates fields (rest_between_groups, time_cap, event_metadata),
activity_groups fields (rest_between_rounds, rest_between_activities), and
activities (set_scheme).

Adds parseJsonOrValue() helper that passes objects through directly and only
calls JSON.parse() on string values, keeping backward compatibility with the
Tauri/SQLite adapter which returns these columns as JSON strings.

* chore: batch enhancement issues cleanup (#71)

* chore: add Zod refinements to chat domain types

Add validation refinements that catch invalid data at parse time:
- conversation.ts: prevent groupId on direct conversations, enforce leftAt > joinedAt
- message.ts: prevent senderId on system messages
- media.ts: add .nonnegative() to durationSeconds and fileSizeBytes

Closes #40, closes #47

* chore: polish realtime manager (simplify clearAllTyping, fix silent catch)

- Simplify clearAllTyping to delegate to clearTypingForConversation helper
- Add error logging to broadcastTyping .catch per error-handling conventions

Closes #42, closes #49

* feat(events): wire EventCountdownBadge navigation to dedicated event route

- Create /events/$templateId route with minimal event detail page
- Update EventCountdownBadge to navigate to new route instead of /library
- Remove _templateId unused-var rename and TODO comment

Closes #31

* perf: batch getUnreadCounts queries with Promise.allSettled

Replace sequential N+1 COUNT queries with concurrent execution via
Promise.allSettled, preserving partial-results error handling.

Closes #39, closes #46

* perf: optimize chat query patterns (CH-2 trigger, findDirectConversation)

- Add error context with [tauri-adapter] prefix to findDirectConversation loop
- Create materialized direct_conversation_pairs lookup table with unique index
- Replace O(N) trigger scan with O(1) index-backed uniqueness check
- Add cleanup trigger for participant departures

Closes #41, closes #48

* docs: add quick plan for enhancement issues batch

* fix(review): resolve PR #71 review findings

Fixes (7): RLS on direct_conversation_pairs, ISO datetime comparison,
findDirectConversation all-fail throw, getUnreadCounts error enrichment,
comment ac…
* fix: add Authorization header to Supabase reachability check

The PostgREST root endpoint requires both `apikey` and `Authorization`
headers. Missing the latter caused a 401 during setup, showing
"Cannot reach server" even with valid credentials.

* fix: auto-validate and surface env var connection failures on setup page

When VITE_SUPABASE_URL and VITE_SUPABASE_PUB_KEY are set but invalid,
the setup page now auto-validates on mount and shows the specific error
(e.g. "Server responded with 401") instead of silently presenting a
blank form. Also adds a banner explaining that environment variables
were detected but failed.

* fix: use Supabase client for connection validation instead of raw fetch

Raw fetch with apikey/Authorization headers only works with legacy anon
keys. New publishable keys (sb_publishable_xxx) require the Supabase
client library which handles auth headers internally. This switch
supports both key formats transparently.

* fix: resolve duplicate migration timestamp collision

Two migrations shared timestamp 20260329000001, causing a primary key
violation in supabase_migrations. Renumbered create_sharing_tables to
20260329000002 and expand_rls_for_sharing to 20260329000003.

* chore: update Claude Code configuration and skills

* feat: complete sync pull path with dynamic upsert and force-pull (Step 9) (#28)

Implement the stubbed pull/upsert path so remote changes from Supabase
are actually written to local SQLite, completing bidirectional sync.

- Fix broken timestamp parsing in upsert_row (ISO 8601 strings were
  silently parsed as 0, causing all remote updates to be discarded)
- Implement dynamic column upsert via pragma_table_info with type
  coercion (timestamps, booleans, JSONB) between Supabase and SQLite
- Add pull_table/pull_all for paginated force-pull via Supabase REST API
- Wire sync_force_pull command with proper state transitions
- Add auto-pull on first sign-in (detect last_pull_at == 0)
- Add 28 new tests (49 total), all passing

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: events & packing lists (Step 13.5) (#30)

* feat(events): add events & packing lists feature (Step 13.5)

Introduces EVENT as a new session category with full support for event
metadata (date, location, URL, requirements) and a categorized packing
checklist with drag-and-drop reorder, optimistic toggle, progress
tracking, and countdown notifications.

Database:
- Migration adds EVENT to category CHECK constraints on session_templates
  and scheduled_sessions; adds event_metadata JSONB to session_templates
  and workout_logs; creates event_items table with polymorphic FK
  (template XOR log), RLS policies, partial indices, and updated_at trigger

Domain:
- eventRequirementSchema, eventMetadataSchema, eventItemSchema (Zod)
- EVENT added to SessionType enum; eventMetadata optional on templates
  and logs; EventItemRow added to database.types.ts

Data layer:
- toEventItem/fromEventItem mappers; eventMetadata parsing in
  toSessionTemplate (JSON.parse) and toWorkoutLog (direct JSONB)
- DataAdapter interface: getEventItems, saveEventItem, updateEventItem,
  deleteEventItem, toggleEventItemPacked (accepts target bool, no race),
  reorderEventItems (Promise.all), cloneSessionTemplate (isPacked reset)
- SupabaseAdapter implements all methods; getSessionTemplateFull fetches
  eventItems for EVENT templates; updateSessionTemplateFull uses 3-way
  reconciliation to preserve item UUIDs and isPacked state
- TauriAdapter stubs (event support deferred, W-8)

Hooks:
- useEventItems, useCreateEventItem, useUpdateEventItem,
  useDeleteEventItem, useToggleEventItemPacked (optimistic),
  useReorderEventItems (optimistic), useNextUpcomingEvent

UI:
- event-builder/: EventTemplateForm (Sheet overlay, create/edit),
  EventDetail (metadata + map link + packing), PackingList (DnD,
  collapsible categories, error feedback), PackingItem (checkbox toggle),
  EventProgressBar (ember on surface-steel, ARIA progressbar),
  EventCard (library), EventCountdownBadge (Today screen, TODAY edge
  case), RequirementEditor, EventItemEditor, shared styles.ts
- Library: EventCard for EVENT templates, NEW EVENT button
- log/$workoutId: interactive EventDetail for event logs
- history/$workoutId: read-only EventDetail for event history
- Today screen: countdown badge when next event within 30 days
- Program builder: flag icon + ember accent for EVENT sessions,
  event date displayed in program-preview grid and detail sections

Native (Rust):
- event_reminders channel registered in notification.rs
- EventReminderState: background Tokio task (60s poll), checks
  configured intervals (7d/3d/1d), quiet hours, packing progress in
  notification body, deterministic notification ID
- schedule_event_reminders / cancel_event_reminders Tauri commands
- Clippy fixes: RemindedState type alias, needless_borrow in lib.rs,
  manual_range_contains in session_reminder.rs
- Lock contention fix: single mutex guard across event loop in tick()

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 002-events-packing-lists

* docs(planning): mark 002-events-packing-lists complete

Updates Steps.md status to Complete, session.md with full wave
execution log and fixes applied, and implementation-plan.md.

Feature: 002-events-packing-lists

* chore(rust): apply cargo fmt across codebase

* chore: add mise config and cortex hooks

* fix(events): address PR #30 review findings

- Fix timezone bug in combineDateTime: store local date strings instead
  of converting to UTC, preventing event dates from shifting by a day
- Add eventUrl validation in form validate() before save
- Switch sequential item persistence to Promise.allSettled with error
  collection for partial-failure reporting
- Fix DST-unsafe daysUntil calculation using date-only comparison
- Make TauriAdapter getEventItems throw consistently with write stubs
- Add "TODAY" special case in Rust event reminder notification title
- Eliminate double hydration of requirements/items in form init
- Add EventItem mapper roundtrip tests and splitDateTime/combineDateTime
  edge case tests (19 new tests)

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(groups): coach program assignment

Adds the ability for a group coach to assign one of their programs to a
member. The program is atomically transferred to the member's ownership
with source tagged as COACH_ASSIGNED and the coach recorded as created_by,
enabling the member to filter out programs they received vs created.

Includes the two-step AssignProgramDialog UI and integrates it into
MemberCard. Both the Tauri SQLite command and the Supabase SECURITY
DEFINER RPC perform the same three authorization checks (caller is COACH,
target is MEMBER, program owned by caller) before executing the transfer.

* fix(sync): resolve incremental push breakage and missing error state

- push.rs: timestamp_millis() -> timestamp() so last_push_at is stored
  in seconds, matching the updated_at column it filters against; the
  millis value was always larger than any updated_at, causing zero rows
  to be found on every push after the first
- sync.rs: sync_force_push now transitions to SyncState::Error on
  failure, matching the sync_force_pull pattern; previously a failed
  push left the engine in Idle, hiding the error from the frontend

* docs(planning): add spec, tech, and steps for coach program assignment

* feat(chat): add chat data layer + QA fixes (#33)

* feat(chat): add chat data layer (Step 21)

Builds the full persistence layer for the chat domain across Supabase,
SQLite, Rust, and TypeScript. No UI -- pure data plumbing for Steps 22-25.

- Supabase migration: conversations, conversation_participants, messages,
  media_attachments with RLS (CH-1), append-only messages (CH-7), and
  direct conversation uniqueness via trigger (CH-2)
- SQLite migration: mirrors Postgres with sync_status on messages for
  offline queueing (pending -> synced/failed state machine)
- Rust: ConversationRow/MessageRow/etc structs, 10 Tauri commands (CRUD
  for conversations, messages, participants, media), chat tables in
  SYNCABLE_TABLES for push/pull sync
- TypeScript: Zod schemas (Conversation, Message, MediaAttachment,
  WorkoutSnapshot), DB row types, bidirectional mappers, 13-method
  ChatAdapter facet on DataAdapter interface, Supabase and Tauri
  adapter implementations, mock adapter updated

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011
Feature: 004-Chat-Data-Layer

* docs(planning): add spec and steps for chat data layer (Step 21)

Feature 004 planning artifacts: Spec.md covering requirements,
testable assertions, and invariants (CH-1, CH-2, CH-4, CH-7);
Steps.md with 4-wave execution plan for db-engineer, rust-engineer,
domain-engineer, and quality-engineer.

Feature: 004-Chat-Data-Layer

* fix(qa): address all QA feedback -- sync resilience, lint, and stale tests

Rust sync hardening:
- sync/mod.rs: replace `return` with `continue` so queue flush failure no longer kills the sync loop permanently; transition to Error state when initial pull fails rather than silently proceeding to push
- sync/pull.rs: add SYNCABLE_TABLES allowlist guard in upsert_row and delete_row
- sync/push.rs: add SYNCABLE_TABLES allowlist guard in push_table
- sync/queue.rs: normalize enqueue timestamp to seconds (was millis, inconsistent with push/pull)

Frontend lint:
- packing-list.tsx: replace setState-in-useEffect anti-pattern with useMemo derivation for mutation error feedback
- event-template-form.tsx: extract splitDateTime/combineDateTime to date-time-utils.ts to fix Fast Refresh violation (non-component exports alongside component)

Tests (31 stale failures fixed):
- Update 5 test files with correct mixed-case text after CSS uppercase refactor
- Rewrite mobile-nav.test.tsx: add useAuth mock, update to 5+1 nav structure, correct icons and routes

* fix(review): resolve PR #33 review findings for chat data layer

Critical fixes (C-1 through C-5):
- Add missing INSERT policy on conversations table (C-1)
- Filter user's own messages from getUnreadCounts in Supabase adapter (C-2)
- Change z.url() to z.string().url() for media attachment URLs (C-3)
- Return Err instead of Ok(()) from SYNCABLE_TABLES allowlist guards (C-4)
- Add missing UPDATE policy on media_attachments table (C-5)

Important fixes (I-2, I-3):
- Add participant check before message insert in send_message (I-2)
- Validate direct conversations require exactly 2 participants (I-3)

Tasks created (S013-S017) for: data mapper tests, dedicated
get_messages_since command, updatedAt mapping fix, unread counts
resilience, and sync guard test coverage.

Suggestions S-1 through S-6 tracked in Ideas backlog.

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

* feat(chat): implement review remediation tasks S013-S017

S013: Add 36 data mapper unit tests for chat entities
  - Conversation, ConversationParticipant, Message, MediaAttachment
  - Round-trip, null handling, and Zod validation tests

S014: Add dedicated get_messages_since Rust command
  - SQL-level WHERE created_at > ? filter replaces client-side filtering
  - Eliminates silent truncation at 1000 message limit

S015: Fix toConversationParticipant updatedAt mapping
  - Use last_read_at ?? joined_at instead of always joined_at

S016: Improve getUnreadCounts error resilience
  - Single conversation failure no longer aborts all counts
  - Returns partial results with console.warn for failed queries

S017: Remove #[cfg(not(test))] from sync allowlist guards
  - Tests now exercise the same allowlist path as production
  - Updated test fixtures to use real SYNCABLE_TABLES entries

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: Supabase Realtime, chat data layer, events & coach assignment (#34)

* docs(planning): add spec, tech, and steps for supabase realtime integration

4-phase blueprint for Step 22 (Supabase Realtime Integration).
Establishes architecture for Broadcast channel management, cursor-based
message pagination, RealtimeManager module, and all chat TanStack Query hooks.

ADR-005: cursor-based getMessages replaces offset-based (no existing callers).

* feat(chat): add Supabase Realtime broadcast channels and chat hooks

Wire up Supabase Realtime Broadcast for live message delivery and typing
indicators. Introduces the RealtimeManager singleton, cursor-based message
pagination, platform-agnostic foreground detection, and all TanStack Query
hooks needed by the Chat UI (Step 23).

New files:
- src/lib/realtime-schemas.ts     -- Zod schemas for broadcast payloads
- src/lib/realtime-manager.ts     -- channel lifecycle, typing state, catch-up
- src/lib/foreground-detector.ts  -- visibilitychange / Tauri focus detection
- src/hooks/use-chat.ts           -- useConversations, useMessages (infinite),
                                     useSendMessage (optimistic), useRealtimeMessages, +6 more
- src/components/chat-realtime-listener.tsx -- app-level cache update listener

Modified:
- data-adapter.ts / supabase-adapter.ts / tauri-adapter.ts / chat.rs
  Replace offset-based getMessages with cursor-based (before?: ISO timestamp)
- main.tsx: mount ChatRealtimeListener alongside SyncListener
- auth.tsx: call resetRealtimeManager() on SIGNED_OUT

Tasks: S001, S002, S003, S004, S005, S006, S007

* fix: resolve QA lint and code quality issues across frontend and Rust

Fix clippy errors in test code (manual_range_contains, approx_constant),
ESLint no-constant-binary-expression in utils test, and three code quality
issues in realtime-manager: validate typing payloads with Zod schema,
replace fragile onTyping callback chain with Set-based listener pattern,
and explicitly discard handleForeground Promise.

* fix(chat): address PR #34 review findings for Supabase Realtime

Multi-listener pattern (addMessageListener/addTypingListener) replacing
single-callback ownership, broadcast try/catch in useSendMessage,
channel lifecycle ownership fix, auth integration via useAuth hook,
and Wave 5 polish: named constants, toMessageFromBroadcast mapper,
simplified clearAllTyping, foreground-detector error logging and
async/await rewrite, broadcastTyping .catch(), MessagePaginationOptions
interface, user_name max(200) constraint.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(chat): complete chat UI with messaging and conversations (#35)

* docs(planning): add spec, tech, and steps for chat UI (Step 23)

* feat(chat): build complete chat UI (Step 23)

Routes, conversation list, virtualized message detail, compose bar,
typing indicators, new conversation flow, group management, and
client-side blocking. Navigation updated -- COMMS replaces Social
on mobile, added after Vault on desktop sidebar with unread badge.

Conversation type extended with participantUserIds so ConversationDetail
can resolve the other user's display name and blocking state for direct
conversations. Supabase adapter fetches participants alongside
conversations; Tauri adapter was already receiving them and now uses them.

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 006-Chat-UI

* docs(planning): add quick plan for conversation participant IDs

Feature: 006-Chat-UI

* fix: update mobile-nav tests for Comms tab and fix Rust formatting

Update test assertions for Social→Comms rename, add useUnreadCounts
mock, and fix rustfmt formatting in event_reminder tests.

* fix(chat): resolve PR #35 review findings

Critical fixes:
- C1: Contact search filter was a no-op; now passes search prop to
  ContactRow which renders null when display name does not match
- C2: Sender name in group bubbles showed raw UUID; resolved via
  useUserProfile inside MessageBubble
- C3: Send failure gave zero feedback; added sendError state and
  inline "Failed to send" indicator on ComposeBar
- C4: Conversation creation failure was silent; added error state
  and inline message in NewConversationSheet
- C5: MessageList had no error/loading state; added spinner and
  error fallback from useMessages isLoading/isError

Important fixes:
- I1: Typing broadcast used user.email; now uses displayName via
  useUserProfile
- I2/I3: useBlockedUsers storage key scoped to userId; added
  console.error/warn on localStorage corruption
- I4: Optimistic message had senderId undefined; now uses current
  user ID from useAuth
- I5: LeaveDialog failure was silent; added inline error message
- I6: ParticipantSheet add failure was silent; shows error including
  Tauri offline message
- I7: Removed rounded-full from avatar containers and unread dots
- I8: Added comment clarifying group threshold (2 selected + server
  adds self = 3 total participants)

Suggestions:
- S3: Removed build-artifact comment from conversation-list.tsx
- S9: Fixed error copy from "pull to retry" to "try again"

Tasks: C1, C2, C3, C4, C5, I1, I2, I3, I4, I5, I6, I7, I8, S3, S9
Feature: 006-Chat-UI

* docs(planning): resolve PR #35 review and add test/polish tasks

Marks 0003-pr35-chat-ui-review.md as fully resolved (5 critical,
8 important, 2 suggestions fixed inline; 6 tasks created; 5
suggestions dismissed with rationale).

Adds Wave 7 tasks to Steps.md:
- S015-T: deriveItems unit tests
- S016-T: useBlockedUsers unit tests
- S017-T: formatTypingText unit tests
- S018-T: relativeTime + getInitials unit tests
- S019: Migrate useBlockedUsers to Zustand store
- S020: Wire archive action in conversation header

Feature: 006-Chat-UI

* refactor(chat): extract utils, add blocked-users store, verify review

Extract pure functions (relativeTime, getInitials, formatTypingText,
deriveItems) into chat-utils.ts and add tests. Migrate useBlockedUsers
to Zustand store with user-scoped persistence. Mark PR #35 review as
verified with follow-up tracked in GitHub issues #43-#51.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: add link preview metadata with OG tags, Twitter Cards, and useMetadata hook (#53)

Add comprehensive link preview support so shared URLs display rich previews
on social platforms and messaging apps. Includes a useMetadata hook for
dynamic per-route title and description updates, used on share pages.

* fix(auth): prevent PKCE code race condition on OAuth callback (#54)

Supabase JS v2.100.1 defaults to detectSessionInUrl: true with PKCE
flow, causing _initialize() to auto-exchange the auth code before the
/auth/callback route handler can. The second exchange attempt fails
(code already consumed), redirecting users to /sign-in?reason=oauth_error.

Disable detectSessionInUrl since we have a dedicated callback route
that handles PKCE code exchange manually.

* fix(auth): enable PKCE flow for OAuth sign-in (#55)

Without explicit flowType: 'pkce', the Supabase client uses implicit
flow, returning tokens as hash fragments (#access_token=...) that the
/auth/callback route cannot read (it expects ?code= query params).

Setting flowType: 'pkce' makes Supabase return a PKCE authorization
code as a query parameter that the callback can exchange for a session.

* fix(db): resolve RLS infinite recursion on group_members and conversation_participants (#56)

The group_members and conversation_participants SELECT policies
self-referenced their own tables, triggering PostgreSQL error 42P17
(infinite recursion). This cascaded 500s to every table whose sharing
policies join group_members (workout_logs, session_templates, etc.).

Creates SECURITY DEFINER helpers (is_group_member, is_group_coach,
is_conversation_participant) that bypass RLS for inner membership
lookups and replaces all six affected policies.

Also removes the PWA manifest (manifest.webmanifest) and its link tag
since Vercel deployment protection returns 401 on the unauthenticated
manifest fetch.

* feat(builder): program builder UI with responsive week grid (#58)

* feat(builder): program builder UI with responsive week grid

Implements the complete program builder and session builder UIs.

Program builder:
- Sidebar layout (desktop) / stacked layout (mobile) at /builder route
- Drag-and-drop block reordering with @dnd-kit
- Week grid defaults to 5-day Mon-Fri view with a toggle to show all 7
  days, improving readability at constrained content widths (~94px/cell
  at 1024px narrowed to nothing legible; 5-day gives ~132px/cell min)
- Session slots reduced from 80px to 56px min-height with type-colored
  left borders (STRENGTH/CONDITIONING/SE/MIXED/EVENT)
- Mobile block editor with vertical day-row tap targets
- Session picker bottom sheet and copy-week dialog
- Program preview overlay with per-session exercise/load tables

Session builder:
- Set scheme editor with per-type form variants
- Activity group editor and session template form

Infrastructure:
- Collapsible shadcn/ui component added
- Removed boilerplate App.tsx/App.css
- Expanded index.css with builder-specific design tokens

* fix(builder): declutter block header with overflow menu

Replace inline action buttons (move up/down, delete) with a dropdown
overflow menu in both desktop and mobile block editors. This prevents
touch target overlap on narrow viewports and isolates the destructive
delete action behind a two-tap interaction.

* feat(builder): guard destructive actions with confirmation and undo

Block and week deletions now require confirmation via AlertDialog,
showing impact ("This will remove N weeks and M sessions.") before
proceeding. Session removal fires immediately but shows a 5-second
undo toast via sonner, letting users recover accidental taps without
blocking the fast path.

- Add alert-dialog.tsx shadcn primitive (Radix AlertDialog)
- Add confirm-delete-dialog.tsx reusable builder component
- Wire block delete in BlockEditor and MobileBlockCard to dialog
- Wire week delete in WeekGrid and MobileWeekSection to dialog
- Add undo toast to SessionSlot and MobileDayRow session removal
- Mount Toaster in main.tsx (bottom-center)

* feat(builder): replace full-screen preview with inline week detail

Remove the disconnected ProgramPreview overlay from the builder flow.
Exercise details now toggle inline per-week via a visibility icon,
keeping the edit-preview feedback loop tight. Save button moves to
the header. Shared formatting helpers extracted to session-detail.tsx.
Also fixes Collapsible asChild bug in block-editor and mobile-block-editor.

* fix(builder): remove interstitial from session template create flow

Tapping "Create new template" in the session picker now opens the
inline form immediately, skipping the Library recommendation callout.
The Library option is preserved as a subtle link ("Want more options?
Create in the Library") below the form for users who need it.

Removes showCreateCallout state and the two-step reveal pattern.

* refactor(builder): consolidate session constants and polish UI details

- Extract SESSION_TINT and SESSION_TYPE_BADGE from session-slot,
  mobile-block-editor, and program-preview into constants.ts
- Remove duplicate @keyframes block-enter and slot-fill in index.css
- Bump section-level labels (WEEK N, day columns, EXERCISE/SETS/LOAD)
  from text-[11px] font-medium to text-xs font-semibold for visual
  hierarchy over metadata badges which stay at text-[11px] font-medium
- Upgrade session picker SheetTitle to text-sm font-display
- Add empty state copy when zero blocks exist in BlockList and
  MobileBlockEditor
- Clarify copy-week dialog: description now states sessions will be
  replaced, and per-week "(N existing)" label gains warning colour

* feat(builder): replace collapsed save error with structured inline validation

Validation errors now appear at the relevant field rather than as a single
joined string below the save button. Program name errors render under the
name input, block name and week errors render on each block card (visible
even when collapsed), and server/auth errors remain global in the header.

* feat(builder): add recently-used templates and reduce week grid noise

Surface the 5 most recently assigned session templates at the top of the
picker sheet so users with large template libraries reach common picks
faster. Adds lastAssignedAt tracking across both Tauri/SQLite and
Supabase backends.

Reduce visual noise in the week grid: restyle empty slots as muted
"Rest" labels instead of prominent calls-to-action, bring the 5/7-day
toggle to mobile, and collapse identical weeks within a block to a
compact summary row (with manual expand).

* refactor(builder): extract session-detail helpers to fix Fast Refresh lint errors

* feat: display broadcast infrastructure (Step 27) (#59)

* feat(chat): video, image, and file sharing (#57)

feat(chat): video, image, and file sharing (Feature 007)

Full-stack media sharing: Cloudflare Stream video uploads with TUS, image/file uploads via Supabase Storage, real-time transcoding status via webhook, signed URL caching, and inline media rendering in chat.

Review: 4 critical + 10 important findings resolved and verified. 12 enhancements implemented. 1,305 tests passing.

* docs: add plan for Step 27 -- display broadcast infrastructure

Spec.md, Tech.md, and Steps.md for Feature 008. Covers DisplaySnapshot
Zod schema, publisher module, store integration, display_visible toggle,
and Supabase Realtime Broadcast channel plumbing. 14 tasks across 4 waves
with 3 agents.

* feat: add display_visible column to user profiles

SQLite and Supabase migrations for display broadcast toggle,
Tauri model and command updates to support the new field.

* feat: add DisplaySnapshot domain type and display publisher

DisplaySnapshot type with Zod schema, display-publisher for
broadcasting workout state, data-mapper and store updates.

* feat: add PushToDisplayButton and display broadcast hook

useDisplayBroadcast hook, PushToDisplayButton component,
workout log and profile route integrations.

* test: add display snapshot and publisher tests

Unit tests for DisplaySnapshot schema, display-publisher,
display-snapshot builder, and updated data-mapper/adapter tests.

* docs: update Steps.md and implementation plan for display broadcast

* fix(review): resolve PR #59 review findings

Fixes: C1 (RLS auth), C2 (image signed URLs), C3 (snapshot try-catch),
C4 (Cloudflare subdomain), I1-I9 (error handling, schema tightening,
z.url() revert, publisher readiness), M1/M3/M5/M6 (env guards,
request parsing, query error logging, JSDoc)
Tasks: S024-T, S025-T added for media-provider and timeout tests
Comments: 9 comment issues resolved

Review: Context/Reviews/0005-pr59-display-broadcast-review.md

* test(media): add unit tests for CloudflareStreamProvider and transcoding timeout

S024-T: media-provider tests covering signed URL TTL cache hit/miss,
null client error, Zod validation of malformed responses, and cache
deduplication (13 tests).

S025-T: use-media-upload tests covering 5-minute transcoding timeout
marks processing attachment as failed, unaffected ready attachments,
query cache invalidation, and timer cleanup on unmount (4 tests).

Tasks: S024-T, S025-T
Feature: 007-video-image-sharing

* docs(review): mark PR #59 review as verified

* feat(edge-functions): deploy CF video edge functions with Vault secrets and local JWT signing

Deploys three Supabase Edge Functions for Cloudflare Stream video support
(Step 25). All Cloudflare credentials read from Supabase Vault using
CF_* secret names (CF_ACCOUNT_ID, CF_STREAM_TOKEN, CF_WEBHOOK_SECRET,
CF_SIGNING_KEY_PEM, CF_SIGNING_KEY_ID, CF_CUSTOMER_SUBDOMAIN).

chat-media-signed-url: replaces Cloudflare token API round-trip with
local RS256 JWT signing via CF_SIGNING_KEY_PEM + CF_SIGNING_KEY_ID.
No outbound HTTP call needed per playback request.
chat-media-upload-url: reads CF_ACCOUNT_ID + CF_STREAM_TOKEN from Vault.
chat-media-webhook: reads CF_WEBHOOK_SECRET + CF_ACCOUNT_ID from Vault.

Adds get_secret() SQL migration granting service_role-only read access
to vault.decrypted_secrets. Fixes duplicate migration timestamp
20260403000002 -> 20260403000003 for session_template_last_assigned.

Updates mock-supabase test util to support keyed RPC results
(get_secret:SECRET_NAME) for parameter-specific vault mocking.
Removes CLOUDFLARE_ACCOUNT_ID from STANDARD_ENV test fixture.

* chore: update app icons and branding assets

Replace default Tauri icons with Ardent Forge branding across all
platforms (Android, iOS, Windows, macOS). Update app display name
in Android strings.xml and tauri.conf.json.

* fix(db): rename migration 008 and auto-recreate DB on checksum mismatch

Rename session_template_last_assigned to migration 009 to make room
for 008_add_display_visible. Add debug-only auto-recreate logic so
migration checksum mismatches during development delete and rebuild
the local database instead of crashing.

* fix(auth): use Chrome Custom Tabs for Google OAuth on Android

Replace window.open with tauri-plugin-opener's inAppBrowser mode to
launch Google OAuth in Chrome Custom Tabs instead of an embedded
WebView. Google blocks OAuth in WebViews under their "Use secure
browsers" policy. Add opener plugin registration and URL scope
permissions for mobile capability.

* chore: update Claude agent configs and mise tooling

* feat(display): display route with board and focused views (#60)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* fix(display): address QA findings from PR #60 review (#61)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* feat(discovery): add discovery client and server URL flow (#63)

* docs(planning): add Feature 009 plan for discovery client + server URL

Creates the full 4-phase planning artifacts for Refactor A of the onboarding
redesign: server URL input with auto-discovery replaces raw Supabase credential
entry as the primary setup flow.

Tasks: S001, S001-T, S002, S003, S003-T, S004, S004-D
Feature: 009-discovery-client-server-url
ADR-006

* feat(setup): add discovery client and server URL flow

Replace raw Supabase credential inputs on the setup screen with a
primary server address field that auto-discovers backend credentials
via GET /.well-known/ardent-forge.json. Manual Supabase URL and key
entry moves into a collapsible "Manual configuration" section as a
fallback.

- Add discoverInstance() in src/lib/discovery.ts with URL normalization,
  8-second AbortController timeout, and typed DiscoveryResult discriminated
  union (NETWORK_ERROR, NOT_FOUND, INVALID_RESPONSE)
- Add 11 Vitest unit tests covering happy path, URL normalization variants,
  and all error branches
- Refactor setup.tsx: server URL primary input, discovery + validation
  state machine, "or" divider, collapsible manual section, disabled QR
  placeholder for future Refactor B
- Add /.well-known/ardent-forge.json handle block to Caddyfile with
  {$SITE_URL} and {$ANON_KEY} env var interpolation
- Add ANON_KEY to caddy service environment in docker-compose.yml
- Document discovery endpoint for static host and Docker deployments
  in docs/self-hosting.md; update mobile app configuration section

Tasks: S001, S001-T, S002, S003, S003-T, S004
Feature: 009-discovery-client-server-url
ADR: ADR-006

* docs(planning): mark Feature 009 Steps.md as complete

* fix(discovery): harden URL validation, use Zod, deduplicate connect logic

- Validate URL protocol via new URL() parse; reject non-http/https schemes
  (file://, ftp://, etc.) before any fetch is made
- Replace manual typeof field checks with a Zod schema (DiscoverySchema)
  to align with project convention and improve type safety
- Extract shared validateAndSave() helper in setup.tsx, eliminating
  duplicated validate-save-init-navigate block between handleConnect
  and handleDiscoverAndConnect
- Add 4 new tests: file:// rejection, ftp:// rejection, empty string,
  whitespace-only input

Feature: 009-discovery-client-server-url

* fix(review): resolve PR review findings for discovery feature

Fixes (P6-001..008): add logging to all error paths, capture error
params in catch blocks, correct error type semantics (INVALID_INPUT
for validation, INVALID_RESPONSE for JSON parse), fix JSDoc, add
aria-expanded to disclosure toggle, add double-submission guard,
fix comment scope.

Refactor: unify 4 parallel state variables into a single SetupState
discriminated union with 7 phases, eliminating impossible states.
Also wraps handlers in try-catch (P6-009..011) and fixes success
state ordering as part of the refactor.

Rules: create .claude/rules/error-handling.md codifying error type
semantics and catch block conventions (P6-017, P6-018).

Tasks: S005..S007-T added to Steps.md for schema tightening and
test coverage gaps (P6-012..015).

Review: Context/Reviews/0006-discovery-client-server-url-2026-04-04.md

* fix(discovery): tighten schema validators and add missing test coverage

S005: Use z.string().url() for supabase_url, z.string().min(1) for
supabase_publishable_key and version in DiscoverySchema. Validation
is now strictest at the system boundary.

S005-T: Add test for missing supabase_publishable_key field.
S006-T: Add test for 500 HTTP response (non-404 error code).
S007-T: Add test for URL with path component being stripped.

Review: P6-012, P6-013, P6-014, P6-015

* docs(review): mark discovery feature review as verified

* feat(display): idle mode with Edge Function broadcast (#62)

* docs(planning): add feature 009 idle mode spec, tech plan, and steps

Full 4-phase blueprint for Step 29 (Idle Mode + Edge Function).

- Spec.md: 8 must-haves, 17 testable assertions, all open questions
  resolved (24h clock default, /display?clock=12h override, max 3
  sessions, silently hide schedule when Edge Function unreachable)
- Tech.md: 8 architecture decisions -- idle_snapshot event type
  extension, Realtime HTTP broadcast API, Supabase cron schedule,
  program hierarchy JOIN query, server_time drift correction, CSS-only
  transitions, TanStack Router search param validation
- Steps.md: 10 tasks across 4 phases, frontend-specialist +
  backend-engineer + quality-engineer, parallel Phase 1 (types + Edge
  Function) and Phase 2 (ClockDisplay + IdleView + transition wrapper)

* feat(display): implement idle mode with Edge Function broadcast

Adds idle mode to the /display route -- the ambient state shown when no
workouts are active. A server-side Edge Function queries today's remaining
scheduled sessions every 60 seconds and broadcasts them to the display
channel so the TV always shows useful information.

- IdleSnapshot Zod schema + idle_snapshot event type in DisplayEventType
- ClockDisplay component (8rem Space Grotesk, server-time drift correction)
- IdleView component (clock, date, sessions list, next-up badge, footer)
- useDisplayMode hook deriving idle/board/focused from session map
- useIdleSnapshot hook subscribing to display Broadcast channel
- DisplayModeTransition wrapper (300ms fade for idle↔board, 400ms zoom for board↔focused)
- /display route composed end-to-end with all hooks and components
- display-idle-snapshot Edge Function with cron every 60 seconds
- get_display_idle_sessions RPC (service_role only, filters completed sessions)
- /display bypasses config guard in __root.tsx (public, no-auth route)
- docs/self-hosting.md: Remote Display section

Tasks: S001, S001-T, S002, S003, S003-T, S004, S005, S005-T, S006, S006-T, S007, S008, S009, S009-D
Feature: 009-idle-mode

* docs(planning): mark feature 009 idle mode as complete

Feature: 009-idle-mode

* fix(display): address QA findings from idle mode review

Frontend:
- Refactor useDisplayMode to use setState-during-render pattern (React
  "storing info from previous renders") instead of refs-in-useMemo,
  fixing react-hooks/refs ESLint violations and the ref-as-dep stability bug
- Add NaN guard and 60s periodic re-sync to ClockDisplay offset to handle
  NTP corrections and DST transitions on long-running displays
- Wire real Realtime channel status to connectionStatus in display route,
  replacing the hardcoded 'connected' placeholder
- Fix useIdleSnapshot cleanup to safely void removeChannel promise; add
  dev-mode console.warn on safeParse failures for easier debugging
- Fix IdleView session key to use display_name+session_name instead of
  array index
- Remove redundant cleanup() calls in display test afterEach blocks

Backend:
- Add day_of_week IS NOT NULL guard to get_display_idle_sessions RPC to
  prevent silent exclusion of sessions with unset day_of_week
- Add composite index idx_workout_logs_session_user_date to prevent full
  table scan on the cron-driven workout_logs LEFT JOIN
- Replace CREATE OR REPLACE with DROP IF EXISTS + CREATE for atomic
  GRANT application
- Remove non-null assertions on sessions[0] in Edge Function
- Add LIMIT 3 comment and verify_jwt rationale comment in config
- Improve Edge Function error messages to include error codes and response
  status for easier production debugging

Feature: 009-idle-mode

* fix(display): resolve PR review findings from 0006-idle-mode-review

Fixes: clock drift bug (P6-001), null client silence (P6-002),
lying connection status (P6-003), broad path bypass (P6-004),
duplicate channel (P6-005), missing CLOSED handling (P6-006),
production-silent parse errors (P6-007), duplicate DisplayMode type
(P6-008), unsafe RPC cast (P6-009), redundant comments (P6-010)

Tasks: S011-S014 added to Steps.md for missing test coverage
Rules: .min(1) on display-facing strings, Tech.md D4 corrected

Review: Context/Reviews/0006-idle-mode-review.md

* test(display): add review remediation tests S011-S014

S011: useIdleSnapshot hook tests (9 tests) -- valid/invalid payload,
null client, cleanup, connection status callback for all channel states
S012: DisplayModeTransition tests (8 tests) -- transition class mapping
for all mode pairs including duration assertions
S013: IdleSnapshot schema edge cases (7 tests) -- invalid session_type,
empty arrays, invalid ISO dates, .min(1) enforcement
S014: ClockDisplay correction test strengthened to assert exact time value

Review: Context/Reviews/0006-idle-mode-review.md

* docs(reviews): mark 0006-idle-mode-review as verified

All 16 findings confirmed resolved by review-verify agent.

Feature: 009-idle-mode

* fix(display): add onIdleSnapshot to subscriber test mock handlers

Fixes TS build error from merge integration -- the test's mock handlers
were missing the new onIdleSnapshot callback added to DisplayEventHandlers.

* chore(harness): update skills, hooks, and Claude Code settings

* docs: update hosting PRD and add onboarding refactoring plans

* feat(onboarding): QR code gen + scan for server setup (#64)

* docs(planning): add Feature 010 QR code gen + scan blueprint

Spec, Tech, and Steps for Refactor B of the onboarding redesign
(QR code generation in Settings + scan/paste on setup screen).
Covers invite link format, qrcode.react config, barcode scanner
plugin setup, and isTauri()-based platform branching. Updates
onboarding-refactoring-plans.md with researched plugin specifics.

Feature: 010-qr-code-gen-scan

* feat(onboarding): add QR code server sharing and scan on setup

Adds QR-based server sharing to reduce friction for onboarding new
users to a self-hosted Ardent Forge instance.

- Settings backend section now shows a "Share this server" area with
  a QRCodeSVG encoding `ardentforge://connect?url=...&key=...` and a
  "Copy invite link" button, guarded by hasConfig()
- Setup screen QR button activates native camera scan via
  @tauri-apps/plugin-barcode-scanner (Tauri/Android) or toggles an
  inline paste field (browser); both paths parse the invite link and
  auto-trigger connection validation
- New src/lib/invite-link.ts provides buildInviteLink() and
  parseInviteLink() as a pure shared utility
- Barcode scanner plugin registered under #[cfg(mobile)] and
  barcode-scanner:default added to mobile capabilities

Tasks: S001, S002, S003, S004, S005, S006
Feature: 010-qr-code-gen-scan

* fix(qr-scan): address QA findings from code review

- Add .catch() to unawaited processInviteLink() calls in paste/keydown
  handlers to prevent silent unhandled promise rejections
- Replace duplicate dynamic import of cancel with cancelRef to avoid
  redundant module resolution on scan overlay dismiss
- Change perms === 'denied' guard to perms !== 'granted' for defensive
  permission handling on non-standard permission states
- Move tauri-plugin-barcode-scanner to target-specific mobile dep in
  Cargo.toml to avoid enlarging desktop binary
- Remove unreachable pathname fallback branch from parseInviteLink
- Add comment to buildInviteLink documenting publishable key intent
- Rename misleading test and add true malformed-URL test coverage
- Apply rustfmt formatting to db.rs

Feature: 010-qr-code-gen-scan

* fix(qr-scan): resolve P7 review findings and add component tests

Fix bare catch blocks in handleCopy and connection-validator, add
try/catch to processInviteLink to prevent UI getting stuck, handle
unhandled promise from handleScan onClick, add user-facing error
toasts for clipboard failures, fix buildInviteLink to use SCHEME/HOST
constants, downgrade parse error log to silent null return, add
aria-label to paste field, improve QR scan failure toast message.

Add component tests for processInviteLink valid/invalid flows and
backend-settings share section conditional rendering and copy button.

Tasks: S006-T1, S006-T2
Feature: 010-qr-code-gen-scan
Review: Context/Reviews/0007-pr64-qr-code-gen-scan-review.md

* docs(planning): resolve P7 review and add S006-T1/T2 tasks to Steps.md

Mark all 14 findings in the QR code gen/scan review as resolved.
Add S006-T1 (processInviteLink component tests) and S006-T2
(backend-settings share section tests) to Feature 010 Steps.md.

Feature: 010-qr-code-gen-scan

* docs(review): mark P7 review verified by review-verify agent

* refactor(review-resolve): simplify bulk triage to bulk accept

Renames bulk triage to bulk accept to better reflect the workflow --
the capture category already IS the suggested action. Removes the
requirement for a reason on discard (individual and bulk).

* chore(harness): add gitignore entries and reformat hooks

Adds .claude/backups/, .claude/worktrees/, .serena/, and
hooks/skill-activation/.state/ to .gitignore. Reformats all hook
TypeScript files to double-quote style for consistency.

* feat(builders): progressive disclosure and contextual help (Step 14.5)

* docs(planning): add Step 14.5 feature plan and user flow docs

Documents the progressive disclosure and contextual help plan
for the template builder and program builder (Step 14.5).
Adds Flow 7 builder progressive disclosure and contextual help
system diagrams to 10-user-flows.md.

Feature: 010-Progressive-Disclosure
Tasks: S001–S014-T

* feat(builders): progressive disclosure and contextual help (Step 14.5)

Adds context-aware field visibility, staged group creation, set scheme
filtering, and inline help to the template and program builders.

New primitives and utilities:
- HelpTrigger component (Popover desktop / Drawer mobile via useMediaQuery)
- shadcn Popover and Drawer with Iron & Ember styling
- useMediaQuery hook (SSR-safe, listener cleanup)
- CollapsedFieldsRow for expandable hidden field groups
- visibility-maps.ts: data-driven lookup tables for all field visibility rules
- help-content.ts: GROUP_TYPE_HELP, BLOCK_TYPE_HELP, SOURCE_HELP constants
- text-label-large and text-body-small typography utilities in index.css

Template builder changes:
- Category-aware Scoring/Time Cap visibility with CollapsedFieldsRow
- Activity group staged reveal: Stage 1 (type selector only), Stage 2 (type-
  dependent rest fields + exercises)
- Set scheme type list filtered by session category; "Show all types" escape
- Load type options filtered by set scheme type; null = hide entire section
- Session picker gains "Create new event" inline creation flow

Program builder changes:
- Block type HelpTrigger with one-liner summary (desktop + mobile)
- Source badge HelpTrigger with all 7 sources

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013
Feature: 010-Progressive-Disclosure

* fix(builders): resolve QA findings from progressive disclosure review

Fixes two blocking hook violations and seven warnings across the builder
components introduced in Step 14.5.

Errors fixed:
- Move useCallback above early return in LoadSpecEditor to satisfy
  Rules of Hooks (conditional hook call)
- Rewrite useMediaQuery with useSyncExternalStore to eliminate
  synchronous setState in effect and double render

Warnings fixed:
- Remove eslint-disable suppression in LoadSpecEditor; expand useEffect
  deps to [allowedLoads, availableTypes, value.type, onChange]
- Replace div[role=button] nested inside <button> in MobileDayRow with
  sibling <button> elements; fix event type to MouseEvent | KeyboardEvent
- Add useId/aria-controls pairing to CollapsedFieldsRow for screen
  reader disclosure association
- Dynamic aria-label on HelpTrigger (Help: ${title} instead of static)
- Extract BLOCK_TYPE_STYLES to constants.ts; remove duplication across
  block-editor and mobile-block-editor
- Remove unused placement prop from HelpTrigger and all four call sites
- Fix PopoverTitle to render <h2> matching its ComponentProps<'h2'> type

Suggestions addressed:
- Sync ladderText in DescendingRepsFields when repLadder resets externally
- Use touchLastAssigned.mutate (stable ref) in useCallback deps
- Add clientId to ActivityGroupData and ActivityData; generate via
  crypto.randomUUID() at creation; use as React key instead of index
- Add aria-expanded/aria-controls to ProgramForm details toggle

Feature: 010-Progressive-Disclosure

* fix(builders): resolve PR review findings P7

Fixes: P7-001 (silent userId guard), P7-002 (JSX triplication),
P7-003 (redundant satisfies), P7-004 (unstable useEffect deps),
P7-005 (block type help duplication), P7-007 (missing JSDoc),
P7-008 (HelpEntry interface), P7-009 (non-empty tuple),
P7-010 (scheme map typing), P7-011 (constants typing)
Rules: P7-013, P7-014 added to typescript-conventions.md;
P7-015 added to error-handling.md
Deferred: P7-012 (ErrorBoundary strategy) to backlog
Discarded: P7-006 (superseded by P7-002 extraction)

Review: Context/Reviews/0007-pr65-progressive-disclosure-review.md

* fix(builders): resolve review-verify type errors and mark review passed

Fix 3 pre-existing TS errors from P7 review fixes: collapsedLabels
cast to non-empty tuple, allowedLoads.includes widened for
LoadSpec['type'], defaultTypes.includes widened for SetSchemeType.
Review file verified and checkbox marked.

* feat: implement Android CI/CD pipeline (Feature 011) (#67)

* docs(ci-cd): add full blueprint for Android CI/CD pipeline

4-phase planning for feature 011: GitHub Actions workflows covering PR
validation (lint, typecheck, unit tests, E2E via Playwright + local
Supabase, Android debug APK build), tagged release pipeline (signed AAB,
Supabase migrations gate, GitHub Release, Play Store internal track
upload), and supporting setup documentation.

Feature: 011-ci-cd-pipeline
ADR-007: GitHub Actions chosen as CI/CD platform

* ci(github-actions): implement complete Android CI/CD pipeline

Adds two GitHub Actions workflows from scratch:

- ci.yml: PR validation with three parallel jobs -- lint/typecheck/unit
  tests, Playwright E2E against local Supabase, and Android debug APK
  build with full Rust/NDK toolchain and caching
- release.yml: tag-triggered release pipeline -- signed AAB build and
  Supabase migrations run in parallel, gated publish job creates GitHub
  Release and uploads to Play Store internal track

Supporting changes:
- Add Playwright (@playwright/test) with smoke tests and test:e2e script
- Add signingConfigs block to build.gradle.kts reading from env vars
- Add .github/BRANCH_PROTECTION.md and .github/README.md
- Add CI/Release workflow badges to README.md

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009
Feature: 011-ci-cd-pipeline

* fix(android-ci): harden pipeline per PR #67 review findings

- Guard signingConfig assignment with keystorePath null check
- Validate KEYSTORE_BASE64 before decode; check decoded file size
- Pass all secrets through env: blocks (not inline shell expansion)
- Add secret validation steps to build-release, migrate, publish jobs
- Reorder publish job: Play Store upload before GitHub Release
- Capture Supabase status once; validate non-empty/non-null values
- Add if-no-files-found: ignore to Playwright report upload
- Fix test:e2e script to use bunx playwright test
- Add webServer timeout: 30_000, stdout/stderr: pipe to playwright.config.ts

Tasks: P8-001, P8-002, P8-003, P8-004, P8-005, P8-006, P8-007, P8-008, P8-015, P8-016

* docs(ci-cd): apply post-review documentation corrections

- Tech.md: Four->Two workflows, remove armv7 target, fix port 4173,
  update signing snippet to null-guard pattern
- Steps.md: add VITE_ prefix to env var names; add S010 secret validation
- ADR-007: status Proposed->Accepted
- Add PR #67 review capture (16/16 findings resolved and verified)
- Update review-resolve skill

Tasks: P8-009, P8-010, P8-011, P8-012, P8-013, P8-014, P8-015

* feat(deep-link): handle ardentforge://connect deep links (#66)

* docs(planning): add Feature 011 deep link handler planning artifacts

Full 4-phase plan for ardentforge://connect deep link handling.
Covers Tauri dispatch extension, browser /connect route, setup
screen pre-population, and transient store for settings handoff.

Feature: 011-deep-link-handler
ADR-007: transient-store-deep-link-state

* feat(deep-link): handle ardentforge://connect deep links

Implements the connect deep link handler so tapping an invite link
opens the app directly to the correct screen with credentials
pre-populated.

- Register ardentforge://connect host in Tauri deep-link config
- Add handleConnectLink() with 4-way branch: invalid, unconfigured,
  same instance, different instance
- Dispatch connect vs auth in onOpenUrl listener (OAuth flow untouched)
- Add /connect browser route as fallback for web users
- Exempt /connect from root route guard
- Setup screen accepts url/key search params for auto-population
- BackendSettings consumes pending connect Zustand store on mount
- Unit tests for all 5 handleConnectLink() branches (5/5 passing)

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010
Feature: 011-deep-link-handler
ADR: ADR-007 (transient Zustand store for cross-route deep link state)

* fix(deep-link): address QA findings from review

- Add desktop deep-link scheme config + permissions to default capability
- Fix bare catch blocks in invite-link.ts and auth.tsx (error-handling convention)
- Refactor setState-in-effect in backend-settings.tsx to useRef guard
- Add HTTPS validation for url param in parseInviteLink
- Accept navigate callback in handleConnectLink (avoids full-page reload in router context)
- Update OAuth tests to mock @tauri-apps/plugin-opener instead of window.open
- Add test for getConfig() returning null with hasConfig true
- Add test for non-HTTPS url param rejection
- Fix setup.test.tsx mock to provide Route.useSearch()

Feature: 011-deep-link-handler

* fix(deep-link): address PR-66 review findings

- Wrap handleConnectLink in try-catch with toast on error (P8-001)
- Isolate connect-link error path from OAuth error handler in
  auth.tsx (P8-002)
- Replace render-phase setState with useRef + useState initializers
  in backend-settings.tsx (P8-003)
- Replace unsafe `as string` casts with typeof guards in
  validateSearch for /connect and /setup routes (P8-004)
- Add toast feedback when auto-validate fails in /setup (P8-005)
- Replace startsWith check with new URL() + protocol/hostname
  validation in parseInviteLink (P8-006)
- Fix bare catch blocks in config-store.ts to capture and log
  errors (P8-007)
- Add store-boundary validation to usePendingConnect.setPending
  (P8-009)
- Add state-management.md coding rule for Zustand conventions
- Add review capture with verification (0008-pr66)
- Add test step S011 to Steps.md (P8-008)

* test(deep-link): add /connect route and auth connect-dispatch tests

- Add connect.test.tsx covering search param validation, pending
  store integration, and redirect behavior
- Update auth-oauth tests for connect deep-link error isolation

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* fix(ci): upgrade supabase/setup-cli from v1 to v2

v1 runs on deprecated Node 20 and installs an older CLI that rejects
newer config keys (oauth_server, web3, cron, etc.), breaking the
migrate job on tagged releases.

* fix(ci): pin supabase CLI to v2.84.4 to match local version

setup-cli@v1 defaults to CLI v2.20.3 which rejects newer config keys.
Pinning to 2.84.4 matches local dev environment. setup-cli v2 is
merged but not yet tagged as a release.

* fix(supabase): remove invalid cron key from config.toml

The [functions.display-idle-snapshot.cron] section is not a valid
config.toml option. Edge function cron scheduling is configured via
pg_cron in SQL, not in config.toml. This was causing supabase link
to fail in CI.

* fix(sync): add HTTP status check and empty update_set guard in pull path (#70)

* fix(sync): add HTTP status check and empty update_set guard in pull path

pull_table deserialized Supabase responses without checking the HTTP
status code, which could silently swallow 4xx/5xx errors or produce
confusing deserialization failures. Added a status check matching the
pattern already used in push.rs.

upsert_row produced invalid SQL ("... ON CONFLICT(id) DO UPDATE SET ")
when the only matching column was `id`. Now falls back to INSERT OR
IGNORE for that edge case.

Closes #68, closes #69

* fix(lint): resolve unused var, explicit any, and unused import errors

- Prefix unused catch parameter with underscore in formatter hook
- Add eslint-disable for necessary `any` cast in test helper
- Remove unused `buildRequest` import from webhook test

* fix(lint): use bare catch block for unused error parameter

* fix(data-mapper): handle JSONB columns already parsed by Supabase PostgREST

Supabase returns JSONB columns as objects, not strings. Calling JSON.parse()
on an object stringifies it to "[object Object]" first, causing a parse error
on session_templates fields (rest_between_groups, time_cap, event_metadata),
activity_groups fields (rest_between_rounds, rest_between_activities), and
activities (set_scheme).

Adds parseJsonOrValue() helper that passes objects through directly and only
calls JSON.parse() on string values, keeping backward compatibility with the
Tauri/SQLite adapter which returns these columns as JSON strings.

* chore: batch enhancement issues cleanup (#71)

* chore: add Zod refinements to chat domain types

Add validation refinements that catch invalid data at parse time:
- conversation.ts: prevent groupId on direct conversations, enforce leftAt > joinedAt
- message.ts: prevent senderId on system messages
- media.ts: add .nonnegative() to durationSeconds and fileSizeBytes

Closes #40, closes #47

* chore: polish realtime manager (simplify clearAllTyping, fix silent catch)

- Simplify clearAllTyping to delegate to clearTypingForConversation helper
- Add error logging to broadcastTyping .catch per error-handling conventions

Closes #42, closes #49

* feat(events): wire EventCountdownBadge navigation to dedicated event route

- Create /events/$templateId route with minimal event detail page
- Update EventCountdownBadge to navigate to new route instead of /library
- Remove _templateId unused-var rename and TODO comment

Closes #31

* perf: batch getUnreadCounts queries with Promise.allSettled

Replace sequential N+1 COUNT queries with concurrent execution via
Promise.allSettled, preserving partial-results error handling.

Closes #39, closes #46

* perf: optimize chat query patterns (CH-2 trigger, findDirectConversation)

- Add error context with [tauri-adapter] prefix to findDirectConversation loop
- Create materialized direct_conversation_pairs lookup table with unique index
- Replace O(N) trigger scan with O(1) index-backed uniqueness check
- Add cleanup trigger for participant departures

Closes #41, closes #48

* docs: add quick plan for enhancement issues batch

* fix(review): resolve PR #71 review findings

Fixes (7): RLS on direct_conversation_pairs, ISO datetime comparison,
findDirectConversation all-fail throw, getUnreadCounts error enrichment,
comment ac…
chore(release): v1.0.0-beta.10

F016: Public visibility -- discover, publish, and clone programs
F017: Program time travel -- edit start date, jump position
fix: Delete week button disabled when block has one week
fix: Resolve email to UUID before inserting connection request
* fix: add Authorization header to Supabase reachability check

The PostgREST root endpoint requires both `apikey` and `Authorization`
headers. Missing the latter caused a 401 during setup, showing
"Cannot reach server" even with valid credentials.

* fix: auto-validate and surface env var connection failures on setup page

When VITE_SUPABASE_URL and VITE_SUPABASE_PUB_KEY are set but invalid,
the setup page now auto-validates on mount and shows the specific error
(e.g. "Server responded with 401") instead of silently presenting a
blank form. Also adds a banner explaining that environment variables
were detected but failed.

* fix: use Supabase client for connection validation instead of raw fetch

Raw fetch with apikey/Authorization headers only works with legacy anon
keys. New publishable keys (sb_publishable_xxx) require the Supabase
client library which handles auth headers internally. This switch
supports both key formats transparently.

* fix: resolve duplicate migration timestamp collision

Two migrations shared timestamp 20260329000001, causing a primary key
violation in supabase_migrations. Renumbered create_sharing_tables to
20260329000002 and expand_rls_for_sharing to 20260329000003.

* chore: update Claude Code configuration and skills

* feat: complete sync pull path with dynamic upsert and force-pull (Step 9) (#28)

Implement the stubbed pull/upsert path so remote changes from Supabase
are actually written to local SQLite, completing bidirectional sync.

- Fix broken timestamp parsing in upsert_row (ISO 8601 strings were
  silently parsed as 0, causing all remote updates to be discarded)
- Implement dynamic column upsert via pragma_table_info with type
  coercion (timestamps, booleans, JSONB) between Supabase and SQLite
- Add pull_table/pull_all for paginated force-pull via Supabase REST API
- Wire sync_force_pull command with proper state transitions
- Add auto-pull on first sign-in (detect last_pull_at == 0)
- Add 28 new tests (49 total), all passing

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: events & packing lists (Step 13.5) (#30)

* feat(events): add events & packing lists feature (Step 13.5)

Introduces EVENT as a new session category with full support for event
metadata (date, location, URL, requirements) and a categorized packing
checklist with drag-and-drop reorder, optimistic toggle, progress
tracking, and countdown notifications.

Database:
- Migration adds EVENT to category CHECK constraints on session_templates
  and scheduled_sessions; adds event_metadata JSONB to session_templates
  and workout_logs; creates event_items table with polymorphic FK
  (template XOR log), RLS policies, partial indices, and updated_at trigger

Domain:
- eventRequirementSchema, eventMetadataSchema, eventItemSchema (Zod)
- EVENT added to SessionType enum; eventMetadata optional on templates
  and logs; EventItemRow added to database.types.ts

Data layer:
- toEventItem/fromEventItem mappers; eventMetadata parsing in
  toSessionTemplate (JSON.parse) and toWorkoutLog (direct JSONB)
- DataAdapter interface: getEventItems, saveEventItem, updateEventItem,
  deleteEventItem, toggleEventItemPacked (accepts target bool, no race),
  reorderEventItems (Promise.all), cloneSessionTemplate (isPacked reset)
- SupabaseAdapter implements all methods; getSessionTemplateFull fetches
  eventItems for EVENT templates; updateSessionTemplateFull uses 3-way
  reconciliation to preserve item UUIDs and isPacked state
- TauriAdapter stubs (event support deferred, W-8)

Hooks:
- useEventItems, useCreateEventItem, useUpdateEventItem,
  useDeleteEventItem, useToggleEventItemPacked (optimistic),
  useReorderEventItems (optimistic), useNextUpcomingEvent

UI:
- event-builder/: EventTemplateForm (Sheet overlay, create/edit),
  EventDetail (metadata + map link + packing), PackingList (DnD,
  collapsible categories, error feedback), PackingItem (checkbox toggle),
  EventProgressBar (ember on surface-steel, ARIA progressbar),
  EventCard (library), EventCountdownBadge (Today screen, TODAY edge
  case), RequirementEditor, EventItemEditor, shared styles.ts
- Library: EventCard for EVENT templates, NEW EVENT button
- log/$workoutId: interactive EventDetail for event logs
- history/$workoutId: read-only EventDetail for event history
- Today screen: countdown badge when next event within 30 days
- Program builder: flag icon + ember accent for EVENT sessions,
  event date displayed in program-preview grid and detail sections

Native (Rust):
- event_reminders channel registered in notification.rs
- EventReminderState: background Tokio task (60s poll), checks
  configured intervals (7d/3d/1d), quiet hours, packing progress in
  notification body, deterministic notification ID
- schedule_event_reminders / cancel_event_reminders Tauri commands
- Clippy fixes: RemindedState type alias, needless_borrow in lib.rs,
  manual_range_contains in session_reminder.rs
- Lock contention fix: single mutex guard across event loop in tick()

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 002-events-packing-lists

* docs(planning): mark 002-events-packing-lists complete

Updates Steps.md status to Complete, session.md with full wave
execution log and fixes applied, and implementation-plan.md.

Feature: 002-events-packing-lists

* chore(rust): apply cargo fmt across codebase

* chore: add mise config and cortex hooks

* fix(events): address PR #30 review findings

- Fix timezone bug in combineDateTime: store local date strings instead
  of converting to UTC, preventing event dates from shifting by a day
- Add eventUrl validation in form validate() before save
- Switch sequential item persistence to Promise.allSettled with error
  collection for partial-failure reporting
- Fix DST-unsafe daysUntil calculation using date-only comparison
- Make TauriAdapter getEventItems throw consistently with write stubs
- Add "TODAY" special case in Rust event reminder notification title
- Eliminate double hydration of requirements/items in form init
- Add EventItem mapper roundtrip tests and splitDateTime/combineDateTime
  edge case tests (19 new tests)

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(groups): coach program assignment

Adds the ability for a group coach to assign one of their programs to a
member. The program is atomically transferred to the member's ownership
with source tagged as COACH_ASSIGNED and the coach recorded as created_by,
enabling the member to filter out programs they received vs created.

Includes the two-step AssignProgramDialog UI and integrates it into
MemberCard. Both the Tauri SQLite command and the Supabase SECURITY
DEFINER RPC perform the same three authorization checks (caller is COACH,
target is MEMBER, program owned by caller) before executing the transfer.

* fix(sync): resolve incremental push breakage and missing error state

- push.rs: timestamp_millis() -> timestamp() so last_push_at is stored
  in seconds, matching the updated_at column it filters against; the
  millis value was always larger than any updated_at, causing zero rows
  to be found on every push after the first
- sync.rs: sync_force_push now transitions to SyncState::Error on
  failure, matching the sync_force_pull pattern; previously a failed
  push left the engine in Idle, hiding the error from the frontend

* docs(planning): add spec, tech, and steps for coach program assignment

* feat(chat): add chat data layer + QA fixes (#33)

* feat(chat): add chat data layer (Step 21)

Builds the full persistence layer for the chat domain across Supabase,
SQLite, Rust, and TypeScript. No UI -- pure data plumbing for Steps 22-25.

- Supabase migration: conversations, conversation_participants, messages,
  media_attachments with RLS (CH-1), append-only messages (CH-7), and
  direct conversation uniqueness via trigger (CH-2)
- SQLite migration: mirrors Postgres with sync_status on messages for
  offline queueing (pending -> synced/failed state machine)
- Rust: ConversationRow/MessageRow/etc structs, 10 Tauri commands (CRUD
  for conversations, messages, participants, media), chat tables in
  SYNCABLE_TABLES for push/pull sync
- TypeScript: Zod schemas (Conversation, Message, MediaAttachment,
  WorkoutSnapshot), DB row types, bidirectional mappers, 13-method
  ChatAdapter facet on DataAdapter interface, Supabase and Tauri
  adapter implementations, mock adapter updated

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011
Feature: 004-Chat-Data-Layer

* docs(planning): add spec and steps for chat data layer (Step 21)

Feature 004 planning artifacts: Spec.md covering requirements,
testable assertions, and invariants (CH-1, CH-2, CH-4, CH-7);
Steps.md with 4-wave execution plan for db-engineer, rust-engineer,
domain-engineer, and quality-engineer.

Feature: 004-Chat-Data-Layer

* fix(qa): address all QA feedback -- sync resilience, lint, and stale tests

Rust sync hardening:
- sync/mod.rs: replace `return` with `continue` so queue flush failure no longer kills the sync loop permanently; transition to Error state when initial pull fails rather than silently proceeding to push
- sync/pull.rs: add SYNCABLE_TABLES allowlist guard in upsert_row and delete_row
- sync/push.rs: add SYNCABLE_TABLES allowlist guard in push_table
- sync/queue.rs: normalize enqueue timestamp to seconds (was millis, inconsistent with push/pull)

Frontend lint:
- packing-list.tsx: replace setState-in-useEffect anti-pattern with useMemo derivation for mutation error feedback
- event-template-form.tsx: extract splitDateTime/combineDateTime to date-time-utils.ts to fix Fast Refresh violation (non-component exports alongside component)

Tests (31 stale failures fixed):
- Update 5 test files with correct mixed-case text after CSS uppercase refactor
- Rewrite mobile-nav.test.tsx: add useAuth mock, update to 5+1 nav structure, correct icons and routes

* fix(review): resolve PR #33 review findings for chat data layer

Critical fixes (C-1 through C-5):
- Add missing INSERT policy on conversations table (C-1)
- Filter user's own messages from getUnreadCounts in Supabase adapter (C-2)
- Change z.url() to z.string().url() for media attachment URLs (C-3)
- Return Err instead of Ok(()) from SYNCABLE_TABLES allowlist guards (C-4)
- Add missing UPDATE policy on media_attachments table (C-5)

Important fixes (I-2, I-3):
- Add participant check before message insert in send_message (I-2)
- Validate direct conversations require exactly 2 participants (I-3)

Tasks created (S013-S017) for: data mapper tests, dedicated
get_messages_since command, updatedAt mapping fix, unread counts
resilience, and sync guard test coverage.

Suggestions S-1 through S-6 tracked in Ideas backlog.

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

* feat(chat): implement review remediation tasks S013-S017

S013: Add 36 data mapper unit tests for chat entities
  - Conversation, ConversationParticipant, Message, MediaAttachment
  - Round-trip, null handling, and Zod validation tests

S014: Add dedicated get_messages_since Rust command
  - SQL-level WHERE created_at > ? filter replaces client-side filtering
  - Eliminates silent truncation at 1000 message limit

S015: Fix toConversationParticipant updatedAt mapping
  - Use last_read_at ?? joined_at instead of always joined_at

S016: Improve getUnreadCounts error resilience
  - Single conversation failure no longer aborts all counts
  - Returns partial results with console.warn for failed queries

S017: Remove #[cfg(not(test))] from sync allowlist guards
  - Tests now exercise the same allowlist path as production
  - Updated test fixtures to use real SYNCABLE_TABLES entries

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: Supabase Realtime, chat data layer, events & coach assignment (#34)

* docs(planning): add spec, tech, and steps for supabase realtime integration

4-phase blueprint for Step 22 (Supabase Realtime Integration).
Establishes architecture for Broadcast channel management, cursor-based
message pagination, RealtimeManager module, and all chat TanStack Query hooks.

ADR-005: cursor-based getMessages replaces offset-based (no existing callers).

* feat(chat): add Supabase Realtime broadcast channels and chat hooks

Wire up Supabase Realtime Broadcast for live message delivery and typing
indicators. Introduces the RealtimeManager singleton, cursor-based message
pagination, platform-agnostic foreground detection, and all TanStack Query
hooks needed by the Chat UI (Step 23).

New files:
- src/lib/realtime-schemas.ts     -- Zod schemas for broadcast payloads
- src/lib/realtime-manager.ts     -- channel lifecycle, typing state, catch-up
- src/lib/foreground-detector.ts  -- visibilitychange / Tauri focus detection
- src/hooks/use-chat.ts           -- useConversations, useMessages (infinite),
                                     useSendMessage (optimistic), useRealtimeMessages, +6 more
- src/components/chat-realtime-listener.tsx -- app-level cache update listener

Modified:
- data-adapter.ts / supabase-adapter.ts / tauri-adapter.ts / chat.rs
  Replace offset-based getMessages with cursor-based (before?: ISO timestamp)
- main.tsx: mount ChatRealtimeListener alongside SyncListener
- auth.tsx: call resetRealtimeManager() on SIGNED_OUT

Tasks: S001, S002, S003, S004, S005, S006, S007

* fix: resolve QA lint and code quality issues across frontend and Rust

Fix clippy errors in test code (manual_range_contains, approx_constant),
ESLint no-constant-binary-expression in utils test, and three code quality
issues in realtime-manager: validate typing payloads with Zod schema,
replace fragile onTyping callback chain with Set-based listener pattern,
and explicitly discard handleForeground Promise.

* fix(chat): address PR #34 review findings for Supabase Realtime

Multi-listener pattern (addMessageListener/addTypingListener) replacing
single-callback ownership, broadcast try/catch in useSendMessage,
channel lifecycle ownership fix, auth integration via useAuth hook,
and Wave 5 polish: named constants, toMessageFromBroadcast mapper,
simplified clearAllTyping, foreground-detector error logging and
async/await rewrite, broadcastTyping .catch(), MessagePaginationOptions
interface, user_name max(200) constraint.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(chat): complete chat UI with messaging and conversations (#35)

* docs(planning): add spec, tech, and steps for chat UI (Step 23)

* feat(chat): build complete chat UI (Step 23)

Routes, conversation list, virtualized message detail, compose bar,
typing indicators, new conversation flow, group management, and
client-side blocking. Navigation updated -- COMMS replaces Social
on mobile, added after Vault on desktop sidebar with unread badge.

Conversation type extended with participantUserIds so ConversationDetail
can resolve the other user's display name and blocking state for direct
conversations. Supabase adapter fetches participants alongside
conversations; Tauri adapter was already receiving them and now uses them.

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 006-Chat-UI

* docs(planning): add quick plan for conversation participant IDs

Feature: 006-Chat-UI

* fix: update mobile-nav tests for Comms tab and fix Rust formatting

Update test assertions for Social→Comms rename, add useUnreadCounts
mock, and fix rustfmt formatting in event_reminder tests.

* fix(chat): resolve PR #35 review findings

Critical fixes:
- C1: Contact search filter was a no-op; now passes search prop to
  ContactRow which renders null when display name does not match
- C2: Sender name in group bubbles showed raw UUID; resolved via
  useUserProfile inside MessageBubble
- C3: Send failure gave zero feedback; added sendError state and
  inline "Failed to send" indicator on ComposeBar
- C4: Conversation creation failure was silent; added error state
  and inline message in NewConversationSheet
- C5: MessageList had no error/loading state; added spinner and
  error fallback from useMessages isLoading/isError

Important fixes:
- I1: Typing broadcast used user.email; now uses displayName via
  useUserProfile
- I2/I3: useBlockedUsers storage key scoped to userId; added
  console.error/warn on localStorage corruption
- I4: Optimistic message had senderId undefined; now uses current
  user ID from useAuth
- I5: LeaveDialog failure was silent; added inline error message
- I6: ParticipantSheet add failure was silent; shows error including
  Tauri offline message
- I7: Removed rounded-full from avatar containers and unread dots
- I8: Added comment clarifying group threshold (2 selected + server
  adds self = 3 total participants)

Suggestions:
- S3: Removed build-artifact comment from conversation-list.tsx
- S9: Fixed error copy from "pull to retry" to "try again"

Tasks: C1, C2, C3, C4, C5, I1, I2, I3, I4, I5, I6, I7, I8, S3, S9
Feature: 006-Chat-UI

* docs(planning): resolve PR #35 review and add test/polish tasks

Marks 0003-pr35-chat-ui-review.md as fully resolved (5 critical,
8 important, 2 suggestions fixed inline; 6 tasks created; 5
suggestions dismissed with rationale).

Adds Wave 7 tasks to Steps.md:
- S015-T: deriveItems unit tests
- S016-T: useBlockedUsers unit tests
- S017-T: formatTypingText unit tests
- S018-T: relativeTime + getInitials unit tests
- S019: Migrate useBlockedUsers to Zustand store
- S020: Wire archive action in conversation header

Feature: 006-Chat-UI

* refactor(chat): extract utils, add blocked-users store, verify review

Extract pure functions (relativeTime, getInitials, formatTypingText,
deriveItems) into chat-utils.ts and add tests. Migrate useBlockedUsers
to Zustand store with user-scoped persistence. Mark PR #35 review as
verified with follow-up tracked in GitHub issues #43-#51.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: add link preview metadata with OG tags, Twitter Cards, and useMetadata hook (#53)

Add comprehensive link preview support so shared URLs display rich previews
on social platforms and messaging apps. Includes a useMetadata hook for
dynamic per-route title and description updates, used on share pages.

* fix(auth): prevent PKCE code race condition on OAuth callback (#54)

Supabase JS v2.100.1 defaults to detectSessionInUrl: true with PKCE
flow, causing _initialize() to auto-exchange the auth code before the
/auth/callback route handler can. The second exchange attempt fails
(code already consumed), redirecting users to /sign-in?reason=oauth_error.

Disable detectSessionInUrl since we have a dedicated callback route
that handles PKCE code exchange manually.

* fix(auth): enable PKCE flow for OAuth sign-in (#55)

Without explicit flowType: 'pkce', the Supabase client uses implicit
flow, returning tokens as hash fragments (#access_token=...) that the
/auth/callback route cannot read (it expects ?code= query params).

Setting flowType: 'pkce' makes Supabase return a PKCE authorization
code as a query parameter that the callback can exchange for a session.

* fix(db): resolve RLS infinite recursion on group_members and conversation_participants (#56)

The group_members and conversation_participants SELECT policies
self-referenced their own tables, triggering PostgreSQL error 42P17
(infinite recursion). This cascaded 500s to every table whose sharing
policies join group_members (workout_logs, session_templates, etc.).

Creates SECURITY DEFINER helpers (is_group_member, is_group_coach,
is_conversation_participant) that bypass RLS for inner membership
lookups and replaces all six affected policies.

Also removes the PWA manifest (manifest.webmanifest) and its link tag
since Vercel deployment protection returns 401 on the unauthenticated
manifest fetch.

* feat(builder): program builder UI with responsive week grid (#58)

* feat(builder): program builder UI with responsive week grid

Implements the complete program builder and session builder UIs.

Program builder:
- Sidebar layout (desktop) / stacked layout (mobile) at /builder route
- Drag-and-drop block reordering with @dnd-kit
- Week grid defaults to 5-day Mon-Fri view with a toggle to show all 7
  days, improving readability at constrained content widths (~94px/cell
  at 1024px narrowed to nothing legible; 5-day gives ~132px/cell min)
- Session slots reduced from 80px to 56px min-height with type-colored
  left borders (STRENGTH/CONDITIONING/SE/MIXED/EVENT)
- Mobile block editor with vertical day-row tap targets
- Session picker bottom sheet and copy-week dialog
- Program preview overlay with per-session exercise/load tables

Session builder:
- Set scheme editor with per-type form variants
- Activity group editor and session template form

Infrastructure:
- Collapsible shadcn/ui component added
- Removed boilerplate App.tsx/App.css
- Expanded index.css with builder-specific design tokens

* fix(builder): declutter block header with overflow menu

Replace inline action buttons (move up/down, delete) with a dropdown
overflow menu in both desktop and mobile block editors. This prevents
touch target overlap on narrow viewports and isolates the destructive
delete action behind a two-tap interaction.

* feat(builder): guard destructive actions with confirmation and undo

Block and week deletions now require confirmation via AlertDialog,
showing impact ("This will remove N weeks and M sessions.") before
proceeding. Session removal fires immediately but shows a 5-second
undo toast via sonner, letting users recover accidental taps without
blocking the fast path.

- Add alert-dialog.tsx shadcn primitive (Radix AlertDialog)
- Add confirm-delete-dialog.tsx reusable builder component
- Wire block delete in BlockEditor and MobileBlockCard to dialog
- Wire week delete in WeekGrid and MobileWeekSection to dialog
- Add undo toast to SessionSlot and MobileDayRow session removal
- Mount Toaster in main.tsx (bottom-center)

* feat(builder): replace full-screen preview with inline week detail

Remove the disconnected ProgramPreview overlay from the builder flow.
Exercise details now toggle inline per-week via a visibility icon,
keeping the edit-preview feedback loop tight. Save button moves to
the header. Shared formatting helpers extracted to session-detail.tsx.
Also fixes Collapsible asChild bug in block-editor and mobile-block-editor.

* fix(builder): remove interstitial from session template create flow

Tapping "Create new template" in the session picker now opens the
inline form immediately, skipping the Library recommendation callout.
The Library option is preserved as a subtle link ("Want more options?
Create in the Library") below the form for users who need it.

Removes showCreateCallout state and the two-step reveal pattern.

* refactor(builder): consolidate session constants and polish UI details

- Extract SESSION_TINT and SESSION_TYPE_BADGE from session-slot,
  mobile-block-editor, and program-preview into constants.ts
- Remove duplicate @keyframes block-enter and slot-fill in index.css
- Bump section-level labels (WEEK N, day columns, EXERCISE/SETS/LOAD)
  from text-[11px] font-medium to text-xs font-semibold for visual
  hierarchy over metadata badges which stay at text-[11px] font-medium
- Upgrade session picker SheetTitle to text-sm font-display
- Add empty state copy when zero blocks exist in BlockList and
  MobileBlockEditor
- Clarify copy-week dialog: description now states sessions will be
  replaced, and per-week "(N existing)" label gains warning colour

* feat(builder): replace collapsed save error with structured inline validation

Validation errors now appear at the relevant field rather than as a single
joined string below the save button. Program name errors render under the
name input, block name and week errors render on each block card (visible
even when collapsed), and server/auth errors remain global in the header.

* feat(builder): add recently-used templates and reduce week grid noise

Surface the 5 most recently assigned session templates at the top of the
picker sheet so users with large template libraries reach common picks
faster. Adds lastAssignedAt tracking across both Tauri/SQLite and
Supabase backends.

Reduce visual noise in the week grid: restyle empty slots as muted
"Rest" labels instead of prominent calls-to-action, bring the 5/7-day
toggle to mobile, and collapse identical weeks within a block to a
compact summary row (with manual expand).

* refactor(builder): extract session-detail helpers to fix Fast Refresh lint errors

* feat: display broadcast infrastructure (Step 27) (#59)

* feat(chat): video, image, and file sharing (#57)

feat(chat): video, image, and file sharing (Feature 007)

Full-stack media sharing: Cloudflare Stream video uploads with TUS, image/file uploads via Supabase Storage, real-time transcoding status via webhook, signed URL caching, and inline media rendering in chat.

Review: 4 critical + 10 important findings resolved and verified. 12 enhancements implemented. 1,305 tests passing.

* docs: add plan for Step 27 -- display broadcast infrastructure

Spec.md, Tech.md, and Steps.md for Feature 008. Covers DisplaySnapshot
Zod schema, publisher module, store integration, display_visible toggle,
and Supabase Realtime Broadcast channel plumbing. 14 tasks across 4 waves
with 3 agents.

* feat: add display_visible column to user profiles

SQLite and Supabase migrations for display broadcast toggle,
Tauri model and command updates to support the new field.

* feat: add DisplaySnapshot domain type and display publisher

DisplaySnapshot type with Zod schema, display-publisher for
broadcasting workout state, data-mapper and store updates.

* feat: add PushToDisplayButton and display broadcast hook

useDisplayBroadcast hook, PushToDisplayButton component,
workout log and profile route integrations.

* test: add display snapshot and publisher tests

Unit tests for DisplaySnapshot schema, display-publisher,
display-snapshot builder, and updated data-mapper/adapter tests.

* docs: update Steps.md and implementation plan for display broadcast

* fix(review): resolve PR #59 review findings

Fixes: C1 (RLS auth), C2 (image signed URLs), C3 (snapshot try-catch),
C4 (Cloudflare subdomain), I1-I9 (error handling, schema tightening,
z.url() revert, publisher readiness), M1/M3/M5/M6 (env guards,
request parsing, query error logging, JSDoc)
Tasks: S024-T, S025-T added for media-provider and timeout tests
Comments: 9 comment issues resolved

Review: Context/Reviews/0005-pr59-display-broadcast-review.md

* test(media): add unit tests for CloudflareStreamProvider and transcoding timeout

S024-T: media-provider tests covering signed URL TTL cache hit/miss,
null client error, Zod validation of malformed responses, and cache
deduplication (13 tests).

S025-T: use-media-upload tests covering 5-minute transcoding timeout
marks processing attachment as failed, unaffected ready attachments,
query cache invalidation, and timer cleanup on unmount (4 tests).

Tasks: S024-T, S025-T
Feature: 007-video-image-sharing

* docs(review): mark PR #59 review as verified

* feat(edge-functions): deploy CF video edge functions with Vault secrets and local JWT signing

Deploys three Supabase Edge Functions for Cloudflare Stream video support
(Step 25). All Cloudflare credentials read from Supabase Vault using
CF_* secret names (CF_ACCOUNT_ID, CF_STREAM_TOKEN, CF_WEBHOOK_SECRET,
CF_SIGNING_KEY_PEM, CF_SIGNING_KEY_ID, CF_CUSTOMER_SUBDOMAIN).

chat-media-signed-url: replaces Cloudflare token API round-trip with
local RS256 JWT signing via CF_SIGNING_KEY_PEM + CF_SIGNING_KEY_ID.
No outbound HTTP call needed per playback request.
chat-media-upload-url: reads CF_ACCOUNT_ID + CF_STREAM_TOKEN from Vault.
chat-media-webhook: reads CF_WEBHOOK_SECRET + CF_ACCOUNT_ID from Vault.

Adds get_secret() SQL migration granting service_role-only read access
to vault.decrypted_secrets. Fixes duplicate migration timestamp
20260403000002 -> 20260403000003 for session_template_last_assigned.

Updates mock-supabase test util to support keyed RPC results
(get_secret:SECRET_NAME) for parameter-specific vault mocking.
Removes CLOUDFLARE_ACCOUNT_ID from STANDARD_ENV test fixture.

* chore: update app icons and branding assets

Replace default Tauri icons with Ardent Forge branding across all
platforms (Android, iOS, Windows, macOS). Update app display name
in Android strings.xml and tauri.conf.json.

* fix(db): rename migration 008 and auto-recreate DB on checksum mismatch

Rename session_template_last_assigned to migration 009 to make room
for 008_add_display_visible. Add debug-only auto-recreate logic so
migration checksum mismatches during development delete and rebuild
the local database instead of crashing.

* fix(auth): use Chrome Custom Tabs for Google OAuth on Android

Replace window.open with tauri-plugin-opener's inAppBrowser mode to
launch Google OAuth in Chrome Custom Tabs instead of an embedded
WebView. Google blocks OAuth in WebViews under their "Use secure
browsers" policy. Add opener plugin registration and URL scope
permissions for mobile capability.

* chore: update Claude agent configs and mise tooling

* feat(display): display route with board and focused views (#60)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* fix(display): address QA findings from PR #60 review (#61)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* feat(discovery): add discovery client and server URL flow (#63)

* docs(planning): add Feature 009 plan for discovery client + server URL

Creates the full 4-phase planning artifacts for Refactor A of the onboarding
redesign: server URL input with auto-discovery replaces raw Supabase credential
entry as the primary setup flow.

Tasks: S001, S001-T, S002, S003, S003-T, S004, S004-D
Feature: 009-discovery-client-server-url
ADR-006

* feat(setup): add discovery client and server URL flow

Replace raw Supabase credential inputs on the setup screen with a
primary server address field that auto-discovers backend credentials
via GET /.well-known/ardent-forge.json. Manual Supabase URL and key
entry moves into a collapsible "Manual configuration" section as a
fallback.

- Add discoverInstance() in src/lib/discovery.ts with URL normalization,
  8-second AbortController timeout, and typed DiscoveryResult discriminated
  union (NETWORK_ERROR, NOT_FOUND, INVALID_RESPONSE)
- Add 11 Vitest unit tests covering happy path, URL normalization variants,
  and all error branches
- Refactor setup.tsx: server URL primary input, discovery + validation
  state machine, "or" divider, collapsible manual section, disabled QR
  placeholder for future Refactor B
- Add /.well-known/ardent-forge.json handle block to Caddyfile with
  {$SITE_URL} and {$ANON_KEY} env var interpolation
- Add ANON_KEY to caddy service environment in docker-compose.yml
- Document discovery endpoint for static host and Docker deployments
  in docs/self-hosting.md; update mobile app configuration section

Tasks: S001, S001-T, S002, S003, S003-T, S004
Feature: 009-discovery-client-server-url
ADR: ADR-006

* docs(planning): mark Feature 009 Steps.md as complete

* fix(discovery): harden URL validation, use Zod, deduplicate connect logic

- Validate URL protocol via new URL() parse; reject non-http/https schemes
  (file://, ftp://, etc.) before any fetch is made
- Replace manual typeof field checks with a Zod schema (DiscoverySchema)
  to align with project convention and improve type safety
- Extract shared validateAndSave() helper in setup.tsx, eliminating
  duplicated validate-save-init-navigate block between handleConnect
  and handleDiscoverAndConnect
- Add 4 new tests: file:// rejection, ftp:// rejection, empty string,
  whitespace-only input

Feature: 009-discovery-client-server-url

* fix(review): resolve PR review findings for discovery feature

Fixes (P6-001..008): add logging to all error paths, capture error
params in catch blocks, correct error type semantics (INVALID_INPUT
for validation, INVALID_RESPONSE for JSON parse), fix JSDoc, add
aria-expanded to disclosure toggle, add double-submission guard,
fix comment scope.

Refactor: unify 4 parallel state variables into a single SetupState
discriminated union with 7 phases, eliminating impossible states.
Also wraps handlers in try-catch (P6-009..011) and fixes success
state ordering as part of the refactor.

Rules: create .claude/rules/error-handling.md codifying error type
semantics and catch block conventions (P6-017, P6-018).

Tasks: S005..S007-T added to Steps.md for schema tightening and
test coverage gaps (P6-012..015).

Review: Context/Reviews/0006-discovery-client-server-url-2026-04-04.md

* fix(discovery): tighten schema validators and add missing test coverage

S005: Use z.string().url() for supabase_url, z.string().min(1) for
supabase_publishable_key and version in DiscoverySchema. Validation
is now strictest at the system boundary.

S005-T: Add test for missing supabase_publishable_key field.
S006-T: Add test for 500 HTTP response (non-404 error code).
S007-T: Add test for URL with path component being stripped.

Review: P6-012, P6-013, P6-014, P6-015

* docs(review): mark discovery feature review as verified

* feat(display): idle mode with Edge Function broadcast (#62)

* docs(planning): add feature 009 idle mode spec, tech plan, and steps

Full 4-phase blueprint for Step 29 (Idle Mode + Edge Function).

- Spec.md: 8 must-haves, 17 testable assertions, all open questions
  resolved (24h clock default, /display?clock=12h override, max 3
  sessions, silently hide schedule when Edge Function unreachable)
- Tech.md: 8 architecture decisions -- idle_snapshot event type
  extension, Realtime HTTP broadcast API, Supabase cron schedule,
  program hierarchy JOIN query, server_time drift correction, CSS-only
  transitions, TanStack Router search param validation
- Steps.md: 10 tasks across 4 phases, frontend-specialist +
  backend-engineer + quality-engineer, parallel Phase 1 (types + Edge
  Function) and Phase 2 (ClockDisplay + IdleView + transition wrapper)

* feat(display): implement idle mode with Edge Function broadcast

Adds idle mode to the /display route -- the ambient state shown when no
workouts are active. A server-side Edge Function queries today's remaining
scheduled sessions every 60 seconds and broadcasts them to the display
channel so the TV always shows useful information.

- IdleSnapshot Zod schema + idle_snapshot event type in DisplayEventType
- ClockDisplay component (8rem Space Grotesk, server-time drift correction)
- IdleView component (clock, date, sessions list, next-up badge, footer)
- useDisplayMode hook deriving idle/board/focused from session map
- useIdleSnapshot hook subscribing to display Broadcast channel
- DisplayModeTransition wrapper (300ms fade for idle↔board, 400ms zoom for board↔focused)
- /display route composed end-to-end with all hooks and components
- display-idle-snapshot Edge Function with cron every 60 seconds
- get_display_idle_sessions RPC (service_role only, filters completed sessions)
- /display bypasses config guard in __root.tsx (public, no-auth route)
- docs/self-hosting.md: Remote Display section

Tasks: S001, S001-T, S002, S003, S003-T, S004, S005, S005-T, S006, S006-T, S007, S008, S009, S009-D
Feature: 009-idle-mode

* docs(planning): mark feature 009 idle mode as complete

Feature: 009-idle-mode

* fix(display): address QA findings from idle mode review

Frontend:
- Refactor useDisplayMode to use setState-during-render pattern (React
  "storing info from previous renders") instead of refs-in-useMemo,
  fixing react-hooks/refs ESLint violations and the ref-as-dep stability bug
- Add NaN guard and 60s periodic re-sync to ClockDisplay offset to handle
  NTP corrections and DST transitions on long-running displays
- Wire real Realtime channel status to connectionStatus in display route,
  replacing the hardcoded 'connected' placeholder
- Fix useIdleSnapshot cleanup to safely void removeChannel promise; add
  dev-mode console.warn on safeParse failures for easier debugging
- Fix IdleView session key to use display_name+session_name instead of
  array index
- Remove redundant cleanup() calls in display test afterEach blocks

Backend:
- Add day_of_week IS NOT NULL guard to get_display_idle_sessions RPC to
  prevent silent exclusion of sessions with unset day_of_week
- Add composite index idx_workout_logs_session_user_date to prevent full
  table scan on the cron-driven workout_logs LEFT JOIN
- Replace CREATE OR REPLACE with DROP IF EXISTS + CREATE for atomic
  GRANT application
- Remove non-null assertions on sessions[0] in Edge Function
- Add LIMIT 3 comment and verify_jwt rationale comment in config
- Improve Edge Function error messages to include error codes and response
  status for easier production debugging

Feature: 009-idle-mode

* fix(display): resolve PR review findings from 0006-idle-mode-review

Fixes: clock drift bug (P6-001), null client silence (P6-002),
lying connection status (P6-003), broad path bypass (P6-004),
duplicate channel (P6-005), missing CLOSED handling (P6-006),
production-silent parse errors (P6-007), duplicate DisplayMode type
(P6-008), unsafe RPC cast (P6-009), redundant comments (P6-010)

Tasks: S011-S014 added to Steps.md for missing test coverage
Rules: .min(1) on display-facing strings, Tech.md D4 corrected

Review: Context/Reviews/0006-idle-mode-review.md

* test(display): add review remediation tests S011-S014

S011: useIdleSnapshot hook tests (9 tests) -- valid/invalid payload,
null client, cleanup, connection status callback for all channel states
S012: DisplayModeTransition tests (8 tests) -- transition class mapping
for all mode pairs including duration assertions
S013: IdleSnapshot schema edge cases (7 tests) -- invalid session_type,
empty arrays, invalid ISO dates, .min(1) enforcement
S014: ClockDisplay correction test strengthened to assert exact time value

Review: Context/Reviews/0006-idle-mode-review.md

* docs(reviews): mark 0006-idle-mode-review as verified

All 16 findings confirmed resolved by review-verify agent.

Feature: 009-idle-mode

* fix(display): add onIdleSnapshot to subscriber test mock handlers

Fixes TS build error from merge integration -- the test's mock handlers
were missing the new onIdleSnapshot callback added to DisplayEventHandlers.

* chore(harness): update skills, hooks, and Claude Code settings

* docs: update hosting PRD and add onboarding refactoring plans

* feat(onboarding): QR code gen + scan for server setup (#64)

* docs(planning): add Feature 010 QR code gen + scan blueprint

Spec, Tech, and Steps for Refactor B of the onboarding redesign
(QR code generation in Settings + scan/paste on setup screen).
Covers invite link format, qrcode.react config, barcode scanner
plugin setup, and isTauri()-based platform branching. Updates
onboarding-refactoring-plans.md with researched plugin specifics.

Feature: 010-qr-code-gen-scan

* feat(onboarding): add QR code server sharing and scan on setup

Adds QR-based server sharing to reduce friction for onboarding new
users to a self-hosted Ardent Forge instance.

- Settings backend section now shows a "Share this server" area with
  a QRCodeSVG encoding `ardentforge://connect?url=...&key=...` and a
  "Copy invite link" button, guarded by hasConfig()
- Setup screen QR button activates native camera scan via
  @tauri-apps/plugin-barcode-scanner (Tauri/Android) or toggles an
  inline paste field (browser); both paths parse the invite link and
  auto-trigger connection validation
- New src/lib/invite-link.ts provides buildInviteLink() and
  parseInviteLink() as a pure shared utility
- Barcode scanner plugin registered under #[cfg(mobile)] and
  barcode-scanner:default added to mobile capabilities

Tasks: S001, S002, S003, S004, S005, S006
Feature: 010-qr-code-gen-scan

* fix(qr-scan): address QA findings from code review

- Add .catch() to unawaited processInviteLink() calls in paste/keydown
  handlers to prevent silent unhandled promise rejections
- Replace duplicate dynamic import of cancel with cancelRef to avoid
  redundant module resolution on scan overlay dismiss
- Change perms === 'denied' guard to perms !== 'granted' for defensive
  permission handling on non-standard permission states
- Move tauri-plugin-barcode-scanner to target-specific mobile dep in
  Cargo.toml to avoid enlarging desktop binary
- Remove unreachable pathname fallback branch from parseInviteLink
- Add comment to buildInviteLink documenting publishable key intent
- Rename misleading test and add true malformed-URL test coverage
- Apply rustfmt formatting to db.rs

Feature: 010-qr-code-gen-scan

* fix(qr-scan): resolve P7 review findings and add component tests

Fix bare catch blocks in handleCopy and connection-validator, add
try/catch to processInviteLink to prevent UI getting stuck, handle
unhandled promise from handleScan onClick, add user-facing error
toasts for clipboard failures, fix buildInviteLink to use SCHEME/HOST
constants, downgrade parse error log to silent null return, add
aria-label to paste field, improve QR scan failure toast message.

Add component tests for processInviteLink valid/invalid flows and
backend-settings share section conditional rendering and copy button.

Tasks: S006-T1, S006-T2
Feature: 010-qr-code-gen-scan
Review: Context/Reviews/0007-pr64-qr-code-gen-scan-review.md

* docs(planning): resolve P7 review and add S006-T1/T2 tasks to Steps.md

Mark all 14 findings in the QR code gen/scan review as resolved.
Add S006-T1 (processInviteLink component tests) and S006-T2
(backend-settings share section tests) to Feature 010 Steps.md.

Feature: 010-qr-code-gen-scan

* docs(review): mark P7 review verified by review-verify agent

* refactor(review-resolve): simplify bulk triage to bulk accept

Renames bulk triage to bulk accept to better reflect the workflow --
the capture category already IS the suggested action. Removes the
requirement for a reason on discard (individual and bulk).

* chore(harness): add gitignore entries and reformat hooks

Adds .claude/backups/, .claude/worktrees/, .serena/, and
hooks/skill-activation/.state/ to .gitignore. Reformats all hook
TypeScript files to double-quote style for consistency.

* feat(builders): progressive disclosure and contextual help (Step 14.5)

* docs(planning): add Step 14.5 feature plan and user flow docs

Documents the progressive disclosure and contextual help plan
for the template builder and program builder (Step 14.5).
Adds Flow 7 builder progressive disclosure and contextual help
system diagrams to 10-user-flows.md.

Feature: 010-Progressive-Disclosure
Tasks: S001–S014-T

* feat(builders): progressive disclosure and contextual help (Step 14.5)

Adds context-aware field visibility, staged group creation, set scheme
filtering, and inline help to the template and program builders.

New primitives and utilities:
- HelpTrigger component (Popover desktop / Drawer mobile via useMediaQuery)
- shadcn Popover and Drawer with Iron & Ember styling
- useMediaQuery hook (SSR-safe, listener cleanup)
- CollapsedFieldsRow for expandable hidden field groups
- visibility-maps.ts: data-driven lookup tables for all field visibility rules
- help-content.ts: GROUP_TYPE_HELP, BLOCK_TYPE_HELP, SOURCE_HELP constants
- text-label-large and text-body-small typography utilities in index.css

Template builder changes:
- Category-aware Scoring/Time Cap visibility with CollapsedFieldsRow
- Activity group staged reveal: Stage 1 (type selector only), Stage 2 (type-
  dependent rest fields + exercises)
- Set scheme type list filtered by session category; "Show all types" escape
- Load type options filtered by set scheme type; null = hide entire section
- Session picker gains "Create new event" inline creation flow

Program builder changes:
- Block type HelpTrigger with one-liner summary (desktop + mobile)
- Source badge HelpTrigger with all 7 sources

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013
Feature: 010-Progressive-Disclosure

* fix(builders): resolve QA findings from progressive disclosure review

Fixes two blocking hook violations and seven warnings across the builder
components introduced in Step 14.5.

Errors fixed:
- Move useCallback above early return in LoadSpecEditor to satisfy
  Rules of Hooks (conditional hook call)
- Rewrite useMediaQuery with useSyncExternalStore to eliminate
  synchronous setState in effect and double render

Warnings fixed:
- Remove eslint-disable suppression in LoadSpecEditor; expand useEffect
  deps to [allowedLoads, availableTypes, value.type, onChange]
- Replace div[role=button] nested inside <button> in MobileDayRow with
  sibling <button> elements; fix event type to MouseEvent | KeyboardEvent
- Add useId/aria-controls pairing to CollapsedFieldsRow for screen
  reader disclosure association
- Dynamic aria-label on HelpTrigger (Help: ${title} instead of static)
- Extract BLOCK_TYPE_STYLES to constants.ts; remove duplication across
  block-editor and mobile-block-editor
- Remove unused placement prop from HelpTrigger and all four call sites
- Fix PopoverTitle to render <h2> matching its ComponentProps<'h2'> type

Suggestions addressed:
- Sync ladderText in DescendingRepsFields when repLadder resets externally
- Use touchLastAssigned.mutate (stable ref) in useCallback deps
- Add clientId to ActivityGroupData and ActivityData; generate via
  crypto.randomUUID() at creation; use as React key instead of index
- Add aria-expanded/aria-controls to ProgramForm details toggle

Feature: 010-Progressive-Disclosure

* fix(builders): resolve PR review findings P7

Fixes: P7-001 (silent userId guard), P7-002 (JSX triplication),
P7-003 (redundant satisfies), P7-004 (unstable useEffect deps),
P7-005 (block type help duplication), P7-007 (missing JSDoc),
P7-008 (HelpEntry interface), P7-009 (non-empty tuple),
P7-010 (scheme map typing), P7-011 (constants typing)
Rules: P7-013, P7-014 added to typescript-conventions.md;
P7-015 added to error-handling.md
Deferred: P7-012 (ErrorBoundary strategy) to backlog
Discarded: P7-006 (superseded by P7-002 extraction)

Review: Context/Reviews/0007-pr65-progressive-disclosure-review.md

* fix(builders): resolve review-verify type errors and mark review passed

Fix 3 pre-existing TS errors from P7 review fixes: collapsedLabels
cast to non-empty tuple, allowedLoads.includes widened for
LoadSpec['type'], defaultTypes.includes widened for SetSchemeType.
Review file verified and checkbox marked.

* feat: implement Android CI/CD pipeline (Feature 011) (#67)

* docs(ci-cd): add full blueprint for Android CI/CD pipeline

4-phase planning for feature 011: GitHub Actions workflows covering PR
validation (lint, typecheck, unit tests, E2E via Playwright + local
Supabase, Android debug APK build), tagged release pipeline (signed AAB,
Supabase migrations gate, GitHub Release, Play Store internal track
upload), and supporting setup documentation.

Feature: 011-ci-cd-pipeline
ADR-007: GitHub Actions chosen as CI/CD platform

* ci(github-actions): implement complete Android CI/CD pipeline

Adds two GitHub Actions workflows from scratch:

- ci.yml: PR validation with three parallel jobs -- lint/typecheck/unit
  tests, Playwright E2E against local Supabase, and Android debug APK
  build with full Rust/NDK toolchain and caching
- release.yml: tag-triggered release pipeline -- signed AAB build and
  Supabase migrations run in parallel, gated publish job creates GitHub
  Release and uploads to Play Store internal track

Supporting changes:
- Add Playwright (@playwright/test) with smoke tests and test:e2e script
- Add signingConfigs block to build.gradle.kts reading from env vars
- Add .github/BRANCH_PROTECTION.md and .github/README.md
- Add CI/Release workflow badges to README.md

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009
Feature: 011-ci-cd-pipeline

* fix(android-ci): harden pipeline per PR #67 review findings

- Guard signingConfig assignment with keystorePath null check
- Validate KEYSTORE_BASE64 before decode; check decoded file size
- Pass all secrets through env: blocks (not inline shell expansion)
- Add secret validation steps to build-release, migrate, publish jobs
- Reorder publish job: Play Store upload before GitHub Release
- Capture Supabase status once; validate non-empty/non-null values
- Add if-no-files-found: ignore to Playwright report upload
- Fix test:e2e script to use bunx playwright test
- Add webServer timeout: 30_000, stdout/stderr: pipe to playwright.config.ts

Tasks: P8-001, P8-002, P8-003, P8-004, P8-005, P8-006, P8-007, P8-008, P8-015, P8-016

* docs(ci-cd): apply post-review documentation corrections

- Tech.md: Four->Two workflows, remove armv7 target, fix port 4173,
  update signing snippet to null-guard pattern
- Steps.md: add VITE_ prefix to env var names; add S010 secret validation
- ADR-007: status Proposed->Accepted
- Add PR #67 review capture (16/16 findings resolved and verified)
- Update review-resolve skill

Tasks: P8-009, P8-010, P8-011, P8-012, P8-013, P8-014, P8-015

* feat(deep-link): handle ardentforge://connect deep links (#66)

* docs(planning): add Feature 011 deep link handler planning artifacts

Full 4-phase plan for ardentforge://connect deep link handling.
Covers Tauri dispatch extension, browser /connect route, setup
screen pre-population, and transient store for settings handoff.

Feature: 011-deep-link-handler
ADR-007: transient-store-deep-link-state

* feat(deep-link): handle ardentforge://connect deep links

Implements the connect deep link handler so tapping an invite link
opens the app directly to the correct screen with credentials
pre-populated.

- Register ardentforge://connect host in Tauri deep-link config
- Add handleConnectLink() with 4-way branch: invalid, unconfigured,
  same instance, different instance
- Dispatch connect vs auth in onOpenUrl listener (OAuth flow untouched)
- Add /connect browser route as fallback for web users
- Exempt /connect from root route guard
- Setup screen accepts url/key search params for auto-population
- BackendSettings consumes pending connect Zustand store on mount
- Unit tests for all 5 handleConnectLink() branches (5/5 passing)

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010
Feature: 011-deep-link-handler
ADR: ADR-007 (transient Zustand store for cross-route deep link state)

* fix(deep-link): address QA findings from review

- Add desktop deep-link scheme config + permissions to default capability
- Fix bare catch blocks in invite-link.ts and auth.tsx (error-handling convention)
- Refactor setState-in-effect in backend-settings.tsx to useRef guard
- Add HTTPS validation for url param in parseInviteLink
- Accept navigate callback in handleConnectLink (avoids full-page reload in router context)
- Update OAuth tests to mock @tauri-apps/plugin-opener instead of window.open
- Add test for getConfig() returning null with hasConfig true
- Add test for non-HTTPS url param rejection
- Fix setup.test.tsx mock to provide Route.useSearch()

Feature: 011-deep-link-handler

* fix(deep-link): address PR-66 review findings

- Wrap handleConnectLink in try-catch with toast on error (P8-001)
- Isolate connect-link error path from OAuth error handler in
  auth.tsx (P8-002)
- Replace render-phase setState with useRef + useState initializers
  in backend-settings.tsx (P8-003)
- Replace unsafe `as string` casts with typeof guards in
  validateSearch for /connect and /setup routes (P8-004)
- Add toast feedback when auto-validate fails in /setup (P8-005)
- Replace startsWith check with new URL() + protocol/hostname
  validation in parseInviteLink (P8-006)
- Fix bare catch blocks in config-store.ts to capture and log
  errors (P8-007)
- Add store-boundary validation to usePendingConnect.setPending
  (P8-009)
- Add state-management.md coding rule for Zustand conventions
- Add review capture with verification (0008-pr66)
- Add test step S011 to Steps.md (P8-008)

* test(deep-link): add /connect route and auth connect-dispatch tests

- Add connect.test.tsx covering search param validation, pending
  store integration, and redirect behavior
- Update auth-oauth tests for connect deep-link error isolation

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* fix(ci): upgrade supabase/setup-cli from v1 to v2

v1 runs on deprecated Node 20 and installs an older CLI that rejects
newer config keys (oauth_server, web3, cron, etc.), breaking the
migrate job on tagged releases.

* fix(ci): pin supabase CLI to v2.84.4 to match local version

setup-cli@v1 defaults to CLI v2.20.3 which rejects newer config keys.
Pinning to 2.84.4 matches local dev environment. setup-cli v2 is
merged but not yet tagged as a release.

* fix(supabase): remove invalid cron key from config.toml

The [functions.display-idle-snapshot.cron] section is not a valid
config.toml option. Edge function cron scheduling is configured via
pg_cron in SQL, not in config.toml. This was causing supabase link
to fail in CI.

* fix(sync): add HTTP status check and empty update_set guard in pull path (#70)

* fix(sync): add HTTP status check and empty update_set guard in pull path

pull_table deserialized Supabase responses without checking the HTTP
status code, which could silently swallow 4xx/5xx errors or produce
confusing deserialization failures. Added a status check matching the
pattern already used in push.rs.

upsert_row produced invalid SQL ("... ON CONFLICT(id) DO UPDATE SET ")
when the only matching column was `id`. Now falls back to INSERT OR
IGNORE for that edge case.

Closes #68, closes #69

* fix(lint): resolve unused var, explicit any, and unused import errors

- Prefix unused catch parameter with underscore in formatter hook
- Add eslint-disable for necessary `any` cast in test helper
- Remove unused `buildRequest` import from webhook test

* fix(lint): use bare catch block for unused error parameter

* fix(data-mapper): handle JSONB columns already parsed by Supabase PostgREST

Supabase returns JSONB columns as objects, not strings. Calling JSON.parse()
on an object stringifies it to "[object Object]" first, causing a parse error
on session_templates fields (rest_between_groups, time_cap, event_metadata),
activity_groups fields (rest_between_rounds, rest_between_activities), and
activities (set_scheme).

Adds parseJsonOrValue() helper that passes objects through directly and only
calls JSON.parse() on string values, keeping backward compatibility with the
Tauri/SQLite adapter which returns these columns as JSON strings.

* chore: batch enhancement issues cleanup (#71)

* chore: add Zod refinements to chat domain types

Add validation refinements that catch invalid data at parse time:
- conversation.ts: prevent groupId on direct conversations, enforce leftAt > joinedAt
- message.ts: prevent senderId on system messages
- media.ts: add .nonnegative() to durationSeconds and fileSizeBytes

Closes #40, closes #47

* chore: polish realtime manager (simplify clearAllTyping, fix silent catch)

- Simplify clearAllTyping to delegate to clearTypingForConversation helper
- Add error logging to broadcastTyping .catch per error-handling conventions

Closes #42, closes #49

* feat(events): wire EventCountdownBadge navigation to dedicated event route

- Create /events/$templateId route with minimal event detail page
- Update EventCountdownBadge to navigate to new route instead of /library
- Remove _templateId unused-var rename and TODO comment

Closes #31

* perf: batch getUnreadCounts queries with Promise.allSettled

Replace sequential N+1 COUNT queries with concurrent execution via
Promise.allSettled, preserving partial-results error handling.

Closes #39, closes #46

* perf: optimize chat query patterns (CH-2 trigger, findDirectConversation)

- Add error context with [tauri-adapter] prefix to findDirectConversation loop
- Create materialized direct_conversation_pairs lookup table with unique index
- Replace O(N) trigger scan with O(1) index-backed uniqueness check
- Add cleanup trigger for participant departures

Closes #41, closes #48

* docs: add quick plan for enhancement issues batch

* fix(review): resolve PR #71 review findings

Fixes (7): RLS on direct_conversation_pairs, ISO datetime comparison,
findDirectConversation all-fail throw, getUnreadCounts error enrichment,
comment accuracy (…
…95)

* docs(planning): add F021 Gym Membership Explicit planning docs

Full 4-phase blueprint for decoupling instance signup from gym
membership (closes #94) and adding gym admin CRUD UI — roster,
ownership transfer (two-step), token invites, and member counts.

Feature: 021-Gym-Membership-Explicit
ADR-015-gym-ownership-transfer-schema
ADR-015-invite-token-redemption-via-rpc

* feat(gym): F021 Phase 1-2 — explicit membership DB + adapter layer

- Drop is_default infrastructure (trigger, function, index, column)
- Add gym_invitations, gym_ownership_transfers, gym_member_counts view
- Add 5 security-definer RPCs: create/redeem invite, propose/accept/
  cancel ownership transfer; error taxonomy INVITE_INVALID/EXPIRED/EXHAUSTED
- RLS: invites owner-only SELECT; transfers readable by proposer/target,
  DML denied (lifecycle via RPC only)
- ADR-015 (ownership transfer schema) + ADR-015 (invite redemption RPC)
- SQL tests for A-001..A-006, A-012..A-018
- Domain types: GymInvitation, GymOwnershipTransfer, GymMemberCount,
  RedeemInviteError discriminated union (Zod dual-export per ADR-008)
- Extend supabase-adapter: listGymMemberCounts, createGymInvite,
  listGymInvites, redeemGymInvite, proposeGymTransfer, acceptGymTransfer,
  cancelOrDeclineGymTransfer, getPendingTransfer
- Strip is_default from GymRow, mappers, adapter, fixtures, tests

Refs #94

* feat(gym): F021 Phase 3-4 -- hooks, UI components, routes

Phase 3 (hooks):
- use-gym-invites: list, create, redeem with INVITE_* error discrimination
- use-gym-transfers: pending query, propose/accept/cancel mutations
- use-gyms: useListAllGymsWithCounts (joins gyms + member counts)
- use-gym-members: useGymRoster (joins members + display names)
- All mutations have prefixed onError logging per error-handling rules

Phase 4 (UI + routes):
- 5 gym detail components: header, roster, owner-actions panel,
  pending-transfer-banner, invite-dialog (QR via qrcode.react)
- Detail route: /profile/gyms/$gymId with leave/join/kick/rename/
  delete/transfer/invite flows
- Join route: /gyms/join?token= with auth gate, URL stripping,
  discriminated error states (invalid/expired/exhausted)
- gym-management-section: real member counts via view, Link to detail
- deep-link-handler: ardentforge://gyms/join?token= dispatch
- DataAdapter interface + TauriAdapter stubs for F021 methods

Refs #94

* docs(planning): update F021 Steps.md progress to Phase 5

Phases 0-4 complete. E2E tests + final validation remaining.
Add duplicate button to SessionTemplateCard component that allows users to
clone their own session templates. The clone button appears between
the editable content area and the delete button, using the same
pattern as EventCard for consistency.

Changes:
- Add onClone and isCloning props to SessionTemplateCard
- Add content_copy icon button with loading state animation
- Wire clone handler in Library page for session templates
- Add toast notifications for clone success and error
- Add test coverage for useCloneSessionTemplate hook

The backend clone infrastructure already existed in the adapter
and hook; this change only adds the UI affordance for
regular session templates (Event templates already had this).
…ary (#97)

Add ability to start a new workout directly from a session template
in the Library page, without requiring a program context. This makes
programContext optional throughout the workout start flow.

Changes:
- Remove runtime guard requiring programContext in active-workout-store
- Make programContext optional in startProgrammedWorkout hook callback
- Update store test to accept workouts without programContext
- Add onStartWorkout and isStarting props to SessionTemplateCard
- Add play_arrow icon button with loading state to SessionTemplateCard
- Import useActiveWorkout and useGymPicker hooks in Library page
- Create handleStartFromTemplate handler with gym picker flow
- Mount GymPickerPortal in Library page
- Wire onStartWorkout and isStarting to SessionTemplateCard
- Only show start button for owned templates in "mine" scope

The start button appears between the editable content area and delete
button, using the same pattern as the clone button for consistency.
Clicking the button opens the gym picker, then creates a workout with
pre-filled exercises/sets from the template and navigates to the
workout log page. Standalone template workouts do not set programContext.

Fixes issue where programContext was incorrectly required at hook and store
layers despite being optional on the WorkoutLog type.
* fix: add Authorization header to Supabase reachability check

The PostgREST root endpoint requires both `apikey` and `Authorization`
headers. Missing the latter caused a 401 during setup, showing
"Cannot reach server" even with valid credentials.

* fix: auto-validate and surface env var connection failures on setup page

When VITE_SUPABASE_URL and VITE_SUPABASE_PUB_KEY are set but invalid,
the setup page now auto-validates on mount and shows the specific error
(e.g. "Server responded with 401") instead of silently presenting a
blank form. Also adds a banner explaining that environment variables
were detected but failed.

* fix: use Supabase client for connection validation instead of raw fetch

Raw fetch with apikey/Authorization headers only works with legacy anon
keys. New publishable keys (sb_publishable_xxx) require the Supabase
client library which handles auth headers internally. This switch
supports both key formats transparently.

* fix: resolve duplicate migration timestamp collision

Two migrations shared timestamp 20260329000001, causing a primary key
violation in supabase_migrations. Renumbered create_sharing_tables to
20260329000002 and expand_rls_for_sharing to 20260329000003.

* chore: update Claude Code configuration and skills

* feat: complete sync pull path with dynamic upsert and force-pull (Step 9) (#28)

Implement the stubbed pull/upsert path so remote changes from Supabase
are actually written to local SQLite, completing bidirectional sync.

- Fix broken timestamp parsing in upsert_row (ISO 8601 strings were
  silently parsed as 0, causing all remote updates to be discarded)
- Implement dynamic column upsert via pragma_table_info with type
  coercion (timestamps, booleans, JSONB) between Supabase and SQLite
- Add pull_table/pull_all for paginated force-pull via Supabase REST API
- Wire sync_force_pull command with proper state transitions
- Add auto-pull on first sign-in (detect last_pull_at == 0)
- Add 28 new tests (49 total), all passing

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: events & packing lists (Step 13.5) (#30)

* feat(events): add events & packing lists feature (Step 13.5)

Introduces EVENT as a new session category with full support for event
metadata (date, location, URL, requirements) and a categorized packing
checklist with drag-and-drop reorder, optimistic toggle, progress
tracking, and countdown notifications.

Database:
- Migration adds EVENT to category CHECK constraints on session_templates
  and scheduled_sessions; adds event_metadata JSONB to session_templates
  and workout_logs; creates event_items table with polymorphic FK
  (template XOR log), RLS policies, partial indices, and updated_at trigger

Domain:
- eventRequirementSchema, eventMetadataSchema, eventItemSchema (Zod)
- EVENT added to SessionType enum; eventMetadata optional on templates
  and logs; EventItemRow added to database.types.ts

Data layer:
- toEventItem/fromEventItem mappers; eventMetadata parsing in
  toSessionTemplate (JSON.parse) and toWorkoutLog (direct JSONB)
- DataAdapter interface: getEventItems, saveEventItem, updateEventItem,
  deleteEventItem, toggleEventItemPacked (accepts target bool, no race),
  reorderEventItems (Promise.all), cloneSessionTemplate (isPacked reset)
- SupabaseAdapter implements all methods; getSessionTemplateFull fetches
  eventItems for EVENT templates; updateSessionTemplateFull uses 3-way
  reconciliation to preserve item UUIDs and isPacked state
- TauriAdapter stubs (event support deferred, W-8)

Hooks:
- useEventItems, useCreateEventItem, useUpdateEventItem,
  useDeleteEventItem, useToggleEventItemPacked (optimistic),
  useReorderEventItems (optimistic), useNextUpcomingEvent

UI:
- event-builder/: EventTemplateForm (Sheet overlay, create/edit),
  EventDetail (metadata + map link + packing), PackingList (DnD,
  collapsible categories, error feedback), PackingItem (checkbox toggle),
  EventProgressBar (ember on surface-steel, ARIA progressbar),
  EventCard (library), EventCountdownBadge (Today screen, TODAY edge
  case), RequirementEditor, EventItemEditor, shared styles.ts
- Library: EventCard for EVENT templates, NEW EVENT button
- log/$workoutId: interactive EventDetail for event logs
- history/$workoutId: read-only EventDetail for event history
- Today screen: countdown badge when next event within 30 days
- Program builder: flag icon + ember accent for EVENT sessions,
  event date displayed in program-preview grid and detail sections

Native (Rust):
- event_reminders channel registered in notification.rs
- EventReminderState: background Tokio task (60s poll), checks
  configured intervals (7d/3d/1d), quiet hours, packing progress in
  notification body, deterministic notification ID
- schedule_event_reminders / cancel_event_reminders Tauri commands
- Clippy fixes: RemindedState type alias, needless_borrow in lib.rs,
  manual_range_contains in session_reminder.rs
- Lock contention fix: single mutex guard across event loop in tick()

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 002-events-packing-lists

* docs(planning): mark 002-events-packing-lists complete

Updates Steps.md status to Complete, session.md with full wave
execution log and fixes applied, and implementation-plan.md.

Feature: 002-events-packing-lists

* chore(rust): apply cargo fmt across codebase

* chore: add mise config and cortex hooks

* fix(events): address PR #30 review findings

- Fix timezone bug in combineDateTime: store local date strings instead
  of converting to UTC, preventing event dates from shifting by a day
- Add eventUrl validation in form validate() before save
- Switch sequential item persistence to Promise.allSettled with error
  collection for partial-failure reporting
- Fix DST-unsafe daysUntil calculation using date-only comparison
- Make TauriAdapter getEventItems throw consistently with write stubs
- Add "TODAY" special case in Rust event reminder notification title
- Eliminate double hydration of requirements/items in form init
- Add EventItem mapper roundtrip tests and splitDateTime/combineDateTime
  edge case tests (19 new tests)

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(groups): coach program assignment

Adds the ability for a group coach to assign one of their programs to a
member. The program is atomically transferred to the member's ownership
with source tagged as COACH_ASSIGNED and the coach recorded as created_by,
enabling the member to filter out programs they received vs created.

Includes the two-step AssignProgramDialog UI and integrates it into
MemberCard. Both the Tauri SQLite command and the Supabase SECURITY
DEFINER RPC perform the same three authorization checks (caller is COACH,
target is MEMBER, program owned by caller) before executing the transfer.

* fix(sync): resolve incremental push breakage and missing error state

- push.rs: timestamp_millis() -> timestamp() so last_push_at is stored
  in seconds, matching the updated_at column it filters against; the
  millis value was always larger than any updated_at, causing zero rows
  to be found on every push after the first
- sync.rs: sync_force_push now transitions to SyncState::Error on
  failure, matching the sync_force_pull pattern; previously a failed
  push left the engine in Idle, hiding the error from the frontend

* docs(planning): add spec, tech, and steps for coach program assignment

* feat(chat): add chat data layer + QA fixes (#33)

* feat(chat): add chat data layer (Step 21)

Builds the full persistence layer for the chat domain across Supabase,
SQLite, Rust, and TypeScript. No UI -- pure data plumbing for Steps 22-25.

- Supabase migration: conversations, conversation_participants, messages,
  media_attachments with RLS (CH-1), append-only messages (CH-7), and
  direct conversation uniqueness via trigger (CH-2)
- SQLite migration: mirrors Postgres with sync_status on messages for
  offline queueing (pending -> synced/failed state machine)
- Rust: ConversationRow/MessageRow/etc structs, 10 Tauri commands (CRUD
  for conversations, messages, participants, media), chat tables in
  SYNCABLE_TABLES for push/pull sync
- TypeScript: Zod schemas (Conversation, Message, MediaAttachment,
  WorkoutSnapshot), DB row types, bidirectional mappers, 13-method
  ChatAdapter facet on DataAdapter interface, Supabase and Tauri
  adapter implementations, mock adapter updated

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011
Feature: 004-Chat-Data-Layer

* docs(planning): add spec and steps for chat data layer (Step 21)

Feature 004 planning artifacts: Spec.md covering requirements,
testable assertions, and invariants (CH-1, CH-2, CH-4, CH-7);
Steps.md with 4-wave execution plan for db-engineer, rust-engineer,
domain-engineer, and quality-engineer.

Feature: 004-Chat-Data-Layer

* fix(qa): address all QA feedback -- sync resilience, lint, and stale tests

Rust sync hardening:
- sync/mod.rs: replace `return` with `continue` so queue flush failure no longer kills the sync loop permanently; transition to Error state when initial pull fails rather than silently proceeding to push
- sync/pull.rs: add SYNCABLE_TABLES allowlist guard in upsert_row and delete_row
- sync/push.rs: add SYNCABLE_TABLES allowlist guard in push_table
- sync/queue.rs: normalize enqueue timestamp to seconds (was millis, inconsistent with push/pull)

Frontend lint:
- packing-list.tsx: replace setState-in-useEffect anti-pattern with useMemo derivation for mutation error feedback
- event-template-form.tsx: extract splitDateTime/combineDateTime to date-time-utils.ts to fix Fast Refresh violation (non-component exports alongside component)

Tests (31 stale failures fixed):
- Update 5 test files with correct mixed-case text after CSS uppercase refactor
- Rewrite mobile-nav.test.tsx: add useAuth mock, update to 5+1 nav structure, correct icons and routes

* fix(review): resolve PR #33 review findings for chat data layer

Critical fixes (C-1 through C-5):
- Add missing INSERT policy on conversations table (C-1)
- Filter user's own messages from getUnreadCounts in Supabase adapter (C-2)
- Change z.url() to z.string().url() for media attachment URLs (C-3)
- Return Err instead of Ok(()) from SYNCABLE_TABLES allowlist guards (C-4)
- Add missing UPDATE policy on media_attachments table (C-5)

Important fixes (I-2, I-3):
- Add participant check before message insert in send_message (I-2)
- Validate direct conversations require exactly 2 participants (I-3)

Tasks created (S013-S017) for: data mapper tests, dedicated
get_messages_since command, updatedAt mapping fix, unread counts
resilience, and sync guard test coverage.

Suggestions S-1 through S-6 tracked in Ideas backlog.

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

* feat(chat): implement review remediation tasks S013-S017

S013: Add 36 data mapper unit tests for chat entities
  - Conversation, ConversationParticipant, Message, MediaAttachment
  - Round-trip, null handling, and Zod validation tests

S014: Add dedicated get_messages_since Rust command
  - SQL-level WHERE created_at > ? filter replaces client-side filtering
  - Eliminates silent truncation at 1000 message limit

S015: Fix toConversationParticipant updatedAt mapping
  - Use last_read_at ?? joined_at instead of always joined_at

S016: Improve getUnreadCounts error resilience
  - Single conversation failure no longer aborts all counts
  - Returns partial results with console.warn for failed queries

S017: Remove #[cfg(not(test))] from sync allowlist guards
  - Tests now exercise the same allowlist path as production
  - Updated test fixtures to use real SYNCABLE_TABLES entries

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: Supabase Realtime, chat data layer, events & coach assignment (#34)

* docs(planning): add spec, tech, and steps for supabase realtime integration

4-phase blueprint for Step 22 (Supabase Realtime Integration).
Establishes architecture for Broadcast channel management, cursor-based
message pagination, RealtimeManager module, and all chat TanStack Query hooks.

ADR-005: cursor-based getMessages replaces offset-based (no existing callers).

* feat(chat): add Supabase Realtime broadcast channels and chat hooks

Wire up Supabase Realtime Broadcast for live message delivery and typing
indicators. Introduces the RealtimeManager singleton, cursor-based message
pagination, platform-agnostic foreground detection, and all TanStack Query
hooks needed by the Chat UI (Step 23).

New files:
- src/lib/realtime-schemas.ts     -- Zod schemas for broadcast payloads
- src/lib/realtime-manager.ts     -- channel lifecycle, typing state, catch-up
- src/lib/foreground-detector.ts  -- visibilitychange / Tauri focus detection
- src/hooks/use-chat.ts           -- useConversations, useMessages (infinite),
                                     useSendMessage (optimistic), useRealtimeMessages, +6 more
- src/components/chat-realtime-listener.tsx -- app-level cache update listener

Modified:
- data-adapter.ts / supabase-adapter.ts / tauri-adapter.ts / chat.rs
  Replace offset-based getMessages with cursor-based (before?: ISO timestamp)
- main.tsx: mount ChatRealtimeListener alongside SyncListener
- auth.tsx: call resetRealtimeManager() on SIGNED_OUT

Tasks: S001, S002, S003, S004, S005, S006, S007

* fix: resolve QA lint and code quality issues across frontend and Rust

Fix clippy errors in test code (manual_range_contains, approx_constant),
ESLint no-constant-binary-expression in utils test, and three code quality
issues in realtime-manager: validate typing payloads with Zod schema,
replace fragile onTyping callback chain with Set-based listener pattern,
and explicitly discard handleForeground Promise.

* fix(chat): address PR #34 review findings for Supabase Realtime

Multi-listener pattern (addMessageListener/addTypingListener) replacing
single-callback ownership, broadcast try/catch in useSendMessage,
channel lifecycle ownership fix, auth integration via useAuth hook,
and Wave 5 polish: named constants, toMessageFromBroadcast mapper,
simplified clearAllTyping, foreground-detector error logging and
async/await rewrite, broadcastTyping .catch(), MessagePaginationOptions
interface, user_name max(200) constraint.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(chat): complete chat UI with messaging and conversations (#35)

* docs(planning): add spec, tech, and steps for chat UI (Step 23)

* feat(chat): build complete chat UI (Step 23)

Routes, conversation list, virtualized message detail, compose bar,
typing indicators, new conversation flow, group management, and
client-side blocking. Navigation updated -- COMMS replaces Social
on mobile, added after Vault on desktop sidebar with unread badge.

Conversation type extended with participantUserIds so ConversationDetail
can resolve the other user's display name and blocking state for direct
conversations. Supabase adapter fetches participants alongside
conversations; Tauri adapter was already receiving them and now uses them.

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 006-Chat-UI

* docs(planning): add quick plan for conversation participant IDs

Feature: 006-Chat-UI

* fix: update mobile-nav tests for Comms tab and fix Rust formatting

Update test assertions for Social→Comms rename, add useUnreadCounts
mock, and fix rustfmt formatting in event_reminder tests.

* fix(chat): resolve PR #35 review findings

Critical fixes:
- C1: Contact search filter was a no-op; now passes search prop to
  ContactRow which renders null when display name does not match
- C2: Sender name in group bubbles showed raw UUID; resolved via
  useUserProfile inside MessageBubble
- C3: Send failure gave zero feedback; added sendError state and
  inline "Failed to send" indicator on ComposeBar
- C4: Conversation creation failure was silent; added error state
  and inline message in NewConversationSheet
- C5: MessageList had no error/loading state; added spinner and
  error fallback from useMessages isLoading/isError

Important fixes:
- I1: Typing broadcast used user.email; now uses displayName via
  useUserProfile
- I2/I3: useBlockedUsers storage key scoped to userId; added
  console.error/warn on localStorage corruption
- I4: Optimistic message had senderId undefined; now uses current
  user ID from useAuth
- I5: LeaveDialog failure was silent; added inline error message
- I6: ParticipantSheet add failure was silent; shows error including
  Tauri offline message
- I7: Removed rounded-full from avatar containers and unread dots
- I8: Added comment clarifying group threshold (2 selected + server
  adds self = 3 total participants)

Suggestions:
- S3: Removed build-artifact comment from conversation-list.tsx
- S9: Fixed error copy from "pull to retry" to "try again"

Tasks: C1, C2, C3, C4, C5, I1, I2, I3, I4, I5, I6, I7, I8, S3, S9
Feature: 006-Chat-UI

* docs(planning): resolve PR #35 review and add test/polish tasks

Marks 0003-pr35-chat-ui-review.md as fully resolved (5 critical,
8 important, 2 suggestions fixed inline; 6 tasks created; 5
suggestions dismissed with rationale).

Adds Wave 7 tasks to Steps.md:
- S015-T: deriveItems unit tests
- S016-T: useBlockedUsers unit tests
- S017-T: formatTypingText unit tests
- S018-T: relativeTime + getInitials unit tests
- S019: Migrate useBlockedUsers to Zustand store
- S020: Wire archive action in conversation header

Feature: 006-Chat-UI

* refactor(chat): extract utils, add blocked-users store, verify review

Extract pure functions (relativeTime, getInitials, formatTypingText,
deriveItems) into chat-utils.ts and add tests. Migrate useBlockedUsers
to Zustand store with user-scoped persistence. Mark PR #35 review as
verified with follow-up tracked in GitHub issues #43-#51.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: add link preview metadata with OG tags, Twitter Cards, and useMetadata hook (#53)

Add comprehensive link preview support so shared URLs display rich previews
on social platforms and messaging apps. Includes a useMetadata hook for
dynamic per-route title and description updates, used on share pages.

* fix(auth): prevent PKCE code race condition on OAuth callback (#54)

Supabase JS v2.100.1 defaults to detectSessionInUrl: true with PKCE
flow, causing _initialize() to auto-exchange the auth code before the
/auth/callback route handler can. The second exchange attempt fails
(code already consumed), redirecting users to /sign-in?reason=oauth_error.

Disable detectSessionInUrl since we have a dedicated callback route
that handles PKCE code exchange manually.

* fix(auth): enable PKCE flow for OAuth sign-in (#55)

Without explicit flowType: 'pkce', the Supabase client uses implicit
flow, returning tokens as hash fragments (#access_token=...) that the
/auth/callback route cannot read (it expects ?code= query params).

Setting flowType: 'pkce' makes Supabase return a PKCE authorization
code as a query parameter that the callback can exchange for a session.

* fix(db): resolve RLS infinite recursion on group_members and conversation_participants (#56)

The group_members and conversation_participants SELECT policies
self-referenced their own tables, triggering PostgreSQL error 42P17
(infinite recursion). This cascaded 500s to every table whose sharing
policies join group_members (workout_logs, session_templates, etc.).

Creates SECURITY DEFINER helpers (is_group_member, is_group_coach,
is_conversation_participant) that bypass RLS for inner membership
lookups and replaces all six affected policies.

Also removes the PWA manifest (manifest.webmanifest) and its link tag
since Vercel deployment protection returns 401 on the unauthenticated
manifest fetch.

* feat(builder): program builder UI with responsive week grid (#58)

* feat(builder): program builder UI with responsive week grid

Implements the complete program builder and session builder UIs.

Program builder:
- Sidebar layout (desktop) / stacked layout (mobile) at /builder route
- Drag-and-drop block reordering with @dnd-kit
- Week grid defaults to 5-day Mon-Fri view with a toggle to show all 7
  days, improving readability at constrained content widths (~94px/cell
  at 1024px narrowed to nothing legible; 5-day gives ~132px/cell min)
- Session slots reduced from 80px to 56px min-height with type-colored
  left borders (STRENGTH/CONDITIONING/SE/MIXED/EVENT)
- Mobile block editor with vertical day-row tap targets
- Session picker bottom sheet and copy-week dialog
- Program preview overlay with per-session exercise/load tables

Session builder:
- Set scheme editor with per-type form variants
- Activity group editor and session template form

Infrastructure:
- Collapsible shadcn/ui component added
- Removed boilerplate App.tsx/App.css
- Expanded index.css with builder-specific design tokens

* fix(builder): declutter block header with overflow menu

Replace inline action buttons (move up/down, delete) with a dropdown
overflow menu in both desktop and mobile block editors. This prevents
touch target overlap on narrow viewports and isolates the destructive
delete action behind a two-tap interaction.

* feat(builder): guard destructive actions with confirmation and undo

Block and week deletions now require confirmation via AlertDialog,
showing impact ("This will remove N weeks and M sessions.") before
proceeding. Session removal fires immediately but shows a 5-second
undo toast via sonner, letting users recover accidental taps without
blocking the fast path.

- Add alert-dialog.tsx shadcn primitive (Radix AlertDialog)
- Add confirm-delete-dialog.tsx reusable builder component
- Wire block delete in BlockEditor and MobileBlockCard to dialog
- Wire week delete in WeekGrid and MobileWeekSection to dialog
- Add undo toast to SessionSlot and MobileDayRow session removal
- Mount Toaster in main.tsx (bottom-center)

* feat(builder): replace full-screen preview with inline week detail

Remove the disconnected ProgramPreview overlay from the builder flow.
Exercise details now toggle inline per-week via a visibility icon,
keeping the edit-preview feedback loop tight. Save button moves to
the header. Shared formatting helpers extracted to session-detail.tsx.
Also fixes Collapsible asChild bug in block-editor and mobile-block-editor.

* fix(builder): remove interstitial from session template create flow

Tapping "Create new template" in the session picker now opens the
inline form immediately, skipping the Library recommendation callout.
The Library option is preserved as a subtle link ("Want more options?
Create in the Library") below the form for users who need it.

Removes showCreateCallout state and the two-step reveal pattern.

* refactor(builder): consolidate session constants and polish UI details

- Extract SESSION_TINT and SESSION_TYPE_BADGE from session-slot,
  mobile-block-editor, and program-preview into constants.ts
- Remove duplicate @keyframes block-enter and slot-fill in index.css
- Bump section-level labels (WEEK N, day columns, EXERCISE/SETS/LOAD)
  from text-[11px] font-medium to text-xs font-semibold for visual
  hierarchy over metadata badges which stay at text-[11px] font-medium
- Upgrade session picker SheetTitle to text-sm font-display
- Add empty state copy when zero blocks exist in BlockList and
  MobileBlockEditor
- Clarify copy-week dialog: description now states sessions will be
  replaced, and per-week "(N existing)" label gains warning colour

* feat(builder): replace collapsed save error with structured inline validation

Validation errors now appear at the relevant field rather than as a single
joined string below the save button. Program name errors render under the
name input, block name and week errors render on each block card (visible
even when collapsed), and server/auth errors remain global in the header.

* feat(builder): add recently-used templates and reduce week grid noise

Surface the 5 most recently assigned session templates at the top of the
picker sheet so users with large template libraries reach common picks
faster. Adds lastAssignedAt tracking across both Tauri/SQLite and
Supabase backends.

Reduce visual noise in the week grid: restyle empty slots as muted
"Rest" labels instead of prominent calls-to-action, bring the 5/7-day
toggle to mobile, and collapse identical weeks within a block to a
compact summary row (with manual expand).

* refactor(builder): extract session-detail helpers to fix Fast Refresh lint errors

* feat: display broadcast infrastructure (Step 27) (#59)

* feat(chat): video, image, and file sharing (#57)

feat(chat): video, image, and file sharing (Feature 007)

Full-stack media sharing: Cloudflare Stream video uploads with TUS, image/file uploads via Supabase Storage, real-time transcoding status via webhook, signed URL caching, and inline media rendering in chat.

Review: 4 critical + 10 important findings resolved and verified. 12 enhancements implemented. 1,305 tests passing.

* docs: add plan for Step 27 -- display broadcast infrastructure

Spec.md, Tech.md, and Steps.md for Feature 008. Covers DisplaySnapshot
Zod schema, publisher module, store integration, display_visible toggle,
and Supabase Realtime Broadcast channel plumbing. 14 tasks across 4 waves
with 3 agents.

* feat: add display_visible column to user profiles

SQLite and Supabase migrations for display broadcast toggle,
Tauri model and command updates to support the new field.

* feat: add DisplaySnapshot domain type and display publisher

DisplaySnapshot type with Zod schema, display-publisher for
broadcasting workout state, data-mapper and store updates.

* feat: add PushToDisplayButton and display broadcast hook

useDisplayBroadcast hook, PushToDisplayButton component,
workout log and profile route integrations.

* test: add display snapshot and publisher tests

Unit tests for DisplaySnapshot schema, display-publisher,
display-snapshot builder, and updated data-mapper/adapter tests.

* docs: update Steps.md and implementation plan for display broadcast

* fix(review): resolve PR #59 review findings

Fixes: C1 (RLS auth), C2 (image signed URLs), C3 (snapshot try-catch),
C4 (Cloudflare subdomain), I1-I9 (error handling, schema tightening,
z.url() revert, publisher readiness), M1/M3/M5/M6 (env guards,
request parsing, query error logging, JSDoc)
Tasks: S024-T, S025-T added for media-provider and timeout tests
Comments: 9 comment issues resolved

Review: Context/Reviews/0005-pr59-display-broadcast-review.md

* test(media): add unit tests for CloudflareStreamProvider and transcoding timeout

S024-T: media-provider tests covering signed URL TTL cache hit/miss,
null client error, Zod validation of malformed responses, and cache
deduplication (13 tests).

S025-T: use-media-upload tests covering 5-minute transcoding timeout
marks processing attachment as failed, unaffected ready attachments,
query cache invalidation, and timer cleanup on unmount (4 tests).

Tasks: S024-T, S025-T
Feature: 007-video-image-sharing

* docs(review): mark PR #59 review as verified

* feat(edge-functions): deploy CF video edge functions with Vault secrets and local JWT signing

Deploys three Supabase Edge Functions for Cloudflare Stream video support
(Step 25). All Cloudflare credentials read from Supabase Vault using
CF_* secret names (CF_ACCOUNT_ID, CF_STREAM_TOKEN, CF_WEBHOOK_SECRET,
CF_SIGNING_KEY_PEM, CF_SIGNING_KEY_ID, CF_CUSTOMER_SUBDOMAIN).

chat-media-signed-url: replaces Cloudflare token API round-trip with
local RS256 JWT signing via CF_SIGNING_KEY_PEM + CF_SIGNING_KEY_ID.
No outbound HTTP call needed per playback request.
chat-media-upload-url: reads CF_ACCOUNT_ID + CF_STREAM_TOKEN from Vault.
chat-media-webhook: reads CF_WEBHOOK_SECRET + CF_ACCOUNT_ID from Vault.

Adds get_secret() SQL migration granting service_role-only read access
to vault.decrypted_secrets. Fixes duplicate migration timestamp
20260403000002 -> 20260403000003 for session_template_last_assigned.

Updates mock-supabase test util to support keyed RPC results
(get_secret:SECRET_NAME) for parameter-specific vault mocking.
Removes CLOUDFLARE_ACCOUNT_ID from STANDARD_ENV test fixture.

* chore: update app icons and branding assets

Replace default Tauri icons with Ardent Forge branding across all
platforms (Android, iOS, Windows, macOS). Update app display name
in Android strings.xml and tauri.conf.json.

* fix(db): rename migration 008 and auto-recreate DB on checksum mismatch

Rename session_template_last_assigned to migration 009 to make room
for 008_add_display_visible. Add debug-only auto-recreate logic so
migration checksum mismatches during development delete and rebuild
the local database instead of crashing.

* fix(auth): use Chrome Custom Tabs for Google OAuth on Android

Replace window.open with tauri-plugin-opener's inAppBrowser mode to
launch Google OAuth in Chrome Custom Tabs instead of an embedded
WebView. Google blocks OAuth in WebViews under their "Use secure
browsers" policy. Add opener plugin registration and URL scope
permissions for mobile capability.

* chore: update Claude agent configs and mise tooling

* feat(display): display route with board and focused views (#60)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* fix(display): address QA findings from PR #60 review (#61)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* feat(discovery): add discovery client and server URL flow (#63)

* docs(planning): add Feature 009 plan for discovery client + server URL

Creates the full 4-phase planning artifacts for Refactor A of the onboarding
redesign: server URL input with auto-discovery replaces raw Supabase credential
entry as the primary setup flow.

Tasks: S001, S001-T, S002, S003, S003-T, S004, S004-D
Feature: 009-discovery-client-server-url
ADR-006

* feat(setup): add discovery client and server URL flow

Replace raw Supabase credential inputs on the setup screen with a
primary server address field that auto-discovers backend credentials
via GET /.well-known/ardent-forge.json. Manual Supabase URL and key
entry moves into a collapsible "Manual configuration" section as a
fallback.

- Add discoverInstance() in src/lib/discovery.ts with URL normalization,
  8-second AbortController timeout, and typed DiscoveryResult discriminated
  union (NETWORK_ERROR, NOT_FOUND, INVALID_RESPONSE)
- Add 11 Vitest unit tests covering happy path, URL normalization variants,
  and all error branches
- Refactor setup.tsx: server URL primary input, discovery + validation
  state machine, "or" divider, collapsible manual section, disabled QR
  placeholder for future Refactor B
- Add /.well-known/ardent-forge.json handle block to Caddyfile with
  {$SITE_URL} and {$ANON_KEY} env var interpolation
- Add ANON_KEY to caddy service environment in docker-compose.yml
- Document discovery endpoint for static host and Docker deployments
  in docs/self-hosting.md; update mobile app configuration section

Tasks: S001, S001-T, S002, S003, S003-T, S004
Feature: 009-discovery-client-server-url
ADR: ADR-006

* docs(planning): mark Feature 009 Steps.md as complete

* fix(discovery): harden URL validation, use Zod, deduplicate connect logic

- Validate URL protocol via new URL() parse; reject non-http/https schemes
  (file://, ftp://, etc.) before any fetch is made
- Replace manual typeof field checks with a Zod schema (DiscoverySchema)
  to align with project convention and improve type safety
- Extract shared validateAndSave() helper in setup.tsx, eliminating
  duplicated validate-save-init-navigate block between handleConnect
  and handleDiscoverAndConnect
- Add 4 new tests: file:// rejection, ftp:// rejection, empty string,
  whitespace-only input

Feature: 009-discovery-client-server-url

* fix(review): resolve PR review findings for discovery feature

Fixes (P6-001..008): add logging to all error paths, capture error
params in catch blocks, correct error type semantics (INVALID_INPUT
for validation, INVALID_RESPONSE for JSON parse), fix JSDoc, add
aria-expanded to disclosure toggle, add double-submission guard,
fix comment scope.

Refactor: unify 4 parallel state variables into a single SetupState
discriminated union with 7 phases, eliminating impossible states.
Also wraps handlers in try-catch (P6-009..011) and fixes success
state ordering as part of the refactor.

Rules: create .claude/rules/error-handling.md codifying error type
semantics and catch block conventions (P6-017, P6-018).

Tasks: S005..S007-T added to Steps.md for schema tightening and
test coverage gaps (P6-012..015).

Review: Context/Reviews/0006-discovery-client-server-url-2026-04-04.md

* fix(discovery): tighten schema validators and add missing test coverage

S005: Use z.string().url() for supabase_url, z.string().min(1) for
supabase_publishable_key and version in DiscoverySchema. Validation
is now strictest at the system boundary.

S005-T: Add test for missing supabase_publishable_key field.
S006-T: Add test for 500 HTTP response (non-404 error code).
S007-T: Add test for URL with path component being stripped.

Review: P6-012, P6-013, P6-014, P6-015

* docs(review): mark discovery feature review as verified

* feat(display): idle mode with Edge Function broadcast (#62)

* docs(planning): add feature 009 idle mode spec, tech plan, and steps

Full 4-phase blueprint for Step 29 (Idle Mode + Edge Function).

- Spec.md: 8 must-haves, 17 testable assertions, all open questions
  resolved (24h clock default, /display?clock=12h override, max 3
  sessions, silently hide schedule when Edge Function unreachable)
- Tech.md: 8 architecture decisions -- idle_snapshot event type
  extension, Realtime HTTP broadcast API, Supabase cron schedule,
  program hierarchy JOIN query, server_time drift correction, CSS-only
  transitions, TanStack Router search param validation
- Steps.md: 10 tasks across 4 phases, frontend-specialist +
  backend-engineer + quality-engineer, parallel Phase 1 (types + Edge
  Function) and Phase 2 (ClockDisplay + IdleView + transition wrapper)

* feat(display): implement idle mode with Edge Function broadcast

Adds idle mode to the /display route -- the ambient state shown when no
workouts are active. A server-side Edge Function queries today's remaining
scheduled sessions every 60 seconds and broadcasts them to the display
channel so the TV always shows useful information.

- IdleSnapshot Zod schema + idle_snapshot event type in DisplayEventType
- ClockDisplay component (8rem Space Grotesk, server-time drift correction)
- IdleView component (clock, date, sessions list, next-up badge, footer)
- useDisplayMode hook deriving idle/board/focused from session map
- useIdleSnapshot hook subscribing to display Broadcast channel
- DisplayModeTransition wrapper (300ms fade for idle↔board, 400ms zoom for board↔focused)
- /display route composed end-to-end with all hooks and components
- display-idle-snapshot Edge Function with cron every 60 seconds
- get_display_idle_sessions RPC (service_role only, filters completed sessions)
- /display bypasses config guard in __root.tsx (public, no-auth route)
- docs/self-hosting.md: Remote Display section

Tasks: S001, S001-T, S002, S003, S003-T, S004, S005, S005-T, S006, S006-T, S007, S008, S009, S009-D
Feature: 009-idle-mode

* docs(planning): mark feature 009 idle mode as complete

Feature: 009-idle-mode

* fix(display): address QA findings from idle mode review

Frontend:
- Refactor useDisplayMode to use setState-during-render pattern (React
  "storing info from previous renders") instead of refs-in-useMemo,
  fixing react-hooks/refs ESLint violations and the ref-as-dep stability bug
- Add NaN guard and 60s periodic re-sync to ClockDisplay offset to handle
  NTP corrections and DST transitions on long-running displays
- Wire real Realtime channel status to connectionStatus in display route,
  replacing the hardcoded 'connected' placeholder
- Fix useIdleSnapshot cleanup to safely void removeChannel promise; add
  dev-mode console.warn on safeParse failures for easier debugging
- Fix IdleView session key to use display_name+session_name instead of
  array index
- Remove redundant cleanup() calls in display test afterEach blocks

Backend:
- Add day_of_week IS NOT NULL guard to get_display_idle_sessions RPC to
  prevent silent exclusion of sessions with unset day_of_week
- Add composite index idx_workout_logs_session_user_date to prevent full
  table scan on the cron-driven workout_logs LEFT JOIN
- Replace CREATE OR REPLACE with DROP IF EXISTS + CREATE for atomic
  GRANT application
- Remove non-null assertions on sessions[0] in Edge Function
- Add LIMIT 3 comment and verify_jwt rationale comment in config
- Improve Edge Function error messages to include error codes and response
  status for easier production debugging

Feature: 009-idle-mode

* fix(display): resolve PR review findings from 0006-idle-mode-review

Fixes: clock drift bug (P6-001), null client silence (P6-002),
lying connection status (P6-003), broad path bypass (P6-004),
duplicate channel (P6-005), missing CLOSED handling (P6-006),
production-silent parse errors (P6-007), duplicate DisplayMode type
(P6-008), unsafe RPC cast (P6-009), redundant comments (P6-010)

Tasks: S011-S014 added to Steps.md for missing test coverage
Rules: .min(1) on display-facing strings, Tech.md D4 corrected

Review: Context/Reviews/0006-idle-mode-review.md

* test(display): add review remediation tests S011-S014

S011: useIdleSnapshot hook tests (9 tests) -- valid/invalid payload,
null client, cleanup, connection status callback for all channel states
S012: DisplayModeTransition tests (8 tests) -- transition class mapping
for all mode pairs including duration assertions
S013: IdleSnapshot schema edge cases (7 tests) -- invalid session_type,
empty arrays, invalid ISO dates, .min(1) enforcement
S014: ClockDisplay correction test strengthened to assert exact time value

Review: Context/Reviews/0006-idle-mode-review.md

* docs(reviews): mark 0006-idle-mode-review as verified

All 16 findings confirmed resolved by review-verify agent.

Feature: 009-idle-mode

* fix(display): add onIdleSnapshot to subscriber test mock handlers

Fixes TS build error from merge integration -- the test's mock handlers
were missing the new onIdleSnapshot callback added to DisplayEventHandlers.

* chore(harness): update skills, hooks, and Claude Code settings

* docs: update hosting PRD and add onboarding refactoring plans

* feat(onboarding): QR code gen + scan for server setup (#64)

* docs(planning): add Feature 010 QR code gen + scan blueprint

Spec, Tech, and Steps for Refactor B of the onboarding redesign
(QR code generation in Settings + scan/paste on setup screen).
Covers invite link format, qrcode.react config, barcode scanner
plugin setup, and isTauri()-based platform branching. Updates
onboarding-refactoring-plans.md with researched plugin specifics.

Feature: 010-qr-code-gen-scan

* feat(onboarding): add QR code server sharing and scan on setup

Adds QR-based server sharing to reduce friction for onboarding new
users to a self-hosted Ardent Forge instance.

- Settings backend section now shows a "Share this server" area with
  a QRCodeSVG encoding `ardentforge://connect?url=...&key=...` and a
  "Copy invite link" button, guarded by hasConfig()
- Setup screen QR button activates native camera scan via
  @tauri-apps/plugin-barcode-scanner (Tauri/Android) or toggles an
  inline paste field (browser); both paths parse the invite link and
  auto-trigger connection validation
- New src/lib/invite-link.ts provides buildInviteLink() and
  parseInviteLink() as a pure shared utility
- Barcode scanner plugin registered under #[cfg(mobile)] and
  barcode-scanner:default added to mobile capabilities

Tasks: S001, S002, S003, S004, S005, S006
Feature: 010-qr-code-gen-scan

* fix(qr-scan): address QA findings from code review

- Add .catch() to unawaited processInviteLink() calls in paste/keydown
  handlers to prevent silent unhandled promise rejections
- Replace duplicate dynamic import of cancel with cancelRef to avoid
  redundant module resolution on scan overlay dismiss
- Change perms === 'denied' guard to perms !== 'granted' for defensive
  permission handling on non-standard permission states
- Move tauri-plugin-barcode-scanner to target-specific mobile dep in
  Cargo.toml to avoid enlarging desktop binary
- Remove unreachable pathname fallback branch from parseInviteLink
- Add comment to buildInviteLink documenting publishable key intent
- Rename misleading test and add true malformed-URL test coverage
- Apply rustfmt formatting to db.rs

Feature: 010-qr-code-gen-scan

* fix(qr-scan): resolve P7 review findings and add component tests

Fix bare catch blocks in handleCopy and connection-validator, add
try/catch to processInviteLink to prevent UI getting stuck, handle
unhandled promise from handleScan onClick, add user-facing error
toasts for clipboard failures, fix buildInviteLink to use SCHEME/HOST
constants, downgrade parse error log to silent null return, add
aria-label to paste field, improve QR scan failure toast message.

Add component tests for processInviteLink valid/invalid flows and
backend-settings share section conditional rendering and copy button.

Tasks: S006-T1, S006-T2
Feature: 010-qr-code-gen-scan
Review: Context/Reviews/0007-pr64-qr-code-gen-scan-review.md

* docs(planning): resolve P7 review and add S006-T1/T2 tasks to Steps.md

Mark all 14 findings in the QR code gen/scan review as resolved.
Add S006-T1 (processInviteLink component tests) and S006-T2
(backend-settings share section tests) to Feature 010 Steps.md.

Feature: 010-qr-code-gen-scan

* docs(review): mark P7 review verified by review-verify agent

* refactor(review-resolve): simplify bulk triage to bulk accept

Renames bulk triage to bulk accept to better reflect the workflow --
the capture category already IS the suggested action. Removes the
requirement for a reason on discard (individual and bulk).

* chore(harness): add gitignore entries and reformat hooks

Adds .claude/backups/, .claude/worktrees/, .serena/, and
hooks/skill-activation/.state/ to .gitignore. Reformats all hook
TypeScript files to double-quote style for consistency.

* feat(builders): progressive disclosure and contextual help (Step 14.5)

* docs(planning): add Step 14.5 feature plan and user flow docs

Documents the progressive disclosure and contextual help plan
for the template builder and program builder (Step 14.5).
Adds Flow 7 builder progressive disclosure and contextual help
system diagrams to 10-user-flows.md.

Feature: 010-Progressive-Disclosure
Tasks: S001–S014-T

* feat(builders): progressive disclosure and contextual help (Step 14.5)

Adds context-aware field visibility, staged group creation, set scheme
filtering, and inline help to the template and program builders.

New primitives and utilities:
- HelpTrigger component (Popover desktop / Drawer mobile via useMediaQuery)
- shadcn Popover and Drawer with Iron & Ember styling
- useMediaQuery hook (SSR-safe, listener cleanup)
- CollapsedFieldsRow for expandable hidden field groups
- visibility-maps.ts: data-driven lookup tables for all field visibility rules
- help-content.ts: GROUP_TYPE_HELP, BLOCK_TYPE_HELP, SOURCE_HELP constants
- text-label-large and text-body-small typography utilities in index.css

Template builder changes:
- Category-aware Scoring/Time Cap visibility with CollapsedFieldsRow
- Activity group staged reveal: Stage 1 (type selector only), Stage 2 (type-
  dependent rest fields + exercises)
- Set scheme type list filtered by session category; "Show all types" escape
- Load type options filtered by set scheme type; null = hide entire section
- Session picker gains "Create new event" inline creation flow

Program builder changes:
- Block type HelpTrigger with one-liner summary (desktop + mobile)
- Source badge HelpTrigger with all 7 sources

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013
Feature: 010-Progressive-Disclosure

* fix(builders): resolve QA findings from progressive disclosure review

Fixes two blocking hook violations and seven warnings across the builder
components introduced in Step 14.5.

Errors fixed:
- Move useCallback above early return in LoadSpecEditor to satisfy
  Rules of Hooks (conditional hook call)
- Rewrite useMediaQuery with useSyncExternalStore to eliminate
  synchronous setState in effect and double render

Warnings fixed:
- Remove eslint-disable suppression in LoadSpecEditor; expand useEffect
  deps to [allowedLoads, availableTypes, value.type, onChange]
- Replace div[role=button] nested inside <button> in MobileDayRow with
  sibling <button> elements; fix event type to MouseEvent | KeyboardEvent
- Add useId/aria-controls pairing to CollapsedFieldsRow for screen
  reader disclosure association
- Dynamic aria-label on HelpTrigger (Help: ${title} instead of static)
- Extract BLOCK_TYPE_STYLES to constants.ts; remove duplication across
  block-editor and mobile-block-editor
- Remove unused placement prop from HelpTrigger and all four call sites
- Fix PopoverTitle to render <h2> matching its ComponentProps<'h2'> type

Suggestions addressed:
- Sync ladderText in DescendingRepsFields when repLadder resets externally
- Use touchLastAssigned.mutate (stable ref) in useCallback deps
- Add clientId to ActivityGroupData and ActivityData; generate via
  crypto.randomUUID() at creation; use as React key instead of index
- Add aria-expanded/aria-controls to ProgramForm details toggle

Feature: 010-Progressive-Disclosure

* fix(builders): resolve PR review findings P7

Fixes: P7-001 (silent userId guard), P7-002 (JSX triplication),
P7-003 (redundant satisfies), P7-004 (unstable useEffect deps),
P7-005 (block type help duplication), P7-007 (missing JSDoc),
P7-008 (HelpEntry interface), P7-009 (non-empty tuple),
P7-010 (scheme map typing), P7-011 (constants typing)
Rules: P7-013, P7-014 added to typescript-conventions.md;
P7-015 added to error-handling.md
Deferred: P7-012 (ErrorBoundary strategy) to backlog
Discarded: P7-006 (superseded by P7-002 extraction)

Review: Context/Reviews/0007-pr65-progressive-disclosure-review.md

* fix(builders): resolve review-verify type errors and mark review passed

Fix 3 pre-existing TS errors from P7 review fixes: collapsedLabels
cast to non-empty tuple, allowedLoads.includes widened for
LoadSpec['type'], defaultTypes.includes widened for SetSchemeType.
Review file verified and checkbox marked.

* feat: implement Android CI/CD pipeline (Feature 011) (#67)

* docs(ci-cd): add full blueprint for Android CI/CD pipeline

4-phase planning for feature 011: GitHub Actions workflows covering PR
validation (lint, typecheck, unit tests, E2E via Playwright + local
Supabase, Android debug APK build), tagged release pipeline (signed AAB,
Supabase migrations gate, GitHub Release, Play Store internal track
upload), and supporting setup documentation.

Feature: 011-ci-cd-pipeline
ADR-007: GitHub Actions chosen as CI/CD platform

* ci(github-actions): implement complete Android CI/CD pipeline

Adds two GitHub Actions workflows from scratch:

- ci.yml: PR validation with three parallel jobs -- lint/typecheck/unit
  tests, Playwright E2E against local Supabase, and Android debug APK
  build with full Rust/NDK toolchain and caching
- release.yml: tag-triggered release pipeline -- signed AAB build and
  Supabase migrations run in parallel, gated publish job creates GitHub
  Release and uploads to Play Store internal track

Supporting changes:
- Add Playwright (@playwright/test) with smoke tests and test:e2e script
- Add signingConfigs block to build.gradle.kts reading from env vars
- Add .github/BRANCH_PROTECTION.md and .github/README.md
- Add CI/Release workflow badges to README.md

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009
Feature: 011-ci-cd-pipeline

* fix(android-ci): harden pipeline per PR #67 review findings

- Guard signingConfig assignment with keystorePath null check
- Validate KEYSTORE_BASE64 before decode; check decoded file size
- Pass all secrets through env: blocks (not inline shell expansion)
- Add secret validation steps to build-release, migrate, publish jobs
- Reorder publish job: Play Store upload before GitHub Release
- Capture Supabase status once; validate non-empty/non-null values
- Add if-no-files-found: ignore to Playwright report upload
- Fix test:e2e script to use bunx playwright test
- Add webServer timeout: 30_000, stdout/stderr: pipe to playwright.config.ts

Tasks: P8-001, P8-002, P8-003, P8-004, P8-005, P8-006, P8-007, P8-008, P8-015, P8-016

* docs(ci-cd): apply post-review documentation corrections

- Tech.md: Four->Two workflows, remove armv7 target, fix port 4173,
  update signing snippet to null-guard pattern
- Steps.md: add VITE_ prefix to env var names; add S010 secret validation
- ADR-007: status Proposed->Accepted
- Add PR #67 review capture (16/16 findings resolved and verified)
- Update review-resolve skill

Tasks: P8-009, P8-010, P8-011, P8-012, P8-013, P8-014, P8-015

* feat(deep-link): handle ardentforge://connect deep links (#66)

* docs(planning): add Feature 011 deep link handler planning artifacts

Full 4-phase plan for ardentforge://connect deep link handling.
Covers Tauri dispatch extension, browser /connect route, setup
screen pre-population, and transient store for settings handoff.

Feature: 011-deep-link-handler
ADR-007: transient-store-deep-link-state

* feat(deep-link): handle ardentforge://connect deep links

Implements the connect deep link handler so tapping an invite link
opens the app directly to the correct screen with credentials
pre-populated.

- Register ardentforge://connect host in Tauri deep-link config
- Add handleConnectLink() with 4-way branch: invalid, unconfigured,
  same instance, different instance
- Dispatch connect vs auth in onOpenUrl listener (OAuth flow untouched)
- Add /connect browser route as fallback for web users
- Exempt /connect from root route guard
- Setup screen accepts url/key search params for auto-population
- BackendSettings consumes pending connect Zustand store on mount
- Unit tests for all 5 handleConnectLink() branches (5/5 passing)

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010
Feature: 011-deep-link-handler
ADR: ADR-007 (transient Zustand store for cross-route deep link state)

* fix(deep-link): address QA findings from review

- Add desktop deep-link scheme config + permissions to default capability
- Fix bare catch blocks in invite-link.ts and auth.tsx (error-handling convention)
- Refactor setState-in-effect in backend-settings.tsx to useRef guard
- Add HTTPS validation for url param in parseInviteLink
- Accept navigate callback in handleConnectLink (avoids full-page reload in router context)
- Update OAuth tests to mock @tauri-apps/plugin-opener instead of window.open
- Add test for getConfig() returning null with hasConfig true
- Add test for non-HTTPS url param rejection
- Fix setup.test.tsx mock to provide Route.useSearch()

Feature: 011-deep-link-handler

* fix(deep-link): address PR-66 review findings

- Wrap handleConnectLink in try-catch with toast on error (P8-001)
- Isolate connect-link error path from OAuth error handler in
  auth.tsx (P8-002)
- Replace render-phase setState with useRef + useState initializers
  in backend-settings.tsx (P8-003)
- Replace unsafe `as string` casts with typeof guards in
  validateSearch for /connect and /setup routes (P8-004)
- Add toast feedback when auto-validate fails in /setup (P8-005)
- Replace startsWith check with new URL() + protocol/hostname
  validation in parseInviteLink (P8-006)
- Fix bare catch blocks in config-store.ts to capture and log
  errors (P8-007)
- Add store-boundary validation to usePendingConnect.setPending
  (P8-009)
- Add state-management.md coding rule for Zustand conventions
- Add review capture with verification (0008-pr66)
- Add test step S011 to Steps.md (P8-008)

* test(deep-link): add /connect route and auth connect-dispatch tests

- Add connect.test.tsx covering search param validation, pending
  store integration, and redirect behavior
- Update auth-oauth tests for connect deep-link error isolation

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* fix(ci): upgrade supabase/setup-cli from v1 to v2

v1 runs on deprecated Node 20 and installs an older CLI that rejects
newer config keys (oauth_server, web3, cron, etc.), breaking the
migrate job on tagged releases.

* fix(ci): pin supabase CLI to v2.84.4 to match local version

setup-cli@v1 defaults to CLI v2.20.3 which rejects newer config keys.
Pinning to 2.84.4 matches local dev environment. setup-cli v2 is
merged but not yet tagged as a release.

* fix(supabase): remove invalid cron key from config.toml

The [functions.display-idle-snapshot.cron] section is not a valid
config.toml option. Edge function cron scheduling is configured via
pg_cron in SQL, not in config.toml. This was causing supabase link
to fail in CI.

* fix(sync): add HTTP status check and empty update_set guard in pull path (#70)

* fix(sync): add HTTP status check and empty update_set guard in pull path

pull_table deserialized Supabase responses without checking the HTTP
status code, which could silently swallow 4xx/5xx errors or produce
confusing deserialization failures. Added a status check matching the
pattern already used in push.rs.

upsert_row produced invalid SQL ("... ON CONFLICT(id) DO UPDATE SET ")
when the only matching column was `id`. Now falls back to INSERT OR
IGNORE for that edge case.

Closes #68, closes #69

* fix(lint): resolve unused var, explicit any, and unused import errors

- Prefix unused catch parameter with underscore in formatter hook
- Add eslint-disable for necessary `any` cast in test helper
- Remove unused `buildRequest` import from webhook test

* fix(lint): use bare catch block for unused error parameter

* fix(data-mapper): handle JSONB columns already parsed by Supabase PostgREST

Supabase returns JSONB columns as objects, not strings. Calling JSON.parse()
on an object stringifies it to "[object Object]" first, causing a parse error
on session_templates fields (rest_between_groups, time_cap, event_metadata),
activity_groups fields (rest_between_rounds, rest_between_activities), and
activities (set_scheme).

Adds parseJsonOrValue() helper that passes objects through directly and only
calls JSON.parse() on string values, keeping backward compatibility with the
Tauri/SQLite adapter which returns these columns as JSON strings.

* chore: batch enhancement issues cleanup (#71)

* chore: add Zod refinements to chat domain types

Add validation refinements that catch invalid data at parse time:
- conversation.ts: prevent groupId on direct conversations, enforce leftAt > joinedAt
- message.ts: prevent senderId on system messages
- media.ts: add .nonnegative() to durationSeconds and fileSizeBytes

Closes #40, closes #47

* chore: polish realtime manager (simplify clearAllTyping, fix silent catch)

- Simplify clearAllTyping to delegate to clearTypingForConversation helper
- Add error logging to broadcastTyping .catch per error-handling conventions

Closes #42, closes #49

* feat(events): wire EventCountdownBadge navigation to dedicated event route

- Create /events/$templateId route with minimal event detail page
- Update EventCountdownBadge to navigate to new route instead of /library
- Remove _templateId unused-var rename and TODO comment

Closes #31

* perf: batch getUnreadCounts queries with Promise.allSettled

Replace sequential N+1 COUNT queries with concurrent execution via
Promise.allSettled, preserving partial-results error handling.

Closes #39, closes #46

* perf: optimize chat query patterns (CH-2 trigger, findDirectConversation)

- Add error context with [tauri-adapter] prefix to findDirectConversation loop
- Create materialized direct_conversation_pairs lookup table with unique index
- Replace O(N) trigger scan with O(1) index-backed uniqueness check
- Add cleanup trigger for participant departures

Closes #41, closes #48

* docs: add quick plan for enhancement issues batch

* fix(review): resolve PR #71 review findings

Fixes (7): RLS on direct_conversation_pairs, ISO datetime comparison,
findDirectConversation all-fail throw, getUnreadCounts error…
* fix: skip gym picker prompt after first workout is completed

After users complete their first workout (initial onboarding), they
should not be prompted to select a gym/display every time. The app
now uses their last saved gym choice automatically.

Changes:
- Import readLastGymChoice in Today page and Library page
- Check firstWorkoutCompleted before showing gym picker
- Use saved gym choice when available, fallback to picker if missing
- Apply same pattern to both handleStartWorkout and handleStartProgrammedSession
- Apply same pattern to handleStartFromTemplate in Library

This improves the user experience by reducing friction for returning
users while preserving the onboarding experience for new users.

* chore: workflow updates
…dialogs (#100)

* feat(builder): move template/event creation out of sidebar into full dialogs

Create new template and event forms now open in a full-width Dialog (max-w-3xl)
instead of rendering inline inside the narrow SessionPickerSheet drawer.

- Add CreateTemplateDialog and CreateEventDialog components
- Remove showCreate/showCreateEvent state and inline form branches from
  SessionPickerSheet; add onCreateTemplate/onCreateEvent callback props
- Lift dialog state to BuilderPage; auto-select newly created template
  on save and close both dialog and picker sheet

* refactor(library): unify template/event form surface to dialogs

Replace the bottom sheet in library.tsx with the same Dialog surface
used by the program builder. Generalise the create-only wrappers into
SessionTemplateDialog and EventTemplateDialog (session-builder/ and
event-builder/ respectively), adding optional editingId support with
inline loaders so both create and edit flows share a single component.

Removes EditTemplateFormLoader and EditEventFormLoader from library.tsx.
…lutions

* fix: add Authorization header to Supabase reachability check

The PostgREST root endpoint requires both `apikey` and `Authorization`
headers. Missing the latter caused a 401 during setup, showing
"Cannot reach server" even with valid credentials.

* fix: auto-validate and surface env var connection failures on setup page

When VITE_SUPABASE_URL and VITE_SUPABASE_PUB_KEY are set but invalid,
the setup page now auto-validates on mount and shows the specific error
(e.g. "Server responded with 401") instead of silently presenting a
blank form. Also adds a banner explaining that environment variables
were detected but failed.

* fix: use Supabase client for connection validation instead of raw fetch

Raw fetch with apikey/Authorization headers only works with legacy anon
keys. New publishable keys (sb_publishable_xxx) require the Supabase
client library which handles auth headers internally. This switch
supports both key formats transparently.

* fix: resolve duplicate migration timestamp collision

Two migrations shared timestamp 20260329000001, causing a primary key
violation in supabase_migrations. Renumbered create_sharing_tables to
20260329000002 and expand_rls_for_sharing to 20260329000003.

* chore: update Claude Code configuration and skills

* feat: complete sync pull path with dynamic upsert and force-pull (Step 9) (#28)

Implement the stubbed pull/upsert path so remote changes from Supabase
are actually written to local SQLite, completing bidirectional sync.

- Fix broken timestamp parsing in upsert_row (ISO 8601 strings were
  silently parsed as 0, causing all remote updates to be discarded)
- Implement dynamic column upsert via pragma_table_info with type
  coercion (timestamps, booleans, JSONB) between Supabase and SQLite
- Add pull_table/pull_all for paginated force-pull via Supabase REST API
- Wire sync_force_pull command with proper state transitions
- Add auto-pull on first sign-in (detect last_pull_at == 0)
- Add 28 new tests (49 total), all passing

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: events & packing lists (Step 13.5) (#30)

* feat(events): add events & packing lists feature (Step 13.5)

Introduces EVENT as a new session category with full support for event
metadata (date, location, URL, requirements) and a categorized packing
checklist with drag-and-drop reorder, optimistic toggle, progress
tracking, and countdown notifications.

Database:
- Migration adds EVENT to category CHECK constraints on session_templates
  and scheduled_sessions; adds event_metadata JSONB to session_templates
  and workout_logs; creates event_items table with polymorphic FK
  (template XOR log), RLS policies, partial indices, and updated_at trigger

Domain:
- eventRequirementSchema, eventMetadataSchema, eventItemSchema (Zod)
- EVENT added to SessionType enum; eventMetadata optional on templates
  and logs; EventItemRow added to database.types.ts

Data layer:
- toEventItem/fromEventItem mappers; eventMetadata parsing in
  toSessionTemplate (JSON.parse) and toWorkoutLog (direct JSONB)
- DataAdapter interface: getEventItems, saveEventItem, updateEventItem,
  deleteEventItem, toggleEventItemPacked (accepts target bool, no race),
  reorderEventItems (Promise.all), cloneSessionTemplate (isPacked reset)
- SupabaseAdapter implements all methods; getSessionTemplateFull fetches
  eventItems for EVENT templates; updateSessionTemplateFull uses 3-way
  reconciliation to preserve item UUIDs and isPacked state
- TauriAdapter stubs (event support deferred, W-8)

Hooks:
- useEventItems, useCreateEventItem, useUpdateEventItem,
  useDeleteEventItem, useToggleEventItemPacked (optimistic),
  useReorderEventItems (optimistic), useNextUpcomingEvent

UI:
- event-builder/: EventTemplateForm (Sheet overlay, create/edit),
  EventDetail (metadata + map link + packing), PackingList (DnD,
  collapsible categories, error feedback), PackingItem (checkbox toggle),
  EventProgressBar (ember on surface-steel, ARIA progressbar),
  EventCard (library), EventCountdownBadge (Today screen, TODAY edge
  case), RequirementEditor, EventItemEditor, shared styles.ts
- Library: EventCard for EVENT templates, NEW EVENT button
- log/$workoutId: interactive EventDetail for event logs
- history/$workoutId: read-only EventDetail for event history
- Today screen: countdown badge when next event within 30 days
- Program builder: flag icon + ember accent for EVENT sessions,
  event date displayed in program-preview grid and detail sections

Native (Rust):
- event_reminders channel registered in notification.rs
- EventReminderState: background Tokio task (60s poll), checks
  configured intervals (7d/3d/1d), quiet hours, packing progress in
  notification body, deterministic notification ID
- schedule_event_reminders / cancel_event_reminders Tauri commands
- Clippy fixes: RemindedState type alias, needless_borrow in lib.rs,
  manual_range_contains in session_reminder.rs
- Lock contention fix: single mutex guard across event loop in tick()

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 002-events-packing-lists

* docs(planning): mark 002-events-packing-lists complete

Updates Steps.md status to Complete, session.md with full wave
execution log and fixes applied, and implementation-plan.md.

Feature: 002-events-packing-lists

* chore(rust): apply cargo fmt across codebase

* chore: add mise config and cortex hooks

* fix(events): address PR #30 review findings

- Fix timezone bug in combineDateTime: store local date strings instead
  of converting to UTC, preventing event dates from shifting by a day
- Add eventUrl validation in form validate() before save
- Switch sequential item persistence to Promise.allSettled with error
  collection for partial-failure reporting
- Fix DST-unsafe daysUntil calculation using date-only comparison
- Make TauriAdapter getEventItems throw consistently with write stubs
- Add "TODAY" special case in Rust event reminder notification title
- Eliminate double hydration of requirements/items in form init
- Add EventItem mapper roundtrip tests and splitDateTime/combineDateTime
  edge case tests (19 new tests)

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(groups): coach program assignment

Adds the ability for a group coach to assign one of their programs to a
member. The program is atomically transferred to the member's ownership
with source tagged as COACH_ASSIGNED and the coach recorded as created_by,
enabling the member to filter out programs they received vs created.

Includes the two-step AssignProgramDialog UI and integrates it into
MemberCard. Both the Tauri SQLite command and the Supabase SECURITY
DEFINER RPC perform the same three authorization checks (caller is COACH,
target is MEMBER, program owned by caller) before executing the transfer.

* fix(sync): resolve incremental push breakage and missing error state

- push.rs: timestamp_millis() -> timestamp() so last_push_at is stored
  in seconds, matching the updated_at column it filters against; the
  millis value was always larger than any updated_at, causing zero rows
  to be found on every push after the first
- sync.rs: sync_force_push now transitions to SyncState::Error on
  failure, matching the sync_force_pull pattern; previously a failed
  push left the engine in Idle, hiding the error from the frontend

* docs(planning): add spec, tech, and steps for coach program assignment

* feat(chat): add chat data layer + QA fixes (#33)

* feat(chat): add chat data layer (Step 21)

Builds the full persistence layer for the chat domain across Supabase,
SQLite, Rust, and TypeScript. No UI -- pure data plumbing for Steps 22-25.

- Supabase migration: conversations, conversation_participants, messages,
  media_attachments with RLS (CH-1), append-only messages (CH-7), and
  direct conversation uniqueness via trigger (CH-2)
- SQLite migration: mirrors Postgres with sync_status on messages for
  offline queueing (pending -> synced/failed state machine)
- Rust: ConversationRow/MessageRow/etc structs, 10 Tauri commands (CRUD
  for conversations, messages, participants, media), chat tables in
  SYNCABLE_TABLES for push/pull sync
- TypeScript: Zod schemas (Conversation, Message, MediaAttachment,
  WorkoutSnapshot), DB row types, bidirectional mappers, 13-method
  ChatAdapter facet on DataAdapter interface, Supabase and Tauri
  adapter implementations, mock adapter updated

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011
Feature: 004-Chat-Data-Layer

* docs(planning): add spec and steps for chat data layer (Step 21)

Feature 004 planning artifacts: Spec.md covering requirements,
testable assertions, and invariants (CH-1, CH-2, CH-4, CH-7);
Steps.md with 4-wave execution plan for db-engineer, rust-engineer,
domain-engineer, and quality-engineer.

Feature: 004-Chat-Data-Layer

* fix(qa): address all QA feedback -- sync resilience, lint, and stale tests

Rust sync hardening:
- sync/mod.rs: replace `return` with `continue` so queue flush failure no longer kills the sync loop permanently; transition to Error state when initial pull fails rather than silently proceeding to push
- sync/pull.rs: add SYNCABLE_TABLES allowlist guard in upsert_row and delete_row
- sync/push.rs: add SYNCABLE_TABLES allowlist guard in push_table
- sync/queue.rs: normalize enqueue timestamp to seconds (was millis, inconsistent with push/pull)

Frontend lint:
- packing-list.tsx: replace setState-in-useEffect anti-pattern with useMemo derivation for mutation error feedback
- event-template-form.tsx: extract splitDateTime/combineDateTime to date-time-utils.ts to fix Fast Refresh violation (non-component exports alongside component)

Tests (31 stale failures fixed):
- Update 5 test files with correct mixed-case text after CSS uppercase refactor
- Rewrite mobile-nav.test.tsx: add useAuth mock, update to 5+1 nav structure, correct icons and routes

* fix(review): resolve PR #33 review findings for chat data layer

Critical fixes (C-1 through C-5):
- Add missing INSERT policy on conversations table (C-1)
- Filter user's own messages from getUnreadCounts in Supabase adapter (C-2)
- Change z.url() to z.string().url() for media attachment URLs (C-3)
- Return Err instead of Ok(()) from SYNCABLE_TABLES allowlist guards (C-4)
- Add missing UPDATE policy on media_attachments table (C-5)

Important fixes (I-2, I-3):
- Add participant check before message insert in send_message (I-2)
- Validate direct conversations require exactly 2 participants (I-3)

Tasks created (S013-S017) for: data mapper tests, dedicated
get_messages_since command, updatedAt mapping fix, unread counts
resilience, and sync guard test coverage.

Suggestions S-1 through S-6 tracked in Ideas backlog.

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

* feat(chat): implement review remediation tasks S013-S017

S013: Add 36 data mapper unit tests for chat entities
  - Conversation, ConversationParticipant, Message, MediaAttachment
  - Round-trip, null handling, and Zod validation tests

S014: Add dedicated get_messages_since Rust command
  - SQL-level WHERE created_at > ? filter replaces client-side filtering
  - Eliminates silent truncation at 1000 message limit

S015: Fix toConversationParticipant updatedAt mapping
  - Use last_read_at ?? joined_at instead of always joined_at

S016: Improve getUnreadCounts error resilience
  - Single conversation failure no longer aborts all counts
  - Returns partial results with console.warn for failed queries

S017: Remove #[cfg(not(test))] from sync allowlist guards
  - Tests now exercise the same allowlist path as production
  - Updated test fixtures to use real SYNCABLE_TABLES entries

Review: Context/Reviews/0001-pr33-chat-data-layer-review.md

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: Supabase Realtime, chat data layer, events & coach assignment (#34)

* docs(planning): add spec, tech, and steps for supabase realtime integration

4-phase blueprint for Step 22 (Supabase Realtime Integration).
Establishes architecture for Broadcast channel management, cursor-based
message pagination, RealtimeManager module, and all chat TanStack Query hooks.

ADR-005: cursor-based getMessages replaces offset-based (no existing callers).

* feat(chat): add Supabase Realtime broadcast channels and chat hooks

Wire up Supabase Realtime Broadcast for live message delivery and typing
indicators. Introduces the RealtimeManager singleton, cursor-based message
pagination, platform-agnostic foreground detection, and all TanStack Query
hooks needed by the Chat UI (Step 23).

New files:
- src/lib/realtime-schemas.ts     -- Zod schemas for broadcast payloads
- src/lib/realtime-manager.ts     -- channel lifecycle, typing state, catch-up
- src/lib/foreground-detector.ts  -- visibilitychange / Tauri focus detection
- src/hooks/use-chat.ts           -- useConversations, useMessages (infinite),
                                     useSendMessage (optimistic), useRealtimeMessages, +6 more
- src/components/chat-realtime-listener.tsx -- app-level cache update listener

Modified:
- data-adapter.ts / supabase-adapter.ts / tauri-adapter.ts / chat.rs
  Replace offset-based getMessages with cursor-based (before?: ISO timestamp)
- main.tsx: mount ChatRealtimeListener alongside SyncListener
- auth.tsx: call resetRealtimeManager() on SIGNED_OUT

Tasks: S001, S002, S003, S004, S005, S006, S007

* fix: resolve QA lint and code quality issues across frontend and Rust

Fix clippy errors in test code (manual_range_contains, approx_constant),
ESLint no-constant-binary-expression in utils test, and three code quality
issues in realtime-manager: validate typing payloads with Zod schema,
replace fragile onTyping callback chain with Set-based listener pattern,
and explicitly discard handleForeground Promise.

* fix(chat): address PR #34 review findings for Supabase Realtime

Multi-listener pattern (addMessageListener/addTypingListener) replacing
single-callback ownership, broadcast try/catch in useSendMessage,
channel lifecycle ownership fix, auth integration via useAuth hook,
and Wave 5 polish: named constants, toMessageFromBroadcast mapper,
simplified clearAllTyping, foreground-detector error logging and
async/await rewrite, broadcastTyping .catch(), MessagePaginationOptions
interface, user_name max(200) constraint.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat(chat): complete chat UI with messaging and conversations (#35)

* docs(planning): add spec, tech, and steps for chat UI (Step 23)

* feat(chat): build complete chat UI (Step 23)

Routes, conversation list, virtualized message detail, compose bar,
typing indicators, new conversation flow, group management, and
client-side blocking. Navigation updated -- COMMS replaces Social
on mobile, added after Vault on desktop sidebar with unread badge.

Conversation type extended with participantUserIds so ConversationDetail
can resolve the other user's display name and blocking state for direct
conversations. Supabase adapter fetches participants alongside
conversations; Tauri adapter was already receiving them and now uses them.

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013, S014
Feature: 006-Chat-UI

* docs(planning): add quick plan for conversation participant IDs

Feature: 006-Chat-UI

* fix: update mobile-nav tests for Comms tab and fix Rust formatting

Update test assertions for Social→Comms rename, add useUnreadCounts
mock, and fix rustfmt formatting in event_reminder tests.

* fix(chat): resolve PR #35 review findings

Critical fixes:
- C1: Contact search filter was a no-op; now passes search prop to
  ContactRow which renders null when display name does not match
- C2: Sender name in group bubbles showed raw UUID; resolved via
  useUserProfile inside MessageBubble
- C3: Send failure gave zero feedback; added sendError state and
  inline "Failed to send" indicator on ComposeBar
- C4: Conversation creation failure was silent; added error state
  and inline message in NewConversationSheet
- C5: MessageList had no error/loading state; added spinner and
  error fallback from useMessages isLoading/isError

Important fixes:
- I1: Typing broadcast used user.email; now uses displayName via
  useUserProfile
- I2/I3: useBlockedUsers storage key scoped to userId; added
  console.error/warn on localStorage corruption
- I4: Optimistic message had senderId undefined; now uses current
  user ID from useAuth
- I5: LeaveDialog failure was silent; added inline error message
- I6: ParticipantSheet add failure was silent; shows error including
  Tauri offline message
- I7: Removed rounded-full from avatar containers and unread dots
- I8: Added comment clarifying group threshold (2 selected + server
  adds self = 3 total participants)

Suggestions:
- S3: Removed build-artifact comment from conversation-list.tsx
- S9: Fixed error copy from "pull to retry" to "try again"

Tasks: C1, C2, C3, C4, C5, I1, I2, I3, I4, I5, I6, I7, I8, S3, S9
Feature: 006-Chat-UI

* docs(planning): resolve PR #35 review and add test/polish tasks

Marks 0003-pr35-chat-ui-review.md as fully resolved (5 critical,
8 important, 2 suggestions fixed inline; 6 tasks created; 5
suggestions dismissed with rationale).

Adds Wave 7 tasks to Steps.md:
- S015-T: deriveItems unit tests
- S016-T: useBlockedUsers unit tests
- S017-T: formatTypingText unit tests
- S018-T: relativeTime + getInitials unit tests
- S019: Migrate useBlockedUsers to Zustand store
- S020: Wire archive action in conversation header

Feature: 006-Chat-UI

* refactor(chat): extract utils, add blocked-users store, verify review

Extract pure functions (relativeTime, getInitials, formatTypingText,
deriveItems) into chat-utils.ts and add tests. Migrate useBlockedUsers
to Zustand store with user-scoped persistence. Mark PR #35 review as
verified with follow-up tracked in GitHub issues #43-#51.

---------

Co-authored-by: Robert Hamilton <rhamilton@intuitivemachines.com>

* feat: add link preview metadata with OG tags, Twitter Cards, and useMetadata hook (#53)

Add comprehensive link preview support so shared URLs display rich previews
on social platforms and messaging apps. Includes a useMetadata hook for
dynamic per-route title and description updates, used on share pages.

* fix(auth): prevent PKCE code race condition on OAuth callback (#54)

Supabase JS v2.100.1 defaults to detectSessionInUrl: true with PKCE
flow, causing _initialize() to auto-exchange the auth code before the
/auth/callback route handler can. The second exchange attempt fails
(code already consumed), redirecting users to /sign-in?reason=oauth_error.

Disable detectSessionInUrl since we have a dedicated callback route
that handles PKCE code exchange manually.

* fix(auth): enable PKCE flow for OAuth sign-in (#55)

Without explicit flowType: 'pkce', the Supabase client uses implicit
flow, returning tokens as hash fragments (#access_token=...) that the
/auth/callback route cannot read (it expects ?code= query params).

Setting flowType: 'pkce' makes Supabase return a PKCE authorization
code as a query parameter that the callback can exchange for a session.

* fix(db): resolve RLS infinite recursion on group_members and conversation_participants (#56)

The group_members and conversation_participants SELECT policies
self-referenced their own tables, triggering PostgreSQL error 42P17
(infinite recursion). This cascaded 500s to every table whose sharing
policies join group_members (workout_logs, session_templates, etc.).

Creates SECURITY DEFINER helpers (is_group_member, is_group_coach,
is_conversation_participant) that bypass RLS for inner membership
lookups and replaces all six affected policies.

Also removes the PWA manifest (manifest.webmanifest) and its link tag
since Vercel deployment protection returns 401 on the unauthenticated
manifest fetch.

* feat(builder): program builder UI with responsive week grid (#58)

* feat(builder): program builder UI with responsive week grid

Implements the complete program builder and session builder UIs.

Program builder:
- Sidebar layout (desktop) / stacked layout (mobile) at /builder route
- Drag-and-drop block reordering with @dnd-kit
- Week grid defaults to 5-day Mon-Fri view with a toggle to show all 7
  days, improving readability at constrained content widths (~94px/cell
  at 1024px narrowed to nothing legible; 5-day gives ~132px/cell min)
- Session slots reduced from 80px to 56px min-height with type-colored
  left borders (STRENGTH/CONDITIONING/SE/MIXED/EVENT)
- Mobile block editor with vertical day-row tap targets
- Session picker bottom sheet and copy-week dialog
- Program preview overlay with per-session exercise/load tables

Session builder:
- Set scheme editor with per-type form variants
- Activity group editor and session template form

Infrastructure:
- Collapsible shadcn/ui component added
- Removed boilerplate App.tsx/App.css
- Expanded index.css with builder-specific design tokens

* fix(builder): declutter block header with overflow menu

Replace inline action buttons (move up/down, delete) with a dropdown
overflow menu in both desktop and mobile block editors. This prevents
touch target overlap on narrow viewports and isolates the destructive
delete action behind a two-tap interaction.

* feat(builder): guard destructive actions with confirmation and undo

Block and week deletions now require confirmation via AlertDialog,
showing impact ("This will remove N weeks and M sessions.") before
proceeding. Session removal fires immediately but shows a 5-second
undo toast via sonner, letting users recover accidental taps without
blocking the fast path.

- Add alert-dialog.tsx shadcn primitive (Radix AlertDialog)
- Add confirm-delete-dialog.tsx reusable builder component
- Wire block delete in BlockEditor and MobileBlockCard to dialog
- Wire week delete in WeekGrid and MobileWeekSection to dialog
- Add undo toast to SessionSlot and MobileDayRow session removal
- Mount Toaster in main.tsx (bottom-center)

* feat(builder): replace full-screen preview with inline week detail

Remove the disconnected ProgramPreview overlay from the builder flow.
Exercise details now toggle inline per-week via a visibility icon,
keeping the edit-preview feedback loop tight. Save button moves to
the header. Shared formatting helpers extracted to session-detail.tsx.
Also fixes Collapsible asChild bug in block-editor and mobile-block-editor.

* fix(builder): remove interstitial from session template create flow

Tapping "Create new template" in the session picker now opens the
inline form immediately, skipping the Library recommendation callout.
The Library option is preserved as a subtle link ("Want more options?
Create in the Library") below the form for users who need it.

Removes showCreateCallout state and the two-step reveal pattern.

* refactor(builder): consolidate session constants and polish UI details

- Extract SESSION_TINT and SESSION_TYPE_BADGE from session-slot,
  mobile-block-editor, and program-preview into constants.ts
- Remove duplicate @keyframes block-enter and slot-fill in index.css
- Bump section-level labels (WEEK N, day columns, EXERCISE/SETS/LOAD)
  from text-[11px] font-medium to text-xs font-semibold for visual
  hierarchy over metadata badges which stay at text-[11px] font-medium
- Upgrade session picker SheetTitle to text-sm font-display
- Add empty state copy when zero blocks exist in BlockList and
  MobileBlockEditor
- Clarify copy-week dialog: description now states sessions will be
  replaced, and per-week "(N existing)" label gains warning colour

* feat(builder): replace collapsed save error with structured inline validation

Validation errors now appear at the relevant field rather than as a single
joined string below the save button. Program name errors render under the
name input, block name and week errors render on each block card (visible
even when collapsed), and server/auth errors remain global in the header.

* feat(builder): add recently-used templates and reduce week grid noise

Surface the 5 most recently assigned session templates at the top of the
picker sheet so users with large template libraries reach common picks
faster. Adds lastAssignedAt tracking across both Tauri/SQLite and
Supabase backends.

Reduce visual noise in the week grid: restyle empty slots as muted
"Rest" labels instead of prominent calls-to-action, bring the 5/7-day
toggle to mobile, and collapse identical weeks within a block to a
compact summary row (with manual expand).

* refactor(builder): extract session-detail helpers to fix Fast Refresh lint errors

* feat: display broadcast infrastructure (Step 27) (#59)

* feat(chat): video, image, and file sharing (#57)

feat(chat): video, image, and file sharing (Feature 007)

Full-stack media sharing: Cloudflare Stream video uploads with TUS, image/file uploads via Supabase Storage, real-time transcoding status via webhook, signed URL caching, and inline media rendering in chat.

Review: 4 critical + 10 important findings resolved and verified. 12 enhancements implemented. 1,305 tests passing.

* docs: add plan for Step 27 -- display broadcast infrastructure

Spec.md, Tech.md, and Steps.md for Feature 008. Covers DisplaySnapshot
Zod schema, publisher module, store integration, display_visible toggle,
and Supabase Realtime Broadcast channel plumbing. 14 tasks across 4 waves
with 3 agents.

* feat: add display_visible column to user profiles

SQLite and Supabase migrations for display broadcast toggle,
Tauri model and command updates to support the new field.

* feat: add DisplaySnapshot domain type and display publisher

DisplaySnapshot type with Zod schema, display-publisher for
broadcasting workout state, data-mapper and store updates.

* feat: add PushToDisplayButton and display broadcast hook

useDisplayBroadcast hook, PushToDisplayButton component,
workout log and profile route integrations.

* test: add display snapshot and publisher tests

Unit tests for DisplaySnapshot schema, display-publisher,
display-snapshot builder, and updated data-mapper/adapter tests.

* docs: update Steps.md and implementation plan for display broadcast

* fix(review): resolve PR #59 review findings

Fixes: C1 (RLS auth), C2 (image signed URLs), C3 (snapshot try-catch),
C4 (Cloudflare subdomain), I1-I9 (error handling, schema tightening,
z.url() revert, publisher readiness), M1/M3/M5/M6 (env guards,
request parsing, query error logging, JSDoc)
Tasks: S024-T, S025-T added for media-provider and timeout tests
Comments: 9 comment issues resolved

Review: Context/Reviews/0005-pr59-display-broadcast-review.md

* test(media): add unit tests for CloudflareStreamProvider and transcoding timeout

S024-T: media-provider tests covering signed URL TTL cache hit/miss,
null client error, Zod validation of malformed responses, and cache
deduplication (13 tests).

S025-T: use-media-upload tests covering 5-minute transcoding timeout
marks processing attachment as failed, unaffected ready attachments,
query cache invalidation, and timer cleanup on unmount (4 tests).

Tasks: S024-T, S025-T
Feature: 007-video-image-sharing

* docs(review): mark PR #59 review as verified

* feat(edge-functions): deploy CF video edge functions with Vault secrets and local JWT signing

Deploys three Supabase Edge Functions for Cloudflare Stream video support
(Step 25). All Cloudflare credentials read from Supabase Vault using
CF_* secret names (CF_ACCOUNT_ID, CF_STREAM_TOKEN, CF_WEBHOOK_SECRET,
CF_SIGNING_KEY_PEM, CF_SIGNING_KEY_ID, CF_CUSTOMER_SUBDOMAIN).

chat-media-signed-url: replaces Cloudflare token API round-trip with
local RS256 JWT signing via CF_SIGNING_KEY_PEM + CF_SIGNING_KEY_ID.
No outbound HTTP call needed per playback request.
chat-media-upload-url: reads CF_ACCOUNT_ID + CF_STREAM_TOKEN from Vault.
chat-media-webhook: reads CF_WEBHOOK_SECRET + CF_ACCOUNT_ID from Vault.

Adds get_secret() SQL migration granting service_role-only read access
to vault.decrypted_secrets. Fixes duplicate migration timestamp
20260403000002 -> 20260403000003 for session_template_last_assigned.

Updates mock-supabase test util to support keyed RPC results
(get_secret:SECRET_NAME) for parameter-specific vault mocking.
Removes CLOUDFLARE_ACCOUNT_ID from STANDARD_ENV test fixture.

* chore: update app icons and branding assets

Replace default Tauri icons with Ardent Forge branding across all
platforms (Android, iOS, Windows, macOS). Update app display name
in Android strings.xml and tauri.conf.json.

* fix(db): rename migration 008 and auto-recreate DB on checksum mismatch

Rename session_template_last_assigned to migration 009 to make room
for 008_add_display_visible. Add debug-only auto-recreate logic so
migration checksum mismatches during development delete and rebuild
the local database instead of crashing.

* fix(auth): use Chrome Custom Tabs for Google OAuth on Android

Replace window.open with tauri-plugin-opener's inAppBrowser mode to
launch Google OAuth in Chrome Custom Tabs instead of an embedded
WebView. Google blocks OAuth in WebViews under their "Use secure
browsers" policy. Add opener plugin registration and URL scope
permissions for mobile capability.

* chore: update Claude agent configs and mise tooling

* feat(display): display route with board and focused views (#60)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* fix(display): address QA findings from PR #60 review (#61)

* feat(display): display route with board and focused views (Step 28)

Implements the /display route -- a full-viewport, unauthenticated gym TV
page that subscribes to the Supabase Broadcast display channel and renders
active workout sessions in real time.

Board view: CSS grid of session cards (1-4 per page), auto-cycling every
10 seconds when 5+ sessions are active. Each card shows display name,
session type, elapsed time, current exercise, set progress, and a compact
rest timer.

Focused view: full-screen single-user layout with set table (prescribed vs
actual with ember accent on current set), large rest timer with pulse
animation under 10 seconds, and distance-readable typography (2-3.5rem).

Idle placeholder: large clock with NO ACTIVE SESSIONS label.

Infrastructure added:
- display-store.ts: Zustand store with sessions Map, focus state,
  pagination, staleness pruning
- display-subscriber.ts: Broadcast channel subscriber with Zod validation
  and connection status callbacks
- use-timer-interpolation.ts: rAF-based countdown hook (1 React re-render
  per second, not per frame)
- display_hello responder: phones re-publish their snapshot on reconnect
  so the display rebuilds state after network drops

Tasks: S001, S002, S003, S004, S001-T, S005, S006, S007
Feature: 009-display-route-board

* docs(context): feature spec, tech plan, and steps for display route (009)

Adds planning artifacts for Step 28: display route with board view,
focused view, timer interpolation, and display_hello reconnection.

Feature: 009-display-route-board

* refactor(display): address QA findings from board/focused route review

- Fix React Compiler lint error: remove synchronous setSeconds(0) from
  useEffect in use-timer-interpolation; derive idle value via return instead
- Extract useElapsedTime hook; remove duplicated useState+setInterval logic
  from session-card and focused-view
- Add exponential backoff reconnection to display-subscriber (2s base,
  30s max); destroyDisplaySubscriber cancels pending retry
- Fix focused-view snapshot type: replace Map infer gymnastics with direct
  DisplaySnapshot import
- Fix pruneStale: early-return when no sessions are stale to avoid
  unnecessary Map clones and Zustand notifications
- Fix board-view interval: use getState().setCurrentPage() consistently
  instead of mixing captured selector with getState() reads
- Fix duplicate JSDoc on setHelloResponder in display-publisher
- Use cn() helper in page-indicator for conditional classes
- Add comment in display.tsx explaining dedicated Supabase client
- Add pruneStale edge case tests: ghost key in lastSeenAt, early-return
  Map identity check

Feature: 009-display-route-board

* fix(display): address QA findings from board/focused route review

Resolves all 14 findings from PR #60 review (P6-001 through P6-014):

Critical fixes:
- Call publishHello() on reconnect via _hasConnectedBefore flag (P6-001)
- Remove dead channel with removeChannel() before nulling _channel (P6-002)
- Wrap boot() in try-catch; set disconnected status on failure (P6-003)

Store fixes:
- Merge dual sessions/lastSeenAt maps into single Map<string, SessionEntry> (P6-013)
- Reset currentPage to 0 in clearAllSessions (P6-004)
- Clamp setCurrentPage to [0, totalPages-1] (P6-005)
- Extract DisplayConnectionStatus to display-snapshot.ts; import everywhere (P6-014)

Subscriber fixes:
- Call onStatusChange('disconnected') before early return when client null (P6-006)

Hook fixes:
- Guard useElapsedTime against NaN timestamps; clamp elapsed to Math.max(0) (P6-007)

Comment fix:
- Update __root.tsx bypass comment to include /display (P6-008)

Tasks: P6-001, P6-002, P6-003, P6-004, P6-005, P6-006, P6-007, P6-008, P6-013, P6-014
Feature: 009-display-route-board

* docs(context): add Wave 4 follow-up tasks and mark review verified

- Add S008-S011 to Steps.md Wave 4 (reconnect retry tests, use-elapsed-time
  tests, schema .min(1) hardening, invalid payload tests for session_ended
  and focus events)
- Add review capture file 0006-pr60-display-route-board-review.md with all
  findings and resolutions documented
- Mark review verified after confirming all fixes landed

Tasks: P6-009, P6-010, P6-011, P6-012
Feature: 009-display-route-board

* feat(discovery): add discovery client and server URL flow (#63)

* docs(planning): add Feature 009 plan for discovery client + server URL

Creates the full 4-phase planning artifacts for Refactor A of the onboarding
redesign: server URL input with auto-discovery replaces raw Supabase credential
entry as the primary setup flow.

Tasks: S001, S001-T, S002, S003, S003-T, S004, S004-D
Feature: 009-discovery-client-server-url
ADR-006

* feat(setup): add discovery client and server URL flow

Replace raw Supabase credential inputs on the setup screen with a
primary server address field that auto-discovers backend credentials
via GET /.well-known/ardent-forge.json. Manual Supabase URL and key
entry moves into a collapsible "Manual configuration" section as a
fallback.

- Add discoverInstance() in src/lib/discovery.ts with URL normalization,
  8-second AbortController timeout, and typed DiscoveryResult discriminated
  union (NETWORK_ERROR, NOT_FOUND, INVALID_RESPONSE)
- Add 11 Vitest unit tests covering happy path, URL normalization variants,
  and all error branches
- Refactor setup.tsx: server URL primary input, discovery + validation
  state machine, "or" divider, collapsible manual section, disabled QR
  placeholder for future Refactor B
- Add /.well-known/ardent-forge.json handle block to Caddyfile with
  {$SITE_URL} and {$ANON_KEY} env var interpolation
- Add ANON_KEY to caddy service environment in docker-compose.yml
- Document discovery endpoint for static host and Docker deployments
  in docs/self-hosting.md; update mobile app configuration section

Tasks: S001, S001-T, S002, S003, S003-T, S004
Feature: 009-discovery-client-server-url
ADR: ADR-006

* docs(planning): mark Feature 009 Steps.md as complete

* fix(discovery): harden URL validation, use Zod, deduplicate connect logic

- Validate URL protocol via new URL() parse; reject non-http/https schemes
  (file://, ftp://, etc.) before any fetch is made
- Replace manual typeof field checks with a Zod schema (DiscoverySchema)
  to align with project convention and improve type safety
- Extract shared validateAndSave() helper in setup.tsx, eliminating
  duplicated validate-save-init-navigate block between handleConnect
  and handleDiscoverAndConnect
- Add 4 new tests: file:// rejection, ftp:// rejection, empty string,
  whitespace-only input

Feature: 009-discovery-client-server-url

* fix(review): resolve PR review findings for discovery feature

Fixes (P6-001..008): add logging to all error paths, capture error
params in catch blocks, correct error type semantics (INVALID_INPUT
for validation, INVALID_RESPONSE for JSON parse), fix JSDoc, add
aria-expanded to disclosure toggle, add double-submission guard,
fix comment scope.

Refactor: unify 4 parallel state variables into a single SetupState
discriminated union with 7 phases, eliminating impossible states.
Also wraps handlers in try-catch (P6-009..011) and fixes success
state ordering as part of the refactor.

Rules: create .claude/rules/error-handling.md codifying error type
semantics and catch block conventions (P6-017, P6-018).

Tasks: S005..S007-T added to Steps.md for schema tightening and
test coverage gaps (P6-012..015).

Review: Context/Reviews/0006-discovery-client-server-url-2026-04-04.md

* fix(discovery): tighten schema validators and add missing test coverage

S005: Use z.string().url() for supabase_url, z.string().min(1) for
supabase_publishable_key and version in DiscoverySchema. Validation
is now strictest at the system boundary.

S005-T: Add test for missing supabase_publishable_key field.
S006-T: Add test for 500 HTTP response (non-404 error code).
S007-T: Add test for URL with path component being stripped.

Review: P6-012, P6-013, P6-014, P6-015

* docs(review): mark discovery feature review as verified

* feat(display): idle mode with Edge Function broadcast (#62)

* docs(planning): add feature 009 idle mode spec, tech plan, and steps

Full 4-phase blueprint for Step 29 (Idle Mode + Edge Function).

- Spec.md: 8 must-haves, 17 testable assertions, all open questions
  resolved (24h clock default, /display?clock=12h override, max 3
  sessions, silently hide schedule when Edge Function unreachable)
- Tech.md: 8 architecture decisions -- idle_snapshot event type
  extension, Realtime HTTP broadcast API, Supabase cron schedule,
  program hierarchy JOIN query, server_time drift correction, CSS-only
  transitions, TanStack Router search param validation
- Steps.md: 10 tasks across 4 phases, frontend-specialist +
  backend-engineer + quality-engineer, parallel Phase 1 (types + Edge
  Function) and Phase 2 (ClockDisplay + IdleView + transition wrapper)

* feat(display): implement idle mode with Edge Function broadcast

Adds idle mode to the /display route -- the ambient state shown when no
workouts are active. A server-side Edge Function queries today's remaining
scheduled sessions every 60 seconds and broadcasts them to the display
channel so the TV always shows useful information.

- IdleSnapshot Zod schema + idle_snapshot event type in DisplayEventType
- ClockDisplay component (8rem Space Grotesk, server-time drift correction)
- IdleView component (clock, date, sessions list, next-up badge, footer)
- useDisplayMode hook deriving idle/board/focused from session map
- useIdleSnapshot hook subscribing to display Broadcast channel
- DisplayModeTransition wrapper (300ms fade for idle↔board, 400ms zoom for board↔focused)
- /display route composed end-to-end with all hooks and components
- display-idle-snapshot Edge Function with cron every 60 seconds
- get_display_idle_sessions RPC (service_role only, filters completed sessions)
- /display bypasses config guard in __root.tsx (public, no-auth route)
- docs/self-hosting.md: Remote Display section

Tasks: S001, S001-T, S002, S003, S003-T, S004, S005, S005-T, S006, S006-T, S007, S008, S009, S009-D
Feature: 009-idle-mode

* docs(planning): mark feature 009 idle mode as complete

Feature: 009-idle-mode

* fix(display): address QA findings from idle mode review

Frontend:
- Refactor useDisplayMode to use setState-during-render pattern (React
  "storing info from previous renders") instead of refs-in-useMemo,
  fixing react-hooks/refs ESLint violations and the ref-as-dep stability bug
- Add NaN guard and 60s periodic re-sync to ClockDisplay offset to handle
  NTP corrections and DST transitions on long-running displays
- Wire real Realtime channel status to connectionStatus in display route,
  replacing the hardcoded 'connected' placeholder
- Fix useIdleSnapshot cleanup to safely void removeChannel promise; add
  dev-mode console.warn on safeParse failures for easier debugging
- Fix IdleView session key to use display_name+session_name instead of
  array index
- Remove redundant cleanup() calls in display test afterEach blocks

Backend:
- Add day_of_week IS NOT NULL guard to get_display_idle_sessions RPC to
  prevent silent exclusion of sessions with unset day_of_week
- Add composite index idx_workout_logs_session_user_date to prevent full
  table scan on the cron-driven workout_logs LEFT JOIN
- Replace CREATE OR REPLACE with DROP IF EXISTS + CREATE for atomic
  GRANT application
- Remove non-null assertions on sessions[0] in Edge Function
- Add LIMIT 3 comment and verify_jwt rationale comment in config
- Improve Edge Function error messages to include error codes and response
  status for easier production debugging

Feature: 009-idle-mode

* fix(display): resolve PR review findings from 0006-idle-mode-review

Fixes: clock drift bug (P6-001), null client silence (P6-002),
lying connection status (P6-003), broad path bypass (P6-004),
duplicate channel (P6-005), missing CLOSED handling (P6-006),
production-silent parse errors (P6-007), duplicate DisplayMode type
(P6-008), unsafe RPC cast (P6-009), redundant comments (P6-010)

Tasks: S011-S014 added to Steps.md for missing test coverage
Rules: .min(1) on display-facing strings, Tech.md D4 corrected

Review: Context/Reviews/0006-idle-mode-review.md

* test(display): add review remediation tests S011-S014

S011: useIdleSnapshot hook tests (9 tests) -- valid/invalid payload,
null client, cleanup, connection status callback for all channel states
S012: DisplayModeTransition tests (8 tests) -- transition class mapping
for all mode pairs including duration assertions
S013: IdleSnapshot schema edge cases (7 tests) -- invalid session_type,
empty arrays, invalid ISO dates, .min(1) enforcement
S014: ClockDisplay correction test strengthened to assert exact time value

Review: Context/Reviews/0006-idle-mode-review.md

* docs(reviews): mark 0006-idle-mode-review as verified

All 16 findings confirmed resolved by review-verify agent.

Feature: 009-idle-mode

* fix(display): add onIdleSnapshot to subscriber test mock handlers

Fixes TS build error from merge integration -- the test's mock handlers
were missing the new onIdleSnapshot callback added to DisplayEventHandlers.

* chore(harness): update skills, hooks, and Claude Code settings

* docs: update hosting PRD and add onboarding refactoring plans

* feat(onboarding): QR code gen + scan for server setup (#64)

* docs(planning): add Feature 010 QR code gen + scan blueprint

Spec, Tech, and Steps for Refactor B of the onboarding redesign
(QR code generation in Settings + scan/paste on setup screen).
Covers invite link format, qrcode.react config, barcode scanner
plugin setup, and isTauri()-based platform branching. Updates
onboarding-refactoring-plans.md with researched plugin specifics.

Feature: 010-qr-code-gen-scan

* feat(onboarding): add QR code server sharing and scan on setup

Adds QR-based server sharing to reduce friction for onboarding new
users to a self-hosted Ardent Forge instance.

- Settings backend section now shows a "Share this server" area with
  a QRCodeSVG encoding `ardentforge://connect?url=...&key=...` and a
  "Copy invite link" button, guarded by hasConfig()
- Setup screen QR button activates native camera scan via
  @tauri-apps/plugin-barcode-scanner (Tauri/Android) or toggles an
  inline paste field (browser); both paths parse the invite link and
  auto-trigger connection validation
- New src/lib/invite-link.ts provides buildInviteLink() and
  parseInviteLink() as a pure shared utility
- Barcode scanner plugin registered under #[cfg(mobile)] and
  barcode-scanner:default added to mobile capabilities

Tasks: S001, S002, S003, S004, S005, S006
Feature: 010-qr-code-gen-scan

* fix(qr-scan): address QA findings from code review

- Add .catch() to unawaited processInviteLink() calls in paste/keydown
  handlers to prevent silent unhandled promise rejections
- Replace duplicate dynamic import of cancel with cancelRef to avoid
  redundant module resolution on scan overlay dismiss
- Change perms === 'denied' guard to perms !== 'granted' for defensive
  permission handling on non-standard permission states
- Move tauri-plugin-barcode-scanner to target-specific mobile dep in
  Cargo.toml to avoid enlarging desktop binary
- Remove unreachable pathname fallback branch from parseInviteLink
- Add comment to buildInviteLink documenting publishable key intent
- Rename misleading test and add true malformed-URL test coverage
- Apply rustfmt formatting to db.rs

Feature: 010-qr-code-gen-scan

* fix(qr-scan): resolve P7 review findings and add component tests

Fix bare catch blocks in handleCopy and connection-validator, add
try/catch to processInviteLink to prevent UI getting stuck, handle
unhandled promise from handleScan onClick, add user-facing error
toasts for clipboard failures, fix buildInviteLink to use SCHEME/HOST
constants, downgrade parse error log to silent null return, add
aria-label to paste field, improve QR scan failure toast message.

Add component tests for processInviteLink valid/invalid flows and
backend-settings share section conditional rendering and copy button.

Tasks: S006-T1, S006-T2
Feature: 010-qr-code-gen-scan
Review: Context/Reviews/0007-pr64-qr-code-gen-scan-review.md

* docs(planning): resolve P7 review and add S006-T1/T2 tasks to Steps.md

Mark all 14 findings in the QR code gen/scan review as resolved.
Add S006-T1 (processInviteLink component tests) and S006-T2
(backend-settings share section tests) to Feature 010 Steps.md.

Feature: 010-qr-code-gen-scan

* docs(review): mark P7 review verified by review-verify agent

* refactor(review-resolve): simplify bulk triage to bulk accept

Renames bulk triage to bulk accept to better reflect the workflow --
the capture category already IS the suggested action. Removes the
requirement for a reason on discard (individual and bulk).

* chore(harness): add gitignore entries and reformat hooks

Adds .claude/backups/, .claude/worktrees/, .serena/, and
hooks/skill-activation/.state/ to .gitignore. Reformats all hook
TypeScript files to double-quote style for consistency.

* feat(builders): progressive disclosure and contextual help (Step 14.5)

* docs(planning): add Step 14.5 feature plan and user flow docs

Documents the progressive disclosure and contextual help plan
for the template builder and program builder (Step 14.5).
Adds Flow 7 builder progressive disclosure and contextual help
system diagrams to 10-user-flows.md.

Feature: 010-Progressive-Disclosure
Tasks: S001–S014-T

* feat(builders): progressive disclosure and contextual help (Step 14.5)

Adds context-aware field visibility, staged group creation, set scheme
filtering, and inline help to the template and program builders.

New primitives and utilities:
- HelpTrigger component (Popover desktop / Drawer mobile via useMediaQuery)
- shadcn Popover and Drawer with Iron & Ember styling
- useMediaQuery hook (SSR-safe, listener cleanup)
- CollapsedFieldsRow for expandable hidden field groups
- visibility-maps.ts: data-driven lookup tables for all field visibility rules
- help-content.ts: GROUP_TYPE_HELP, BLOCK_TYPE_HELP, SOURCE_HELP constants
- text-label-large and text-body-small typography utilities in index.css

Template builder changes:
- Category-aware Scoring/Time Cap visibility with CollapsedFieldsRow
- Activity group staged reveal: Stage 1 (type selector only), Stage 2 (type-
  dependent rest fields + exercises)
- Set scheme type list filtered by session category; "Show all types" escape
- Load type options filtered by set scheme type; null = hide entire section
- Session picker gains "Create new event" inline creation flow

Program builder changes:
- Block type HelpTrigger with one-liner summary (desktop + mobile)
- Source badge HelpTrigger with all 7 sources

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010, S011, S012, S013
Feature: 010-Progressive-Disclosure

* fix(builders): resolve QA findings from progressive disclosure review

Fixes two blocking hook violations and seven warnings across the builder
components introduced in Step 14.5.

Errors fixed:
- Move useCallback above early return in LoadSpecEditor to satisfy
  Rules of Hooks (conditional hook call)
- Rewrite useMediaQuery with useSyncExternalStore to eliminate
  synchronous setState in effect and double render

Warnings fixed:
- Remove eslint-disable suppression in LoadSpecEditor; expand useEffect
  deps to [allowedLoads, availableTypes, value.type, onChange]
- Replace div[role=button] nested inside <button> in MobileDayRow with
  sibling <button> elements; fix event type to MouseEvent | KeyboardEvent
- Add useId/aria-controls pairing to CollapsedFieldsRow for screen
  reader disclosure association
- Dynamic aria-label on HelpTrigger (Help: ${title} instead of static)
- Extract BLOCK_TYPE_STYLES to constants.ts; remove duplication across
  block-editor and mobile-block-editor
- Remove unused placement prop from HelpTrigger and all four call sites
- Fix PopoverTitle to render <h2> matching its ComponentProps<'h2'> type

Suggestions addressed:
- Sync ladderText in DescendingRepsFields when repLadder resets externally
- Use touchLastAssigned.mutate (stable ref) in useCallback deps
- Add clientId to ActivityGroupData and ActivityData; generate via
  crypto.randomUUID() at creation; use as React key instead of index
- Add aria-expanded/aria-controls to ProgramForm details toggle

Feature: 010-Progressive-Disclosure

* fix(builders): resolve PR review findings P7

Fixes: P7-001 (silent userId guard), P7-002 (JSX triplication),
P7-003 (redundant satisfies), P7-004 (unstable useEffect deps),
P7-005 (block type help duplication), P7-007 (missing JSDoc),
P7-008 (HelpEntry interface), P7-009 (non-empty tuple),
P7-010 (scheme map typing), P7-011 (constants typing)
Rules: P7-013, P7-014 added to typescript-conventions.md;
P7-015 added to error-handling.md
Deferred: P7-012 (ErrorBoundary strategy) to backlog
Discarded: P7-006 (superseded by P7-002 extraction)

Review: Context/Reviews/0007-pr65-progressive-disclosure-review.md

* fix(builders): resolve review-verify type errors and mark review passed

Fix 3 pre-existing TS errors from P7 review fixes: collapsedLabels
cast to non-empty tuple, allowedLoads.includes widened for
LoadSpec['type'], defaultTypes.includes widened for SetSchemeType.
Review file verified and checkbox marked.

* feat: implement Android CI/CD pipeline (Feature 011) (#67)

* docs(ci-cd): add full blueprint for Android CI/CD pipeline

4-phase planning for feature 011: GitHub Actions workflows covering PR
validation (lint, typecheck, unit tests, E2E via Playwright + local
Supabase, Android debug APK build), tagged release pipeline (signed AAB,
Supabase migrations gate, GitHub Release, Play Store internal track
upload), and supporting setup documentation.

Feature: 011-ci-cd-pipeline
ADR-007: GitHub Actions chosen as CI/CD platform

* ci(github-actions): implement complete Android CI/CD pipeline

Adds two GitHub Actions workflows from scratch:

- ci.yml: PR validation with three parallel jobs -- lint/typecheck/unit
  tests, Playwright E2E against local Supabase, and Android debug APK
  build with full Rust/NDK toolchain and caching
- release.yml: tag-triggered release pipeline -- signed AAB build and
  Supabase migrations run in parallel, gated publish job creates GitHub
  Release and uploads to Play Store internal track

Supporting changes:
- Add Playwright (@playwright/test) with smoke tests and test:e2e script
- Add signingConfigs block to build.gradle.kts reading from env vars
- Add .github/BRANCH_PROTECTION.md and .github/README.md
- Add CI/Release workflow badges to README.md

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009
Feature: 011-ci-cd-pipeline

* fix(android-ci): harden pipeline per PR #67 review findings

- Guard signingConfig assignment with keystorePath null check
- Validate KEYSTORE_BASE64 before decode; check decoded file size
- Pass all secrets through env: blocks (not inline shell expansion)
- Add secret validation steps to build-release, migrate, publish jobs
- Reorder publish job: Play Store upload before GitHub Release
- Capture Supabase status once; validate non-empty/non-null values
- Add if-no-files-found: ignore to Playwright report upload
- Fix test:e2e script to use bunx playwright test
- Add webServer timeout: 30_000, stdout/stderr: pipe to playwright.config.ts

Tasks: P8-001, P8-002, P8-003, P8-004, P8-005, P8-006, P8-007, P8-008, P8-015, P8-016

* docs(ci-cd): apply post-review documentation corrections

- Tech.md: Four->Two workflows, remove armv7 target, fix port 4173,
  update signing snippet to null-guard pattern
- Steps.md: add VITE_ prefix to env var names; add S010 secret validation
- ADR-007: status Proposed->Accepted
- Add PR #67 review capture (16/16 findings resolved and verified)
- Update review-resolve skill

Tasks: P8-009, P8-010, P8-011, P8-012, P8-013, P8-014, P8-015

* feat(deep-link): handle ardentforge://connect deep links (#66)

* docs(planning): add Feature 011 deep link handler planning artifacts

Full 4-phase plan for ardentforge://connect deep link handling.
Covers Tauri dispatch extension, browser /connect route, setup
screen pre-population, and transient store for settings handoff.

Feature: 011-deep-link-handler
ADR-007: transient-store-deep-link-state

* feat(deep-link): handle ardentforge://connect deep links

Implements the connect deep link handler so tapping an invite link
opens the app directly to the correct screen with credentials
pre-populated.

- Register ardentforge://connect host in Tauri deep-link config
- Add handleConnectLink() with 4-way branch: invalid, unconfigured,
  same instance, different instance
- Dispatch connect vs auth in onOpenUrl listener (OAuth flow untouched)
- Add /connect browser route as fallback for web users
- Exempt /connect from root route guard
- Setup screen accepts url/key search params for auto-population
- BackendSettings consumes pending connect Zustand store on mount
- Unit tests for all 5 handleConnectLink() branches (5/5 passing)

Tasks: S001, S002, S003, S004, S005, S006, S007, S008, S009, S010
Feature: 011-deep-link-handler
ADR: ADR-007 (transient Zustand store for cross-route deep link state)

* fix(deep-link): address QA findings from review

- Add desktop deep-link scheme config + permissions to default capability
- Fix bare catch blocks in invite-link.ts and auth.tsx (error-handling convention)
- Refactor setState-in-effect in backend-settings.tsx to useRef guard
- Add HTTPS validation for url param in parseInviteLink
- Accept navigate callback in handleConnectLink (avoids full-page reload in router context)
- Update OAuth tests to mock @tauri-apps/plugin-opener instead of window.open
- Add test for getConfig() returning null with hasConfig true
- Add test for non-HTTPS url param rejection
- Fix setup.test.tsx mock to provide Route.useSearch()

Feature: 011-deep-link-handler

* fix(deep-link): address PR-66 review findings

- Wrap handleConnectLink in try-catch with toast on error (P8-001)
- Isolate connect-link error path from OAuth error handler in
  auth.tsx (P8-002)
- Replace render-phase setState with useRef + useState initializers
  in backend-settings.tsx (P8-003)
- Replace unsafe `as string` casts with typeof guards in
  validateSearch for /connect and /setup routes (P8-004)
- Add toast feedback when auto-validate fails in /setup (P8-005)
- Replace startsWith check with new URL() + protocol/hostname
  validation in parseInviteLink (P8-006)
- Fix bare catch blocks in config-store.ts to capture and log
  errors (P8-007)
- Add store-boundary validation to usePendingConnect.setPending
  (P8-009)
- Add state-management.md coding rule for Zustand conventions
- Add review capture with verification (0008-pr66)
- Add test step S011 to Steps.md (P8-008)

* test(deep-link): add /connect route and auth connect-dispatch tests

- Add connect.test.tsx covering search param validation, pending
  store integration, and redirect behavior
- Update auth-oauth tests for connect deep-link error isolation

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* chore(release): bump version to 1.0.0-beta

Update version across all manifests (package.json, tauri.conf.json,
Cargo.toml, Cargo.lock) and add CHANGELOG entry covering all features,
improvements, and bug fixes since v0.8.0.

Also carries forward the uncommitted AndroidManifest deep-link intent
filter from #66.

* fix(ci): upgrade supabase/setup-cli from v1 to v2

v1 runs on deprecated Node 20 and installs an older CLI that rejects
newer config keys (oauth_server, web3, cron, etc.), breaking the
migrate job on tagged releases.

* fix(ci): pin supabase CLI to v2.84.4 to match local version

setup-cli@v1 defaults to CLI v2.20.3 which rejects newer config keys.
Pinning to 2.84.4 matches local dev environment. setup-cli v2 is
merged but not yet tagged as a release.

* fix(supabase): remove invalid cron key from config.toml

The [functions.display-idle-snapshot.cron] section is not a valid
config.toml option. Edge function cron scheduling is configured via
pg_cron in SQL, not in config.toml. This was causing supabase link
to fail in CI.

* fix(sync): add HTTP status check and empty update_set guard in pull path (#70)

* fix(sync): add HTTP status check and empty update_set guard in pull path

pull_table deserialized Supabase responses without checking the HTTP
status code, which could silently swallow 4xx/5xx errors or produce
confusing deserialization failures. Added a status check matching the
pattern already used in push.rs.

upsert_row produced invalid SQL ("... ON CONFLICT(id) DO UPDATE SET ")
when the only matching column was `id`. Now falls back to INSERT OR
IGNORE for that edge case.

Closes #68, closes #69

* fix(lint): resolve unused var, explicit any, and unused import errors

- Prefix unused catch parameter with underscore in formatter hook
- Add eslint-disable for necessary `any` cast in test helper
- Remove unused `buildRequest` import from webhook test

* fix(lint): use bare catch block for unused error parameter

* fix(data-mapper): handle JSONB columns already parsed by Supabase PostgREST

Supabase returns JSONB columns as objects, not strings. Calling JSON.parse()
on an object stringifies it to "[object Object]" first, causing a parse error
on session_templates fields (rest_between_groups, time_cap, event_metadata),
activity_groups fields (rest_between_rounds, rest_between_activities), and
activities (set_scheme).

Adds parseJsonOrValue() helper that passes objects through directly and only
calls JSON.parse() on string values, keeping backward compatibility with the
Tauri/SQLite adapter which returns these columns as JSON strings.

* chore: batch enhancement issues cleanup (#71)

* chore: add Zod refinements to chat domain types

Add validation refinements that catch invalid data at parse time:
- conversation.ts: prevent groupId on direct conversations, enforce leftAt > joinedAt
- message.ts: prevent senderId on system messages
- media.ts: add .nonnegative() to durationSeconds and fileSizeBytes

Closes #40, closes #47

* chore: polish realtime manager (simplify clearAllTyping, fix silent catch)

- Simplify clearAllTyping to delegate to clearTypingForConversation helper
- Add error logging to broadcastTyping .catch per error-handling conventions

Closes #42, closes #49

* feat(events): wire EventCountdownBadge navigation to dedicated event route

- Create /events/$templateId route with minimal event detail page
- Update EventCountdownBadge to navigate to new route instead of /library
- Remove _templateId unused-var rename and TODO comment

Closes #31

* perf: batch getUnreadCounts queries with Promise.allSettled

Replace sequential N+1 COUNT queries with concurrent execution via
Promise.allSettled, preserving partial-results error handling.

Closes #39, closes #46

* perf: optimize chat query patterns (CH-2 trigger, findDirectConversation)

- Add error context with [tauri-adapter] prefix to findDirectConversation loop
- Create materialized direct_conversation_pairs lookup table with unique index
- Replace O(N) trigger scan with O(1) index-backed uniqueness check
- Add cleanup trigger for participant departures

Closes #41, closes #48

* docs: add quick plan for enhancement issues batch

* fix(review): resolve PR #71 review findings

Fixes (7): RLS on direct_conversation_pairs, ISO datetime comparison,
findDirectConversation all-fail throw, getUnreadCou…
@vercel

vercel Bot commented Apr 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ardent-forge Ready Ready Preview, Comment Apr 12, 2026 9:53pm

* fix(db): resolve duplicate migration 013 and invalid SQLite DROP COLUMN syntax

- Rename 013_workout_note_tags.sql to 014_workout_note_tags.sql to fix
  duplicate version number conflict with 013_drop_display_visible.sql
- Remove IF EXISTS from ALTER TABLE DROP COLUMN in migration 013; SQLite
  does not support that syntax variant, causing a startup panic

* chore(release): bump version to 1.0.0-beta.12

* fix(connections): show peer display names instead of raw UUIDs

Resolves connection list showing raw user UUIDs by resolving each peer
profile via the existing useUserProfile hook (gated by display_visible RLS).
Falls back to 'Connected Athlete' when the peer profile is private or has
no displayName set.

* fix(profile): make gym row navigable and hide leave button for owners

Wraps the gym name/address area in a Link to the gym detail page and
suppresses the Leave button for gym owners (who cannot leave their own gym).
Updates test coverage for the conditional leave button behavior.

This branch was successfully deployed

2 active deployments
Preview — 2661f550 Deployed Apr 12, 2026 by vercel[bot]
Production — 2661f550 Deployed Apr 12, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant