[2] Storage-backed LawNet BFF and frontend #37

Closed
opened 2026-06-14 05:23:36 +02:00 by dernbu · 2 comments
Owner

Objective

Build the BFF and frontend around an explicit storage contract so LawNet catalog and case-detail screens can run against either local mock files or PostgreSQL.

This slice is intentionally limited to the web/BFF product surface and the storage boundary. It should make frontend work debuggable without Docker Compose or PostgreSQL, while still defining the PostgreSQL-backed contract that later ingestion work will write to.

Dependencies

Depends on #36.

Scope

  • Define a BFF storage contract for LawNet catalog listing and LawNet case detail lookup.
  • Select the storage implementation from an environment variable.
  • Implement file-system storage for local mock/debug usage.
  • Store mock catalog data in JSON and mock artifacts as local PDF/HTML files.
  • Implement PostgreSQL storage for the same catalog/detail contract.
  • Add the LawNet-related PostgreSQL schema needed by the BFF for both catalog records and document/artifact metadata used by #37 and #38.
  • Add API/query support for listing LawNet records through the storage contract.
  • Add API/query support for loading one LawNet case detail through the storage contract.
  • Add a frontend dashboard/table showing LawNet cases from the selected storage backend.
  • Add a frontend detail page showing source metadata, available renditions, and artifact links from the selected storage backend.
  • Add fixture/mock-backed tests for the file-system storage and BFF/frontend flow.
  • Add PostgreSQL-backed tests for the Postgres storage contract where practical.

Out of scope

  • LawNet live discovery/listing ingestion.
  • LawNet document or PDF fetching.
  • Durable worker, scheduler, or operator ingestion commands.
  • eLitigation ingestion or comparison.
  • Sanitized public HTML rendering as the final reader experience.
  • Full-text judgment search.

Done when

  • The web app can list LawNet mock records using file-system storage without PostgreSQL or Docker Compose.
  • The web app can open a mock LawNet case detail using JSON plus local PDF/HTML files.
  • The same BFF routes can use PostgreSQL storage by changing an environment variable.
  • The PostgreSQL schema supports LawNet catalog records, document renditions, and artifact metadata needed by #37 and #38.
  • Only the BFF/frontend owns behavior in this issue; ingestion work is deferred to #38.
  • Malformed or incomplete mock/source-shaped records remain visible enough for debugging instead of being silently discarded.
## Objective Build the BFF and frontend around an explicit storage contract so LawNet catalog and case-detail screens can run against either local mock files or PostgreSQL. This slice is intentionally limited to the web/BFF product surface and the storage boundary. It should make frontend work debuggable without Docker Compose or PostgreSQL, while still defining the PostgreSQL-backed contract that later ingestion work will write to. ## Dependencies Depends on #36. ## Scope - Define a BFF storage contract for LawNet catalog listing and LawNet case detail lookup. - Select the storage implementation from an environment variable. - Implement file-system storage for local mock/debug usage. - Store mock catalog data in JSON and mock artifacts as local PDF/HTML files. - Implement PostgreSQL storage for the same catalog/detail contract. - Add the LawNet-related PostgreSQL schema needed by the BFF for both catalog records and document/artifact metadata used by #37 and #38. - Add API/query support for listing LawNet records through the storage contract. - Add API/query support for loading one LawNet case detail through the storage contract. - Add a frontend dashboard/table showing LawNet cases from the selected storage backend. - Add a frontend detail page showing source metadata, available renditions, and artifact links from the selected storage backend. - Add fixture/mock-backed tests for the file-system storage and BFF/frontend flow. - Add PostgreSQL-backed tests for the Postgres storage contract where practical. ## Out of scope - LawNet live discovery/listing ingestion. - LawNet document or PDF fetching. - Durable worker, scheduler, or operator ingestion commands. - eLitigation ingestion or comparison. - Sanitized public HTML rendering as the final reader experience. - Full-text judgment search. ## Done when - The web app can list LawNet mock records using file-system storage without PostgreSQL or Docker Compose. - The web app can open a mock LawNet case detail using JSON plus local PDF/HTML files. - The same BFF routes can use PostgreSQL storage by changing an environment variable. - The PostgreSQL schema supports LawNet catalog records, document renditions, and artifact metadata needed by #37 and #38. - Only the BFF/frontend owns behavior in this issue; ingestion work is deferred to #38. - Malformed or incomplete mock/source-shaped records remain visible enough for debugging instead of being silently discarded.
dernbu changed title from [2] LawNet catalog MVP to [2] Storage-backed LawNet BFF and frontend 2026-06-14 07:13:28 +02:00
Author
Owner

Implementation plan

This issue is planning-only for the BFF/frontend/storage-contract slice. Do not implement LawNet live ingestion here.

Core goal

Build the LawNet catalog and case-detail product surface through a storage abstraction that can run against either local mock files or PostgreSQL.

The implementation should deliver:

  • A LawNet dashboard/list page.
  • A LawNet case detail page.
  • BFF API routes used by those pages.
  • A shared storage interface with two implementations: filesystem and PostgreSQL.
  • Filesystem storage that runs without Docker Compose or PostgreSQL.
  • PostgreSQL schema and storage implementation that #38 ingestion will populate.

Important design decision

Use class-based dependency injection, not a backend parameter passed through every function.

Define a shared interface and inject an implementation at the server boundary:

export interface LawNetStorage {
  listCases(input: ListLawNetCasesInput): Promise<ListLawNetCasesResult>;
  getCase(caseId: string): Promise<LawNetCaseDetail | null>;
  getArtifact(artifactId: string): Promise<LawNetArtifactContent | null>;
}

Concrete classes:

export class FileSystemLawNetStorage implements LawNetStorage { ... }
export class PostgresLawNetStorage implements LawNetStorage { ... }

Factory:

export function createLawNetStorageFromEnv(): LawNetStorage { ... }

API/query handlers should receive or create a LawNetStorage instance and call the common interface. Do not thread storageBackend or usePostgres style parameters through individual methods.

Proposed file layout

services/web/src/server/lawnet/
  artifact-response.server.ts
  config.server.ts
  filesystem-storage.server.ts
  postgres-storage.server.ts
  storage.server.ts
  types.ts

services/web/src/routes/api/lawnet/cases.ts
services/web/src/routes/api/lawnet/cases.$caseId.ts
services/web/src/routes/api/lawnet/artifacts.$artifactId.ts

services/web/src/routes/lawnet.tsx
services/web/src/routes/lawnet.$caseId.tsx

services/web/mock/lawnet/cases.json
services/web/mock/lawnet/artifacts/*.html
services/web/mock/lawnet/artifacts/*.pdf

services/flyway/sql/V2__lawnet_storage.sql

All storage/config modules should import @tanstack/react-start/server-only so they cannot enter the client bundle.

Environment variables

Use one explicit backend selector:

LAWNET_STORAGE_BACKEND=filesystem

Allowed values:

filesystem
postgres

Recommended defaults:

  • Standalone pnpm dev: default to filesystem.
  • Compose: default to postgres.
  • postgres requires DATABASE_URL.
  • filesystem uses LAWNET_FILE_STORAGE_ROOT, defaulting to ./mock/lawnet from services/web.

Add to .env.example:

LAWNET_STORAGE_BACKEND=postgres
LAWNET_FILE_STORAGE_ROOT=./mock/lawnet

Pass both through the web service in compose.yaml.

Storage contract types

Use typed nullable fields so malformed/incomplete records remain visible.

Recommended enum unions:

export type LawNetStorageBackend = "filesystem" | "postgres";
export type LawNetAvailability = "available" | "unavailable" | "unknown";
export type LawNetRecordStatus = "ok" | "incomplete" | "malformed";
export type LawNetRenditionKind = "judgment" | "slr" | "unknown";
export type LawNetArtifactKind = "html" | "pdf";

Summary fields:

export type LawNetCaseSummary = {
  caseId: string;
  sourceKey: string;
  citation: string | null;
  title: string | null;
  decisionDate: string | null;
  court: string | null;
  caseNumber: string | null;
  availability: LawNetAvailability;
  recordStatus: LawNetRecordStatus;
  issueSummary: string[];
  hasHtml: boolean;
  hasPdf: boolean;
  updatedAt: string | null;
};

Detail fields:

export type LawNetCaseDetail = LawNetCaseSummary & {
  sourceUrl: string | null;
  renditions: LawNetRendition[];
  artifacts: LawNetArtifactMetadata[];
};

Do not expose raw source payloads through the public BFF in this issue.

Case IDs and artifact IDs

Treat IDs as opaque at the API/frontend boundary.

Recommended:

  • caseId = base64url(sourceKey).
  • Filesystem artifact IDs are opaque values derived from mock artifact keys.
  • Postgres artifact IDs are opaque values derived from database artifact IDs.
  • API routes should not assume that source keys are URL-safe.

PostgreSQL schema

Add services/flyway/sql/V2__lawnet_storage.sql.

Recommended first-cut tables:

CREATE SCHEMA IF NOT EXISTS catalog;

CREATE TABLE catalog.lawnet_source_record (
  source_key text PRIMARY KEY,
  source_url text NULL,
  citation text NULL,
  title text NULL,
  decision_date date NULL,
  court text NULL,
  case_number text NULL,
  availability text NOT NULL DEFAULT 'unknown'
    CHECK (availability IN ('available', 'unavailable', 'unknown')),
  record_status text NOT NULL DEFAULT 'ok'
    CHECK (record_status IN ('ok', 'incomplete', 'malformed')),
  issue_summary text[] NOT NULL DEFAULT ARRAY[]::text[],
  raw_payload jsonb NOT NULL DEFAULT '{}'::jsonb,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE catalog.lawnet_document_rendition (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  source_key text NOT NULL REFERENCES catalog.lawnet_source_record(source_key) ON DELETE CASCADE,
  document_key text NOT NULL,
  rendition_kind text NOT NULL CHECK (rendition_kind IN ('judgment', 'slr', 'unknown')),
  title text NULL,
  source_url text NULL,
  is_primary boolean NOT NULL DEFAULT false,
  raw_payload jsonb NOT NULL DEFAULT '{}'::jsonb,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE (source_key, document_key)
);

CREATE TABLE catalog.lawnet_artifact (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  source_key text NOT NULL REFERENCES catalog.lawnet_source_record(source_key) ON DELETE CASCADE,
  rendition_id bigint NULL REFERENCES catalog.lawnet_document_rendition(id) ON DELETE SET NULL,
  artifact_key text NOT NULL,
  artifact_kind text NOT NULL CHECK (artifact_kind IN ('html', 'pdf')),
  content_type text NOT NULL,
  filename text NOT NULL,
  byte_length bigint NOT NULL CHECK (byte_length >= 0),
  sha256 text NULL,
  content bytea NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE (source_key, artifact_key)
);

Add indexes for list/detail queries:

CREATE INDEX law_net_source_record_decision_date_idx
  ON catalog.lawnet_source_record (decision_date DESC NULLS LAST, source_key);

CREATE INDEX law_net_source_record_court_idx
  ON catalog.lawnet_source_record (court);

CREATE INDEX law_net_source_record_availability_idx
  ON catalog.lawnet_source_record (availability);

CREATE INDEX law_net_document_source_key_idx
  ON catalog.lawnet_document_rendition (source_key);

CREATE INDEX law_net_artifact_source_key_idx
  ON catalog.lawnet_artifact (source_key);

Decision: store first-cut Postgres artifact bytes in bytea. This keeps #37 self-contained and avoids premature object-storage or host-volume design.

Filesystem storage

Use:

services/web/mock/lawnet/cases.json
services/web/mock/lawnet/artifacts/

The JSON should mirror the storage contract rather than the final LawNet API.

Security requirements:

  • Reject absolute artifact paths.
  • Reject paths containing ...
  • Resolve artifact paths under LAWNET_FILE_STORAGE_ROOT.
  • Verify the resolved path stays inside the storage root.
  • Return 404 for missing files.
  • Never derive a filesystem path directly from request params.

Postgres storage

Use pg, already present in services/web/package.json.

Implementation requirements:

  • Lazily create a connection pool from DATABASE_URL.
  • Keep SQL local to postgres-storage.server.ts.
  • Map DB rows into the exact same contract types as filesystem storage.
  • Empty tables should return an empty catalog, not a server error.
  • Sort catalog results by decision_date DESC NULLS LAST, then source_key ASC.
  • Support nullable fields throughout.

Pagination:

  • limit default: 50.
  • limit max: 100.
  • cursor should be opaque.
  • First cut may use base64url-encoded offset internally.
  • Response field should be nextCursor, not offset.

BFF API routes

Add:

GET /api/lawnet/cases
GET /api/lawnet/cases/$caseId
GET /api/lawnet/artifacts/$artifactId

GET /api/lawnet/cases response:

{
  "items": [],
  "nextCursor": null,
  "storageBackend": "filesystem"
}

Supported query params:

q
court
availability
from
to
limit
cursor

q can be simple in #37:

  • Filesystem: case-insensitive match over citation/title/case number.
  • Postgres: ILIKE over citation/title/case number.

GET /api/lawnet/cases/$caseId response:

{
  "case": { ... }
}

Return 404 when unknown.

GET /api/lawnet/artifacts/$artifactId:

  • Return artifact bytes.
  • Set Content-Type from metadata.
  • Set Content-Disposition: attachment; filename="...".
  • Set X-Content-Type-Options: nosniff.
  • Return 404 when unknown.

Do not render raw HTML artifacts inline in this issue.

Frontend routes

Add:

/lawnet
/lawnet/$caseId

Dashboard requirements:

  • Show selected storage backend as a small developer/status line.
  • Provide search input.
  • Provide simple filters for court and availability if straightforward.
  • Show citation, title, decision date, court, availability, record status, and artifact flags.
  • Link each row/card to /lawnet/$caseId.
  • Show clear empty/error states.

Detail page requirements:

  • Show citation, title, decision date, court, case number, availability.
  • Show recordStatus and issueSummary prominently when present.
  • Show source URL when present.
  • Show renditions with kind, title, and primary marker.
  • Show artifact download links for HTML/PDF.
  • Do not use dangerouslySetInnerHTML.
  • Raw HTML rendering/sanitized reader belongs to a later issue.

The / route may remain a landing page and link to /lawnet.

Tests

Minimum useful coverage:

  • Config selection: filesystem/postgres and unknown backend handling.
  • Filesystem storage: list cases, load detail, load artifact, reject path traversal.
  • Postgres storage: row-to-contract mapping as pure tests.
  • Optional Postgres integration tests guarded by WEB_TEST_DATABASE_URL.
  • Artifact response headers and 404 behavior.
  • API handler tests by calling handler/helper functions directly.

Avoid adding heavy browser test dependencies in this issue unless necessary.

Verification commands

Run at least:

cd services/web && pnpm test
cd services/web && pnpm typecheck
cd services/web && pnpm lint
cd services/web && pnpm build
just check-compose

If migrations are changed, also verify Flyway through the existing project workflow.

Explicitly out of scope

Do not implement in #37:

  • LawNet live HTTP client.
  • LawNet discovery command.
  • LawNet document fetch jobs.
  • PDF validation against live responses.
  • Durable queue/scheduler hardening.
  • eLitigation.
  • Sanitized HTML reader.
  • Full-text search.

Agreed decisions

  • Store first-cut Postgres artifact bytes in bytea.
  • Raw HTML artifacts download only in #37; no trusted inline rendering.
  • Standalone pnpm dev should default to filesystem storage.
  • Compose should default to Postgres storage.
  • This plan should be kept on #37 so implementation agents can follow it layer by layer.
## Implementation plan This issue is planning-only for the BFF/frontend/storage-contract slice. Do not implement LawNet live ingestion here. ## Core goal Build the LawNet catalog and case-detail product surface through a storage abstraction that can run against either local mock files or PostgreSQL. The implementation should deliver: - A LawNet dashboard/list page. - A LawNet case detail page. - BFF API routes used by those pages. - A shared storage interface with two implementations: filesystem and PostgreSQL. - Filesystem storage that runs without Docker Compose or PostgreSQL. - PostgreSQL schema and storage implementation that #38 ingestion will populate. ## Important design decision Use class-based dependency injection, not a backend parameter passed through every function. Define a shared interface and inject an implementation at the server boundary: ```ts export interface LawNetStorage { listCases(input: ListLawNetCasesInput): Promise<ListLawNetCasesResult>; getCase(caseId: string): Promise<LawNetCaseDetail | null>; getArtifact(artifactId: string): Promise<LawNetArtifactContent | null>; } ``` Concrete classes: ```ts export class FileSystemLawNetStorage implements LawNetStorage { ... } export class PostgresLawNetStorage implements LawNetStorage { ... } ``` Factory: ```ts export function createLawNetStorageFromEnv(): LawNetStorage { ... } ``` API/query handlers should receive or create a `LawNetStorage` instance and call the common interface. Do not thread `storageBackend` or `usePostgres` style parameters through individual methods. ## Proposed file layout ```text services/web/src/server/lawnet/ artifact-response.server.ts config.server.ts filesystem-storage.server.ts postgres-storage.server.ts storage.server.ts types.ts services/web/src/routes/api/lawnet/cases.ts services/web/src/routes/api/lawnet/cases.$caseId.ts services/web/src/routes/api/lawnet/artifacts.$artifactId.ts services/web/src/routes/lawnet.tsx services/web/src/routes/lawnet.$caseId.tsx services/web/mock/lawnet/cases.json services/web/mock/lawnet/artifacts/*.html services/web/mock/lawnet/artifacts/*.pdf services/flyway/sql/V2__lawnet_storage.sql ``` All storage/config modules should import `@tanstack/react-start/server-only` so they cannot enter the client bundle. ## Environment variables Use one explicit backend selector: ```dotenv LAWNET_STORAGE_BACKEND=filesystem ``` Allowed values: ```text filesystem postgres ``` Recommended defaults: - Standalone `pnpm dev`: default to `filesystem`. - Compose: default to `postgres`. - `postgres` requires `DATABASE_URL`. - `filesystem` uses `LAWNET_FILE_STORAGE_ROOT`, defaulting to `./mock/lawnet` from `services/web`. Add to `.env.example`: ```dotenv LAWNET_STORAGE_BACKEND=postgres LAWNET_FILE_STORAGE_ROOT=./mock/lawnet ``` Pass both through the web service in `compose.yaml`. ## Storage contract types Use typed nullable fields so malformed/incomplete records remain visible. Recommended enum unions: ```ts export type LawNetStorageBackend = "filesystem" | "postgres"; export type LawNetAvailability = "available" | "unavailable" | "unknown"; export type LawNetRecordStatus = "ok" | "incomplete" | "malformed"; export type LawNetRenditionKind = "judgment" | "slr" | "unknown"; export type LawNetArtifactKind = "html" | "pdf"; ``` Summary fields: ```ts export type LawNetCaseSummary = { caseId: string; sourceKey: string; citation: string | null; title: string | null; decisionDate: string | null; court: string | null; caseNumber: string | null; availability: LawNetAvailability; recordStatus: LawNetRecordStatus; issueSummary: string[]; hasHtml: boolean; hasPdf: boolean; updatedAt: string | null; }; ``` Detail fields: ```ts export type LawNetCaseDetail = LawNetCaseSummary & { sourceUrl: string | null; renditions: LawNetRendition[]; artifacts: LawNetArtifactMetadata[]; }; ``` Do not expose raw source payloads through the public BFF in this issue. ## Case IDs and artifact IDs Treat IDs as opaque at the API/frontend boundary. Recommended: - `caseId = base64url(sourceKey)`. - Filesystem artifact IDs are opaque values derived from mock artifact keys. - Postgres artifact IDs are opaque values derived from database artifact IDs. - API routes should not assume that source keys are URL-safe. ## PostgreSQL schema Add `services/flyway/sql/V2__lawnet_storage.sql`. Recommended first-cut tables: ```sql CREATE SCHEMA IF NOT EXISTS catalog; CREATE TABLE catalog.lawnet_source_record ( source_key text PRIMARY KEY, source_url text NULL, citation text NULL, title text NULL, decision_date date NULL, court text NULL, case_number text NULL, availability text NOT NULL DEFAULT 'unknown' CHECK (availability IN ('available', 'unavailable', 'unknown')), record_status text NOT NULL DEFAULT 'ok' CHECK (record_status IN ('ok', 'incomplete', 'malformed')), issue_summary text[] NOT NULL DEFAULT ARRAY[]::text[], raw_payload jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE catalog.lawnet_document_rendition ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, source_key text NOT NULL REFERENCES catalog.lawnet_source_record(source_key) ON DELETE CASCADE, document_key text NOT NULL, rendition_kind text NOT NULL CHECK (rendition_kind IN ('judgment', 'slr', 'unknown')), title text NULL, source_url text NULL, is_primary boolean NOT NULL DEFAULT false, raw_payload jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (source_key, document_key) ); CREATE TABLE catalog.lawnet_artifact ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, source_key text NOT NULL REFERENCES catalog.lawnet_source_record(source_key) ON DELETE CASCADE, rendition_id bigint NULL REFERENCES catalog.lawnet_document_rendition(id) ON DELETE SET NULL, artifact_key text NOT NULL, artifact_kind text NOT NULL CHECK (artifact_kind IN ('html', 'pdf')), content_type text NOT NULL, filename text NOT NULL, byte_length bigint NOT NULL CHECK (byte_length >= 0), sha256 text NULL, content bytea NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (source_key, artifact_key) ); ``` Add indexes for list/detail queries: ```sql CREATE INDEX law_net_source_record_decision_date_idx ON catalog.lawnet_source_record (decision_date DESC NULLS LAST, source_key); CREATE INDEX law_net_source_record_court_idx ON catalog.lawnet_source_record (court); CREATE INDEX law_net_source_record_availability_idx ON catalog.lawnet_source_record (availability); CREATE INDEX law_net_document_source_key_idx ON catalog.lawnet_document_rendition (source_key); CREATE INDEX law_net_artifact_source_key_idx ON catalog.lawnet_artifact (source_key); ``` Decision: store first-cut Postgres artifact bytes in `bytea`. This keeps #37 self-contained and avoids premature object-storage or host-volume design. ## Filesystem storage Use: ```text services/web/mock/lawnet/cases.json services/web/mock/lawnet/artifacts/ ``` The JSON should mirror the storage contract rather than the final LawNet API. Security requirements: - Reject absolute artifact paths. - Reject paths containing `..`. - Resolve artifact paths under `LAWNET_FILE_STORAGE_ROOT`. - Verify the resolved path stays inside the storage root. - Return `404` for missing files. - Never derive a filesystem path directly from request params. ## Postgres storage Use `pg`, already present in `services/web/package.json`. Implementation requirements: - Lazily create a connection pool from `DATABASE_URL`. - Keep SQL local to `postgres-storage.server.ts`. - Map DB rows into the exact same contract types as filesystem storage. - Empty tables should return an empty catalog, not a server error. - Sort catalog results by `decision_date DESC NULLS LAST`, then `source_key ASC`. - Support nullable fields throughout. Pagination: - `limit` default: `50`. - `limit` max: `100`. - `cursor` should be opaque. - First cut may use base64url-encoded offset internally. - Response field should be `nextCursor`, not `offset`. ## BFF API routes Add: ```text GET /api/lawnet/cases GET /api/lawnet/cases/$caseId GET /api/lawnet/artifacts/$artifactId ``` `GET /api/lawnet/cases` response: ```json { "items": [], "nextCursor": null, "storageBackend": "filesystem" } ``` Supported query params: ```text q court availability from to limit cursor ``` `q` can be simple in #37: - Filesystem: case-insensitive match over citation/title/case number. - Postgres: `ILIKE` over citation/title/case number. `GET /api/lawnet/cases/$caseId` response: ```json { "case": { ... } } ``` Return `404` when unknown. `GET /api/lawnet/artifacts/$artifactId`: - Return artifact bytes. - Set `Content-Type` from metadata. - Set `Content-Disposition: attachment; filename="..."`. - Set `X-Content-Type-Options: nosniff`. - Return `404` when unknown. Do not render raw HTML artifacts inline in this issue. ## Frontend routes Add: ```text /lawnet /lawnet/$caseId ``` Dashboard requirements: - Show selected storage backend as a small developer/status line. - Provide search input. - Provide simple filters for court and availability if straightforward. - Show citation, title, decision date, court, availability, record status, and artifact flags. - Link each row/card to `/lawnet/$caseId`. - Show clear empty/error states. Detail page requirements: - Show citation, title, decision date, court, case number, availability. - Show `recordStatus` and `issueSummary` prominently when present. - Show source URL when present. - Show renditions with kind, title, and primary marker. - Show artifact download links for HTML/PDF. - Do not use `dangerouslySetInnerHTML`. - Raw HTML rendering/sanitized reader belongs to a later issue. The `/` route may remain a landing page and link to `/lawnet`. ## Tests Minimum useful coverage: - Config selection: filesystem/postgres and unknown backend handling. - Filesystem storage: list cases, load detail, load artifact, reject path traversal. - Postgres storage: row-to-contract mapping as pure tests. - Optional Postgres integration tests guarded by `WEB_TEST_DATABASE_URL`. - Artifact response headers and `404` behavior. - API handler tests by calling handler/helper functions directly. Avoid adding heavy browser test dependencies in this issue unless necessary. ## Verification commands Run at least: ```sh cd services/web && pnpm test cd services/web && pnpm typecheck cd services/web && pnpm lint cd services/web && pnpm build just check-compose ``` If migrations are changed, also verify Flyway through the existing project workflow. ## Explicitly out of scope Do not implement in #37: - LawNet live HTTP client. - LawNet discovery command. - LawNet document fetch jobs. - PDF validation against live responses. - Durable queue/scheduler hardening. - eLitigation. - Sanitized HTML reader. - Full-text search. ## Agreed decisions - Store first-cut Postgres artifact bytes in `bytea`. - Raw HTML artifacts download only in #37; no trusted inline rendering. - Standalone `pnpm dev` should default to filesystem storage. - Compose should default to Postgres storage. - This plan should be kept on #37 so implementation agents can follow it layer by layer.
Author
Owner

Split into three subtasks:

  • [37.1] Frontend‑only local mock for LawNet BFF (#48) — storage contract + file‑system provider + tests, no Postgres, no UI.
  • [37.2] PostgreSQL dependency injection for LawNet BFF storage (#49) — env‑var switch, PostgreSQL provider, Flyway schema, Postgres‑backed tests, no UI.
  • [37.3] Debug dashboard UI for LawNet BFF (#50) — /debug dashboard and case‑detail pages, lightweight debug UI.
Split into three subtasks: - **[37.1] Frontend‑only local mock for LawNet BFF** (#48) — storage contract + file‑system provider + tests, no Postgres, no UI. - **[37.2] PostgreSQL dependency injection for LawNet BFF storage** (#49) — env‑var switch, PostgreSQL provider, Flyway schema, Postgres‑backed tests, no UI. - **[37.3] Debug dashboard UI for LawNet BFF** (#50) — `/debug` dashboard and case‑detail pages, lightweight debug UI.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dernbu/lawnet-scraper#37
No description provided.