Merge branch 'origin/main' into main
This commit is contained in:
@@ -10,15 +10,16 @@ ssh kraken.qentra.top
|
|||||||
|
|
||||||
`~/.ssh/config` на Eagle:
|
`~/.ssh/config` на Eagle:
|
||||||
```
|
```
|
||||||
Host kraken-ssh
|
Host kraken
|
||||||
HostName 91.207.28.205
|
HostName kraken
|
||||||
Port 2223
|
|
||||||
User kraken
|
User kraken
|
||||||
|
Port 22
|
||||||
IdentityFile ~/.ssh/id_rsa
|
IdentityFile ~/.ssh/id_rsa
|
||||||
StrictHostKeyChecking no
|
StrictHostKeyChecking no
|
||||||
|
ProxyCommand bash -c 'if ifconfig | grep -q 10.99.0.2; then nc 10.99.1.2 22; else nc 192.168.1.14 22; fi'
|
||||||
```
|
```
|
||||||
|
|
||||||
`kraken-ssh` — алиас в `~/.ssh/config`, подключается через VPS `91.207.28.205:2223` → reverse tunnel → `localhost:22` Кракена.
|
`kraken` — алиас в `~/.ssh/config`, автоматически выбирает маршрут: по VPN (`10.99.1.2`) если туннель поднят, иначе по LAN (`192.168.1.14`).
|
||||||
|
|
||||||
`kraken.qentra.top` — CF Tunnel, маршрутизирует HTTP на `localhost:8642` (Hermes API).
|
`kraken.qentra.top` — CF Tunnel, маршрутизирует HTTP на `localhost:8642` (Hermes API).
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ ip: 192.168.1.14
|
|||||||
|
|
||||||
SSH подключение (пока через WiFi/роутер):
|
SSH подключение (пока через WiFi/роутер):
|
||||||
```bash
|
```bash
|
||||||
ssh -i ~/.ssh/id_rsa kraken@kraken.local
|
ssh -i ~/.ssh/id_rsa kraken@kraken
|
||||||
```
|
```
|
||||||
|
|
||||||
Обновление системы:
|
Обновление системы:
|
||||||
@@ -112,7 +112,7 @@ Pi перезагрузится автоматически по окончани
|
|||||||
|
|
||||||
## Шаг 4 — Первый вход в веб-интерфейс
|
## Шаг 4 — Первый вход в веб-интерфейс
|
||||||
|
|
||||||
- URL: `http://192.168.100.2` (или `http://kraken.local`)
|
- URL: `http://192.168.100.2` (или `http://kraken` через LAN)
|
||||||
- Логин: `admin`
|
- Логин: `admin`
|
||||||
- Пароль: `openmediavault`
|
- Пароль: `openmediavault`
|
||||||
- **Сразу сменить пароль!** (System → General Settings → Web Administrator Password)
|
- **Сразу сменить пароль!** (System → General Settings → Web Administrator Password)
|
||||||
@@ -277,7 +277,7 @@ Compose: `/home/kraken/timemachine/docker-compose.yml`
|
|||||||
- Пароль: `timemachine` ⚠️ сменить позже
|
- Пароль: `timemachine` ⚠️ сменить позже
|
||||||
|
|
||||||
**Подключение с Mac:**
|
**Подключение с Mac:**
|
||||||
1. Finder → `⌘K` → `smb://192.168.1.14`
|
1. Finder → `⌘K` → `smb://kraken`
|
||||||
2. Логин: `timemachine` / `timemachine`
|
2. Логин: `timemachine` / `timemachine`
|
||||||
3. System Preferences → Time Machine → Add Disk → выбрать `kraken`
|
3. System Preferences → Time Machine → Add Disk → выбрать `kraken`
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ force user = kraken
|
|||||||
|
|
||||||
`smbd` включён в автозапуск: `sudo systemctl enable smbd`
|
`smbd` включён в автозапуск: `sudo systemctl enable smbd`
|
||||||
|
|
||||||
**Подключение с Mac:** Finder → `⌘K` → `smb://192.168.1.14` → шара `media` → гостевой доступ.
|
**Подключение с Mac:** Finder → `⌘K` → `smb://kraken` → шара `media` → гостевой доступ.
|
||||||
|
|
||||||
> ⚠️ OMV не управляет smb.conf (плагин не установлен) — правки не слетают при `omv-salt deploy run`.
|
> ⚠️ OMV не управляет smb.conf (плагин не установлен) — правки не слетают при `omv-salt deploy run`.
|
||||||
> ⚠️ nmbd не нужен (`disable netbios = yes` в глобальных настройках OMV).
|
> ⚠️ nmbd не нужен (`disable netbios = yes` в глобальных настройках OMV).
|
||||||
@@ -315,7 +315,7 @@ force user = kraken
|
|||||||
|
|
||||||
### Фильмы с Mac на NAS
|
### Фильмы с Mac на NAS
|
||||||
```bash
|
```bash
|
||||||
rsync -avh --progress /path/to/movies/ kraken@kraken.local:/srv/.../media/movies/
|
rsync -avh --progress /path/to/movies/ kraken@kraken:/srv/.../media/movies/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Time Machine
|
### Time Machine
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ services:
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh kraken-ssh "cd /srv/.../docker/jackett && docker compose up -d"
|
ssh kraken "cd /srv/.../docker/jackett && docker compose up -d"
|
||||||
```
|
```
|
||||||
|
|
||||||
Web UI: `http://192.168.1.14:9117`
|
Web UI: `http://192.168.1.14:9117`
|
||||||
@@ -257,7 +257,7 @@ ssh -i ~/.ssh/id_rsa bazzite@192.168.1.86 \
|
|||||||
|
|
||||||
### Шаг 4: Jackett на Кракене
|
### Шаг 4: Jackett на Кракене
|
||||||
```bash
|
```bash
|
||||||
ssh kraken-ssh "mkdir -p /srv/.../docker/jackett && cd /srv/.../docker/jackett"
|
ssh kraken "mkdir -p /srv/.../docker/jackett && cd /srv/.../docker/jackett"
|
||||||
# Создать docker-compose.yml (выше), добавить RuTracker учётку
|
# Создать docker-compose.yml (выше), добавить RuTracker учётку
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ Eagle (10.99.0.2) ←→ wg0 VPS (10.99.0.1) ←→ wg1 VPS (10.99.1.1) ←→ K
|
|||||||
|
|
||||||
## Auto-connect на Eagle
|
## Auto-connect на Eagle
|
||||||
|
|
||||||
`/usr/local/bin/wg-auto.sh` — следит за SSID через `WatchPaths` на `/Library/Preferences/SystemConfiguration/`:
|
`/usr/local/bin/wg-auto.sh` — polling каждые 30 сек (launchd `StartInterval`, **НЕ** WatchPaths):
|
||||||
- SSID == `ASUS5` → `wg-quick down` (дома, Kraken в локалке, туннель не нужен)
|
- Дома (роутер MAC `f0:79:59:77:9b:70`) → `wg-quick down`
|
||||||
- SSID != `ASUS5` → `wg-quick up` (вне дома, TM через VPN)
|
- Вне дома → `wg-quick up`
|
||||||
|
|
||||||
|
WatchPaths вызывал race condition: `wg-quick down` менял DNS через networksetup → писал в `/Library/Preferences/SystemConfiguration/` → launchd тригерил скрипт снова → туннель рвался, IP слетал.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -58,6 +60,14 @@ Destination обновлён:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## DNS
|
||||||
|
|
||||||
|
wg0.conf **без** `DNS =`. PostUp/PostDown управляют scoped resolver:
|
||||||
|
- `PostUp = mkdir -p /etc/resolver && echo "nameserver 10.99.0.1" > /etc/resolver/kraken && killall -HUP mDNSResponder`
|
||||||
|
- `PostDown = rm -f /etc/resolver/kraken && killall -HUP mDNSResponder`
|
||||||
|
|
||||||
|
Только домен `kraken` резолвится через VPS dnsmasq (10.99.0.1). Глобальный DNS не трогается — Wi-Fi IP не слетает.
|
||||||
|
|
||||||
## Ключи
|
## Ключи
|
||||||
|
|
||||||
| Узел | Публичный ключ |
|
| Узел | Публичный ключ |
|
||||||
@@ -80,7 +90,7 @@ ping kraken
|
|||||||
ssh root@91.207.28.205 "wg show all"
|
ssh root@91.207.28.205 "wg show all"
|
||||||
|
|
||||||
# Kraken
|
# Kraken
|
||||||
ssh kraken-ssh "sudo wg show"
|
ssh kraken "sudo wg show"
|
||||||
|
|
||||||
# Логи auto-connect Eagle
|
# Логи auto-connect Eagle
|
||||||
cat /var/log/wg-auto.log
|
cat /var/log/wg-auto.log
|
||||||
|
|||||||
@@ -278,8 +278,8 @@ docker run --rm ... media-pipeline:kraken \
|
|||||||
| Transmission | http://192.168.1.14:9091 |
|
| Transmission | http://192.168.1.14:9091 |
|
||||||
| Исходники | `~/Developer/media_files_db/`, ветка `kraken-agent` |
|
| Исходники | `~/Developer/media_files_db/`, ветка `kraken-agent` |
|
||||||
| Docker образ | `media-pipeline:kraken` (arm64, Colima 24GB) |
|
| Docker образ | `media-pipeline:kraken` (arm64, Colima 24GB) |
|
||||||
| kraken-ssh | `ssh kraken-ssh` → 10.99.1.2:22 через WireGuard (обновлено 2026-05-14) |
|
| kraken | `ssh kraken` → LAN (`192.168.1.14`) или VPN (`10.99.1.2`) через DNS-резолв |
|
||||||
| docker deploy | `docker save img -o /tmp/img.tar && scp /tmp/img.tar kraken-ssh:/tmp/ && ssh kraken-ssh "docker load -i /tmp/img.tar && rm /tmp/img.tar"` — `docker save \| ssh` зависает через WireGuard tunnel |
|
| docker deploy | `docker save img -o /tmp/img.tar && scp /tmp/img.tar kraken:/tmp/ && ssh kraken "docker load -i /tmp/img.tar && rm /tmp/img.tar"` — `docker save \| ssh` зависает через WireGuard tunnel |
|
||||||
| Transmission RPC | Токен: `curl -D /tmp/hdrs ... > /dev/null`, затем `grep X-Transmission-Session-Id /tmp/hdrs \| awk '{print $2}' \| tr -d '\r\n'`. Payload с base64 торрента — писать в файл (`-d @file`), не аргументом (иначе "Argument list too long") |
|
| Transmission RPC | Токен: `curl -D /tmp/hdrs ... > /dev/null`, затем `grep X-Transmission-Session-Id /tmp/hdrs \| awk '{print $2}' \| tr -d '\r\n'`. Payload с base64 торрента — писать в файл (`-d @file`), не аргументом (иначе "Argument list too long") |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,124 +1,282 @@
|
|||||||
---
|
---
|
||||||
created: '2026-05-16'
|
created: '2026-05-16'
|
||||||
updated: '2026-05-16'
|
updated: '2026-05-19'
|
||||||
tags:
|
tags:
|
||||||
- project
|
- project
|
||||||
- dev
|
- dev
|
||||||
- ios
|
- ios
|
||||||
- swift
|
- swift
|
||||||
|
- android
|
||||||
|
- skip
|
||||||
---
|
---
|
||||||
# Reflect App — Dev Setup & Architecture
|
# Reflect App — Dev Setup & Architecture
|
||||||
|
|
||||||
> Путь: `~/Developer/reflect-app/`
|
> **iOS (reflect-app):** `~/Desktop/reflect-app/` — нативный SwiftUI, не трогать
|
||||||
> Статус: BUILD SUCCEEDED, 30/30 тестов
|
> **Cross-platform (reflect-skip):** `~/Developer/reflect-skip/` — Skip Fuse, iOS + Android
|
||||||
|
> Gitea: `git.mallexxx.duckdns.org/git_admin/reflect-app`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Структура проекта
|
## Статус (2026-05-19)
|
||||||
|
|
||||||
```
|
### Что реализовано
|
||||||
reflect-app/
|
|
||||||
├── project.yml ← XcodeGen spec (генерирует .xcodeproj)
|
| Компонент | Статус |
|
||||||
├── proxy_server.py ← Local AI proxy server (Mac → iPhone)
|
|-----------|--------|
|
||||||
├── ReflectApp/
|
| iOS build (reflect-app) | ✅ Build succeeded, 30/30 тестов |
|
||||||
│ ├── Models/
|
| Android build (reflect-skip) | ✅ APK собирается, запускается на эмуляторе |
|
||||||
│ │ ├── CrisisKeywords.swift
|
| Экран 0: Privacy intro | ✅ Личный текст, GDPR opt-out, дисклеймер |
|
||||||
│ │ ├── Message.swift
|
| Онбординг | ✅ 3 info-слайда + 5 вопросов + AI инсайт |
|
||||||
│ │ ├── Session.swift
|
| Сессия: dynamic intro | ✅ AI-генерированное вступление + max 2 крючка из истории |
|
||||||
│ │ └── UserProfile.swift
|
| Сессия: one-by-one вопросы | ✅ Вопросы по одному, батч-подбивка агенту |
|
||||||
│ ├── Services/
|
| Сессия: closing phrase | ✅ Тёплая фраза от Narrator'а |
|
||||||
│ │ ├── ClaudeService.swift ← AI бэкенд (cloud + local proxy)
|
| Сессия: one last thing | ✅ Маленький input + неакцентная кнопка Завершить |
|
||||||
│ │ ├── AnalystBot.swift ← Бот 2 (Sonnet, CBT вопросы)
|
| Dual-bot pipeline | ✅ Narrator (Haiku) → Analyst (Sonnet) |
|
||||||
│ │ ├── NarratorBot.swift ← Бот 1 (Haiku, 3rd-person reframe)
|
| Crisis detection | ✅ Hardcoded RU+EN, CrisisView |
|
||||||
│ │ ├── CrisisDetector.swift ← Hardcoded SI/SH детекция (RU+EN)
|
| Profile vault | ✅ profile.md, sessions/, insights.md |
|
||||||
│ │ ├── ProfileManager.swift ← Obsidian-like on-device vault
|
| App Lock (Face ID) | ✅ BiometricAuthentication |
|
||||||
│ │ └── SessionScheduler.swift ← Напоминания (UNUserNotificationCenter)
|
| Settings: API key, proxy | ✅ Keychain + LocalProxy |
|
||||||
│ ├── Views/
|
| Settings: Сбросить данные | ✅ Алерт + полный сброс + онбординг заново |
|
||||||
│ │ ├── OnboardingView.swift ← 3 слайда + 5 вопросов + инсайт
|
| Session scheduler | ✅ UNCalendarNotification |
|
||||||
│ │ ├── HomeView.swift
|
|
||||||
│ │ ├── SessionView.swift
|
### Что не реализовано (план)
|
||||||
│ │ ├── CrisisView.swift
|
|
||||||
│ │ ├── ProfileView.swift ← Obsidian-like markdown viewer
|
| Компонент | Приоритет |
|
||||||
│ │ ├── SettingsView.swift ← Выбор бэкенда + API key + server URL
|
|-----------|-----------|
|
||||||
│ │ ├── CardView.swift
|
| ProfileView: история сессий | 🟠 Средний |
|
||||||
│ │ └── OrbCharacter.swift ← SwiftUI анимированный орб
|
| Session recovery (незавершённые) | 🟠 Средний |
|
||||||
│ └── ViewModels/
|
| Полноценный sessions/YYYY-MM-DD.md | 🟡 Низкий |
|
||||||
│ ├── HomeViewModel.swift
|
| RevenueCat paywall | 🔴 Нужен до публикации |
|
||||||
│ └── SessionViewModel.swift
|
| Голосовой ввод (iOS) | 🟡 Phase 2 |
|
||||||
└── ReflectAppTests/
|
| Push-уведомления opt-in | 🟡 Phase 2 |
|
||||||
└── ReflectAppTests.swift ← 30 тестов
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Генерация проекта
|
## 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
|
```bash
|
||||||
# Установить XcodeGen (один раз)
|
cd ~/Developer/reflect-skip
|
||||||
brew install xcodegen
|
|
||||||
|
|
||||||
# Регенерировать .xcodeproj после изменения project.yml
|
export ANDROID_HOME=~/Library/Android/sdk
|
||||||
cd ~/Developer/reflect-app
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@21
|
||||||
xcodegen generate
|
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
|
```bash
|
||||||
# Узнать ID симулятора
|
DERIVED="$HOME/Library/Developer/Xcode/DerivedData/Project-bpfpf..."
|
||||||
xcrun simctl list devices | grep "iPhone 16 Pro"
|
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
|
||||||
xcodebuild -project ReflectApp.xcodeproj -scheme ReflectApp \
|
case "$MODULE" in
|
||||||
-destination "id=9FF2D6A8-E0DF-4157-9FA5-463E45BB5FAD" build
|
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
|
||||||
|
```
|
||||||
|
|
||||||
# Тесты (30 тестов, ~0.1 сек)
|
### 3. AndroidBridge.initBridge — обязательно до первого JNI вызова
|
||||||
xcodebuild test -project ReflectApp.xcodeproj -scheme ReflectApp \
|
|
||||||
-destination "id=9FF2D6A8-E0DF-4157-9FA5-463E45BB5FAD"
|
В `Android/app/src/main/kotlin/Main.kt` порядок в `onCreate`:
|
||||||
|
```kotlin
|
||||||
|
ProcessInfo.launch(applicationContext)
|
||||||
|
AndroidBridge.initBridge("ReflectApp") // ← ДО AppDelegate.shared.onInit()
|
||||||
|
AppDelegate.shared.onInit()
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Локальный AI сервер (режим без ключа)
|
## Структура проекта (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
|
```bash
|
||||||
# claude-code — OAuth через Hermes (нет API ключа, читает CLAUDE_CODE_OAUTH_TOKEN из ~/.hermes/.env)
|
cd ~/Developer/reflect-app # iOS проект
|
||||||
cd ~/Developer/reflect-app
|
python3 proxy_server.py --backend claude-code # OAuth через Hermes
|
||||||
python3 proxy_server.py --backend claude-code
|
# или
|
||||||
|
|
||||||
# Ollama — локальная модель
|
|
||||||
python3 proxy_server.py --backend ollama --ollama-model qwen2.5:7b
|
python3 proxy_server.py --backend ollama --ollama-model qwen2.5:7b
|
||||||
|
|
||||||
# Claude API key
|
|
||||||
export ANTHROPIC_API_KEY=sk-ant-...
|
|
||||||
python3 proxy_server.py --backend claude
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**claude-code бэкенд** — приоритет источников токена:
|
В Settings приложения: AI Backend → Локальный сервер → URL сервера.
|
||||||
1. `CLAUDE_CODE_OAUTH_TOKEN` из `~/.hermes/.env` (Hermes-managed, наиболее надёжный)
|
|
||||||
2. macOS Keychain (`Claude Code-credentials`)
|
|
||||||
3. `~/.claude/.credentials.json` (с авто-обновлением если есть refreshToken)
|
|
||||||
|
|
||||||
### Настройка на iPhone
|
|
||||||
|
|
||||||
1. Открыть приложение → шестерёнка (Settings)
|
|
||||||
2. AI Backend → "Локальный сервер (Mac)"
|
|
||||||
3. URL сервера: скопировать из вывода `proxy_server.py`
|
|
||||||
4. Нажать "Проверить" — должно показать "Доступен · qwen2.5:7b"
|
|
||||||
|
|
||||||
### Архитектура proxy
|
|
||||||
|
|
||||||
```
|
|
||||||
iPhone (ClaudeService)
|
|
||||||
→ POST /v1/messages → proxy_server.py (8765)
|
|
||||||
→ Anthropic API format → Ollama (11434)
|
|
||||||
← response ← Ollama response (конвертируется в Anthropic формат)
|
|
||||||
← response ← proxy_server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -131,106 +289,13 @@ CrisisDetector.detect() — hardcoded, <5ms
|
|||||||
↓ (если .critical → CrisisView)
|
↓ (если .critical → CrisisView)
|
||||||
NarratorBot (Haiku) — переформулировка в 3-е лицо
|
NarratorBot (Haiku) — переформулировка в 3-е лицо
|
||||||
↓
|
↓
|
||||||
AnalystBot (Sonnet) — читает profile.md + вопросы
|
AnalystBot (Sonnet) — читает profile.md + инструменты (search_sessions, read_profile...)
|
||||||
↓
|
↓
|
||||||
CardView с вопросами
|
1 вопрос из батча → ответ → следующий вопрос → нумерованная подбивка → AnalystBot
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
**Закрытие сессии:**
|
||||||
|
1. `endSession()` → AnalystBot генерирует наблюдение (3-е лицо)
|
||||||
## On-device хранилище (Obsidian-like)
|
2. NarratorBot генерирует тёплую closing phrase
|
||||||
|
3. One last thing: маленький input
|
||||||
```
|
4. Если нет расписания → предложить дату следующей сессии
|
||||||
Documents/UserProfile/
|
|
||||||
├── profile.md ← паттерны, наблюдения, прогресс
|
|
||||||
├── insights.md ← кристаллизованные инсайты
|
|
||||||
└── sessions/
|
|
||||||
├── 2026-05-16.md
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## App Lock (Face ID / Touch ID / Passcode)
|
|
||||||
|
|
||||||
**Файлы:**
|
|
||||||
- `Services/AppLockService.swift` — `@MainActor ObservableObject`, `LAContext`, состояние
|
|
||||||
- `Views/LockView.swift` — экран блокировки, автоматически запускает биометрию при появлении
|
|
||||||
- `ReflectApp.swift` — `checkOnLaunch()` при старте, `scenePhase.background → lock()`
|
|
||||||
- `SettingsView.swift` — секция "Безопасность" с Toggle
|
|
||||||
|
|
||||||
**Поведение:**
|
|
||||||
- Включение/выключение требует подтверждения биометрией
|
|
||||||
- При уходе в фон (`scenePhase == .background`) — блокируется
|
|
||||||
- При возврате — LockView показывается поверх всего, сразу запрашивает биометрию
|
|
||||||
- Симулятор: биометрия эмулируется через Features → Face ID → Enrolled
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Голосовой ввод
|
|
||||||
|
|
||||||
**`Services/VoiceInputService.swift`** — `@MainActor ObservableObject`:
|
|
||||||
- `AVAudioEngine` + `SFSpeechRecognizer` (locale: ru-RU с fallback на системный)
|
|
||||||
- Push-to-talk: `startRecording()` / `stopRecording()`
|
|
||||||
- `onTranscript: ((String) -> Void)?` — коллбэк с финальным текстом
|
|
||||||
- `audioLevel: Float` — уровень звука в реальном времени (→ орб анимация)
|
|
||||||
- `requestPermissions()` — запрашивает mic + speech recognition
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Адаптивное расписание (ScheduleNegotiationService)
|
|
||||||
|
|
||||||
**`Services/ScheduleNegotiationService.swift`**:
|
|
||||||
- `nextSessionDate` — следующая запланированная сессия (UserDefaults)
|
|
||||||
- `buildSuggestion()` → предлагает время на основе паттерна прошлых встреч
|
|
||||||
- `acceptSchedule(date)` → сохраняет и ставит разовое `UNCalendarNotificationTrigger`
|
|
||||||
- `postpone(by: PostponeOption)` → переносит на +1ч / +3ч / завтра / skip
|
|
||||||
- Уведомление содержит action-кнопки прямо из шторы (без открытия приложения)
|
|
||||||
|
|
||||||
**Флоу в конце сессии:**
|
|
||||||
1. Агент завершает сессию → `SessionManager.endSession()` → `clearSchedule()`
|
|
||||||
2. `SessionViewModel.endSession()` → показывает `CardData` с предложением времени
|
|
||||||
3. Пользователь: "Да, подходит" / "Другое время" / "Без расписания"
|
|
||||||
4. При выборе времени → `ScheduleNegotiationService.acceptSchedule(date)` → уведомление
|
|
||||||
|
|
||||||
**HomeView:**
|
|
||||||
- Если есть расписание → показывает badge "Следующая: пятница 20:00" + кнопка "Перенести"
|
|
||||||
- Если нет расписания → "Назначить следующую встречу" (кнопка открывает `PostponeSheetView`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Тулинг агента (AnalystBot)
|
|
||||||
|
|
||||||
Аналитик использует tool-use loop (`ClaudeService.completeWithTools`).
|
|
||||||
|
|
||||||
**4 инструмента:**
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `get_index` | Индекс сессий — таблица дат/тем/тегов, < 1K токенов |
|
|
||||||
| `search_sessions` | Полнотекстовый поиск по архиву |
|
|
||||||
| `read_session` | Прочитать полную сессию по дате |
|
|
||||||
| `read_profile` | Прочитать profile.md |
|
|
||||||
|
|
||||||
Агент сам решает когда обращаться к хранилищу — когда видит паттерн или нужен контекст прошлых встреч.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Session Index
|
|
||||||
|
|
||||||
**`ProfileManager.updateSessionIndex()`** обновляет `sessions/index.md`:
|
|
||||||
```
|
|
||||||
| Дата | Темы | Теги | Содержание |
|
|
||||||
|------------|------------------------|-------------------|-------------------|
|
|
||||||
| 2026-05-16 | работа, усталость | #сессия #инсайт | Говорит что устал |
|
|
||||||
```
|
|
||||||
Заполняется автоматически при `endSession()`. Агент читает через `get_index` — без засорения контекста полными файлами.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Тест-результаты (2026-05-16)
|
|
||||||
|
|
||||||
- ClaudeServiceTests: 4/4
|
|
||||||
- CrisisDetectorTests: 17/17 (включая RU+EN, нормализация)
|
|
||||||
- ProfileManagerTests: 6/6 (включая onboarding init)
|
|
||||||
- SessionSchedulerTests: 3/3
|
|
||||||
- **ИТОГО: 30/30, 0 failures**
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ updated: '2026-05-19'
|
|||||||
|
|
||||||
| Ситуация | Метод |
|
| Ситуация | Метод |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| Eagle на VPN (вне LAN) | через Кракен: `ssh kraken-ssh` + python3 magic packet |
|
| Eagle на VPN (вне LAN) | через Кракен: `ssh kraken` + python3 magic packet |
|
||||||
| Eagle дома (192.168.x) | напрямую |
|
| Eagle дома (192.168.x) | напрямую |
|
||||||
|
|
||||||
Поллить ответ HTPC с Кракена.
|
Поллить ответ HTPC с Кракена.
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ confidence: high
|
|||||||
|
|
||||||
| Host | IP | Notes |
|
| Host | IP | Notes |
|
||||||
|------|----|-------|
|
|------|----|-------|
|
||||||
| Kraken | 192.168.1.14 | LAN (eth0, DHCP static lease). `ssh kraken-ssh` |
|
| Kraken | 192.168.1.14 | LAN (eth0, DHCP static lease). `ssh kraken` |
|
||||||
| Kraken WiFi | 192.168.1.15 | wlan0 (ASUS5), активен с 2026-05-19 |
|
| Kraken WiFi | 192.168.1.15 | wlan0 (ASUS5), активен с 2026-05-19 |
|
||||||
| HTPC | 192.168.1.86 | Bazzite, LAN |
|
| HTPC | 192.168.1.86 | Bazzite, LAN |
|
||||||
|
|
||||||
## Важные правила
|
## Важные правила
|
||||||
|
|
||||||
- HTPC **не достигает** Kraken через WireGuard — только LAN (192.168.1.14)
|
- HTPC **не достигает** Kraken через WireGuard — только LAN (`192.168.1.14`)
|
||||||
- Kraken **достигает** HTPC напрямую по LAN (192.168.1.86)
|
- Kraken **достигает** HTPC напрямую по LAN (192.168.1.86)
|
||||||
- Для rsync: **всегда тянуть на Kraken** (Kraken→HTPC), никогда push с HTPC
|
- Для rsync: **всегда тянуть на Kraken** (Kraken→HTPC), никогда push с HTPC
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ confidence: high
|
|||||||
docker image prune -f
|
docker image prune -f
|
||||||
|
|
||||||
# Kraken (после docker load)
|
# Kraken (после docker load)
|
||||||
ssh kraken-ssh "docker image prune -f"
|
ssh kraken "docker image prune -f"
|
||||||
```
|
```
|
||||||
|
|
||||||
Полная последовательность деплоя:
|
Полная последовательность деплоя:
|
||||||
@@ -62,8 +62,8 @@ docker build -f Dockerfile.kraken -t media-pipeline:kraken .
|
|||||||
|
|
||||||
# 2. Перенести
|
# 2. Перенести
|
||||||
docker save media-pipeline:kraken -o /tmp/mp.tar
|
docker save media-pipeline:kraken -o /tmp/mp.tar
|
||||||
scp /tmp/mp.tar kraken-ssh:/tmp/
|
scp /tmp/mp.tar kraken:/tmp/
|
||||||
ssh kraken-ssh "docker load -i /tmp/mp.tar && rm /tmp/mp.tar && docker image prune -f"
|
ssh kraken "docker load -i /tmp/mp.tar && rm /tmp/mp.tar && docker image prune -f"
|
||||||
rm /tmp/mp.tar
|
rm /tmp/mp.tar
|
||||||
|
|
||||||
# 3. Почистить локально
|
# 3. Почистить локально
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## Inbox Triage — 2026-05-19 (since 14:00 local)
|
||||||
|
|
||||||
|
### New / Changed — Action Required
|
||||||
|
|
||||||
|
**[New Assignment]** — [PR: iOS: AI onboarding for custom product pages - Transition to Contextual + Resume Linear Flow [2-3 days]](https://app.asana.com/0/0/1214928613949181) — assigned to Alex, due 2026-05-20
|
||||||
|
- Section: inbox (no project)
|
||||||
|
- PR: https://github.com/duckduckgo/apple-browsers/pull/4917
|
||||||
|
- Action: Review/pick up — due tomorrow, needs attention today.
|
||||||
|
|
||||||
|
### New / Changed — Monitoring
|
||||||
|
|
||||||
|
**[Comment]** — [DRI <> Project Advisor async for Demo tracker blocking after chat in onboarding](https://app.asana.com/0/0/1213551072611261) — Alessandro at 14:16
|
||||||
|
- "Approved Privacy Config PR, thank you!"
|
||||||
|
- No action needed — approval acknowledgement. PR is merged/approved.
|
||||||
Reference in New Issue
Block a user