Skip to content

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.payload allowlist rejects note, crisis_text, response_text, etc. Postgres CHECK is defense-in-depth.
  • need_support is 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. See CRISIS_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:

  1. Pull main on lastvet-api at /opt/last.vet-api
  2. npm ci && npm run build
  3. Restart lastvet-api-staging / lastvet-api systemd 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.

  1. 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;
  1. 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;
  1. Delete only the target cycle (prod — Ryan Last1 veteran 8daac85a-87fb-428f-bc16-738bbfd3504f, cycle 2026-W38 cleared 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;
\""
  1. 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.


  • 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