spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
1# InternetPressure.io — web frontend23Next 16 (App Router, React 19, TypeScript strict, Tailwind 4) instrument for **The real-time pressure gauge for the4Internet**. It renders the public API (`docs/API.md`) server-side, then follows the `/api/v1/live` SSE stream in small5client islands. Dark-first, tabular numerals, no motion without a real update (spec §66), visible "Instrument degraded"6state when `internal_status ≠ ok` (spec §57).78## Structure910```11apps/web12├── next.config.ts env loading (../../.env), /api → API_URL rewrite, security + cache headers, standalone output13├── Dockerfile multi-stage node:22-alpine → .next/standalone (port 8351)14├── mock/server.mjs DEV-ONLY API fixtures for every endpoint of docs/API.md, incl. SSE (never shipped)15├── scripts/copy-maplibre-worker.mjs copies MapLibre 6 module worker to public/maplibre (predev/prebuild)16├── qa/screens.mjs Playwright screenshots + overflow/console checks at 1440×900 and 390×84417├── public/logo.svg gauge glyph (also inline in components/chrome/Logo.tsx and src/app/icon.svg)18└── src19 ├── app20 │ ├── layout.tsx html.dark, Geist fonts, TimeProvider (UTC/local)21 │ ├── (site)/ public chrome: layout (LiveProvider + header/footer), all public routes22 │ ├── admin/ separate layout (token gate, no SSE) + 10 sections23 │ ├── opengraph-image.tsx OG image from the live index · robots.ts · sitemap.ts · error.tsx · not-found.tsx24 │ └── globals.css design tokens, pressure scale CSS variables, .tbl dense tables25 ├── components26 │ ├── chrome/ Header, NavLinks, Footer, Logo, LiveIndicator, DegradedBanner, TimeToggle, Search (⌘K)27 │ ├── gauge/ Gauge (the number), ExplainPanel (explain rows → components → signals)28 │ ├── home/ ComponentRows, Fronts, IncidentsList, Ticker, Clock, RegionsTable, ProbeStrip29 │ ├── map/ MapIsland (lazy, ssr:false) → WorldMap (MapLibre), ModeSelector, modes30 │ ├── charts/ echarts core registration, useEChart, HistoryChart, SeriesChart31 │ ├── detail/ ScopeHeader, ComponentGrid, Tables (targets/probes/latency matrix), ScopeCharts32 │ ├── incidents/ routes/ service/ history/ bgp/ targets/ admin/33 │ └── ui/ primitives (Section, LevelBadge, Delta, Bar, Sparkline, Stat…), AnimatedNumber34 └── lib35 ├── types.ts TypeScript mirror of docs/API.md (public + admin)36 ├── api.ts server-side fetch (API_URL_INTERNAL, no-store, 404 → notFound)37 ├── live.tsx one EventSource per page, backoff reconnect, sliced store (useSyncExternalStore)38 ├── pressure.ts levels, colours, pressureColor(level|value), component labels39 ├── format.ts numerals, deltas (true minus), durations, formatTime(ts, utc|local)40 ├── time.tsx UTC/local toggle persisted in localStorage; <Time/> component41 ├── geo.ts great-circle interpolation for Pressure Front arcs42 ├── iso-numeric-to-alpha2.ts world-atlas numeric ids → ISO alpha-243 └── admin-fetch.ts X-IP-Admin-Token client (token in sessionStorage)44```4546## Routes4748| Route | Consumes |49|---|---|50| `/` | `pressure/global`, `ticker`, `pressure/regions`, `pressure/countries`, `fronts`, `incidents?status=active`, `probes`, `pressure/history?range=24h`, `latency` + live stream |51| `/internet/[region]` | `pressure/region/{id}` |52| `/country/[cc]` | `pressure/country/{cc}`, `latency` (matrix rows of its region) |53| `/asn/[asn]`, `/asns` | `pressure/asn/{asn}`, `asns` |54| `/service/[slug]`, `/services` | `service/{slug}`, `services` |55| `/routes` | `routes/pairs`, `probes`, `targets`, `routes?probe&target` (client refetch on change) |56| `/event/[slug]`, `/incidents` | `incident/{slug}`, `incidents?status=…` |57| `/history`, `/history/[year]`, `/history/[year]/[month]` | `history/summary[?year[&month]]` |58| `/probes` | `probes`, `pressure/regions`, `latency` |59| `/targets` | `targets` |60| `/bgp` | `bgp/stats` + live `bgp_stats` |61| `/methodology` (static, revalidate 300) | `methodology` + prose from SPEC §3–8, 20, 40, 57, 66 |62| `/api` (static) | none (documentation) |63| `/admin/*` | `/api/admin/*` (client-side, token header) |64| `/opengraph-image`, `/sitemap.xml`, `/robots.txt` | `pressure/global`; countries/regions/asns/services/incidents/history |6566All dynamic pages export `dynamic = 'force-dynamic'` so `next build` never bakes API data.6768## Environment variables6970| Variable | Default | Used by |71|---|---|---|72| `API_URL` | `http://127.0.0.1:8352` | `next.config.ts` rewrite of browser `/api/*` calls (dev; in production the edge Caddy routes `/api` directly) |73| `API_URL_INTERNAL` | falls back to `API_URL` | server components / OG image / sitemap fetching the API inside the network |74| `NEXT_PUBLIC_SITE_URL` | `https://www.internetpressure.io` | canonical URLs, OG, sitemap, API examples |7576A single `.env` at the repository root is loaded by `next.config.ts` (like the sibling projects).7778## Development7980```bash81pnpm install82node mock/server.mjs # dev-only fixtures on :8352 (MOCK_DEGRADED=1 to see the degraded state; admin token dev-admin-token)83pnpm dev # http://localhost:8351 (predev copies the MapLibre worker to public/maplibre)84pnpm typecheck && pnpm lint85node qa/screens.mjs # screenshots into qa/screens/ (Playwright borrowed from ~/Desktop/uqo-eval/node_modules)86```8788The mock is **development-only**: it is excluded from the Docker image (`Dockerfile.dockerignore`) and must never be89deployed. The production site only ever talks to `apps/api`.9091## Build & Docker9293```bash94pnpm build # requires the API (or the mock) reachable at API_URL_INTERNAL for the two static pages95pnpm start # next start -p 8351 -H 0.0.0.09697# from the repository root98docker build -f apps/web/Dockerfile -t internetpressure-web .99docker run --rm -p 8351:8351 -e API_URL_INTERNAL=http://api:8352 internetpressure-web100```101102`output: 'standalone'` with `outputFileTracingRoot` at the repo root puts the server at103`.next/standalone/apps/web/server.js`; the Dockerfile copies `.next/static` and `public` next to it.104105pnpm ≥ 11.2 enforces a supply-chain `minimumReleaseAge` policy and rejects lockfile entries published in the last 24 h106(`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`). `.npmrc` sets `minimum-release-age=0` and the Dockerfile exports107`npm_config_minimum_release_age=0` so a freshly bumped dependency does not break the image build; remove both once the108dependency set has settled if you want the policy back.109110## Notes on MapLibre 6111112MapLibre GL ≥ 6 is ESM-only and spawns a *module* worker resolved from `import.meta.url`. Bundled by Turbopack that URL113points at a chunk, the worker 404s and the map stays black. `scripts/copy-maplibre-worker.mjs` copies114`maplibre-gl-worker.mjs` + `maplibre-gl-shared.mjs` to `public/maplibre/` and `WorldMap.tsx` calls115`setWorkerUrl('/maplibre/maplibre-gl-worker.mjs')`. Basemap: `https://tiles.openfreemap.org/styles/dark` (no key) with116an offline fallback style; countries from `world-atlas` 110m; only observed countries are coloured.117118## SSE through a proxy119120The live stream must be sent with `Cache-Control: no-store, no-transform` (plus `X-Accel-Buffering: no`, as in API.md).121Without `no-transform`, compressing proxies — including the Next dev rewrite — gzip-buffer `text/event-stream` and the122browser receives nothing until the connection closes. The mock does this; `apps/api` must too.123124## Contract notes (docs/API.md)125126Everything consumed is in API.md. Places where the frontend had to interpret the contract:127128- `GET /pressure/region/{id}` and `/pressure/country/{cc}`: `probes`/`targets`/`incidents` are **counts** in the list129 objects and **lists** in the detail objects — typed separately (`RegionDetailResponse`, `CountryDetailResponse`).130- `GET /history/summary` without `month`: the month rows (`months`) are assumed to carry `{month, min, max, avg, events}`131 (mirrors the `days` shape with `month: "YYYY-MM"`); API.md only says "per-month rows".132- `fronts[].status` values are not enumerated in API.md; the map animates the arc only for `developing`/`active`.133- The map's "Packet loss" mode derives per-region loss from `latency.matrix[].loss_pct` (source view) because there is no134 loss component in `regions[].components`; countries are neutral in that mode.135- Admin `GET /incidents` rows are assumed to carry `review` and `note` (the PATCH body fields) so the review state can136 be displayed; API.md does not list them on the GET response.137- Admin `GET /annotations` rows assumed `{id?, ts, author?, scope_type, scope_id, text}`.138