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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ STREAMING_AVAILABILITY_MONTHLY_CAP=800
LAKE_ROOT=data/lake
DATABASE_PATH=data/warehouse.duckdb
WATCHPULSE_SERVING_DB_PATH=data/warehouse_serving.duckdb
WATCHPULSE_FRONTEND_ORIGINS=http://127.0.0.1:5173,http://localhost:5173

# Product defaults. Regions use ISO 3166-1 alpha-2 codes.
DEFAULT_REGION=GR
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,35 @@ jobs:
dbt deps
dbt debug
dbt parse

frontend:
name: Frontend quality, tests, and build
runs-on: ubuntu-latest
timeout-minutes: 10

defaults:
run:
working-directory: frontend

steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install
run: npm ci

- name: Lint and typecheck
run: npm run lint && npm run typecheck

- name: Test
run: npm test

- name: Build
run: npm run build
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ warehouse/.user.yml
logs/
warehouse/package-lock.yml
.vscode
frontend/node_modules/
frontend/dist/
frontend/coverage/
frontend/.env.local
23 changes: 22 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
PYTHON ?= python

.PHONY: install install-hooks dbt-deps dbt-debug dbt-parse dbt-validate dbt-publish lint precommit pre-commit prepush pre-push syntax test coverage ci
.PHONY: install install-hooks api-dev dbt-deps dbt-debug dbt-parse dbt-validate dbt-publish frontend-install frontend-dev frontend-lint frontend-test frontend-build frontend-ci lint precommit pre-commit prepush pre-push syntax test coverage ci

install:
$(PYTHON) -m pip install -e ".[dev,warehouse]"

install-hooks:
$(PYTHON) -m pre_commit install --hook-type pre-commit --hook-type pre-push --hook-type commit-msg

api-dev:
uvicorn watchpulse.api:create_app --factory --reload

frontend-install:
cd frontend && npm install

frontend-dev:
cd frontend && npm run dev

frontend-lint:
cd frontend && npm run lint
cd frontend && npm run typecheck

frontend-test:
cd frontend && npm test

frontend-build:
cd frontend && npm run build

frontend-ci: frontend-lint frontend-test frontend-build

dbt-deps:
cd warehouse && dbt deps

Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ offline tests and warehouse builds do not require it.
## Requirements

- Python 3.12 recommended (Python 3.10 or newer is required)
- Node.js 22 for v0.5 frontend development (`npm` is included with Node.js)
- A TMDB v3 API key
- `pyenv` and `pyenv-virtualenv` are optional

Expand Down Expand Up @@ -149,6 +150,31 @@ API reads only `WATCHPULSE_SERVING_DB_PATH`; requests never call TMDB or the
Streaming Availability API. See the [API guide](api/README.md) for routes,
filters, section semantics, and examples.

## Run the frontend

The Python installation does not provide Node.js. If `npm` is not found, use
the committed `frontend/.nvmrc` with Node Version Manager:

```bash
cd frontend
nvm install
nvm use
cd ..
```

You can instead install Node.js 22 through its official operating-system
installer. Keep the API running, then use a second terminal:

```bash
make frontend-install
make frontend-dev
```

Open `http://127.0.0.1:5173`. The v0.5 application shell reads API health and
catalog freshness from FastAPI, proving the complete local serving path. The
browser never calls either upstream data provider. See the
[frontend guide](frontend/README.md) for configuration and quality commands.

## Dependency management

`pyproject.toml` is the canonical project configuration. It contains package
Expand Down
22 changes: 22 additions & 0 deletions api/tests/test_foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ async def request() -> httpx.Response:
return asyncio.run(request())


def _options(app: FastAPI, path: str, origin: str) -> httpx.Response:
async def request() -> httpx.Response:
transport = httpx.ASGITransport(app=app)
headers = {
"Origin": origin,
"Access-Control-Request-Method": "GET",
}
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
return await client.options(path, headers=headers)

return asyncio.run(request())


def _create_catalog(path: Path) -> None:
with duckdb.connect(str(path)) as connection:
connection.execute("create schema main_marts")
Expand Down Expand Up @@ -100,6 +113,15 @@ def test_settings_configure_the_serving_database_path() -> None:
assert settings.serving_database_path == Path("/tmp/watchpulse-api.duckdb")


def test_local_frontend_origin_can_read_the_api(tmp_path: Path) -> None:
app = create_app(repository=CatalogRepository(tmp_path / "missing.duckdb"))

response = _options(app, "/health", "http://127.0.0.1:5173")

assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "http://127.0.0.1:5173"


def test_openapi_documents_the_foundation_endpoints(tmp_path: Path) -> None:
app = create_app(repository=CatalogRepository(tmp_path / "missing.duckdb"))

Expand Down
27 changes: 26 additions & 1 deletion docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,36 @@ and ranking remain fixed business rules layered onto the same engine. Adding a
new serving database requires replacing the repository implementation, not the
HTTP or filter contracts.

## ADR-018: React and TypeScript for the discovery frontend

- Status: accepted for v0.5
- Date: 2026-08-23

Decision: build the guest discovery interface as a React 19 single-page
application using TypeScript and Vite. The frontend owns presentation and
browser-local preferences, while all availability, filtering, and ranking stay
behind the FastAPI contract. Vitest and Testing Library cover user-visible
behavior. Production dependencies are kept deliberately small.

Why: the interface has shared filter state, five reactive discovery rails, and
title details, which benefit from component composition and typed API
contracts. Vite provides a fast development/build loop without introducing a
full-stack JavaScript server that would duplicate FastAPI responsibilities.

Consequences:

- Node.js 22 is the supported development and CI runtime;
- the browser calls only the configured WatchPulse API base URL;
- FastAPI permits an explicit environment-configured list of frontend origins;
- SEO pre-rendering remains possible later, but is not required for the local
deterministic MVP;
- authentication, server-side rendering, and a UI component framework are not
introduced by this decision.

## Open decisions

These remain unresolved and should receive new ADRs when decided:

- frontend framework for the production-looking MVP;
- hosting platform and atomic DuckDB artifact delivery;
- initial public regions and providers beyond the configurable Greece default;
- measured threshold for moving serving from DuckDB to Postgres.
5 changes: 5 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ the [v0.4 release notes](releases/v0.4.md).

Goal: deliver the complete deterministic user experience locally.

Current status: **in progress (2026-08-23)**. React, TypeScript, and Vite are
selected in ADR-018. The first increment establishes the responsive application
shell, typed local API client, catalog readiness state, explicit development
CORS policy, frontend tests, and CI build validation.

Scope:

- choose and document the frontend framework;
Expand Down
1 change: 1 addition & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_BASE_URL=http://127.0.0.1:8000
1 change: 1 addition & 0 deletions frontend/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
53 changes: 49 additions & 4 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,51 @@
# Frontend
# WatchPulse frontend

Planned deterministic discovery UI for `v0.5`.
The WatchPulse guest discovery interface is a React, TypeScript, and Vite
single-page application. It calls only the local FastAPI discovery API; it
never calls TMDB or Streaming Availability directly.

The framework will be selected through an ADR. This component calls only the
WatchPulse API and may persist guest preferences in browser-local storage.
## Local development

The frontend requires Node.js 22, which includes `npm`. If `npm` is not found,
install Node.js first. With `nvm`:

```bash
cd frontend
nvm install
nvm use
node --version
npm --version
cd ..
```

Alternatively, install Node.js 22 using the official installer for your
operating system. Python's `requirements.txt` cannot install Node.js or npm.

Start the API from the repository root:

```bash
make dbt-publish
make api-dev
```

In another terminal, install and start the frontend:

```bash
make frontend-install
make frontend-dev
```

Open <http://127.0.0.1:5173>. The initial application shell displays the API
and catalog status to prove the browser-to-backend path is working.

Vite reads `VITE_API_BASE_URL`; copy `.env.example` to `.env.local` only when
the backend is not available at the default `http://127.0.0.1:8000` address.

## Quality commands

```bash
npm run lint
npm run typecheck
npm test
npm run build
```
19 changes: 19 additions & 0 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";

export default tseslint.config(
{ ignores: ["dist", "coverage"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: { ecmaVersion: 2022, globals: globals.browser },
plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh },
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }]
}
}
);
14 changes: 14 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#090b10" />
<meta name="description" content="Find what to watch on your streaming services." />
<title>WatchPulse</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading