Skip to content

Runs: Surface actual on-instrument acquisition time (acquired_at) - #62

Merged
wasimxyz merged 8 commits into
stagingfrom
wa/run-created-at
May 8, 2026
Merged

Runs: Surface actual on-instrument acquisition time (acquired_at)#62
wasimxyz merged 8 commits into
stagingfrom
wa/run-created-at

Conversation

@wasimxyz

@wasimxyz wasimxyz commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

Adds an acquired_at timestamp on instrument_runs populated by the watcher from the minimum file_created_at across a run's files. This lets backfilled runs (data that already existed on the PC when a watcher first connects) appear with their true acquisition time instead of "now". The list query default sort, date filter, and dashboard time windows switch to coalesce(acquired_at, created_at), and the run detail header shows both "Run started" and "Reported".

Changes

  • Schema: nullable instrument_runs.acquired_at TIMESTAMPTZ plus idx_instrument_runs_active_acquired_at (partial expression index on coalesce(acquired_at, created_at) desc). Migration 0020_add_acquired_at.sql includes a one-time backfill from files.file_created_at.
  • API:
    • POST /runs accepts acquired_at, falls back to min(detected_files[].file_created_at), and folds late watcher-vs-lambda race writes via LEAST(coalesce(acquired_at, $1), $1).
    • PATCH /runs/:id applies the same LEAST semantics so the value can only move earlier; GET/PATCH responses now expose acquired_at.
  • Queries: buildRunListQuery default sort and dateFrom/dateTo filter use coalesce(acquired_at, created_at). lookupRunByNaturalKey, dashboard.ts (lastRunAt, 24h/7d windows), and sidebar.ts updated likewise. MCP search_runs sort enum extended.
  • Watcher: RunDetector tracks acquired_at_sent on RunState, sends acquired_at (ISO UTC) on _report_new_run, and PATCHes only when a later-stable file reveals an earlier birthtime. Hydration seeds the cursor from the persisted manifest.
  • UI: All seven run-list table variants render row.acquired_at ?? row.created_at in the existing "Created" cell. RunHeader shows "Run started …" (when present), "Reported …", "Updated …".
  • Tests: New integration block in instrument-runs.test.ts covers explicit-POST, derived-from-files POST, earlier-PATCH, later-PATCH ignored, default coalesce sort. New TestAcquiredAt class in test_run_detector_hydration.py covers the helper, POST payload shape, the earlier-PATCH path, and the monotonic no-PATCH path.

Breaking changes

None. acquired_at is nullable and all sort/filter call sites coalesce to created_at for runs predating the watcher backfill or coming from the Lambda. Existing API callers that omit acquired_at are unaffected.

Driveby changes

  • npm audit fix bump in web-app/package-lock.json (0027c9d, unrelated to this feature).

Testing

  • make check-all (ruff, pyright, prettier, eslint, tsc) — clean.
  • make py-test — watcher unit tests, including new TestAcquiredAt cases.
  • make fe-testinstrument-runs.test.ts integration tests, including new Run acquired_at block.
  • Apply migration 0020_add_acquired_at.sql against staging and spot-check that pre-existing runs have acquired_at populated from the file manifest.
  • Manual: run detail header shows both "Run started" and "Reported"; list "Created" column reflects acquisition time for watcher-reported runs and falls back to insert time for Lambda-only runs.
  • Manual: roll out updated watcher to a PC with historical files, confirm new runs POST with acquired_at and that out-of-order stable files PATCH it earlier.

wasimxyz and others added 2 commits May 8, 2026 11:08
Adds a nullable `acquired_at` column to `instrument_runs` populated by
the watcher from `min(file_created_at)` over a run's files. The watcher
sends it on POST and on PATCHes that introduce an earlier file; the API
applies LEAST(coalesce(...), incoming) so the value can only move
earlier. The list query default sort and date filter switch to
`coalesce(acquired_at, created_at)` so backfilled runs (e.g. data
discovered when a new instrument is first added) interleave with fresh
runs by their true acquisition time. The run detail header now shows
both "Run started" and "Reported" timestamps.

Includes a one-time backfill that derives `acquired_at` from the
existing `files.file_created_at` manifest for runs reported by older
watchers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@wasimxyz wasimxyz self-assigned this May 8, 2026
@vercel

vercel Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
data-hub Ready Ready Preview, Comment May 8, 2026 8:03pm

Request Review

drizzle's gte/lte rely on the column's PgColumn mapper to serialize a JS
Date for the postgres-js driver. With a raw SQL fragment as the LHS that
mapper is bypassed, so the driver received a Date and threw
ERR_INVALID_ARG_TYPE on every list query that bound a date_from/date_to.
Bind ISO strings explicitly and cast to timestamptz on the server, and
add a regression test that calls buildRunListQuery with a date range.

Co-authored-by: Cursor <cursoragent@cursor.com>
wasimxyz and others added 2 commits May 8, 2026 12:37
The `LEAST(coalesce(...), $1)` SQL in the POST race-conflict path and the
PATCH `acquired_at` update bound a JS Date to a raw sql fragment, which
hits the same postgres-js Date->string coercion bug as the
date_from/date_to filter — same fix: bind the ISO string explicitly and
cast to timestamptz. The integration regression test now hits the API
via date_from/date_to (previously imported buildRunListQuery directly,
which broke because integration test workers don't share the app's
@/lib/db singleton).

date_from/date_to query params are now exposed on
GET /api/v1/instruments/:id/runs and GET /api/v1/instrument-runs,
matching what the page-level RSCs already pass to buildRunListQuery.

Co-authored-by: Cursor <cursoragent@cursor.com>
`buildRunCountSubquery` was the last query still anchored to created_at,
so backfilled runs (acquired weeks ago, but reported today) were both
showing up as the most recent activity and being counted in the
this-week window. Switch to coalesce(acquired_at, created_at) like the
dashboard, sidebar, and list query.

Co-authored-by: Cursor <cursoragent@cursor.com>
The cell now renders coalesce(acquired_at, created_at), so the previous
"Created" header was misleading — sorting and date-filtering use the
run's actual on-instrument acquisition time, not the row's creation
time. Adds a shared AcquiredColumnHeader (mirroring RawFileColumnHeader)
with a tooltip explaining the fallback to created_at for older or
Lambda-created runs, and wires it into all six per-instrument table
variants and the dashboard table.

Co-authored-by: Cursor <cursoragent@cursor.com>
@wasimxyz
wasimxyz merged commit 5a62df1 into staging May 8, 2026
6 checks passed
@wasimxyz
wasimxyz deleted the wa/run-created-at branch May 8, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant