Repository navigation
fix(db): resolve startup crash from duplicate migration and invalid SQLite syntax - #104
Closed
rghamilton3 wants to merge 17 commits into
Closed
rghamilton3 wants to merge 17 commits into
rghamilton3 wants to merge 17 commits into
Conversation
* 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…
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
* 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.
rghamilton3
force-pushed
the
fix/migration-crash-beta12
branch
from
April 12, 2026 21:50
cf7f551 to
93c5f5b
Compare
rghamilton3
force-pushed
the
fix/migration-crash-beta12
branch
from
April 12, 2026 21:52
93c5f5b to
2661f55
Compare
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
013_workout_note_tags.sql→014_workout_note_tags.sqlto resolve duplicate migration version number conflict with013_drop_display_visible.sqlIF EXISTSfromALTER TABLE ... DROP COLUMNin migration 013 -- SQLite does not support this syntax variant, causing a startup panicBoth 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 passingbun run lint-- cleanbun run build-- cleanbun tauri dev-- app starts and DB initializes correctly