v1.1.0:4 — multi-config AI, background generation, ollama auto-detect, system prompt overhaul

User-feedback-driven release after testing v1.1.0:3. Nine themes:

1. Multi-config persistence
   - New AIConfigProfile table (per-user). Save N configs, toggle one
     active. Switching providers no longer wipes the previous setup.
   - UserPreferences gains activeAIConfigId; legacy single-config
     columns are mirrored from the active profile so existing reads
     keep working without conditional logic.
   - Idempotent boot migration lifts any existing single-config row
     into a default profile.

2. Ollama auto-detect
   - The "Add config" form probes /api/tags on the StartOS internal
     addresses (ollama.startos / ollama.embassy on :11434). If
     reachable: URL pre-fills, model field becomes a dropdown of
     installed models. Fixes the copy-paste UX.

3. Curated model dropdowns for major providers
   - Claude: Opus 4.7, Sonnet 4.6 (1M ctx), Haiku 4.5
   - OpenAI: GPT-5.5, 5.4, 5.4-mini, 5.4-nano
   - Gemini: 3.1-pro-preview, 2.5-pro, 2.5-flash, etc.
   - "Other (type your own)" stays for niche models.
   - Fixes "I tried gemini-3.0-pro and got 404."

4. Background generation
   - lib/ai/generationRunner.ts: detached runner with in-memory
     pub/sub bus. POST /api/ai/generate kicks it off and returns
     immediately. SSE stream attaches by id. The runner survives
     request cancellation; navigating away no longer kills it.
   - New AIGeneration columns: progressText (in-flight stream),
     durationMs (final wall-clock).
   - Generate UI shows a banner explaining background-safety.
   - History detail page polls progress + renders partial JSON
     live for cross-process resume (page refresh, new tab).

5. System prompt overhaul
   - lib/ai/systemPromptBase.ts: structural contract prepended to
     every template. Forces JSON-only output, library-exerciseId
     usage (kills "exerciseId doesn't belong to this user" errors),
     and per-resistance-exercise suggestedWeight (with-history vs
     without-history variants).
   - aiExerciseSchema + ProgramExercise gain suggestedWeight +
     suggestedWeightUnit. Starting a workout from a ProgramDay
     pre-populates SetLog.weight from the suggestion.

6. Test connection improvements
   - Latency in seconds (was ms — confusing for slow Ollama).
   - Stale "✓ Connected" clears on form change.
   - Per-config Test (no need to activate first).
   - Generous maxOutputTokens for thinking models.
   - Gemini surfaces finishReason on empty response (e.g. "blocked
     by safety filter") instead of generic "empty response."
   - Test endpoint accepts a draft body so you can verify before
     saving + before activating.

7. History detail view
   - Click row → full program tree + exact prompts sent. Apply from
     here without re-generating. Pending rows poll for progress.

8. Sidebar sub-navigation
   - AI: Generate / History / Templates
   - Settings: General / Password / Sessions / AI integration /
     Export / Instance (admin) / Danger zone, with anchor scroll.

9. API key UX
   - "Key saved" indicator on saved configs (was confusing to see
     an empty input after a successful save).

Schema migrations (additive, idempotent in entrypoint):
  - AIConfigProfile table created
  - UserPreferences.activeAIConfigId
  - AIGeneration.progressText + durationMs
  - ProgramExercise.suggestedWeight + suggestedWeightUnit

Tests: 16 new (systemPromptBase, modelMenu, generationRunner). 177
total pass.
This commit is contained in:
Keysat
2026-05-11 08:09:01 -05:00
parent dba478aa23
commit 7a62690a4a
35 changed files with 3509 additions and 632 deletions
+76
View File
@@ -172,6 +172,82 @@ if command -v sqlite3 >/dev/null 2>&1 && [ -f "$DB_PATH" ]; then
"
fi
# v1.1.0:4 added AIConfigProfile table (multi-config support) +
# UserPreferences.activeAIConfigId pointer + AIGeneration progress/
# duration columns + ProgramExercise suggested-weight columns.
if ! sqlite3 "$DB_PATH" \
"SELECT name FROM sqlite_master WHERE type='table' AND name='AIConfigProfile';" \
2>/dev/null | grep -q AIConfigProfile; then
log "creating AIConfigProfile table"
sqlite3 "$DB_PATH" "
CREATE TABLE AIConfigProfile (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
name TEXT NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
baseUrl TEXT,
apiKey TEXT,
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES User(id) ON DELETE CASCADE
);
CREATE INDEX AIConfigProfile_userId_idx ON AIConfigProfile(userId);
"
fi
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('UserPreferences');" 2>/dev/null | grep -q "|activeAIConfigId|"; then
log "adding UserPreferences.activeAIConfigId"
sqlite3 "$DB_PATH" "ALTER TABLE UserPreferences ADD COLUMN activeAIConfigId TEXT;"
fi
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('AIGeneration');" 2>/dev/null | grep -q "|progressText|"; then
log "adding AIGeneration.progressText"
sqlite3 "$DB_PATH" "ALTER TABLE AIGeneration ADD COLUMN progressText TEXT;"
fi
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('AIGeneration');" 2>/dev/null | grep -q "|durationMs|"; then
log "adding AIGeneration.durationMs"
sqlite3 "$DB_PATH" "ALTER TABLE AIGeneration ADD COLUMN durationMs INTEGER;"
fi
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('ProgramExercise');" 2>/dev/null | grep -q "|suggestedWeight|"; then
log "adding ProgramExercise.suggestedWeight + suggestedWeightUnit"
sqlite3 "$DB_PATH" "ALTER TABLE ProgramExercise ADD COLUMN suggestedWeight REAL;"
sqlite3 "$DB_PATH" "ALTER TABLE ProgramExercise ADD COLUMN suggestedWeightUnit TEXT;"
fi
# v1.1.0:4 one-shot migration: lift each user's legacy single-config
# (UserPreferences.aiProvider/aiModel/...) into a new AIConfigProfile
# row marked active. Idempotent — only runs for users who have a
# configured legacy config but no profiles yet.
log "migrating any legacy single-config to AIConfigProfile (idempotent)"
sqlite3 "$DB_PATH" "
INSERT INTO AIConfigProfile (id, userId, name, provider, model, baseUrl, apiKey)
SELECT
'c' || lower(hex(randomblob(12))),
up.userId,
'Default (' || up.aiProvider || ')',
up.aiProvider,
up.aiModel,
up.aiBaseUrl,
up.aiApiKey
FROM UserPreferences up
WHERE up.aiProvider IS NOT NULL
AND up.aiModel IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM AIConfigProfile p WHERE p.userId = up.userId);
" 2>/dev/null || log "WARN: legacy-config migration skipped"
# Set activeAIConfigId for users who now have exactly one profile.
sqlite3 "$DB_PATH" "
UPDATE UserPreferences
SET activeAIConfigId = (
SELECT id FROM AIConfigProfile WHERE userId = UserPreferences.userId LIMIT 1
)
WHERE activeAIConfigId IS NULL
AND (SELECT COUNT(*) FROM AIConfigProfile WHERE userId = UserPreferences.userId) = 1;
" 2>/dev/null || true
if ! sqlite3 "$DB_PATH" \
"SELECT name FROM sqlite_master WHERE type='table' AND name='InstanceSettings';" \
2>/dev/null | grep -q InstanceSettings; then