# Get Record Transcripts Dashboard

> Collects call recordings + transcripts for a list of DNA Insights case names and exports them to CSV (id, case_name, date_time_start, date_time_end, video_link, transcribe_file_link_v2). For every call longer than 2:00 it triggers the transcript service when the case has no transcript yet, waits until the new transcript lands, then appends the row and tracks the case as done (resumable). Runs entirely over the dashboard HTTP API — no browser, no 10 tabs. Use when the user hands over a file/list of case names and asks for the call records, recordings or transcripts, or says "get record transcripts", "lấy transcript của list case này", "export call records ra csv". Invoked as `get-record-transcripts-dashboard <case-list-file>`.

- Skill: `nhutnguyengkimvn/get-record-transcripts-dashboard` (Agent Skill, multi-file: 12 files)
- Install (CLI): `npx skillmds@latest add nhutnguyengkimvn/get-record-transcripts-dashboard`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nhutnguyengkimvn/get-record-transcripts-dashboard/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: nhutnguyengkimvn (https://skillmd.com/u/nhutnguyengkimvn)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/nhutnguyengkimvn/get-record-transcripts-dashboard

---


# 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](#evidence-run-2026-09-03-ca-xcrojyts)). 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

```bash
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 `--force` without the user asking.** Without it the script only triggers calls whose
  `transcribe_file_link_v2` is 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`). `--force` does something the UI cannot, and every forced call is a
  fresh paid transcription of a real patient call.
- **Credentials in `data/account.json` are READ-ONLY.** Missing or placeholder → STOP and ask.
  Never invent an account.
- **Match the case name exactly.** `keyword=` is a substring search; `CA-ABC123` can return
  several cases. `findCase()` requires `name === <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_call` cannot be transcribed.** The service reads `result/<room_id>/`
  and answers `HTTP 400 [Errno 2] No such file or directory: 'result/<id>/'`. `canTrigger()`
  filters these out, as does the FE.
- **`duration` is in seconds.** `1125` renders as `18:45` in the panel. `> 2:00` is `> 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_at` moved
  `2026-08-28T18:06:30` → `2026-09-03T11:45:48` and the link's date folder changed
  `recording-v2/2026-08-28/…` → `recording-v2/2026-09-03/…`. Detected between the 84s and 94s
  poll after the timeout. `transcribe_file_data_v2` held 312 segments before and after.
- Readiness signal used by the script: `transcribe_file_link_v2` present, plus (on `--force`)
  `updated_at` different 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`.

