- Add KeychainService for encrypted token storage (auth, refresh, health JWT, API key) - Remove hardcoded email/password from HealthAPIService, store in Keychain - Move all tokens from UserDefaults to Keychain - API key sent via X-API-Key header instead of URL query parameter - Replace force unwrap URL(string:)! with guard let + throws - Fix force unwrap Calendar.date() in HealthKitService - Mark HealthKitService @MainActor for thread-safe @Published - Use withTaskGroup for parallel habit log fetching in TrackerView - Check notification permission before scheduling reminders - Add input validation (title max 200 chars) - Add privacy policy and terms links in Settings - Update CLAUDE.md with security section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
155 lines
8.4 KiB
Markdown
155 lines
8.4 KiB
Markdown
# Pulse Health — iOS App
|
||
|
||
## Overview
|
||
iOS-приложение для управления жизнью: привычки, задачи, финансы, накопления, здоровье. Интегрируется с Apple Watch через HealthKit и серверным API.
|
||
|
||
- **Bundle ID:** com.daniil.pulsehealth
|
||
- **Platform:** iOS 17+, SwiftUI, Swift 5.9
|
||
- **Build:** XcodeGen (project.yml → .xcodeproj)
|
||
- **Team ID:** V9AG8JTFLC
|
||
- **Язык интерфейса:** Русский
|
||
|
||
## Architecture
|
||
|
||
### Project Structure
|
||
```
|
||
PulseHealth/
|
||
├── App.swift # Entry point, AuthManager, BGTask registration
|
||
├── Models/
|
||
│ ├── AuthModels.swift # Login/Register/Refresh requests & responses
|
||
│ ├── UserModels.swift # UserProfile, UpdateProfileRequest
|
||
│ ├── HealthModels.swift # Readiness, Latest health, Heatmap, SleepSegment
|
||
│ ├── TaskModels.swift # PulseTask, CreateTask, UpdateTask (priorities 1-3)
|
||
│ ├── HabitModels.swift # Habit, HabitLog, HabitFreeze, HabitStats, HabitFrequency enum
|
||
│ ├── FinanceModels.swift # Transaction, Category (emoji field, not icon), Summary, Analytics
|
||
│ └── SavingsModels.swift # Category, Transaction, Stats, RecurringPlan, MonthlyPaymentDetail, OverduePayment
|
||
├── Services/
|
||
│ ├── APIService.swift # Main REST client (api.digital-home.site), auto token refresh on 401
|
||
│ ├── HealthAPIService.swift # Health API (health.digital-home.site), separate JWT auth
|
||
│ ├── HealthKitService.swift # HealthKit data collection & sync, sleep segments timeline
|
||
│ └── NotificationService.swift # Local push notifications (morning/evening, task deadlines)
|
||
├── Views/
|
||
│ ├── DesignSystem.swift # Theme colors, GlassCard modifier, GlowIcon, GlowStatCard
|
||
│ ├── MainTabView.swift # Custom tab bar with glow effects (VStack layout, not ZStack)
|
||
│ ├── LoginView.swift # Auth + ForgotPassword
|
||
│ ├── Dashboard/
|
||
│ │ └── DashboardView.swift # Home: progress, stats, habits, tasks, FAB
|
||
│ ├── Health/
|
||
│ │ ├── HealthView.swift # Full health dashboard: readiness, metrics, HR, trends, recovery, tips
|
||
│ │ ├── MetricCardView.swift # SleepCard, StepsCard, SleepPhasesCard, InsightsCard, GradientIcon
|
||
│ │ ├── WeeklyChartView.swift # Line chart (not bar), animated
|
||
│ │ ├── SleepDetailView.swift # Detailed sleep timeline with phases from HealthKit
|
||
│ │ ├── ReadinessCardView.swift # (empty, replaced by ReadinessBanner in HealthView)
|
||
│ │ └── ToastView.swift # Toast notification modifier
|
||
│ ├── Tracker/
|
||
│ │ └── TrackerView.swift # Tabs: Habits, Tasks, Statistics. Separate tap zones for edit vs toggle
|
||
│ ├── Tasks/
|
||
│ │ ├── TasksView.swift
|
||
│ │ ├── AddTaskView.swift
|
||
│ │ ├── EditTaskView.swift
|
||
│ │ └── TaskRowView.swift
|
||
│ ├── Habits/
|
||
│ │ ├── HabitsView.swift
|
||
│ │ ├── AddHabitView.swift
|
||
│ │ ├── EditHabitView.swift
|
||
│ │ └── HabitRowView.swift
|
||
│ ├── Finance/
|
||
│ │ ├── FinanceView.swift # Overview, Transactions, Analytics, Categories tabs
|
||
│ │ └── AddTransactionView.swift
|
||
│ ├── Savings/
|
||
│ │ ├── SavingsView.swift # Overview (monthly payments, overdues), Categories, Operations
|
||
│ │ └── EditSavingsCategoryView.swift
|
||
│ ├── Settings/
|
||
│ │ └── SettingsView.swift # Appearance, Profile, Timezone (Menu picker)
|
||
│ └── Profile/
|
||
│ └── ProfileView.swift # ChangePasswordView
|
||
```
|
||
|
||
## API Integration
|
||
|
||
### Main API: `https://api.digital-home.site`
|
||
- Auth: JWT Bearer token with auto-refresh via `/auth/refresh`
|
||
- AuthManager stores token + refreshToken in UserDefaults
|
||
- APIService.authManager weak ref enables transparent 401 → refresh → retry
|
||
|
||
**Endpoints used:**
|
||
- Auth: login, register, me, refresh
|
||
- Profile: GET/PUT /profile
|
||
- Tasks: CRUD + complete/uncomplete, priorities 1-3 (not 4!)
|
||
- Habits: CRUD + log (sends `date` not `completed_at`), freezes, stats
|
||
- Habits frequency: iOS uses daily/weekly/monthly/interval internally, sends `custom` to API for monthly/interval
|
||
- Finance: transactions, categories (field is `emoji` not `icon`), summary, analytics
|
||
- Savings: categories, transactions (date required!), stats (includes monthly_payment_details, overdues), recurring plans
|
||
|
||
### Health API: `https://health.digital-home.site`
|
||
- Separate JWT auth (hardcoded credentials in HealthAPIService)
|
||
- API key for data sync: `health-cosmo-2026`
|
||
- Server code: `/Users/daniilklimov/Personal/health-webhook` (Node.js + SQLite)
|
||
|
||
**Data flow:**
|
||
1. Apple Watch → HealthKit on iPhone
|
||
2. App collects from HealthKit → POST /api/health?key=API_KEY
|
||
3. health-webhook stores JSON files → parses on GET requests
|
||
4. App displays from GET /api/health/latest, /readiness, /heatmap
|
||
|
||
**HealthKit metrics collected:**
|
||
- step_count, heart_rate, resting_heart_rate, heart_rate_variability
|
||
- active_energy (kcal→kJ), blood_oxygen_saturation, walking_running_distance
|
||
- respiratory_rate, sleep_analysis (with phase breakdown: deep/rem/core/awake + timestamps)
|
||
|
||
**Sleep format for webhook:**
|
||
```json
|
||
{"totalSleep": 7.5, "deep": 1.2, "rem": 2.0, "core": 4.3, "awake": 0.5,
|
||
"inBedStart": "...", "sleepEnd": "...", "source": "Apple Watch"}
|
||
```
|
||
|
||
**API response field mapping (CodingKeys):**
|
||
- `spo2` → `bloodOxygen` (BloodOxygenData)
|
||
- `respiratoryRate` → as-is
|
||
- `distance` → as-is
|
||
- `activeEnergy` → as-is
|
||
|
||
## Design System
|
||
- **Background:** `#06060f` (deep dark)
|
||
- **Accent:** `#0D9488` (teal)
|
||
- **Glass cards:** `.glassCard()` modifier — ultraThinMaterial + gradient fill + gradient stroke
|
||
- **Glow icons:** `GlowIcon` — circle with blur glow behind
|
||
- **Tab bar:** Custom VStack-based (not standard TabView), each tab has its own glow color
|
||
- **All sheets:** `.presentationDetents([.large])`, background `Color(hex: "06060f")`
|
||
- **Color pickers:** LazyVGrid 5 columns (not HStack — overflow on small screens)
|
||
- **App icon:** Glassmorphism style, Assets.xcassets/AppIcon.appiconset
|
||
|
||
## Security
|
||
- **Keychain** — все токены (auth, refresh, health JWT, API key) хранятся в iOS Keychain через `KeychainService.swift`, не в UserDefaults
|
||
- **Health credentials** — email/password для health API хранятся в Keychain, устанавливаются один раз при первом запуске
|
||
- **API key** — передаётся в `X-API-Key` header, не в URL query parameter
|
||
- **No force unwraps** — URL создаются через guard/optional binding
|
||
- **HealthKitService** — помечен `@MainActor` для thread-safe @Published
|
||
- **Privacy policy** — ссылки в Settings (pulse.digital-home.site/privacy, /terms)
|
||
|
||
## Key Design Decisions & Gotchas
|
||
- **Buttons in ScrollView/List MUST have `.buttonStyle(.plain)`** — otherwise taps get swallowed
|
||
- **Tracker rows:** Separate tap zones — `.onTapGesture` on text area for edit, `Button` with `.buttonStyle(.plain)` for checkbox
|
||
- **`try?` is avoided in save functions** — errors are shown in UI via `@State errorMessage`
|
||
- **Tab bar uses VStack, not ZStack** — prevents content overlap
|
||
- **onChange uses iOS 17+ syntax:** `{ }` not `{ _ in }`
|
||
- **XcodeGen:** All capabilities must be in project.yml, manual Xcode changes get reset
|
||
|
||
## Background Sync
|
||
- BGProcessingTask: `com.daniil.pulsehealth.healthsync`
|
||
- Scheduled every ~30 minutes
|
||
- Collects HealthKit data and POSTs to health-webhook
|
||
- Registered in App.init(), scheduled in .onAppear
|
||
|
||
## External Services & Paths
|
||
- **Pulse API source:** `/Users/daniilklimov/digital-home/pulse-api` (Go)
|
||
- **Pulse Web source:** `/Users/daniilklimov/digital-home/pulse-web` (React)
|
||
- **Health webhook:** `/Users/daniilklimov/Personal/health-webhook` (Node.js)
|
||
- **Infrastructure docs:** `~/Obsidian/daniil/Инфраструктура`
|
||
|
||
## Known Issues / TODO
|
||
- Finance tab is owner-only (user.id === 1) in web, no such restriction in iOS
|
||
- Savings members endpoints (multi-user) not implemented in iOS
|
||
- Auth: password change works via direct URLRequest, not through APIService
|
||
- Health readiness `activity.value` from API shows different step count than latest (different time periods)
|