# Signal Banner Redesign — Expandable Email + Re-classification Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Expand signal banners to show full email body, add inline re-classification chips, and remove single-click stage advancement (always route through MoveToSheet). **Architecture:** Backend adds `body`/`from_addr` to the signal query and a new `POST /api/stage-signals/{id}/reclassify` endpoint. The `StageSignal` TypeScript interface gains two nullable fields. Both `InterviewCard.vue` (kanban) and `InterviewsView.vue` (pre-list) get an expand toggle, body display, and six re-classification chips. Optimistic local mutation drives reactive re-labeling; neutral triggers a two-call dismiss path to preserve Avocet training signal. **Tech Stack:** FastAPI (Python), SQLite, Vue 3, TypeScript, Pinia, `useApiFetch` composable **Spec:** `docs/superpowers/specs/2026-03-19-signal-banner-reclassify-design.md` --- ## File Map | File | Action | |---|---| | `dev-api.py` | Add `body, from_addr` to signal SELECT; add `reclassify_signal` endpoint | | `tests/test_dev_api_interviews.py` | Add `body`/`from_addr` columns to fixture; extend existing signal test; add 3 reclassify tests | | `web/src/stores/interviews.ts` | Add `body: string \| null`, `from_addr: string \| null` to `StageSignal` | | `web/src/components/InterviewCard.vue` | `bodyExpanded` ref; expand toggle button; body+from_addr display; 6 reclassify chips; `reclassifySignal()` | | `web/src/views/InterviewsView.vue` | `bodyExpandedMap` ref; `toggleBodyExpand()`; same body display + chips for pre-list rows | --- ## Task 1: Backend — body/from_addr fields + reclassify endpoint **Files:** - Modify: `dev-api.py` (lines ~309–325 signal SELECT + append block; after line 388 for new endpoint) - Modify: `tests/test_dev_api_interviews.py` (fixture schema + 4 test changes) ### Step 1.1: Write the four failing tests Add to `tests/test_dev_api_interviews.py`: ```python # ── Body/from_addr in signal response ───────────────────────────────────── def test_interviews_signal_includes_body_and_from_addr(client): resp = client.get("/api/interviews") assert resp.status_code == 200 jobs = {j["id"]: j for j in resp.json()} sig = jobs[1]["stage_signals"][0] # Fields must exist (may be None when DB column is NULL) assert "body" in sig assert "from_addr" in sig # ── POST /api/stage-signals/{id}/reclassify ──────────────────────────────── def test_reclassify_signal_updates_label(client, tmp_db): resp = client.post("/api/stage-signals/10/reclassify", json={"stage_signal": "positive_response"}) assert resp.status_code == 200 assert resp.json() == {"ok": True} con = sqlite3.connect(tmp_db) row = con.execute( "SELECT stage_signal FROM job_contacts WHERE id = 10" ).fetchone() con.close() assert row[0] == "positive_response" def test_reclassify_signal_invalid_label(client): resp = client.post("/api/stage-signals/10/reclassify", json={"stage_signal": "not_a_real_label"}) assert resp.status_code == 400 def test_reclassify_signal_404_for_missing_id(client): resp = client.post("/api/stage-signals/9999/reclassify", json={"stage_signal": "neutral"}) assert resp.status_code == 404 ``` - [ ] Add the four test functions above to `tests/test_dev_api_interviews.py` ### Step 1.2: Also extend `test_interviews_includes_stage_signals` to assert body/from_addr The existing test (line 64) asserts `id`, `stage_signal`, and `subject`. Add assertions for the two new fields after the existing `assert signals[0]["id"] == 10` line: ```python assert "body" in signals[0] assert "from_addr" in signals[0] ``` - [ ] Add those two lines inside `test_interviews_includes_stage_signals`, after `assert signals[0]["id"] == 10` ### Step 1.3: Update the fixture to include body and from_addr columns The `job_contacts` CREATE TABLE in the `tmp_db` fixture is missing `body TEXT` and `from_addr TEXT`. The fixture test-side schema must match the real DB. Replace the `job_contacts` CREATE TABLE block (currently `id, job_id, subject, received_at, stage_signal, suggestion_dismissed`) with: ```sql CREATE TABLE job_contacts ( id INTEGER PRIMARY KEY, job_id INTEGER, subject TEXT, received_at TEXT, stage_signal TEXT, suggestion_dismissed INTEGER DEFAULT 0, body TEXT, from_addr TEXT ); ``` - [ ] Update the `tmp_db` fixture's `job_contacts` schema to add `body TEXT` and `from_addr TEXT` ### Step 1.4: Run tests to confirm they all fail as expected ```bash cd /Library/Development/CircuitForge/peregrine/.worktrees/feature-vue-spa /devl/miniconda3/envs/job-seeker/bin/pytest tests/test_dev_api_interviews.py -v ``` Expected: 5 new/modified tests FAIL (body/from_addr not in response; reclassify endpoint 404s); existing 8 tests still PASS. - [ ] Run and confirm ### Step 1.5: Update `dev-api.py` — add body/from_addr to signal SELECT and append Find the signal SELECT query (line ~309). Replace: ```python sig_rows = db.execute( f"SELECT id, job_id, subject, received_at, stage_signal " ``` With: ```python sig_rows = db.execute( f"SELECT id, job_id, subject, received_at, stage_signal, body, from_addr " ``` Then extend the `signals_by_job` append dict (line ~319). Replace: ```python signals_by_job[sr["job_id"]].append({ "id": sr["id"], "subject": sr["subject"], "received_at": sr["received_at"], "stage_signal": sr["stage_signal"], }) ``` With: ```python signals_by_job[sr["job_id"]].append({ "id": sr["id"], "subject": sr["subject"], "received_at": sr["received_at"], "stage_signal": sr["stage_signal"], "body": sr["body"], "from_addr": sr["from_addr"], }) ``` - [ ] Apply both edits to `dev-api.py` ### Step 1.6: Add the reclassify endpoint to `dev-api.py` After the `dismiss_signal` endpoint (around line 388), add: ```python # ── POST /api/stage-signals/{id}/reclassify ────────────────────────────── VALID_SIGNAL_LABELS = { 'interview_scheduled', 'offer_received', 'rejected', 'positive_response', 'survey_received', 'neutral', 'event_rescheduled', 'unrelated', 'digest', } class ReclassifyBody(BaseModel): stage_signal: str @app.post("/api/stage-signals/{signal_id}/reclassify") def reclassify_signal(signal_id: int, body: ReclassifyBody): if body.stage_signal not in VALID_SIGNAL_LABELS: raise HTTPException(400, f"Invalid label: {body.stage_signal}") db = _get_db() result = db.execute( "UPDATE job_contacts SET stage_signal = ? WHERE id = ?", (body.stage_signal, signal_id), ) rowcount = result.rowcount db.commit() db.close() if rowcount == 0: raise HTTPException(404, "Signal not found") return {"ok": True} ``` Note: `BaseModel` is already imported via `from pydantic import BaseModel` at the top of the file — check before adding a duplicate import. - [ ] Add the endpoint and `VALID_SIGNAL_LABELS` / `ReclassifyBody` to `dev-api.py` after the dismiss endpoint ### Step 1.7: Run the full test suite to verify all tests pass ```bash /devl/miniconda3/envs/job-seeker/bin/pytest tests/test_dev_api_interviews.py -v ``` Expected: all tests PASS (13 total: 8 existing + 5 new/extended). - [ ] Run and confirm ### Step 1.8: Commit ```bash cd /Library/Development/CircuitForge/peregrine/.worktrees/feature-vue-spa git add dev-api.py tests/test_dev_api_interviews.py git commit -m "feat(signals): add body/from_addr to signal query; add reclassify endpoint" ``` - [ ] Commit --- ## Task 2: Store — add body/from_addr to StageSignal interface **Files:** - Modify: `web/src/stores/interviews.ts` (lines 5–10, `StageSignal` interface) **Why this is its own task:** Both `InterviewCard.vue` and `InterviewsView.vue` import `StageSignal`. TypeScript will error on `sig.body` / `sig.from_addr` until the interface is updated. Committing the type change first keeps Tasks 3 and 4 independently compilable. ### Step 2.1: Update `StageSignal` in `web/src/stores/interviews.ts` Replace: ```typescript export interface StageSignal { id: number // job_contacts.id — used for POST /api/stage-signals/{id}/dismiss subject: string received_at: string // ISO timestamp stage_signal: 'interview_scheduled' | 'positive_response' | 'offer_received' | 'survey_received' | 'rejected' } ``` With: ```typescript export interface StageSignal { id: number // job_contacts.id — used for POST /api/stage-signals/{id}/dismiss subject: string received_at: string // ISO timestamp stage_signal: 'interview_scheduled' | 'positive_response' | 'offer_received' | 'survey_received' | 'rejected' body: string | null // email body text; null if not available from_addr: string | null // sender address; null if not available } ``` - [ ] Edit `web/src/stores/interviews.ts` ### Step 2.2: Verify TypeScript compiles cleanly ```bash cd /Library/Development/CircuitForge/peregrine/.worktrees/feature-vue-spa/web npx vue-tsc --noEmit ``` Expected: 0 errors (the new fields are nullable so no existing code should break). - [ ] Run and confirm ### Step 2.3: Commit ```bash cd /Library/Development/CircuitForge/peregrine/.worktrees/feature-vue-spa git add web/src/stores/interviews.ts git commit -m "feat(signals): add body and from_addr to StageSignal interface" ``` - [ ] Commit --- ## Task 3: InterviewCard.vue — expand toggle, body display, reclassify chips **Files:** - Modify: `web/src/components/InterviewCard.vue` **Context:** This component is the kanban card. It shows one signal by default, and a `+N more` button for additional signals. The `bodyExpanded` ref is per-card (not per-signal) because at most one signal is visible in collapsed state. ### Step 3.1: Add `bodyExpanded` ref and `reclassifySignal` function In the `