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

8.4 KiB
Raw Permalink Blame History

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:

{"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):

  • spo2bloodOxygen (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)