From 61459ff076f2d89c7533b6c78815141327280f0b Mon Sep 17 00:00:00 2001 From: Wasim Sandhu Date: Mon, 22 Jun 2026 15:03:46 -0700 Subject: [PATCH 1/9] web: migrate from Prettier + ESLint to Ultracite + Biome Replace the Prettier + ESLint toolchain with Ultracite + Biome. Adds biome.jsonc (extends ultracite core/react/next presets) and editor settings, removes the ESLint/Prettier configs and devDependencies, and rewires the lint/format scripts. Applies Biome's safe autofixes across the app; remaining lint violations are addressed in follow-up commits. Co-authored-by: Cursor --- web/.prettierignore | 7 - web/.prettierrc | 11 - web/.vscode/settings.json | 54 + .../api/local-s3/[bucket]/[...key]/route.ts | 24 +- web/app/api/v1/archive-jobs/[id]/route.ts | 29 +- .../api/v1/files/[fileId]/download/route.ts | 12 +- .../api/v1/files/[fileId]/reprocess/route.ts | 8 +- web/app/api/v1/files/[fileId]/route.ts | 33 +- web/app/api/v1/instrument-runs/route.ts | 6 +- .../v1/instruments/[instrumentId]/route.ts | 28 +- .../runs/[runId]/attributions/me/route.ts | 12 +- .../[runId]/comments/[commentId]/route.ts | 10 +- .../runs/[runId]/comments/route.ts | 12 +- .../runs/[runId]/download-archive/route.ts | 28 +- .../runs/[runId]/files/route.ts | 10 +- .../runs/[runId]/reprocess/route.ts | 8 +- .../runs/[runId]/request-upload-all/route.ts | 8 +- .../runs/[runId]/request-upload-url/route.ts | 8 +- .../runs/[runId]/request-upload/route.ts | 8 +- .../runs/[runId]/restore/route.ts | 8 +- .../[instrumentId]/runs/[runId]/route.ts | 16 +- .../instruments/[instrumentId]/runs/route.ts | 12 +- web/app/api/v1/instruments/route.ts | 12 +- web/app/api/v1/mcp/route.ts | 8 +- web/app/api/v1/notifications/route.ts | 12 +- .../instruments/[instrumentId]/route.ts | 10 +- .../api/v1/settings/notifications/route.ts | 12 +- .../api/v1/settings/watcher-release/route.ts | 18 +- web/app/api/v1/tokens/[id]/route.ts | 6 +- web/app/api/v1/tokens/route.ts | 8 +- web/app/api/v1/users/[userId]/route.ts | 8 +- web/app/api/v1/users/route.ts | 6 +- .../[watcherId]/config-checksum/route.ts | 6 +- .../v1/watchers/[watcherId]/config/route.ts | 8 +- .../v1/watchers/[watcherId]/events/route.ts | 14 +- .../watchers/[watcherId]/heartbeat/route.ts | 8 +- .../watchers/[watcherId]/heartbeats/route.ts | 8 +- web/app/api/v1/watchers/[watcherId]/route.ts | 12 +- .../[watcherId]/update-check/route.ts | 6 +- .../[watcherId]/upload-queue/route.ts | 8 +- web/app/api/v1/watchers/register/route.ts | 8 +- web/app/api/v1/watchers/route.ts | 8 +- web/app/globals.css | 222 +- web/app/instruments/[instrumentId]/page.tsx | 28 +- .../[instrumentId]/runs/[runId]/page.tsx | 32 +- web/app/instruments/page.tsx | 6 +- web/app/layout.tsx | 16 +- web/app/login/page.tsx | 4 +- web/app/page.tsx | 18 +- web/app/settings/layout.tsx | 2 +- web/app/settings/members/page.tsx | 16 +- web/app/settings/notifications/page.tsx | 16 +- web/app/settings/page.tsx | 2 +- web/app/settings/tokens/page.tsx | 28 +- web/app/settings/watchers/page.tsx | 14 +- web/app/watchers/[watcherId]/page.tsx | 18 +- web/app/watchers/page.tsx | 4 +- web/biome.jsonc | 8 + .../app-sidebar/app-sidebar-content.tsx | 2 +- web/components/app-sidebar/index.tsx | 20 +- web/components/app-sidebar/main-nav.tsx | 26 +- web/components/app-sidebar/settings-nav.tsx | 8 +- .../app-sidebar/user-menu-footer.tsx | 14 +- web/components/auth/dev-sign-in-form.tsx | 10 +- web/components/auth/sign-in-required.tsx | 2 +- web/components/code-block.tsx | 2 +- web/components/copy-button.tsx | 14 +- web/components/dashboard/dashboard-stats.tsx | 20 +- web/components/dashboard/relative-time.tsx | 2 +- web/components/dashboard/runs-table.tsx | 24 +- web/components/dashboard/runs-toolbar.tsx | 38 +- .../instruments/edit-instrument-dialog.tsx | 26 +- .../instruments/instrument-header.tsx | 20 +- .../instruments/instrument-runs-toolbar.tsx | 20 +- .../instruments/instruments-table.tsx | 20 +- .../instruments/row-actions-cell.tsx | 2 +- .../runs-table/acquired-column-header.tsx | 4 +- .../instruments/runs-table/clickable-row.tsx | 2 +- .../runs-table/default-runs-table.tsx | 14 +- .../runs-table/epson-scanner-runs-table.tsx | 24 +- .../runs-table/filterable-column-header.tsx | 12 +- .../runs-table/gel-doc-runs-table.tsx | 36 +- .../runs-table/hina-runs-table.tsx | 22 +- .../instruments/runs-table/index.tsx | 8 +- .../instruments/runs-table/metadata-utils.tsx | 6 +- .../runs-table/plate-reader-runs-table.tsx | 32 +- .../runs-table/qpcr-runs-table.tsx | 20 +- .../instruments/runs-table/ran-by-cell.tsx | 76 +- .../runs-table/raw-file-column-header.tsx | 4 +- .../runs-table/run-bulk-action-bar.tsx | 78 +- .../instruments/runs-table/run-id-label.tsx | 10 +- .../runs-table/run-row-actions.tsx | 63 +- .../runs-table/run-select-checkbox.tsx | 8 +- .../runs-table/run-selection-provider.tsx | 8 +- .../runs-table/run-status-icon.tsx | 14 +- .../runs-table/runs-table-footer.tsx | 2 +- web/components/instruments/status-actions.tsx | 10 +- web/components/members/admin-toggle.tsx | 14 +- web/components/members/members-table.tsx | 10 +- .../instrument-notification-switch.tsx | 14 +- .../instrument-notifications-cell.tsx | 2 +- .../notification-bell-content.tsx | 98 +- .../notifications/notification-bell.tsx | 18 +- .../notifications/notifications-provider.tsx | 28 +- .../notifications-settings-form.tsx | 36 +- web/components/pagination-nav.tsx | 35 +- .../runs/archive-download-dialog.tsx | 46 +- .../runs/archive-download-provider.tsx | 14 +- web/components/runs/colony-data-table.tsx | 82 +- web/components/runs/comment-markdown.tsx | 20 +- web/components/runs/delete-run-dialog.tsx | 26 +- web/components/runs/delete-runs-dialog.tsx | 26 +- web/components/runs/file-bulk-action-bar.tsx | 40 +- .../runs/file-selection-provider.tsx | 18 +- web/components/runs/file-status-badge.tsx | 2 +- web/components/runs/hina-report-section.tsx | 36 +- web/components/runs/metadata-badges.tsx | 68 +- web/components/runs/plate-map-grid.tsx | 82 +- web/components/runs/raman-report-section.tsx | 8 +- web/components/runs/raman-spectrum-viewer.tsx | 148 +- web/components/runs/reprocess-runs-dialog.tsx | 16 +- web/components/runs/restore-run-button.tsx | 8 +- .../runs/run-attributions-section.tsx | 2 +- web/components/runs/run-comment-form.tsx | 26 +- web/components/runs/run-comment-item.tsx | 60 +- web/components/runs/run-comments-list.tsx | 30 +- web/components/runs/run-comments-section.tsx | 6 +- web/components/runs/run-detail.ts | 2 +- web/components/runs/run-files-section.tsx | 108 +- web/components/runs/run-files-table.tsx | 70 +- web/components/runs/run-filters-combobox.tsx | 20 +- web/components/runs/run-header.tsx | 10 +- web/components/runs/run-metadata-badges.tsx | 144 +- web/components/runs/run-metadata.tsx | 2 +- web/components/runs/run-report-section.tsx | 32 +- .../runs/runs-date-filter-calendar.tsx | 22 +- web/components/runs/runs-date-filter.tsx | 56 +- .../runs/variants/default-run-detail.tsx | 12 +- .../variants/epson-scanner-run-detail.tsx | 12 +- .../runs/variants/gel-doc-run-detail.tsx | 12 +- .../variants/hina-microscope-run-detail.tsx | 12 +- .../variants/instant-raman-run-detail.tsx | 12 +- .../runs/variants/plate-reader-run-detail.tsx | 58 +- .../runs/variants/qpcr-run-detail.tsx | 12 +- .../runs/variants/tape-station-run-detail.tsx | 12 +- .../runs/watcher-gated-upload-button.tsx | 4 +- .../runs/watcher-status-provider.tsx | 2 +- web/components/table-pending.tsx | 8 +- web/components/theme-provider.tsx | 4 +- web/components/tokens/create-token-dialog.tsx | 70 +- web/components/tokens/delete-token-dialog.tsx | 14 +- web/components/ui/accordion.tsx | 19 +- web/components/ui/alert-dialog.tsx | 34 +- web/components/ui/alert.tsx | 14 +- web/components/ui/avatar.tsx | 24 +- web/components/ui/badge.tsx | 6 +- web/components/ui/breadcrumb.tsx | 29 +- web/components/ui/button.tsx | 20 +- web/components/ui/calendar.tsx | 105 +- web/components/ui/card.tsx | 24 +- web/components/ui/carousel.tsx | 43 +- web/components/ui/chart.tsx | 20 +- web/components/ui/checkbox.tsx | 11 +- web/components/ui/command.tsx | 31 +- web/components/ui/dialog.tsx | 27 +- web/components/ui/dropdown-menu.tsx | 55 +- web/components/ui/field.tsx | 40 +- web/components/ui/input-group.tsx | 26 +- web/components/ui/input.tsx | 8 +- web/components/ui/label.tsx | 6 +- web/components/ui/pagination.tsx | 25 +- web/components/ui/popover.tsx | 14 +- web/components/ui/scroll-area.tsx | 16 +- web/components/ui/select.tsx | 37 +- web/components/ui/separator.tsx | 8 +- web/components/ui/sheet.tsx | 27 +- web/components/ui/sidebar.tsx | 171 +- web/components/ui/skeleton.tsx | 2 +- web/components/ui/slider.tsx | 18 +- web/components/ui/sonner.tsx | 2 +- web/components/ui/switch.tsx | 10 +- web/components/ui/table.tsx | 24 +- web/components/ui/tabs.tsx | 20 +- web/components/ui/textarea.tsx | 6 +- web/components/ui/toggle-group.tsx | 24 +- web/components/ui/toggle.tsx | 6 +- web/components/ui/tooltip.tsx | 8 +- .../watcher-release/watcher-release-form.tsx | 50 +- web/components/watchers/deregister-dialog.tsx | 16 +- web/components/watchers/event-log-toolbar.tsx | 24 +- web/components/watchers/event-log.tsx | 20 +- web/components/watchers/heartbeat-chart.tsx | 58 +- web/components/watchers/status-toolbar.tsx | 16 +- web/components/watchers/watcher-config.tsx | 10 +- .../watchers/watcher-detail-tabs.tsx | 20 +- web/components/watchers/watcher-header.tsx | 16 +- .../watchers/watcher-online-status.ts | 4 +- .../watchers/watcher-status-badge.tsx | 4 +- web/components/watchers/watchers-table.tsx | 110 +- web/components/watchers/watchers-view.tsx | 8 +- web/drizzle/meta/0000_snapshot.json | 140 +- web/drizzle/meta/0001_snapshot.json | 145 +- web/drizzle/meta/0002_snapshot.json | 145 +- web/drizzle/meta/0003_snapshot.json | 141 +- web/drizzle/meta/0004_snapshot.json | 125 +- web/drizzle/meta/0005_snapshot.json | 125 +- web/drizzle/meta/0006_snapshot.json | 127 +- web/drizzle/meta/0007_snapshot.json | 148 +- web/drizzle/meta/0008_snapshot.json | 140 +- web/drizzle/meta/0009_snapshot.json | 140 +- web/drizzle/meta/0010_snapshot.json | 140 +- web/drizzle/meta/0011_snapshot.json | 140 +- web/drizzle/meta/0012_snapshot.json | 140 +- web/drizzle/meta/0013_snapshot.json | 140 +- web/drizzle/meta/0014_snapshot.json | 140 +- web/drizzle/meta/0015_snapshot.json | 156 +- web/drizzle/meta/0016_snapshot.json | 179 +- web/drizzle/meta/0017_snapshot.json | 179 +- web/drizzle/meta/0018_snapshot.json | 179 +- web/drizzle/meta/0019_snapshot.json | 179 +- web/drizzle/meta/0020_snapshot.json | 179 +- web/drizzle/meta/0021_snapshot.json | 179 +- web/drizzle/meta/0022_snapshot.json | 179 +- web/drizzle/meta/0023_snapshot.json | 179 +- web/drizzle/meta/0024_snapshot.json | 187 +- web/drizzle/meta/0025_snapshot.json | 254 +- web/drizzle/meta/_journal.json | 2 +- web/eslint.config.mjs | 18 - web/hooks/use-archive-download.ts | 4 +- web/lib/admin-emails.ts | 12 +- web/lib/api/archive-builder.ts | 14 +- web/lib/api/archive-jobs.ts | 2 +- web/lib/api/auth.ts | 10 +- web/lib/api/dashboard.ts | 4 +- web/lib/api/file-reprocessing.ts | 10 +- web/lib/api/files.ts | 8 +- web/lib/api/instrument-runs.ts | 43 +- web/lib/api/instruments.ts | 24 +- web/lib/api/notifications.ts | 32 +- web/lib/api/run-archive.ts | 6 +- web/lib/api/run-comments.ts | 6 +- web/lib/api/sidebar.ts | 4 +- web/lib/api/validators.ts | 22 +- web/lib/api/watcher-versions.ts | 28 +- web/lib/api/watchers.ts | 24 +- web/lib/auth.ts | 14 +- web/lib/avatar-color.ts | 8 +- web/lib/db/schema.ts | 5 +- web/lib/db/seed.ts | 56 +- web/lib/lambda.ts | 2 +- web/lib/mcp/prompts.ts | 114 +- web/lib/mcp/resources.ts | 4 +- web/lib/mcp/tools.ts | 84 +- web/lib/s3-local-mirror.ts | 4 +- web/lib/s3.ts | 28 +- web/lib/utils.ts | 32 +- web/next.config.mjs | 4 +- web/package-lock.json | 7820 +++++------------ web/package.json | 13 +- web/postcss.config.mjs | 4 +- web/proxy.ts | 2 +- web/scripts/process-fixtures.ts | 30 +- web/scripts/seed-database.ts | 4 +- web/tests/integration/archive-jobs.test.ts | 4 +- web/tests/integration/auth.test.ts | 2 +- .../integration/download-archive.test.ts | 2 +- web/tests/integration/files.test.ts | 4 +- web/tests/integration/global-setup.ts | 6 +- web/tests/integration/helpers.ts | 18 +- web/tests/integration/instrument-runs.test.ts | 2 +- web/tests/integration/instruments.test.ts | 2 +- web/tests/integration/mcp.test.ts | 8 +- web/tests/integration/notifications.test.ts | 8 +- .../integration/request-upload-url.test.ts | 2 +- .../request-upload-watcher-offline.test.ts | 4 +- .../integration/run-attributions.test.ts | 2 +- web/tests/integration/run-comments.test.ts | 2 +- web/tests/integration/scopes.test.ts | 2 +- .../upload-request-cancellation.test.ts | 4 +- web/tests/integration/users.test.ts | 2 +- web/tests/integration/watcher-release.test.ts | 4 +- web/tests/integration/watchers.test.ts | 2 +- web/tests/mcp/mcp-protocol.test.ts | 16 +- web/tests/unit/estimate-retry-after.test.ts | 2 +- web/tests/unit/scope-validation.test.ts | 38 +- web/vitest.config.ts | 2 +- web/vitest.integration.config.ts | 2 +- 287 files changed, 6502 insertions(+), 10984 deletions(-) delete mode 100644 web/.prettierignore delete mode 100644 web/.prettierrc create mode 100644 web/.vscode/settings.json create mode 100644 web/biome.jsonc delete mode 100644 web/eslint.config.mjs diff --git a/web/.prettierignore b/web/.prettierignore deleted file mode 100644 index 461b0084..00000000 --- a/web/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -dist/ -node_modules/ -.next/ -.turbo/ -coverage/ -pnpm-lock.yaml -.pnpm-store/ \ No newline at end of file diff --git a/web/.prettierrc b/web/.prettierrc deleted file mode 100644 index 031af2c9..00000000 --- a/web/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "endOfLine": "lf", - "semi": true, - "singleQuote": false, - "tabWidth": 2, - "trailingComma": "es5", - "printWidth": 80, - "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"], - "tailwindStylesheet": "app/globals.css", - "tailwindFunctions": ["cn", "cva"] -} diff --git a/web/.vscode/settings.json b/web/.vscode/settings.json new file mode 100644 index 00000000..4d00146a --- /dev/null +++ b/web/.vscode/settings.json @@ -0,0 +1,54 @@ +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "emmet.showExpandedAbbreviation": "never", + "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[graphql]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[html]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[markdown]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[mdx]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[svelte]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[vue]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[yaml]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + } +} diff --git a/web/app/api/local-s3/[bucket]/[...key]/route.ts b/web/app/api/local-s3/[bucket]/[...key]/route.ts index db39add0..5cff5cb1 100644 --- a/web/app/api/local-s3/[bucket]/[...key]/route.ts +++ b/web/app/api/local-s3/[bucket]/[...key]/route.ts @@ -13,18 +13,18 @@ // on the Edge runtime and there's no production deployment story for // this route anyway. -import { - getLocalMirrorRoot, - mimeFor, - resolveMirrorPath, -} from "@/lib/s3-local-mirror"; -import type { NextRequest } from "next/server"; import { createReadStream, createWriteStream } from "node:fs"; import { mkdir, stat } from "node:fs/promises"; import path from "node:path"; import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; import type { ReadableStream as NodeWebReadableStream } from "node:stream/web"; +import type { NextRequest } from "next/server"; +import { + getLocalMirrorRoot, + mimeFor, + resolveMirrorPath, +} from "@/lib/s3-local-mirror"; type RouteContext = { params: Promise<{ bucket: string; key: string[] }>; @@ -34,7 +34,9 @@ const NOT_FOUND_RESPONSE = () => new Response("Not Found", { status: 404 }); export async function GET(request: NextRequest, { params }: RouteContext) { const root = getLocalMirrorRoot(); - if (!root) return NOT_FOUND_RESPONSE(); + if (!root) { + return NOT_FOUND_RESPONSE(); + } const { bucket, key } = await params; const joinedKey = key.join("/"); @@ -50,7 +52,9 @@ export async function GET(request: NextRequest, { params }: RouteContext) { let fileSize: number; try { const s = await stat(filePath); - if (!s.isFile()) return NOT_FOUND_RESPONSE(); + if (!s.isFile()) { + return NOT_FOUND_RESPONSE(); + } fileSize = s.size; } catch { return NOT_FOUND_RESPONSE(); @@ -79,7 +83,9 @@ export async function GET(request: NextRequest, { params }: RouteContext) { export async function PUT(request: NextRequest, { params }: RouteContext) { const root = getLocalMirrorRoot(); - if (!root) return NOT_FOUND_RESPONSE(); + if (!root) { + return NOT_FOUND_RESPONSE(); + } const { bucket, key } = await params; const joinedKey = key.join("/"); diff --git a/web/app/api/v1/archive-jobs/[id]/route.ts b/web/app/api/v1/archive-jobs/[id]/route.ts index 632d5621..19ca723e 100644 --- a/web/app/api/v1/archive-jobs/[id]/route.ts +++ b/web/app/api/v1/archive-jobs/[id]/route.ts @@ -1,10 +1,10 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { isValidUUID } from "@/lib/api/validators"; import { db } from "@/lib/db"; import { archiveJobs } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ id: string }>; @@ -41,7 +41,9 @@ type PatchBody = { export async function PATCH(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "archive-jobs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { id } = await params; if (!isValidUUID(id)) { @@ -68,17 +70,16 @@ export async function PATCH(request: NextRequest, { params }: RouteContext) { const status = body.status as "pending" | "building" | "ready" | "failed"; - if (status === "ready") { - if ( - typeof body.archive_bucket !== "string" || - typeof body.archive_key !== "string" - ) { - return apiError( - 400, - VALIDATION_ERROR, - "archive_bucket and archive_key are required when status is 'ready'" - ); - } + if ( + status === "ready" && + (typeof body.archive_bucket !== "string" || + typeof body.archive_key !== "string") + ) { + return apiError( + 400, + VALIDATION_ERROR, + "archive_bucket and archive_key are required when status is 'ready'" + ); } const update: Partial = { status }; diff --git a/web/app/api/v1/files/[fileId]/download/route.ts b/web/app/api/v1/files/[fileId]/download/route.ts index f1db206e..617665e7 100644 --- a/web/app/api/v1/files/[fileId]/download/route.ts +++ b/web/app/api/v1/files/[fileId]/download/route.ts @@ -1,10 +1,10 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { db } from "@/lib/db"; import { files, instrumentRuns } from "@/lib/db/schema"; import { getPresignedDownloadUrl } from "@/lib/s3"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ fileId: string }>; @@ -20,10 +20,12 @@ type RouteContext = { export async function GET(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "files:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { fileId } = await params; - const numericId = parseInt(fileId, 10); + const numericId = Number.parseInt(fileId, 10); if (isNaN(numericId)) { return apiError(400, VALIDATION_ERROR, "Invalid file ID"); } @@ -57,7 +59,7 @@ export async function GET(request: NextRequest, { params }: RouteContext) { return apiError(404, NOT_FOUND, `File '${fileId}' not found`); } - if (!file.s3Bucket || !file.s3Key) { + if (!(file.s3Bucket && file.s3Key)) { return apiError(404, NOT_FOUND, "File has not been uploaded to S3 yet"); } diff --git a/web/app/api/v1/files/[fileId]/reprocess/route.ts b/web/app/api/v1/files/[fileId]/reprocess/route.ts index 791a1c2b..359a67bf 100644 --- a/web/app/api/v1/files/[fileId]/reprocess/route.ts +++ b/web/app/api/v1/files/[fileId]/reprocess/route.ts @@ -1,7 +1,7 @@ +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, VALIDATION_ERROR } from "@/lib/api/errors"; import { reprocessFile } from "@/lib/api/file-reprocessing"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ fileId: string }>; @@ -18,10 +18,12 @@ type RouteContext = { export async function POST(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "files:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { fileId } = await params; - const numericId = parseInt(fileId, 10); + const numericId = Number.parseInt(fileId, 10); if (isNaN(numericId)) { return apiError(400, VALIDATION_ERROR, "Invalid file ID"); } diff --git a/web/app/api/v1/files/[fileId]/route.ts b/web/app/api/v1/files/[fileId]/route.ts index 55eec1d4..f682f6c2 100644 --- a/web/app/api/v1/files/[fileId]/route.ts +++ b/web/app/api/v1/files/[fileId]/route.ts @@ -1,3 +1,5 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -7,8 +9,6 @@ import { } from "@/lib/api/errors"; import { db } from "@/lib/db"; import { files, instrumentRuns } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ fileId: string }>; @@ -43,10 +43,12 @@ const VALID_TRANSITIONS: Record = { export async function PATCH(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "files:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { fileId } = await params; - const numericId = parseInt(fileId, 10); + const numericId = Number.parseInt(fileId, 10); if (isNaN(numericId)) { return apiError(400, VALIDATION_ERROR, "Invalid file ID"); } @@ -93,7 +95,7 @@ export async function PATCH(request: NextRequest, { params }: RouteContext) { // Status transition validation. if ("status" in body && typeof body.status === "string") { const allowed = VALID_TRANSITIONS[file.status]; - if (!allowed || !allowed.includes(body.status)) { + if (!(allowed && allowed.includes(body.status))) { return apiError( 409, CONFLICT, @@ -126,11 +128,18 @@ export async function PATCH(request: NextRequest, { params }: RouteContext) { } // S3 info — set when transitioning to "uploaded" (watcher path). - if (typeof body.s3_bucket === "string") updates.s3Bucket = body.s3_bucket; - if (typeof body.s3_key === "string") updates.s3Key = body.s3_key; - if (typeof body.content_type === "string") + if (typeof body.s3_bucket === "string") { + updates.s3Bucket = body.s3_bucket; + } + if (typeof body.s3_key === "string") { + updates.s3Key = body.s3_key; + } + if (typeof body.content_type === "string") { updates.contentType = body.content_type; - if (typeof body.size_bytes === "number") updates.sizeBytes = body.size_bytes; + } + if (typeof body.size_bytes === "number") { + updates.sizeBytes = body.size_bytes; + } // Metadata — flat JSON object set by the Lambda after processing. if ( @@ -190,10 +199,12 @@ export async function PATCH(request: NextRequest, { params }: RouteContext) { export async function DELETE(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "files:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { fileId } = await params; - const numericId = parseInt(fileId, 10); + const numericId = Number.parseInt(fileId, 10); if (isNaN(numericId)) { return apiError(400, VALIDATION_ERROR, "Invalid file ID"); } diff --git a/web/app/api/v1/instrument-runs/route.ts b/web/app/api/v1/instrument-runs/route.ts index 3f1dde3b..fcfee38a 100644 --- a/web/app/api/v1/instrument-runs/route.ts +++ b/web/app/api/v1/instrument-runs/route.ts @@ -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 type { NextRequest } from "next/server"; // --------------------------------------------------------------------------- // GET /api/v1/instrument-runs @@ -13,7 +13,9 @@ import type { NextRequest } from "next/server"; export async function GET(request: NextRequest) { const authResult = await authorize(request, "runs:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { searchParams } = request.nextUrl; diff --git a/web/app/api/v1/instruments/[instrumentId]/route.ts b/web/app/api/v1/instruments/[instrumentId]/route.ts index ef5305ee..4893c298 100644 --- a/web/app/api/v1/instruments/[instrumentId]/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/route.ts @@ -1,3 +1,5 @@ +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 { db } from "@/lib/db"; @@ -7,15 +9,15 @@ import { VALID_INSTRUMENT_TYPES, watchers, } from "@/lib/db/schema"; -import { and, count, eq, isNull } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function GET( request: NextRequest, { params }: { params: Promise<{ instrumentId: string }> } ) { const authResult = await authorize(request, "instruments:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId } = await params; @@ -71,14 +73,18 @@ export async function PATCH( { params }: { params: Promise<{ instrumentId: string }> } ) { const authResult = await authorize(request, "instruments:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } // Browser callers (the Edit dialog and the "Confirm pending" button on // `/instruments`) must additionally be admins. PAT callers — the watcher // CLI and Lambda — pass through purely on the `instruments:write` scope // so existing automation continues to work without rotation. const adminGate = await requireAdminForSession(authResult); - if (adminGate) return adminGate; + if (adminGate) { + return adminGate; + } const { instrumentId } = await params; @@ -135,9 +141,15 @@ export async function PATCH( } const updates: Record = {}; - if ("status" in body) updates.status = body.status; - if ("display_name" in body) updates.displayName = body.display_name; - if ("instrument_type" in body) updates.instrumentType = body.instrument_type; + if ("status" in body) { + updates.status = body.status; + } + if ("display_name" in body) { + updates.displayName = body.display_name; + } + if ("instrument_type" in body) { + updates.instrumentType = body.instrument_type; + } if (Object.keys(updates).length === 0) { return apiError(400, VALIDATION_ERROR, "No valid fields to update"); diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/attributions/me/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/attributions/me/route.ts index 86ddb7d0..e37a79b2 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/attributions/me/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/attributions/me/route.ts @@ -1,3 +1,5 @@ +import { and, eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND } from "@/lib/api/errors"; import { @@ -6,8 +8,6 @@ import { } from "@/lib/api/instrument-runs"; import { db } from "@/lib/db"; import { runAttributions } from "@/lib/db/schema"; -import { and, eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ instrumentId: string; runId: string }>; @@ -24,7 +24,9 @@ type RouteContext = { export async function PUT(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); @@ -58,7 +60,9 @@ export async function PUT(request: NextRequest, { params }: RouteContext) { export async function DELETE(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/comments/[commentId]/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/comments/[commentId]/route.ts index 1b1e01c1..7c17f5bb 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/comments/[commentId]/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/comments/[commentId]/route.ts @@ -1,3 +1,4 @@ +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -12,7 +13,6 @@ import { softDeleteComment, updateComment, } from "@/lib/api/run-comments"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ @@ -100,7 +100,9 @@ async function preflight( export async function PATCH(request: NextRequest, { params }: RouteContext) { const pre = await preflight(request, params); - if (pre.kind === "error") return pre.response; + if (pre.kind === "error") { + return pre.response; + } let payload: Record; try { @@ -150,7 +152,9 @@ export async function PATCH(request: NextRequest, { params }: RouteContext) { export async function DELETE(request: NextRequest, { params }: RouteContext) { const pre = await preflight(request, params); - if (pre.kind === "error") return pre.response; + if (pre.kind === "error") { + return pre.response; + } await softDeleteComment({ commentId: pre.commentId, diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/comments/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/comments/route.ts index 0eb6e2d3..78c07f1c 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/comments/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/comments/route.ts @@ -1,3 +1,5 @@ +import type { NextRequest } from "next/server"; +import { after } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -8,8 +10,6 @@ import { import { lookupRunByNaturalKey } from "@/lib/api/instrument-runs"; import { notifyComment } from "@/lib/api/notifications"; import { createComment, listCommentsForRun } from "@/lib/api/run-comments"; -import type { NextRequest } from "next/server"; -import { after } from "next/server"; type RouteContext = { params: Promise<{ instrumentId: string; runId: string }>; @@ -28,7 +28,9 @@ const MAX_BODY_LENGTH = 10_000; export async function GET(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); @@ -54,7 +56,9 @@ export async function GET(request: NextRequest, { params }: RouteContext) { export async function POST(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/download-archive/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/download-archive/route.ts index 2d1e451c..200e3114 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/download-archive/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/download-archive/route.ts @@ -1,3 +1,4 @@ +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, INTERNAL_ERROR, NOT_FOUND } from "@/lib/api/errors"; import { @@ -6,7 +7,6 @@ import { lookupRunByNaturalKey, } from "@/lib/api/instrument-runs"; import { prepareRunArchive } from "@/lib/api/run-archive"; -import type { NextRequest } from "next/server"; const FILES_STATUS_VALUES: ReadonlySet = new Set([ "all", @@ -23,7 +23,7 @@ function parseStatusParam(value: string | null): FilesStatusFilter | undefined { if (value && FILES_STATUS_VALUES.has(value as FilesStatusFilter)) { return value as FilesStatusFilter; } - return undefined; + return; } type RouteContext = { @@ -48,12 +48,16 @@ export const maxDuration = 300; // translates that into the same 404 a non-matching id list would. function parseFileIdsParam(searchParams: URLSearchParams): number[] | null { const raw = searchParams.getAll("file_ids"); - if (raw.length === 0) return null; + if (raw.length === 0) { + return null; + } const ids = new Set(); for (const entry of raw) { for (const part of entry.split(",")) { const n = Number.parseInt(part.trim(), 10); - if (Number.isInteger(n) && n > 0) ids.add(n); + if (Number.isInteger(n) && n > 0) { + ids.add(n); + } } } return Array.from(ids); @@ -71,7 +75,9 @@ async function resolveFileIdsFilter( runId: string ): Promise { const explicit = parseFileIdsParam(request.nextUrl.searchParams); - if (explicit !== null) return explicit; + if (explicit !== null) { + return explicit; + } const sp = request.nextUrl.searchParams; const search = sp.get("search")?.trim() || undefined; @@ -82,13 +88,17 @@ async function resolveFileIdsFilter( search !== undefined || (status !== undefined && status !== "all") || includeDismissed; - if (!hasFilter) return null; + if (!hasFilter) { + return null; + } // `lookupRunByNaturalKey` is request-cached, so this doesn't double the // lookup `prepareRunArchive` performs. A missing run falls through to the // 404 that helper raises. const run = await lookupRunByNaturalKey(instrumentId, runId); - if (!run) return null; + if (!run) { + return null; + } return getFilteredFileIds(run.id, { search, status, includeDismissed }); } @@ -122,7 +132,9 @@ async function resolveFileIdsFilter( // --------------------------------------------------------------------------- export async function GET(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "files:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/files/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/files/route.ts index d8e237ec..4ca4af43 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/files/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/files/route.ts @@ -1,3 +1,5 @@ +import { and, eq, isNull } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -8,8 +10,6 @@ import { import { lookupRunByNaturalKey } from "@/lib/api/instrument-runs"; import { db } from "@/lib/db"; import { files } from "@/lib/db/schema"; -import { and, eq, isNull } from "drizzle-orm"; -import type { NextRequest } from "next/server"; // Statuses where a row is "pre-S3" — safe for the Lambda path to overwrite // when adopting a watcher-created row. Anything beyond uploaded is left @@ -33,7 +33,9 @@ type RouteContext = { export async function POST(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "files:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); @@ -63,7 +65,7 @@ export async function POST(request: NextRequest, { params }: RouteContext) { const filename = typeof body.filename === "string" ? body.filename.trim() : ""; - if (!s3Bucket || !s3Key || !filename) { + if (!(s3Bucket && s3Key && filename)) { return apiError( 400, VALIDATION_ERROR, diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/reprocess/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/reprocess/route.ts index 632f735e..6560d6a3 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/reprocess/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/reprocess/route.ts @@ -1,11 +1,11 @@ +import { and, eq, inArray, isNull } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, CONFLICT, NOT_FOUND } from "@/lib/api/errors"; import { reprocessFile } from "@/lib/api/file-reprocessing"; import { lookupRunByNaturalKey } from "@/lib/api/instrument-runs"; import { db } from "@/lib/db"; import { files } from "@/lib/db/schema"; -import { and, eq, inArray, isNull } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ instrumentId: string; runId: string }>; @@ -25,7 +25,9 @@ const REPROCESSABLE_STATUSES = ["completed", "failed"] as const; export async function POST(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload-all/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload-all/route.ts index 5bc3495b..25ea4f77 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload-all/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload-all/route.ts @@ -1,3 +1,5 @@ +import { and, eq, isNull } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -9,8 +11,6 @@ import { lookupRunByNaturalKey } from "@/lib/api/instrument-runs"; import { instrumentHasOnlineWatcher } from "@/lib/api/instruments"; import { db } from "@/lib/db"; import { files, instrumentRuns } from "@/lib/db/schema"; -import { and, eq, isNull } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ instrumentId: string; runId: string }>; @@ -27,7 +27,9 @@ type RouteContext = { export async function POST(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload-url/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload-url/route.ts index f73a1d39..d56ce58b 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload-url/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload-url/route.ts @@ -1,3 +1,5 @@ +import { and, eq, isNull } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -10,8 +12,6 @@ import { lookupRunByNaturalKey } from "@/lib/api/instrument-runs"; import { db } from "@/lib/db"; import { files } from "@/lib/db/schema"; import { getPresignedUploadUrl, getS3RawDataBucket } from "@/lib/s3"; -import { and, eq, isNull } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ instrumentId: string; runId: string }>; @@ -37,7 +37,9 @@ const UPLOADED_OR_LATER_STATUSES = new Set([ export async function POST(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload/route.ts index 7f3c880d..6b87aa1d 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/request-upload/route.ts @@ -1,3 +1,5 @@ +import { eq, inArray } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -10,8 +12,6 @@ import { lookupRunByNaturalKey } from "@/lib/api/instrument-runs"; import { instrumentHasOnlineWatcher } from "@/lib/api/instruments"; import { db } from "@/lib/db"; import { files, instrumentRuns } from "@/lib/db/schema"; -import { eq, inArray } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ instrumentId: string; runId: string }>; @@ -29,7 +29,9 @@ type RouteContext = { export async function POST(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/restore/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/restore/route.ts index d451dab5..1b829b6b 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/restore/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/restore/route.ts @@ -1,10 +1,10 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, CONFLICT, NOT_FOUND } from "@/lib/api/errors"; import { lookupRunByNaturalKey } from "@/lib/api/instrument-runs"; import { db } from "@/lib/db"; import { instrumentRuns } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ instrumentId: string; runId: string }>; @@ -20,7 +20,9 @@ type RouteContext = { export async function POST(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/route.ts index 3573ec75..b7a5a1b1 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/[runId]/route.ts @@ -1,3 +1,5 @@ +import { eq, sql } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -12,8 +14,6 @@ import { import { db } from "@/lib/db"; import { files, instrumentRuns } from "@/lib/db/schema"; import { getPresignedDownloadUrl } from "@/lib/s3"; -import { eq, sql } from "drizzle-orm"; -import type { NextRequest } from "next/server"; type RouteContext = { params: Promise<{ instrumentId: string; runId: string }>; @@ -28,7 +28,9 @@ type RouteContext = { export async function GET(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); @@ -102,7 +104,9 @@ export async function GET(request: NextRequest, { params }: RouteContext) { export async function PATCH(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); @@ -223,7 +227,9 @@ export async function PATCH(request: NextRequest, { params }: RouteContext) { export async function DELETE(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); diff --git a/web/app/api/v1/instruments/[instrumentId]/runs/route.ts b/web/app/api/v1/instruments/[instrumentId]/runs/route.ts index c90588c8..58ee0856 100644 --- a/web/app/api/v1/instruments/[instrumentId]/runs/route.ts +++ b/web/app/api/v1/instruments/[instrumentId]/runs/route.ts @@ -1,3 +1,5 @@ +import { and, eq, isNull, sql } from "drizzle-orm"; +import { after, type NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { buildRunListQuery, parseAcquiredAt } from "@/lib/api/instrument-runs"; @@ -6,8 +8,6 @@ import { parseIntParam } from "@/lib/api/validators"; import { db } from "@/lib/db"; import { files, instrumentRuns, instruments, watchers } from "@/lib/db/schema"; import { sendSlackMessage } from "@/lib/slack"; -import { and, eq, isNull, sql } from "drizzle-orm"; -import { after, type NextRequest } from "next/server"; type RouteContext = { params: Promise<{ instrumentId: string }>; @@ -26,7 +26,9 @@ type RouteContext = { export async function POST(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId } = await params; @@ -216,7 +218,9 @@ export async function POST(request: NextRequest, { params }: RouteContext) { export async function GET(request: NextRequest, { params }: RouteContext) { const authResult = await authorize(request, "runs:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { instrumentId } = await params; diff --git a/web/app/api/v1/instruments/route.ts b/web/app/api/v1/instruments/route.ts index a40a8b80..fa21ea7b 100644 --- a/web/app/api/v1/instruments/route.ts +++ b/web/app/api/v1/instruments/route.ts @@ -1,3 +1,5 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, CONFLICT, VALIDATION_ERROR } from "@/lib/api/errors"; import { isValidKebabCase } from "@/lib/api/validators"; @@ -7,12 +9,12 @@ import { instruments, VALID_INSTRUMENT_TYPES, } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function GET(request: NextRequest) { const authResult = await authorize(request, "instruments:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const rows = await db .select({ @@ -28,7 +30,9 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { const authResult = await authorize(request, "instruments:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } let body: { id?: string; display_name?: string; instrument_type?: string }; try { diff --git a/web/app/api/v1/mcp/route.ts b/web/app/api/v1/mcp/route.ts index a8ce4913..ae7ca38f 100644 --- a/web/app/api/v1/mcp/route.ts +++ b/web/app/api/v1/mcp/route.ts @@ -1,9 +1,9 @@ +import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; +import { createMcpHandler, withMcpAuth } from "mcp-handler"; import { authenticateWithToken } from "@/lib/api/auth"; import { registerPrompts } from "@/lib/mcp/prompts"; import { registerResources } from "@/lib/mcp/resources"; import { registerTools } from "@/lib/mcp/tools"; -import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; -import { createMcpHandler, withMcpAuth } from "mcp-handler"; const handler = createMcpHandler( (server) => { @@ -35,7 +35,9 @@ const verifyToken = async ( bearerToken?: string ): Promise => { const result = await authenticateWithToken(req); - if (!result) return undefined; + if (!result) { + return; + } return { token: bearerToken ?? "", diff --git a/web/app/api/v1/notifications/route.ts b/web/app/api/v1/notifications/route.ts index 0152165f..46a8ec4a 100644 --- a/web/app/api/v1/notifications/route.ts +++ b/web/app/api/v1/notifications/route.ts @@ -1,3 +1,5 @@ +import type { NextRequest } from "next/server"; +import { z } from "zod"; import { requireSession } from "@/lib/api/auth"; import { apiError, UNAUTHORIZED, VALIDATION_ERROR } from "@/lib/api/errors"; import { @@ -5,8 +7,6 @@ import { listNotifications, markRead, } from "@/lib/api/notifications"; -import type { NextRequest } from "next/server"; -import { z } from "zod"; // Notification reads/writes are session-only — these are personal-UX // surfaces, never invoked by the watcher / Lambda PATs, so they don't @@ -26,7 +26,9 @@ const PostBodySchema = z.object({ export async function GET(request: NextRequest) { const auth = await requireSession(); - if (!auth) return apiError(401, UNAUTHORIZED, "Authentication required"); + if (!auth) { + return apiError(401, UNAUTHORIZED, "Authentication required"); + } const unreadOnly = request.nextUrl.searchParams.get("unread_only") === "true"; const [items, unreadCount] = await Promise.all([ @@ -64,7 +66,9 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { const auth = await requireSession(); - if (!auth) return apiError(401, UNAUTHORIZED, "Authentication required"); + if (!auth) { + return apiError(401, UNAUTHORIZED, "Authentication required"); + } let raw: unknown = {}; // Empty body is fine — it means "mark all". Only error on outright diff --git a/web/app/api/v1/settings/notifications/instruments/[instrumentId]/route.ts b/web/app/api/v1/settings/notifications/instruments/[instrumentId]/route.ts index ae4148d9..02befec5 100644 --- a/web/app/api/v1/settings/notifications/instruments/[instrumentId]/route.ts +++ b/web/app/api/v1/settings/notifications/instruments/[instrumentId]/route.ts @@ -1,3 +1,6 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; +import { z } from "zod"; import { requireSession } from "@/lib/api/auth"; import { apiError, @@ -8,9 +11,6 @@ import { import { setInstrumentSubscription } from "@/lib/api/notifications"; import { db } from "@/lib/db"; import { instruments } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; -import { z } from "zod"; const PutBodySchema = z.object({ enabled: z.boolean() }).strict(); @@ -29,7 +29,9 @@ type RouteContext = { export async function PUT(request: NextRequest, { params }: RouteContext) { const auth = await requireSession(); - if (!auth) return apiError(401, UNAUTHORIZED, "Authentication required"); + if (!auth) { + return apiError(401, UNAUTHORIZED, "Authentication required"); + } const { instrumentId } = await params; diff --git a/web/app/api/v1/settings/notifications/route.ts b/web/app/api/v1/settings/notifications/route.ts index fdda3040..0d55d9fe 100644 --- a/web/app/api/v1/settings/notifications/route.ts +++ b/web/app/api/v1/settings/notifications/route.ts @@ -1,3 +1,5 @@ +import type { NextRequest } from "next/server"; +import { z } from "zod"; import { requireSession } from "@/lib/api/auth"; import { apiError, UNAUTHORIZED, VALIDATION_ERROR } from "@/lib/api/errors"; import { @@ -5,8 +7,6 @@ import { listInstrumentSubscriptions, updatePreferences, } from "@/lib/api/notifications"; -import type { NextRequest } from "next/server"; -import { z } from "zod"; // PUT body is a partial: every key is optional and only present fields // are written. Defaults live on the column, so a missing key on a fresh @@ -30,7 +30,9 @@ const PutBodySchema = z export async function GET() { const auth = await requireSession(); - if (!auth) return apiError(401, UNAUTHORIZED, "Authentication required"); + if (!auth) { + return apiError(401, UNAUTHORIZED, "Authentication required"); + } const [prefs, subscriptions] = await Promise.all([ getPreferences(auth.userId), @@ -59,7 +61,9 @@ export async function GET() { export async function PUT(request: NextRequest) { const auth = await requireSession(); - if (!auth) return apiError(401, UNAUTHORIZED, "Authentication required"); + if (!auth) { + return apiError(401, UNAUTHORIZED, "Authentication required"); + } let raw: unknown; try { diff --git a/web/app/api/v1/settings/watcher-release/route.ts b/web/app/api/v1/settings/watcher-release/route.ts index 0c573212..393d0bc7 100644 --- a/web/app/api/v1/settings/watcher-release/route.ts +++ b/web/app/api/v1/settings/watcher-release/route.ts @@ -1,10 +1,10 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; +import { z } from "zod"; import { requireAdmin } from "@/lib/api/auth"; import { apiError, VALIDATION_ERROR } from "@/lib/api/errors"; import { db } from "@/lib/db"; import { users, watcherReleaseConfig } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; -import { z } from "zod"; // Admin-only read/write of the singleton `watcher_release_config` row, // edited via `/settings/watchers`. The `update-check` endpoint reads @@ -23,7 +23,9 @@ const VERSION_REGEX = /^\d+\.\d+\.\d+([.-].+)?$/; // means unset" everywhere — operators don't have to remember to send // `null` instead of `""`. Shared by both version fields. function normalizeVersionInput(v: string | null | undefined): string | null { - if (v == null) return null; + if (v == null) { + return null; + } const trimmed = v.trim(); return trimmed.length === 0 ? null : trimmed; } @@ -124,14 +126,18 @@ async function readCurrent(): Promise { export async function GET() { const authResult = await requireAdmin(); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } return Response.json(await readCurrent()); } export async function PUT(request: NextRequest) { const authResult = await requireAdmin(); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } let rawBody: unknown; try { diff --git a/web/app/api/v1/tokens/[id]/route.ts b/web/app/api/v1/tokens/[id]/route.ts index 62995cc5..67d92b91 100644 --- a/web/app/api/v1/tokens/[id]/route.ts +++ b/web/app/api/v1/tokens/[id]/route.ts @@ -1,8 +1,8 @@ +import { eq } from "drizzle-orm"; import { requireAdmin } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { db } from "@/lib/db"; import { personalAccessTokens } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; export async function DELETE( _request: Request, @@ -13,7 +13,9 @@ export async function DELETE( // tokens during an audit. The previous owner-scoped delete made // multi-user revocation impossible from the UI. const authResult = await requireAdmin(); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { id } = await params; diff --git a/web/app/api/v1/tokens/route.ts b/web/app/api/v1/tokens/route.ts index 041ef6d4..44d5fd23 100644 --- a/web/app/api/v1/tokens/route.ts +++ b/web/app/api/v1/tokens/route.ts @@ -1,11 +1,11 @@ +import { desc, eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { requireAdmin, requireSession } from "@/lib/api/auth"; import { apiError, UNAUTHORIZED, VALIDATION_ERROR } from "@/lib/api/errors"; import { validateRequestedScopes } from "@/lib/api/scopes"; import { db } from "@/lib/db"; import { personalAccessTokens } from "@/lib/db/schema"; import { generateToken, getTokenPrefix, hashToken } from "@/lib/tokens"; -import { desc, eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function GET() { // Listing is open to any signed-in user — regular members see their own @@ -40,7 +40,9 @@ export async function POST(request: NextRequest) { // PAT list on `/settings/tokens` and call `GET` above, but only admins // can mint new credentials. const authResult = await requireAdmin(); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } let body: { name?: string; expires_at?: string; scopes?: unknown }; try { diff --git a/web/app/api/v1/users/[userId]/route.ts b/web/app/api/v1/users/[userId]/route.ts index a89110f5..e8a86921 100644 --- a/web/app/api/v1/users/[userId]/route.ts +++ b/web/app/api/v1/users/[userId]/route.ts @@ -1,9 +1,9 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { requireAdmin } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { db } from "@/lib/db"; import { users } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; const ALLOWED_PATCH_FIELDS = new Set(["is_admin"]); @@ -20,7 +20,9 @@ export async function PATCH( { params }: { params: Promise<{ userId: string }> } ) { const authResult = await requireAdmin(); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { userId } = await params; diff --git a/web/app/api/v1/users/route.ts b/web/app/api/v1/users/route.ts index 5a86a510..a1dcdc57 100644 --- a/web/app/api/v1/users/route.ts +++ b/web/app/api/v1/users/route.ts @@ -1,7 +1,7 @@ +import { asc } from "drizzle-orm"; import { requireAdmin } from "@/lib/api/auth"; import { db } from "@/lib/db"; import { users } from "@/lib/db/schema"; -import { asc } from "drizzle-orm"; // Admin-only roster used by `/settings/members`. Returns every signed-in // user along with their workspace admin flag so the members page can @@ -11,7 +11,9 @@ import { asc } from "drizzle-orm"; // state. export async function GET() { const authResult = await requireAdmin(); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const rows = await db .select({ diff --git a/web/app/api/v1/watchers/[watcherId]/config-checksum/route.ts b/web/app/api/v1/watchers/[watcherId]/config-checksum/route.ts index 5a85ee46..e808ce84 100644 --- a/web/app/api/v1/watchers/[watcherId]/config-checksum/route.ts +++ b/web/app/api/v1/watchers/[watcherId]/config-checksum/route.ts @@ -1,15 +1,17 @@ +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { isValidUUID } from "@/lib/api/validators"; import { findActiveWatcher } from "@/lib/api/watchers"; -import type { NextRequest } from "next/server"; export async function GET( request: NextRequest, { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { diff --git a/web/app/api/v1/watchers/[watcherId]/config/route.ts b/web/app/api/v1/watchers/[watcherId]/config/route.ts index 17b1db1d..88fc3d88 100644 --- a/web/app/api/v1/watchers/[watcherId]/config/route.ts +++ b/web/app/api/v1/watchers/[watcherId]/config/route.ts @@ -1,3 +1,5 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { isValidUUID } from "@/lib/api/validators"; @@ -8,15 +10,15 @@ import { } from "@/lib/api/watchers"; import { db } from "@/lib/db"; import { watcherEvents, watchers } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function PUT( request: NextRequest, { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { diff --git a/web/app/api/v1/watchers/[watcherId]/events/route.ts b/web/app/api/v1/watchers/[watcherId]/events/route.ts index c4938260..9759dad2 100644 --- a/web/app/api/v1/watchers/[watcherId]/events/route.ts +++ b/web/app/api/v1/watchers/[watcherId]/events/route.ts @@ -1,3 +1,5 @@ +import { and, desc, eq, gte, inArray } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { @@ -8,8 +10,6 @@ import { import { findActiveWatcher } from "@/lib/api/watchers"; import { db } from "@/lib/db"; import { watcherEvents, watcherEventTypeEnum } from "@/lib/db/schema"; -import { and, desc, eq, gte, inArray } from "drizzle-orm"; -import type { NextRequest } from "next/server"; // Derived from the Drizzle enum so adding a new event type is a one-line // schema change — historically this was a hand-maintained Set and drifted @@ -22,7 +22,9 @@ export async function POST( { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { @@ -63,7 +65,7 @@ export async function POST( const values = []; for (let i = 0; i < body.events.length; i++) { const evt = body.events[i] as EventInput; - if (!evt.event_type || !evt.timestamp || !evt.message) { + if (!(evt.event_type && evt.timestamp && evt.message)) { return apiError( 400, VALIDATION_ERROR, @@ -100,7 +102,9 @@ export async function GET( { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { diff --git a/web/app/api/v1/watchers/[watcherId]/heartbeat/route.ts b/web/app/api/v1/watchers/[watcherId]/heartbeat/route.ts index 9612d446..b2ac0092 100644 --- a/web/app/api/v1/watchers/[watcherId]/heartbeat/route.ts +++ b/web/app/api/v1/watchers/[watcherId]/heartbeat/route.ts @@ -1,3 +1,5 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -14,15 +16,15 @@ import { watcherReleaseConfig, watchers, } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function POST( request: NextRequest, { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { diff --git a/web/app/api/v1/watchers/[watcherId]/heartbeats/route.ts b/web/app/api/v1/watchers/[watcherId]/heartbeats/route.ts index 7cfda44e..2885a9a7 100644 --- a/web/app/api/v1/watchers/[watcherId]/heartbeats/route.ts +++ b/web/app/api/v1/watchers/[watcherId]/heartbeats/route.ts @@ -1,3 +1,5 @@ +import { and, desc, eq, gte } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { @@ -8,15 +10,15 @@ import { import { findActiveWatcher } from "@/lib/api/watchers"; import { db } from "@/lib/db"; import { watcherHeartbeats } from "@/lib/db/schema"; -import { and, desc, eq, gte } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function GET( request: NextRequest, { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { diff --git a/web/app/api/v1/watchers/[watcherId]/route.ts b/web/app/api/v1/watchers/[watcherId]/route.ts index d9db57ac..e76febaa 100644 --- a/web/app/api/v1/watchers/[watcherId]/route.ts +++ b/web/app/api/v1/watchers/[watcherId]/route.ts @@ -1,3 +1,5 @@ +import { eq } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -9,15 +11,15 @@ import { isValidUUID } from "@/lib/api/validators"; import { computeEffectiveStatus, findActiveWatcher } from "@/lib/api/watchers"; import { db } from "@/lib/db"; import { instruments, watchers } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function GET( request: NextRequest, { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { @@ -55,7 +57,9 @@ export async function DELETE( { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { diff --git a/web/app/api/v1/watchers/[watcherId]/update-check/route.ts b/web/app/api/v1/watchers/[watcherId]/update-check/route.ts index 185c253c..54fc5843 100644 --- a/web/app/api/v1/watchers/[watcherId]/update-check/route.ts +++ b/web/app/api/v1/watchers/[watcherId]/update-check/route.ts @@ -1,10 +1,10 @@ +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { isValidUUID } from "@/lib/api/validators"; import { findActiveWatcher } from "@/lib/api/watchers"; import { db } from "@/lib/db"; import { watcherReleaseConfig } from "@/lib/db/schema"; -import type { NextRequest } from "next/server"; /** * Server-reported watcher release metadata. @@ -52,7 +52,9 @@ export async function GET( { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { diff --git a/web/app/api/v1/watchers/[watcherId]/upload-queue/route.ts b/web/app/api/v1/watchers/[watcherId]/upload-queue/route.ts index 2fcbe703..9c125661 100644 --- a/web/app/api/v1/watchers/[watcherId]/upload-queue/route.ts +++ b/web/app/api/v1/watchers/[watcherId]/upload-queue/route.ts @@ -1,18 +1,20 @@ +import { and, eq, isNotNull, isNull } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors"; import { isValidUUID } from "@/lib/api/validators"; import { findActiveWatcher } from "@/lib/api/watchers"; import { db } from "@/lib/db"; import { files, instrumentRuns } from "@/lib/db/schema"; -import { and, eq, isNotNull, isNull } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function GET( request: NextRequest, { params }: { params: Promise<{ watcherId: string }> } ) { const authResult = await authorize(request, "watchers:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { watcherId } = await params; if (!isValidUUID(watcherId)) { diff --git a/web/app/api/v1/watchers/register/route.ts b/web/app/api/v1/watchers/register/route.ts index e12831c7..1b2b14df 100644 --- a/web/app/api/v1/watchers/register/route.ts +++ b/web/app/api/v1/watchers/register/route.ts @@ -1,3 +1,5 @@ +import { and, eq, isNull } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { apiError, @@ -7,12 +9,12 @@ import { } from "@/lib/api/errors"; import { db } from "@/lib/db"; import { instruments, watchers } from "@/lib/db/schema"; -import { and, eq, isNull } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function POST(request: NextRequest) { const authResult = await authorize(request, "watchers:write"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } let body: { instrument_id?: string; diff --git a/web/app/api/v1/watchers/route.ts b/web/app/api/v1/watchers/route.ts index 09f9bc9b..f34467ae 100644 --- a/web/app/api/v1/watchers/route.ts +++ b/web/app/api/v1/watchers/route.ts @@ -1,13 +1,15 @@ +import { and, eq, isNull, sql } from "drizzle-orm"; +import type { NextRequest } from "next/server"; import { authorize } from "@/lib/api/auth"; import { computeEffectiveStatus, STALE_THRESHOLD_MS } from "@/lib/api/watchers"; import { db } from "@/lib/db"; import { instruments, watchers } from "@/lib/db/schema"; -import { and, eq, isNull, sql } from "drizzle-orm"; -import type { NextRequest } from "next/server"; export async function GET(request: NextRequest) { const authResult = await authorize(request, "watchers:read"); - if (authResult instanceof Response) return authResult; + if (authResult instanceof Response) { + return authResult; + } const { searchParams } = request.nextUrl; const instrumentIdFilter = searchParams.get("instrument_id"); diff --git a/web/app/globals.css b/web/app/globals.css index 39a44ba7..1383e5e9 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -5,120 +5,124 @@ @custom-variant dark (&:is(.dark *)); @theme inline { - --font-heading: var(--font-sans); - --font-sans: var(--font-sans); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --color-foreground: var(--foreground); - --color-background: var(--background); - --radius-sm: calc(var(--radius) * 0.6); - --radius-md: calc(var(--radius) * 0.8); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) * 1.4); - --radius-2xl: calc(var(--radius) * 1.8); - --radius-3xl: calc(var(--radius) * 2.2); - --radius-4xl: calc(var(--radius) * 2.6); + --font-heading: var(--font-sans); + --font-sans: var(--font-sans); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); } :root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: #F7F7F7; - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --radius: 0.625rem; - --sidebar: #F3F3F3; - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.91 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: #f7f7f7; + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: #f3f3f3; + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.91 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.21 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.21 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --sidebar: oklch(0.2 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.2 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } @keyframes table-pending-slide { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(400%); } + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(400%); + } } html .shiki, @@ -136,12 +140,12 @@ html.dark .shiki span { @layer base { * { @apply border-border outline-ring/50; - } + } html, body { @apply bg-muted text-foreground dark:bg-background; - } + } html { @apply font-sans; - } -} \ No newline at end of file + } +} diff --git a/web/app/instruments/[instrumentId]/page.tsx b/web/app/instruments/[instrumentId]/page.tsx index 3a291af7..55d48f55 100644 --- a/web/app/instruments/[instrumentId]/page.tsx +++ b/web/app/instruments/[instrumentId]/page.tsx @@ -1,3 +1,5 @@ +import { notFound } from "next/navigation"; +import type { Metadata } from "next/types"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { InstrumentHeader } from "@/components/instruments/instrument-header"; import { InstrumentRunsToolbar } from "@/components/instruments/instrument-runs-toolbar"; @@ -32,8 +34,6 @@ import { } from "@/lib/api/notifications"; import { auth } from "@/lib/auth"; import { instrumentDetailParamsCache } from "@/lib/search-params"; -import { notFound } from "next/navigation"; -import type { Metadata } from "next/types"; type Props = { params: Promise<{ instrumentId: string }>; @@ -74,8 +74,8 @@ function renderRunsTableVariant( return ( ); @@ -83,8 +83,8 @@ function renderRunsTableVariant( return ( ); @@ -92,8 +92,8 @@ function renderRunsTableVariant( return ( ); @@ -101,8 +101,8 @@ function renderRunsTableVariant( return ( ); @@ -110,8 +110,8 @@ function renderRunsTableVariant( return ( ); @@ -178,7 +178,9 @@ export default async function InstrumentDetailPage({ listInstrumentSubscriptions(session.user.id!), ]); - if (!instrument) notFound(); + if (!instrument) { + notFound(); + } // Fetch whichever per-instrument filter options apply, in parallel with // the attribution dropdown options. @@ -242,7 +244,7 @@ export default async function InstrumentDetailPage({ ); return ( -
+
{renderRunsTableVariant( filterOptions, @@ -274,8 +276,8 @@ export default async function InstrumentDetailPage({ diff --git a/web/app/instruments/[instrumentId]/runs/[runId]/page.tsx b/web/app/instruments/[instrumentId]/runs/[runId]/page.tsx index 0b32f881..0f20dafd 100644 --- a/web/app/instruments/[instrumentId]/runs/[runId]/page.tsx +++ b/web/app/instruments/[instrumentId]/runs/[runId]/page.tsx @@ -1,3 +1,5 @@ +import { notFound } from "next/navigation"; +import type { Metadata } from "next/types"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { RunAttributionsSection } from "@/components/runs/run-attributions-section"; import { RunCommentsSection } from "@/components/runs/run-comments-section"; @@ -15,8 +17,6 @@ import { listCommentsForRun } from "@/lib/api/run-comments"; import { auth } from "@/lib/auth"; import { formatDate } from "@/lib/date"; import { runDetailParamsCache } from "@/lib/search-params"; -import { notFound } from "next/navigation"; -import type { Metadata } from "next/types"; const FILES_PER_PAGE = 10; @@ -28,7 +28,9 @@ type Props = { export async function generateMetadata({ params }: Props): Promise { const { instrumentId, runId } = await params; const run = await lookupRunByNaturalKey(instrumentId, runId); - if (!run) return { title: "Run Not Found" }; + if (!run) { + return { title: "Run Not Found" }; + } const title = `${run.runId} | ${run.instrumentDisplayName}`; @@ -74,7 +76,9 @@ export default async function RunDetailPage({ params, searchParams }: Props) { } const run = await lookupRunByNaturalKey(instrumentId, runId); - if (!run) notFound(); + if (!run) { + notFound(); + } const [filesPage, fileStats, reportFiles, instrument, comments] = await Promise.all([ @@ -100,26 +104,26 @@ export default async function RunDetailPage({ params, searchParams }: Props) {
} + fileStats={fileStats} + files={filesPage.data} + filesPagination={filesPage.pagination} + instrumentId={instrumentId} + reportFiles={reportFiles} + run={run} + runId={runId} + wellData={wellData} />
diff --git a/web/app/instruments/page.tsx b/web/app/instruments/page.tsx index e17fd0c0..82ce442b 100644 --- a/web/app/instruments/page.tsx +++ b/web/app/instruments/page.tsx @@ -1,3 +1,4 @@ +import type { Metadata } from "next/types"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { InstrumentRowManagementActions, @@ -9,7 +10,6 @@ import { listInstrumentSubscriptions, } from "@/lib/api/notifications"; import { auth } from "@/lib/auth"; -import type { Metadata } from "next/types"; const description = "Instruments connected to Data Hub."; @@ -55,15 +55,15 @@ export default async function InstrumentsPage() { return (
-

Instruments

+

Instruments

); diff --git a/web/app/layout.tsx b/web/app/layout.tsx index ead4906f..916864f7 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,6 +1,10 @@ import { Geist, Geist_Mono } from "next/font/google"; import "@/app/globals.css"; +import type { Metadata } from "next"; +import { cookies } from "next/headers"; +import { SessionProvider } from "next-auth/react"; +import { NuqsAdapter } from "nuqs/adapters/next/app"; import { AppSidebar } from "@/components/app-sidebar"; import { NotificationBell } from "@/components/notifications/notification-bell"; import { NotificationsProvider } from "@/components/notifications/notifications-provider"; @@ -18,10 +22,6 @@ import { countUnread } from "@/lib/api/notifications"; import { getSidebarInstruments, getSidebarWatchers } from "@/lib/api/sidebar"; import { auth, signOut } from "@/lib/auth"; import { cn } from "@/lib/utils"; -import type { Metadata } from "next"; -import { SessionProvider } from "next-auth/react"; -import { cookies } from "next/headers"; -import { NuqsAdapter } from "nuqs/adapters/next/app"; const fontSans = Geist({ subsets: ["latin"], variable: "--font-sans" }); @@ -112,14 +112,14 @@ export default async function RootLayout({ return ( @@ -133,13 +133,13 @@ export default async function RootLayout({ { "use server"; await signOut({ redirectTo: "/login" }); }} + watchers={watchers} />
diff --git a/web/app/login/page.tsx b/web/app/login/page.tsx index 381572e2..f2c9a912 100644 --- a/web/app/login/page.tsx +++ b/web/app/login/page.tsx @@ -1,7 +1,7 @@ +import type { Metadata } from "next/types"; import { DevSignInForm } from "@/components/auth/dev-sign-in-form"; import { Button } from "@/components/ui/button"; import { isDevAuthEnabled, signIn } from "@/lib/auth"; -import { Metadata } from "next/types"; export const metadata: Metadata = { title: "Login", @@ -12,7 +12,7 @@ export default function LoginPage() {
-

+

Welcome to Data Hub

diff --git a/web/app/page.tsx b/web/app/page.tsx index 4ddd6765..1a3c2020 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,3 +1,6 @@ +import { ArrowRight } from "lucide-react"; +import Link from "next/link"; +import type { Metadata } from "next/types"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { DashboardStatsCards } from "@/components/dashboard/dashboard-stats"; import { RunsTable } from "@/components/dashboard/runs-table"; @@ -15,9 +18,6 @@ import { buildRunListQuery } from "@/lib/api/instrument-runs"; import { getRecentActiveInstrumentsForDashboard } from "@/lib/api/instruments"; import { auth } from "@/lib/auth"; import { dashboardParamsCache, hasActiveFilters } from "@/lib/search-params"; -import { ArrowRight } from "lucide-react"; -import Link from "next/link"; -import type { Metadata } from "next/types"; // `default: "Data Hub"` on the root metadata template already renders // `Data Hub` here, so we skip an explicit `title` and @@ -112,13 +112,13 @@ export default async function DashboardPage({

-

Instruments

+

Instruments

View all {totalActiveInstruments} instruments @@ -128,7 +128,7 @@ export default async function DashboardPage({
-

Recent runs

+

Recent runs

@@ -137,16 +137,16 @@ export default async function DashboardPage({ diff --git a/web/app/settings/layout.tsx b/web/app/settings/layout.tsx index 8391badc..7951ba74 100644 --- a/web/app/settings/layout.tsx +++ b/web/app/settings/layout.tsx @@ -1,6 +1,6 @@ +import type { Metadata } from "next"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { auth } from "@/lib/auth"; -import type { Metadata } from "next"; export const metadata: Metadata = { title: { diff --git a/web/app/settings/members/page.tsx b/web/app/settings/members/page.tsx index 96bbccd5..9d95ee12 100644 --- a/web/app/settings/members/page.tsx +++ b/web/app/settings/members/page.tsx @@ -1,11 +1,11 @@ +import { asc } from "drizzle-orm"; +import { ShieldOff } from "lucide-react"; +import type { Metadata } from "next/types"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { MembersTable } from "@/components/members/members-table"; import { auth } from "@/lib/auth"; import { db } from "@/lib/db"; import { users } from "@/lib/db/schema"; -import { asc } from "drizzle-orm"; -import { ShieldOff } from "lucide-react"; -import type { Metadata } from "next/types"; const description = "Manage workspace members and admin access."; @@ -34,10 +34,10 @@ export default async function MembersPage() { return (
-

+

Admins only

-

+

You need workspace admin access to view or change member roles. Ask an existing admin if you need to be promoted.

@@ -60,15 +60,15 @@ export default async function MembersPage() {
-

Members

-

+

Members

+

Grant or revoke admin access for teammates signed in to Data Hub.

- +
); diff --git a/web/app/settings/notifications/page.tsx b/web/app/settings/notifications/page.tsx index 0650a243..4e85ca26 100644 --- a/web/app/settings/notifications/page.tsx +++ b/web/app/settings/notifications/page.tsx @@ -1,3 +1,4 @@ +import type { Metadata } from "next/types"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { NotificationsSettingsForm } from "@/components/notifications/notifications-settings-form"; import { @@ -5,7 +6,6 @@ import { listInstrumentSubscriptions, } from "@/lib/api/notifications"; import { auth } from "@/lib/auth"; -import type { Metadata } from "next/types"; const description = "Choose which Data Hub events to be notified about."; @@ -38,8 +38,8 @@ export default async function NotificationsSettingsPage() { return (
-

Notifications

-

+

Notifications

+

Choose which Data Hub events should produce an in-app notification. Per-instrument subscriptions opt you in to new run{" "} notifications; comment notifications fire when someone replies on a @@ -49,16 +49,16 @@ export default async function NotificationsSettingsPage() {

({ instrumentId: s.instrumentId, displayName: s.displayName, enabled: s.enabled, }))} + initialPreferences={{ + runsAllMuted: prefs.runsAllMuted, + commentsAttributedEnabled: prefs.commentsAttributedEnabled, + commentsParticipatedEnabled: prefs.commentsParticipatedEnabled, + }} />
diff --git a/web/app/settings/page.tsx b/web/app/settings/page.tsx index c099ab66..797fa6a8 100644 --- a/web/app/settings/page.tsx +++ b/web/app/settings/page.tsx @@ -1,6 +1,6 @@ +import { redirect } from "next/navigation"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { auth } from "@/lib/auth"; -import { redirect } from "next/navigation"; export default async function SettingsPage() { // Self-defending auth gate; mirrors `tokens/page.tsx`. Without this an diff --git a/web/app/settings/tokens/page.tsx b/web/app/settings/tokens/page.tsx index 8b5b67e1..0be0c86d 100644 --- a/web/app/settings/tokens/page.tsx +++ b/web/app/settings/tokens/page.tsx @@ -1,3 +1,6 @@ +import { desc, eq } from "drizzle-orm"; +import { KeyRound } from "lucide-react"; +import type { Metadata } from "next/types"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { CreateTokenDialog } from "@/components/tokens/create-token-dialog"; import { DeleteTokenDialog } from "@/components/tokens/delete-token-dialog"; @@ -21,9 +24,6 @@ import { avatarColor, toInitials } from "@/lib/avatar-color"; import { db } from "@/lib/db"; import { personalAccessTokens, users } from "@/lib/db/schema"; import { formatRelativeTime } from "@/lib/utils"; -import { desc, eq } from "drizzle-orm"; -import { KeyRound } from "lucide-react"; -import type { Metadata } from "next/types"; const description = "Personal access tokens for the Data Hub API."; @@ -52,8 +52,8 @@ function TokenScopeBadges({ scopes }: { scopes: string[] }) { if (scopes.length === 0) { return ( No scopes @@ -62,7 +62,7 @@ function TokenScopeBadges({ scopes }: { scopes: string[] }) { if (scopes.length === 1 && scopes[0] === "*") { return ( - + Full access ); @@ -75,7 +75,7 @@ function TokenScopeBadges({ scopes }: { scopes: string[] }) { if (sorted.length === 1) { return ( - + {sorted[0]} ); @@ -86,10 +86,10 @@ function TokenScopeBadges({ scopes }: { scopes: string[] }) {
- + {first} - + +{rest.length}
@@ -157,10 +157,10 @@ export default async function TokensPage() {
-

+

Access Tokens

-

+

{isAdmin ? "Manage personal access tokens for API authentication." : "View personal access tokens for API authentication."} @@ -173,10 +173,10 @@ export default async function TokensPage() { {tokens.length === 0 ? (

-

+

No access tokens yet

-

+

{isAdmin ? "Create a token to authenticate with the API." : "Ask an admin to create a token for you."} @@ -212,8 +212,8 @@ export default async function TokensPage() { {token.user.image ? ( ) : null} {token.tokenPrefix}… diff --git a/web/app/settings/watchers/page.tsx b/web/app/settings/watchers/page.tsx index 51da4e54..513eeb9d 100644 --- a/web/app/settings/watchers/page.tsx +++ b/web/app/settings/watchers/page.tsx @@ -1,11 +1,11 @@ +import { eq } from "drizzle-orm"; +import { ShieldOff } from "lucide-react"; +import type { Metadata } from "next/types"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { WatcherReleaseForm } from "@/components/watcher-release/watcher-release-form"; import { auth } from "@/lib/auth"; import { db } from "@/lib/db"; import { users, watcherReleaseConfig } from "@/lib/db/schema"; -import { eq } from "drizzle-orm"; -import { ShieldOff } from "lucide-react"; -import type { Metadata } from "next/types"; export const metadata: Metadata = { title: "Watchers", @@ -31,10 +31,10 @@ export default async function WatchersSettingsPage() { return (

-

+

Admins only

-

+

You need workspace admin access to change watcher settings. Ask an existing admin if you need to be promoted.

@@ -70,10 +70,10 @@ export default async function WatchersSettingsPage() {
-

+

Watcher Version

-

+

Configure the release advertised by{" "} GET /api/v1/watchers/:id/update-check diff --git a/web/app/watchers/[watcherId]/page.tsx b/web/app/watchers/[watcherId]/page.tsx index caddacb0..3964ca5e 100644 --- a/web/app/watchers/[watcherId]/page.tsx +++ b/web/app/watchers/[watcherId]/page.tsx @@ -1,17 +1,17 @@ +import { notFound, redirect } from "next/navigation"; +import type { Metadata } from "next/types"; import { WatcherConfig } from "@/components/watchers/watcher-config"; import { WatcherDetailTabs } from "@/components/watchers/watcher-detail-tabs"; import { WatcherHeader } from "@/components/watchers/watcher-header"; import { - WATCHER_PAGE_SIZE, getAllWatcherHeartbeats, getWatcherById, getWatcherEvents, + WATCHER_PAGE_SIZE, } from "@/lib/api/watchers"; import { auth } from "@/lib/auth"; import { todayDateString } from "@/lib/date"; import { watcherDetailParamsCache } from "@/lib/search-params"; -import { notFound, redirect } from "next/navigation"; -import type { Metadata } from "next/types"; type Props = { params: Promise<{ watcherId: string }>; @@ -31,7 +31,9 @@ export default async function WatcherDetailPage({ searchParams, }: Props) { const session = await auth(); - if (!session) redirect("/login"); + if (!session) { + redirect("/login"); + } const { watcherId } = await params; const filters = watcherDetailParamsCache.parse(await searchParams); @@ -65,7 +67,9 @@ export default async function WatcherDetailPage({ }), ]); - if (!watcher) notFound(); + if (!watcher) { + notFound(); + } const logsTotalPages = Math.ceil(eventResult.total / WATCHER_PAGE_SIZE); @@ -74,11 +78,11 @@ export default async function WatcherDetailPage({ } - heartbeats={heartbeats} events={eventResult.rows} - eventsTotal={eventResult.total} eventsPage={filters.logs_page} + eventsTotal={eventResult.total} eventsTotalPages={logsTotalPages} + heartbeats={heartbeats} />

); diff --git a/web/app/watchers/page.tsx b/web/app/watchers/page.tsx index 517f65a8..7e518a3b 100644 --- a/web/app/watchers/page.tsx +++ b/web/app/watchers/page.tsx @@ -1,8 +1,8 @@ +import type { Metadata } from "next/types"; import { SignInRequired } from "@/components/auth/sign-in-required"; import { WatchersView } from "@/components/watchers/watchers-view"; import { getWatcherList } from "@/lib/api/watchers"; import { auth } from "@/lib/auth"; -import type { Metadata } from "next/types"; const description = "Watcher agents reporting into Data Hub."; @@ -34,7 +34,7 @@ export default async function WatchersPage() { return (
-

Watchers

+

Watchers

diff --git a/web/biome.jsonc b/web/biome.jsonc new file mode 100644 index 00000000..0e8c66e5 --- /dev/null +++ b/web/biome.jsonc @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "extends": [ + "ultracite/biome/core", + "ultracite/biome/react", + "ultracite/biome/next" + ] +} diff --git a/web/components/app-sidebar/app-sidebar-content.tsx b/web/components/app-sidebar/app-sidebar-content.tsx index 65fbb3f1..7c8c7931 100644 --- a/web/components/app-sidebar/app-sidebar-content.tsx +++ b/web/components/app-sidebar/app-sidebar-content.tsx @@ -1,10 +1,10 @@ "use client"; +import { usePathname } from "next/navigation"; import { MainNav } from "@/components/app-sidebar/main-nav"; import { SettingsNav } from "@/components/app-sidebar/settings-nav"; import { SidebarContent } from "@/components/ui/sidebar"; import type { SidebarInstrument, SidebarWatcher } from "@/lib/api/sidebar"; -import { usePathname } from "next/navigation"; type AppSidebarContentProps = { instruments: SidebarInstrument[]; diff --git a/web/components/app-sidebar/index.tsx b/web/components/app-sidebar/index.tsx index bf95934d..617db5e4 100644 --- a/web/components/app-sidebar/index.tsx +++ b/web/components/app-sidebar/index.tsx @@ -1,3 +1,6 @@ +import Image from "next/image"; +import Link from "next/link"; +import type { Session } from "next-auth"; import { AppSidebarContent } from "@/components/app-sidebar/app-sidebar-content"; import { UserMenuFooter } from "@/components/app-sidebar/user-menu-footer"; import { @@ -10,9 +13,6 @@ import { SidebarRail, } from "@/components/ui/sidebar"; import type { SidebarInstrument, SidebarWatcher } from "@/lib/api/sidebar"; -import type { Session } from "next-auth"; -import Image from "next/image"; -import Link from "next/link"; type AppSidebarProps = { session: Session; @@ -34,19 +34,19 @@ export function AppSidebar({ Data Hub - Data Hub + Data Hub @@ -54,11 +54,11 @@ export function AppSidebar({ - + diff --git a/web/components/app-sidebar/main-nav.tsx b/web/components/app-sidebar/main-nav.tsx index ca397e3b..69dac10a 100644 --- a/web/components/app-sidebar/main-nav.tsx +++ b/web/components/app-sidebar/main-nav.tsx @@ -1,5 +1,8 @@ "use client"; +import { ChevronRight, Cpu, Home, type LucideIcon, Radio } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; import { Collapsible, CollapsibleContent, @@ -17,9 +20,6 @@ import { SidebarMenuSubItem, } from "@/components/ui/sidebar"; import type { SidebarInstrument, SidebarWatcher } from "@/lib/api/sidebar"; -import { ChevronRight, Cpu, Home, Radio, type LucideIcon } from "lucide-react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; type MainNavProps = { instruments: SidebarInstrument[]; @@ -49,23 +49,22 @@ export function MainNav({ instruments, watchers }: MainNavProps) { ({ key: instrument.id, href: `/instruments/${instrument.id}`, label: instrument.displayName, }))} - currentPath={pathname} + label="Instruments" + viewAllHref="/instruments" /> ({ key: watcher.id, href: `/watchers/${watcher.id}`, @@ -74,7 +73,8 @@ export function MainNav({ instruments, watchers }: MainNavProps) { // row never collapses to an empty label. label: watcher.hostname ?? `${watcher.id.slice(0, 8)}…`, }))} - currentPath={pathname} + label="Watchers" + viewAllHref="/watchers" /> @@ -110,8 +110,8 @@ function CollapsibleNavSection({ return ( @@ -138,8 +138,8 @@ function CollapsibleNavSection({ View all diff --git a/web/components/app-sidebar/settings-nav.tsx b/web/components/app-sidebar/settings-nav.tsx index 570e7685..724402c6 100644 --- a/web/components/app-sidebar/settings-nav.tsx +++ b/web/components/app-sidebar/settings-nav.tsx @@ -1,5 +1,8 @@ "use client"; +import { ChevronLeft } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; import { SidebarGroup, SidebarGroupContent, @@ -7,9 +10,6 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar"; -import { ChevronLeft } from "lucide-react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; type SettingsSection = { href: string; @@ -51,8 +51,8 @@ export function SettingsNav({ isAdmin }: { isAdmin: boolean }) { diff --git a/web/components/app-sidebar/user-menu-footer.tsx b/web/components/app-sidebar/user-menu-footer.tsx index 9a5838d5..74c7d3c4 100644 --- a/web/components/app-sidebar/user-menu-footer.tsx +++ b/web/components/app-sidebar/user-menu-footer.tsx @@ -1,5 +1,8 @@ "use client"; +import { ChevronsUpDown, LogOut, Settings } from "lucide-react"; +import Link from "next/link"; +import { useTransition } from "react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, @@ -14,9 +17,6 @@ import { SidebarMenuItem, useSidebar, } from "@/components/ui/sidebar"; -import { ChevronsUpDown, LogOut, Settings } from "lucide-react"; -import Link from "next/link"; -import { useTransition } from "react"; type UserMenuFooterProps = { user: { @@ -54,12 +54,12 @@ export function UserMenuFooter({ user, signOutAction }: UserMenuFooterProps) { {user.image && ( - + )} {initials} @@ -70,7 +70,7 @@ export function UserMenuFooter({ user, signOutAction }: UserMenuFooterProps) { {user.name ?? "User"} {user.email && ( - + {user.email} )} @@ -79,9 +79,9 @@ export function UserMenuFooter({ user, signOutAction }: UserMenuFooterProps) { diff --git a/web/components/auth/dev-sign-in-form.tsx b/web/components/auth/dev-sign-in-form.tsx index 7ba593bb..eaef31b6 100644 --- a/web/components/auth/dev-sign-in-form.tsx +++ b/web/components/auth/dev-sign-in-form.tsx @@ -21,8 +21,8 @@ export function DevSignInForm({ inputId?: string; }) { return ( -
-

+

+

Local development

diff --git a/web/components/auth/sign-in-required.tsx b/web/components/auth/sign-in-required.tsx index 69538dfe..bdeecf2f 100644 --- a/web/components/auth/sign-in-required.tsx +++ b/web/components/auth/sign-in-required.tsx @@ -35,7 +35,7 @@ export function SignInRequired({ callbackUrl, children }: SignInRequiredProps) {
-

+

Sign in to Data Hub

{children ? ( diff --git a/web/components/code-block.tsx b/web/components/code-block.tsx index 64df7841..243080b2 100644 --- a/web/components/code-block.tsx +++ b/web/components/code-block.tsx @@ -1,9 +1,9 @@ import { codeToHtml } from "shiki"; interface CodeBlockProps { + className?: string; code: string; lang: string; - className?: string; } export async function CodeBlock({ code, lang, className }: CodeBlockProps) { diff --git a/web/components/copy-button.tsx b/web/components/copy-button.tsx index b3de0155..c0ee2ca0 100644 --- a/web/components/copy-button.tsx +++ b/web/components/copy-button.tsx @@ -1,15 +1,15 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; import { Check, Copy } from "lucide-react"; import { useCallback, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; interface CopyButtonProps { - value: string; className?: string; - variant?: React.ComponentProps["variant"]; size?: React.ComponentProps["size"]; + value: string; + variant?: React.ComponentProps["variant"]; } export function CopyButton({ @@ -28,11 +28,11 @@ export function CopyButton({ return ( diff --git a/web/components/dashboard/dashboard-stats.tsx b/web/components/dashboard/dashboard-stats.tsx index dd90dd06..42abd7cc 100644 --- a/web/components/dashboard/dashboard-stats.tsx +++ b/web/components/dashboard/dashboard-stats.tsx @@ -1,7 +1,7 @@ +import type { ReactNode } from "react"; import { Card } from "@/components/ui/card"; import type { DashboardStats } from "@/lib/api/dashboard"; import { cn, formatBytes } from "@/lib/utils"; -import type { ReactNode } from "react"; const numberFormatter = new Intl.NumberFormat("en-US"); @@ -21,18 +21,18 @@ function StatCard({ valueClassName?: string; }) { return ( - +
-

{label}

+

{label}

{value}

-

{subline}

+

{subline}

); @@ -66,42 +66,42 @@ export function DashboardStatsCards({ stats }: { stats: DashboardStats }) {
} + value={formatNumber(runsLast24Hours.total)} /> } + value={formatNumber(runsThisWeek.total)} /> 0 ? `${formatBytes(pendingUploads.totalBytes)} queued` : "Upload queue is clear" } + value={formatNumber(pendingUploads.count)} + valueClassName={pendingHasBacklog ? "text-destructive" : undefined} /> 0 ? `${formatNumber(runsThisWeek.unattributed)} unattributed` : "All runs attributed" } + value={formatNumber(runsThisWeek.mine)} />
); diff --git a/web/components/dashboard/relative-time.tsx b/web/components/dashboard/relative-time.tsx index fb49b8be..2b95683f 100644 --- a/web/components/dashboard/relative-time.tsx +++ b/web/components/dashboard/relative-time.tsx @@ -15,8 +15,8 @@ export function RelativeTime({ date }: { date: string }) { diff --git a/web/components/dashboard/runs-table.tsx b/web/components/dashboard/runs-table.tsx index f92ae621..898eac85 100644 --- a/web/components/dashboard/runs-table.tsx +++ b/web/components/dashboard/runs-table.tsx @@ -1,3 +1,4 @@ +import { SearchX } from "lucide-react"; import { RelativeTime } from "@/components/dashboard/relative-time"; import { AcquiredColumnHeader } from "@/components/instruments/runs-table/acquired-column-header"; import { RanByCell } from "@/components/instruments/runs-table/ran-by-cell"; @@ -24,7 +25,6 @@ import { import type { RunListRow } from "@/lib/api/instrument-runs"; import { runRowToRef } from "@/lib/runs/row-actions"; import { cn, formatBytes } from "@/lib/utils"; -import { SearchX } from "lucide-react"; export function RunsTable({ data, @@ -45,7 +45,7 @@ export function RunsTable({ return (
-

+

{hasFilters ? "No runs match your filters." : "No instrument runs yet."} @@ -87,38 +87,38 @@ export function RunsTable({ const href = `/instruments/${row.instrument_id}/runs/${encodeURIComponent(row.run_id)}`; return ( -

+
{row.instrument_display_name}
{isDeleted ? ( deleted @@ -133,9 +133,9 @@ export function RunsTable({ @@ -154,11 +154,11 @@ export function RunsTable({
); diff --git a/web/components/dashboard/runs-toolbar.tsx b/web/components/dashboard/runs-toolbar.tsx index 08838b10..20b61c20 100644 --- a/web/components/dashboard/runs-toolbar.tsx +++ b/web/components/dashboard/runs-toolbar.tsx @@ -1,5 +1,8 @@ "use client"; +import { Check, ChevronsUpDown, Search, X } from "lucide-react"; +import { useQueryStates } from "nuqs"; +import { useState } from "react"; import { RunFiltersCombobox } from "@/components/runs/run-filters-combobox"; import { RunsDateFilter } from "@/components/runs/runs-date-filter"; import { useTablePending } from "@/components/table-pending"; @@ -21,9 +24,6 @@ import { } from "@/components/ui/popover"; import { dashboardSearchParams, hasActiveFilters } from "@/lib/search-params"; import { cn } from "@/lib/utils"; -import { Check, ChevronsUpDown, Search, X } from "lucide-react"; -import { useQueryStates } from "nuqs"; -import { useState } from "react"; type Instrument = { id: string; @@ -69,36 +69,36 @@ export function RunsToolbar({ instruments }: { instruments: Instrument[] }) {
setFilters({ search: e.target.value, page: 1 })} placeholder="Search runs..." value={filters.search} - onChange={(e) => setFilters({ search: e.target.value, page: 1 })} - className="pl-9" />
{/* Instrument multi-select */} - + - + @@ -109,8 +109,8 @@ export function RunsToolbar({ instruments }: { instruments: Instrument[] }) { return ( toggleInstrument(inst.id)} + value={inst.displayName} > Clear @@ -144,7 +144,8 @@ export function RunsToolbar({ instruments }: { instruments: Instrument[] }) { )} setFilters({ date_from: range.from, @@ -152,15 +153,14 @@ export function RunsToolbar({ instruments }: { instruments: Instrument[] }) { page: 1, }) } - align="end" - defaultPreset="24h" + value={{ from: filters.date_from, to: filters.date_to }} /> setFilters({ include_deleted: includeDeleted, page: 1 }) } + values={{ includeDeleted: filters.include_deleted }} />
diff --git a/web/components/instruments/edit-instrument-dialog.tsx b/web/components/instruments/edit-instrument-dialog.tsx index d227f441..fed3a168 100644 --- a/web/components/instruments/edit-instrument-dialog.tsx +++ b/web/components/instruments/edit-instrument-dialog.tsx @@ -1,5 +1,9 @@ "use client"; +import { Loader2, Pencil } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState, useTransition } from "react"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -20,10 +24,6 @@ import { SelectValue, } from "@/components/ui/select"; import { VALID_INSTRUMENT_TYPES } from "@/lib/db/schema"; -import { Loader2, Pencil } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useState, useTransition } from "react"; -import { toast } from "sonner"; const TYPE_LABELS: Record = { generic: "Generic", @@ -81,7 +81,6 @@ export function EditInstrumentDialog({ return ( { setOpen(value); // Re-sync form state from props on open so the dialog reflects any @@ -91,9 +90,10 @@ export function EditInstrumentDialog({ setType(instrumentType); } }} + open={open} > - @@ -110,17 +110,17 @@ export function EditInstrumentDialog({
setName(e.target.value)} maxLength={100} - autoFocus + onChange={(e) => setName(e.target.value)} + value={name} />
- + @@ -131,13 +131,13 @@ export function EditInstrumentDialog({ ))} -

+

Controls the run detail page layout.

-
diff --git a/web/components/instruments/instruments-table.tsx b/web/components/instruments/instruments-table.tsx index 4e6ef0e2..02261dc8 100644 --- a/web/components/instruments/instruments-table.tsx +++ b/web/components/instruments/instruments-table.tsx @@ -1,3 +1,5 @@ +import { SearchX } from "lucide-react"; +import type { ReactNode } from "react"; import { RelativeTime } from "@/components/dashboard/relative-time"; import { EditInstrumentDialog } from "@/components/instruments/edit-instrument-dialog"; import { RowActionsCell } from "@/components/instruments/row-actions-cell"; @@ -16,8 +18,6 @@ import { import { getWatcherOnlineStatus } from "@/components/watchers/watcher-online-status"; import { WatcherStatusBadge } from "@/components/watchers/watcher-status-badge"; import type { InstrumentListItem } from "@/lib/api/instruments"; -import { SearchX } from "lucide-react"; -import type { ReactNode } from "react"; /** * Default row actions used by the management page: an approval action for @@ -31,8 +31,8 @@ export function InstrumentRowManagementActions(row: InstrumentListItem) {
{row.status === "pending" && }
@@ -72,7 +72,7 @@ export function InstrumentsTable({ return (
-

+

No instruments configured yet.

@@ -100,20 +100,20 @@ export function InstrumentsTable({ const watcherStatus = getWatcherOnlineStatus(row); return ( {row.displayName} - + {row.runCount} total {row.runCount === 1 ? "run" : "runs"} @@ -121,9 +121,9 @@ export function InstrumentsTable({
{row.filePatterns.map((p) => ( {p} @@ -143,10 +143,10 @@ export function InstrumentsTable({ {notifications ? ( ) : null} diff --git a/web/components/instruments/row-actions-cell.tsx b/web/components/instruments/row-actions-cell.tsx index 6fed95b3..4a4df585 100644 --- a/web/components/instruments/row-actions-cell.tsx +++ b/web/components/instruments/row-actions-cell.tsx @@ -1,7 +1,7 @@ "use client"; -import { TableCell } from "@/components/ui/table"; import type { ReactNode } from "react"; +import { TableCell } from "@/components/ui/table"; /** * Wraps an `` actions cell so clicks inside it (edit diff --git a/web/components/instruments/runs-table/acquired-column-header.tsx b/web/components/instruments/runs-table/acquired-column-header.tsx index 2dabc613..96ababfc 100644 --- a/web/components/instruments/runs-table/acquired-column-header.tsx +++ b/web/components/instruments/runs-table/acquired-column-header.tsx @@ -16,14 +16,14 @@ export function AcquiredColumnHeader() { return ( Acquired diff --git a/web/components/instruments/runs-table/clickable-row.tsx b/web/components/instruments/runs-table/clickable-row.tsx index 53f12006..9d6710ee 100644 --- a/web/components/instruments/runs-table/clickable-row.tsx +++ b/web/components/instruments/runs-table/clickable-row.tsx @@ -1,7 +1,7 @@ "use client"; -import { TableRow } from "@/components/ui/table"; import { useRouter } from "next/navigation"; +import { TableRow } from "@/components/ui/table"; export function ClickableRow({ href, diff --git a/web/components/instruments/runs-table/default-runs-table.tsx b/web/components/instruments/runs-table/default-runs-table.tsx index acd591f7..8c2c6401 100644 --- a/web/components/instruments/runs-table/default-runs-table.tsx +++ b/web/components/instruments/runs-table/default-runs-table.tsx @@ -46,8 +46,8 @@ export function DefaultRunsTable({ @@ -63,8 +63,8 @@ export function DefaultRunsTable({ const isDeleted = row.deleted_at !== null; return ( @@ -72,21 +72,21 @@ export function DefaultRunsTable({
{isDeleted && ( - + deleted )} @@ -100,9 +100,9 @@ export function DefaultRunsTable({ diff --git a/web/components/instruments/runs-table/epson-scanner-runs-table.tsx b/web/components/instruments/runs-table/epson-scanner-runs-table.tsx index bc2848b7..8edc6fd2 100644 --- a/web/components/instruments/runs-table/epson-scanner-runs-table.tsx +++ b/web/components/instruments/runs-table/epson-scanner-runs-table.tsx @@ -20,7 +20,7 @@ import { cn, formatBytes } from "@/lib/utils"; import type { RunRow } from "."; import { AcquiredColumnHeader } from "./acquired-column-header"; import { FilterableColumnHeader } from "./filterable-column-header"; -import { MetadataFieldBadge, getMetadataField } from "./metadata-utils"; +import { getMetadataField, MetadataFieldBadge } from "./metadata-utils"; import { RanByCell } from "./ran-by-cell"; import { RawFileColumnHeader } from "./raw-file-column-header"; import { RunIdLabel } from "./run-id-label"; @@ -71,22 +71,22 @@ export function EpsonScannerRunsTable({ @@ -104,8 +104,8 @@ export function EpsonScannerRunsTable({ const colorMode = getMetadataField(row.metadata, "color_mode"); return ( @@ -113,21 +113,21 @@ export function EpsonScannerRunsTable({
{isDeleted && ( - + deleted )} @@ -141,23 +141,23 @@ export function EpsonScannerRunsTable({ diff --git a/web/components/instruments/runs-table/filterable-column-header.tsx b/web/components/instruments/runs-table/filterable-column-header.tsx index 558245d4..2241898d 100644 --- a/web/components/instruments/runs-table/filterable-column-header.tsx +++ b/web/components/instruments/runs-table/filterable-column-header.tsx @@ -1,5 +1,8 @@ "use client"; +import { ChevronsUpDown, ListFilter } from "lucide-react"; +import { useQueryStates } from "nuqs"; +import type { inferParserType } from "nuqs/server"; import { useTablePending } from "@/components/table-pending"; import { Button } from "@/components/ui/button"; import { @@ -12,9 +15,6 @@ import { } from "@/components/ui/dropdown-menu"; import { instrumentDetailSearchParams } from "@/lib/search-params"; import { cn } from "@/lib/utils"; -import { ChevronsUpDown, ListFilter } from "lucide-react"; -import { useQueryStates } from "nuqs"; -import type { inferParserType } from "nuqs/server"; type InstrumentDetailFilters = inferParserType< typeof instrumentDetailSearchParams @@ -68,12 +68,12 @@ export function FilterableColumnHeader({ @@ -154,7 +168,7 @@ export function RanByCell({ - @@ -171,17 +185,17 @@ export function RanByCell({ {attribution.avatarUrl ? ( ) : null} @@ -198,11 +212,6 @@ export function RanByCell({ @@ -220,13 +234,13 @@ export function RanByCell({ diff --git a/web/components/instruments/runs-table/raw-file-column-header.tsx b/web/components/instruments/runs-table/raw-file-column-header.tsx index e4cf3ade..e3824156 100644 --- a/web/components/instruments/runs-table/raw-file-column-header.tsx +++ b/web/components/instruments/runs-table/raw-file-column-header.tsx @@ -14,14 +14,14 @@ export function RawFileColumnHeader({ label }: { label: string }) { return ( {label} diff --git a/web/components/instruments/runs-table/run-bulk-action-bar.tsx b/web/components/instruments/runs-table/run-bulk-action-bar.tsx index 0b76d423..6f6b0c77 100644 --- a/web/components/instruments/runs-table/run-bulk-action-bar.tsx +++ b/web/components/instruments/runs-table/run-bulk-action-bar.tsx @@ -1,5 +1,9 @@ "use client"; +import { ArrowDownToLine, ArrowUpToLine, RotateCw, Trash2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState, useTransition } from "react"; +import { toast } from "sonner"; import { DeleteRunsDialog } from "@/components/runs/delete-runs-dialog"; import { ReprocessRunsDialog } from "@/components/runs/reprocess-runs-dialog"; import { Button } from "@/components/ui/button"; @@ -11,12 +15,8 @@ import { } from "@/components/ui/tooltip"; import { useArchiveDownload } from "@/hooks/use-archive-download"; import { cn } from "@/lib/utils"; -import { ArrowDownToLine, ArrowUpToLine, RotateCw, Trash2 } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useState, useTransition } from "react"; -import { toast } from "sonner"; -import { useRunSelection, type RunRef } from "./run-selection-provider"; +import { type RunRef, useRunSelection } from "./run-selection-provider"; // --------------------------------------------------------------------------- // Bulk action bar shown as a floating card pinned to the center bottom of @@ -64,7 +64,9 @@ async function fanOutUpload( ref.runId )}/request-upload-all`; const res = await fetch(url, { method: "POST" }); - if (!res.ok) throw new Error(await res.text()); + if (!res.ok) { + throw new Error(await res.text()); + } const body = (await res.json()) as { files_queued?: number }; return body.files_queued ?? 0; }) @@ -92,7 +94,9 @@ export function RunBulkActionBar() { const [reprocessOpen, setReprocessOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); - if (meta.count === 0) return null; + if (meta.count === 0) { + return null; + } const refs = Array.from(state.selected.values()); @@ -189,12 +193,12 @@ export function RunBulkActionBar() { return ( <>
{meta.count}{" "} @@ -204,20 +208,20 @@ export function RunBulkActionBar() {
@@ -226,12 +230,12 @@ export function RunBulkActionBar() { @@ -300,16 +304,16 @@ export function RunBulkActionBar() {
actions.clear()} onOpenChange={setReprocessOpen} + open={reprocessOpen} runs={reprocessTargets} - onComplete={() => actions.clear()} /> actions.clear()} onOpenChange={setDeleteOpen} + open={deleteOpen} runs={deleteTargets} - onComplete={() => actions.clear()} /> ); diff --git a/web/components/instruments/runs-table/run-id-label.tsx b/web/components/instruments/runs-table/run-id-label.tsx index 46222691..bc9b67f0 100644 --- a/web/components/instruments/runs-table/run-id-label.tsx +++ b/web/components/instruments/runs-table/run-id-label.tsx @@ -1,12 +1,12 @@ "use client"; +import Link from "next/link"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import Link from "next/link"; const MAX_RUN_ID_LENGTH = 32; @@ -33,19 +33,21 @@ export function RunIdLabel({ ); const label = href ? ( - + {display} ) : ( {display} ); - if (!isTruncated) return label; + if (!isTruncated) { + return label; + } return ( {label} - + {runId} diff --git a/web/components/instruments/runs-table/run-row-actions.tsx b/web/components/instruments/runs-table/run-row-actions.tsx index 26de6b8c..43c99263 100644 --- a/web/components/instruments/runs-table/run-row-actions.tsx +++ b/web/components/instruments/runs-table/run-row-actions.tsx @@ -1,5 +1,18 @@ "use client"; +import { + ArrowDownToLine, + ArrowUpToLine, + Loader2, + MoreHorizontal, + RotateCw, + Trash2, +} from "lucide-react"; +import { useRouter } from "next/navigation"; +import { type MouseEvent, useState, useTransition } from "react"; +import { toast } from "sonner"; +import { DeleteRunsDialog } from "@/components/runs/delete-runs-dialog"; +import { ReprocessRunsDialog } from "@/components/runs/reprocess-runs-dialog"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -16,20 +29,6 @@ import { import { useArchiveDownload } from "@/hooks/use-archive-download"; import { computeRunCaps } from "@/lib/runs/row-actions"; import { cn } from "@/lib/utils"; -import { - ArrowDownToLine, - ArrowUpToLine, - Loader2, - MoreHorizontal, - RotateCw, - Trash2, -} from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useState, useTransition, type MouseEvent } from "react"; -import { toast } from "sonner"; - -import { DeleteRunsDialog } from "@/components/runs/delete-runs-dialog"; -import { ReprocessRunsDialog } from "@/components/runs/reprocess-runs-dialog"; import type { RunRow } from "."; // Actions cell shown inline at the end of every runs-table row. The strip is @@ -54,7 +53,9 @@ export function RunRowActions({ row }: { row: RunRow }) { const [deleteOpen, setDeleteOpen] = useState(false); // Deleted rows get no actions — the row is read-only. - if (row.deleted_at !== null) return null; + if (row.deleted_at !== null) { + return null; + } const baseUrl = `/api/v1/instruments/${row.instrument_id}/runs/${encodeURIComponent( row.run_id @@ -109,13 +110,13 @@ export function RunRowActions({ row }: { row: RunRow }) { @@ -146,15 +147,15 @@ export function RunRowActions({ row }: { row: RunRow }) { )} - + @@ -175,12 +176,12 @@ export function RunRowActions({ row }: { row: RunRow }) { {caps.reprocess && caps.delete && } {caps.delete && ( { e.preventDefault(); setMenuOpen(false); setDeleteOpen(true); }} + variant="destructive" > Delete run @@ -190,8 +191,8 @@ export function RunRowActions({ row }: { row: RunRow }) { +
+
0 && meta.allSelected(refs)} diff --git a/web/components/instruments/runs-table/run-selection-provider.tsx b/web/components/instruments/runs-table/run-selection-provider.tsx index e0145039..a9c0156f 100644 --- a/web/components/instruments/runs-table/run-selection-provider.tsx +++ b/web/components/instruments/runs-table/run-selection-provider.tsx @@ -80,9 +80,13 @@ export function RunSelectionProvider({ const next = new Map(prev); const alreadyAll = refs.every((r) => next.has(r.id)); if (alreadyAll) { - for (const r of refs) next.delete(r.id); + for (const r of refs) { + next.delete(r.id); + } } else { - for (const r of refs) next.set(r.id, r); + for (const r of refs) { + next.set(r.id, r); + } } return next; }); diff --git a/web/components/instruments/runs-table/run-status-icon.tsx b/web/components/instruments/runs-table/run-status-icon.tsx index 009cfc5f..7a6c5a16 100644 --- a/web/components/instruments/runs-table/run-status-icon.tsx +++ b/web/components/instruments/runs-table/run-status-icon.tsx @@ -1,11 +1,5 @@ "use client"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; import { CircleCheck, CircleDashed, @@ -13,6 +7,12 @@ import { Clock, LoaderCircle, } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; export function RunStatusIcon({ fileCount, @@ -91,11 +91,11 @@ export function RunStatusIcon({ {icon} {lines.length === 1 ? ( lines[0] diff --git a/web/components/instruments/runs-table/runs-table-footer.tsx b/web/components/instruments/runs-table/runs-table-footer.tsx index 51ec01d6..b5a52efb 100644 --- a/web/components/instruments/runs-table/runs-table-footer.tsx +++ b/web/components/instruments/runs-table/runs-table-footer.tsx @@ -12,7 +12,7 @@ export function RunsTableFooter({ ranByYouCount: number; }) { return ( -
+

Showing {shownCount} of{" "} {totalCount} diff --git a/web/components/instruments/status-actions.tsx b/web/components/instruments/status-actions.tsx index 1f7fd05d..ef9d0e9a 100644 --- a/web/components/instruments/status-actions.tsx +++ b/web/components/instruments/status-actions.tsx @@ -1,10 +1,10 @@ "use client"; -import { Button } from "@/components/ui/button"; import { Check, Loader2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTransition } from "react"; import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; // Instruments registered by a watcher start as "pending" and require admin // approval. This button transitions them to "active" via the PATCH API. @@ -33,11 +33,11 @@ export function StatusActions({ instrumentId }: { instrumentId: string }) { return (

); - if (!isSelf) return control; + if (!isSelf) { + return control; + } return ( diff --git a/web/components/members/members-table.tsx b/web/components/members/members-table.tsx index 46d8a6ac..f0b10e0b 100644 --- a/web/components/members/members-table.tsx +++ b/web/components/members/members-table.tsx @@ -50,7 +50,7 @@ export function MembersTable({ data, currentUserId }: MembersTableProps) {
{member.image ? ( - + ) : null} {toInitials(displayName)} @@ -60,7 +60,7 @@ export function MembersTable({ data, currentUserId }: MembersTableProps) { {displayName} {isSelf ? ( - + (you) ) : null} @@ -75,7 +75,7 @@ export function MembersTable({ data, currentUserId }: MembersTableProps) { {member.isAdmin ? ( Admin ) : ( - + Member )} @@ -83,10 +83,10 @@ export function MembersTable({ data, currentUserId }: MembersTableProps) {
diff --git a/web/components/notifications/instrument-notification-switch.tsx b/web/components/notifications/instrument-notification-switch.tsx index b87c5422..98cd34e9 100644 --- a/web/components/notifications/instrument-notification-switch.tsx +++ b/web/components/notifications/instrument-notification-switch.tsx @@ -1,13 +1,13 @@ "use client"; +import { useState, useTransition } from "react"; +import { toast } from "sonner"; import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { useState, useTransition } from "react"; -import { toast } from "sonner"; // Composition over a `tooltip` boolean prop: the Tooltip is wrapped here // so callers don't have to know about the active vs muted copy. Three @@ -55,7 +55,9 @@ export function InstrumentNotificationSwitch({ body: JSON.stringify({ enabled: next }), } ); - if (!res.ok) throw new Error(`HTTP ${res.status}`); + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } } catch (err) { setEnabled(previous); toast.error("Couldn't update instrument notifications", { @@ -81,11 +83,11 @@ export function InstrumentNotificationSwitch({ pointer events otherwise. */} diff --git a/web/components/notifications/instrument-notifications-cell.tsx b/web/components/notifications/instrument-notifications-cell.tsx index 28d647a1..8a0e8b66 100644 --- a/web/components/notifications/instrument-notifications-cell.tsx +++ b/web/components/notifications/instrument-notifications-cell.tsx @@ -22,8 +22,8 @@ export function InstrumentNotificationsCell({ return ( e.stopPropagation()}> diff --git a/web/components/notifications/notification-bell-content.tsx b/web/components/notifications/notification-bell-content.tsx index 5c574ec8..f531a90b 100644 --- a/web/components/notifications/notification-bell-content.tsx +++ b/web/components/notifications/notification-bell-content.tsx @@ -1,30 +1,30 @@ "use client"; -import { - useNotifications, - type NotificationItem, -} from "@/components/notifications/notifications-provider"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { avatarColor } from "@/lib/avatar-color"; -import type { InstrumentType } from "@/lib/db/schema"; -import { cn, formatRelativeTime } from "@/lib/utils"; import { Activity, BellOff, ChevronDown, FlaskConical, Image as ImageIcon, + type LucideIcon, Microscope, Radar, ScanLine, Settings, TestTube, - type LucideIcon, } from "lucide-react"; import Link from "next/link"; import { useMemo, useState } from "react"; +import { + type NotificationItem, + useNotifications, +} from "@/components/notifications/notifications-provider"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { avatarColor } from "@/lib/avatar-color"; +import type { InstrumentType } from "@/lib/db/schema"; +import { cn, formatRelativeTime } from "@/lib/utils"; // --------------------------------------------------------------------------- // Bell popover content. The provider is the single source of truth for @@ -112,10 +112,14 @@ function bucketOf(createdAt: string, now: Date): Bucket { now.getMonth(), now.getDate() ); - if (created >= startOfToday) return "today"; + if (created >= startOfToday) { + return "today"; + } const startOfYesterday = new Date(startOfToday); startOfYesterday.setDate(startOfYesterday.getDate() - 1); - if (created >= startOfYesterday) return "yesterday"; + if (created >= startOfYesterday) { + return "yesterday"; + } return "earlier"; } @@ -165,8 +169,8 @@ function buildEntries(items: NotificationItem[], now: Date): BucketedEntries { const EMPTY_STATE = (
-

You're all caught up.

-

+

You're all caught up.

+

New runs and replies will show up here when you have something subscribed.

@@ -192,12 +196,12 @@ export function NotificationBellContent({ return (
{ void markAllRead(); }} onNavigate={onNavigate} + unreadCount={unreadCount} /> {isEmpty ? ( EMPTY_STATE @@ -205,7 +209,9 @@ export function NotificationBellContent({
{BUCKET_ORDER.map((bucket) => { const entries = buckets[bucket]; - if (entries.length === 0) return null; + if (entries.length === 0) { + return null; + } return ( {entries.map((entry) => @@ -222,8 +228,8 @@ export function NotificationBellContent({ /> ) : ( { void markOneRead(notificationId); }} @@ -260,12 +266,12 @@ function NotificationsHeader({ return (
-

Notifications

+

Notifications

{hasUnread ? ( {unreadCount > 99 ? "99+" : unreadCount} new @@ -273,21 +279,21 @@ function NotificationsHeader({
@@ -311,7 +317,7 @@ function NotificationSection({ }) { return (
-
+
{label}
    {children}
@@ -365,14 +371,14 @@ function CommentNotificationRow({ return ( {n.actor ? ( {n.actor.avatarUrl ? ( - + ) : null} {n.actor.initials} @@ -391,12 +397,12 @@ function CommentNotificationRow({

{n.commentBody ? ( -

+

“{n.commentBody}”

) : null}

{formatRelativeTime(n.createdAt)} @@ -458,12 +464,14 @@ function RunGroupNotificationRow({ return ( { - if (onlyRun.readAt === null) onActivate(onlyRun.id); + if (onlyRun.readAt === null) { + onActivate(onlyRun.id); + } onNavigate?.(); }} - className="flex cursor-pointer items-start gap-3 px-4 py-3 outline-none focus-visible:ring-2 focus-visible:ring-ring/50" > {iconBlock}

@@ -471,11 +479,11 @@ function RunGroupNotificationRow({ 1 new run on{" "} {group.instrumentDisplayName}

-

+

{onlyRun.runDisplayId}

{formatRelativeTime(group.latestCreatedAt)} @@ -489,15 +497,15 @@ function RunGroupNotificationRow({ return ( @@ -127,12 +135,14 @@ export function ArchiveDownloadDialog({ diff --git a/web/components/runs/archive-download-provider.tsx b/web/components/runs/archive-download-provider.tsx index 42adda97..ad836794 100644 --- a/web/components/runs/archive-download-provider.tsx +++ b/web/components/runs/archive-download-provider.tsx @@ -2,12 +2,12 @@ import { createContext, + type ReactNode, useCallback, useEffect, useMemo, useRef, useState, - type ReactNode, } from "react"; import { toast } from "sonner"; @@ -60,7 +60,7 @@ type ArchiveDownloadContextValue = { export const ArchiveDownloadContext = createContext(null); -const POLL_INTERVAL_MS = 2_000; +const POLL_INTERVAL_MS = 2000; // Stop polling after a generous amount of time so a stuck Lambda doesn't // leave the dialog spinning forever. Lambda Function URLs cap at 15 minutes; // we double that as a hard ceiling. @@ -203,7 +203,7 @@ export function ArchiveDownloadProvider({ children }: { children: ReactNode }) { triggerDownload(downloadUrl, filename); // Auto-dismiss the row a moment later so the dialog doesn't linger // when the build was actually a cache hit. - window.setTimeout(() => dismiss(id), 1_500); + window.setTimeout(() => dismiss(id), 1500); }, [dismiss, updateJob] ); @@ -259,11 +259,15 @@ export function ArchiveDownloadProvider({ children }: { children: ReactNode }) { // don't spin up N timers. useEffect(() => { const building = jobs.filter((j) => j.status === "building"); - if (building.length === 0) return; + if (building.length === 0) { + return; + } const interval = window.setInterval(async () => { for (const job of jobsRef.current) { - if (job.status !== "building") continue; + if (job.status !== "building") { + continue; + } if (Date.now() - job.startedAt > POLL_TIMEOUT_MS) { updateJob(job.id, { status: "failed", diff --git a/web/components/runs/colony-data-table.tsx b/web/components/runs/colony-data-table.tsx index 2a0d72ab..036423b7 100644 --- a/web/components/runs/colony-data-table.tsx +++ b/web/components/runs/colony-data-table.tsx @@ -1,5 +1,13 @@ "use client"; +import { parse } from "csv-parse/browser/esm/sync"; +import { + AlertTriangle, + ChevronLeft, + ChevronRight, + ExternalLink, +} from "lucide-react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { @@ -12,14 +20,6 @@ import { } from "@/components/ui/table"; import type { RunFile } from "@/lib/api/instrument-runs"; import { cn } from "@/lib/utils"; -import { parse } from "csv-parse/browser/esm/sync"; -import { - AlertTriangle, - ChevronLeft, - ChevronRight, - ExternalLink, -} from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; const PAGE_SIZE = 10; @@ -65,7 +65,9 @@ export function ColonyDataTable({ file }: { file: RunFile }) { const state: LoadState = useMemo(() => { const cached = cacheRef.current.get(fileId); - if (cached) return { status: "ready", rows: cached }; + if (cached) { + return { status: "ready", rows: cached }; + } if (asyncResult && asyncResult.fileId === fileId) { return asyncResult.status === "ready" ? { status: "ready", rows: asyncResult.rows } @@ -78,16 +80,22 @@ export function ColonyDataTable({ file }: { file: RunFile }) { }, [fileId, asyncResult, retryNonce]); useEffect(() => { - if (cacheRef.current.has(fileId)) return; + if (cacheRef.current.has(fileId)) { + return; + } let cancelled = false; fetchCsvRows(fileId) .then((rows) => { cacheRef.current.set(fileId, rows); - if (cancelled) return; + if (cancelled) { + return; + } setAsyncResult({ fileId, status: "ready", rows }); }) .catch((err: unknown) => { - if (cancelled) return; + if (cancelled) { + return; + } const message = err instanceof Error ? err.message : "Failed to load CSV"; setAsyncResult({ fileId, status: "error", message }); @@ -106,31 +114,31 @@ export function ColonyDataTable({ file }: { file: RunFile }) { return (

{state.status === "loading" && ( - + )} {state.status === "error" && (
- -

{state.message}

-
)} {state.status === "ready" && ( )}
@@ -155,11 +163,15 @@ function ColonyDataTableView({ // Computed once per row set so per-cell rendering stays cheap. const numericColumns = useMemo>(() => { const out = new Set(); - if (rows.length === 0) return out; + if (rows.length === 0) { + return out; + } for (const col of columns) { for (const row of rows) { const v = row[col]; - if (v === undefined || v === "") continue; + if (v === undefined || v === "") { + continue; + } if (!Number.isNaN(Number(v)) && Number.isFinite(Number(v))) { out.add(col); } @@ -180,7 +192,7 @@ function ColonyDataTableView({ if (total === 0) { return ( -
+
CSV is empty.
); @@ -197,11 +209,11 @@ function ColonyDataTableView({ {columns.map((col) => ( {col} @@ -213,11 +225,11 @@ function ColonyDataTableView({ {columns.map((col) => ( {row[col] ?? ""} @@ -227,7 +239,7 @@ function ColonyDataTableView({
-
+
Showing {start + 1}{end} of{" "} @@ -239,20 +251,20 @@ function ColonyDataTableView({
diff --git a/web/components/runs/comment-markdown.tsx b/web/components/runs/comment-markdown.tsx index 855ee272..b543f736 100644 --- a/web/components/runs/comment-markdown.tsx +++ b/web/components/runs/comment-markdown.tsx @@ -12,17 +12,16 @@ import remarkGfm from "remark-gfm"; // react-markdown bundle (~30 KB) only ships when there's a comment to render. export function CommentMarkdown({ body }: { body: string }) { return ( -
+

, a: (props) => ( ), ul: (props) => ( @@ -61,18 +60,18 @@ export function CommentMarkdown({ body }: { body: string }) { ), blockquote: (props) => (

), h1: (props) => ( -

+

), h2: (props) => ( -

+

), h3: (props) => ( -

+

), hr: () =>
, table: (props) => ( @@ -82,14 +81,15 @@ export function CommentMarkdown({ body }: { body: string }) { ), th: (props) => ( ), td: (props) => ( - + ), }} + remarkPlugins={[remarkGfm]} > {body} diff --git a/web/components/runs/delete-run-dialog.tsx b/web/components/runs/delete-run-dialog.tsx index bd498b17..b8948e76 100644 --- a/web/components/runs/delete-run-dialog.tsx +++ b/web/components/runs/delete-run-dialog.tsx @@ -1,5 +1,9 @@ "use client"; +import { Loader2, Trash2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState, useTransition } from "react"; +import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, @@ -14,10 +18,6 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Loader2, Trash2 } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useState, useTransition } from "react"; -import { toast } from "sonner"; export function DeleteRunDialog({ instrumentId, @@ -61,12 +61,12 @@ export function DeleteRunDialog({ } return ( - + diff --git a/web/components/runs/file-selection-provider.tsx b/web/components/runs/file-selection-provider.tsx index 6c3e2548..a6b31a4f 100644 --- a/web/components/runs/file-selection-provider.tsx +++ b/web/components/runs/file-selection-provider.tsx @@ -1,7 +1,7 @@ "use client"; -import type { RunFile } from "@/lib/api/instrument-runs"; import { createContext, use, useCallback, useMemo, useState } from "react"; +import type { RunFile } from "@/lib/api/instrument-runs"; // --------------------------------------------------------------------------- // File selection provider for the run files table. Mirrors RunSelectionProvider @@ -37,12 +37,16 @@ export type FileRef = { // (dismissed files, transient `upload_requested` rows). Caller treats null // the same as "no checkbox in this row". export function buildFileRef(file: RunFile): FileRef | null { - if (file.deletedAt !== null) return null; + if (file.deletedAt !== null) { + return null; + } const isDetected = file.status === "detected"; const canDownload = DOWNLOADABLE_STATUSES.has(file.status); const canReprocess = REPROCESSABLE_STATUSES.has(file.status) && file.s3Key !== null; - if (!isDetected && !canDownload) return null; + if (!(isDetected || canDownload)) { + return null; + } return { id: file.id, filename: file.filename, @@ -107,9 +111,13 @@ export function FileSelectionProvider({ const next = new Map(prev); const alreadyAll = refs.length > 0 && refs.every((r) => next.has(r.id)); if (alreadyAll) { - for (const r of refs) next.delete(r.id); + for (const r of refs) { + next.delete(r.id); + } } else { - for (const r of refs) next.set(r.id, r); + for (const r of refs) { + next.set(r.id, r); + } } return next; }); diff --git a/web/components/runs/file-status-badge.tsx b/web/components/runs/file-status-badge.tsx index f8a37153..fdebc72d 100644 --- a/web/components/runs/file-status-badge.tsx +++ b/web/components/runs/file-status-badge.tsx @@ -30,7 +30,7 @@ export function FileStatusBadge({ status }: { status: string }) { }; return ( - + {config.label} ); diff --git a/web/components/runs/hina-report-section.tsx b/web/components/runs/hina-report-section.tsx index 09045c0f..e9167936 100644 --- a/web/components/runs/hina-report-section.tsx +++ b/web/components/runs/hina-report-section.tsx @@ -1,18 +1,18 @@ "use client"; +import { ExternalLink } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { - type CarouselApi, Carousel, + type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, } from "@/components/ui/carousel"; import type { RunFile } from "@/lib/api/instrument-runs"; -import { ExternalLink } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; const IMAGE_EXTENSIONS = /\.(png|jpe?g|gif|webp|tiff?)$/i; @@ -43,7 +43,9 @@ export function HinaReportSection({ files }: { files: RunFile[] }) { const [currentIndex, setCurrentIndex] = useState(0); useEffect(() => { - if (!api) return; + if (!api) { + return; + } // Subscribe to Embla's own "select" and "reInit" events — no synchronous // state sync needed on mount since Embla defaults to snap 0 which matches // our initial state. `reInit` covers the case where the carousel recalcs @@ -60,10 +62,10 @@ export function HinaReportSection({ files }: { files: RunFile[] }) { if (processedImages.length === 0) { return (
-

Report Data

+

Report Data

-

+

No report data has been generated for this run.

@@ -78,9 +80,9 @@ export function HinaReportSection({ files }: { files: RunFile[] }) { return (
-

+

Report Data{" "} - + {processedImages.length} image(s)

@@ -88,30 +90,30 @@ export function HinaReportSection({ files }: { files: RunFile[] }) {
- + {processedImages.map((file, i) => { const url = `/api/v1/files/${file.id}/download`; @@ -120,10 +122,10 @@ export function HinaReportSection({ files }: { files: RunFile[] }) {
{/* eslint-disable-next-line @next/next/no-img-element */} {file.filename}
diff --git a/web/components/runs/metadata-badges.tsx b/web/components/runs/metadata-badges.tsx index e02d0ec2..3b5ce461 100644 --- a/web/components/runs/metadata-badges.tsx +++ b/web/components/runs/metadata-badges.tsx @@ -10,16 +10,24 @@ export function getMetadataField( metadata: unknown, key: string ): string | null { - if (!metadata || typeof metadata !== "object") return null; + if (!metadata || typeof metadata !== "object") { + return null; + } const value = (metadata as Record)[key]; - return value != null ? String(value) : null; + return value == null ? null : String(value); } export function getMetadataArray(metadata: unknown, key: string): string[] { - if (!metadata || typeof metadata !== "object") return []; + if (!metadata || typeof metadata !== "object") { + return []; + } const value = (metadata as Record)[key]; - if (Array.isArray(value)) return value.map(String); - if (value != null) return [String(value)]; + if (Array.isArray(value)) { + return value.map(String); + } + if (value != null) { + return [String(value)]; + } return []; } @@ -27,7 +35,9 @@ export function getMetadataRecord( metadata: unknown, key: string ): Record | null { - if (!metadata || typeof metadata !== "object") return null; + if (!metadata || typeof metadata !== "object") { + return null; + } const value = (metadata as Record)[key]; if (value && typeof value === "object" && !Array.isArray(value)) { return value as Record; @@ -39,9 +49,13 @@ export function getMetadataObjectArray( metadata: unknown, key: string ): Record[] { - if (!metadata || typeof metadata !== "object") return []; + if (!metadata || typeof metadata !== "object") { + return []; + } const value = (metadata as Record)[key]; - if (!Array.isArray(value)) return []; + if (!Array.isArray(value)) { + return []; + } return value.filter( (v): v is Record => v !== null && typeof v === "object" && !Array.isArray(v) @@ -59,9 +73,15 @@ export function sortWavelengths(wavelengths: string[]): string[] { const nb = Number(b); const aNum = Number.isFinite(na); const bNum = Number.isFinite(nb); - if (aNum && bNum) return na - nb; - if (aNum) return -1; - if (bNum) return 1; + if (aNum && bNum) { + return na - nb; + } + if (aNum) { + return -1; + } + if (bNum) { + return 1; + } return a.localeCompare(b); }); } @@ -73,9 +93,11 @@ export function MetadataFieldBadge({ value: string | null; colorClass?: string; }) { - if (!value) return ; + if (!value) { + return ; + } return ( - + {value} ); @@ -94,9 +116,9 @@ function BadgeRow({
{values.map((v) => ( {v} @@ -112,9 +134,10 @@ export function MetadataArrayBadges({ values: string[]; colorMap?: Record; }) { - if (values.length === 0) + if (values.length === 0) { return ; - return ; + } + return ; } /** @@ -132,11 +155,12 @@ export function TruncatedBadges({ colorMap?: Record; maxVisible?: number; }) { - if (values.length === 0) + if (values.length === 0) { return ; + } if (values.length <= maxVisible) { - return ; + return ; } const visible = values.slice(0, maxVisible); @@ -148,24 +172,24 @@ export function TruncatedBadges({
{visible.map((v) => ( {v} ))} +{hiddenCount}
- + ); diff --git a/web/components/runs/plate-map-grid.tsx b/web/components/runs/plate-map-grid.tsx index 2f8e527d..53ed58d6 100644 --- a/web/components/runs/plate-map-grid.tsx +++ b/web/components/runs/plate-map-grid.tsx @@ -1,26 +1,30 @@ "use client"; +import { Fragment, useMemo, useState } from "react"; import { Slider } from "@/components/ui/slider"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { Fragment, useMemo, useState } from "react"; export type PlateWellData = { well: string; value: unknown }; function parseWell(well: string): { row: number; col: number } | null { const match = well.match(/^([A-P])(\d{1,2})$/i); - if (!match) return null; + if (!match) { + return null; + } return { row: match[1].toUpperCase().charCodeAt(0) - 65, - col: parseInt(match[2], 10) - 1, + col: Number.parseInt(match[2], 10) - 1, }; } function formatCellValue(value: unknown): string { - if (value === null || value === undefined) return ""; + if (value === null || value === undefined) { + return ""; + } if (typeof value === "number") { return Number.isInteger(value) ? String(value) @@ -92,10 +96,14 @@ export function PlateMapGrid({ plateName, wavelength, }: PlateMapGridProps) { - if (!Array.isArray(data)) return null; + if (!Array.isArray(data)) { + return null; + } const wells = data as PlateWellData[]; - if (wells.length === 0) return null; + if (wells.length === 0) { + return null; + } let maxRow = 0; let maxCol = 0; @@ -103,17 +111,23 @@ export function PlateMapGrid({ for (const w of wells) { const pos = parseWell(w.well); - if (!pos) continue; - if (pos.row > maxRow) maxRow = pos.row; - if (pos.col > maxCol) maxCol = pos.col; + if (!pos) { + continue; + } + if (pos.row > maxRow) { + maxRow = pos.row; + } + if (pos.col > maxCol) { + maxCol = pos.col; + } cellMap.set(`${pos.row}-${pos.col}`, w.value); } const rows = maxRow + 1; const cols = maxCol + 1; - let vMin = Infinity; - let vMax = -Infinity; + let vMin = Number.POSITIVE_INFINITY; + let vMax = Number.NEGATIVE_INFINITY; if (heatmap) { if (heatmapRange) { vMin = heatmapRange.min; @@ -121,8 +135,12 @@ export function PlateMapGrid({ } else { for (const v of cellMap.values()) { if (typeof v === "number") { - if (v < vMin) vMin = v; - if (v > vMax) vMax = v; + if (v < vMin) { + vMin = v; + } + if (v > vMax) { + vMax = v; + } } } } @@ -138,11 +156,11 @@ export function PlateMapGrid({
{(plateName || wavelength) && (
-

+

{plateName}

{wavelength && ( - + {wavelength} nm )} @@ -158,8 +176,8 @@ export function PlateMapGrid({ {/* Column headers */} {colLabels.map((c, ci) => (
{c} @@ -170,7 +188,7 @@ export function PlateMapGrid({ {rowLabels.map((rowLabel, ri) => (
{rowLabel} @@ -228,7 +246,7 @@ export function PlateMapGrid({
- {heatmap && hasRange && } + {heatmap && hasRange && }
); } @@ -252,17 +270,23 @@ function PlasmaColorBar({ min, max }: { min: number; max: number }) { function computeGlobalHeatmapRange( frames: PlateWellData[][] ): { min: number; max: number } | undefined { - let min = Infinity; - let max = -Infinity; + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; for (const frame of frames) { for (const w of frame) { if (typeof w.value === "number") { - if (w.value < min) min = w.value; - if (w.value > max) max = w.value; + if (w.value < min) { + min = w.value; + } + if (w.value > max) { + max = w.value; + } } } } - if (!isFinite(min) || !isFinite(max)) return undefined; + if (!(isFinite(min) && isFinite(max))) { + return; + } return { min, max }; } @@ -294,7 +318,9 @@ export function KineticPlateMapWithTimeSlider({ [heatmap, frames] ); - if (frames.length === 0) return null; + if (frames.length === 0) { + return null; + } return (
@@ -307,7 +333,7 @@ export function KineticPlateMapWithTimeSlider({ /> {frames.length > 1 && (
-
+
Time setIndex(v[0] ?? 0)} step={1} value={[selectedIndex]} - onValueChange={(v) => setIndex(v[0] ?? 0)} - aria-label="Select measurement time" /> -

Report Data

+

Report Data

-

+

No report data has been generated for this run.

@@ -28,9 +28,9 @@ export function RamanReportSection({ return (
-

+

Report Data{" "} - + {spectra.length} {spectra.length === 1 ? "spectrum" : "spectra"}

diff --git a/web/components/runs/raman-spectrum-viewer.tsx b/web/components/runs/raman-spectrum-viewer.tsx index 6d6c126e..5b589059 100644 --- a/web/components/runs/raman-spectrum-viewer.tsx +++ b/web/components/runs/raman-spectrum-viewer.tsx @@ -1,5 +1,15 @@ "use client"; +import { parse } from "csv-parse/browser/esm/sync"; +import { + AlertTriangle, + Check, + ChevronLeft, + ChevronRight, + ChevronsUpDown, +} from "lucide-react"; +import { startTransition, useEffect, useMemo, useRef, useState } from "react"; +import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; import type { RamanSpectrumFileRef } from "@/components/runs/raman-report-section"; import { Button } from "@/components/ui/button"; import { @@ -24,16 +34,6 @@ import { import { Skeleton } from "@/components/ui/skeleton"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { cn } from "@/lib/utils"; -import { parse } from "csv-parse/browser/esm/sync"; -import { - AlertTriangle, - Check, - ChevronLeft, - ChevronRight, - ChevronsUpDown, -} from "lucide-react"; -import { startTransition, useEffect, useMemo, useRef, useState } from "react"; -import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; type SpectrumPoint = { wavenumber: number; @@ -84,8 +84,10 @@ async function fetchSpectrum(fileId: number): Promise { } const first = rows[0]; if ( - !(CSV_HEADER_WAVENUMBER in first) || - !(CSV_HEADER_INTENSITY in first || CSV_HEADER_DARK in first) + !( + CSV_HEADER_WAVENUMBER in first && + (CSV_HEADER_INTENSITY in first || CSV_HEADER_DARK in first) + ) ) { throw new Error( `CSV is missing expected columns (${CSV_HEADER_WAVENUMBER} + ${CSV_HEADER_INTENSITY}/${CSV_HEADER_DARK})` @@ -114,13 +116,13 @@ function SpectrumPicker({ const selected = spectra.find((s) => s.fileId === selectedId); return ( - + @@ -142,11 +144,11 @@ function SpectrumPicker({ return ( { onSelect(s.fileId); setOpen(false); }} + value={s.filename} > { // Don't let the user deselect every series — that would leave an // empty chart with no obvious way to recover. - if (next.length === 0) return; + if (next.length === 0) { + return; + } onChange(next as Series[]); }} - aria-label="Series visibility" + size="sm" + type="multiple" + value={visible} > {ALL_SERIES.map((key) => ( - + + - + v.toFixed(0)} label={{ value: "Wavenumber (cm\u207B\u00B9)", position: "insideBottom", @@ -241,15 +241,19 @@ function SpectrumChart({ fill: "var(--color-muted-foreground)", }, }} + tickFormatter={(v: number) => v.toFixed(0)} + tickLine={false} + tickMargin={8} + type="number" /> Math.abs(v) >= 1000 ? `${(v / 1000).toFixed(1)}k` : v.toFixed(0) } + tickLine={false} + tickMargin={8} + width={64} /> {showIntensity && ( )} {showDark && ( )} @@ -328,11 +332,15 @@ export function RamanSpectrumViewer({ const canGoNext = currentIndex >= 0 && currentIndex < spectra.length - 1; function goPrev() { - if (!canGoPrev) return; + if (!canGoPrev) { + return; + } setSelectedId(spectra[currentIndex - 1].fileId); } function goNext() { - if (!canGoNext) return; + if (!canGoNext) { + return; + } setSelectedId(spectra[currentIndex + 1].fileId); } @@ -341,9 +349,13 @@ export function RamanSpectrumViewer({ const cacheRef = useRef>(new Map()); const state: LoadState = useMemo(() => { - if (selectedId == null) return { status: "idle" }; + if (selectedId == null) { + return { status: "idle" }; + } const cached = cacheRef.current.get(selectedId); - if (cached) return { status: "ready", points: cached }; + if (cached) { + return { status: "ready", points: cached }; + } if (asyncResult && asyncResult.fileId === selectedId) { return asyncResult.status === "ready" ? { status: "ready", points: asyncResult.points } @@ -356,14 +368,20 @@ export function RamanSpectrumViewer({ }, [selectedId, asyncResult, retryNonce]); useEffect(() => { - if (selectedId == null) return; - if (cacheRef.current.has(selectedId)) return; + if (selectedId == null) { + return; + } + if (cacheRef.current.has(selectedId)) { + return; + } let cancelled = false; fetchSpectrum(selectedId) .then((points) => { cacheRef.current.set(selectedId, points); - if (cancelled) return; + if (cancelled) { + return; + } // Rendering ~2k points to recharts is the heavy part of this update; // a transition lets the picker close stay snappy. startTransition(() => { @@ -371,7 +389,9 @@ export function RamanSpectrumViewer({ }); }) .catch((err: unknown) => { - if (cancelled) return; + if (cancelled) { + return; + } const message = err instanceof Error ? err.message : "Failed to load spectrum"; setAsyncResult({ fileId: selectedId, status: "error", message }); @@ -383,7 +403,9 @@ export function RamanSpectrumViewer({ }, [selectedId, retryNonce]); function handleRetry() { - if (selectedId == null) return; + if (selectedId == null) { + return; + } cacheRef.current.delete(selectedId); setAsyncResult(null); setRetryNonce((n) => n + 1); @@ -393,42 +415,42 @@ export function RamanSpectrumViewer({
- +
{state.status === "loading" && ( - + )} {state.status === "error" && (
- -

{state.message}

-
diff --git a/web/components/runs/reprocess-runs-dialog.tsx b/web/components/runs/reprocess-runs-dialog.tsx index 9fe7b386..c24246aa 100644 --- a/web/components/runs/reprocess-runs-dialog.tsx +++ b/web/components/runs/reprocess-runs-dialog.tsx @@ -1,5 +1,9 @@ "use client"; +import { Loader2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useTransition } from "react"; +import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, @@ -10,10 +14,6 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Loader2 } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useTransition } from "react"; -import { toast } from "sonner"; export type ReprocessRunTarget = { instrumentId: string; @@ -39,7 +39,9 @@ async function fanOut( r.runId )}/reprocess`; const res = await fetch(url, { method: "POST" }); - if (!res.ok) throw new Error(await res.text()); + if (!res.ok) { + throw new Error(await res.text()); + } const body = (await res.json()) as { files_queued?: number }; return body.files_queued ?? 0; }) @@ -105,7 +107,7 @@ export function ReprocessRunsDialog({ : `Reprocess ${runCount} runs?`; return ( - + {title} @@ -119,7 +121,7 @@ export function ReprocessRunsDialog({ Cancel - + {isPending && } Reprocess diff --git a/web/components/runs/restore-run-button.tsx b/web/components/runs/restore-run-button.tsx index ba9467a3..f1ea8259 100644 --- a/web/components/runs/restore-run-button.tsx +++ b/web/components/runs/restore-run-button.tsx @@ -1,10 +1,10 @@ "use client"; -import { Button } from "@/components/ui/button"; import { Loader2, RotateCcw } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTransition } from "react"; import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; export function RestoreRunButton({ instrumentId, @@ -36,11 +36,11 @@ export function RestoreRunButton({ return (