diff --git a/.gitignore b/.gitignore index 9302638..12a5ef7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -# Secretos. Las credenciales de Supabase viven en .env (service_role, lado Python) y -# en frontend/.env.local (solo la clave anon). Ninguno de los dos se versiona jamás. +# Secretos. Las credenciales de Supabase viven en UN solo fichero, el .env de la raíz: +# la clave anon y la service_role. No se versiona jamás. (Antes había además un +# frontend/.env.local con la anon; se quitó porque dos copias se desincronizan solas, +# pero el patrón .env.* lo sigue cubriendo por si alguien lo recrea.) .env .env.* !.env.example @@ -29,6 +31,9 @@ dist/ .mypy_cache/ # Editor / SO +# settings.local.json son los permisos de herramientas de cada cual, no del proyecto: +# .claude/settings.json (compartido) sí se versionaría; el local nunca. +.claude/settings.local.json .vscode/ .idea/ *.swp diff --git a/.gitmessage b/.gitmessage index b80f64a..2a94017 100644 --- a/.gitmessage +++ b/.gitmessage @@ -1,7 +1,7 @@ # (<ámbito>): # # Tipos: feat fix docs style refactor test chore perf ci revert -# Ámbitos de este repo: sim perception planning control runner metrics configs scripts +# Ámbitos de este repo: front live sdk telemetry seed sql backend contrato vocabulario # # Cuerpo: explica el POR QUÉ, no el qué (el diff ya dice el qué). # diff --git a/AGENTS.md b/AGENTS.md index 106724e..4edab9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,7 @@ percepción. Este repo solo sabe de episodios, métricas y pantallas. backend/ SQL + SDK. Supabase ES el backend; no hay servidor propio. sql/001_schema.sql tablas, vistas y RLS sql/002_design.sql columnas y vistas que pide el diseño + sql/003_snapshots.sql las fotos del palé (solo la URL; los PNG van a Storage) theker_telemetry/ el SDK que importa quien produce episodios schema.py EpisodeResult, FAILURES, TASKS, RunWriter core.py cliente PostgREST y RunLog @@ -36,7 +37,7 @@ backend/ SQL + SDK. Supabase ES el backend; no hay servidor prop pyproject.toml frontend/ Next.js 16. Lee Supabase con la clave anon, bajo RLS. - app/ rutas: / (Live), /runs + app/ rutas: / (Live), /runs, /runs/[id], /runs/[id]/[seed] api/clients/ una función por endpoint de PostgREST api/hooks/ los mismos, envueltos en TanStack Query components/ atoms / organisms / screens @@ -49,8 +50,18 @@ docs/ BRIEFING-observabilidad.md qué se construye y por qué API.md el contrato: endpoints, tipos y vocabularios design/ los 11 tableros del diseño, a 1440 px + img/ capturas y variantes de marca que usa el README + +README.md cara pública del repo, en inglés: requisitos, instalación, + desarrollo, uso, dependencias con su auditoría de licencias. +CONTRIBUTING.md el flujo real: ramas, commits, comprobaciones y qué exige CI. +LICENSE MIT, 2026 STACKSPECT. ``` +Los tres anteriores están **en inglés** a propósito, y son los únicos: son lo que lee +quien llega de fuera. Todo lo demás —este documento, `docs/`, los comentarios— sigue en +castellano. + **La dependencia va en un solo sentido.** La simulación importa `theker_telemetry`; esta plataforma no sabe que MuJoCo existe. Por eso el esquema de métricas vive aquí y no allí: quien define la forma del dato es quien lo almacena, y así reescribir la @@ -64,6 +75,7 @@ simulación no se lleva por delante la observabilidad. # 1. Esquema: pegar en el SQL editor de Supabase, en orden. Son idempotentes. backend/sql/001_schema.sql backend/sql/002_design.sql +backend/sql/003_snapshots.sql # 2. Credenciales. .env en la raíz (gitignorado): # SUPABASE_URL=https://xxxx.supabase.co @@ -229,7 +241,9 @@ evolución en vez de borrón y cuenta nueva. Ningún otro archivo (CSS, TS, TSX) lleva `#hex`, `rgb()`, `hsl()` ni un color con nombre; consume `var(--…)`. Un color con transparencia es otra variable de ese archivo, calculada con `color-mix` desde la base. `npm run lint` lo comprueba (`scripts/check-colors.mjs`). -- Nombres de código en inglés; comentarios y documentos en castellano. +- Nombres de código en inglés; comentarios y documentos en castellano. Las tres + excepciones son `README.md`, `CONTRIBUTING.md` y `LICENSE`, que van en inglés porque + son lo que lee quien llega de fuera (§2). - Commits pequeños, con números cuando toquen comportamiento. - El diseño de referencia es `docs/design/observality-platform-design.html`: un bundle autoextraíble con 11 tableros. Se abre en el navegador. Manda en la **estructura y el @@ -254,7 +268,8 @@ evolución en vez de borrón y cuenta nueva. los tres estados no felices. El tamaño del palé sale de `config.pallet_size_m` vía `palletSize()`: **no** se dibuja siempre a 1200x800, porque el palé real puede ser una maqueta a escala y entonces todas las cotas saldrían mal por el mismo factor. -- **Pendiente:** las rutas `/runs/[id]` y `/runs/[id]/[seed]`. +- **Detalle de ejecución:** `/runs/[id]` y `/runs/[id]/[seed]` ya están, sobre el mismo + `EpisodeDashboard` que Live: cambia de dónde salen los datos, no cómo se ven. Aviso de Next 16: `params` es una Promise (`const { id } = await params`) y existen los tipos globales `PageProps<'/runs/[id]'>`. Hay documentación offline en diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6dc0532 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,265 @@ +# Contributing + +Thanks for taking an interest. This is a small, opinionated repository: a Supabase +schema, a dependency-free Python telemetry SDK, and a Next.js frontend. Most of the +friction a newcomer hits here comes from rules that already exist and are enforced by +CI, so this page front-loads them. + +Two documents outrank this one on matters of substance: + +- **[`AGENTS.md`](AGENTS.md)** — the permanent context of the repository: what it is, + how it is laid out, and the hard rules. Read it before writing code. If something + there is out of date, fix it in the same PR. +- **[`docs/API.md`](docs/API.md)** — the contract between the database and the + frontend: endpoints, types, and the closed vocabularies. + +Both are written in Spanish, as are all code comments and commit subjects. Issues and +pull requests in English are welcome too — see [Language](#language). + +--- + +## Branches + +`dev` is the integration branch. **Everything targets `dev`**, never `main`. + +``` +main ← dev ← your branch +``` + +- Branch off the latest `dev`: `git fetch origin dev && git switch -c feature/ origin/dev` +- Branch names in use: `feature/`, `fix/`. +- Open the pull request against `dev`. `main` only ever moves by merging `dev`. +- Keep commits small. A commit that changes behaviour should be readable on its own. + +## Commits + +Conventional commits, with the **subject written in Spanish, in the imperative, no +trailing period, ≤50 characters**. The repository ships a template — turn it on once: + +```bash +git config commit.template .gitmessage +``` + +Types: `feat` `fix` `docs` `style` `refactor` `test` `chore` `perf` `ci` `revert`. + +Scopes actually in use in this repository: `front`, `live`, `sdk`, `telemetry`, `seed`, +`sql`, `backend`, `contrato`, `vocabulario`. A scope is optional; a wrong one is worse +than none. + +The body explains **why**, not what — the diff already says what. + +**If the commit changes behaviour, put a number in it.** This repository exists to +measure improvement; a commit that claims one without evidence is the thing it was +built to prevent: + +``` +feat(planner): reordena la cola por masa descendente + +nivel 2: 62 % -> 81 % de bultos colocados (n=50) +``` + +Real examples from the history: + +``` +fix(live): el aviso de conexion perdida saltaba sin haberse perdido nada +feat(front): pantalla de detalle de una ejecución (#4) +refactor(front): un solo .env, el de la raiz +test(contrato): vocabulario de fallos cuadrado en los tres sitios +``` + +Footer: `Closes #123`, `BREAKING CHANGE: …`. + +--- + +## Getting set up + +You need **Node 22**, **Python 3.11+**, and a Supabase project you can write to. +(Docker only if you want to run the networked backend tests.) + +### 1. Credentials + +There is exactly **one** credentials file: `.env` at the repository root. Copy the +example and fill it in from *Supabase → Project Settings → Data API*: + +```bash +cp .env.example .env +``` + +```dotenv +SUPABASE_URL=https://xxxxxxxxxxxx.supabase.co +SUPABASE_ANON_KEY=eyJ... # public, read-only under RLS +SUPABASE_SERVICE_KEY=eyJ... # SECRET, Python side only +``` + +> [!WARNING] +> **`SUPABASE_SERVICE_KEY` bypasses row-level security.** It must never reach the +> browser and never be committed. `frontend/next.config.ts` publishes only an explicit +> whitelist of keys as `NEXT_PUBLIC_*`, and fails the build if a public variable's name +> smells like a secret. Do not widen that whitelist. There is no +> `frontend/.env.local` — a second copy of the same values desynchronises on its own, +> and it already did once. + +### 2. Schema + +The schema is applied **by hand**, in order, from the Supabase SQL editor. All three +files are idempotent, so pasting one again is safe: + +| File | What it adds | +|---|---| +| [`backend/sql/001_schema.sql`](backend/sql/001_schema.sql) | tables, views, RLS | +| [`backend/sql/002_design.sql`](backend/sql/002_design.sql) | the columns and views the design needs | +| [`backend/sql/003_snapshots.sql`](backend/sql/003_snapshots.sql) | pallet snapshots (URLs; the PNGs live in Storage) | + +### 3. Everything else, in one command + +```bash +./dev.sh +``` + +`dev.sh` checks the credentials, installs the SDK into `.venv` if it is missing, runs +`npm ci` when `package-lock.json` is newer than `node_modules`, **probes the live schema +for the specific columns the frontend queries**, and then starts the dev server on +. + +```bash +./dev.sh --check # verify only, start nothing +./dev.sh --seed # seed sample data first +``` + +That schema probe is the point of the script: the SQL is pasted by hand, so it is normal +for the repository to run ahead of the database. Discovering that in front of an +audience is the expensive failure. + +If the project is empty, the generators under `backend/seed/` will fill it — see +[Sample data](README.md#4-sample-data) in the README. + +--- + +## Checks + +Run both before you push. **CI runs exactly these**, so a red result here is a red PR. + +```bash +# Frontend +cd frontend && npm ci && npm run lint && npm run build + +# Backend +pip install -e './backend[dev]' +ruff check backend +pytest backend -q +``` + +`pytest backend -q` touches no network. Export `DATABASE_URL` pointing at a throwaway +Postgres and it additionally applies the real schema and verifies the contract against +the frontend's queries (`backend/tests/test_contrato.py`) — which is what CI does: + +```bash +docker run --rm -d --name pg -e POSTGRES_PASSWORD=postgres -p 5433:5432 postgres:16 +export DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres +pytest backend -q +``` + +### What CI enforces + +[`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs on every push and every pull +request, in two jobs: + +- **backend** — Python 3.11 with a `postgres:16` service, then `ruff check backend` and + `pytest backend -q` with `DATABASE_URL` set, so the schema tests really run. +- **frontend** — Node 22, `npm ci`, `npm run lint`, `npm run build`. It is `build` and + not `tsc --noEmit` on purpose: Next 16 generates the `PageProps<'/runs/[id]'>` types + during the build, so only a build catches a route typed against them. + +### `npm run lint` is three checkers, not one + +``` +eslint && node scripts/check-colors.mjs && node scripts/check-contrast.mjs +``` + +The two project-specific ones fail more often than eslint does: + +- **`check-colors.mjs`** — no colour literals anywhere outside + `frontend/styles/colors.css`. It catches `#hex`, `rgb()/rgba()`, `hsl()/hsla()`, + `hwb()`, `lab()/lch()`, `oklab()/oklch()`, and named colours used as a value, in CSS, + TS and TSX alike. `transparent`, `currentColor` and `inherit` are allowed — they are + not a specific colour. Run it alone with `npm run lint:colors`. +- **`check-contrast.mjs`** — WCAG contrast on every colour pair that carries text, in + **both** themes: 4.5:1 for text, 3:1 for meaningful graphical elements. A grey that + looks fine on the laptop it was picked on may not be readable on a projector. Run it + alone with `npm run lint:contrast`. + +--- + +## Hard rules + +These are the ones a review will bounce you for. The reasoning behind each is in +[`AGENTS.md`](AGENTS.md) §4 and §7 — this is the checklist. + +**No Tailwind.** The design is exact values in `docs/design/`; restating them as +utilities adds noise where there is no decision to make. Components use CSS Modules +next to the component. + +**Colours only as variables, defined in one file.** `frontend/styles/colors.css` holds +every colour for both themes. No other file — CSS, TS, or TSX — carries a literal; they +consume `var(--…)`. A translucent colour is another variable in that same file, derived +from the base with `color-mix`. Enforced by `check-colors.mjs`. + +**No server between the frontend and Supabase.** PostgREST, RLS and Realtime *are* the +backend. One more process is one more thing that falls over at 3am. + +**Never put `service_role` in the frontend.** It bypasses RLS. + +**No dependencies without a reason.** The Python SDK has none on purpose: it speaks +PostgREST with `urllib` from the standard library. Keep it that way — it is imported by +the simulation repositories, and every package it grows is a package they inherit. + +**Units are always visible, and converted in one place.** The database stores SI +(metres, radians, seconds); the screen shows millimetres and degrees. That conversion +lives only in `frontend/lib/ui.ts`. `4.1 mm`, never `4.1`. + +**Never show a raw failure identifier.** `stack_collapse` reads as «el montón se +derrumbó». The translation is `FAILURE_TEXT` in `frontend/lib/ui.ts`. + +**Don't compute in the client what a view can compute.** Totals, medians and dominant +causes come out of the SQL views. Two places summing the same thing eventually disagree. + +**Medians, not means.** One episode that collapses at 3 s drags the mean cycle time down +and makes a bad run look fast. + +**Synthetic and oracle data never share a comparison with measured data.** `runs.oracle` +and `runs.synthetic` are drawn with a diagonal hatch, and `comparability()` in +`frontend/lib/supabase.ts` blocks the delta outright. A pretty, false chart is exactly +what an evaluator will find. + +### Adding a failure cause + +The vocabulary is closed. A new one means touching **three** places: + +1. `FAILURES` in `backend/theker_telemetry/schema.py` +2. the `CHECK` on `episodes.failure` in `backend/sql/001_schema.sql` +3. `FAILURE_TEXT` in `frontend/lib/ui.ts` + +Skip the second and the episode writes to disk fine while the upload fails wholesale — +nobody notices until the interface is empty. You no longer have to remember: +`backend/tests/test_vocabulario.py` compares all three and fails if one lags behind. + +--- + +## Language + +- **Code identifiers:** English. +- **Comments, documentation and commit subjects:** Spanish. +- **`README.md`, `CONTRIBUTING.md` and `LICENSE`:** English — they are the public face + of the repository. +- Issues and pull requests: either. Write in whichever you are clearer in. + +## A note on `frontend/AGENTS.md` + +That file is written and re-added by `next dev` itself (see +`node_modules/next/dist/server/lib/generate-agent-files.js`). Deleting it from a diff +only recreates the uncommitted change; commit it with your work to keep the tree clean. + +## Licence + +By contributing you agree that your contributions are licensed under the +[MIT License](LICENSE). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..78647a1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 STACKSPECT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..57f573f --- /dev/null +++ b/README.md @@ -0,0 +1,597 @@ +
+ + + + + + + + + STACKSPECT + + +### Palletizing observability platform + +**Every episode a robotic arm runs, measured — centre of gravity, stability and the +improvement curve between commits.** + +[![License: MIT](https://img.shields.io/badge/License-MIT-1f1f1f?style=flat-square)](LICENSE) +[![CI](https://img.shields.io/github/actions/workflow/status/STACKSPECT/Platform/ci.yml?branch=dev&style=flat-square&label=CI)](https://github.com/STACKSPECT/Platform/actions/workflows/ci.yml) +[![Next.js](https://img.shields.io/badge/Next.js-16.3.5-1f1f1f?style=flat-square&logo=nextdotjs&logoColor=white)](https://nextjs.org) +[![Python](https://img.shields.io/badge/Python-3.11%2B-1f1f1f?style=flat-square&logo=python&logoColor=white)](https://www.python.org) +[![Supabase](https://img.shields.io/badge/Supabase-PostgREST%20%C2%B7%20Realtime-1f1f1f?style=flat-square&logo=supabase&logoColor=white)](https://supabase.com) + +[Installation](#installation) · +[Development](#development) · +[Usage](#usage) · +[Dependencies](#dependencies) · +[Contributing](CONTRIBUTING.md) + +
+ +--- + +## What this is + +A robotic arm stacks packages on a pallet while watching the centre of gravity, reads +the parcels by vision, and reproduces ordered stacks. **This repository is the +instrument panel for that arm** — not the arm. + +It answers three questions, and nothing else: + +| | Screen | Question | +|---|---|---| +| **Live** | `/` | What is happening on the pallet *right now*? | +| **Runs** | `/runs` | Which runs exist, and did the last change actually help? | +| **Episode** | `/runs/{id}` · `/runs/{id}/{seed}` | Why did *this* episode end the way it did? | + +Built for the THEKER Robotics challenge at HackSpain '26. One of the five criteria on +track B asks literally for *"measured iteration and improvement: your own indicators, +measured, and data backing how much you improved the solution during development."* +This platform **is** that evidence, which is why it refuses to draw a comparison between +two runs that did not measure the same thing, and marks every synthetic number it shows. + +**The simulation lives in another repository.** There is no MuJoCo here, no physics, no +perception. This repository knows about episodes, metrics and screens. + +--- + +## Screenshots + +**Runs** — every run with its totals, the filters, and the comparison panel. Here it is +doing the job it exists for: **refusing** to compare two runs that do not measure the +same thing, and saying exactly why. + + + + The Runs screen: a table of runs with filters and totals, and a comparison panel showing 'No comparable — these two runs do not measure the same thing', with the seed ranges 1–1 and 900–901 and zero seeds in common. + + +**Episode** — the same dashboard Live uses, against one finished episode: pallet from +above with the support polygon and the centre-of-gravity marker, the side elevation +drawn to the real pallet size, the KPI grid, and the event feed. Every magnitude carries +its unit. + + + + The Episode dashboard: top view of a pallet with dimensions and a centre-of-gravity marker, a side elevation, KPI cards reading 10 of 10 placed, 19.9 s per package, +58 mm stability margin and 77 % utilisation, and a timestamped event feed. + + +Both screens follow the viewer's theme; the images above do too. Captured at +1440 px from real measured runs — note the Oracle badge, which marks a run whose +perception was replaced by ground-truth poses. + +--- + +## Architecture + +There is **no server of our own**. Supabase *is* the backend: PostgREST for reads, RLS +for authorisation, Realtime for the live screen, Storage for pallet snapshots. One less +process to fall over at 3am. + +```mermaid +flowchart LR + subgraph producer ["Simulation repo (MuJoCo)"] + direction TB + SIM["Palletizing
simulation"] + JSONL[("episodes.jsonl
source of truth")] + SIM -->|"disk first, always"| JSONL + end + + SDK["theker_telemetry
Python SDK
zero deps · urllib"] + + subgraph supabase ["Supabase — this is the backend"] + direction TB + PGREST["PostgREST
REST over the schema"] + PG[("PostgreSQL
runs · episodes · placements
pallet_states · events · snapshots
v_*_summary views · RLS")] + RT["Realtime
episodes · events
pallet_states
"] + ST["Storage
pallet snapshot PNGs"] + PGREST <--> PG + PG --> RT + end + + FE["Next.js 16 frontend
Live · Runs · Episode"] + + SIM -->|"RunLog.begin / event /
pallet_state / end()"| SDK + JSONL -.->|"backfill.py"| SDK + SDK -->|"write · service_role"| PGREST + SDK -->|"snapshot upload"| ST + + PGREST -->|"read · anon key, under RLS"| FE + RT -->|"live push"| FE + ST -->|"image URLs"| FE +``` + +Two properties are worth stating out loud, because everything else follows from them: + +- **The dependency points one way.** The simulation imports `theker_telemetry`; this + platform does not know MuJoCo exists. The metrics schema lives here, with whoever + stores the data — so rewriting the simulation does not take observability down with it. +- **Disk wins.** The `episodes.jsonl` the simulation writes is the source of truth; + Supabase is a queryable replica. A network failure warns once and the episode carries + on, because the demo cannot depend on the room's wifi. If the replica is lost, + [`backend/backfill.py`](backend/backfill.py) rebuilds it. + +--- + +## Requirements + +| | Version | Why | +|---|---|---| +| **Node.js** | **22.x** | what CI pins, and what `frontend/package.json` declares in `engines` | +| **Python** | **3.11+** | declared by `backend/pyproject.toml`; CI runs 3.11 | +| **Supabase project** | any | the free tier is enough; it provides PostgREST, RLS, Realtime and Storage | +| Docker | optional | only to run the networked backend tests against a throwaway `postgres:16` | + +No database server, no application server and no container runtime are needed to develop +against a Supabase project. + +--- + +## Installation + +### 1. Clone and configure credentials + +There is exactly **one** credentials file: `.env` at the repository root. It is read by +`load_env()` on the Python side and, through +[`frontend/next.config.ts`](frontend/next.config.ts), by the frontend as well. + +```bash +git clone https://github.com/STACKSPECT/Platform.git +cd Platform +cp .env.example .env +``` + +Fill it in from *Supabase → Project Settings → Data API*: + +```dotenv +SUPABASE_URL=https://xxxxxxxxxxxx.supabase.co +SUPABASE_ANON_KEY=eyJ... # public: read-only, under RLS +SUPABASE_SERVICE_KEY=eyJ... # SECRET: bypasses RLS, Python side only +``` + +> [!WARNING] +> **`SUPABASE_SERVICE_KEY` bypasses row-level security and must never reach the +> browser.** Everything prefixed `NEXT_PUBLIC_` is bundled into JavaScript that anyone +> can download. `next.config.ts` therefore promotes an explicit **whitelist** of two +> keys and never walks the file; as a second belt, it throws at build time if any +> `NEXT_PUBLIC_*` variable has a name that smells like a secret (`SERVICE_KEY`, +> `SERVICE_ROLE`, `SECRET`, `PASSWORD`). `.env` and `.env.*` are gitignored. +> +> There is deliberately no `frontend/.env.local`: two copies of the same values +> desynchronise on their own, and once did. + +### 2. Apply the schema + +The schema is pasted **by hand**, in order, into the Supabase SQL editor. All three +files are idempotent — pasting one again breaks nothing. + +| Order | File | What it creates | +|---|---|---| +| 1 | [`backend/sql/001_schema.sql`](backend/sql/001_schema.sql) | tables, views and RLS policies | +| 2 | [`backend/sql/002_design.sql`](backend/sql/002_design.sql) | the extra columns and views the design needs | +| 3 | [`backend/sql/003_snapshots.sql`](backend/sql/003_snapshots.sql) | the `snapshots` table — URLs only; the PNGs live in Storage | + +Everything derivable is derived in a view rather than stored in a column. A stored +number that can also be computed is a number that will one day disagree with the +computed one, and then nobody knows which is lying. + +### 3. Start everything with one command + +```bash +./dev.sh +``` + +[`dev.sh`](dev.sh) is the whole installation procedure in a script. It: + +1. **verifies the credentials** — that `.env` exists and all three keys have values, and + warns if a stale `frontend/.env.local` would shadow them; +2. **installs the SDK** into `.venv` if `theker_telemetry` is not importable; +3. **runs `npm ci`** when `package-lock.json` is newer than `node_modules`; +4. **probes the live schema** for the exact columns the frontend queries + (`v_episode_summary.started_at`, `v_episode_summary.config`, `v_run_summary.config`, + `snapshots.url`) and names the `.sql` file to paste if one is missing; +5. **prints how much data is there** — runs, how many measured versus seeded, episodes; +6. **starts the dev server** on . + +```bash +./dev.sh --check # run every check, start nothing +./dev.sh --seed # seed sample data, then start +./dev.sh --help +``` + +Step 4 is the reason the script exists. The SQL is applied by hand, so it is *normal* +for the repository to run ahead of the database — and a missing column returns a 400 and +leaves the screen blank. Finding that out in front of an audience is the expensive +failure. + +
+Manual setup, without dev.sh + +```bash +# Python SDK (plus pytest and ruff) +python3 -m venv .venv && .venv/bin/pip install -e './backend[dev]' + +# Frontend +cd frontend && npm ci && npm run dev +``` + +
+ +### 4. Sample data + +The interface needs rows before it shows anything. Three generators, all writing through +the real SDK rather than raw SQL — so the write path the production pipeline will use is +exercised before it exists: + +```bash +python backend/seed/palletizing.py # a seeded history: 6 runs +python backend/seed/palletizing.py --wipe # remove only what was seeded +python backend/seed/ejemplo_contrato.py # one entry touching the ENTIRE contract +python backend/seed/humo_live.py # builds a pallet slowly: smoke-tests Live +``` + +- **`palletizing.py`** — a plausible history. Not a simulation, but the causality is + real: the CoG moves according to where and with what mass each package lands, and the + stack collapses when it leaves the support polygon. +- **`ejemplo_contrato.py`** — one run that exercises every column in + [`docs/API.md`](docs/API.md): all five tables, all six event kinds with the `payload` + keys the event feed expects, several failure causes, and the edge cases the frontend + has to render (an episode that placed nothing, planned versus actual poses, positive + overhang). +- **`humo_live.py`** — drives the `begin()` / `event()` / `pallet_state()` / `end()` + cycle so Realtime has something to deliver. With `/` open in the browser, the pallet + must build package by package and the KPIs move on their own, without a reload. Its + run is labelled `humo-live`; delete it when you are done, because unlike the other two + it is **not** flagged synthetic. + +> [!IMPORTANT] +> Everything `palletizing.py` and `ejemplo_contrato.py` write is marked +> **`synthetic = true`**, and that is neither optional nor switchable. The interface +> hatches synthetic data and never lets it into a comparison with measured data. An +> invented number that reads as a measured one is the fastest way to lose credibility in +> front of a jury. + +To import real episodes already on disk: + +```bash +python backend/backfill.py --runs-dir /simulation/runs --dry-run +python backend/backfill.py --runs-dir /simulation/runs +``` + +--- + +## Development + +### Layout + +``` +backend/ SQL + SDK. Supabase IS the backend; there is no server of ours. + sql/001_schema.sql tables, views, RLS + sql/002_design.sql the columns and views the design needs + sql/003_snapshots.sql pallet snapshots (URLs; PNGs live in Storage) + theker_telemetry/ the SDK imported by whoever produces episodes + schema.py EpisodeResult, FAILURES, TASKS, RunWriter + core.py PostgREST client and RunLog + pallet.py support polygon and stability margin + seed/ sample-data generators + backfill.py uploads runs/ from disk to Supabase + tests/ the networked ones need DATABASE_URL + +frontend/ Next.js 16. Reads Supabase with the anon key, under RLS. + app/ routes: / (Live), /runs, /runs/[id], /runs/[id]/[seed] + api/clients/ one function per PostgREST endpoint + api/hooks/ the same, wrapped in TanStack Query + components/ atoms / molecules / organisms / screens + lib/ui.ts ALL unit conversion, in one place + lib/pallet.ts drawing geometry; the pallet size is NOT fixed + lib/supabase.ts the types, which are half the contract + styles/colors.css ALL colours, both themes, in one place + scripts/ check-colors.mjs · check-contrast.mjs + +docs/ + API.md the contract: endpoints, types and vocabularies + BRIEFING-observabilidad.md what is being built and why + design/ the 11 design boards, at 1440 px + img/ screenshots and brand variants used by this README +``` + +### Checks + +These are exactly what CI runs, so a red result here is a red pull request. + +```bash +# Frontend +cd frontend && npm ci && npm run lint && npm run build + +# Backend +pip install -e './backend[dev]' +ruff check backend && pytest backend -q +``` + +`npm run lint` is **three** checkers, not one: + +``` +eslint && node scripts/check-colors.mjs && node scripts/check-contrast.mjs +``` + +- **`check-colors.mjs`** fails on any colour literal outside + `frontend/styles/colors.css` — `#hex`, `rgb()`, `hsl()`, `oklch()`, named colours — in + CSS, TS and TSX alike. +- **`check-contrast.mjs`** fails on any text colour pair below WCAG 4.5:1 (3:1 for + meaningful graphical elements), **in both themes**. + +`pytest backend -q` touches no network. Point `DATABASE_URL` at a throwaway Postgres and +it additionally applies the real schema and verifies the contract against the frontend's +queries — which is what CI does: + +```bash +docker run --rm -d --name pg -e POSTGRES_PASSWORD=postgres -p 5433:5432 postgres:16 +export DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres +pytest backend -q +``` + +### npm scripts + +| Script | What it does | +|---|---| +| `npm run dev` | dev server on | +| `npm run build` | production build (also generates Next 16's `PageProps<…>` route types) | +| `npm run start` | serve the production build | +| `npm run lint` | eslint + colour checker + contrast checker | +| `npm run lint:colors` | the colour checker alone | +| `npm run lint:contrast` | the contrast checker alone | + +### House rules worth knowing before the first commit + +The full list, with the reasoning, is in [`AGENTS.md`](AGENTS.md) and +[`CONTRIBUTING.md`](CONTRIBUTING.md). + +- **No Tailwind.** CSS Modules next to each component; the design is exact values. +- **Colours only as `var(--…)`**, defined once in `frontend/styles/colors.css`. +- **The unit is always visible** — `4.1 mm`, never `4.1`. The database stores SI; the + screen shows millimetres and degrees; the conversion lives only in `frontend/lib/ui.ts`. +- **Never render a raw failure identifier.** `stack_collapse` reads as *«el montón se + derrumbó»*, via `FAILURE_TEXT`. +- **Medians, not means.** One episode collapsing at 3 s drags the mean cycle time down + and makes a bad run look fast. +- **The client aggregates nothing.** Totals, medians and dominant causes come out of the + SQL views. + +--- + +## Usage + +### Producing telemetry from a simulation + +This is the only integration point. A simulation repository installs the SDK +(`pip install -e /backend`) and imports it — that pulls in nothing else, because +it speaks PostgREST with `urllib` from the standard library. + +```python +from theker_telemetry import EpisodeResult, RunLog + +log = RunLog( + repo, # writes runs//episodes.jsonl here + task="palletizing", level=2, + oracle=False, # True when perception is replaced by real poses + config={"pallet_size_m": [0.6, 0.4]}, # the real pallet may be a scale model + remote=True, # also replicate to Supabase +) + +log.episode( + EpisodeResult( + seed=7, level=2, n_objects=12, n_placed=11, n_misrouted=0, + success=False, duration_s=48.2, failure="stack_collapse", oracle=False, + task="palletizing", metrics={"cog_offset_xy": 0.031, "fill_ratio": 0.62}, + ), + placements=[...], pallet_states=[...], events=[...], +) +log.close() +``` + +**For the Live screen to be live, use the episode lifecycle instead.** `episode()` +uploads everything at once, so there is no `running` row and no `UPDATE` for Realtime to +react to — Live would show a pallet that is already built: + +```python +log.begin(seed=7, n_objects=12) # inserts the episode as `running` +for i, package in enumerate(queue): + log.event(kind="pick", package_id=package.id, payload={...}) + log.placement(seq=i, package_id=package.id, actual_pose=[...]) + log.pallet_state(after_seq=i, mass_kg=..., cog_x=..., stability_margin_m=...) +log.end(result) # closes it with the EpisodeResult +``` + +`failure` and `task` are validated against closed vocabularies on construction — an +unknown value raises immediately rather than failing the upload later. + +
+The failure vocabulary + +| enum | on screen (Spanish) | +|---|---| +| `no_detection` | no vio ningún paquete | +| `ik_unreachable` | no alcanza la posición | +| `collision` | chocó | +| `grasp_slip` | se le escapó de la pinza | +| `wrong_placement` | lo dejó fuera de tolerancia | +| `timeout` | se quedó sin tiempo | +| `stack_collapse` | el montón se derrumbó | +| `overhang_violation` | lo dejó fuera del palé | +| `place_inaccurate` | *historical, no longer emitted* | + +The vocabulary is closed. Adding one means touching three places — +`backend/theker_telemetry/schema.py`, the `CHECK` in `backend/sql/001_schema.sql`, and +`frontend/lib/ui.ts` — and `backend/tests/test_vocabulario.py` fails if one lags behind. + +
+ +### Reading the screens + +**Live** (`/`) shows the episode that is running *now*. When it finishes it holds the +result for ten seconds, so the outcome is visible and so a following episode of the same +run replaces it directly instead of flickering. With nothing running it says so — it does +**not** backfill with the last finished episode, because that would read as if it were +happening. If the connection drops mid-episode it freezes the last thing it received and +says that too. It never goes blank. + +**Runs** (`/runs`) lists every run with its totals, filters, and a comparison panel. +`comparability()` in `frontend/lib/supabase.ts` refuses to show a delta between two runs +that did not measure the same thing — different task, different level, different seed +range, or one of them oracle or synthetic. Blocking that is not form validation; a +pretty, false chart is exactly what an evaluator goes looking for. + +**Episode** (`/runs/{id}`, or `/runs/{id}/{seed}`) is the same dashboard as Live against +a chosen episode: a top view and a side elevation of the pallet — drawn to the real +pallet size from `config.pallet_size_m`, with a toggle to the simulator's own snapshot — +plus the KPI grid, the result card and the event feed. + +### The API + +The frontend talks to PostgREST directly, with the anon key, under RLS. Every endpoint, +type and closed vocabulary is written down in **[`docs/API.md`](docs/API.md)**; the +design rationale is in +**[`docs/BRIEFING-observabilidad.md`](docs/BRIEFING-observabilidad.md)**. + +Reads go through three views — `v_run_summary`, `v_episode_summary`, +`v_failure_breakdown` — plus the `placements`, `pallet_states`, `events` and `snapshots` +tables. Realtime is published on `episodes`, `events` and `pallet_states`. + +--- + +## Dependencies + +Both sides are deliberately thin. + +### Python — `backend/` + +**The SDK has no runtime dependencies at all**, and that is a rule rather than a +coincidence: it is imported by the simulation repositories, so every package it grows is +a package they inherit, and one more thing that can break the night before a demo. It +speaks PostgREST with `urllib` from the standard library. + +`pytest` and `ruff` are pinned to exact versions in the `dev` extra — `ruff` changes its +default rule set between minor releases, and a `>=` range lets CI go red on a Tuesday +over something nobody touched. + +### Node — `frontend/` + +Five runtime dependencies, each pinned exactly to what `package-lock.json` had already +resolved: + +| Package | Version | Licence | +|---|---|---| +| [`next`](https://github.com/vercel/next.js) | 16.3.5 | MIT | +| [`react`](https://github.com/facebook/react) | 19.2.8 | MIT | +| [`react-dom`](https://github.com/facebook/react) | 19.2.8 | MIT | +| [`@supabase/supabase-js`](https://github.com/supabase/supabase-js) | 2.116.0 | MIT | +| [`@tanstack/react-query`](https://github.com/TanStack/query) | 5.103.1 | MIT | + +Dev: `typescript` 5.9.3, `eslint` 9.39.5, `eslint-config-next` 16.3.5 and the `@types/*` +packages — pinned exactly too. + +### Licence audit + +Measured, not assumed — `license-checker` over the installed Node tree, `pip-licenses` +over a clean venv of `backend[dev]`: + +```bash +cd frontend && npm ci && npx license-checker --production --summary +pip install -e './backend[dev]' pip-licenses && pip-licenses +``` + +**Python** — `backend[dev]` resolves to six packages: `pytest` (MIT), `ruff` (MIT), +`iniconfig` (MIT), `pluggy` (MIT), `Pygments` (BSD-2-Clause) and `packaging` +(Apache-2.0 OR BSD-2-Clause). All permissive. + +**Node** — the shipped set (`--production`, 32 packages): + +| Licence | Count | +|---|---| +| MIT | 21 | +| Apache-2.0 | 5 | +| ISC | 2 | +| BSD-3-Clause | 1 | +| 0BSD | 1 | +| CC-BY-4.0 | 1 | +| LGPL-3.0-or-later | 1 | + +Including dev dependencies (350 packages) adds only BSD-2-Clause ×7, MPL-2.0, +Python-2.0, CC0-1.0 and BlueOak-1.0.0 — all permissive, all build-time only. + +Two entries deserve a sentence each, since they are the only ones that are not plainly +permissive: + +- **`@img/sharp-libvips-linux-x64` — LGPL-3.0-or-later.** This is the prebuilt `libvips` + binary behind `sharp`, which Next.js declares as an **optional** dependency for image + optimisation. It is a separate, platform-specific npm package loaded dynamically at + runtime: it is not vendored into this repository, not statically linked, and not + bundled into the JavaScript the browser downloads. Using an LGPL library that way puts + no obligation on the licence of the work that uses it, so it does not constrain + releasing this project under MIT. (`sharp` itself is Apache-2.0.) +- **`caniuse-lite` — CC-BY-4.0.** A browser-support *dataset* consumed by the toolchain + at build time. Permissive with attribution, and none of it is redistributed here. + +**Conclusion: nothing in the dependency tree is incompatible with releasing this project +under the MIT licence.** + +--- + +## Security + +- **`SUPABASE_SERVICE_KEY` bypasses row-level security.** It exists only on the Python + side — SDK, backfill, seeds. It must never be committed and must never reach the + browser, under any variable name. +- Everything prefixed `NEXT_PUBLIC_` is compiled into JavaScript that anyone can + download. [`frontend/next.config.ts`](frontend/next.config.ts) therefore promotes an + explicit whitelist of exactly two keys, never iterating the `.env` file, and **throws + at build time** if any `NEXT_PUBLIC_*` variable's name contains `SERVICE_KEY`, + `SERVICE_ROLE`, `SECRET` or `PASSWORD`. +- The frontend reads with the **anon** key only, under the RLS policies created by + `backend/sql/001_schema.sql`. +- `.env` and `.env.*` are gitignored. `.env.example` is the only one committed, and it + carries placeholders. + +Found something that looks like a real problem? Open an issue without the exploit details +and we will take it from there. + +--- + +## Documentation + +| Document | What it covers | +|---|---| +| [`AGENTS.md`](AGENTS.md) | permanent repository context and the hard rules *(Spanish)* | +| [`CONTRIBUTING.md`](CONTRIBUTING.md) | branches, commits, checks, what CI enforces | +| [`docs/API.md`](docs/API.md) | the database↔frontend contract *(Spanish)* | +| [`docs/BRIEFING-observabilidad.md`](docs/BRIEFING-observabilidad.md) | what is being built and why *(Spanish)* | +| [`docs/design/`](docs/design/) | the 11 design boards at 1440 px — open the HTML bundle in a browser | + +Code identifiers are in English; comments and internal documents are in Spanish. + +--- + +## Licence + +[MIT](LICENSE) © 2026 STACKSPECT diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 3dfe850..44c71e6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -13,8 +13,12 @@ dependencies = [] # Solo para desarrollar y para CI. El SDK que importa la simulación sigue sin # dependencias: nada de esto viaja con él. +# +# Clavadas a una versión exacta y no a un mínimo: ruff cambia de reglas por defecto +# entre versiones menores, así que un `>=` deja que CI se ponga rojo un martes por algo +# que nadie ha tocado. Subirlas es un commit, y eso es justo lo que se quiere. [project.optional-dependencies] -dev = ["pytest>=8", "ruff>=0.6"] +dev = ["pytest==9.1.1", "ruff==0.16.8"] [tool.setuptools] packages = ["theker_telemetry"] diff --git a/docs/img/episode-dark.png b/docs/img/episode-dark.png new file mode 100644 index 0000000..1bc4fbf Binary files /dev/null and b/docs/img/episode-dark.png differ diff --git a/docs/img/episode-light.png b/docs/img/episode-light.png new file mode 100644 index 0000000..4c7f67b Binary files /dev/null and b/docs/img/episode-light.png differ diff --git a/docs/img/mark-light.png b/docs/img/mark-light.png new file mode 100644 index 0000000..88014d2 Binary files /dev/null and b/docs/img/mark-light.png differ diff --git a/docs/img/runs-dark.png b/docs/img/runs-dark.png new file mode 100644 index 0000000..5082954 Binary files /dev/null and b/docs/img/runs-dark.png differ diff --git a/docs/img/runs-light.png b/docs/img/runs-light.png new file mode 100644 index 0000000..520f972 Binary files /dev/null and b/docs/img/runs-light.png differ diff --git a/docs/img/wordmark-light.png b/docs/img/wordmark-light.png new file mode 100644 index 0000000..9bfa7de Binary files /dev/null and b/docs/img/wordmark-light.png differ diff --git a/frontend/README.md b/frontend/README.md index e215bc4..9b9ddf3 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,36 +1,21 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# frontend -## Getting Started +The Next.js 16 interface of the STACKSPECT palletizing observability platform. It reads +Supabase directly with the anon key, under RLS — there is no server of ours in between. -First, run the development server: +**Everything you need is in the [root README](../README.md):** requirements, +installation, development, usage and dependencies. Conventions and the checks CI runs +are in [`CONTRIBUTING.md`](../CONTRIBUTING.md). -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More +Three things that trip people up here, in short: -To learn more about Next.js, take a look at the following resources: +- Credentials come from the **root `.env`**, translated by + [`next.config.ts`](next.config.ts). There is no `.env.local`. +- `npm run lint` is eslint **plus** `scripts/check-colors.mjs` and + `scripts/check-contrast.mjs`. Colours only ever live in `styles/colors.css`. +- No Tailwind. CSS Modules next to each component. -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +```bash +npm ci +npm run dev # http://localhost:3000 — or ../dev.sh, which checks the schema first +``` diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ac80c2d..30f608d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,20 +7,24 @@ "": { "name": "frontend", "version": "0.1.0", + "license": "MIT", "dependencies": { - "@supabase/supabase-js": "^2.116.0", - "@tanstack/react-query": "^5.103.1", + "@supabase/supabase-js": "2.116.0", + "@tanstack/react-query": "5.103.1", "next": "16.3.5", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", + "@types/node": "20.19.43", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "eslint": "9.39.5", "eslint-config-next": "16.3.5", - "typescript": "^5" + "typescript": "5.9.3" + }, + "engines": { + "node": ">=22 <23" } }, "node_modules/@babel/code-frame": { diff --git a/frontend/package.json b/frontend/package.json index 88359fa..a6aaf28 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,6 +2,10 @@ "name": "frontend", "version": "0.1.0", "private": true, + "license": "MIT", + "engines": { + "node": ">=22 <23" + }, "scripts": { "dev": "next dev", "build": "next build", @@ -11,18 +15,18 @@ "lint:contrast": "node scripts/check-contrast.mjs" }, "dependencies": { - "@supabase/supabase-js": "^2.116.0", - "@tanstack/react-query": "^5.103.1", + "@supabase/supabase-js": "2.116.0", + "@tanstack/react-query": "5.103.1", "next": "16.3.5", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", + "@types/node": "20.19.43", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "eslint": "9.39.5", "eslint-config-next": "16.3.5", - "typescript": "^5" + "typescript": "5.9.3" } }