# Health & Fitness Game App — Software Specification

Oct 9, 2026 · @John Byrne

## 1. Purpose and build instructions

This specification defines a gamified health and fitness web app (working name **Mettle**) to be built by an AI coding model on a PHP 8.2+, MySQL/MariaDB, HTML, CSS and vanilla-plus-light-library JavaScript stack, deployable to ordinary shared hosting. It is the technical companion to the separate *Feature Description* document, which remains the source of truth for product intent.

**Instructions to the AI developer**

1. Build in the phase order in section 11. Finish and test each phase before starting the next.
2. Do not introduce anything that needs a long-running server process, Node.js on the server, root access, Redis, WebSockets or Docker in production. Everything must run on Apache + PHP-FPM + MySQL with cron.
3. Use only free, open-source libraries with permissive licences (MIT, BSD, Apache 2.0, ISC). Record each one in `docs/LICENSES.md`.
4. Composer and any front-end build tools run **only on the developer's local machine**. The production server receives built files, including `vendor/`.
5. All configuration (database credentials, mail, API keys, base URL) lives in a `.env` file outside the web root. Never hard-code secrets.
6. Write clean, commented, PSR-12 PHP with strict types. Every database query uses prepared statements.
7. When a requirement is ambiguous, choose the simplest option that satisfies it, record the decision in `docs/DECISIONS.md`, and continue.
8. Each phase ends with: migrations, seed data, automated tests passing, and a short `CHANGELOG.md` entry.

**Glossary**

| Term | Meaning |
| --- | --- |
| Check-in | The 4-weekly assessment (questions + self-tests) that recalibrates plans |
| Micro check-in | Daily 30-second sleep, energy, soreness and mood sliders |
| Stat | One of six character attributes: Strength, Power, Fortitude, Stamina, Agility, Balance |
| Quest | A task with XP reward: daily, weekly, story or Weak Spot |
| Party | A user's trusted circle of up to 8 invited people |
| Food Clan | The diet style a user follows (keto, vegan, etc.) |
| HIIT / HIIW | High intensity interval training / high intensity interval walking |

## 2. Technology stack and free libraries

The app is a server-rendered PHP application enhanced with lightweight JavaScript, installable as a Progressive Web App (PWA). Server rendering keeps it fast on cheap hosting; htmx and Alpine.js give it an app-like feel without a heavy single-page framework.

**Server side (installed with Composer locally)**

| Package | Licence | Purpose |
| --- | --- | --- |
| PHP 8.2+ (8.3 preferred) | PHP | Language runtime; extensions: pdo\_mysql, mbstring, gd or imagick, intl, openssl, curl, zip |
| MySQL 8.0+ or MariaDB 10.6+ | GPL | Database, InnoDB, utf8mb4 |
| slim/slim 4 + slim/psr7 | MIT | Routing, middleware, PSR-7 request handling |
| php-di/php-di | MIT | Dependency injection container |
| twig/twig + slim/twig-view | BSD-3 | HTML templating with auto-escaping |
| robmorgan/phinx | MIT | Database migrations and seeders |
| vlucas/phpdotenv | BSD-3 | Loads `.env` configuration |
| monolog/monolog | MIT | Logging to `storage/logs` |
| phpmailer/phpmailer | LGPL-2.1 | SMTP email (verification, reminders, summaries) |
| respect/validation | MIT | Input validation |
| minishlink/web-push | MIT | Browser push notifications via VAPID |
| intervention/image | MIT | Resize and strip metadata from uploaded photos |
| dompdf/dompdf | LGPL-2.1 | Printable check-in summaries as PDF |
| nesbot/carbon | MIT | Dates, time zones, streak maths |
| phpunit/phpunit, phpstan/phpstan (dev only) | BSD-3 / MIT | Tests and static analysis |

**Client side (downloaded into `public/assets/vendor/`, no CDN dependency in production)**

| Library | Licence | Purpose |
| --- | --- | --- |
| htmx | BSD-2 | Partial page updates, form posts without full reloads |
| Alpine.js | MIT | Small reactive components: timers, sliders, modals |
| Tailwind CSS (standalone CLI, local build only) | MIT | Utility CSS compiled to one minified file; no Node needed |
| Chart.js | MIT | Progress charts, stat radar, mood trends |
| anime.js | MIT | Avatar, XP bar and level-up animations |
| canvas-confetti | ISC | Celebration effects |
| Lucide icons (SVG) | ISC | Interface icons |
| Howler.js | MIT | Interval chimes and sound effects |
| Dexie.js | Apache-2.0 | IndexedDB wrapper for offline workout logging queue |
| Workbox (service worker modules) | MIT | PWA caching and background sync |

Browser-native APIs used: Web Speech API (voice cues), Notifications + Push API, Screen Wake Lock API (keep screen on during workouts), Vibration API (interval switches on mobile).

**Open data sources (import once, store locally)**

| Source | Licence | Use |
| --- | --- | --- |
| wger exercise database | Data CC-BY-SA | Seed exercise library (names, muscles, equipment, descriptions); attribution required |
| FSANZ Australian Food Composition Database | Free with attribution | Nutrient values for Australian foods |
| USDA FoodData Central | Public domain | Supplementary nutrient values |
| Open Food Facts | ODbL | Optional barcode lookup |

**Optional paid add-on (feature-flagged, off by default):** an AI vision/language API (e.g. the Claude API) for meal-photo portion estimates and personalised coach messages. The app must work fully without it, using rule-based coach messages and manual logging.

## 3. Hosting, local development and deployment

The app is edited and built on a local machine and uploaded to cPanel-style shared hosting that has no SSH, Composer or Node.js on the server.

**Assumed shared-hosting capabilities**

- Apache with `.htaccess` and `mod_rewrite`; PHP 8.2+ selectable in cPanel; one or more MySQL databases; free SSL (AutoSSL/Let's Encrypt); cron jobs (minimum 5-minute interval); SMTP mail; FTP/SFTP or File Manager upload.
- Limits to design around: \~30–60 s max execution time, 128–256 MB memory, no background workers, no persistent processes.

**Local development environment**

- Any of: Laragon (Windows), MAMP (macOS), XAMPP, or DDEV. Match the host's PHP and MySQL versions.
- Local tools: Composer, Tailwind standalone CLI, Git, PHPUnit, PHPStan.
- `composer run dev` script: compiles Tailwind in watch mode and starts PHP's built-in server on `public/`.

**Deployment workflow**

1. `composer install --no-dev --optimize-autoloader` and `composer run build` (minifies CSS/JS, versions asset filenames, updates the service-worker cache list).
2. `composer run package` creates `dist/Mettle-<version>.zip` containing everything except `.env`, tests, `node_modules` and local storage.
3. Upload and extract the zip on the server (cPanel File Manager or SFTP). Upload `.env` once, separately.
4. Run migrations through a protected web endpoint: `/admin/maintenance/migrate`, accessible only to admin users and a one-time token from `.env`. (No SSH means Phinx is invoked from PHP.)
5. Maintenance mode: a `storage/maintenance.flag` file makes the app show a friendly "The forge is being upgraded" page to everyone except admins.

**Document root**

- Preferred: point the domain's document root at `public/` so `app/`, `vendor/`, `storage/` and `.env` are not web-accessible.
- Fallback when the host fixes the document root to `public_html/`: put the app in `~/Mettle/` and copy only `public/` contents into `public_html/`, with `index.php` updated to require `../Mettle/bootstrap.php`. Provide both layouts in `docs/DEPLOY.md`.

**Scheduled jobs (single cron entry)**

`*/5 * * * * /usr/local/bin/php /home/USER/Mettle/bin/cron.php`

`cron.php` runs a lightweight scheduler that executes due tasks: send push and email reminders, generate daily quests at each user's local midnight, close weekly quests, process the email queue (max 50 per run), roll seasons, prune expired sessions and tokens. Each task must finish well inside the host's execution limit and resume where it stopped.

**Backups:** nightly `mysqldump` via cron to `storage/backups/` (keep 14), plus an admin "Download backup" button.

## 4. Architecture and folder structure

The app follows a layered structure: thin controllers handle HTTP, services hold all business rules, repositories hold all SQL. Game logic sits in its own engine so it can be unit-tested without a browser.

- **Request flow:** browser → `public/index.php` → Slim middleware (session, CSRF, auth, rate limit, maintenance) → controller → service → repository (PDO) → Twig view or JSON response.
- **Events:** services raise domain events (e.g. `WorkoutCompleted`, `MealLogged`, `CheckInCompleted`). A synchronous in-process dispatcher passes them to listeners in the Game Engine (award XP, update quests, streaks, achievements) and the Notification service. No queue server is needed.
- **Front end:** Twig renders full pages; htmx swaps fragments (e.g. ticking off a quest returns the updated quest card and XP bar). Alpine.js handles local interactive state. Pages work without JavaScript wherever practical.
- **Offline:** the service worker caches the app shell, today's plan, and exercise media; workout logs made offline are queued in IndexedDB (Dexie) and posted to `/api/sync` when back online.

```
Mettle/
├── app/
│   ├── Controllers/        # Http controllers, one per module
│   ├── Middleware/         # Auth, Csrf, RateLimit, Maintenance, Locale
│   ├── Services/           # Auth, CheckIn, Nutrition, MealPlanner, Training,
│   │                       # Interval, Coach, Party, Notification, Export
│   ├── Game/               # XpEngine, LevelTable, StatCalculator, QuestGenerator,
│   │                       # StreakTracker, AchievementRules, SeasonManager, MapProgress
│   ├── Adaptive/           # PlanGenerator, ProgressionRules, WeakSpotFinder, Readiness
│   ├── Repositories/       # PDO data access, one per aggregate
│   ├── Domain/             # Entities, value objects, enums, events
│   ├── Safety/             # Screening, NutritionLimits, RiskFlags
│   └── Support/            # Helpers, Clock, Mailer, Push, Image
├── bin/                    # cron.php, import-wger.php, import-foods.php
├── config/                 # settings.php, routes.php, container.php, game.php
├── database/
│   ├── migrations/
│   └── seeds/              # diets, exercises, recipes, quests, achievements, story
├── docs/                   # DEPLOY.md, DECISIONS.md, LICENSES.md, API.md
├── public/                 # web root
│   ├── index.php
│   ├── .htaccess
│   ├── manifest.webmanifest
│   ├── sw.js
│   └── assets/             # css/, js/, img/, audio/, vendor/
├── resources/
│   ├── views/              # Twig templates: layouts/, pages/, partials/, emails/
│   ├── css/app.css         # Tailwind source
│   └── js/                 # app modules (ES modules, no bundler required)
├── storage/                # logs/, cache/, uploads/, backups/ (not web-accessible)
├── tests/                  # Unit/, Integration/
├── .env.example
├── bootstrap.php
└── composer.json
```

`config/game.php` holds every tunable number (XP values, level curve, streak rules, safety limits) so balancing never requires code changes.

## 5. Database schema (MySQL)

All tables use InnoDB, `utf8mb4_unicode_ci`, `BIGINT UNSIGNED` auto-increment primary keys, `created_at`/`updated_at` timestamps, and foreign keys with `ON DELETE CASCADE` from `users` so account deletion removes all personal data. Times are stored in UTC; each user has an IANA time zone. Implement as Phinx migrations; the core definitions are below and the AI developer should add indexes on every foreign key and on (`user_id`, date) pairs.

**Groups of tables**

| Group | Tables |
| --- | --- |
| Accounts | users, user\_profiles, user\_settings, auth\_tokens, login\_attempts |
| Safety | screening\_responses, risk\_flags |
| Check-ins | checkins, checkin\_answers, checkin\_tests, daily\_readiness |
| Nutrition | diets, user\_diets, foods, recipes, recipe\_ingredients, recipe\_tags, meal\_plans, meal\_plan\_items, meal\_logs, shopping\_list\_items, water\_logs |
| Training | exercises, exercise\_progressions, interval\_protocols, training\_plans, planned\_sessions, planned\_session\_items, workout\_logs, set\_logs, interval\_logs |
| Game | characters, character\_stats, xp\_ledger, quest\_templates, user\_quests, streaks, achievements, user\_achievements, village\_buildings, user\_buildings, story\_chapters, user\_story\_progress, map\_nodes, user\_map\_progress, seasons, season\_scores, cosmetic\_items, user\_inventory |
| Social | parties, party\_members, share\_permissions, cheers, buddy\_pairs, party\_raids, raid\_contributions, invitations |
| Habits | habits, habit\_logs, non\_scale\_victories, mood\_logs |
| System | notifications, push\_subscriptions, email\_queue, feature\_flags, audit\_log, cron\_runs |

**Core definitions**

```sql
CREATE TABLE users (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(190) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  display_name VARCHAR(60) NOT NULL,
  role ENUM('user','admin') NOT NULL DEFAULT 'user',
  email_verified_at DATETIME NULL,
  timezone VARCHAR(64) NOT NULL DEFAULT 'Australia/Sydney',
  units ENUM('metric','imperial') NOT NULL DEFAULT 'metric',
  date_of_birth DATE NOT NULL,              -- must be 18+ at signup
  status ENUM('active','paused','deleted') NOT NULL DEFAULT 'active',
  last_active_at DATETIME NULL,
  created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL
);

CREATE TABLE user_profiles (
  user_id BIGINT UNSIGNED PRIMARY KEY,
  height_cm DECIMAL(5,1) NULL,
  sex_for_calcs ENUM('female','male','unspecified') NOT NULL DEFAULT 'unspecified',
  activity_level ENUM('sedentary','light','moderate','active','very_active') NOT NULL,
  goal_weights JSON NOT NULL,             -- {"fat_loss":3,"muscle":2,"strength":1,"endurance":2}
  minutes_per_week SMALLINT UNSIGNED NOT NULL,
  equipment JSON NOT NULL,                -- ["bodyweight","dumbbells","gym","treadmill"]
  avoid_movements JSON NULL,              -- ["jumping","overhead","kneeling"]
  cooking_minutes TINYINT UNSIGNED NOT NULL DEFAULT 30,
  weekly_food_budget_cents INT UNSIGNED NULL,
  hide_body_metrics TINYINT(1) NOT NULL DEFAULT 0,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE user_settings (
  user_id BIGINT UNSIGNED PRIMARY KEY,
  coach_tone ENUM('cheerleader','mate','drill_sergeant','calm_mentor') NOT NULL DEFAULT 'mate',
  reminder_times JSON NULL,
  push_enabled TINYINT(1) NOT NULL DEFAULT 0,
  email_digest ENUM('off','weekly') NOT NULL DEFAULT 'weekly',
  sound_enabled TINYINT(1) NOT NULL DEFAULT 1,
  reduced_motion TINYINT(1) NOT NULL DEFAULT 0,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE screening_responses (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL,
  answers JSON NOT NULL,
  outcome ENUM('clear','see_gp','blocked_high_intensity') NOT NULL,
  gp_clearance_confirmed_at DATETIME NULL,
  created_at DATETIME NOT NULL,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE checkins (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL,
  type ENUM('onboarding','scheduled','manual') NOT NULL,
  status ENUM('in_progress','complete') NOT NULL,
  weight_kg DECIMAL(5,2) NULL, waist_cm DECIMAL(5,1) NULL,
  completed_at DATETIME NULL, next_due_on DATE NULL,
  created_at DATETIME NOT NULL,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE checkin_tests (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  checkin_id BIGINT UNSIGNED NOT NULL,
  test_code ENUM('pushups','sit_to_stand_60','plank_hold','step_test_hr','sit_reach','single_leg_balance') NOT NULL,
  variant VARCHAR(30) NULL,               -- e.g. 'knees' for push-ups
  value DECIMAL(7,2) NULL, unit VARCHAR(10) NOT NULL, skipped TINYINT(1) NOT NULL DEFAULT 0,
  FOREIGN KEY (checkin_id) REFERENCES checkins(id) ON DELETE CASCADE
);

CREATE TABLE daily_readiness (
  user_id BIGINT UNSIGNED NOT NULL, log_date DATE NOT NULL,
  sleep TINYINT UNSIGNED, energy TINYINT UNSIGNED, soreness TINYINT UNSIGNED, mood TINYINT UNSIGNED, -- 1-5
  pain_flag TINYINT(1) NOT NULL DEFAULT 0, pain_area VARCHAR(40) NULL,
  PRIMARY KEY (user_id, log_date),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE exercises (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  slug VARCHAR(80) UNIQUE NOT NULL, name VARCHAR(120) NOT NULL,
  category ENUM('strength','cardio','hiit','walking','mobility','balance','recovery') NOT NULL,
  primary_muscles JSON, equipment JSON, impact ENUM('low','medium','high') NOT NULL,
  stat_targets JSON NOT NULL,             -- {"strength":0.7,"fortitude":0.3}
  tags JSON NULL,                          -- ["no_jump","seated","kneeling"]
  difficulty TINYINT UNSIGNED NOT NULL,    -- 1-10
  instructions TEXT, cues JSON, media_url VARCHAR(255) NULL, source_attribution VARCHAR(255) NULL
);

CREATE TABLE exercise_progressions (
  exercise_id BIGINT UNSIGNED NOT NULL, easier_id BIGINT UNSIGNED NULL, harder_id BIGINT UNSIGNED NULL,
  PRIMARY KEY (exercise_id)
);

CREATE TABLE interval_protocols (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  code VARCHAR(40) UNIQUE NOT NULL,        -- 'hiit_20_40','tabata','hiiw_1_3','hiiw_3_3_jp','hiiw_hill'
  mode ENUM('hiit','hiiw') NOT NULL,
  work_seconds SMALLINT UNSIGNED NOT NULL, rest_seconds SMALLINT UNSIGNED NOT NULL,
  rounds TINYINT UNSIGNED NOT NULL, warmup_seconds SMALLINT UNSIGNED NOT NULL, cooldown_seconds SMALLINT UNSIGNED NOT NULL,
  level TINYINT UNSIGNED NOT NULL,         -- 1 beginner .. 5 advanced
  work_effort TINYINT UNSIGNED NOT NULL,   -- target RPE 1-10
  rest_effort TINYINT UNSIGNED NOT NULL
);

CREATE TABLE planned_sessions (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL, plan_id BIGINT UNSIGNED NOT NULL,
  scheduled_on DATE NOT NULL,
  session_type ENUM('strength','hiit','hiiw','cardio','mobility','recovery','rest') NOT NULL,
  interval_protocol_id BIGINT UNSIGNED NULL,
  target_minutes SMALLINT UNSIGNED NOT NULL,
  status ENUM('planned','done','skipped','swapped') NOT NULL DEFAULT 'planned',
  is_weak_spot TINYINT(1) NOT NULL DEFAULT 0,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE workout_logs (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL, planned_session_id BIGINT UNSIGNED NULL,
  started_at DATETIME NOT NULL, ended_at DATETIME NULL,
  overall_effort TINYINT UNSIGNED NULL, notes VARCHAR(500) NULL,
  client_uuid CHAR(36) NOT NULL UNIQUE,    -- idempotent offline sync
  source ENUM('app','wearable','manual') NOT NULL DEFAULT 'app',
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE set_logs (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  workout_log_id BIGINT UNSIGNED NOT NULL, exercise_id BIGINT UNSIGNED NOT NULL,
  set_no TINYINT UNSIGNED NOT NULL, reps SMALLINT UNSIGNED NULL, load_kg DECIMAL(6,2) NULL,
  seconds SMALLINT UNSIGNED NULL, rating ENUM('too_easy','just_right','too_hard') NULL,
  FOREIGN KEY (workout_log_id) REFERENCES workout_logs(id) ON DELETE CASCADE
);

CREATE TABLE interval_logs (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  workout_log_id BIGINT UNSIGNED NOT NULL, interval_protocol_id BIGINT UNSIGNED NOT NULL,
  rounds_completed TINYINT UNSIGNED NOT NULL, work_seconds_total SMALLINT UNSIGNED NOT NULL,
  avg_work_hr SMALLINT UNSIGNED NULL, distance_m INT UNSIGNED NULL, steps INT UNSIGNED NULL,
  FOREIGN KEY (workout_log_id) REFERENCES workout_logs(id) ON DELETE CASCADE
);

CREATE TABLE recipes (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  slug VARCHAR(120) UNIQUE NOT NULL, title VARCHAR(160) NOT NULL,
  diet_codes JSON NOT NULL,                -- ["keto","paleo"]
  allergens JSON NOT NULL, prep_minutes SMALLINT UNSIGNED, cook_minutes SMALLINT UNSIGNED,
  servings TINYINT UNSIGNED NOT NULL, cost_cents_per_serve INT UNSIGNED NULL,
  kcal_per_serve SMALLINT UNSIGNED, protein_g DECIMAL(5,1), carbs_g DECIMAL(5,1), fat_g DECIMAL(5,1), fibre_g DECIMAL(5,1),
  steps JSON NOT NULL, image_path VARCHAR(255) NULL, season JSON NULL, cuisine VARCHAR(40) NULL,
  owner_user_id BIGINT UNSIGNED NULL       -- NULL = library recipe; set = user's own
);

CREATE TABLE meal_logs (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL, eaten_on DATE NOT NULL,
  slot ENUM('breakfast','lunch','dinner','snack') NOT NULL,
  recipe_id BIGINT UNSIGNED NULL, food_id BIGINT UNSIGNED NULL, servings DECIMAL(4,2) NOT NULL DEFAULT 1,
  kcal SMALLINT UNSIGNED, protein_g DECIMAL(5,1), veg_serves DECIMAL(3,1),
  from_plan TINYINT(1) NOT NULL DEFAULT 0, photo_path VARCHAR(255) NULL,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE characters (
  user_id BIGINT UNSIGNED PRIMARY KEY,
  name VARCHAR(40) NOT NULL, class ENUM('warrior','ranger','monk','alchemist','hybrid') NOT NULL,
  avatar JSON NOT NULL,                    -- body type, skin, hair, outfit layer ids
  level SMALLINT UNSIGNED NOT NULL DEFAULT 1, xp_total INT UNSIGNED NOT NULL DEFAULT 0,
  rest_tokens TINYINT UNSIGNED NOT NULL DEFAULT 2,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE character_stats (
  user_id BIGINT UNSIGNED NOT NULL,
  stat ENUM('strength','power','fortitude','stamina','agility','balance') NOT NULL,
  value SMALLINT UNSIGNED NOT NULL,        -- 1-100 scale
  checkin_id BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY (user_id, stat, checkin_id)
);

CREATE TABLE xp_ledger (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL, amount INT NOT NULL,
  reason VARCHAR(40) NOT NULL,             -- 'workout','quest','recovery','streak_bonus','cheer'
  ref_type VARCHAR(30) NULL, ref_id BIGINT UNSIGNED NULL,
  created_at DATETIME NOT NULL,
  UNIQUE KEY uq_award (user_id, reason, ref_type, ref_id),   -- prevents double awards
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE quest_templates (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  code VARCHAR(60) UNIQUE NOT NULL, cadence ENUM('daily','weekly','story','weak_spot','season') NOT NULL,
  title VARCHAR(120) NOT NULL, description VARCHAR(255),
  rule JSON NOT NULL,                      -- {"metric":"steps","op":">=","target":"user.step_goal"}
  xp SMALLINT UNSIGNED NOT NULL, min_level SMALLINT UNSIGNED NOT NULL DEFAULT 1,
  requires JSON NULL                       -- diet, equipment or screening conditions
);

CREATE TABLE user_quests (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL, template_id BIGINT UNSIGNED NOT NULL,
  period_start DATE NOT NULL, period_end DATE NOT NULL,
  target DECIMAL(10,2) NOT NULL, progress DECIMAL(10,2) NOT NULL DEFAULT 0,
  status ENUM('active','complete','expired') NOT NULL DEFAULT 'active', completed_at DATETIME NULL,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE parties (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(60) NOT NULL, owner_user_id BIGINT UNSIGNED NOT NULL, created_at DATETIME NOT NULL
);

CREATE TABLE party_members (
  party_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL,
  role ENUM('owner','member') NOT NULL, joined_at DATETIME NOT NULL,
  PRIMARY KEY (party_id, user_id)          -- app enforces max 8 members
);

CREATE TABLE share_permissions (
  owner_user_id BIGINT UNSIGNED NOT NULL, viewer_user_id BIGINT UNSIGNED NOT NULL,
  scope ENUM('activity','streaks','stats','level','nutrition','body_metrics','checkin_summary') NOT NULL,
  PRIMARY KEY (owner_user_id, viewer_user_id, scope)   -- absence = not shared
);

CREATE TABLE risk_flags (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL,
  type ENUM('low_intake','rapid_loss','excessive_training','pain_repeat','screening') NOT NULL,
  details JSON NULL, status ENUM('open','acknowledged','resolved') NOT NULL DEFAULT 'open',
  created_at DATETIME NOT NULL,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
```

Remaining tables in the groups above follow the same conventions; the AI developer defines them from the requirements in section 6 and documents them in `docs/SCHEMA.md`.

## 6. Functional modules and requirements

Each requirement has an ID so tests, commits and the changelog can reference it. "Must" items are needed for launch; "Should" items follow in later phases.

### 6.1 Accounts and onboarding (AUTH)

- **AUTH-1 Must:** Register with email, password (min 10 chars, checked against a common-password list), display name, date of birth (18+ only) and time zone (auto-detected, editable).
- **AUTH-2 Must:** Email verification link (expires 24 h); password reset link (expires 1 h); tokens stored hashed.
- **AUTH-3 Must:** Login with rate limiting (5 failures per 15 min per account and per IP), "remember me" cookie (30 days, rotating token).
- **AUTH-4 Must:** Onboarding wizard, resumable at any step: welcome → safety screen → goals → lifestyle and equipment → diet style → coach tone → create character → first check-in tests (optional, can be done later) → first plan generated.
- **AUTH-5 Must:** Account page: change email/password, export all my data (JSON + CSV zip), pause account, delete account (hard delete after 7-day grace period).

### 6.2 Safety screening (SAFE)

- **SAFE-1 Must:** Pre-exercise screening questionnaire modelled on the adult pre-exercise screening approach (heart condition, chest pain, dizziness/fainting, asthma, diabetes, bone/joint problems, pregnancy, recent surgery, medications affecting heart rate). Question wording is held in a seed file for review by a qualified professional.
- **SAFE-2 Must:** Any "yes" sets outcome `see_gp`: tests and HIIT are locked, HIIW and low-intensity plans remain, and the user sees a clear message recommending a GP check. User can confirm GP clearance to unlock (timestamped).
- **SAFE-3 Must:** Nutrition targets never go below a floor set in `config/game.php` (default 1,200 kcal/day for women, 1,500 for men, 1,400 unspecified) and planned weight loss never exceeds 1% of body weight per week.
- **SAFE-4 Must:** Risk flag rules (run nightly): logged intake below floor on 3+ days in 7; weight loss above 1.5% per week for 2 weeks; 7+ hard sessions in 7 days; pain reported on the same body area 3 times in 14 days. A flag shows a caring in-app message with support links (Butterfly Foundation 1800 33 4673 for eating concerns; "see a physio" for pain) and pauses related XP rewards.
- **SAFE-5 Must:** Diet-style warnings: keto, carnivore and fasting show notes for diabetes, kidney disease, pregnancy and breastfeeding, recommending GP advice.
- **SAFE-6 Must:** Visible disclaimer: the app gives general information, is not medical advice and is not a medical device.

### 6.3 Check-ins (CHK)

- **CHK-1 Must:** Full check-in at onboarding and then every 28 days (configurable 14–42). Due check-ins appear as a quest worth large XP.
- **CHK-2 Must:** Questions: goals ranking, time available, equipment, sleep, energy, stress, problem areas, motivation style, what stopped past attempts.
- **CHK-3 Must:** Six guided self-tests (push-ups with variant, 60 s sit-to-stand, plank hold, 3-minute step test with 1-minute recovery heart rate, sit-and-reach, single-leg balance). Each has a demo video or animation, a built-in timer with voice countdown, and a "skip" option. Locked when SAFE outcome is not clear.
- **CHK-4 Must:** Optional weight and waist entry, hideable from all views.
- **CHK-5 Must:** On completion: recalculate stats (section 7.3), identify weak spot, regenerate training plan, adjust nutrition targets, show a "Trial results" screen comparing with the previous check-in.
- **CHK-6 Must:** Daily micro check-in (sleep, energy, soreness, mood 1–5; pain yes/no + area) in under 30 seconds, feeding the readiness rule (section 7.5).
- **CHK-7 Should:** Printable/emailable check-in summary PDF the user can give to a GP, dietitian or trainer.

### 6.4 Nutrition (NUT)

- **NUT-1 Must:** Diet styles: keto, carnivore, paleo, Mediterranean, vegetarian, vegan, pescatarian, high-protein, low-FODMAP-friendly, balanced plate. Intermittent fasting as an optional eating-window layer (12:12, 14:10, 16:8 only). Extra filters: halal, kosher, gluten-free, dairy-free, nut-free, plus personal dislikes.
- **NUT-2 Must:** Daily energy and macro targets from Mifflin–St Jeor BMR × activity factor, adjusted for goal, clamped by SAFE-3; macro split per diet style defined in `config/diets.php`. Users who hide body metrics see food-group targets (protein serves, veg serves) instead of calories.
- **NUT-3 Must:** Recipe library: search and filter by diet, time, cost, protein, cuisine, season; recipe page with scalable servings, step-by-step cook mode (large text, timers, Web Speech read-aloud, wake lock).
- **NUT-4 Must:** Weekly meal-plan generator (section 7.6): respects diet, filters, budget, cooking time, leftovers and batch-cook day; "Swap it" on any meal returns 3 alternatives within ±10% kcal and same diet.
- **NUT-5 Must:** Shopping list built from the plan, merged quantities, grouped by aisle, tick-off on mobile, shareable read-only link.
- **NUT-6 Must:** Logging: tick planned meal (one tap), pick recipe or food, quick-add, water and veg-serve counters. Off-plan meals are fine and earn Recovery XP for the next logged on-plan meal.
- **NUT-7 Should:** Save own recipes; "lighten it" suggestions using ingredient swap rules.
- **NUT-8 Should:** Photo logging: photo stored (resized, EXIF stripped); if the AI add-on is enabled, returns an estimate the user confirms or edits.
- **NUT-9 Should:** Budget mode: cheapest plan meeting targets using `cost_cents_per_serve`.

### 6.5 Training (TRN)

- **TRN-1 Must:** Exercise library of at least 250 exercises (seeded from wger plus hand-written entries) with category, muscles, equipment, impact, difficulty, stat targets, cues, and easier/harder progressions.
- **TRN-2 Must:** Plan generator (section 7.4) produces a 4-week block of planned sessions fitting minutes per week, equipment, goals, avoided movements and screening outcome, with a deload in week 4 every second block.
- **TRN-3 Must:** Workout player: exercise card with media and cues, set/rep/time logging, rest timer, one-tap effort rating per set (too easy / just right / too hard), swap exercise button (offers easier/harder/joint-friendly), works offline.
- **TRN-4 Must:** Session length options: 10, 20, 30, 45, 60 minutes, generated from the same plan by trimming accessory work.
- **TRN-5 Must:** Missed session handling: never stack; the next session shrinks by up to 30% and the plan shifts forward.
- **TRN-6 Must:** Readiness adjustment from the daily micro check-in (section 7.5).
- **TRN-7 Must:** Real-world milestone detection (first full push-up, 5 km continuous walk/run, 60 s plank, etc.) awarding named achievements.

### 6.6 Interval training: HIIT and HIIW (INT)

- **INT-1 Must:** HIIT protocols seeded at five levels, e.g. L1 20 s work / 40 s rest × 10 low-impact moves (10 min); L3 30/30 × 16; L5 40/20 or Tabata 20/10 × 8 per block, plyometrics allowed, 20–25 min. Every HIIT session includes warm-up (5 min) and cool-down (3–5 min).
- **INT-2 Must:** HIIW protocols at five levels, e.g. L1 1 min brisk / 3 min easy × 4; L3 2/3 × 5; L4 3 min fast / 3 min easy × 5 (Japanese-style interval walking, 30 min); L5 hill or treadmill-incline intervals.
- **INT-3 Must:** Interval timer screen: large countdown, colour change per phase (work/rest/warm-up/cool-down), round counter, voice cue ("Fast walk now — 3 minutes"), chime via Howler.js, phone vibration, wake lock, pause/resume, works offline and with screen locked as far as the browser allows (audio cues continue).
- **INT-4 Must:** Intensity guided by 1–10 effort scale; target effort shown for each phase. Post-session rating and optional average heart rate, distance and steps entry.
- **INT-5 Must:** Gating: HIIW available from day one for everyone. HIIT unlocks only when screening is clear (or GP clearance confirmed) and the user has completed 6 sessions of any type in the previous 3 weeks. Low-impact HIIT (no jumping) is the default when `avoid_movements` includes jumping, impact tags conflict, or the user selects it.
- **INT-6 Must:** Recovery spacing: no two HIIT sessions on consecutive days; maximum 3 HIIT per week; HIIW counts as moderate and can follow HIIT.
- **INT-7 Must:** Level progression: after two consecutive "just right" or "too easy" ratings with all rounds completed, offer the next level; after a "too hard" or incomplete session, repeat or drop one level.
- **INT-8 Must:** Game tie-in: HIIT sessions are "Sprint Trials" weighted to Power and Stamina XP; HIIW fast intervals count double for walk-to-explore map progress.

### 6.7 Game layer (GAME)

- **GAME-1 Must:** Character creation: name, class, avatar built from layered SVG parts (diverse body types, skin tones, hair, outfits); avatar outfits change with level and cosmetic unlocks.
- **GAME-2 Must:** XP, levels, stats, streaks, rest tokens, achievements and village buildings as defined in section 7.
- **GAME-3 Must:** Daily quests (3–5) generated at user's local midnight by cron; weekly quests generated Monday; each quest's progress updates automatically from logged events.
- **GAME-4 Must:** Dashboard ("Village") shows avatar, level and XP bar, today's quests, streak, next session, and village scene that gains buildings with milestones.
- **GAME-5 Should:** Story campaign: chapters unlocked by consistency (e.g. 2 weeks with ≥3 sessions each); boss battles are real challenges with target ranges based on the user's level.
- **GAME-6 Should:** Walk-to-explore map: an SVG fantasy map where nodes unlock by cumulative steps/distance; each node reveals a short lore snippet or recipe.
- **GAME-7 Should:** 8-week seasons with themed quest pools, season XP track with cosmetic rewards, and a friends-only seasonal leaderboard ranked by effort points (not weight lost).
- **GAME-8 Must:** All rewards are cosmetic; no payments, loot boxes or real-money purchases.

### 6.8 Coach companion (COACH)

- **COACH-1 Must:** Rule-based message engine: templates keyed by trigger (session done, streak at risk, plateau, missed sessions, check-in due, comeback, milestone) × tone (4 tones). At least 15 variants per trigger/tone in seed data to avoid repetition.
- **COACH-2 Must:** Pattern nudges, e.g. "You've skipped 3 of the last 4 Mondays — want Mondays to be a 10-minute session?" with one-tap accept.
- **COACH-3 Must:** Comeback mode after 7+ inactive days: welcome-back screen, no streak shaming, easier first week, double XP for the first 3 sessions.
- **COACH-4 Should:** Optional AI-generated messages when the AI add-on is enabled, constrained by a system prompt that enforces tone, safety rules and no medical advice.

### 6.9 Party and sharing (SOC)

- **SOC-1 Must:** Create a party and invite up to 7 others by email or invite link (expires 7 days). Joining requires an account.
- **SOC-2 Must:** Per-viewer, per-scope sharing (`share_permissions`). Default for new members: activity and streaks only. Body metrics are never shared by default.
- **SOC-3 Must:** Party feed of shared events; reactions (cheer, high-five, flex, fire) and short text messages (280 chars). Voice notes are Should (max 30 s, stored compressed).
- **SOC-4 Must:** Accountability buddy pairs: a shared weekly goal and a visible "checked in this week" status for each.
- **SOC-5 Should:** Party raids (combined weekly target) and handicapped friendly challenges scaled to each member's level.
- **SOC-6 Must:** Shareable progress cards rendered as PNG (server-side GD) for level-ups, milestones and season recaps; never include weight or body photos.
- **SOC-7 Must:** Leave party, remove member (owner), block user, report message.

### 6.10 Habits and insights (HAB)

- **HAB-1 Should:** Habit stacking builder ("After \[existing habit\], I will \[new habit\]") with daily tick-off quests.
- **HAB-2 Should:** Non-scale victories log with prompts.
- **HAB-3 Should:** Insights page: Chart.js charts of stats over check-ins, training volume, interval levels, mood vs sleep vs training, weight trend (hideable).
- **HAB-4 Should:** Craving rescue: a 2-minute breathing or tap mini-game plus a swap suggestion from the user's diet.

### 6.11 Notifications (NOTIF)

- **NOTIF-1 Must:** In-app notification centre.
- **NOTIF-2 Must:** Web push (VAPID) for session reminders, streak at risk, cheers received, check-in due; user chooses which and quiet hours.
- **NOTIF-3 Must:** Email via queued SMTP: verification, reset, weekly digest, check-in due, party invites. Unsubscribe link in every non-essential email.

### 6.12 Data from wearables (WEAR)

- **WEAR-1 Should:** Manual entry and CSV import of steps, heart rate and sleep.
- **WEAR-2 Should:** Fitbit Web API OAuth integration for daily steps, sleep and resting heart rate (pulled by cron).
- **WEAR-3 Note:** Apple Health and Android Health Connect have no web API; they need a native companion app, so they are out of scope for the web build. Garmin and others require partner approval; treat as future.

### 6.13 Admin (ADM)

- **ADM-1 Must:** Admin area: users (search, view non-health account info, suspend), content management for recipes, exercises, interval protocols, quests, coach messages, story chapters, seasons and cosmetics.
- **ADM-2 Must:** Feature flags, maintenance mode, migration runner, backup download, cron health (last run per task), error log viewer.
- **ADM-3 Must:** Reported messages queue.
- **ADM-4 Must:** Admins cannot see users' health data (check-ins, weights, meal logs) through the UI; access to the database is logged in `audit_log` where performed through the app.

## 7. Game rules and adaptive algorithms

All numbers below are defaults held in `config/game.php`; the game engine must read them from config, never hard-code them. Rewards favour consistency, effort and recovery over results, so plateaus never stall progress.

### 7.1 XP awards

| Event | XP | Limit |
| --- | --- | --- |
| Planned workout completed | 50 + 2 per minute (max 150) | 2 per day |
| HIIT "Sprint Trial" completed | +25 bonus | 1 per day |
| HIIW session completed | +15 bonus | 1 per day |
| Daily micro check-in | 10 | 1 per day |
| Full check-in completed | 300 | 1 per 14 days |
| Daily quest completed | 20–40 (per template) | 5 per day |
| All daily quests completed | 50 bonus | 1 per day |
| Weekly quest completed | 150–300 | per template |
| Weak Spot Quest session | +50% of workout XP | per session |
| Planned meal logged | 5 | 4 per day |
| Recovery XP (on-plan after off-plan) | 15 | 2 per day |
| Rest day taken as planned | 20 | per planned rest |
| Cheer sent to a party member | 2 | 10 per day |
| Milestone achievement | 100–500 | once each |

All awards go through `XpEngine::award()`, which writes to `xp_ledger` (unique key prevents duplicates), updates `characters.xp_total`, checks level-up, and returns a result the UI uses to animate XP gain.

### 7.2 Levels

XP required to reach level *n* (n ≥ 2), capped at level 100:

```latex
XP_{required}(n) = \left\lfloor 120 \times (n-1)^{1.6} \right\rfloor
```

This gives roughly level 6–7 after 2 weeks, level 10 around week 4 (at about 150 XP a day) and level 30 after about 6 months of steady use. Each level-up triggers confetti, a coach message and possibly a cosmetic unlock; every 5th level unlocks a village building tier.

### 7.3 Stat calculation

- Each self-test maps to a stat: push-ups → Strength; sit-to-stand → Power; plank → Fortitude; step-test recovery HR → Stamina (lower is better); sit-and-reach → Agility; single-leg balance → Balance.
- `config/norms.php` holds score bands by age group (18–29, 30–39, 40–49, 50–59, 60–69, 70+) and `sex_for_calcs`; unspecified uses the average of both bands.
- Stat value 1–100 = linear interpolation of the result between band thresholds (Poor = 1–20, Below average = 21–40, Average = 41–60, Good = 61–80, Excellent = 81–100). Push-ups from knees score at 60% of full push-ups.
- Skipped tests carry forward the previous value; first-time skipped tests default to 30 and show "untested".
- Stats displayed as a Chart.js radar chart with previous check-in overlaid.
- **Weak spot** = lowest stat; ties broken by the user's goal weights. The weak spot drives the Weak Spot Quest and an extra accessory block in the plan.

### 7.4 Training plan generator

Inputs: goal weights, minutes per week, equipment, avoided movements, screening outcome, stats, weak spot, current interval levels.

1. **Sessions per week** = clamp(round(minutes\_per\_week / 30), 2, 6), with at least one rest day.
2. **Session mix** distributed by goal weights: strength-heavy goals favour strength sessions; fat-loss and endurance goals add HIIW and (if unlocked) HIIT; every week includes at least one mobility or recovery block of 10 minutes.
3. **Exercise selection** for each strength session: pick 4–6 exercises covering push, pull, legs, core, filtered by equipment and avoided movements, choosing difficulty close to (stat value / 10). Add one accessory block for the weak spot.
4. **Volume start points**: 2–3 sets; rep target 8–12 (strength goals 5–8 for advanced users); timed holds 20–60 s based on test results.
5. **Interval sessions** use the user's current HIIT or HIIW level (INT-7) and respect INT-6 spacing.
6. **Block structure**: 4-week blocks; every second block's final week is a deload (volume −40%).

### 7.5 Readiness and real-time adaptation

Readiness score R = mean(sleep, energy, mood, 6 − soreness), each 1–5.

| Condition | Adjustment to today's session |
| --- | --- |
| R ≥ 3.3 and no pain | As planned |
| 2.5 ≤ R < 3.3 | Volume −20%; HIIT swapped to HIIW at equal level |
| R < 2.5 | Swap to 15–20 min mobility/recovery or easy walk; XP for doing it is unchanged |
| Pain reported | Remove exercises loading the reported area; offer joint-friendly alternatives; repeated pain triggers SAFE-4 |

**Per-set progression (double progression):** if all sets hit the top of the rep range rated "just right" or "too easy", next session moves to the harder variation or +2.5 kg (upper body) / +5 kg (lower body). Any "too hard" rating or missed reps keeps the load; two consecutive "too hard" sessions step down one variation.

**Missed sessions:** TRN-5. Plans never show a backlog.

### 7.6 Meal-plan generator

1. Compute daily targets (NUT-2) and split across slots: breakfast 25%, lunch 30%, dinner 35%, snacks 10%.
2. Candidate recipes = those matching diet and filters, not disliked, within cooking-time limit.
3. Greedy fill per day and slot choosing the recipe that best fits remaining kcal and protein (score = |kcal gap| / kcal target + 2 × |protein gap| / protein target), penalising repeats within 3 days.
4. Leftovers: dinner recipes with servings ≥ 2 roll into next day's lunch when the user enables leftovers.
5. Batch-cook day: choose 2 recipes that scale and cover 3+ weekday lunches.
6. Budget mode adds a cost term to the score and stops when the weekly budget would be exceeded, falling back to the cheapest compliant recipes.
7. Daily totals must land within ±10% of kcal target and ≥ 90% of protein target; if not possible, show the closest plan and a note.

### 7.7 Streaks and rest tokens

- A day counts toward the streak when at least one of: workout completed, planned rest day, micro check-in plus one daily quest.
- Users hold up to 3 rest tokens; one is earned per 7-day streak. A missed day auto-spends a token instead of breaking the streak. Sick mode (user-declared, up to 7 days) freezes the streak without tokens.

### 7.8 Quest generation

- Daily: choose 3–5 templates by weighted random from eligible templates (level, diet, equipment, screening), always including one movement quest and one nutrition or hydration quest; never more than one quest the user failed yesterday.
- Weekly: 2 quests plus the Weak Spot Quest.
- Targets personalised from the user's recent 14-day averages (e.g. steps target = recent average + 10%, rounded to 500).

## 8. Routes and API endpoints

Pages are server-rendered; htmx calls return HTML fragments; the `/api/*` routes return JSON for the offline sync, the interval timer and charts. All state-changing requests require a session plus CSRF token (header `X-CSRF-Token` for htmx/fetch). JSON errors use `{"error":{"code":"...","message":"...","fields":{}}}` with proper HTTP status codes.

| Method | Path | Returns | Purpose |
| --- | --- | --- | --- |
| GET/POST | /register, /login, /logout | Page | Auth (AUTH-1–3) |
| GET/POST | /forgot, /reset/{token}, /verify/{token} | Page | Password reset, email verification |
| GET/POST | /onboarding/{step} | Page | Resumable onboarding wizard |
| GET | / (Village) | Page | Dashboard |
| GET/POST | /screening | Page | Safety questionnaire |
| GET/POST | /checkin, /checkin/{id}/{step} | Page | Full check-in flow |
| POST | /checkin/daily | Fragment | Micro check-in |
| GET | /checkin/{id}/summary.pdf | PDF | Printable summary |
| GET | /train | Page | This week's plan |
| GET | /train/session/{id} | Page | Workout player |
| GET | /train/interval/{id} | Page | Interval timer (HIIT/HIIW) |
| POST | /train/session/{id}/swap | Fragment | Swap session or exercise |
| GET | /exercises, /exercises/{slug} | Page | Exercise library |
| GET | /eat | Page | Today's meals and targets |
| GET/POST | /eat/plan, /eat/plan/regenerate | Page | Weekly meal plan |
| POST | /eat/plan/item/{id}/swap | Fragment | Swap a meal |
| POST | /eat/log | Fragment | Log meal, water or veg serve |
| GET | /eat/shopping, /share/shopping/{token} | Page | Shopping list, public read-only link |
| GET | /recipes, /recipes/{slug}, /recipes/{slug}/cook | Page | Library, detail, cook mode |
| GET | /quests | Page | All active quests |
| GET | /character, /village, /map, /story, /season | Page | Game screens |
| GET/POST | /party, /party/invite, /party/join/{token} | Page | Party management |
| POST | /party/cheer, /party/message | Fragment | Social actions |
| GET/POST | /party/sharing | Page | Per-person share permissions |
| GET | /insights | Page | Charts |
| GET/POST | /settings, /account, /account/export, /account/delete | Page | Settings and data rights |
| POST | /api/sync | JSON | Upload queued offline logs (idempotent by `client_uuid`) |
| GET | /api/today | JSON | Today's plan, quests and readiness for offline cache |
| GET | /api/interval/{protocol} | JSON | Phase list for the timer |
| POST | /api/workout/{id}/set | JSON | Log a set, returns next prescription + XP |
| POST | /api/workout/{id}/finish | JSON | Finish session, returns XP, level-up, quest and achievement updates |
| GET | /api/charts/{name} | JSON | Chart.js datasets |
| POST | /api/push/subscribe, /api/push/unsubscribe | JSON | Web push subscriptions |
| GET | /api/notifications | JSON | Unread notifications |
| GET/POST | /admin/\* | Page | Admin area (ADM), role `admin` only |
| POST | /admin/maintenance/migrate | Page | Run pending migrations (token + admin) |

**Example: finishing a workout**

```json
POST /api/workout/812/finish
{ "overall_effort": 7, "notes": "Felt strong", "client_uuid": "6f1c..." }

200 OK
{
  "xp": { "awarded": 145, "breakdown": [{"reason":"workout","amount":110},{"reason":"sprint_trial","amount":25},{"reason":"quest","amount":10}] },
  "level": { "current": 7, "leveled_up": false, "xp_into_level": 410, "xp_for_next": 620 },
  "quests_completed": ["daily_move_20"],
  "achievements": [],
  "streak": { "days": 12, "rest_tokens": 2 },
  "coach_message": "Twelve days strong. The forge is getting hot!",
  "next_session": { "id": 813, "type": "hiiw", "scheduled_on": "2026-10-11" }
}
```

## 9. Pages, UI and UX requirements

The interface is mobile-first, warm and playful, with a hand-crafted fantasy-village feel rather than a clinical dashboard. It must also be fast, accessible and usable one-handed mid-workout.

**Navigation:** bottom tab bar on mobile (Village, Train, Eat, Party, More); left sidebar on desktop. A floating "Start today's session" button appears on Village and Train when a session is due.

**Key screens**

| Screen | Must show |
| --- | --- |
| Village (home) | Avatar, level and XP bar, streak flame with rest tokens, today's quests as tappable cards, next session, micro check-in prompt, village scene with unlocked buildings, latest party cheers |
| Train | Week view of sessions with type icons (strength, HIIT, HIIW, mobility, rest), weak-spot badge, plan block progress |
| Workout player | One exercise per screen, large tap targets, set logger, rest timer, effort buttons, swap, progress dots |
| Interval timer | Full-screen countdown, phase colour, round x/y, next-phase preview, pause; readable at arm's length outdoors |
| Eat | Today's meals with tick-to-log, targets as rings or food-group serves, water and veg counters, shopping list link |
| Recipe / cook mode | Hero image, serving scaler, ingredients, steps; cook mode with one step per screen and timers |
| Check-in | Step-by-step wizard with progress bar; test screens with demo, timer, entry; results screen with radar chart comparison |
| Character | Avatar editor, class, stats radar, achievements grid, inventory |
| Map / Story / Season | Illustrated SVG map with unlocked nodes; chapter list; season XP track |
| Party | Members with what they share, feed, buddy status, raid progress, invite |
| Insights | Charts with plain-language captions |

**Visual design**

- Design tokens as CSS custom properties in Tailwind config: a parchment-and-forest palette (warm cream backgrounds, deep green primary, ember orange for XP and streaks, slate text), rounded cards, soft shadows.
- Light and dark mode following the system setting, with a manual toggle.
- Typography: a characterful display font for headings and a highly legible sans-serif for body (both from Google Fonts, self-hosted, SIL OFL licence).
- Illustrations and avatar parts as layered SVG so they stay crisp and small; store under `public/assets/img/game/`.
- Animations with anime.js: XP bar fill, level-up burst, quest-complete tick, building appearing. All honour `prefers-reduced-motion` and the in-app reduced-motion setting.

**Accessibility (WCAG 2.2 AA)**

- Colour contrast ≥ 4.5:1; never rely on colour alone (interval phases also use text and sound).
- Full keyboard navigation, visible focus, semantic HTML, labelled form controls, ARIA live regions for timers and XP announcements.
- Tap targets ≥ 44 × 44 px; text resizable to 200% without breaking layout.
- Captions or text descriptions for every exercise video.

**Performance targets**

- First load under 200 KB of CSS + JS (gzipped), excluding images; Lighthouse performance ≥ 90 on mobile.
- Images served as WebP with width-appropriate sizes; lazy-loaded below the fold.
- Server responses under 300 ms for typical pages on shared hosting; cache expensive queries (plan, charts) in `storage/cache` for 5–60 minutes.

**PWA**

- Web app manifest with name, icons (192, 512, maskable), theme colour, `display: standalone`.
- Service worker: precache app shell; network-first for pages; cache-first for assets and exercise media; offline fallback page; background sync for `/api/sync`.
- Add-to-home-screen prompt shown after the user's third session, not on first visit.

**Language and tone**

- Australian English spelling. Friendly, never shaming: "Let's go again tomorrow" instead of "You failed". Avoid words like "cheat meal", "bad food" or "burn off".
- Coach tone setting changes message wording only, never safety messages.

## 10. Security, privacy and safety requirements

The app stores health information, which the Australian Privacy Act treats as sensitive information, so security and privacy are launch requirements, not extras.

**Application security (follow OWASP Top 10 and ASVS Level 2 where practical)**

- Passwords with `password_hash()` (Argon2id if available, else bcrypt cost 12); auto-rehash on login.
- Sessions: `HttpOnly`, `Secure`, `SameSite=Lax` cookies; regenerate ID on login and privilege change; 30-minute idle timeout for admin.
- CSRF tokens on all state-changing requests; Twig auto-escaping on; no raw output of user content.
- PDO prepared statements only; `PDO::ATTR_EMULATE_PREPARES = false`.
- Authorisation checked in services, not just routes: every query for user data is scoped by `user_id`; party data checked against `share_permissions`.
- Security headers via middleware: Content-Security-Policy (self only, no inline scripts; use nonces), HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy.
- File uploads: images only, max 8 MB, MIME sniffed, re-encoded with Intervention Image, EXIF stripped, random filenames, stored outside web root and served through an authorised controller.
- Rate limiting (DB-backed) on login, registration, password reset, invites, messages and `/api/*`.
- Errors logged with Monolog; users see friendly pages; `APP_DEBUG=false` in production.
- Dependencies checked with `composer audit` before each release.

**Privacy**

- Comply with the Australian Privacy Principles; collect only what each feature needs; plain-English privacy policy and collection notice at signup; explicit consent checkbox for health information.
- Health data never shared outside the user's chosen party members; never sold; no third-party ad or tracking scripts. Use privacy-friendly, self-hosted analytics (e.g. Matomo, GPL) or none.
- Data export (AUTH-5) and deletion within 7 days of request; backups containing deleted users expire within 14 days.
- Optional AI add-on: off by default; when on, only the minimum data is sent, with a clear in-app notice, and nothing identifies the user.
- If launching to EU users, also meet GDPR (lawful basis, data processing record, DPA with hosting provider).

**User safety (see also SAFE-1 to SAFE-6)**

- No feature rewards eating less, losing weight faster, or training through pain.
- Leaderboards rank effort points only, never weight or body measurements.
- Users can hide all body metrics; hidden metrics are excluded from charts, shares, emails and coach messages.
- Adults only (18+), enforced at signup.
- Safety content (screening questions, warnings, support links) stored in seed files and flagged for review by a qualified health professional before launch.

## 11. Build phases and acceptance criteria

Build in eight phases (0–7); each must pass its acceptance criteria and deploy cleanly to a staging subdomain on the shared host before the next begins.

| Phase | Scope (requirement IDs) | Done when |
| --- | --- | --- |
| 0. Foundations | Project skeleton, Slim + Twig + DI, `.env`, Phinx, Tailwind build, layout and design tokens, error handling, security headers, cron runner, deploy packaging, `docs/DEPLOY.md` | A "hello" page deploys to the shared host from the zip; migrations run via the admin endpoint; cron writes to `cron_runs`; `composer test` and PHPStan level 6 pass |
| 1. Accounts and safety | AUTH-1–5, SAFE-1–6, NOTIF-3 (email queue) | A new user can register, verify, log in, complete screening, and export and delete their account; rate limits verified by tests |
| 2. Check-ins and character | CHK-1–6, GAME-1, stat calculation 7.3, onboarding wizard | Onboarding produces a character with stats from test results; radar chart shows; skipped tests handled; norms unit-tested for every age band |
| 3. Training and intervals | TRN-1–7, INT-1–8, readiness 7.5, plan generator 7.4, exercise seed import | A 4-week plan is generated for 5 test personas (beginner bodyweight, gym regular, low-impact, screening flagged, time-poor); HIIT stays locked for the flagged persona; interval timer runs a full HIIW L4 session with voice and chimes, offline |
| 4. Nutrition | NUT-1–6, meal-plan generator 7.6, recipe seed (min 150 recipes covering every diet) | Every diet style yields a 7-day plan within ±10% kcal and ≥90% protein; shopping list merges quantities; SAFE-3 floors enforced in tests |
| 5. Game layer | GAME-2–4, 7.1, 7.2, 7.7, 7.8, COACH-1–3, NOTIF-1–2, PWA | XP cannot be double-awarded (test); daily quests generate at local midnight across 3 time zones; streak and rest-token rules pass tests; app installs as PWA and logs a workout offline, syncing later without duplicates |
| 6. Social | SOC-1–7 | Two test users in a party see only what each has shared; revoking a permission hides data immediately; progress card PNG contains no body metrics |
| 7. Polish and Should items | GAME-5–7, HAB-1–4, NUT-7–9, CHK-7, WEAR-1–2, COACH-4, ADM-1–4 | Lighthouse mobile ≥ 90 performance and ≥ 95 accessibility on Village, Train and Eat; axe-core shows no serious issues; admin can manage all content types |

**Testing standards**

- PHPUnit unit tests for every class in `Game/`, `Adaptive/` and `Safety/` (target 90% line coverage there; 60% overall).
- Integration tests against a real MySQL test database for repositories and key flows.
- Seeded demo accounts for the 5 personas, plus a "time travel" admin tool to simulate days passing for testing streaks, quests and check-ins.
- Manual test checklist per phase in `docs/QA.md`, including iPhone Safari and Android Chrome.

**Definition of done for every feature:** requirement ID referenced, tests written and passing, no PHPStan errors, accessible markup checked, works on a 375 px wide screen, documented in `CHANGELOG.md`.

## 12. Open questions

These decisions are needed before or during the build; until answered, the AI developer uses the default shown.

- [ ] **Hosting provider and plan:** which host, and does it allow pointing the document root at `public/`, PHP 8.2+, and 5-minute cron? *Default: assume cPanel with `public_html/` fixed and use the fallback layout.*
- [ ] **Final app name and domain:** trade mark and domain checks pending. *Default: "Mettle" as the working name, kept in one config value.*
- [ ] **Content:** who writes or sources recipes, exercise videos and illustrations? *Default: seed with wger exercise data, placeholder SVG illustrations and 150 starter recipes written by the developer.*
- [ ] **Professional review:** which qualified professional (GP, accredited exercise physiologist, accredited practising dietitian) reviews the screening, test norms, nutrition floors and warnings?
- [ ] **Data licences:** confirm attribution wording for wger (CC-BY-SA), the FSANZ food database and Open Food Facts before import.
- [ ] **AI add-on:** include the optional AI coach and photo estimates at launch, and who pays the API costs? *Default: off.*
- [ ] **Wearables:** is the Fitbit integration needed at launch? *Default: phase 7.*
- [ ] **Audience and market:** Australia only, or also overseas (affects GDPR and units)? *Default: Australia, metric, with imperial option.*
- [ ] **Monetisation:** free, freemium or subscription? *Default: free, no payment code.*
