Transfer-status card pattern — bulk-copy dashboards for >60min jobs (parked 2026-05-25)
DARE.CO.UK · PARKED SKETCH · 2026-05-26
Mirrored from ~/.claude/.../memory/parked_sketch_transfer_status_card_pattern_2026-05-25.md. This is a design sketch parked for future build — read for context, not as a current deliverable.
When a bulk copy/migration is expected to run over 60 minutes (RAID ingest, Takeout import, multi-TB rclone, drive-to-drive archive), emit a status card to status.gf.cx/transfers/ with src/dst/throughput/eta/verify fields. Skip the dashboard for short copies. First real instance will be the Immich 4TB→RAID migration; pattern emerged from in-flight 4TB-Evernote archive 2026-05-25.
Dan 2026-05-25: “Short durations dont really need it, but anything over 60 min, feels like a win”
Build trigger
Build the status card emitter + render WHEN ANY:
- A planned bulk-copy job’s ETA exceeds 60 minutes (current rate × bytes-to-move). Don’t build for sub-60-min jobs.
- The Immich 4TB→RAID migration becomes imminent (this is the first job that definitely crosses the threshold)
- A multi-day Takeout import is queued
- Any rclone sync over ~200 GB
Below the threshold (Evernote resource-cache at ~100 GB / 24 min, 4K Stogram at 52 GB / 35 min), running progress reporting in the chat is enough — no surface needed.
Card axes (what each transfer surfaces)
| Field | Why it matters | Example |
|---|---|---|
| name | URL slug + ledger key | 4tb-evernote-archive · raid-ingest-dare-takeout |
| kind | Drives badge color + filter; one of archive (delete src) · migration (delete src after period) · sync (keep both) · restore |
archive |
| src | Where data is moving FROM, with volume context | ~/Library/.../Evernote/resource-cache (internal SSD) |
| dst | Where it’s going, with volume + filesystem | /Volumes/4TB/archive-from-mac-.../... (APFS, ext) |
| state | Lifecycle machine: queued → starting → copying → verifying → done · or failed · paused |
copying |
| bytes_done / bytes_total | Percent + remaining | 45.0 GB / 100 GB (45%) |
| throughput_inst | Last sample MB/s | 70 MB/s |
| throughput_sustained | Median over last N samples | 65 MB/s |
| interface | What’s actually carrying the bits — load-bearing for Immich planning | USB-C (cable-swap 2026-05-25) · TB4 · USB 2.0 |
| eta_seconds | Live (bytes_remaining / throughput_sustained) | ~21 min |
| verify | Post-copy: file count match · size delta · sha256 sample (optional) | 19,311/19,311 files · Δ +22MB · 100 samples ok |
| started_at / finished_at | UTC, ISO-8601 | 2026-05-25T15:42:11Z · – |
| interrupted / resume_count | Track flakiness across reconnects | 0 |
| operator | Who kicked it off (manual / launchd / claude-code-session-id) | claude:d8367dda-... |
Throughput log — the load-bearing side-effect
Append-only ledger per (drive + cable + filesystem) combination — this is what informs Immich pre-flight:
2026-05-25 4TB-USB-C-old-cable 22 MB/s sustained (4K Stogram 52GB)
2026-05-25 4TB-USB-C-better-cable 65 MB/s sustained (Evernote 100GB)
2026-XX-XX TB4-enclosure-RAID ??? MB/s sustained (first 100GB synthetic)
2026-XX-XX TB4-enclosure-RAID ??? MB/s sustained (Takeout dare 200GB)
This is the “empirical throughput data” referenced in parked_sketch_audrey_4tb_photo_library_to_r2_2026-05-24.md — the dashboard is its natural home, not a markdown note.
Two-file architecture (mirrors pa_job_status_update.py shape)
~/bin/transfer_status_update.py
# CLI: start | progress | verify | done | fail
# Writes JSON to ~/pa.gf.cx/data/transfers/<slug>.json
# Appends throughput sample to ~/pa.gf.cx/data/transfers/<slug>.samples.jsonl
~/pa.gf.cx/transfers/render.py
# Reads all <slug>.json from data/
# Emits status.gf.cx/transfers/index.html — active grid + archive ledger
# Per-job page status.gf.cx/transfers/<slug>/ — full timeline + samples chart
CLI shape
transfer_status_update.py start \
--slug 4tb-evernote-archive \
--kind archive \
--src "~/Library/.../Evernote/resource-cache" \
--dst "/Volumes/4TB/archive-from-mac-2026-05-25/evernote-resource-cache" \
--bytes-total $(du -sk SRC | awk '{print $1*1024}') \
--interface "USB-C-better-cable" \
--operator "claude:d8367dda"
# Every 90s during copy:
transfer_status_update.py progress \
--slug 4tb-evernote-archive \
--bytes-done $(du -sk DST | awk '{print $1*1024}')
# After ditto exits:
transfer_status_update.py verify --slug 4tb-evernote-archive \
--src-files 19311 --dst-files 19311 --delta-kb 22912
transfer_status_update.py done --slug 4tb-evernote-archive
Render shape — sibling to existing status.gf.cx hub
| Surface | What it shows |
|---|---|
status.gf.cx/transfers/ |
Active transfers grid (1 card each) + archive ledger (last 50, paginated) |
status.gf.cx/transfers/<slug>/ |
Full timeline: started → samples chart → verified → done; 60min+ jobs only |
status.gf.cx/ (the meta-hub) |
Adds a transfers row to the 4-line registry — transfers · last completed 2hr ago · 3 active |
Card visual (sketch — uses existing cards primitive)
┌─────────────────────────────────────────────┐
│ ARCHIVE · copying │
│ ┌─────────────────────────────────────┐ │
│ │ 4tb-evernote-archive │ │
│ └─────────────────────────────────────┘ │
│ │
│ internal SSD ──▶ 4TB external (USB-C) │
│ Library/.../resource-cache │
│ → archive-from-mac-2026-05-25/... │
│ │
│ ▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░ 45.0 GB / 100 GB │
│ 65 MB/s sustained · ETA 14 min │
│ │
│ started 15:42:11 · sample 12 of ~50 │
└─────────────────────────────────────────────┘
Match the pa scope-icon library (heraldic SVGs) — use a “freight” or “anchor” icon for transfers.
Why park (not build now)
- No active >60min job in flight right now — the in-flight Evernote copy is sub-30-min so the dashboard is overkill
- First real >60min job will be the Immich/RAID work — building the card surface alongside that real workload validates the pattern with stakes
- Throughput-log is the durable artefact — even without the surface, every transfer should leave a samples.jsonl row so the log accrues; the visualisation layer is the part we defer
Pre-build checklist when the time comes
- Decide: are throughput samples written to
~/pa.gf.cx/data/transfers/or to a new domain (e.g.infra.gf.cx/transfers/)?pa.is personal-admin scope; bulk-copy ops might warrant their own infra bucket - ditto vs rsync vs rclone — each has a different progress-emit shape;
transfer_status_update.py progressshould be invocable by any of them viadu -sk DSTpolling - Failure detection: a copy that’s silent for >5 min while ditto is still alive is suspicious — emit
pausedstate - Verify is non-trivial for large jobs — sha256 sampling, not full-tree (computing 4TB of hashes takes hours)
- Throughput-log keying —
(filesystem_src, filesystem_dst, interface, cable_label_if_known)— let Dan annotate the cable label at start
Cross-references
feedback_ingest_dashboard_pattern.md— parent pattern (this is its bulk-copy specialisation)feedback_status_gfcx_meta_dashboard_pattern.md— where the cards renderparked_sketch_audrey_4tb_photo_library_to_r2_2026-05-24.md— the source of the “empirical throughput data matters” insight; this dashboard is the natural home for that logfeedback_pa_scope_icon_asset_library.md— needs a freight/anchor icon addedfeedback_state_emerges_from_data.md— card state derives from sample timestamps + bytes_done, not manual flags
The aphorism
Build the dashboard when you’d already be staring at a progress bar for an hour. Below that, terminal updates are dashboard enough. The valuable side-effect — the throughput ledger per drive + cable — accrues either way.