feat(web): iPhone PWA fixes (M1) + warm RTO redesign (M2) #3

Merged
serversdown merged 19 commits from dev into main 2026-06-21 02:10:53 -04:00

19 Commits

Author SHA1 Message Date
serversdown debb553fe9 Merge feat/modes-hud: session modes, HUD, rituals, history, nav, fixes 2026-06-21 06:08:30 +00:00
serversdown 44a559c5f9 fix: render flat-logged hands + on-demand "build replay"
Quick-logged hands (log_hand) store flat fields with no structured JSON, so the
hand viewer dead-ended with "no structured data to replay" — even when the full
street-by-street action was captured (e.g. the KhQh-vs-Louis hand). Now:

- hand.html renders a readable STATIC view of any flat hand (hero cards, board,
  street narratives, result, lesson) instead of erroring; also handles empty/garbage
  structured rows by falling back to the flat view.
- "▶ Build replay" button + poker.reconstruct_hand + POST /hand/{id}/reconstruct:
  parse a flat hand's narrative into structured form on demand, making any
  quick-logged hand replayable without an LLM call per log during live play.
- test_modes.py +1 (reconstruct wiring).

(Also reconstructed the two live Meadows hands and removed one empty hand in the DB.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:02:10 +00:00
serversdown f2de7dec61 feat(web): shared left-sidebar navigation across all pages
Desktop nav was scattered and inconsistent — the chat header was crammed with
cross-page links and each standalone page had its own ad-hoc, incomplete back-links
(e.g. /hands could only reach Chat). Now a single nav.js (one source of truth, no
build step) injects a left sidebar on desktop (>=769px) with active-page
highlighting across Chat/Session/History/Hands/Mind/Journal/Logs + Settings.

- nav.js: injects sidebar + its own CSS; body gets padding-left on desktop; hidden
  on mobile (each page keeps its bottom bar / back-links there).
- Included on every page (index, session, history, hands, self, journal, logs,
  recap, hand).
- Decluttered the chat header: removed the now-redundant cross-page links (kept the
  chat-specific session selector + inline Live Log toggle).
- Sidebar Settings opens the chat modal, or navigates to /?settings=1 from elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 05:27:55 +00:00
serversdown 559faaed30 feat: view past sessions, edit session details, log rituals while reviewing
- View any past session as a read-only HUD: /session?id=N (hud(session_id) +
  /session/data?id=); /history rows now link there. Closed sessions show played
  duration + final net; recap link when one exists.
- Edit session details during or after play: poker.update_session (recomputes net
  when buy-in/cash-out change), PATCH /session/{id}, an update_session tool ("venue
  was actually Bellagio", "I bought in for 600"), and an inline ✎ Edit form on the HUD.
- Rituals attach to the most-recent session post-close (poker.review_session_id),
  so scar/confidence/reset work while reviewing after you rack up.
- Edit form is poll-safe (won't clobber mid-edit); past-session view doesn't poll.
- test_modes.py +3 (edit, review rituals, past-session HUD); 49 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 05:12:13 +00:00
serversdown cca8322ee2 perf: WAL synchronous=NORMAL — stop fsyncing every commit
lyra.db is on disk-backed ext4, where each WAL commit fsync'd (~0.15s here). Every
chat turn does several writes (remember user+assistant, summaries, poker logging),
so this was adding real per-turn latency, and made the dream loop + tests crawl.
synchronous=NORMAL is WAL's recommended companion: durable across app crashes, only
a power/OS crash can drop the last txn (never corrupts). Per-write dropped from
~0.4s to ~0.001s; test suite 72s -> 24s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 05:12:13 +00:00
serversdown 67cf51a53f feat: undo / delete logged entries (fix fat-fingered live logging)
Previously the only delete was whole-session, so a mis-logged stack or a
mis-parsed hand was stuck on the HUD. Now:

- undo_last tool ("scratch that") — deletes the most recent hand/stack/read/
  scar/confidence/reset in the live session; added to the Cash toolset.
- poker.delete_hand/stack/read/ritual + delete_entry dispatch + undo_last.
- DELETE /session/entry/{kind}/{id} endpoint.
- HUD: per-row × delete buttons on hands, confidence-bank, and scar-note rows
  (stack/read deletes via the tool). Row ids now surfaced in the hud() bundle.
- test_modes.py +2 (undo_last across kinds, tool handler); 46 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 04:32:04 +00:00
serversdown df591e4e01 feat: decouple embeddings from the local-chat backend (EMBED_BASE_URL)
Embeddings shared LOCAL_BASE_URL with the local chat backend (the 3090's Ollama),
so the 3090 being powered off killed all chat (every turn embeds to recall + to
store). Add a separate EMBED_BASE_URL (defaults to LOCAL_BASE_URL, so existing
setups are unchanged) and use it in llm.embed.

Deployed: a user-level Ollama (CPU) now runs nomic-embed-text on lyra-cortex
itself; EMBED_BASE_URL points at 127.0.0.1:11434 while LOCAL_BASE_URL still points
the local chat backend at the 3090. Local embeddings verified identical to the
3090's (cosine 0.999994, 768-dim) so existing vectors stay valid — no re-embed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 04:19:26 +00:00
serversdown 5c41bd48d1 fix: consolidation no longer stalls or breaks the live chat turn
Two bugs surfacing in the log during live play:
- SUMMARY_BACKEND=mi50 (llama.cpp, 32B) was fed 24k-char chunks → "Context size
  has been exceeded". Chunk budget is now backend-aware: cloud 24k, local/mi50 8k,
  and the merge step recurses so merged partials never overflow either.
- maybe_summarize ran inline in the chat turn and retried 4× with backoff (~30s),
  stalling the reply and surfacing the error. It now runs in a background daemon
  thread, swallows errors (consolidation is best-effort maintenance), and dedupes
  so at most one summary per session runs at a time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 04:37:17 +00:00
serversdown 5e9f3efeec fix(web): copy button actually copies on iOS
The execCommand fallback returned true but copied nothing because the textarea
was readOnly=false. iOS only copies from a readOnly + contentEditable field with
a real Range selection + setSelectionRange — fixed that. Also skip the async
Clipboard API unless window.isSecureContext (on the plain-HTTP LAN PWA it could
resolve without copying, showing a false checkmark). If programmatic copy still
fails, fall back to a prompt() with the text so it can be copied by hand, and only
show the ✓ on real success.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 03:46:33 +00:00
serversdown 654a7531e8 feat(web): multiline composer — Enter adds a newline, arrow sends
Stops fat-fingered early sends. The single-line input is now an auto-growing
textarea (Claude-app style): Enter inserts a newline and expands the box (up to a
cap, then it scrolls); you tap the ↑ arrow to send. ⌘/Ctrl+Enter still sends from
a hardware keyboard. Send button is now a round arrow; box resets to one line
after sending. Mobile button is a 42-44px touch target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 03:20:50 +00:00
serversdown cebb87205c feat(web): tap-to-copy button on every chat message
Each user and assistant message gets a copy button (⧉ → ✓) that puts the whole
message on the clipboard. Assistant copies the raw markdown (its dataset.raw);
user copies its text.

- copyToClipboard uses the async Clipboard API when available and falls back to a
  hidden-textarea + execCommand with an explicit iOS selection range, so it works
  on the iPhone PWA over plain-HTTP LAN (no secure context).
- Copy sits in the assistant rate-bar and in a right-aligned bar on user bubbles;
  tools stay visible on touch (@media hover:none) since there's no hover on iOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 01:09:27 +00:00
serversdown e1e89c07e4 feat: poker session history — browse, delete, and Lyra lookup
Answers three gaps: no way to delete a single poker session (only clear_all),
no way to browse past sessions, and Lyra could only see aggregate stats.

- poker.list_sessions() (per-session summary + hand count + recap flag) and
  poker.delete_session() (removes a session + its hands/reads/observations/
  stacks/rituals; keeps the persistent villain file).
- /history page (date, stakes, venue, net, hours, recap link, per-row delete with
  confirm) + /history/data + DELETE /history/{id}. Nav links from chat + HUD.
- recent_sessions read tool, added to the shared lookups so Lyra can answer
  "how'd my last few sessions go?" in either mode.
- Delete is UI/CLI only — deliberately not a Lyra tool.
- test_modes.py +2 (list/delete, recent_sessions); 44 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:21:48 +00:00
serversdown 35c973df05 feat: session_state read tool so she can see the HUD
She could write everything the HUD shows but not read most of it back (stack,
live net, alligator state, scar/confidence entries) — so "what's my live net?"
or "what's in my confidence bank?" was a memory guess.

- session_state tool returns the same bundle the HUD renders, as a readable
  summary; added to the Cash toolset.
- Cash card tells her the HUD exists, that she and it share the same data, and to
  answer where-am-I questions from session_state, never memory.
- test_modes.py +1; 42 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:16:40 +00:00
serversdown 974ee33f71 feat: live mental-game rituals in Cash mode
Brian's own rituals (mined from his logs) become first-class, live tools instead
of post-hoc recap sections:

- Scar Note — instructive mistakes with the punt/cooler/standard distinction.
- Confidence Bank — good process, banked regardless of result.
- Alligator Blood — invokable adversity state; she suggests it when he's
  card-dead/short/stuck, and her coaching register shifts while it's on (live
  state injected into context per-turn via chat._mode_state_note).
- Reset — tilt circuit-breaker; mental marker only, stats stay continuous.

poker_rituals table + log_ritual/list_rituals/set_alligator/alligator_active;
4 tools added to the Cash toolset and taught in the mode card; HUD gains a 🐊
banner + Confidence Bank + Scar Notes panels; recap grounded via _rituals_block.
tests/test_modes.py +5 ritual tests; 41 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 06:24:28 +00:00
serversdown dfb6425395 feat: session modes (Talk/Cash) + live session HUD
Lyra now switches register based on what she's doing at the table instead of
being a wishy-washy companion mid-session.

Modes (lyra/modes.py):
- Talk (default companion) + Cash (live cash copilot); a mode = prompt card +
  tool allow-list. Tool gating via tools.specs(allow=).
- Two-register Cash voice: act-first one-line logging when fed facts; full warm
  companion voice for strategy / tilt / mental game.
- mode persisted per chat session (new sessions.mode column); auto-switch into
  Cash when start_session fires; UI forces cloud backend in Cash (tools only
  fire there).

Stack tracking + HUD:
- log_stack tool + poker_stack_log table; live net while sitting (stack - buy-in).
- poker.hud() bundle; /session HUD page (stack sparkline, hands, villains, notes,
  stats) polling /session/data every 5s; Talk/Cash switcher + Session nav.

Endpoints: /session, /session/data, GET/POST /sessions/{id}/mode, /modes.
tests/test_modes.py (gating, mode roundtrip, stack/HUD); 36 tests green. v0.3.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 05:28:15 +00:00
serversdown d9f5055ec1 chore: sync uv.lock to version 0.2.0
Lockfile caught up to the pyproject version bump from 1f5a321 (it wasn't
regenerated at the time). No dependency changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 05:11:50 +00:00
serversdown 50f460eeb2 feat(web): bottom tab bar navigation (M4)
- Mobile bottom tab bar: Chat · Hands · Mind · More. "More" opens the drawer
  for the long tail (Journal, Log, Settings, sessions); hamburger retired.
- Auto-hides while the keyboard is open (body.kb) so the input pins to the
  keyboard; mobile-only (desktop keeps its header nav).
- Removed now-redundant Mind/Hands from the drawer + their listeners.
- Bottom-fill fix: #chat uses 100dvh (the visible viewport) — 100vh/inset:0
  reach into the home-indicator zone iOS won't comfortably show, clipping the
  bar; dvh/svh exclude it. Tab bar is flex:none with a small fixed bottom
  padding (safe-area padding double-counts at dvh height), and the body bg
  matches the bar so any strip below #chat is seamless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 04:36:08 +00:00
serversdown 5dc3fa17d7 feat(web): stream chat replies token-by-token (M3)
- llm.chat_call_stream: streaming generator for all 3 backends (Ollama NDJSON,
  OpenAI/MI50 SSE), accumulating tool-call fragments by index.
- chat.respond_stream: mirrors respond()'s tool loop and persistence/compaction,
  yielding ("delta", text) / ("tool", name) / ("done", reply).
- POST /v1/chat/stream: SSE endpoint; blocking generator bridged to async via a
  worker thread + asyncio.Queue. Old completions endpoint kept as fallback.
- Client streams into a live bubble with a blinking caret; rAF-throttled render
  (no full re-parse per token) and instant scroll during stream — fixes iOS
  Safari ghosting from per-token smooth-scroll. Falls back to the blocking
  endpoint only if nothing streamed (no double-persist).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 00:06:51 +00:00
serversdown fa168271e1 feat(web): iPhone PWA fixes (M1) + warm RTO redesign (M2)
M1 — PWA mechanics:
- Generate real app icons (apple-touch-icon + manifest 192/512/maskable)
  via pure-stdlib gen_icons.py; iOS uses apple-touch-icon, not manifest icons.
- viewport-fit=cover + env(safe-area-inset-*) on header/input/menu so content
  clears the notch and home indicator.
- Dynamic height pinned to the VisualViewport (height + offsetTop, re-measured
  across the keyboard animation) so the input stays above the iOS keyboard;
  100dvh fallback. Kills the squish/gap bugs in standalone mode.
- overscroll containment; flesh out manifest (scope, portrait, maskable).

M2 — visual redesign:
- Realign style.css to the warm low-glow RTO palette already used by the
  standalone pages (#0e0e0e panels, #2a1d12 borders); remove the neon
  saturated-orange borders and ~15 glow shadows.
- Reserve filled accent for one element (Send); glow only on status pulse +
  input focus. Flat warm message bubbles with tail corners.
- Reclaim the mobile header into [≡] Lyra · [status dot]; drop the redundant
  status bar (relay status now the header dot, updated in checkHealth).
- prefers-reduced-motion support; fix undefined var(--text); real light-mode tokens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:20:11 +00:00