--- title: "Reflect App — Dev Setup" aliases: - "Dev Setup" - "reflect-app dev" - "dev-setup" - "iOS Android setup" created: '2026-05-16' updated: '2026-05-31' tags: - project - dev - ios - swift - android - skip related: - "[[personal/projects/psychologist-app/overview]]" - "[[personal/projects/psychologist-app/skip-tools-poc]]" - "[[personal/projects/nolvu-reflect-skip]]" --- # Reflect App — Dev Setup & Architecture > **iOS (reflect-app):** `~/Desktop/reflect-app/` — нативный SwiftUI, не трогать > **Cross-platform (reflect-skip):** `~/Developer/reflect-skip/` — Skip Fuse, iOS + Android > Gitea: `git.mallexxx.duckdns.org/git_admin/reflect-app` ## F-Droid Internal Repo | Параметр | Значение | |---|---| | **URL** | `https://reflect.qentra.top` | | CF Tunnel | mbp black (same as zulip.qentra.top), hostname reflect → localhost:8766 | | CF Token (Zone:DNS) | `cfat_iKrPBaTAqlL8Zo7Ss4EHaiGqRGxBK4RDVX78jhgD51fdd7a1` | | Сервер | `python3 ~/Developer/reflect-skip/scripts/fdroid-server.py` (LaunchAgent: com.reflect.fdroid-server) | | Workspace | `~/Developer/reflect-skip/fdroid-workspace/` — подписанный репо, генерируется `fdroid update` | ### Как устроен деплой Репо генерируется через **fdroidserver** (`pip3 install fdroidserver`), а не вручную. HTTP-сервер (`fdroid-server.py`) раздаёт содержимое `fdroid-workspace/repo/` — там лежат подписанные `index-v1.jar`, `entry.jar`, `index-v2.json`. Ключевые файлы workspace: ``` fdroid-workspace/ ├── config.yml ← repo_url, keystore, имя репо ├── keystore.p12 ← подписывающий ключ (не коммитить) └── repo/ ← раздаётся HTTP-сервером ├── index-v1.jar ← подписанный JAR (Droid-ify требует его) ├── entry.jar ← новый формат entry point ├── index-v2.json └── reflect-app-debug.apk ``` ### Сборка и публикация APK ```bash cd ~/Developer/reflect-skip # Полная сборка (Xcode + Gradle) + публикация bash scripts/build-and-publish.sh # Быстрая (только Gradle, если Swift не менялся) bash scripts/build-and-publish.sh --quick ``` `build-and-publish.sh` вызывает `generate-index.sh`, который: 1. Копирует APK в `fdroid-workspace/repo/` 2. Запускает `fdroid update -c` (JAVA_HOME=/opt/homebrew/opt/openjdk@21) 3. Генерирует подписанные JAR и JSON индексы ### Первоначальная настройка (один раз) ```bash cd ~/Developer/reflect-skip/fdroid-workspace # Удалить старый keystore если есть, создать новый keytool -genkey -v \ -keystore keystore.p12 -storetype PKCS12 \ -alias fdroid -storepass reflect123 -keypass reflect123 \ -keyalg RSA -keysize 2048 -validity 10000 \ -dname "CN=Reflect App, O=Dev, C=US" # Сгенерировать репо ANDROID_HOME=~/Library/Android/sdk \ JAVA_HOME=/opt/homebrew/opt/openjdk@21 \ PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH" \ fdroid update -c ``` ### Добавить репо в Droid-ify Settings → Repositories → **+** → `https://reflect.qentra.top` > ⚠️ URL без `/repo` — сервер уже раздаёт содержимое `fdroid-workspace/repo/` напрямую с корня. --- ## Статус (2026-05-19) ### Что реализовано | Компонент | Статус | |-----------|--------| | iOS build (reflect-app) | ✅ Build succeeded, 30/30 тестов | | Android build (reflect-skip) | ✅ APK собирается, запускается на эмуляторе | | Экран 0: Privacy intro | ✅ Личный текст, GDPR opt-out, дисклеймер | | Онбординг | ✅ 3 info-слайда + 5 вопросов + AI инсайт | | Сессия: dynamic intro | ✅ AI-генерированное вступление + max 2 крючка из истории | | Сессия: one-by-one вопросы | ✅ Вопросы по одному, батч-подбивка агенту | | Сессия: closing phrase | ✅ Тёплая фраза от Narrator'а | | Сессия: one last thing | ✅ Маленький input + неакцентная кнопка Завершить | | Dual-bot pipeline | ✅ Narrator (Haiku) → Analyst (Sonnet) | | Crisis detection | ✅ Hardcoded RU+EN, CrisisView | | Profile vault | ✅ profile.md, sessions/, insights.md | | App Lock (Face ID) | ✅ BiometricAuthentication | | Settings: API key, proxy | ✅ Keychain + LocalProxy | | Settings: Сбросить данные | ✅ Алерт + полный сброс + онбординг заново | | Session scheduler | ✅ UNCalendarNotification | ### Что не реализовано (план) | Компонент | Приоритет | | ---------------------------------- | ---------------------- | | ProfileView: история сессий | 🟠 Средний | | Session recovery (незавершённые) | 🟠 Средний | | Полноценный sessions/YYYY-MM-DD.md | 🟡 Низкий | | RevenueCat paywall | 🔴 Нужен до публикации | | Голосовой ввод (iOS) | 🟡 Phase 2 | | Push-уведомления opt-in | 🟡 Phase 2 | | Session id to measure token burn | | | Analytics | | --- Paywall option - Adapty ## Android Signing **Keystore:** `~/.android/signing/nolvu-release.jks` (вне репо, не в git) **Fingerprint (SHA-256):** `9A:17:73:F0:A7:4E:8C:7B:2E:AA:D5:0F:AD:FC:71:84:92:D1:88:FB:04:2F:32:56:63:12:ED:3E:28:3E:2B:F7` **keystore.properties:** `Android/app/keystore.properties` (в .gitignore, абсолютный путь к keystore) - storeFile: `/Users/admin/.android/signing/nolvu-release.jks` - keyAlias: `nolvu` / пароли: `nolvu2026` > При переезде на новую машину — скопировать `~/.android/signing/nolvu-release.jks` и пересоздать `Android/app/keystore.properties` с правильным путём. > Fingerprint при этом останется тем же — пользователям переподключать репо не нужно. ## Android / Skip Fuse — сборка ### Требования - Xcode 16+, `xcodebuild` - Java 21: `/opt/homebrew/opt/openjdk@21` - Android SDK: `~/Library/Android/sdk` - Android Emulator: `reflect-android` (Pixel 6, API 35) - Skip CLI: `skip` (из Homebrew или из Xcode DerivedData) ### Полная сборка (первый раз или после новых Swift файлов) ```bash cd ~/Developer/reflect-skip export ANDROID_HOME=~/Library/Android/sdk export JAVA_HOME=/opt/homebrew/opt/openjdk@21 export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:/opt/homebrew/bin:$PATH" # Шаг 1: Xcode генерирует Kotlin стабы (skipstone) + собирает Android xcodebuild \ -workspace Project.xcworkspace \ -scheme "ReflectApp App" \ -destination "generic/platform=iOS Simulator" \ -configuration Debug \ -skipPackagePluginValidation \ SKIP_ACTION=build # Шаг 2: Линкуем symlinks субмодулей (один раз после очистки) # Делается скриптом или вручную — см. раздел ниже # Шаг 3: Gradle собирает APK cd Darwin skip gradle -p ../Android assembleDebug ``` ### Быстрая пересборка (инкрементальная) ```bash cd ~/Developer/reflect-skip/Darwin export ANDROID_HOME=~/Library/Android/sdk JAVA_HOME=/opt/homebrew/opt/openjdk@21 export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:/opt/homebrew/bin:$PATH" skip gradle -p ../Android assembleDebug ``` ### Деплой на эмулятор ```bash export ANDROID_HOME=~/Library/Android/sdk export PATH="$ANDROID_HOME/platform-tools:$PATH" # Запустить эмулятор если не запущен $ANDROID_HOME/emulator/emulator -avd reflect-android & APK=~/Developer/reflect-skip/.build/Android/app/outputs/apk/debug/app-debug.apk adb -s emulator-5554 install -r "$APK" adb -s emulator-5554 shell am start -n com.reflect.app/reflect.app.MainActivity ``` --- ## ⚠️ Критические особенности Skip Fuse ### 1. Новые Swift View файлы требуют ручного Kotlin стаба При добавлении нового `*.swift` файла с View в `Sources/ReflectApp/Views/`: skipstone **не всегда** автоматически генерирует `.kt` стаб. Нужно создать вручную. **Шаблон** (копируем `OnboardingView.kt`, меняем класс): ```kotlin package reflect.app import skip.lib.* import skip.ui.* import skip.foundation.* import skip.model.* @androidx.annotation.Keep internal class MyNewView: skip.ui.View, skip.bridge.SwiftPeerBridged, skip.lib.SwiftProjecting { var Swift_peer: skip.bridge.SwiftObjectPointer = skip.bridge.SwiftObjectNil constructor(Swift_peer: skip.bridge.SwiftObjectPointer, marker: skip.bridge.SwiftPeerMarker?) { this.Swift_peer = Swift_peer } fun finalize() { Swift_release(Swift_peer); Swift_peer = skip.bridge.SwiftObjectNil } private external fun Swift_release(Swift_peer: skip.bridge.SwiftObjectPointer) override fun Swift_peer(): skip.bridge.SwiftObjectPointer = Swift_peer override fun equals(other: Any?): Boolean { if (other !is skip.bridge.SwiftPeerBridged) return false return Swift_peer == other.Swift_peer() } override fun hashCode(): Int = Swift_peer.hashCode() override fun body(): skip.ui.View { return skip.ui.ComposeBuilder { ctx -> Swift_composableBody(Swift_peer)?.Compose(ctx) ?: skip.ui.ComposeResult.ok } } private external fun Swift_composableBody(Swift_peer: skip.bridge.SwiftObjectPointer): skip.ui.View? constructor(/* params */) { Swift_peer = Swift_constructor_0(/* params */) } private external fun Swift_constructor_0(/* params */): skip.bridge.SwiftObjectPointer override fun Swift_projection(options: Int): () -> Any = Swift_projectionImpl(options) private external fun Swift_projectionImpl(options: Int): () -> Any @androidx.annotation.Keep companion object {} } ``` **Файл кладём в оба места:** ``` ~/Library/Developer/Xcode/DerivedData/Project-bpfpf.../Build/Intermediates.noindex/BuildToolPluginIntermediates/reflect-skip.output/ReflectApp/skipstone/ReflectApp/src/main/kotlin/reflect/app/ ~/Developer/reflect-skip/.build/plugins/outputs/reflect-skip/ReflectApp/destination/skipstone/ReflectApp/src/main/kotlin/reflect/app/ ``` ### 2. Kotlin субмодули — symlinks После очистки кеша/DerivedData Kotlin исходники субмодулей (SkipLib, SkipBridge, etc.) перестают быть видны Gradle. Нужно перелинковать: ```bash DERIVED="$HOME/Library/Developer/Xcode/DerivedData/Project-bpfpf..." SKIPSTONE="$HOME/Developer/reflect-skip/.build/plugins/outputs/reflect-skip/ReflectApp/destination/skipstone" for MODULE in SkipLib SkipBridge SkipFoundation SkipUI SkipAndroidBridge SkipModel SkipSwiftUI SkipUnit; do case "$MODULE" in SkipLib) PKG="skip-lib" ;; SkipBridge) PKG="skip-bridge" ;; SkipFoundation) PKG="skip-foundation" ;; SkipUI) PKG="skip-ui" ;; SkipAndroidBridge) PKG="skip-android-bridge" ;; SkipModel) PKG="skip-model" ;; SkipSwiftUI) PKG="skip-fuse-ui" ;; SkipUnit) PKG="skip-unit" ;; esac REAL_KT="$DERIVED/Build/Intermediates.noindex/BuildToolPluginIntermediates/${PKG}.output/${MODULE}/skipstone/${MODULE}/src/main/kotlin" mv "$SKIPSTONE/$MODULE/src/main/kotlin" "$SKIPSTONE/$MODULE/src/main/kotlin.bak" 2>/dev/null || true ln -sfn "$REAL_KT" "$SKIPSTONE/$MODULE/src/main/kotlin" done ``` ### 3. AndroidBridge.initBridge — обязательно до первого JNI вызова В `Android/app/src/main/kotlin/Main.kt` порядок в `onCreate`: ```kotlin ProcessInfo.launch(applicationContext) AndroidBridge.initBridge("ReflectApp") // ← ДО AppDelegate.shared.onInit() AppDelegate.shared.onInit() ``` --- ## Структура проекта (reflect-skip) ``` reflect-skip/ ├── Package.swift ← SPM, SkipFuseUI зависимость ├── Skip.env ← ANDROID_PACKAGE_NAME=reflect.app ├── Project.xcworkspace/ ← Workspace для xcodebuild ├── Darwin/ ← Xcode проект (iOS таргет + Skip build phases) │ ├── ReflectApp.xcodeproj/ │ └── Sources/Main.swift ├── Android/ ← Gradle Android проект │ ├── app/src/main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/Main.kt ← Entry point, AndroidBridge.initBridge │ └── settings.gradle.kts └── Sources/ReflectApp/ ├── ReflectAppMain.swift ← App root + PrivacyIntroView → Onboarding → Home ├── Models/ ├── Services/ │ ├── ClaudeService.swift ← Claude API (cloud + local proxy) │ ├── AnalystBot.swift ← Бот 2 (Sonnet, CBT вопросы + tools) │ ├── NarratorBot.swift ← Бот 1 (Haiku, 3rd-person reframe) │ ├── CrisisDetector.swift ← Hardcoded SI/SH │ ├── ProfileManager.swift ← On-device vault (profile.md, sessions/) │ ├── SessionManager.swift ← Оркестратор: intro/closing/pipeline │ └── ScheduleNegotiationService.swift └── Views/ ├── PrivacyIntroView.swift ← Экран 0: личный текст + GDPR ├── OnboardingView.swift ← Онбординг ├── HomeView.swift ├── SessionView.swift ← Фазы: loading/intro/dialogue/closing/oneLastThing ├── CardView.swift ├── SettingsView.swift ← AI backend + Сбросить данные └── OrbCharacter.swift ``` --- ## Архитектура флоу онбординга ``` Первый запуск → PrivacyIntroView (privacy_accepted) → OnboardingView (5 вопросов + AI инсайт → profile.md) → HomeView → SessionView (loading → intro → dialogue → closing → oneLastThing → done) ``` --- ## On-device хранилище ``` Documents/UserProfile/ ├── profile.md ← паттерны, наблюдения, счётчик сессий ├── insights.md ← кристаллизованные инсайты └── sessions/ ├── index.md ← таблица дат/тем для быстрого поиска └── 2026-05-19.md ``` **Сброс:** Settings → Данные → Сбросить все данные → удаляет `UserProfile/`, сбрасывает `privacy_accepted`, `onboarding_completed`, API key. --- ## Локальный AI прокси ```bash cd ~/Developer/reflect-skip PROXY_TOKEN= PROXY_BACKEND=claude-code PROXY_PORT=8765 python3 proxy_server.py ``` ### Публичный endpoint (ai.qentra.top) | Параметр | Значение | |---|---| | **URL** | `https://ai.qentra.top` | | CF Tunnel | Кракен, тот же connector, маршрут `ai.qentra.top → localhost:8765` | | **Proxy Token** | `bab835a69b4073c32a283472658d8815bfae59d8` | | Заголовок | `X-Proxy-Token: ` | | Backend | `claude-code` (OAuth через `~/.claude/.credentials.json`) | | Запуск | `PROXY_TOKEN= PROXY_BACKEND=claude-code PROXY_PORT=8765 python3 proxy_server.py` | **Защита:** `GET /health` публичный. `POST /v1/messages` без токена → 401. **DevSetupView (debug):** при первом запуске debug-сборки — экран ввода URL + токена. Сохраняет в UserDefaults, переключает backend на `localProxy`, флаг `dev_setup_done = true`. В релизной сборке (`#if DEBUG`) экран не показывается. В Settings: AI Backend → Локальный сервер → URL + Токен. --- ## Архитектура двойного бота ``` User input (1-е лицо) ↓ CrisisDetector.detect() — hardcoded, <5ms ↓ (если .critical → CrisisView) NarratorBot (Haiku) — переформулировка в 3-е лицо ↓ AnalystBot (Sonnet) — читает profile.md + инструменты (search_sessions, read_profile...) ↓ 1 вопрос из батча → ответ → следующий вопрос → нумерованная подбивка → AnalystBot ``` **Закрытие сессии:** 1. `endSession()` → AnalystBot генерирует наблюдение (3-е лицо) 2. NarratorBot генерирует тёплую closing phrase 3. One last thing: маленький input 4. Если нет расписания → предложить дату следующей сессии --- ## Связанные заметки - [[personal/projects/psychologist-app/overview|Overview]] - [[personal/projects/psychologist-app/skip-tools-poc|Skip.tools PoC]] - [[personal/projects/nolvu-reflect-skip|nolvu / reflect-skip]]