docs: add CLAUDE.md as architecture map (renamed from NEXT.md)
Adds design system v2 section covering P3 colors, squircle shapes, GlassSurface, multi-layer shadows, 4-theme system, sensor tilt state, motion tokens, and new test inventory.
This commit is contained in:
parent
6465b47bbb
commit
f1c6a88c4c
1 changed files with 254 additions and 0 deletions
254
CLAUDE.md
Normal file
254
CLAUDE.md
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
# Kaizen App
|
||||||
|
|
||||||
|
Architecture map for the native Android client. Source of truth for any agent or human picking up this codebase.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the **native Android app** (`kaizen-app`) a valid daily-driver that talks to the **Kaizen backend** (`kaizen`). One binary, deployment-agnostic: one uniform `Authorization: Bearer <token>` contract, same against the official server or any self-host. Design constraint (hard): backend complexity stays invisible to the user.
|
||||||
|
|
||||||
|
## Repos (absolute paths)
|
||||||
|
|
||||||
|
- Backend (Next.js 16, Drizzle, Postgres): `/Users/brunodeanoz/Kaizen/kaizen` — git remote `origin` = `git@git.kryptomrx.de:krypto/kaizen.git`, branch `main`. Read its `CLAUDE.md` first; it is the architecture map.
|
||||||
|
- Android app (Kotlin, Jetpack Compose, package `dev.kaizen.app`): `/Users/brunodeanoz/Kaizen/kaizen-app` — built/run from Windows Android Studio on a real S25 Ultra; everything else (backend, Postgres, Docker) runs on the dev Mac + dedicated servers.
|
||||||
|
- **Note:** `/Users/brunodeanoz/Kaizen` itself is NOT a git repo. Only `kaizen` and `kaizen-app` are separate git repos.
|
||||||
|
|
||||||
|
## Platform strategy
|
||||||
|
|
||||||
|
- Android: Kotlin + Jetpack Compose (native, no WebView)
|
||||||
|
- iOS (planned): Swift + SwiftUI + SwiftData (native, separate codebase)
|
||||||
|
- No KMP — each platform uses its native stack for best feel
|
||||||
|
|
||||||
|
## The API contract the app uses (source of truth — verify, don't guess)
|
||||||
|
|
||||||
|
- **Auth:** every request carries `Authorization: Bearer kzn_...`. Backend resolves via `requireActiveUserOrToken` (`lib/auth/guard.ts`).
|
||||||
|
- **Token acquisition:** in-app login (`POST {baseUrl}/api/v1/auth/token` with `{ email, password, name }`). Consumer "Sign in" pattern — no copy-paste.
|
||||||
|
- **Chat:** `POST {baseUrl}/api/v1/chat` — `{ model, messages: [{ role, content }], reasoningEffort?, preferThroughput? }`. Response is `text/plain` chunked stream (NOT SSE). Parsed by `StreamConsumer.Incremental` in `net/StreamConsumer.kt`.
|
||||||
|
- **Sentinels** (`lib/chat/constants.ts`): `\x00` USAGE, `\x01` REASONING, `\x02` SOURCES, `\x03` TOOL, `\x04` QUERY. App currently strips all sentinels and renders only visible content.
|
||||||
|
- **Models:** `GET {baseUrl}/api/v1/models` → `{ models: [{ id, name?, provider? }] }`.
|
||||||
|
- **Conversations:** `GET /api/v1/conversations` → `{ conversations, hasMore }`. `POST /api/v1/conversations` → create. `GET /api/v1/conversations/[id]` → `{ messages }`. `POST /api/v1/conversations/[id]/messages` → persist. `POST /api/v1/conversations/[id]/title` → auto-title.
|
||||||
|
- **Identity:** `GET /api/v1/me` → `{ id, email, name, role, defaultModel }`.
|
||||||
|
- **Capability handshake:** `GET /api/v1/meta` → `{ app, apiVersion, features }` (unauth).
|
||||||
|
- **Upload:** `POST /api/v1/upload` — multipart file → `{ url, kind, name, mimeType, size, fileId }`.
|
||||||
|
|
||||||
|
## App architecture
|
||||||
|
|
||||||
|
### Package structure (`app/src/main/java/dev/kaizen/app/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
auth/ LoginScreen
|
||||||
|
chat/ ChatScreen, ChatViewModel, Sidebar, ChatComponents, ChatModels,
|
||||||
|
ModelSheet, Markdown, Background
|
||||||
|
db/ Room offline cache: KaizenDatabase, Entities, DAOs,
|
||||||
|
Repositories, Converters, Mappers
|
||||||
|
haptics/ Phase-aware haptics
|
||||||
|
net/ KaizenApi (HTTP), SecureStore (encrypted prefs),
|
||||||
|
SessionViewModel, ServerConfig, StreamConsumer
|
||||||
|
settings/ SettingsScreen, SettingsViewModel, SettingsComponents
|
||||||
|
ui/
|
||||||
|
theme/ Color (P3), Theme (multi-theme), Type, Oklab, Dither
|
||||||
|
theme/themes/ AccentScheme, AmberTheme, BlauTheme, MonoTheme, AuroraTheme, ThemeRegistry
|
||||||
|
shape/ SquircleShape (superellipse), KaizenShapes (token system)
|
||||||
|
effect/ GlassSurface (RenderEffect blur), Shadow (multi-layer stack)
|
||||||
|
motion/ Motion (easing curves, spring specs, duration tokens)
|
||||||
|
sensor/ TiltState (gyro/accel parallax, lifecycle-aware)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key components
|
||||||
|
|
||||||
|
- **KaizenApi** (`net/KaizenApi.kt`) — singleton `object`, all HTTP calls via OkHttp. Streaming chat returns `Flow<String>` with incremental sentinel-stripping.
|
||||||
|
- **StreamConsumer** (`net/StreamConsumer.kt`) — O(chunk-size) incremental parser, each character visited exactly once across the entire stream.
|
||||||
|
- **SessionViewModel** (`net/SessionViewModel.kt`) — Compose snapshot state for auth session. Holds `ServerConfig` (baseUrl, token, model, email, speed). Persisted in `EncryptedSharedPreferences` via `SecureStore`.
|
||||||
|
- **ChatViewModel** (`chat/ChatViewModel.kt`) — owns Room database instance + repositories. Created in `MainActivity`, passed to `ChatScreen`.
|
||||||
|
- **ChatScreen** (`chat/ChatScreen.kt`) — main composable, orchestrates sidebar, messages, streaming, file uploads.
|
||||||
|
|
||||||
|
### Design System v2 (`ui/`) — added 2026-06-21
|
||||||
|
|
||||||
|
Full spec: `DESIGN.md`. Branch: `feature/design-system-v2`.
|
||||||
|
|
||||||
|
**Six pillars** (load-bearing — if any regress, the design regresses):
|
||||||
|
|
||||||
|
1. **P3 color pipeline** — all tokens in DisplayP3 color space (`Color.kt`). `window.colorMode = COLOR_MODE_HDR` (API 34+) / `COLOR_MODE_WIDE_COLOR_GAMUT` (API 26+) in `MainActivity`. No `Color(0xFF…)` for brand colors outside `Color.kt`.
|
||||||
|
2. **Squircle shapes** — iOS-style superellipse with G2 continuity (`ui/shape/Squircle.kt`). Token system `KaizenShapes.{xs,sm,md,lg,xl,pill,circle}`. Replaces all `RoundedCornerShape`.
|
||||||
|
3. **GlassSurface** — real `Modifier.blur()` (RenderEffect on API 31+), tint + inner highlight + border. API 26–30 fallback: higher tint alpha. Opacity tiers per component in `GlassTiers`.
|
||||||
|
4. **Multi-layer shadows** — `ShadowStack` with 2–4 overlapping layers (`KaizenShadows.level0..4`). Dark mode multiplies alphas ×1.8. Replaces all `Modifier.shadow()`.
|
||||||
|
5. **4-theme system** — Amber/Blau/Mono/Aurora. Each theme = `KaizenAccentScheme` (primary + 4 blob colors). Neutrals are theme-independent. `LocalKaizenAccent` CompositionLocal. Server sync planned via `GET /api/v1/me` → `uiTheme`.
|
||||||
|
6. **Sensor-reactive motion** — `rememberTiltState()` provides smoothed tilt `Offset(-1..1)`. Uses `TYPE_GAME_ROTATION_VECTOR` (fused, no drift). Auto-disables on battery saver, reduced motion, background, extreme tilt.
|
||||||
|
|
||||||
|
**Motion tokens** (`ui/motion/Motion.kt`): `EaseSpring` (overshoot), `EaseSmooth` (expo-out), `EaseEmphasized` (M3), `SpringSnappy/Default/Gentle`. All durations centralised in `Durations` object.
|
||||||
|
|
||||||
|
**What's NOT yet migrated to the design system:** The existing UI components (ChatScreen, Sidebar, ChatInput, etc.) still use the old inline glass styling with manual `Brush` + `border`. Migration to `GlassSurface` + `KaizenShapes` + `kaizenShadow` is the next step.
|
||||||
|
|
||||||
|
### Offline-first cache layer (Room) — added 2026-06-21
|
||||||
|
|
||||||
|
Architecture decision document: `OFFLINE_CACHE_ADD.md`.
|
||||||
|
|
||||||
|
**Principle:** Read-cache, not sync engine. Server is source of truth. No offline-queue, no conflict resolution.
|
||||||
|
|
||||||
|
**Stack:** Room 2.8.4 + KSP 2.2.10-2.0.2. `room-ktx` merged into `room-runtime` since Room 2.7 (no separate dep needed). `exportSchema = false`, `fallbackToDestructiveMigration()` — it's a cache, not a DB. On schema changes the SQLite is wiped; cache rebuilds on next server fetch.
|
||||||
|
|
||||||
|
**Schema:**
|
||||||
|
- `conversations` — id, title, locked, pinned, updatedAt, cachedAt
|
||||||
|
- `messages` — id, conversationId (FK CASCADE), role, content, attachments (JSON via TypeConverter), sortOrder
|
||||||
|
|
||||||
|
**Data flow:**
|
||||||
|
```
|
||||||
|
App start → Room (instant) → UI shows cached data
|
||||||
|
→ Background: server fetch → Room update → Flow emits → UI updates
|
||||||
|
Chat open → Room (instant) → Background: server fetch → Room update
|
||||||
|
Send message → in-memory during streaming → Room flush after stream ends
|
||||||
|
```
|
||||||
|
|
||||||
|
**Streaming hybrid model:** During active streaming, messages are mutated in-memory (60x/s). Room is NOT written during streaming — that would be absurd I/O. After stream completion, user + assistant messages are upserted into Room. This avoids SQLite lock contention while keeping the cache fresh.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `db/Entities.kt` — `ConversationEntity`, `MessageEntity`
|
||||||
|
- `db/ConversationDao.kt` — `observeAll()` (Flow), `replaceAll()` (transaction: deleteAll + upsertAll, avoids SQLite 999-variable limit), `lastCachedAt()`
|
||||||
|
- `db/MessageDao.kt` — `observeByConversation()`, `getByConversation()`, `upsertAll()`, `deleteByConversation()`
|
||||||
|
- `db/KaizenDatabase.kt` — singleton via `companion object` (double-checked locking), thread-safe
|
||||||
|
- `db/ConversationRepository.kt` — `observeAll()` (Flow of ConversationSummary), `refresh()` (server → Room)
|
||||||
|
- `db/MessageRepository.kt` — `observeMessages()`, `getCached()`, `fetchAndCache()`, `cacheMessages()`
|
||||||
|
- `db/Converters.kt` — `List<Attachment>` ↔ JSON string via kotlinx.serialization
|
||||||
|
- `db/Mappers.kt` — `ConversationSummary` ↔ `ConversationEntity`, `StoredMessage` ↔ `MessageEntity`
|
||||||
|
|
||||||
|
**AGP 9.x compat:** `android.disallowKotlinSourceSets=false` in `gradle.properties` (KSP issue [#2729](https://github.com/google/ksp/issues/2729)).
|
||||||
|
|
||||||
|
## What's DONE and verified
|
||||||
|
|
||||||
|
### Backend — all complete, deployed, tested
|
||||||
|
|
||||||
|
All backend work for the app is done. `pnpm test:run` green (774+), `tsc` + `lint` clean.
|
||||||
|
|
||||||
|
- **`api_tokens` table** + SHA-256 hash storage (never raw). Migration `0024`.
|
||||||
|
- **Bearer auth path** (`getTokenSession` / `requireActiveUserOrToken` in `lib/auth/guard.ts`).
|
||||||
|
- **Token mint endpoint** `POST /api/v1/auth/token` — unauthenticated, Argon2id verify, anti-enumeration, rate-limited.
|
||||||
|
- **`GET /api/v1/me`** — identity + `defaultModel` for native clients.
|
||||||
|
- **`GET /api/v1/models`** — Bearer-authed, returns filtered model catalog.
|
||||||
|
- **`/api/v1/conversations`** — thin re-exports of canonical handlers, Bearer-authed.
|
||||||
|
- **`GET /api/v1/meta`** — capability handshake.
|
||||||
|
- **App-devices card** (`components/settings/app-tokens-card.tsx`) — see/revoke signed-in devices on `/settings/security`.
|
||||||
|
- **API versioning** — `/api/v1/` for outward contract, additive-only within v1. Web-internal endpoints stay unversioned.
|
||||||
|
|
||||||
|
### App — shipped features (on-device verified on S25 Ultra)
|
||||||
|
|
||||||
|
All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKotlin` clean).
|
||||||
|
|
||||||
|
| Commit | Feature |
|
||||||
|
|--------|---------|
|
||||||
|
| `44c17b6` | In-app login (email+password → `POST /api/v1/auth/token`), `EncryptedSharedPreferences`, real streaming via `StreamConsumer.Incremental`, `ServerConfig` persistence |
|
||||||
|
| `aa0a97b` | Dither overlay to kill 8-bit sRGB banding on orb + mesh gradients |
|
||||||
|
| `d2f269f` | Model + speed picker (top pill → bottom sheet), response-speed segment, favorites, provider badges |
|
||||||
|
| `ea5e625` | Sidebar wired to real conversations, persistence (create/save/load/auto-title), date-grouped history |
|
||||||
|
| `7399c73` | Model sheet redesign with OpenRouter/Vertex badging, offline-first caching |
|
||||||
|
| `d81c6ff` | Robust JSON parsing (malformed response handling) |
|
||||||
|
| `827a2f3` | Streaming performance, error handling |
|
||||||
|
| `423abc0` | **Specific error diagnostics** — `FetchResult<T>` replaces null-returns; error banner shows exact cause (HTTP status, DNS, SSL, timeout, JSON parsing) instead of generic "Verbindung zum Server fehlgeschlagen" |
|
||||||
|
| `4616ef2` | **Markdown rendering with syntax-highlighted code blocks** — native Compose renderer: headings, bold/italic, inline code, fenced code blocks with keyword highlighting (15+ languages), ordered/unordered lists, streaming cursor integration |
|
||||||
|
| `d8dcb25` | **Attachment support — display, upload, and send files** — parse + render message attachments from loaded conversations (images inline via OkHttp + in-memory cache, other kinds as icon chips); + button wired to Android file picker with upload to `/api/v1/upload`, progress chips, send attachments with chat requests + persist in saved messages. Also: Markdown horizontal rules, blockquotes, links, code block copy button |
|
||||||
|
| `9759d47` | **Offline-first cache layer (Room)** — sidebar + messages load instantly from SQLite cache, background refresh syncs with server, streaming hybrid model (in-memory during stream, Room flush after), ChatViewModel, repositories, mappers, TypeConverters, unit tests |
|
||||||
|
|
||||||
|
**Earlier UI/feel work (Phase 0 prototype → feel-first):**
|
||||||
|
- Liquid glass styling, floating panels, glassmorphic sidebar
|
||||||
|
- KaizenOrb with 3D sphere rendering, streaming animation
|
||||||
|
- Mesh gradient background with dithered blobs
|
||||||
|
- Phase-aware haptics (click → thinkingStart → responseStart → responseEnd)
|
||||||
|
- Tablet/landscape optimization (width capping)
|
||||||
|
- Dynamic keyboard handling (suggestion pills hide, spacer shrinks)
|
||||||
|
- Settings screen with SettingsViewModel
|
||||||
|
|
||||||
|
### Bug fixed this session (2026-06-19/20)
|
||||||
|
|
||||||
|
**"Verbindung zum Server fehlgeschlagen" on S25** — root cause was **server running an old build** without the v1/conversations route (404) and without Bearer auth on models (401). Fix was `git pull && docker compose -f docker-compose.prod.yml up -d --build` on the server. Additionally, the error diagnostics were improved (see `423abc0`) so future issues show the exact cause in the UI banner instead of a generic message.
|
||||||
|
|
||||||
|
## What's NOT done yet (app, priority order)
|
||||||
|
|
||||||
|
### 1. Remaining visual/UX gaps (daily-drive blockers)
|
||||||
|
|
||||||
|
These are what separate "prototype" from "daily-driver":
|
||||||
|
|
||||||
|
- [ ] **Markdown rendering during streaming** — currently renders live but performance with long code blocks during high-speed streaming needs real-device verification
|
||||||
|
- [x] **Code block copy button** — tap-to-copy on fenced code blocks (clipboard + haptic feedback)
|
||||||
|
- [x] **Horizontal rule / blockquote rendering** — `---` and `> quote` not yet parsed
|
||||||
|
- [ ] **Table rendering** — Markdown tables render as raw text
|
||||||
|
- [x] **Link rendering** — styled visually, not yet clickable (tap-to-open pending)
|
||||||
|
|
||||||
|
### 2. Conversation management from the app
|
||||||
|
|
||||||
|
- [ ] **Conversation rename/delete** from the sidebar (currently read-only)
|
||||||
|
- [ ] **Conversation pin/unpin** from the sidebar
|
||||||
|
- [ ] **In-app unlock for locked conversations** (currently skipped with "Gesperrter Chat" label)
|
||||||
|
|
||||||
|
### 3. Security hardening
|
||||||
|
|
||||||
|
- [ ] **Auto-logout on 401** — if the token is rejected, route to login screen instead of showing error banner
|
||||||
|
- [ ] **Server version check** — call `/api/v1/meta` after login to detect server skew and warn cleanly
|
||||||
|
- [ ] **Biometric lock** (`BiometricPrompt` + KeyStore binding) — see `LATER.md` §1C
|
||||||
|
- [ ] **`FLAG_SECURE`** on sensitive screens — see `LATER.md` §1B
|
||||||
|
|
||||||
|
### 4. Features for parity with the web
|
||||||
|
|
||||||
|
- [x] **Attachments** — image/file upload, attachment preview in messages, display in loaded conversations (camera capture still TODO)
|
||||||
|
- [ ] **Reasoning/thinking display** — parse `\x01` sentinel, show collapsible thinking block
|
||||||
|
- [ ] **Web search sources** — parse `\x02`/`\x04` sentinels, render source cards
|
||||||
|
- [ ] **Tool call display** — parse `\x03` sentinel, show live tool steps
|
||||||
|
- [ ] **Native passkey login** (WebAuthn/FIDO2 on Android)
|
||||||
|
- [ ] **WebSocket streaming** (replace chunked HTTP for lower latency on mobile)
|
||||||
|
- [ ] **Usage/cost display**
|
||||||
|
- [ ] **User context / KI-Profil**
|
||||||
|
|
||||||
|
### 5. App distribution
|
||||||
|
|
||||||
|
- [ ] **Push notifications** (FCM)
|
||||||
|
- [ ] **Play Store / F-Droid listing**
|
||||||
|
- [ ] **Auto-update mechanism**
|
||||||
|
|
||||||
|
### 6. Offline-cache enhancements (future iterations, see `OFFLINE_CACHE_ADD.md` §11)
|
||||||
|
|
||||||
|
- [ ] **Stale-check enforcement** — use `cachedAt` to trigger background refresh when older than N minutes
|
||||||
|
- [ ] **Offline-queue for sends** — queue messages locally, send when network returns (needs conflict resolution)
|
||||||
|
- [ ] **Incremental sync** — only fetch changed conversations (needs server-side `since` parameter)
|
||||||
|
- [ ] **Media cache** — cache images/files locally (Coil or custom disk cache)
|
||||||
|
- [ ] **Full-text search** — FTS4/FTS5 over cached messages (Room supports this natively)
|
||||||
|
- [ ] **Attachment queries** — "show me all chats with images/PDFs" via Room DB
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# gradle/libs.versions.toml (key versions)
|
||||||
|
agp = "9.2.1"
|
||||||
|
kotlin = "2.2.10"
|
||||||
|
ksp = "2.2.10-2.0.2"
|
||||||
|
room = "2.8.4"
|
||||||
|
composeBom = "2026.02.01"
|
||||||
|
okhttp = "4.12.0"
|
||||||
|
kotlinxSerialization = "1.7.3"
|
||||||
|
securityCrypto = "1.1.0-alpha06"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
**Unit tests** (`app/src/test/`): JVM-only, no emulator needed.
|
||||||
|
|
||||||
|
- `SettingsViewModelTest` — state transitions, default values, theme selection, name updates
|
||||||
|
- `StreamConsumerTest` — incremental parser, sentinel stripping, edge cases
|
||||||
|
- `OklabTest` — 2 pre-existing failures (round-trip color drift > 0.01 tolerance, unrelated to backend wiring, likely too-tight tolerance or headless float precision)
|
||||||
|
- `MappersTest` — `ConversationSummary` ↔ `ConversationEntity`, `StoredMessage` ↔ `MessageEntity`, round-trip integrity, attachment preservation
|
||||||
|
- `ConvertersTest` — `List<Attachment>` ↔ JSON round-trips, malformed JSON returns empty list, unknown fields tolerance via `ignoreUnknownKeys`
|
||||||
|
- `ColorPipelineTest` — all tokens in DisplayP3, approximate value ranges, no pure black/white in neutrals
|
||||||
|
- `SquircleShapeTest` — KaizenShapes token existence/distinctness, SquircleShape toString
|
||||||
|
- `ThemeRegistryTest` — 4 themes present, forId lookup, fromString parsing + fallback, distinct blob colors
|
||||||
|
- `ShadowStackTest` — layer counts per level, alpha range validation, blur/alpha progression
|
||||||
|
|
||||||
|
## Pre-existing issues (not from this work)
|
||||||
|
|
||||||
|
- `OklabTest` has 2 headless failures (`testColorToOklabAndBack`, `testGradientGeneration` — round-trip color drift > 0.01 tolerance). Unrelated to backend wiring; likely too-tight tolerance or headless float precision.
|
||||||
|
|
||||||
|
## Constraints & gotchas
|
||||||
|
|
||||||
|
- Local dev Mac has **no RustFS** (storage on server only); upload/storage flows fail locally with `ECONNREFUSED :9000`.
|
||||||
|
- Live OpenRouter key is encrypted in the DB (`api_keys`), `.env.local` is stale.
|
||||||
|
- Never hand-write migrations or fake timestamps — use `pnpm db:generate`. Always `git pull` before `docker build` on prod.
|
||||||
|
- App philosophy: the real driver is *feel*, not features; native Compose (no WebView). Backend complexity stays invisible.
|
||||||
|
- **Deploy gotcha (learned 2026-06-19):** `git pull` BEFORE `docker compose up --build`, else the image bakes stale code and the app gets 404/401 against endpoints that exist in the repo but aren't in the running build.
|
||||||
|
- Headless compile on Mac: `JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./gradlew :app:compileDebugKotlin` — JDK 21 via Homebrew, Android SDK at `/opt/homebrew/share/android-commandlinetools`.
|
||||||
|
- **AGP 9.x + KSP:** requires `android.disallowKotlinSourceSets=false` in `gradle.properties`.
|
||||||
|
- **Room is a cache:** `fallbackToDestructiveMigration()` means the SQLite DB is wiped on schema changes. No data loss — server is source of truth, cache rebuilds on next fetch.
|
||||||
Loading…
Reference in a new issue