Timezone:
- Add diligence/utils/dates.py with today_for_user(), now_for_user(), day_start_utc()
- Replace all 20 date.today() calls across 10 files with timezone-aware helpers
- Remove hardcoded Asia/Bangkok offset hack in nutrition router
- Change defaults from Asia/Bangkok to UTC in user model, nutrition model, config
- Add timezone field to RegisterRequest schema
- Set user.timezone from browser detection during registration
- Frontend sends Intl.DateTimeFormat().resolvedOptions().timeZone on register
Branding:
- Login page: Fitness Rewards -> Diligence, updated tagline and emoji
- index.html title: Fitness Rewards -> Diligence
Frontend rebuild:
- Fresh Vite build with all changes (index-CsKoBN0D.js)
- Nav bar, timezone detection, and branding all verified in built bundle
- New AgentConnect.jsx page with MCP URL, token, setup instructions
- Claude Desktop config snippet with copy button
- Claude Code CLI command with copy button
- Agent capabilities overview
- Added to bottom nav bar (6th item, plug icon)
- Backend /api/agent/config endpoint returns deployment-aware MCP details
- Nav padding adjusted for 6 items
- Frontend rebuilt with new page
Bug: API_TOKEN replacement ran before .env existed, and before the
existence check. On a clean clone the script errored immediately.
Fixed flow: banner → existence check → generate SECRET_KEY →
generate API_TOKEN → copy .env.example → replace both keys.
Also adds MCP connection info to output (matching setup.sh).
Root cause: ai_provider.py assumed a single JSON blob in an
'encrypted_value' column, but IntegrationConfig stores each credential
as a separate row with config_key + config_value columns.
Fixes:
- Query groups rows by provider, decrypts each config_value separately
- Correct decrypt_value(secret_key, ciphertext) argument order
- Filter by user_id so providers are per-user
- Callers (ai_chat.py, chat()) now pass user_id through
SEC-11 removed algorithm from Settings but missed 3 call sites:
- auth.py line 68: jwt.decode in get_current_user
- integrations.py lines 58, 120: jwt.decode in OAuth callbacks
All now use ALGORITHM constant or inline 'HS256'.
The SEC-06 replacement swallowed the trailing comma into the comment:
allow_credentials=False # comment, ← comma in comment
Fixed to:
allow_credentials=False, # comment ← comma after value
Previous commit wiped App.jsx due to emoji encoding error in the
patch script. Restored from Stream A commit (c075eb9), then applied
ChatCoach changes using a file-based Python script to handle unicode.
- import ChatCoach from './pages/ChatCoach' (line 21)
- NavLink /chat with brain emoji + 'Coach' label (line 95)
- Route /chat -> ProtectedRoute -> ChatCoach (line 125)
WHOOP developer registration requires device ownership. Sync service
will be built against their documented API; first WHOOP-owning user
serves as the integration tester. Focus on Garmin and Oura first.
Frontend:
- ChatCoach.jsx: streaming chat UI with SSE, provider indicator, suggestion
chips, 'no AI configured' state with setup link
- App.jsx: import + /chat route + nav bar (More → Coach with brain emoji)
- api.js: getAIStatus() method
Documentation:
- README.md: 'Built-in AI Coach' section with 8-provider table, external
agent config snippets for Claude Desktop, Claude Code, Cursor, Windsurf,
and COROS multi-MCP pattern
- AGENT_GUIDE.md: multi-device integration, Strava AI/ML warning,
provider-agnostic statement
Nav bar: Home | Log | Keto | Programs | Coach
Settings still reachable at /settings (linked from Coach setup prompt)
Abstract base class for device sync services (device_sync_base.py):
- Standard OAuth token management with automatic refresh
- Credential decryption from integration_configs table
- Activity import pipeline with deduplication by external_id
- Webhook validation and handling interface
- get_valid_token(), get_credentials(), import_activity() helpers
Garmin, WHOOP, and Oura sync service implementations follow when
API credentials are obtained (Scot action items):
- Apply for Garmin Developer Program (DiligenceWorks Pte. Ltd.)
- Register WHOOP developer app at developer.whoop.com
- Register Oura app at cloud.ouraring.com
AI coaching service (backend/app/services/ai_provider.py):
- Two code paths: OpenAI-compatible (6 providers) + Anthropic (Claude)
- Streaming SSE responses via httpx async
- System prompt built from AGENT_GUIDE + live user context
- Graceful error handling for all provider failures
- Gemini adapter for Google's generateContent API
Chat endpoint (backend/app/routers/ai_chat.py):
- POST /api/ai/chat — SSE streaming response
- GET /api/ai/status — check configured provider
- History capped at 20 messages, content at 4K chars
Provider registry expanded to 20 providers:
- 9 device integrations (strava, polar, garmin, whoop, oura, coros, fitbit, withings, suunto)
- 8 AI providers (openai, openrouter, huggingface, groq, ollama, claude, gemini, custom_ai)
- 3 other (usda, nutritionix, telegram)
- Categorized: device, ai_provider, nutrition, notifications
- COROS: MCP bridge approach (no API integration needed)
- Strava: AI/ML warning per their ToS
Frontend ChatCoach.jsx to follow in next commit.
SEC-16: Add frontend ports mapping (80:80) to docker-compose.yml
UI-01: Add routes for Welcome, SettingsIntegrations, MealPlan in App.jsx
SEC-01: Remove traceback leak from auth error responses
SEC-02: Fix require_admin to use is_admin column (was undefined ADMIN_USERNAME)
SEC-03: Add API_TOKEN auth for MCP connector -> backend communication
SEC-04: OAuth state now uses signed JWT tokens (was raw user UUID)
OS-01: First registered user gets admin immediately during registration
10 files changed, 97 insertions(+), 25 deletions(-)
Meal plan now includes:
- Bilingual headers throughout (day names, meal labels, notes)
- Full recipes for all 11 meals inline with bilingual ingredient tables
- Thai translation of every cooking step
- Bilingual prep schedule and substitution rules
Shopping list already had bilingual item tables; added Thai to intro and cost sections.
Two bugs fixed:
1. Backend: list_programs endpoint did not include catalog_id in its
response, so the frontend filter (p.catalog_id) always matched zero
programs. The previous LogActivity card never rendered because
loadActiveProgram() found no active program with a catalog link.
Now also exposes current_week.
2. UX restructure: instead of showing the program workout card at the
top of /log unconditionally, the card now appears AFTER the user
selects the 'Workout' category — matching the natural flow:
Log -> Workout -> see today's program workout option.
The Workout category now reveals:
- Active program banner (clickable, shows program name + week,
navigates to ProgramDetail on tap)
- Today's workout card with exercise preview and one-tap 75 pts
- Other uncompleted workouts in the current week (up to 3)
- 'Or log freeform' divider before the existing form
When all workouts in the current week are done, a green completion
banner appears instead.
Refactored the workout card into a ProgramWorkoutCard component since
it's now used for both today's featured workout and weekly upcoming.
Dashboard:
- The program bar at the top of the home page is now clickable and
navigates to the program detail page. Backend get_today_status now
returns program_id alongside name/day/total_days.
LogActivity:
- On mount, fetches the user's active catalog program and its today's
workout. If there's an uncompleted non-rest workout for today, a
featured 'Today's Program Workout' card appears at the top with an
exercise preview and a one-tap '✓ Complete — 75 pts' button.
- Tapping Complete fires the same /workout/{id}/complete endpoint as
ProgramDetail, surfaces weekly/completion bonuses in the success
message, and returns to the Dashboard.
- The standard category grid below is now relabeled 'Or log something
else' when a program workout is available.
CatalogDetail, ProgramDetail, ProgramSearch and SupportAdmin all use
inline-styled root divs instead of the .page CSS class, so they were
missing the padding-bottom: 96px that clears the fixed bottom nav bar.
Last button/section was hidden behind the nav.
Adds paddingBottom: 96px to each page's root div. Support.jsx has its
own full-height chat layout and is intentionally left as-is.
Catalog stores only the template (typically week 1) but programs span
many weeks. The schedule endpoint was returning only the literal stored
rows, so after day 3 of StrongLifts a user would see no upcoming
workout despite being mid-program.
Changes:
- Add week_number column to workout_logs (migration in main.py) so the
same template workout can be completed once per real week
- Synthetic workout IDs: '{template_uuid}::{real_week}' — frontend
treats opaquely, backend parses on every endpoint
- get_program_schedule rotates the template across catalog.duration_weeks
real weeks and returns synthetic IDs
- today_workout: first uncompleted non-rest entry in current real week
(more flexible than literal day-of-week match)
- complete_workout / get_workout_detail parse synthetic IDs and track
completion per (template_id, real_week) pair
- check_weekly_bonus filters by workout_logs.week_number for the real week
- check_completion_bonus and progress endpoint compute total as
template_per_week * total_weeks, not just template count
The 'View Details' button on ProgramSearch navigated to /catalog/:id
but no route or page existed. Now creates a proper detail view that
fetches via getCatalogProgram, shows program metadata, weeks/workouts,
progression rules, and an Adopt button that redirects to the user's
new program tracking page on success.
ROOT CAUSE FOUND: 'db' resolved to multiple IPs on the Docker network.
Coolify's own PostgreSQL (or another stack's) also responds to 'db'
via the predefined network. Our backend was connecting to the WRONG
postgres — one that requires password auth.
Fix: rename service from 'db' to 'fitness-db' so the hostname is unique.
Coolify's compose env var passing is unreliable. Moving all config
to Dockerfile ENV directives guarantees the values are present
regardless of how Coolify handles the compose file.