Gate 1 — Radio Check slice 1 (API spine + iOS)¶
Gate: Gate 1 sandbox OS (Radio Check spine)
Stack: R940 — api.last.vet → lastvet-edge → lastvet-api (VM 107) → lastvet-db (VM 108)
VA data: Sandbox until VA production case #00015000 clears
Scope¶
| Phase | Deliverable | Status |
|---|---|---|
| A | Migration 062, emitter, cadence, routes, tests |
API spine |
| B | iOS Home card above Your Turn, API client, Need-support 988 stub | Veteran UI |
| C | Section 2 crisis UI + detection (feature-flagged) | Built; stub restored 2026-09-16 evening (Mic go-live conditions) |
| D | Mic clinical endorsement → flip crisis flag | Clinical content endorsed; live flip rolled back until validation — see CRISIS_LIVE_FLIP_LOG.md + CRISIS_PRE_GO_LIVE_VALIDATION.md |
| E | LastVet OS → last1.app bridge (org + member visibility) | Tracked; not urgent — see RADIO_CHECK_SLICE_2_LAST1APP_BRIDGE.md |
Crisis discipline (non-negotiable)¶
- No crisis free text on server.
radio_check_events.payloadallowlist rejectsnote,crisis_text,response_text, etc. Postgres CHECK is defense-in-depth. need_supportis a categorical enum only — not a log of distress content (CRISIS_PROTOCOL.md§5).LASTVET_CRISIS_LIVE_ENABLED=false(restored 2026-09-16 evening). Need support → 988 stub until Mic pre-go-live validation is green. Artifact scoped: clinical content endorsed, go-live conditional. SeeCRISIS_LIVE_FLIP_LOG.md+CRISIS_PRE_GO_LIVE_VALIDATION.md.- Flag-off UX: Need support → honest 988 stub (never hide the button).
Canonical protocol: lastvet-gates/CRISIS_PROTOCOL.md
Environment flags (R940)¶
| Variable | Default | Meaning |
|---|---|---|
RADIO_CHECK_OS_ENABLED |
true |
Master kill-switch for Radio Check routes |
LASTVET_CRISIS_LIVE_ENABLED |
false (restored 2026-09-16 evening) |
Full Section 2 handoff; re-flip only after Mic validation checklist |
Set in Bitwarden → /etc/lastvet-api/lastvet-api-{staging,prod}.env on lastvet-api VM 107.
Crisis flip/rollback: CRISIS_LIVE_FLIP_LOG.md. Pre-go-live: CRISIS_PRE_GO_LIVE_VALIDATION.md. Do not set MERIDIAN_M1_ELIGIBLE=true until Ryan GO for live Service Matches labeling. Counsel is not a gate. Meridian Ask grounded answers already use the on-device synced VA cache and do not require this flag.
Migration (staging first, then prod)¶
lastvet_master from LAN only (192.168.1.91). See lastvet-gates/docs/runbooks/r940-prod-deploy.md.
Staging¶
ssh lastvet-api 'sudo bash -c "
set -a && source /etc/lastvet-api/lastvet-api-staging.env && set +a
export MIGRATION_DATABASE_URL=\"\${MIGRATION_DATABASE_URL//lastvet-db.tailc3bbdc.ts.net/192.168.1.91}\"
cd /opt/last.vet-api
git pull origin main
node scripts/migrate.js --list-pending
node scripts/migrate.js --expect 062_radio_check_os.sql
"'
Production¶
Snapshot on lastvet-db before prod schema change (see r940-prod-deploy.md §5).
ssh lastvet-api 'sudo bash -c "
export LASTVET_ALLOW_R940_PROD_DEPLOY=1
set -a && source /etc/lastvet-api/lastvet-api-prod.env && set +a
export MIGRATION_DATABASE_URL=\"\${MIGRATION_DATABASE_URL//lastvet-db.tailc3bbdc.ts.net/192.168.1.91}\"
cd /opt/last.vet-api
git pull origin main
node scripts/migrate.js --list-pending
node scripts/migrate.js --expect 062_radio_check_os.sql
"'
API deploy (VM 107)¶
After migrate + env flags:
- Pull
mainon lastvet-api at/opt/last.vet-api npm ci && npm run build- Restart
lastvet-api-staging/lastvet-apisystemd units (per existing R940 deploy runbook)
API surface¶
| Method | Path | Body |
|---|---|---|
GET |
/api/veteran/me/radio-check/status |
— |
PUT |
/api/veteran/me/radio-check/settings |
{ enabled, dayOfWeek, hourLocal, minuteLocal, timezone } |
POST |
/api/veteran/me/radio-check/respond |
{ response: "ok" \| "struggling" \| "need_support" } only |
Defaults: weekly (7 days), Sunday 10:00 local, America/Los_Angeles, enabled. Status includes settings.summaryLabel and activeVeteran (≥3 of trailing 4 ISO weeks with a response).
409 if veteran already responded this ISO week.
Post-deploy verification¶
# Session exchange (synthetic veteran on R940)
eval "$(bws secret list -o env | rg 'LASTVET_R940_PROD_VETERAN_SESSION_EXCHANGE_SECRET=')"
TOKEN=$(curl -s -X POST "https://api.last.vet/api/auth/veteran/session" \
-H "Content-Type: application/json" \
-H "x-veteran-session-exchange: $VETERAN_SESSION_EXCHANGE_SECRET" \
-d '{"vaPatientId":"2000163"}' | jq -r '.accessToken')
curl -s "https://api.last.vet/api/veteran/me/radio-check/status" \
-H "Authorization: Bearer $TOKEN" | jq '{osEnabled, crisisLiveHandoffEnabled, settings, activeVeteran, dueNow}'
curl -s -X POST "https://api.last.vet/api/veteran/me/radio-check/respond" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"response":"ok"}' | jq .
# Free text must fail (400 invalid_request — strict schema rejects extra keys)
curl -s -o /dev/null -w "%{http_code}" -X POST "https://api.last.vet/api/veteran/me/radio-check/respond" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"response":"ok","note":"crisis text"}'
# Expect 400
Expect: crisisLiveHandoffEnabled: false, osEnabled: true, transparent settings labels.
Ops — reset current ISO week (crisis stub device test)¶
Use case: Veteran already responded this week (409 already_responded_this_cycle) but must verify Need support → minimal 988 stub on device. One response per ISO week is by design.
Table: radio_check_events has append-only rules (protect_radio_check_events_no_delete). Deletes require lastvet_master (migration URL), not the app role.
- Resolve the veteran row by Last1-linked id (not stale VA sandbox row):
SELECT id, last1_identity_id::text
FROM veterans
WHERE last1_identity_id IS NOT NULL
ORDER BY created_at DESC;
- Confirm current cycle responses:
SELECT id, cycle_key, occurred_at, payload
FROM radio_check_events
WHERE veteran_id = '<veteran_uuid>'
AND event_type = 'check_in_responded'
ORDER BY occurred_at DESC;
- Delete only the target cycle (prod — Ryan Last1 veteran
8daac85a-87fb-428f-bc16-738bbfd3504f, cycle2026-W38cleared 2026-09-15 for crisis-stub retest):
ssh lastvet-api 'sudo bash -c "
set -a && source /etc/lastvet-api/lastvet-api-prod.env && set +a
psql \"\${MIGRATION_DATABASE_URL//lastvet-db.tailc3bbdc.ts.net/192.168.1.91}\" -c \"
BEGIN;
ALTER TABLE radio_check_events DISABLE RULE protect_radio_check_events_no_delete;
DELETE FROM radio_check_events
WHERE veteran_id = '\''<veteran_uuid>'\''
AND event_type = '\''check_in_responded'\''
AND cycle_key = '\''<YYYY-Www>'\'';
ALTER TABLE radio_check_events ENABLE RULE protect_radio_check_events_no_delete;
COMMIT;
\""
- On device: pull to refresh Home → Radio Check should show buttons again → tap Need support → confirm minimal stub (not legacy handoff). See
last.vet-ios/docs/testflight/BUILD_77_RADIO_CHECK.md§Need support.
Note: Stale VA-session veteran rows may hold older test rows; production Last1 login uses the row with last1_identity_id set.
Related¶
-
docs/runbooks/gate-1/RADIO_CHECK_SLICE_2_LAST1APP_BRIDGE.md(next slice — last1.app visibility, WIN-relevant, not urgent) -
lastvet-gates/gates/GATE_1_sovereign_profile_and_os.md lastvet-gates/gates/GATE_1_TRACTION_THRESHOLD.md§3 (active veteran)lastvet-gates/LOOP_LEDGER_AND_PROOF_MACHINE_SPEC.md(one clock)lastvet-gates/CRISIS_PROTOCOL.md