---
phase: 03-apple-health-integration
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - server/routes/health.js
  - server/routes/systemHealth.js
  - server/index.js
  - client/src/services/api.js
  - server/lib/appleHealthValidation.js
  - server/services/appleHealthIngest.js
  - server/services/appleHealthQuery.js
  - server/routes/appleHealth.js
  - server/lib/fileUtils.js
autonomous: true
requirements:
  - HLT-01
  - HLT-02

must_haves:
  truths:
    - "POST /api/health/ingest accepts Health Auto Export JSON, validates with Zod, and returns success with record counts"
    - "Duplicate records (same metric name + timestamp) are silently skipped on re-ingest"
    - "Health data is persisted to day-partitioned files at data/health/YYYY-MM-DD.json"
    - "GET /api/health/metrics returns aggregated metric data for a requested date range"
    - "Existing system health endpoints moved to /api/system/health and /api/system/health/details without breakage"
  artifacts:
    - path: "server/routes/systemHealth.js"
      provides: "Renamed system health routes (was health.js)"
      exports: ["default (router)"]
    - path: "server/lib/appleHealthValidation.js"
      provides: "Zod schemas for Health Auto Export JSON ingest payload"
      exports: ["healthIngestSchema"]
    - path: "server/services/appleHealthIngest.js"
      provides: "JSON ingest, dedup, day-file merge logic"
      exports: ["ingestHealthData"]
    - path: "server/services/appleHealthQuery.js"
      provides: "Date-range queries for health metrics"
      exports: ["getMetrics", "getMetricSummary", "getAvailableDateRange"]
    - path: "server/routes/appleHealth.js"
      provides: "REST endpoints for ingest and query"
      exports: ["default (router)"]
  key_links:
    - from: "server/routes/appleHealth.js"
      to: "server/services/appleHealthIngest.js"
      via: "ingestHealthData(payload)"
      pattern: "ingestHealthData"
    - from: "server/routes/appleHealth.js"
      to: "server/services/appleHealthQuery.js"
      via: "getMetrics(metricName, from, to)"
      pattern: "getMetrics"
    - from: "server/index.js"
      to: "server/routes/appleHealth.js"
      via: "app.use('/api/health', appleHealthRoutes)"
      pattern: "appleHealthRoutes"
    - from: "server/index.js"
      to: "server/routes/systemHealth.js"
      via: "app.use('/api/system', systemHealthRoutes)"
      pattern: "systemHealthRoutes"
---

<objective>
Build the Apple Health data ingest pipeline: Zod validation for Health Auto Export JSON, deduplication by metric+timestamp, day-partitioned file storage at `data/health/YYYY-MM-DD.json`, and query service for reading metrics by date range. Also resolves the `/api/health` namespace conflict by moving existing system health endpoints to `/api/system/health`.

Purpose: Creates the data foundation that both XML import (Plan 03-02) and dashboard cards (Plan 03-03) depend on.
Output: Working REST API at `/api/health/ingest` and `/api/health/metrics`, data persisted to day-partitioned files. System health endpoints relocated to `/api/system/health`.
</objective>

<execution_context>
@/Users/antic/.claude/get-shit-done/workflows/execute-plan.md
@/Users/antic/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-apple-health-integration/03-RESEARCH.md

@server/lib/fileUtils.js — PATHS constants, readJSONFile, ensureDir
@server/lib/meatspaceValidation.js — Zod schema patterns
@server/routes/backup.js — Route pattern with asyncHandler, io access
@server/routes/health.js — EXISTING system health routes (must be renamed to avoid collision)
@server/services/meatspaceAlcohol.js — Service patterns (pure functions, file read/write)
@server/index.js — Route registration pattern

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From server/lib/fileUtils.js:
```javascript
export const PATHS = {
  root: join(__lib_dirname, '../..'),
  data: join(__lib_dirname, '../../data'),
  meatspace: join(__lib_dirname, '../../data/meatspace')
  // ... health needs to be added here
};
export async function ensureDir(dir) { ... }
export async function readJSONFile(filePath, defaultValue) { ... }
```

From server/lib/errorHandler.js:
```javascript
export function asyncHandler(fn) { ... }
export class ServerError extends Error { ... }
```

From server/lib/validation.js:
```javascript
export function validateRequest(schema, data) { ... }
```

From server/routes/health.js (EXISTING — must be relocated):
```javascript
// Currently registered as: app.use('/api', healthRoutes)
// Routes: GET /health, GET /health/system
// These collide with Apple Health's /api/health namespace
// Must be renamed to: GET /system/health, GET /system/health/details
```

From client/src/services/api.js (EXISTING — must be updated):
```javascript
// Currently:
export const checkHealth = () => request('/health');
export const getSystemHealth = (options) => request('/health/system', options);
// Must become:
export const checkHealth = () => request('/system/health');
export const getSystemHealth = (options) => request('/system/health/details', options);
```

Health Auto Export JSON schema (from research):
```json
{
  "data": {
    "metrics": [
      {
        "name": "step_count",
        "units": "count",
        "data": [{ "qty": 8432, "date": "2024-01-15 08:30:00 -0800" }]
      }
    ]
  }
}
```

Day-partitioned file schema:
```json
{
  "date": "2024-01-15",
  "metrics": {
    "step_count": [
      { "date": "2024-01-15 08:30:00 -0800", "qty": 4211, "unit": "count" }
    ]
  },
  "updated": "2024-01-16T10:00:00.000Z"
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Rename system health routes to free /api/health namespace</name>
  <files>
    server/routes/health.js
    server/routes/systemHealth.js
    server/index.js
    client/src/services/api.js
  </files>
  <action>
**Why:** The existing `server/routes/health.js` registers GET `/health` and GET `/health/system`, mounted at `app.use('/api', healthRoutes)` in index.js. This means GET `/api/health` and GET `/api/health/system` are taken. The locked decision from CONTEXT.md specifies Health Auto Export pushes to `/api/health/ingest`. To avoid route collision, we must relocate the system health endpoints to `/api/system/health`.

**1. Rename server/routes/health.js to server/routes/systemHealth.js:**

Use `git mv server/routes/health.js server/routes/systemHealth.js` to preserve git history.

**2. Update route paths inside server/routes/systemHealth.js:**

Change the two route definitions:
- `router.get('/health', ...)` becomes `router.get('/health', ...)` (unchanged, since mount point changes)
- `router.get('/health/system', ...)` becomes `router.get('/health/details', ...)` (rename `/system` to `/details` to avoid redundancy with the new `/api/system` mount point)

The routes inside the file should be:
```javascript
router.get('/health', async (req, res) => { ... });       // → GET /api/system/health
router.get('/health/details', async (req, res) => { ... }); // → GET /api/system/health/details
```

**3. Update server/index.js:**

Change the import:
```javascript
// Before:
import healthRoutes from './routes/health.js';
// After:
import systemHealthRoutes from './routes/systemHealth.js';
```

Change the route registration:
```javascript
// Before:
app.use('/api', healthRoutes);
// After:
app.use('/api/system', systemHealthRoutes);
```

Place the new registration alphabetically among the other `/api/*` routes — near `/api/standardize` or `/api/settings`.

**4. Update client/src/services/api.js:**

Change the two system health API calls:
```javascript
// Before:
export const checkHealth = () => request('/health');
export const getSystemHealth = (options) => request('/health/system', options);
// After:
export const checkHealth = () => request('/system/health');
export const getSystemHealth = (options) => request('/system/health/details', options);
```

**5. Search for any other references to the old paths:**

Run `grep -r "/api/health" client/src/ server/` to find any other references to the old `/api/health` or `/health/system` paths. Update any found references. Key places to check:
- Dashboard components that call `checkHealth()` or `getSystemHealth()` — these call the API functions, not URLs directly, so they should work without changes.
- Any test files referencing `/api/health` — update to `/api/system/health`.
  </action>
  <verify>
    <automated>cd server && npm test</automated>
  </verify>
  <done>
    - server/routes/health.js renamed to server/routes/systemHealth.js via git mv
    - System health routes now at GET /api/system/health and GET /api/system/health/details
    - server/index.js updated: import systemHealthRoutes, mount at /api/system
    - client/src/services/api.js updated: checkHealth and getSystemHealth use new paths
    - No stale references to old /api/health or /api/health/system paths remain
    - All existing tests pass
  </done>
</task>

<task type="auto">
  <name>Task 2: Create validation schemas, ingest service, and day-partitioned storage</name>
  <files>
    server/lib/appleHealthValidation.js
    server/services/appleHealthIngest.js
    server/lib/fileUtils.js
  </files>
  <action>
**1. Add PATHS.health to fileUtils.js:**

In `server/lib/fileUtils.js`, add to the PATHS object:
```javascript
health: join(__lib_dirname, '../../data/health'),
```
Place it alphabetically between `digitalTwin` and `memory`.

**2. Create server/lib/appleHealthValidation.js:**

Zod schemas following the meatspaceValidation.js pattern:

- `healthDataPointSchema`: Object with `date` (string, required), `qty` (number, optional), `Min`/`Avg`/`Max` (number, optional for heart rate), `totalSleep`/`deep`/`rem`/`core`/`awake` (number, optional for sleep). Use `.passthrough()` to allow future fields from app updates.

- `healthMetricSchema`: Object with `name` (string, min 1), `units` (string, optional), `data` (array of healthDataPointSchema).

- `healthIngestSchema`: Object with `data` (object containing `metrics` array of healthMetricSchema, defaulting to `[]`, and `workouts` array of `z.unknown()`, defaulting to `[]`). Use `.passthrough()` on the `data` object to allow future Health Auto Export fields.

Export all three schemas.

**3. Create server/services/appleHealthIngest.js:**

Pure functions + file I/O service:

- `extractDateStr(dateString)`: Extract YYYY-MM-DD from Apple Health timestamp string. Use `dateString.substring(0, 10)` (not Date parsing) to avoid timezone conversion issues per research pitfall 5. Validate with regex `/^\d{4}-\d{2}-\d{2}$/`. Return null for invalid dates.

- `dedupKey(metricName, dateString)`: Return `${metricName}::${dateString}` for dedup comparisons.

- `readDayFile(dateStr)`: Read `join(PATHS.health, dateStr + '.json')` using `readJSONFile` with default `{ date: dateStr, metrics: {} }`.

- `writeDayFile(dateStr, data)`: Ensure PATHS.health dir exists, set `data.updated = new Date().toISOString()`, write JSON with 2-space indent.

- `mergeIntoDay(dateStr, metricName, newPoints)`: Load existing day file, build Set of existing date strings for that metric, filter new points to only those not already present, append unique points, write day file. Return count of new unique points added.

- `ingestHealthData(payload)`: Main entry point. Iterate `payload.data.metrics`. For each metric, group its `data` points by extracted date string (skip points with null/invalid date). For each day group, call `mergeIntoDay`. Accumulate totals. Return `{ metricsProcessed, recordsIngested, recordsSkipped (dupes), daysAffected }`. Log with emoji prefix: `console.log('🍎 Health ingest: ${recordsIngested} records across ${daysAffected} days')`.

**Important:** All metric types from the payload are stored (not just the core four). The decision locks "import ALL Apple Health metric types."
  </action>
  <verify>
    <automated>cd server && node -e "import('./lib/appleHealthValidation.js').then(m => { const r = m.healthIngestSchema.safeParse({ data: { metrics: [{ name: 'step_count', units: 'count', data: [{ date: '2024-01-15 08:30:00 -0800', qty: 100 }] }] } }); console.log(r.success ? 'PASS' : 'FAIL: ' + JSON.stringify(r.error.issues)); })"</automated>
  </verify>
  <done>
    - healthIngestSchema validates Health Auto Export JSON format with passthrough for future fields
    - extractDateStr correctly pulls YYYY-MM-DD from timestamp strings without timezone conversion
    - mergeIntoDay deduplicates by metric name + date string, skipping existing records
    - Day files written to data/health/YYYY-MM-DD.json with correct schema
    - PATHS.health added to fileUtils.js
  </done>
</task>

<task type="auto">
  <name>Task 3: Create query service, REST routes, and client API functions</name>
  <files>
    server/services/appleHealthQuery.js
    server/routes/appleHealth.js
    server/index.js
    client/src/services/api.js
  </files>
  <action>
**1. Create server/services/appleHealthQuery.js:**

Query functions for reading health data:

- `listDayFiles()`: Read the `PATHS.health` directory, return sorted array of date strings (from filenames without .json extension). Handle dir not existing (return `[]`).

- `getAvailableDateRange()`: Call `listDayFiles()`, return `{ from: firstDate, to: lastDate, totalDays }` or null if no files.

- `getMetrics(metricName, from, to)`: Read day files in the date range. For each day file, extract the metric's data array. Return flat array of all data points across the range, sorted by date. If `from`/`to` not provided, default to last 7 days.

- `getMetricSummary(metricName, from, to)`: Call `getMetrics`, then compute summary stats: `count`, `latest` (most recent data point), `average` (of `qty` values if present, or `Avg` for heart rate). Return `{ metricName, count, latest, average, from, to, dataPoints }`.

- `getDailyAggregates(metricName, from, to)`: Group data points by day and aggregate. For `step_count`: sum `qty` per day. For `heart_rate`: average of `Avg` values per day. For `heart_rate_variability_sdnn`: average `qty` per day. For `sleep_analysis`: take `totalSleep` directly. For all others: average `qty`. Return array of `{ date, value }` objects.

- `getCorrelationData(from, to)`: Read health metrics AND alcohol data (import `getDailyAlcohol` from `meatspaceAlcohol.js`) for the date range. Build daily records with: `{ date, hrv, alcoholGrams, steps }`. Return the merged array. Also read blood tests (import `getBloodTests` from `meatspaceHealth.js`) for activity-blood correlation. Return `{ dailyData, bloodTests }`.

**2. Create server/routes/appleHealth.js:**

Following backup.js route pattern with asyncHandler:

- `POST /ingest`: Validate body with `healthIngestSchema` using `validateRequest`. Call `ingestHealthData(validated)`. Return result object.

- `GET /metrics/:metricName`: Query params `from`, `to` (optional strings). Call `getMetricSummary(metricName, from, to)`. Return summary.

- `GET /metrics/:metricName/daily`: Query params `from`, `to`. Call `getDailyAggregates(metricName, from, to)`. Return array.

- `GET /range`: Call `getAvailableDateRange()`. Return range object.

- `GET /correlation`: Query params `from`, `to`. Call `getCorrelationData(from, to)`. Return correlation data.

Export default router.

**3. Register routes in server/index.js:**

Add import at top (alphabetical with other route imports):
```javascript
import appleHealthRoutes from './routes/appleHealth.js';
```

Add route registration — mount at `/api/health` to match the locked decision path `/api/health/ingest`:
```javascript
app.use('/api/health', appleHealthRoutes);
```

Place this before `/api/instances` (alphabetically). Note: The old system health routes were already moved to `/api/system` in Task 1, so `/api/health` is now free.

**4. Add client API functions in client/src/services/api.js:**

Add a new section "// Apple Health" (alphabetical placement, near the top after "// Apps"):

```javascript
// Apple Health
export const ingestAppleHealth = (data) => request('/health/ingest', {
  method: 'POST',
  body: JSON.stringify(data)
});
export const getAppleHealthMetrics = (metricName, from, to) => {
  const params = new URLSearchParams();
  if (from) params.set('from', from);
  if (to) params.set('to', to);
  return request(`/health/metrics/${metricName}/daily?${params}`);
};
export const getAppleHealthSummary = (metricName, from, to) => {
  const params = new URLSearchParams();
  if (from) params.set('from', from);
  if (to) params.set('to', to);
  return request(`/health/metrics/${metricName}?${params}`);
};
export const getAppleHealthRange = () => request('/health/range');
export const getAppleHealthCorrelation = (from, to) => {
  const params = new URLSearchParams();
  if (from) params.set('from', from);
  if (to) params.set('to', to);
  return request(`/health/correlation?${params}`);
};
```
  </action>
  <verify>
    <automated>cd server && node -e "import('./routes/appleHealth.js').then(m => console.log(m.default ? 'PASS: router exported' : 'FAIL'))" && node -e "import('./services/appleHealthQuery.js').then(m => console.log(typeof m.getMetrics === 'function' && typeof m.getDailyAggregates === 'function' ? 'PASS' : 'FAIL'))"</automated>
  </verify>
  <done>
    - GET /api/health/metrics/:metricName returns metric summary for date range
    - GET /api/health/metrics/:metricName/daily returns daily aggregated values
    - GET /api/health/range returns available data date range
    - GET /api/health/correlation returns merged HRV+alcohol+steps+blood data
    - POST /api/health/ingest validates and persists Health Auto Export JSON
    - Route registered in index.js at /api/health (namespace freed by Task 1)
    - Client API functions added for all endpoints using /health/* paths
  </done>
</task>

</tasks>

<verification>
1. `curl -s http://localhost:5554/api/system/health | node -e "process.stdin.on('data',d=>{const r=JSON.parse(d);console.log(r.status==='ok'?'PASS':'FAIL')})"` -- system health moved successfully
2. `cd server && node -e "import('./lib/appleHealthValidation.js').then(m => console.log('validation OK'))"` -- validation module loads
3. `cd server && node -e "import('./services/appleHealthIngest.js').then(m => console.log('ingest OK'))"` -- ingest service loads
4. `cd server && node -e "import('./services/appleHealthQuery.js').then(m => console.log('query OK'))"` -- query service loads
5. `cd server && node -e "import('./routes/appleHealth.js').then(m => console.log('routes OK'))"` -- routes load
6. `cd server && npm test` -- all existing tests pass (no regressions)
</verification>

<success_criteria>
- Existing system health endpoints relocated to /api/system/health and /api/system/health/details
- POST /api/health/ingest accepts Health Auto Export JSON and returns record counts (matches locked decision)
- Duplicate records are silently skipped on re-ingest
- Data persists to data/health/YYYY-MM-DD.json files
- Query service returns aggregated metrics for date ranges
- All existing tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/03-apple-health-integration/03-01-SUMMARY.md`
</output>
