SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
8.8 KB · 138 lines markdown
Rendered Raw Blame History
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