Get Record Transcripts (DNA Insights dashboard)
Input: a file (or pasted list) of case names — CA-XXXXXXXX.
Output: one CSV row per call longer than 2:00, plus a resumable progress file.
Do NOT drive the browser for this
The manual flow the user described — login, search the case, open it, click Call History,
click the transcript icon on every call over 2:00, wait, and juggle 10 tabs — is entirely three
HTTP calls per case. All of it was verified live against prod on 2026-09-03 (see
Evidence run). Opening tabs is slower, flakier, and
buys nothing. Concurrency ("10 tabs") is the --concurrency flag.
| Manual step | What it actually is |
|---|---|
| 1. login PSS | POST /api/v1/account/login/ {username, password, account_type:"general"} → access |
| 2. search case name + open | GET /api/v1/ehealth/cases/v2/?provider=1&basic=1&offset=0&limit=10&keyword=<CASE> → data.result[], match name === <CASE> exactly → id (case uuid) + patient.id (patient uuid) |
| 3. click Call History | GET /api/v1/ehealth/patients/patient-call-history/<patient_uuid>/?reference_name=<CASE> → data.result.list_video_call[] |
| 4. click the transcript icon | POST <transcript_call.get_transcript_url> {host:"https://dashboard.dnainsights.ai", room_id:<call.room_call>} — no auth header |
| 5. wait for the new transcript | poll step 3 until transcribe_file_link_v2 is set (and updated_at moved, on a re-run) |
| 6. 10 tabs | 10 workers in one process (--concurrency) |
| 7. read the API response | already the same call as step 3 — the response IS the data source |
| 8. CSV + track the case | appended per case, progress flushed per case |
The transcript service URL is not hardcoded — it is read at runtime from
GET /api/v1/core/get-setting/?key=all_general_setting →
data.value_json.transcript_call.get_transcript_url (prod today:
https://transcript.dnainsights.ai:5002/api/v1/excute-transcript/, current_version: v2).
Run it
cd .claude/skills/get-record-transcripts-dashboard
node scripts/run-transcript-batch.mjs --list /path/to/case-names.txt --env prod
The list file can be anything — .txt one per line, a CSV column, a pasted block. Every
CA-XXXXXXXX in it is taken, in order, deduped.
| Flag | Default | Why |
|---|---|---|
--list <file> |
— | required (or --only <CASE> for a single case) |
--env dev|prod |
prod |
picks the origin and the credentials block |
--csv <path> |
state/call-records-<env>-<date>.csv |
an existing file is APPENDED to; its call ids are read first so rows never duplicate |
--state <path> |
state/progress-<env>.json |
resume file; a case marked done is skipped on re-run |
--concurrency <n> |
10 |
cases processed at once — the "10 tabs" knob |
--min-duration <s> |
120 |
the "> 2:00" filter, in seconds |
--wait-minutes <m> |
25 |
per-case cap on waiting for transcripts |
--poll-seconds <s> |
15 |
how often the call history is re-read |
--force |
off | re-transcribe calls that ALREADY have a transcript — costs a real transcription run each time, see below |
--dry-run |
off | report the plan, trigger nothing, write nothing |
Always --dry-run first on a list you have not run before. It prints, per case, how many
calls are over 2:00, how many already have transcripts, and how many would be triggered.
Report back to the user
✅ get-record-transcripts-dashboard done
Environment : prod (nhuttestpss@gkxim.com)
Cases : <n> in list — <done> done, <timeout> timed out, <error> errored, <no-calls> with no call > 2:00
CSV : <path> (<rows> rows appended)
Progress : <path>
Re-run : same command — finished cases are skipped automatically
Rules that matter
- Never
--forcewithout the user asking. Without it the script only triggers calls whosetranscribe_file_link_v2is empty — exactly what the UI allows, since a call that already has a transcript shows the view icon (fa-file-lines), not the trigger icon (fa-file-exclamation).--forcedoes something the UI cannot, and every forced call is a fresh paid transcription of a real patient call. - Credentials in
data/account.jsonare READ-ONLY. Missing or placeholder → STOP and ask. Never invent an account. - Match the case name exactly.
keyword=is a substring search;CA-ABC123can return several cases.findCase()requiresname === <CASE>and throws otherwise — do not relax it. - Do not await the trigger POST. The transcript service transcribes synchronously and the
socket stays open for minutes (60s was not enough for an 18-minute call). The script floats
the promise and polls instead. Aborting the client does not cancel the job — that is how a
timed-out client still produced a transcript in the evidence run — but keep the promise alive
anyway, because that is what the browser does and it is the only way to see a
400. - A call with no
room_callcannot be transcribed. The service readsresult/<room_id>/and answersHTTP 400 [Errno 2] No such file or directory: 'result/<id>/'.canTrigger()filters these out, as does the FE. durationis in seconds.1125renders as18:45in the panel.> 2:00is> 120.
Layout
get-record-transcripts-dashboard/
├── SKILL.md
├── data/account.json # PSS credentials per env (READ-ONLY)
├── scripts/
│ ├── dnai-api-client.mjs # login, case lookup, call history, transcript trigger
│ ├── call-selection.mjs # the > 2:00 filter + the FE's trigger gates
│ ├── csv-and-progress.mjs # CSV sink (dedupe by call id) + resumable progress + list parser
│ └── run-transcript-batch.mjs # worker pool, polling, reporting
└── state/ # CSV + progress output
CSV shape
Column order matches the user's own export (~/Downloads/call_records.csv), UTF-8 with BOM on
a fresh file:
id,case_name,date_time_start,date_time_end,video_link,transcribe_file_link_v2
a14e7ee0-…,CA-XCROJYTS,2026-08-27T19:05:15,2026-08-27T19:24:00,https://top-twillo.s3…/RE….mp3?…,https://top-twillo.s3…/final_result.json?…
id is list_video_call[].id (the call/audit-log uuid), case_name is reference_name. Both
links are pre-signed S3 URLs with an Expires param — they die after a few hours, so
download what you need soon after the export, or re-run to refresh them.
Evidence run (2026-09-03, CA-XCROJYTS)
Prod, patient 90c1af57-062f-4f16-884e-18c1b400a2cb, one conference call
a14e7ee0-6b19-4f4c-84ff-2b58d09b679f, duration: 1125 (= the 18:45 pill in the panel),
room_call: f9aeefb2-d8af-45de-814e-176b7c497949.
POST …:5002/api/v1/excute-transcript/ {host, room_id}— the client hit its own 60s timeout with no response; the job ran anyway.- The new transcript appeared ~2.5–3 min after the trigger:
updated_atmoved2026-08-28T18:06:30→2026-09-03T11:45:48and the link's date folder changedrecording-v2/2026-08-28/…→recording-v2/2026-09-03/…. Detected between the 84s and 94s poll after the timeout.transcribe_file_data_v2held 312 segments before and after. - Readiness signal used by the script:
transcribe_file_link_v2present, plus (on--force)updated_atdifferent from the pre-trigger value — a forced re-run keeps the same call id and can keep the same segment count, so the link alone is not proof of a new run.
Dead ends — do not retry these
| Attempt | Outcome |
|---|---|
| Driving the UI with Playwright | works but pointless; every step is a documented API call |
page.goto('#/?cid=<case_uuid>') to open a case |
the SPA restores its own last-opened case from storage and ignores the cid — the header showed a different case entirely |
| Search box → click card, right after login | the task list's initial unfiltered fetch lands late, resets the list and auto-selects the FIRST case, silently replacing the case just opened. Needs a settle wait, which is more moving parts than the API |
globalThis.__netlog across browser_run_code_unsafe calls |
each call is a fresh VM context; nothing persists |
require('fs') / await import('node:fs') inside browser_run_code_unsafe |
require is not defined / ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING — return data in the result instead |
| Waiting on the trigger POST response | 60s timeout, no body; the job completes regardless |
GET /api/v1/core/get-setting/?key=transcript_call |
404 — the key is nested inside all_general_setting |
Fetching …/all_general_setting.json from CloudFront |
needs a signed CloudFront policy; use the get-setting API instead |
Playwright's browser was missing at the start of that session; installed with
npx @playwright/mcp@latest install-browser chrome-for-testing (only needed if something
genuinely requires the browser — this skill does not).
Related
.claude/skills/test-submit-case-to-pending/— the PSS login + Task List quirks, if a future task really does need the UI.- FE source of truth (read-only):
prod-telehealth-provider-dashboard/src/components/PatientCallHistory/CallDetail/index.js(the transcript icon's three states),src/features/patient-call-history/services/index.ts,src/helpers/recFormApis.js→getTranscriptByRole.