P247 iOS: Phase 2 Implementation Brief
P247 iOS: Phase 2 Implementation Brief
Date: 25 March 2026
For: iOS Development Team
From: Myles Bruggeling, Founder
Status: Backend ready. iOS work required.
Production API: https://app.p247.io
Auth: x-api-key header on all requests
What This Covers
Four features the iOS app needs. Backend is live and tested for all of them. The app just needs to wire in.
1. HealthKit Native Sync (Replace Health Auto Export)
Currently the app relies on a third-party app (Health Auto Export) to get Apple Health data to the backend. This needs to go. The P247 app should read HealthKit directly and push data to our API.
Drop-in Code
HealthKitManager.swift is written and ready: p247/ios/HealthKitSync/HealthKitManager.swift
It handles:
- Permission requests for all 35+ HealthKit types P247 uses
- Background observer queries (iOS wakes the app when new data arrives)
- BGTaskScheduler fallback sync every 2-4 hours
- 14-day backfill on first launch with inline progress
- Daily aggregation (raw samples → one summary per day)
- Deduplication
What You Need To Do
- Add
HealthKitManager.swiftto the Xcode project - Add capabilities: HealthKit, Background Modes (Background fetch, Background processing)
- Add Info.plist usage descriptions for every HealthKit type (Apple rejects without these)
- Call
HealthKitManager.shared.requestAuthorization()during onboarding - Show backfill progress inline on the Home screen (“Syncing 14 days of health data… 6/14”)
- Register background tasks in
AppDelegate/@mainApp struct
API Endpoint
POST /sync/healthkit (live, tested)
{
"date": "2026-03-25",
"timezone": "Australia/Sydney",
"device_sync_timestamp": "2026-03-25T07:00:00+11:00",
"metrics": {
"hrv": 32,
"resting_hr": 57,
"heart_rate_samples": { "avg": 72, "min": 45, "max": 165, "count": 1440 },
"step_count": 8432,
"distance_km": 6.2,
"active_calories": 520,
"basal_calories": 1854,
"exercise_min": 45,
"vo2max": 39.7,
"daylight_min": 35,
"respiratory_rate": 18,
"blood_oxygen_saturation": 97,
"protein_g": 145,
"carbs_g": 280,
"total_fat": 65,
"fiber_g": 22,
"dietary_energy_kj": 2100,
"sugar_g": 45,
"water_ml": 2400,
"potassium_mg": 3500,
"walking_speed_kmh": 5.2,
"apple_sleeping_wrist_temperature": 36.3,
"alcohol_drinks": 0
},
"sleep": {
"total_hours": 7.2,
"core_hours": 4.8,
"deep_hours": 0.9,
"rem_hours": 1.5,
"awake_hours": 0.3,
"in_bed_start": "2026-03-24T22:15:00+11:00",
"in_bed_end": "2026-03-25T05:45:00+11:00",
"source": "Apple Watch"
},
"workouts": [
{
"type": "FunctionalStrengthTraining",
"start": "2026-03-25T06:00:00+11:00",
"end": "2026-03-25T06:55:00+11:00",
"duration_seconds": 3300,
"energy_kcal": 420,
"avg_heart_rate": 145,
"max_heart_rate": 172,
"distance_m": null,
"source": "Apple Watch"
}
],
"running_mechanics": {
"running_speed_kmh": 10.2,
"running_power_w": 285,
"running_stride_m": 1.12,
"running_oscillation_cm": 8.2,
"running_ground_contact_time": 248
}
}
Response: {"status": "accepted", "user": "...", "received_at": "...", "records": 1}
Important Notes
- Send one POST per day of data, not raw samples
- Energy units: send kcal (the field name
dietary_energy_kjis a legacy misnomer, it expects kcal) - Sleep: attribute to wake-up date (Monday night’s sleep = Tuesday’s POST)
- The app must work without HealthKit permission (Apple requirement). Show a banner prompting to connect.
- Backfill: 14 days on first permission grant, one POST per day, resume if interrupted
Verify Sync Is Working
GET /sync/stats returns counts per data type:
{"user": "...", "healthkit_syncs": 14, "checkins": 0, "body_compositions": 0}
GET /sync/status returns last sync timestamps:
{"last_healthkit_sync": "2026-03-25T07:00:00Z", "last_checkin": null, "last_bodycomp": null}
Estimate: 2-3 days to integrate HealthKitManager.swift + test on device.
2. Coach Image Attachments
The Coach tab currently drops images silently. When an athlete attaches a photo, the app puts the filename as text (“Attached image: photo-2026-03-24.jpg”) instead of sending the actual image data. The coach never sees the image and hallucinates a response.
What You Need To Do
- Add image picker to the Coach message composer (camera + photo library)
- On image selection:
- Resize to max 1024px on longest edge
- Convert HEIC to JPEG
- Base64 encode the result
- Send in the
imagesarray of the POST request (NOT as text incontent) - Display image thumbnails in conversation history (tap to view full size)
API: POST /agent/messages (updated, live)
{
"content": "Here's today's workout",
"images": [
{
"data": "<base64-encoded JPEG, no data: prefix>",
"mime_type": "image/jpeg",
"filename": "workout.jpg"
}
]
}
Validation (enforced server-side, enforce client-side too)
- Max 4 images per message
- Max 5MB per image (decoded size)
- Allowed types:
image/jpeg,image/png,image/webp,image/heic - HEIC is auto-converted to JPEG server-side as a safety net
Response
Same as text-only, plus images array on the user message:
{
"user_message": {
"id": "msg_35",
"role": "user",
"content": "Here's today's workout",
"type": "text",
"read": true,
"timestamp": "2026-03-25T00:00:00Z",
"images": ["images/1_abc123def456.jpg"]
},
"agent_message": {
"id": "msg_36",
"role": "agent",
"content": "Mixed modal session today...",
"type": "text",
"read": false,
"timestamp": "2026-03-25T00:00:05Z"
}
}
Conversation History
GET /agent/messages now returns images on messages that had attachments. Display these as thumbnails in the chat view.
Critical
Do NOT put the filename or “Attached image:” text into the content field. That’s the current bug. The image data must go in the images array as base64. The content field should only contain the athlete’s typed text (or empty string if they just sent a photo with no caption).
Estimate: 1-2 days for image picker + compression. 1 day for thumbnail display in history.
3. Hydration Display
The brief nutrition section now returns water_ml. Display it alongside the existing nutrition metrics.
Brief Response (nutrition block)
"nutrition": {
"calories": 2278,
"protein_g": 146,
"carbs_g": 283,
"fat_g": 16,
"fiber_g": 30,
"water_ml": 2400
}
water_ml will be null until the data pipeline is connected (pending HAE config change or native HealthKit sync). The UI should handle null gracefully (hide or show “No data”).
Estimate: 30 minutes.
4. Push Notifications (Blocked)
Push notification delivery requires an APNs .p8 signing key from the Apple Developer account. The backend endpoints are ready:
POST /users/me/device-token(live) — register the device tokenPUT /users/me/notification-preferences(live) — set preferencesGET /users/me/notification-preferences(live) — get preferences
iOS side to build:
- Request notification permission during onboarding
- Register for remote notifications
- Send device token to
POST /users/me/device-tokenwith{"token": "...", "platform": "ios"} - Build notification preferences screen in Settings (toggles for: morning brief, health alerts, coaching messages, weekly summary, sync warnings + time picker for morning brief)
- Implement
UNUserNotificationCenterdelegate for foreground handling - Notification tap opens relevant brief
Blocked until: APNs .p8 key is generated from Apple Developer account and configured on the backend.
Estimate: 1 week (once .p8 key is available).
5. Strava Activity Feed
Activities are now synced from Strava into the P247 database. The iOS app can display an activity feed.
Endpoints (all live)
| Method | Endpoint | Description |
|---|---|---|
POST /activities/sync |
Trigger a sync from Strava (incremental, deduplicates) | |
GET /activities/?limit=20&offset=0&type=Run |
List activities, newest first. Paginated. Filterable by type. | |
GET /activities/{strava_id} |
Single activity detail | |
GET /activities/summary/weekly |
7-day summary: count, distance, time, type breakdown |
Activity types returned
Run, Ride, Swim, Walk, WeightTraining, Workout, Hike, TrailRun, VirtualRun, etc.
Response fields per activity
id, name, type, sport_type, start_date, start_date_local, elapsed_time_seconds, moving_time_seconds, moving_time_display (human readable, e.g. “1h 08m”), distance_m, distance_km, elevation_gain_m, average_speed_ms, max_speed_ms, average_heartrate, max_heartrate, calories, average_watts, suffer_score, has_heartrate, pace (calculated for runs, e.g. “6:34/km”).
What to build
- Activity feed screen showing recent activities (name, type, time, distance, HR, pace)
- Pull-to-refresh calls
POST /activities/syncthen reloads the list - Activity detail view with full enriched data (see Section 5.1 below)
- Weekly summary card on the home screen or activity tab
5.1 Activity Detail View (Enriched — Backend Live)
GET /activities/{strava_id} now returns Apple Fitness-level detail. The backend runs enrichment on first view (fetches Strava streams, calculates zones/TRIMP/effort, generates AI summary) and caches everything.
Sections to render (conditional by activity type, see P247-Activity-Detail-Enrichment-Spec.md for full matrix):
| Section | Data field | Chart type |
|---|---|---|
| AI Summary | summary |
Text paragraph |
| Body Regions | body_regions |
Tag pills (strength/workout only) |
| Heart Rate | heart_rate.samples |
Bar chart, color by zone |
| HR Zones | heart_rate.zones |
Horizontal stacked bars (6 zones) |
| HR Zone Context | heart_rate.zones_context |
Text |
| Effort | effort.score, effort.label, effort.comparison |
Score dial (0-10) |
| Training Load | training_load.trimp_exp |
Gauge |
| Training Load Focus | training_load.focus |
Stacked bar (anaerobic/high/low aerobic) |
| Training Load Effect | training_load.effect |
Before/after gauges |
| Intensity | intensity.percentage, intensity.label |
Progress bar |
| Speed/Pace | speed.samples |
Bar chart (runs/rides) |
| Elevation | elevation.samples |
Area chart (runs/rides/walks) |
| Route Map | route.coordinates |
MapKit polyline |
| Power | power.samples |
Line chart (if data exists) |
| HR Recovery | heart_rate.recovery |
Line chart + rating badge |
| METs | mets |
Single value display |
Use Swift Charts for all time-series. HR chart bars should be color-coded by zone (see spec for zone boundary calculations).
Estimate: 2 weeks for full detail view with charts, map, and all conditional sections.
Full spec: p247/docs/P247-Activity-Detail-Enrichment-Spec.md
Priority Order
| # | Feature | Effort | Blocked? |
|---|---|---|---|
| 1 | HealthKit native sync | 2-3 days | No |
| 2 | Plan tab (Events → Plan) | 3-4 days | No |
| 3 | Coach image attachments | 2-3 days | No |
| 4 | Strava activity feed (list + sync) | 2-3 days | No |
| 5 | Activity detail view (enriched, charts, map) | 2 weeks | No |
| 6 | Hydration display | 30 min | No |
| 7 | Push notifications | 1 week | Yes (APNs .p8 key) |
Items 1-6 can start immediately. All backend work is complete.
4. Plan Tab (Events → Plan)
The “Events” tab becomes “Plan”. It’s a unified daily action feed showing completed workouts, coach-suggested sessions, scheduled events, and recovery alerts in one chronological view.
This is a full feature spec. See P247-Plan-Tab-iOS-Brief.md for the complete implementation brief including card layouts, SwiftUI wireframes, design tokens, merge logic, exercise checkbox behaviour, and empty state.
Summary of what’s needed
- Rename Events tab to “Plan”, change icon to clipboard (SF Symbol:
list.clipboard) - Build 4 card types: CompletedWorkoutCard, CoachSuggestionCard, RecoveryAlertCard, EventCard
- Build the Plan feed view with “Completed” / “Now” marker / “Up Next” sections
- Integrate
GET /plan/today(new) + merge with existingGET /events/ - Exercise checkboxes + start/complete workout flow
- Handle
plan_iteminPOST /agent/messagesresponse (badge Plan tab, show toast)
Key endpoints
GET /plan/today → today's plan items
GET /plan/history?days=7 → historical view
POST /plan/ → create plan item
POST /plan/{item_id}/start → mark as in-progress
POST /plan/{item_id}/complete → mark done + exercise completion data
DELETE /plan/{item_id} → dismiss (soft delete)
Coach auto-generation
When the athlete asks the coach for a workout suggestion, the coach automatically creates a plan item. The POST /agent/messages response now includes an optional plan_item field:
{
"user_message": {...},
"agent_message": {...},
"plan_item": {
"id": "plan_2",
"type": "coach_suggestion",
"title": "PM Session: Pull + Mobility"
}
}
When plan_item is present, show a toast (“Added to Plan ✨”) and/or badge the Plan tab.
Estimate: 3-4 days.
Reference Documents
p247/ios/HealthKitSync/HealthKitManager.swift— drop-in HealthKit sync codep247/ios/HealthKitSync/README.md— integration guidep247/docs/P247-Plan-Tab-iOS-Brief.md— full Plan tab iOS implementation specp247/docs/P247-Coach-Image-Support-and-Prompt-Rewrite.md— full image support spec (includes plan item auto-generation)p247/docs/P247-Strava-Connect-iOS-Brief.md— Strava OAuth (already implemented, for reference)p247/docs/P247-Phase2-HealthKit-Sync-and-Notifications-Brief.md— full Phase 2 architecture
Questions?
Ping Myles in #p247-backend-app.