Files
pulse-mobile/CLAUDE.md
Daniil Klimov 44c759c190 fix: security hardening — Keychain, no hardcoded creds, safe URLs
- 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>
2026-04-06 14:11:10 +03:00

155 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)