bingo: move the public site to the datacenterbingo repo
All checks were successful
Build and Deploy to Production / test (push) Successful in 25s
Build and Deploy to Production / build (push) Successful in 1m2s
Build and Deploy to Production / deploy (push) Successful in 38s

The public card now runs from its own repo and container, with a
Traefik router of its own. Drop BingoWeb, its views, icons, Host
dispatch and the asxpio-bingo router here; keep the admin, the schema
and the photo pipeline, which the new site reads through a read-only
role.

BINGO_HOST stays for IndexNow pings and the admin's site link.

Assisted-by: Claude:opus-5
This commit is contained in:
Sergei Poljanski 2026-10-12 03:52:30 +04:00
commit 849980042e
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
19 changed files with 34 additions and 1007 deletions

View file

@ -121,22 +121,23 @@ Admin pages use a shared dark palette extended in `public/style.css` (search for
- Anyone with the UUID can fetch the PDF. UUIDs are 122-bit random so not enumerable, but they aren't access-controlled. If a stronger gate is ever needed (email confirmation, expiry on paid, etc.) it goes in the `/i/:uuid` and `/i/:uuid/pdf` handlers.
- The MinIO bucket is **private**; only the app's scoped service account can put/get. Presigned URLs use `S3_PUBLIC_ENDPOINT=https://s3.asxp.io` so the signed host matches what the browser fetches.
## Datacenter bingo
## Datacenter bingo (admin side)
A second public site served from the **same process** on `BINGO_HOST` (`datacenterbingo.xyz`, `www.` redirects to it; optional `BINGO_ALT_HOST` serves the same card on a second hostname (unused; it also needs adding to the Traefik rule); non-secret config in `deploy.yaml` → `.env` → Traefik router `asxpio-bingo` via compose interpolation). `config.ru` dispatches by Host: `BINGO_HOST` → `BingoWeb` (`bingo_web.rb`), everything else → `AsxpioWeb`. Unset `BINGO_HOST` = bingo unreachable.
The public site (datacenterbingo.xyz) lives in its own repo and container: `git.czsk.it/asxpi/datacenterbingo`, mirrored to GitHub. It reads the tables and photos below through a read-only DB role (`bingo`) and MinIO user. What stays here is the admin, which writes, and the schema.
- 5×5 card of the 25 largest Russian datacenters; a cell is crossed out when an incident (with a public source) is recorded. Rows/columns/diagonals → BINGO banner.
- Locales `ru` / `en` / `uk` at `/ru`, `/en`, `/uk`; `/` redirects by `Accept-Language` (fallback `en`). Per-locale DB columns (`name_*`, `city_*`, `title_*`); `ru` is required, others fall back to it. UI strings in `Bingo::T` (`lib/bingo.rb`).
- Tables `bingo_datacenters`, `bingo_incidents` (`db/migrations/004_bingo.rb`) in the asxpio DB. `updated_at` is set on save; `renumber!` bumps only moved rows and deleting an incident bumps its datacenter (use `destroy`, not dataset `delete`, so the hook runs). First boot seeds from `db/bingo_seed.json` (verified 2026-10-10, sources in `info_url`) only if the table is empty — after that, edit via admin, not the JSON.
- Admin: `/admin/bingo` on asxp.io (`lib/bingo_admin.rb`, registered into `AsxpioWeb`, so HTTP Basic + CSRF apply). Record/delete incidents; add, edit, delete DCs (delete cascades to incidents); upload a photo or import it from a URL; bulk import missing / re-import all photos from `photo_source_url`; "Sort by MW".
- Status: operational | building | planned. `mw_type` (it | connected | total | design | mva, NULL = not stated) tags the MW figure in the dialog; mva swaps the unit.
- Views: the bare locale URL is the **operational** card (default, indexed); `?view=all` adds construction/planned projects in the same ranking with lines recomputed, and is noindex with canonical to the bare URL. The admin dashboard tile shows the operational card too.
- Provider filter: `bingo_datacenters.provider` (a `Bingo::PROVIDERS` slug; migration 016 backfilled it from `operator`, `Bingo.guess_provider` fills new rows, editable in admin). `?p[]=yandex&p[]=rtk` (or `?p=yandex,rtk`) builds the usual 5×5 card from only those providers' datacenters, first 25 by position; a smaller selection leaves empty cells. Filtered URLs are noindex with canonical to the bare page; language and view links keep the selection.
- Ordering: `position` is dense 1..N (`Bingo.place!` inserts and shifts, `renumber!` after deletes). The card shows positions 1–25; anything beyond is reserve. Slug is fixed at creation (it names photo objects).
- Photos: stored in the existing bucket under `bingo/<slug>-<hex>.<ext>` (no storage-repo change), proxied by `BingoWeb` at `/p/:file` with immutable caching (new upload = new key; old objects are kept). Type sniffed from magic bytes (JPEG/PNG/WebP), 8 MB cap, metadata stripped (GPS!). URL import is https-only, admin-only. On store, ImageMagick (`convert`, installed in the Dockerfile) derives `-480.webp` (card cells), `-1200.webp` (dialog, JSON-LD) and `-og.jpg` (1200×630 centre crop, og:image); `photo_variants`/`photo_og` record success (rows without `photo_og` use the 1200 WebP as og:image) and the page falls back to the original without them. "Rebuild resized photos" in admin re-derives from stored originals.
- SEO: `BingoWeb` has `static` off (public/ is asxp.io's), its own `/robots.txt`, `/sitemap.xml` (hreflang alternates, `lastmod` = max `updated_at`), and favicon/apple-touch-icon routes serving `assets/bingo/`. The h1 carries a per-locale `tagline`; `/en/` 301s to `/en`. Per-locale `seo_title`/`seo_desc` with the live score, OG/Twitter tags (og:image = latest hit's photo, alt = its name and city), JSON-LD WebSite + ItemList of Places (`info_url` as `subjectOf`, incidents as `event`). HSTS via Rack::Protection. Canonical/hreflang always use `BINGO_HOST`, so `BINGO_ALT_HOST` doesn't compete.
- Datacenter pages: `/:locale/dc/:slug` (`views/bingo/dc.erb`) for every row, reserve included. Title/description/facts are generated (`dc_facts`, `dc_title*` strings); optional `about_*` prose (admin form, no fallback across locales). Card cells are `<a>` to these pages, and JS opens the dialog on a plain click. Dialog and page share `_dc_meta`/`_dc_incidents`. The layout takes `@title @desc @path_for @x_default @og @ld @noindex` from the route. The sitemap lists the card and all datacenter pages × locales.
- IndexNow (`Bingo::IndexNow`): admin incident/DC/sort actions ping api.indexnow.org in a thread with the changed URLs; key from the `INDEXNOW` secret → `INDEXNOW_KEY`, served at `/<key>.txt`. Unset = no-op.
- **Schema is owned here.** After adding a bingo migration, refresh `test/schema.sql` in datacenterbingo and update its models. Keep `Bingo::PROVIDERS`, `Board` and the model methods in step with its `lib/bingo.rb`. UI strings (`T`) and views are only there.
- `BINGO_HOST` (deploy.yaml → `.env`) is used only for the IndexNow host and the admin's "view site" link; Traefik no longer routes it here.
- Tables `bingo_datacenters`, `bingo_incidents` (`db/migrations/004_bingo.rb`…). `updated_at` is set on save; `renumber!` bumps only moved rows, and deleting an incident bumps its datacenter (use `destroy`, not dataset `delete`, so the hook runs). First boot seeds from `db/bingo_seed.json` only if the table is empty; after that, edit via the admin.
- Admin: `/admin/bingo` (`lib/bingo_admin.rb`). Record and delete incidents; add, edit and delete DCs (delete cascades); upload a photo or import it from a URL; bulk import or re-import photos from `photo_source_url`; "Sort by MW".
- Status: operational | building | planned. `mw_type` (it | connected | total | design | mva, NULL = not stated) tags the MW figure; mva swaps the unit. The public default card is operational only, and so is the dashboard tile (`Board.load('operational')`).
- Provider: `bingo_datacenters.provider` holds a `Bingo::PROVIDERS` slug (backfilled by migration 016; new rows get it from `Bingo.guess_provider`; editable in the admin). The public site filters cards by it.
- Ordering: `position` is dense 1..N (`Bingo.place!` inserts and shifts; `renumber!` runs after deletes). Positions 1–25 are the card; the rest are reserve. Slug is fixed at creation because it names the photo objects.
- Photos: stored in the bucket as `bingo/<slug>-<hex>.<ext>`, with the type sniffed from magic bytes (JPEG/PNG/WebP).
- Limits: 8 MB cap, and metadata is stripped (GPS!). URL import is https-only.
- On store, ImageMagick derives `-480.webp`, `-1200.webp` and `-og.jpg` (1200×630). `photo_variants` / `photo_og` record whether that succeeded.
- "Rebuild resized photos" re-derives the variants from the stored originals.
- Served publicly by the bingo site at `/p/:file`; previewed here at `/admin/bingo/p/:file`.
- IndexNow (`Bingo::IndexNow`): admin actions ping api.indexnow.org with the changed URLs. The key comes from `INDEXNOW` → `INDEXNOW_KEY`; the bingo site has the same key and serves `/<key>.txt`. Unset = no-op.
- Photos are third-party (operator sites, Habr, Wikimedia Commons CC BY-SA) — keep `photo_credit` filled; renders/illustrations are marked as such.
- No street addresses or coordinates on the page — city level only.