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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion dev-docs/SPEC-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,16 @@ This prints a `dsk_`-prefixed raw key (hand to the client) and the `label:hash`
|--------|---------|
| `401` | Missing or invalid API key (on any endpoint except `/status`) |

**Clients.** The Python SDK sends the header automatically when given a key: pass `DatastreamClient(api_key=...)` (or the module-level `get_data(..., api_key=...)`), set it globally via `configure(api_key=...)`, or export `DATASTREAM_API_KEY`. The browser frontend does **not** yet send a key, so its requests currently return `401`; browser auth is handled in a later change (see `SPEC-frontend.md`).
**Clients.** The Python SDK sends the header automatically when given a key: pass `DatastreamClient(api_key=...)` (or the module-level `get_data(..., api_key=...)`), set it globally via `configure(api_key=...)`, or export `DATASTREAM_API_KEY`. The browser frontend prompts the user for a key and sends it on every request (see `SPEC-frontend.md`).

### CORS

The browser frontend is served from GitHub Pages — a different origin than the API — so `main.py` adds Starlette's `CORSMiddleware`:

- **Allowed origins** come from the `CORS_ALLOW_ORIGINS` env var (comma-separated exact origins). Default: `https://wat-street.github.io,http://localhost:5173` (the Pages origin and the local Vite dev server). Origins never include a path — the Pages origin is `https://wat-street.github.io`, not `.../datastream`.
- The middleware is added last, so it wraps outermost: preflight `OPTIONS` requests short-circuit before auth and request-context logging.
- `allow_credentials` stays off (auth is a bearer header, not cookies); allowed headers are `Authorization` and `Content-Type`; all methods are allowed.
- **Auth is unchanged.** CORS only permits browsers to send the `Authorization` header cross-origin; every non-public route still requires a valid API key.

### Datasets endpoint

Expand Down
109 changes: 64 additions & 45 deletions dev-docs/SPEC-frontend.md
Original file line number Diff line number Diff line change
@@ -1,114 +1,133 @@
# Datastream Frontend

A lightweight internal UI built with Svelte 5 + Vite, using Bun as the package manager.
A lightweight internal UI built with React 19 + TypeScript + Vite, using Bun as the package manager and JS runtime (`bunx --bun vite`, so the installer and runtime always agree on native-binding architecture).

- **Local dev**: `just frontend-dev` starts the Vite dev server on port 5173. The Vite config proxies `/api` to `http://localhost:3000` (the Python FastAPI server) to avoid CORS issues.
- **Docker**: the frontend is built as static files (`bun run build`) and served by nginx on port 80. nginx proxies `/api` to `http://builder:3000` via Docker internal DNS.
- The frontend is not containerized -- it runs locally via `just frontend-dev` and proxies to the backend container on port 3000.
- **Production**: deployed to **GitHub Pages** at `https://wat-street.github.io/datastream/` by `.github/workflows/deploy-pages.yml` (see "Deployment" below). The frontend is not containerized and nothing in `infra/` serves it — Caddy only proxies the API.

## Design

This is a purely internal tool. The goal is functional clarity, not aesthetics.

- **No CSS framework or component library** -- scoped Svelte `<style>` blocks + a global `app.css` for resets and CSS custom properties
- **Dark theme** -- dark background with light text, using CSS custom properties for all colors
- **No custom fonts** -- system font stack (Inter fallback to system sans-serif, JetBrains Mono fallback to system monospace)
- **Layout** -- single-column, max-width 1200px centered; no sidebars or complex layouts
- **Spacing** -- CSS custom properties (`--space-xs` through `--space-xl`) for consistent spacing
- **Interactive states** -- subtle hover transitions on clickable rows and buttons
- **Tailwind CSS v4 + shadcn/ui** (new-york style, CSS variables) — theme tokens live in `src/index.css`
- **Dark-only theme** — the dark palette is set directly on `:root`; no theme toggle, no `.dark` class
- **No custom fonts** — system font stack (Inter fallback to system sans-serif, JetBrains Mono fallback to system monospace)
- **Layout** — single-column, max-width 1200px centered; no sidebars or complex layouts

## Architecture

### Navigation

State-based navigation in `App.svelte` using Svelte 5 runes. Two views:
State-based navigation in `App.tsx`. Two views:
- `'list'`: dataset catalog (landing page)
- `'detail'`: dataset data viewer

No routing library. `view` and `selectedDataset` state variables control which component renders.
No routing library. A single discriminated-union `view` state controls which component renders. This also means GitHub Pages needs no SPA 404 fallback.

### File structure

```
frontend/src/
main.js # entrypoint, mounts App and imports global CSS
app.css # CSS reset, custom properties, base styles
App.svelte # root component: header + view switching
main.tsx # entrypoint: QueryClient (central 401 handling), providers, mount
index.css # tailwind v4 theme: shadcn tokens + json highlight colors
App.tsx # root component: header, api-key button, view switching
lib/
api.js # fetch wrappers for /api/v1 endpoints
format.js # date formatting, JSON syntax highlighting
api.ts # typed fetch client for /api/v1 (ApiError, bearer header)
api-key.ts # localStorage key storage + masking helper
format.ts # toISODate, defaultDateRange (5-year window), formatTimestamp
utils.ts # shadcn cn() helper
hooks/
use-api-key.tsx # ApiKeyProvider context + requestApiKey() bridge for non-react code
use-datasets.ts # TanStack Query hook for GET /datasets
use-dataset-data.ts # TanStack Query hook for GET /data
components/
DatasetList.svelte # landing page: fetches and lists all datasets
DatasetDetail.svelte # detail view: paginated data table + modal
DataTable.svelte # dynamic-column HTML table with rowspan support
JsonModal.svelte # modal overlay with syntax-highlighted JSON
ui/ # shadcn-generated components (button, table, dialog, form, ...)
dataset-list.tsx # landing page: all datasets + has_data dot
dataset-detail.tsx # detail view: paginated data table (50/page, newest first)
data-table.tsx # dynamic-column table with rowspan for multi-entry timestamps
json-view.tsx # recursive jsx syntax highlighter (no innerHTML)
json-modal.tsx # row details in a shadcn Dialog
api-key-dialog.tsx # zod + react-hook-form key entry dialog
```

### Dependencies

Zero additional runtime dependencies beyond Svelte 5 and Vite.
Runtime: React 19, TanStack Query (fetch state, caching, retries, central error handling), react-hook-form + zod + `@hookform/resolvers` (forms/validation), shadcn/ui's underlying packages (`radix-ui`, `lucide-react`, `sonner`, `class-variance-authority`, `clsx`, `tailwind-merge`).

### Tooling and quality gates

Frontend developer tooling is managed in `frontend/package.json` with Bun scripts:
Frontend developer tooling is managed in `frontend/package.json` with Bun scripts (same script names as the previous Svelte app, so pre-commit hooks and CI are unchanged):

- `bun run format:check` -- Prettier check for all frontend files
- `bun run format:write` -- Prettier write mode
- `bun run lint` -- ESLint (flat config + `eslint-plugin-svelte`)
- `bun run typecheck` -- `svelte-check --workspace ./src --no-tsconfig` (Svelte-focused checks)
- `bun run build` -- Vite production build
- `bun run format:check` / `format:write` -- Prettier (with the tailwind class-sorting plugin)
- `bun run lint` -- ESLint (flat config: typescript-eslint + react-hooks + react-refresh)
- `bun run typecheck` -- `tsc -b` (project references: app + node configs)
- `bun run build` -- Vite production build (under the bun runtime)

Repository git hooks are managed via root `.pre-commit-config.yaml` and split by stage:

- **pre-commit** (fast checks): `frontend-format-check`, `frontend-eslint`
- **pre-push** (heavier checks): `frontend-typecheck`, `frontend-build`

Both frontend hook types are installed by default (`default_install_hook_types: [pre-commit, pre-push]`).

CI mirrors local frontend gates in `.github/workflows/frontend-ci.yml`:

- installs Bun
- runs `bun install --frozen-lockfile` in `frontend/`
- runs frontend hook ids directly for both `pre-commit` and `pre-push` stages
CI mirrors local frontend gates in `.github/workflows/frontend-ci.yml` (installs Bun, `bun install --frozen-lockfile`, runs the hook ids for both stages).

### API integration

All API calls go through `lib/api.js` and use the `/api/v1` prefix (proxied to the backend by Vite in dev).
All API calls go through `lib/api.ts` and use the `/api/v1` prefix.

- **Base URL**: `VITE_API_BASE_URL` (baked in at build time, set for Pages builds) or relative `/api` in dev, where Vite proxies to localhost:3000
- `fetchDatasets()` -- `GET /api/v1/datasets`
- `fetchData(name, version, start, end)` -- `GET /api/v1/data/{name}/{version}?start=...&end=...&build-data=false`

The frontend is read-only and never triggers builds (`build-data=false` always). Both 200 and 206 responses are treated as valid (206 indicates partial/incomplete data).
The frontend is read-only and never triggers builds (`build-data=false` always). Both 200 and 206 responses are treated as valid (206 indicates partial/incomplete data). Failures throw `ApiError` (carries the HTTP status) so the query layer can react per-status.

### API-key auth

The backend requires `Authorization: Bearer <dsk_...>` on everything except `/status`. Since Pages is a static host, the key is supplied by the user:

- The **api-key dialog** (zod-validated, `dsk_` prefix) auto-opens on first load when no key is stored, and can be reopened anytime from the header button. It shows a masked view of the stored key and offers a clear button.
- The key is stored in **localStorage** (`datastream.api_key`) and attached to every request by `lib/api.ts`. Saving a key invalidates all queries so they refetch immediately.
- **Central 401 handling**: the QueryClient's `QueryCache.onError` turns any 401 into a toast and reopens the dialog. Retries skip 401/403/404.
- Caveat: localStorage is readable by any script on the page (XSS). Accepted for an internal tool; the JSX-based `json-view` (no `innerHTML` anywhere) is part of keeping that risk low.

> **Note:** the backend now requires an API key on all endpoints except `/status`. The frontend does **not** yet send one, so its data requests currently return `401`. Browser auth (injecting the key server-side so it never enters the bundle) is deferred to a later change.
## Deployment (GitHub Pages)

`.github/workflows/deploy-pages.yml` runs on pushes to `main` touching `frontend/**` (or manual dispatch): bun install + build, then `actions/deploy-pages` publishes `frontend/dist`.

- `vite.config.ts` sets `base: "/datastream/"` for production builds only; local dev stays at `/`
- One-time repo setup: **Settings → Pages → Source = "GitHub Actions"**, and an Actions **variable** `VITE_API_BASE_URL` pointing at the https API origin (the Caddy `DOMAIN`; no trailing slash, no `/api/v1`)
- Pages serves over https, so the API origin must also be https (mixed content is blocked; localhost is exempt)
- The API must allow the Pages origin via CORS — see "CORS" in `SPEC-backend.md`

## Features

### Dataset list (landing page)

`DatasetList.svelte` fetches all datasets on mount and displays them in a table with columns: name, version, and a green/gray dot indicating whether data exists in the database. Rows are clickable and navigate to the detail view. Handles loading, error (with retry button), and empty states.
`dataset-list.tsx` fetches all datasets and displays them in a table with columns: name, version, and a green/gray dot indicating whether data exists in the database. Rows are clickable and navigate to the detail view. Handles loading, error (with retry button), and empty states.

### Dataset detail view

`DatasetDetail.svelte` shows data for a single dataset with client-side pagination.
`dataset-detail.tsx` shows data for a single dataset with client-side pagination.

- **Auto-fetch**: fetches all existing data on mount (5-year window, `build-data=false`)
- **Pagination**: 50 rows per page, newest data first. "newer" / "older" buttons to navigate pages
- **Metadata**: shows total timestamp count and current page number
- **Metadata**: shows returned timestamp count and current page number
- **Error handling**: error banner with retry button

### Data table

`DataTable.svelte` renders dataset rows as an HTML table with dynamically derived column headers (computed from the keys of the first data entry).
`data-table.tsx` renders dataset rows as an HTML table with dynamically derived column headers (computed from the keys of the first data entry).

- Multi-row timestamps (e.g. multiple tickers at the same timestamp) use `rowspan` on the timestamp cell
- All rows are clickable and open the JSON modal
- Horizontally scrollable for wide datasets

### JSON detail modal

`JsonModal.svelte` displays a clicked row's data as syntax-highlighted JSON in a modal overlay.
`json-modal.tsx` displays a clicked row's data as syntax-highlighted JSON in a shadcn Dialog.

- Highlighting is done by `json-view.tsx`, a recursive JSX renderer (colors keys, strings, numbers, booleans, null) — no HTML strings, no `dangerouslySetInnerHTML`
- Closes on: Escape key, backdrop click, or close button (built into the Dialog)

## Planned (not yet implemented)

- CSS-based syntax highlighting using `highlightJson()` from `lib/format.js` -- colors keys, strings, numbers, booleans, and null values
- Closes on: Escape key, backdrop click, or close button
- Scrollable for large data objects
- **Create-dataset UI**: a form for name/version/calendar/granularity/start-date/schema/dependencies plus a builder-script editor (CodeMirror). Blocked on approval of the corresponding backend endpoint, which changes the builder trust model (script upload = code execution gated by API key instead of git review).
Loading