# 2026-05-01 — Radio archive UI redesign recovery + Jupiter audio-404 diagnosis

## User
- **User:** Mike Swanson (mike)
- **Machine:** GURU-BEAST-ROG
- **Role:** admin
- **Session span:** 2026-04-30 ~11:17 PT (UI redesign work, then machine reboot) → 2026-05-01 ~05:30 PT (recovery, commit, bug triage, sync)

---

## Session Summary

The session opened with Mike reporting that GURU-BEAST-ROG had rebooted while Claude was mid-task and asking what was in flight. Triage found a single dangling artifact — an 820-line uncommitted diff (`+607/-213`) to `projects/radio-show/audio-processor/server/main.py`, mtime 2026-04-30 11:17:35 PT. The other modified file in `git status` (`.claude/scheduled_tasks.lock`) was identified as transient session-lock state and explicitly left alone. Today's existing session log at `session-logs/2026-04-30-session.md` (cPanel CVE remediation, committed in `7128b9e`) made no mention of any radio-show work, confirming this was un-logged territory.

Diff inspection showed the change was scoped purely to the two embedded HTML templates inside the FastAPI server — `INDEX_HTML` (search/browse page) and `EPISODE_HTML` (episode detail page). No Python / backend / SQL logic changed. The index page received a full CSS-custom-property theme (light with `#c39733` accent), an embedded SVG search-icon on the input, focus rings, divider-separated control groups, a styled "browse mode" toggle using the `:has()` selector, hit-card hover states with arrow indicator + focus-visible outlines, restyled Q/A pill badges, refined score badges and topic chips, and an animated loading-dots state. The episode page gained a sticky `<audio>` player and sticky aside, an active-Q&A highlight that follows the audio playhead via a new `timeupdate` listener (builds a sorted index of QA blocks at load, computes each block's `end` as next-start capped at +180s, toggles `.active` on both the body block and its corresponding aside list item), a "NOW PLAYING" pill revealed only on `.qa.active`, an active state on the intro-marker, and an `<audio preload>` change from `none` → `metadata` so seek-to-hash works without prior user interaction.

Mike approved the commit. The Gitea Agent staged only the radio-show file (no `-A`, no push, no amend) and landed commit `296d157` on top of `7128b9e`; `scheduled_tasks.lock` remained unstaged.

Mike then reported the deployed Jupiter instance at `http://172.16.3.20:8765/episode/139#qa-377` was broken — "none of the links work, does not play audio." Probing the endpoint confirmed the symptom is **not** a regression from the redesign: `/api/audio/139` returns HTTP 404, `Content-Type: application/json` (the FastAPI 404 detail). The endpoint logic is correct; the docstring even acknowledges the deployment shape: *"Jupiter currently has no episodes/ tree — that's a clean 404."* The audio MP3s have never been deployed to Jupiter — `EPISODES_DIR` defaults to `/data/episodes` and is empty there. The browser correctly fires the `<audio>` element's `error` event on the 404, the existing handler hides the player and reveals the `<div id=audio_missing>` notice, and any subsequent `seek()` call has no working media element to drive — so every link appears to do nothing. Three remediation paths were laid out (rsync the ~30–40 GB archive from IX to Jupiter, proxy `/api/audio/{id}` from Jupiter to IX on demand, or point the `<audio src>` at IX directly). Mike has not yet picked one — the bug remains open and tracked.

The session closed with a `/sync`. Pulled 15 commits (Discord bot Phase 1 MVP from Mike's Mac, two Howard sessions on Cascades — CA phased rollout + MHS kiosk fix + Sombra onboarding side-quest, a Syncro billing-verification correction thread, and a DKIM/MSP work session log). Resolved a `.claude/scheduled_tasks.lock` rebase conflict by keeping the current session's value. Pushed two outgoing commits — the radio UI redesign (rebased `296d157` → `d7ce9cb`) and the auto-sync lock bump (`4a7d07a`). Surfaced two `## Note for Mike` blocks from Howard's Cascades logs (CA rollout posture + admin@ FIDO2 todo; Tenant Admin SP scope expansion + MHS kiosk package-name gotcha + Sombra Server2013-is-actually-2012 finding) and addressed each before reporting sync status.

---

## Key Decisions

- **Stage only `main.py`, leave `.claude/scheduled_tasks.lock` unstaged.** The lock is regenerated each Claude session. Committing it would have created noise and a rebase conflict on every subsequent sync. (The conflict happened anyway because prior Mac sessions *had* committed it; documented below as a known issue.)
- **Commit before testing the UI in a browser.** Standard CLAUDE.md guidance is to verify UI changes in a running dev server. Skipped here because the work was already done before the reboot, the diff was surface-level (CSS + JS only, no Python logic), and Mike explicitly said "Commit" after seeing the summary. The audio-404 bug Mike reported next turned out to be unrelated to the redesign — pre-existing deployment state — so the skip didn't hide a real regression. Worth flagging the precedent though: if the UI changes had been functional, this would have been the wrong call.
- **Diagnose-only on the audio bug, do not auto-fix.** The fix space spans deployment / architecture choices (rsync 30+ GB to Jupiter, build a streaming proxy, switch the audio src). Each has different implications for IX bandwidth, Jupiter disk, and auth posture. Surfaced the three options and held for Mike's pick instead of guessing.
- **During sync rebase, take the local session's `scheduled_tasks.lock` value (`--theirs` semantics during rebase).** The lock is meaningful only for the currently-running Claude — keeping the just-committed local value preserves accuracy of "what session is running now." A future cleanup is to add this file to `.gitignore`; not done this session to avoid scope creep.

---

## Problems Encountered

- **Ollama narrative draft pulled stale content from a prior session.** The /save protocol writes a prompt file at `C:/Users/guru/AppData/Local/Temp/save_narrative_prompt.txt` and reads it back through qwen3:14b. The Write tool call failed because that file already existed (from the previous /save flow for the cPanel session) and hadn't been read first; the subsequent `py` call read the leftover prompt contents and qwen3 produced a perfectly-coherent but completely-wrong narrative about cPanel CVE work. Recovery: wrote the narrative directly per the `OLLAMA` empty fallback in the protocol. Future fix: the /save protocol should either delete the prompt file before re-writing or use a unique per-session filename.
- **Sync rebase conflict on `.claude/scheduled_tasks.lock`.** Inevitable when both the local auto-sync commit and an incoming auto-sync commit from another machine modify the same single-line lock. Resolved with `git checkout --theirs <file> && git add <file> && git rebase --continue`. Cleanest long-term fix is to gitignore this file; it serves no purpose in version control.
- **`ssh mike@172.16.3.20` denied (publickey,password,keyboard-interactive).** Tried during audio-bug investigation to inspect Jupiter's `/data/episodes` directly. Backed off — the public probe via `curl` already gave conclusive evidence (HTTP 404 from the endpoint with the "no episodes/ tree" docstring), so direct shell access wasn't needed to diagnose. Jupiter SSH credentials remain uncaptured for `mike@` from GURU-BEAST-ROG; not pursued this session.

---

## Configuration Changes

### Files modified (committed, pushed)
- `projects/radio-show/audio-processor/server/main.py` — `INDEX_HTML` and `EPISODE_HTML` template overhaul (CSS custom properties, mobile viewport, sticky audio player, active-Q&A highlight via `timeupdate`, `preload="metadata"`). +607 / −213. No Python logic changes.

### Files modified (not committed — transient)
- `.claude/scheduled_tasks.lock` — modified during the auto-sync flow, eventually committed by `sync.sh` as `4a7d07a` (and later as the rebase resolution).

### Files created
- `projects/radio-show/session-logs/2026-05-01-ui-redesign-recovery.md` — this file.

---

## Commands & Outputs

### Recovery probes (start of session)
```bash
git -C /c/Users/guru/ClaudeTools status --short
#  M .claude/scheduled_tasks.lock
#  M projects/radio-show/audio-processor/server/main.py

git -C /c/Users/guru/ClaudeTools diff --stat HEAD -- projects/radio-show/audio-processor/server/main.py
#  projects/radio-show/audio-processor/server/main.py | 820 +++++++++++++++------
#  1 file changed, 607 insertions(+), 213 deletions(-)

stat -c '%y %n' projects/radio-show/audio-processor/server/main.py
#  2026-04-30 11:17:35.639586700 -0700
```

### Audio-404 diagnosis (Jupiter, `172.16.3.20:8765`)
```bash
curl -s -o /dev/null -w "page: %{http_code} size:%{size_download}\n" http://172.16.3.20:8765/episode/139
#  page: 200 size:69695  -- episode page renders fine

curl -s -I http://172.16.3.20:8765/api/audio/139
#  HTTP/1.1 405 Method Not Allowed
#  allow: GET
#  -- HEAD not implemented; endpoint exists

curl -s -r 0-127 -o /dev/null -w "audio: %{http_code} ct:%{content_type}\n" http://172.16.3.20:8765/api/audio/139
#  audio: 404 ct:application/json
#  -- file not found on Jupiter; FastAPI returns {"detail":"Not Found"}
```

### Sync (rebase + conflict resolution)
```bash
bash .claude/scripts/sync.sh
# ... pulled 15 commits, conflict on .claude/scheduled_tasks.lock during rebase

git checkout --theirs .claude/scheduled_tasks.lock
git add .claude/scheduled_tasks.lock
git -c core.editor=true rebase --continue
#  Successfully rebased and updated refs/heads/main.

git push origin main
#  e7ec4a8..4a7d07a  main -> main
```

---

## Infrastructure & Servers

### Jupiter (radio-show FastAPI host) — `172.16.3.20`
- Port `8765`, uvicorn (per `Server: uvicorn` response header)
- Endpoints confirmed live this session: `GET /` (search index), `GET /episode/{id}` (HTML detail page), `GET /api/audio/{id}` (Range-supported MP3 stream — currently 404 for all IDs), `GET /api/qa`
- `EPISODES_DIR` env var defaults to `/data/episodes` per `main.py:33`. Tree is empty / not deployed.
- SQLite DB *is* deployed (search and episode-page rendering both work — title, segments, Q&A, intros all render correctly)

### IX server (radio archive source) — `172.16.3.10`
- Archive root: `/home/gurushow/public_html/archive/Radio/` (per `2026-04-27-qa-extraction-cohost-indexing.md`)
- 579 MP3s, 2010–2018 (no 2013 season), ~30–40 GB total
- Tailscale + paramiko-with-`look_for_keys=False, allow_agent=False` known-working access pattern for `gurushow@`

### Endpoint code reference for `_resolve_audio_path`
- `main.py:344` — joins `EPISODES_DIR` + `rel_path` from DB row, requires `candidate.is_file()`, returns `None` if missing. Path-traversal guard via `relative_to(base)`.
- `main.py:408` — `@app.get("/api/audio/{episode_id}")`, returns 404 when `_resolve_audio_path` is `None`. Docstring: *"Jupiter currently has no episodes/ tree — that's a clean 404. The audio element on the transcript page checks the response and hides itself on 404."*

---

## Pending / Incomplete Tasks

### From this session
- [ ] **Audio-not-playing on Jupiter — pick remediation.** Three options:
  1. **rsync the archive to Jupiter** (`/data/episodes/`, ~30–40 GB). Most correct, biggest disk hit. Matches the existing `_resolve_audio_path` contract directly.
  2. **Proxy `/api/audio/{id}` from Jupiter to IX** on demand. ~5 lines added to `stream_audio()`. Keeps Jupiter thin; doubles bandwidth on cache miss; requires IX→Jupiter auth.
  3. **Point `<audio src>` at IX directly** (`archive.azcomputerguru.com/Radio/<rel_path>` or similar). Bypasses `/api/audio` entirely; simplest if a public-readable HTTPS endpoint already serves that tree.
- [ ] **Add `.claude/scheduled_tasks.lock` to `.gitignore`.** Removes the recurring rebase-conflict noise from every multi-machine sync. Low priority but easy.
- [ ] **Fix `/save` protocol's stale-prompt-file bug.** Either delete `save_narrative_prompt.txt` before re-writing, or use a per-session filename (e.g. include a UUID or PID). Right now Ollama drafts can silently inherit the previous /save's prompt.

### From Howard's notes (synced this session, surfaced separately)
- [ ] **Enroll YubiKey on `admin@cascadestucson.com`** + decide breakglass posture (dedicated `breakglass@` vs. keep Howard's Option 1). No CA enforcement, no FIDO2 today.
- [ ] **Decide per-tenant whether to opt into the 4 new Intune Graph permissions** for the Tenant Admin SP. Cascades is already re-consented; other customer tenants are not auto-updated.
- [ ] **Sombra Residential "Server2013" is actually Server 2012 RTM (EOL 2023-10-10).** Migration plan needed. Daily admin: `Administrator/Tick8800`; sysadmin password not captured.

---

## Reference Information

### File paths
- Server source: `projects/radio-show/audio-processor/server/main.py` (FastAPI app, embedded HTML templates, SQLite query layer)
- Prior radio-show context: `projects/radio-show/session-logs/2026-04-27-qa-extraction-cohost-indexing.md`
- Sync script: `.claude/scripts/sync.sh`
- Scheduled-tasks lock (transient, regenerated per session): `.claude/scheduled_tasks.lock`

### Commits this session
- `d7ce9cb` — `radio: visual redesign of search + episode pages, active-Q&A highlight follows playhead` (was `296d157` pre-rebase). Author: Mike Swanson <mike@azcomputerguru.com>. 1 file, +607 / −213.
- `4a7d07a` — `sync: auto-sync from GURU-BEAST-ROG at 2026-05-01 05:35:53`. `.claude/scheduled_tasks.lock` only.

### URLs probed
- Episode page (renders fine): `http://172.16.3.20:8765/episode/139`
- Audio endpoint (404 — file not deployed): `http://172.16.3.20:8765/api/audio/139`
- Mike's reported broken URL with hash: `http://172.16.3.20:8765/episode/139#qa-377`

---

## Update: 06:31 PT — Local deployment + intro/QA sort bug fix

### What happened

Mike was unclear on the relative status of the UI redesign vs. the audio-not-playing bug — they were two independent things and prior reports had conflated them. Clarified: redesign was committed (`d7ce9cb`) and pushed to Gitea but **never deployed to Jupiter**, so `172.16.3.20:8765` was still serving the prior visual design; and the audio 404 was a pre-existing Jupiter deployment gap (no `/data/episodes/` tree on that host) that was independent of any UI version.

Mike chose to defer the Jupiter audio-tree fix and instead **deploy the new interface locally** so he could see the redesign with working audio. Local probe found everything needed already on disk under `projects/radio-show/audio-processor/`:

- `.venv/Scripts/python.exe` (FastAPI 0.115.6, uvicorn 0.34.0 already installed)
- `archive-data/archive.db` — 572 episodes (full archive, not just the 6 test episodes from the 2026-04-27 session)
- `archive-data/episodes/` — full MP3 tree, including episode 139 (`2011/3 - March/3-26-11 HR 2.mp3`, 9.9 MB)

Booted uvicorn at `127.0.0.1:8765` in the background:

```bash
cd c:/Users/guru/ClaudeTools/projects/radio-show/audio-processor
ARCHIVE_DB=archive-data/archive.db EPISODES_DIR=archive-data/episodes PORT=8765 \
  .venv/Scripts/python.exe -m uvicorn server.main:app \
  --host 127.0.0.1 --port 8765 --log-level info
```

Smoke tests confirmed the new UI was live and audio worked end-to-end:

```
GET /                       : 200    (6 new-UI markers: --accent #c39733, browse-toggle, loading::after)
GET /episode/139            : 200    (8 new-UI markers: now-playing, preload="metadata", qaBlocks)
GET /api/audio/139 (0-127)  : 206    audio/mpeg  -- Range streaming working
```

Mike then loaded `http://127.0.0.1:8765/episode/479#qa-1134` and got **500 Internal Server Error**. Server traceback pinpointed:

```
File "server/main.py", line 597, in _episode_html
    intro_by_time = sorted(
        ((r["intro_time_sec"] or 0.0), r) for r in intros
    )
TypeError: '<' not supported between instances of 'sqlite3.Row' and 'sqlite3.Row'
```

Root cause: `sorted()` over `(float, sqlite3.Row)` tuples with no `key=`. When two intros share the same `intro_time_sec`, Python's tuple comparison falls through to the second element — `sqlite3.Row` does not implement `__lt__`, so it raises. Episode 479 happens to have an intro-time collision; episode 139 didn't, which is why the bug surfaced now and not earlier. The bug is **not** caused by today's UI redesign — the offending `sorted()` call predates it. Same bug existed at line 551 for `qa_starts` (where the second element is a `dict` from `[dict(r) for r in qa]`; dict comparison is also unsupported in Python 3) — would have surfaced eventually on a QA timestamp collision.

Minimal fix: added `key=lambda x: x[0]` to both `sorted()` calls so the sort is strictly by timestamp. Ties are kept in DB-row order (stable sort), which is fine — the consumer (`_flush_inline_at`) only cares that items at-or-before the current segment time are flushed in non-decreasing order.

After restart, retested:

```
GET /episode/479            : 200
GET /episode/139            : 200    (regression check — still works)
GET /api/audio/479 (range)  : 206 audio/mpeg
ep 479 page contains qa-1134 anchor: yes
```

The fix is currently uncommitted in the working tree; Mike has not yet OK'd a commit for it.

### Key Decisions (this update)

- **Use port 8765 locally** to mirror Jupiter's port — preserves any browser bookmarks / muscle memory; no conflict because the local server binds `127.0.0.1` while Jupiter is `172.16.3.20`.
- **`--host 127.0.0.1` (not `0.0.0.0`)** for the local server. No reason to expose this dev instance on the LAN; Jupiter is the canonical host.
- **Minimal-diff bug fix (`key=lambda x: x[0]`) over a refactor.** The wider `sorted(...) for r in ...` shape is fine; the only defect is the tie-break behavior. Changing the data shape (e.g. dropping the tuple, using `key=lambda r: r["intro_time_sec"] or 0.0`) would have rippled into the `next_intro[0]` / `next_intro[1]` indexing further down. Two-line fix landed instead.

### Problems Encountered (this update)

- **`/episode/479` returned 500.** Root cause analyzed above — pre-existing `sorted()` tie-break bug in `_episode_html`, exposed by ep 479's intro-time collision. Fixed at lines 551 and 597 of `main.py` by adding `key=lambda x: x[0]`.

### Configuration Changes (this update)

#### Files modified (uncommitted)
- `projects/radio-show/audio-processor/server/main.py` — added `key=lambda x: x[0]` to both `sorted()` calls at lines 551–554 (qa_starts) and 597–600 (intro_by_time). Net: +4 / −2.

#### Background process
- `uvicorn` running locally on `127.0.0.1:8765`. Bash background task ID `bj1leiit0`. Log at `/tmp/radio-server.log`. Will need to be killed when Mike's done viewing (`taskkill //F //PID <pid>` or just close the terminal).

### Pending / Incomplete Tasks (this update)

- [ ] **Commit the intro/QA sort tie-break fix.** Two-line diff at lines 551 and 597 of `server/main.py`. Suggested commit subject: `radio: fix episode page 500 when intro/QA timestamps collide`. Awaiting Mike's OK.
- [ ] **Kill the local uvicorn** (`bj1leiit0`) when Mike is done viewing. PID will be in `/tmp/radio-server.log` first line ("Started server process [N]").
- [ ] **(Carried) Audio fix for Jupiter** — still deferred per Mike's "we'll deal with the Jupiter file tree later." Three options unchanged: rsync archive (~30–40 GB), proxy `/api/audio/{id}` to IX, point `<audio src>` at IX directly.

### Reference (this update)

- Local archive root: `c:/Users/guru/ClaudeTools/projects/radio-show/audio-processor/archive-data/`
  - DB: `archive-data/archive.db` (572 episodes, 10+ Q&A pairs across the indexed set)
  - MP3 tree: `archive-data/episodes/{YYYY}/{MM - Month}/<filename>.mp3`
  - Episode 139 file: `archive-data/episodes/2011/3 - March/3-26-11 HR 2.mp3`
- Local server URL: `http://127.0.0.1:8765`
- Smoke-test URL Mike was using: `http://127.0.0.1:8765/episode/479#qa-1134`
- Server entry point: `server.main:app` (FastAPI app object); env vars `ARCHIVE_DB`, `EPISODES_DIR`, `PORT`
