Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Documentation

User-, operator-, and admin-facing documentation — installing a watcher, adding an instrument, managing tokens, deploying the web app and AWS infrastructure, security/permissions — lives on the docs site at https://arcadia-data-hub-docs.vercel.app/, **not in this repository**. Search there first for "how do I use/deploy Data Hub" questions; don't rely on training data or guess at UI flows, since the site's `/llms.txt` and `/llms-full.txt` routes (and a `.md` suffix on any page URL) serve clean Markdown that's cheap to fetch.
User-, operator-, and admin-facing documentation — installing a watcher, adding an instrument, managing tokens, deploying the web app and AWS infrastructure, security/permissions — lives on the docs site at https://datahub.arcadiascience.com/docs, **not in this repository**. Search there first for "how do I use/deploy Data Hub" questions; don't rely on training data or guess at UI flows, since the site's `/docs/llms.txt` and `/docs/llms-full.txt` routes (and a `.md` suffix on any page URL) serve clean Markdown that's cheap to fetch.

This repo's `developer-docs/` only covers contributing to Data Hub itself: architecture internals, local dev setup (`getting-started.md`, `local-development.md`), conventions, CI/deployment, and per-package references (`lambda.md`, `watcher.md`, `shared-library.md`). See `developer-docs/README.md` for the full index.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ make dev

See the full [Getting started guide](developer-docs/getting-started.md) for prerequisites and details. Don't have AWS/Google credentials? [Local development](developer-docs/local-development.md) covers a zero-credential setup for the web app + API + database alone (no watcher or Lambda needed).

Developer docs live in [developer-docs/](developer-docs/README.md). You can find user documentation (self-hosted deployment, watcher installation, adding an instrument, managing tokens) on the [docs site](https://arcadia-data-hub-docs.vercel.app/).
Developer docs live in [developer-docs/](developer-docs/README.md). You can find user documentation (self-hosted deployment, watcher installation, adding an instrument, managing tokens) on the [docs site](https://datahub.arcadiascience.com/docs).

## Checks and tests

Expand Down
2 changes: 1 addition & 1 deletion developer-docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Documentation for developing Data Hub itself. User, operator, and admin
documentation (installing a watcher, adding an instrument, managing tokens,
deployment) lives on the [docs site](https://arcadia-data-hub-docs.vercel.app/)
deployment) lives on the [docs site](https://datahub.arcadiascience.com/docs)
instead — see the root [README](../README.md#getting-started) for that split.

- [Getting started](getting-started.md) — development setup, environment variables, running locally
Expand Down
2 changes: 1 addition & 1 deletion developer-docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ Steps 1–3 are the same, but the watcher does not upload immediately. Instead:
- **Lambda-built run archives.** The "Download all" actions on a run delegate to the Lambda, which streams files from the raw + processed buckets directly into a zip in a separate `arcadia-data-hub-archives-{env}` bucket via S3 multipart upload. The web app then 302s the browser to a short-lived presigned URL on that archive — bytes never traverse Vercel, eliminating Fast Origin Transfer for run downloads. Builds are cached at `runs/{instrument_id}/{run_id}/{fingerprint}.zip` and expire after 7 days. Every cache miss is dispatched asynchronously: the route inserts an `archive_jobs` row, schedules the Lambda invocation via `after()`, returns `202 { job_id }`, and the UI polls the same `/download-archive` URL (which HEADs S3 on each call) until the artifact appears — making the S3 object, not the row's `status`, the source of truth for "ready". See [Run archives](run-archives.md) for the full flow and runbook.
- **Public page metadata, gated bodies.** Routes commonly shared into Slack/Notion (dashboard, instruments, instrument and run detail, settings) are reachable without a session so link unfurlers can read `<head>` metadata; the page or layout renders a `SignInRequired` CTA in place of the real body when there's no session. `/watchers/*` stays redirected to `/login` by `web/proxy.ts`. Three independent layers prevent search indexing: a `robots` field on the root layout, an `app/robots.ts` that disallows all generic crawlers (with an allow-list for unfurl bots), and an `X-Robots-Tag: noindex, nofollow` header on every response.
- **Shared library for contracts.** Instrument IDs, S3 utilities, and environment config live in `data-hub-shared` so they stay consistent across Lambda and the watcher without duplicating code.
- **Wildcard PAT scope for the legacy backfill.** Migration `0022_pat_scopes` backfilled every pre-existing token with `["*"]` so deployed watchers and the Lambda kept working once scope enforcement shipped. `POST /api/v1/tokens` rejects `*` from API callers, so every token minted since carries explicit least-privilege scopes; see [Security & permissions](https://arcadia-data-hub-docs.vercel.app/docs/security#token-scopes) for the scope vocabulary.
- **Wildcard PAT scope for the legacy backfill.** Migration `0022_pat_scopes` backfilled every pre-existing token with `["*"]` so deployed watchers and the Lambda kept working once scope enforcement shipped. `POST /api/v1/tokens` rejects `*` from API callers, so every token minted since carries explicit least-privilege scopes; see [Security & permissions](https://datahub.arcadiascience.com/docs/security#token-scopes) for the scope vocabulary.
- **MCP for AI access.** The web app includes a [Model Context Protocol](https://modelcontextprotocol.io/) server at `/api/v1/mcp` that exposes read-only tools, resources, and prompts. AI clients (e.g. Claude Desktop, Cursor) can query instruments, runs, and system status using a personal access token.
- **Integration tests against a real server.** The shared `testing.py` module spins up a real Next.js server backed by a Postgres database, so Lambda and watcher integration tests exercise the actual API surface.
2 changes: 1 addition & 1 deletion developer-docs/ci-and-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ make sam-deploy ENV=staging

### Watcher (PyPI)

The `data-hub-watcher` Python package is published to [PyPI](https://pypi.org/project/data-hub-watcher/) so lab PCs can install and self-update via `uv tool install data-hub-watcher`. The full release flow — version bump, tag, approval, env-var roll-out, mandatory updates, and rollback — is documented in the admin-facing [Managing watchers → Releases and fleet updates](https://arcadia-data-hub-docs.vercel.app/docs/managing-watchers#releases-and-fleet-updates) guide; this section is intentionally a pointer rather than a second source of truth so the two can't drift.
The `data-hub-watcher` Python package is published to [PyPI](https://pypi.org/project/data-hub-watcher/) so lab PCs can install and self-update via `uv tool install data-hub-watcher`. The full release flow — version bump, tag, approval, env-var roll-out, mandatory updates, and rollback — is documented in the admin-facing [Managing watchers → Releases and fleet updates](https://datahub.arcadiascience.com/docs/managing-watchers#releases-and-fleet-updates) guide; this section is intentionally a pointer rather than a second source of truth so the two can't drift.

Trusted publishing is configured under **Project → Publishing** on PyPI for `Arcadia-Science/data-hub` and the workflow `publish-watcher.yml`; no API token lives in repo secrets. If trust is ever revoked or rotated, update it there and re-run the workflow.

Expand Down
2 changes: 1 addition & 1 deletion developer-docs/lambda.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Slack channel notifications are sent by the **web app** (`web/lib/slack.ts`), no

4. **Add tests.** Add unit tests in `lambda/tests/` for the new processor.

5. **Configure the S3 trigger and deploy.** See [Deploying AWS infrastructure → Adding a Lambda processor for a new instrument](https://arcadia-data-hub-docs.vercel.app/docs/deploying-aws-infrastructure#adding-a-lambda-processor-for-a-new-instrument) for the `infra/template.yaml` trigger entry and the deploy steps.
5. **Configure the S3 trigger and deploy.** See [Deploying AWS infrastructure → Adding a Lambda processor for a new instrument](https://datahub.arcadiascience.com/docs/deploying-aws-infrastructure#adding-a-lambda-processor-for-a-new-instrument) for the `infra/template.yaml` trigger entry and the deploy steps.

## Local processing CLI

Expand Down
4 changes: 2 additions & 2 deletions developer-docs/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,5 +226,5 @@ The same builders back the integration test harness in [web/tests/integration/he

- [Getting started](getting-started.md) — full setup with real Google OAuth and AWS credentials.
- [Architecture](architecture.md) — system overview and data flow.
- [REST API](https://arcadia-data-hub-docs.vercel.app/docs/api-reference) — endpoint reference for the seeded PAT.
- [MCP server](https://arcadia-data-hub-docs.vercel.app/docs/mcp-server) — Model Context Protocol tools at `/api/v1/mcp`.
- [REST API](https://datahub.arcadiascience.com/docs/api-reference) — endpoint reference for the seeded PAT.
- [MCP server](https://datahub.arcadiascience.com/docs/mcp-server) — Model Context Protocol tools at `/api/v1/mcp`.
2 changes: 1 addition & 1 deletion developer-docs/run-archives.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The "Download all" actions on the run detail page and the runs table deliver eve

Each archive can mix files from the raw bucket and the processed bucket in a single zip. This matters for instruments that produce processed artifacts via Lambda preprocessing (SpectraMax raw `.xls` → processed CSV; Hina `.nd2` → processed JPG; Azure 600 Gel Doc `.tif` → processed PNG): the run's file rows reference both buckets, and "Download all" zips them together.

This page covers the end-to-end flow, the cache + dedup model, and the on-call runbook. For the Lambda invocation contract, see [Lambda → Function URL (archive build)](lambda.md#function-url-archive-build). For the HTTP endpoints, see [REST API → Archive jobs](https://arcadia-data-hub-docs.vercel.app/docs/api-reference#archive-jobs).
This page covers the end-to-end flow, the cache + dedup model, and the on-call runbook. For the Lambda invocation contract, see [Lambda → Function URL (archive build)](lambda.md#function-url-archive-build). For the HTTP endpoints, see [REST API → Archive jobs](https://datahub.arcadiascience.com/docs/api-reference#archive-jobs).

## Flow

Expand Down
6 changes: 3 additions & 3 deletions developer-docs/watcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ uv run data-hub-watcher init
uv run data-hub-watcher watch
```

For lab-PC installs (PyPI), see [Installing a watcher](https://arcadia-data-hub-docs.vercel.app/docs/installing-a-watcher). For releasing new versions and how the in-place upgrade flow works (CLI `self-update` and the Windows-service auto-updater), see [Upgrading the watcher](https://arcadia-data-hub-docs.vercel.app/docs/upgrading-the-watcher).
For lab-PC installs (PyPI), see [Installing a watcher](https://datahub.arcadiascience.com/docs/installing-a-watcher). For releasing new versions and how the in-place upgrade flow works (CLI `self-update` and the Windows-service auto-updater), see [Upgrading the watcher](https://datahub.arcadiascience.com/docs/upgrading-the-watcher).

## Commands

Expand Down Expand Up @@ -71,7 +71,7 @@ While running:
- **Upload worker** (manual mode only) polls the server's upload queue on its own long-lived thread every 60 seconds, decoupled from the heartbeat so a slow or large upload can't delay heartbeats and make a busy watcher look offline. On shutdown it is stopped and joined before the state DB is closed. Auto mode has no worker: uploads run on the monitor's stability-checker thread via the run detector's upload callback.
- **Heartbeat loop** sends periodic heartbeats (every 60 seconds) to the API. The payload includes the watcher version, instrument ID, watch directory, upload mode, per-interval activity counters, and process uptime; a final `status="stopped"` heartbeat is sent on graceful shutdown.
- **Event reporter** batches and flushes lifecycle events (started, stopped, file uploaded, errors) to the API. See [Observability](#observability) for the full taxonomy.
- **Auto-updater** runs from the same heartbeat tick on every platform — not only Windows services. It polls `GET /watchers/:id/update-check` roughly hourly and applies new releases when the watcher has been idle long enough not to clobber an in-flight run. The full activity-window guard, mandatory-update behavior, and rollback flow are documented in [Upgrading the watcher](https://arcadia-data-hub-docs.vercel.app/docs/upgrading-the-watcher); auto-update is hard-disabled in the `preview` environment.
- **Auto-updater** runs from the same heartbeat tick on every platform — not only Windows services. It polls `GET /watchers/:id/update-check` roughly hourly and applies new releases when the watcher has been idle long enough not to clobber an in-flight run. The full activity-window guard, mandatory-update behavior, and rollback flow are documented in [Upgrading the watcher](https://datahub.arcadiascience.com/docs/upgrading-the-watcher); auto-update is hard-disabled in the `preview` environment.

Use `--dry-run` to validate config and preview what would happen without starting the monitor.

Expand Down Expand Up @@ -126,7 +126,7 @@ Manage the watcher as a Windows service:

### `self-update`

Checks the API for a newer published version and runs the appropriate `uv tool install --reinstall` (or `pip install -U`) subprocess in place. See [Upgrading the watcher](https://arcadia-data-hub-docs.vercel.app/docs/upgrading-the-watcher) for the supported install methods, the activity-window guard, mandatory updates, and rollback flow.
Checks the API for a newer published version and runs the appropriate `uv tool install --reinstall` (or `pip install -U`) subprocess in place. See [Upgrading the watcher](https://datahub.arcadiascience.com/docs/upgrading-the-watcher) for the supported install methods, the activity-window guard, mandatory updates, and rollback flow.

## Configuration

Expand Down
2 changes: 1 addition & 1 deletion watcher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ data-hub-watcher self-update # check for and apply package updates
data-hub-watcher service install # Windows: install as a service
```

See [the operator guide](https://arcadia-data-hub-docs.vercel.app/docs/installing-a-watcher) for the full setup walk-through, configuration reference, and troubleshooting.
See [the operator guide](https://datahub.arcadiascience.com/docs/installing-a-watcher) for the full setup walk-through, configuration reference, and troubleshooting.

## License

Expand Down
2 changes: 1 addition & 1 deletion watcher/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Homepage = "https://github.com/Arcadia-Science/data-hub"
# the PyPI page actually needs. The developer-facing `developer-docs/watcher.md`
# describes the editable-checkout workflow and is reachable from the
# repository link below.
Documentation = "https://arcadia-data-hub-docs.vercel.app/docs/installing-a-watcher"
Documentation = "https://datahub.arcadiascience.com/docs/installing-a-watcher"
Repository = "https://github.com/Arcadia-Science/data-hub"
Issues = "https://github.com/Arcadia-Science/data-hub/issues"

Expand Down
7 changes: 4 additions & 3 deletions web/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Public docs site base URL. Defaults to https://arcadia-data-hub-docs.vercel.app
# when unset.
# DOCS_BASE_URL=
# Public docs site origin. Defaults to https://datahub.arcadiascience.com when
# unset; docs links are built as `<origin>/docs/...`. Must be NEXT_PUBLIC_ so
# client-rendered docs links (e.g. the sidebar user menu) pick it up.
# NEXT_PUBLIC_DOCS_BASE_URL=

# Database.
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/data-hub-local
Expand Down
3 changes: 2 additions & 1 deletion web/app/api/v1/instrument-runs/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextRequest } from "next/server";
import { authorize } from "@/lib/api/auth";
import { buildRunListQuery } from "@/lib/api/instrument-runs";
import { parseIntParam } from "@/lib/api/validators";
import { parseIntParam, parseRunStatusParam } from "@/lib/api/validators";

// ---------------------------------------------------------------------------
// GET /api/v1/instrument-runs
Expand Down Expand Up @@ -38,6 +38,7 @@ export async function GET(request: NextRequest) {
}),
includeDeleted: searchParams.get("include_deleted") === "true",
ranBy: searchParams.get("ran_by") ?? undefined,
statuses: parseRunStatusParam(searchParams),
});

return Response.json(result);
Expand Down
47 changes: 35 additions & 12 deletions web/app/api/v1/instruments/[instrumentId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { and, count, eq, isNull } from "drizzle-orm";
import type { NextRequest } from "next/server";
import { authorize, requireAdminForSession } from "@/lib/api/auth";
import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors";
import { deregisterInstrumentWatchers } from "@/lib/api/watchers";
import { db } from "@/lib/db";
import {
instrumentRuns,
Expand Down Expand Up @@ -143,6 +144,15 @@ export async function PATCH(
const updates: Record<string, unknown> = {};
if ("status" in body) {
updates.status = body.status;
// Keep the retirement audit fields in lockstep with the status: only an
// `inactive` instrument has a retirer.
if (body.status === "inactive") {
updates.retiredAt = new Date();
updates.retiredBy = authResult.userId;
} else {
updates.retiredAt = null;
updates.retiredBy = null;
}
}
if ("display_name" in body) {
updates.displayName = body.display_name;
Expand All @@ -155,18 +165,31 @@ export async function PATCH(
return apiError(400, VALIDATION_ERROR, "No valid fields to update");
}

const [updated] = await db
.update(instruments)
.set(updates)
.where(eq(instruments.id, instrumentId))
.returning({
id: instruments.id,
display_name: instruments.displayName,
status: instruments.status,
instrument_type: instruments.instrumentType,
created_at: instruments.createdAt,
updated_at: instruments.updatedAt,
});
// Retirement flips the status and tears down every watcher; both run in one
// transaction so a mid-teardown failure can't leave the instrument
// `inactive` while its watchers stay live and heartbeating.
const updated = await db.transaction(async (tx) => {
const [row] = await tx
.update(instruments)
.set(updates)
.where(eq(instruments.id, instrumentId))
.returning({
id: instruments.id,
display_name: instruments.displayName,
status: instruments.status,
instrument_type: instruments.instrumentType,
created_at: instruments.createdAt,
updated_at: instruments.updatedAt,
});

// A retired instrument has no live agent, so always tear down its watchers,
// attributing the teardown to the same actor that retired it.
if (updates.status === "inactive") {
await deregisterInstrumentWatchers(instrumentId, authResult.userId, tx);
}

return row;
});

return Response.json(updated);
}
3 changes: 2 additions & 1 deletion web/app/api/v1/instruments/[instrumentId]/runs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { authorize } from "@/lib/api/auth";
import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors";
import { buildRunListQuery, parseAcquiredAt } from "@/lib/api/instrument-runs";
import { notifyRunCreated } from "@/lib/api/notifications";
import { parseIntParam } from "@/lib/api/validators";
import { parseIntParam, parseRunStatusParam } from "@/lib/api/validators";
import { db } from "@/lib/db";
import { files, instrumentRuns, instruments, watchers } from "@/lib/db/schema";
import { sendSlackMessage } from "@/lib/slack";
Expand Down Expand Up @@ -258,6 +258,7 @@ export async function GET(request: NextRequest, { params }: RouteContext) {
}),
includeDeleted: searchParams.get("include_deleted") === "true",
ranBy: searchParams.get("ran_by") ?? undefined,
statuses: parseRunStatusParam(searchParams),
});

return Response.json(result);
Expand Down
Loading