Commit graph

116 commits

Author SHA1 Message Date
9a802106ce
admin: send /admin/ to /admin, also after login
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 1m1s
Build and Deploy to Production / deploy (push) Successful in 36s
Opening /admin/ logged out stored next=/admin/ in the login form, and a
successful login then redirected to /admin/, which has no route (404).

Redirect trailing-slash admin URLs to the slashless route with a 301,
and drop trailing slashes from the post-login target too.

Assisted-by: Claude:opus-5-5
2026-10-18 10:39:10 +04:00
648effc21f
docker: ship bin/monitor in the image
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 35s
.dockerignore left out all of bin/, which only held bin/test until now.
The asxpio-monitor container runs bin/monitor from the image and
crash-looped with "No such file or directory -- bin/monitor". Exclude
just bin/test.

Assisted-by: Claude:opus-5-5
2026-10-18 10:36:01 +04:00
0cc3e1dece
bingo: add a reachability monitor for the status page
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 1m11s
Build and Deploy to Production / deploy (push) Successful in 37s
datacenterbingo gets a public page showing whether each datacenter's
network answers. The probing lives here, next to the schema and the
admin, since the public site only has a read-only role.

bin/monitor runs as a second container from the same image. Once a
minute it opens one TCP connection to every active target (a handshake
or a refusal both count as answered) and folds the results into
5-minute rows per datacenter, plus a per-bucket row for control hosts in
Russia, so a lost vantage point isn't mistaken for dark sites. Rows are
kept 30 days. Targets are managed at /admin/bingo/monitor; only hosts a
public source ties to that one site belong there.

Migration 017 also grants the new tables to the bingo role, so the
public site can read them without a storage redeploy.

Assisted-by: Claude:opus-5-5
2026-10-18 10:31:00 +04:00
e0038ef3a4
Rerun CI after the runner's nix image was pinned
All checks were successful
Build and Deploy to Production / test (push) Successful in 49s
Build and Deploy to Production / build (push) Successful in 1m9s
Build and Deploy to Production / deploy (push) Successful in 36s
The previous runs died on the runner, not in the code: its nix-store
volume no longer matched nixos/nix:latest. The runner now uses
nixos/nix:2.35.3 with a volume seeded from that image.

Assisted-by: Claude:opus-5-5
2026-10-18 09:40:44 +04:00
642bec845a
ci: set nix options through NIX_CONFIG
Some checks failed
Build and Deploy to Production / test (push) Failing after 15s
Build and Deploy to Production / build (push) Has been skipped
Build and Deploy to Production / deploy (push) Has been skipped
The runner mounts a persistent nix-store volume over /nix. The current
nixos/nix image makes /etc/nix/nix.conf a symlink into /nix/store, and
the volume does not hold that path, so the link dangles and appending
to it fails before checkout: "/etc/nix/nix.conf: No such file or
directory".

Pass the settings in NIX_CONFIG instead. Without a config file Nix also
falls back to build-users-group = nixbld, which the container lacks, so
set it empty and turn the sandbox off, as the image config does.

Assisted-by: Claude:opus-5-5
2026-10-18 09:34:44 +04:00
9ea965190a
bingo: add VK to the providers
Some checks failed
Build and Deploy to Production / test (push) Failing after 1m7s
Build and Deploy to Production / build (push) Has been skipped
Build and Deploy to Production / deploy (push) Has been skipped
VK's own datacenter ИЦВА in Novosergiyevka is joining the card after
the drone strike of 18 October. Give it a provider slug so the filter
can show it, and guess it from the VK/ВКонтакте/М100 operator names.

Assisted-by: Claude:opus-5-5
2026-10-18 09:31:27 +04:00
f1ceb4efc7
bingo: store photos in their own bucket
All checks were successful
Build and Deploy to Production / test (push) Successful in 23s
Build and Deploy to Production / build (push) Successful in 1m8s
Build and Deploy to Production / deploy (push) Successful in 37s
The bingo photos sat under bingo/ in the invoice bucket. Store them in
BINGO_S3_BUCKET (datacenterbingo-photos in prod, created and pre-filled
by the storage repo with the same keys) and fall back to S3_BUCKET when
unset, so dev and tests need nothing new.

Assisted-by: Claude:opus-5
2026-10-12 04:09:08 +04:00
849980042e
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
2026-10-12 03:52:30 +04:00
510998c0ed
bingo: credit Claude and hackyourmom in the footer
All checks were successful
Build and Deploy to Production / test (push) Successful in 28s
Build and Deploy to Production / build (push) Successful in 1m2s
Build and Deploy to Production / deploy (push) Successful in 38s
Part of the list was collected and verified with Claude, and several
entries came from the hackyourmom data-center map (Cat Eyes OSINT data).
Say so in the footer of the card and data-center pages, in all three
languages, and note that each entry was re-verified against operator
sites and the press.

Assisted-by: Claude:opus-5
2026-10-12 02:13:57 +04:00
79d28cae9e
bingo: more providers for the filter
All checks were successful
Build and Deploy to Production / test (push) Successful in 27s
Build and Deploy to Production / build (push) Successful in 1m2s
Build and Deploy to Production / deploy (push) Successful in 39s
Add DataSpace, Cloud.ru, X5 Group, КРОК, Stack Group and Beeline to the
provider list (with operator patterns for the automatic guess), ahead of
adding their verified sites.

Assisted-by: Claude:opus-5
2026-10-11 21:19:58 +04:00
c64098813b
bingo: build the card from chosen providers
All checks were successful
Build and Deploy to Production / test (push) Successful in 27s
Build and Deploy to Production / build (push) Successful in 1m1s
Build and Deploy to Production / deploy (push) Successful in 38s
Let visitors tick providers (Yandex, IXcellerate, RTK-DC, Sber, …) and
get the usual 5×5 card made of only those providers' datacenters, first
25 in ranking order, with lines scored as normal. A selection with fewer
than 25 sites leaves the rest of the card empty, so the card is always
5×5. Each provider chip shows crossed/total for the current view.

Datacenters get a provider slug, backfilled from the free-text operator,
guessed for new rows and editable in the admin. Filtered URLs are
noindex with canonical to the plain card.

Assisted-by: Claude:opus-5
2026-10-11 21:12:47 +04:00
9ba108b6b0
bingo: default to the operational card
All checks were successful
Build and Deploy to Production / test (push) Successful in 28s
Build and Deploy to Production / build (push) Successful in 1m8s
Build and Deploy to Production / deploy (push) Successful in 41s
The card led with construction projects (Sber Balakovo at #1, MOS7,
MOS11, Yandex Vladimir-2), so the first thing visitors saw was not what
actually runs today. Make the operational-only card the bare URL and
move the full list to ?view=all. The variant is noindex with canonical
to the bare page, old ?view=operational links still land on the default,
and the admin dashboard tile follows the public default.

Assisted-by: Claude:opus-5
2026-10-11 20:34:24 +04:00
a432a003a8
bingo: "63 MW (design)" in datacenter page prose
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 1m3s
Build and Deploy to Production / deploy (push) Successful in 39s
The facts sentence reused the dialog's compact label, so it read
"Capacity: 63 MW · design." Put the capacity type in parentheses
when it appears in prose.

Assisted-by: Claude:opus-5
2026-10-10 09:45:49 +04:00
f7ed3831f5
CLAUDE.md: document datacenter pages and IndexNow
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 1m2s
Build and Deploy to Production / deploy (push) Successful in 38s
Assisted-by: Claude:opus-5
2026-10-10 09:42:47 +04:00
26e862ff04
bingo: ping IndexNow when the card changes
A newly recorded incident only showed up in search once a crawler
happened to come back. IndexNow lets Bing, Yandex and the other
participating engines know about changed URLs right away.

Bingo::IndexNow posts the card URLs plus the affected datacenter pages
to api.indexnow.org, in a background thread so admin saves don't wait.
Incidents ping their datacenter. Edits ping the datacenter, or every
page when positions move (create, delete, reorder, sort). A deleted
datacenter's pages are included so the engines see the 404. BingoWeb
serves the key at /<key>.txt.

The key comes from the INDEXNOW Forgejo secret, written to .env as
INDEXNOW_KEY. With no key set this does nothing.

Assisted-by: Claude:opus-5
2026-10-10 09:42:46 +04:00
ca48f4292b
bingo: optional prose per datacenter page
The datacenter pages carry only generated sentences, which is thin
content to rank on. Migration 015 adds about_ru/en/uk, edited in the
admin form and rendered as paragraphs below the facts.

There is deliberately no fallback to ru: Russian prose on the English
page would hurt more than an absent paragraph.

Assisted-by: Claude:opus-5
2026-10-10 09:41:11 +04:00
feac965d02
bingo: a page per datacenter
Everything lived on one URL per language, with each datacenter's
details and incidents inside a <dialog>. That page can rank for the
brand and a couple of head terms, but not for searches like
"Yandex Sasovo drone strike".

Add /:locale/dc/:slug for every datacenter, reserve included. Each page
has its own title (naming the latest incident when there is one), a
description built from the facts, canonical and hreflang links, the
photo as og:image, and JSON-LD Place + BreadcrumbList. It shows the
facts in a sentence or two, the incident list with sources, and
prev/next links by position. The slug never changes, so the URLs are
stable.

The card cells are now links to these pages. A plain click still opens
the dialog, while modified clicks open the page. The dialog and the
incident log link to the page too, and the sitemap lists every page in
all three languages. The layout takes title, description, alternates
and JSON-LD from the route, and the dialog and the page share the meta
and incident partials.

Assisted-by: Claude:opus-5
2026-10-10 09:40:16 +04:00
1646caec1b
CLAUDE.md: document the bingo SEO changes
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 1m1s
Build and Deploy to Production / deploy (push) Successful in 38s
Assisted-by: Claude:opus-5
2026-10-10 09:31:25 +04:00
29bd2df28a
bingo: send HSTS
The edge already redirects http:// to https://, but without HSTS the
first request of every visit can still go out in the clear. Turn on
Rack::Protection's StrictTransport for the bingo app: one year, this
host only, no preload.

Assisted-by: Claude:opus-5
2026-10-10 09:31:25 +04:00
6f37e537ca
bingo: lastmod from the last edit, not the last insert
The "updated" date and the sitemap lastmod came from max(created_at),
so editing a datacenter, fixing an incident title or deleting an
incident left them stale. Google stops trusting lastmod once it is
seen to be wrong.

Migration 014 adds updated_at (backfilled from created_at). Saves set
it, renumbering bumps only the rows whose position actually moved, and
deleting an incident bumps its datacenter. The admin delete now goes
through the model so the hook runs. The sitemap gives lastmod as a
full timestamp and drops changefreq, which Google ignores.

Assisted-by: Claude:opus-5
2026-10-10 09:30:37 +04:00
b100b6a8cd
bingo: fix the JSON-LD place markup
Each Place carried its info_url as sameAs, but sameAs means "this is
the same entity", and the URL is an operator page or news article
about the datacenter. Use subjectOf with a CreativeWork instead.

Incidents were missing from the structured data entirely. Add them as
Events at the place (name, start date, source URL), and give the
ItemList a dateModified.

Assisted-by: Claude:opus-5
2026-10-10 09:29:35 +04:00
4e074fd4bf
bingo: 1200x630 JPEG for og:image
og:image pointed at the 1200px WebP dialog variant: no declared size,
a 16:9 frame that link previews crop unpredictably, and WebP isn't
rendered by every preview client. The alt text was just the site name.

Photo variants now include <slug>-<hex>-og.jpg, a centre crop to
1200x630, and the head declares its type and size. Migration 013 adds
photo_og so rows built before this keep using the WebP until "Rebuild
resized photos" is run. The alt text names the pictured datacenter and
city. resize and the new og_crop share one convert call.

Assisted-by: Claude:opus-5
2026-10-10 09:28:57 +04:00
cb2eba1a26
bingo: serve real favicon files
The favicon was an emoji in a data: URI and /favicon.ico was a 404.
Google only shows a site icon in results when the favicon has a
crawlable URL, and the emoji looked different on every platform.

Add an SVG (a 5x5 card with a crossed-out diagonal), a 16/32/48 ICO
and a 180px apple-touch-icon under assets/bingo/, served by explicit
routes because public/ belongs to asxp.io.

Assisted-by: Claude:opus-5
2026-10-10 09:27:44 +04:00
d2d6ac1d99
bingo: redirect /en/ to /en, follow the view links
/en/ answered 200 with the same page as /en. The canonical tag covered
it, but a 301 keeps crawlers and shared links on a single URL.

The All / Operational toggle links were rel="nofollow". The filtered
page is already noindex with a canonical back to the locale URL, so
nofollow on our own links only threw away internal link signals.

Assisted-by: Claude:opus-5
2026-10-10 09:27:02 +04:00
dc799d6d47
bingo: put the topic in the h1
The h1 was just the brand ("Datacenter Bingo"), so the page's main
heading carried none of the terms people search for. Add a per-locale
tagline inside the h1 and shorten the lead paragraph so it no longer
repeats it.

Assisted-by: Claude:opus-5
2026-10-10 09:26:38 +04:00
7e262b8f89
admin: fix the new-user username placeholder
All checks were successful
Build and Deploy to Production / test (push) Successful in 27s
Build and Deploy to Production / build (push) Successful in 1m2s
Build and Deploy to Production / deploy (push) Successful in 40s
The username field's attributes were emitted as one string through
<%= %>, which HTML-escapes the quotes, so the browser read the
placeholder as the literal "leave and dropped the rest. Write the
attributes in the template and test the rendered field.

Assisted-by: Claude:opus-5
2026-10-10 09:17:39 +04:00
33ba7650c3
invoices: self-contained payout addresses
Crypto options were limited to a hard-coded asset list, with one
address per coin in a side table. A payout address now carries
everything an invoice needs: ticker, name, QR URI scheme, amount
parameter, CoinGecko id, address and an active flag. Any coin can be
added, several addresses per ticker are fine, and known tickers fill the
QR and rate fields from the built-in templates.

The new-invoice form picks a payout address. The invoice snapshots the
ticker, address, name, scheme and amount parameter, and the PDF and QR
render from that copy, so editing, retiring or deleting a payout entry
never changes an issued invoice. "Fetch live" looks up the rate by the
payout address's CoinGecko id.

Migration 012 keeps the existing rows and backfills both the payout
metadata and the snapshots on existing crypto invoices.

Assisted-by: Claude:opus-5
2026-10-10 09:17:09 +04:00
78ea9926e7
admin: invite links without a username
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
Leaving the username empty when creating a user makes the account
invite-<hex>; the invitee picks the real username on the invite page
(taken names and the invite- prefix are refused). Without an email this
gives a plain invite link to share.

Assisted-by: Claude:opus-5
2026-10-10 09:12:21 +04:00
9128867a33
invoices: statistics page
Add /admin/invoices/stats with a year filter: invoiced, received and
outstanding totals in GEL at each invoice's captured rate (the one
currency every invoice has), overdue count, average days to pay, money
received per month by payment date as a bar chart with the same figures
in a table, per-currency totals, top clients and crypto option usage.
Cancelled invoices are left out of every amount; hidden ones follow the
usual superuser-only visibility.

Assisted-by: Claude:opus-5
2026-10-10 09:12:21 +04:00
12d29dec0c
mailer: send account emails as "asxp.io admin"
All checks were successful
Build and Deploy to Production / test (push) Successful in 25s
Build and Deploy to Production / build (push) Successful in 1m1s
Build and Deploy to Production / deploy (push) Successful in 38s
Invite and reset mails reused MAIL_FROM, whose display name is still
"IE Sergei Poljanski Contact Form" from the removed contact form. Keep
its verified address but use "asxp.io admin" as the name (overridable
with ADMIN_MAIL_NAME).

Assisted-by: Claude:opus-5
2026-10-10 08:50:30 +04:00
bf4f270fb4
invoices: fix 500 on the admin invoice page
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 1m2s
Build and Deploy to Production / deploy (push) Successful in 36s
Replacing the paid button with the cancel/hide action block cut the old
markup at the first <% end %>, which belonged to the button's inner
if/else, leaving a stray </button></form><% end %> behind. show.erb no
longer compiled and every admin invoice page returned 500; nothing
rendered that page in tests.

Remove the leftovers, render the page in every state (pending, paid,
cancelled, hidden), and compile every view in a test so an unbalanced
template fails CI even when no test visits that page.

Assisted-by: Claude:opus-5
2026-10-10 08:47:23 +04:00
6094d29a3f
admin: let superusers rename accounts
Some checks failed
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 1m1s
Build and Deploy to Production / deploy (push) Has been cancelled
ADMIN_USER only seeds the first superuser on an empty table, so renaming
the secret afterwards had no effect and there was no other way to change
a login name. Superusers can now edit usernames in /admin/users,
including their own; sessions key on the user id and survive, duplicates
are refused, and the rename is audited.

Assisted-by: Claude:opus-5
2026-10-10 08:45:20 +04:00
1dd8f9081f
invoices: payout addresses in the database
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 59s
Build and Deploy to Production / deploy (push) Successful in 38s
Default crypto payout addresses lived in the CRYPTO_ADDRESSES and
LTC_ADDRESS Forgejo secrets, so changing one meant editing a secret and
redeploying. Keep them in a payout_addresses table edited at
/admin/invoices/payout-addresses (read to view, write to change, every
change audited), and prefill the new-invoice form from it.

The old env values are imported once on the first boot with an empty
table, so the current addresses carry over; after that the secrets can
be removed.

Assisted-by: Claude:opus-5
2026-10-10 08:36:32 +04:00
eeac07ed34
invoices: cancel with stamped PDF, superuser-only hiding
An invoice issued by mistake could only be left pending or marked paid.
Add cancel (with an optional reason) and reinstate. Cancelling renders a
third PDF variant with a diagonal CANCELLED stamp and the date/reason,
and it becomes the current one, so the client's link and download show
the invoice as void. Cancelled wins over paid in the status, and marking
a cancelled invoice paid is refused.

Superusers can also hide an invoice: it disappears from the admin list,
dashboard and invoice pages for every other user, while the client's
public link keeps working.

Assisted-by: Claude:opus-5
2026-10-10 08:35:01 +04:00
97919f3f71
admin: restore form styling
The invoice form's text inputs, textareas and label layout depended on
.contact-form rules that were deleted with the contact form, so they
fell back to white browser defaults with labels running inline. Give
every admin form (.invoice-form) one style: labels above fields, gray
inputs on the surface token, a focus state, single column on phones,
and the submit button styled like the rest.

Assisted-by: Claude:opus-5
2026-10-10 08:32:28 +04:00
d91a72f603
admin: user management, invites, password reset, audit log
Add /admin/users to create, edit, disable and delete accounts with the
per-area permission grid. New users get a one-time invite link (48 h)
by email; the admin also sees it once, so an account without email can
still be onboarded. Forgotten passwords get a 1 h reset link with the
same answer for unknown accounts. Only token hashes are stored.

Guards against escalation and lockout: a user manager can only grant
flags they hold and cannot touch flags they lack; only superusers change
superuser accounts; nobody can disable or delete themselves; the last
active superuser stays.

Logins, user changes, and bingo and invoice actions are recorded in an
audit log at /admin/audit.

Read timestamps as UTC: they are all written with Time.now.utc, and on a
UTC+4 host a fresh 1 h token read back as already expired.

Assisted-by: Claude:opus-5
2026-10-10 08:31:42 +04:00
8471570187
admin: per-area permissions on every route
Give each user independent read, write and delete flags per area
(invoices, bingo, users), so someone can e.g. review and remove bingo
incidents without being able to add any. The request method and path
decide the flag needed: GET is read except the new/duplicate forms, POST
to .../delete is delete, any other POST is write. Unmapped admin paths
are superuser-only, and a test enumerates every registered admin route
so a new one cannot ship without a rule.

Pages follow the same flags: nav and dashboard tiles show readable areas
only, write forms and delete buttons are hidden, and the datacenter form
is shown read-only.

Assisted-by: Claude:opus-5
2026-10-10 08:27:37 +04:00
9bf40690e5
admin: user accounts and session login
The admin was one shared HTTP Basic login: no way to give someone
partial access, revoke one person, or tell who changed what. Add a users
table and a session login form. Passwords are scrypt hashes from Ruby's
OpenSSL, so no native gem is needed. Sessions carry a per-user version
that is bumped on password change, which logs that user out everywhere.

ADMIN_USER/ADMIN_PASSWORD now bootstrap the first superuser on an empty
table and afterwards only work while no active superuser exists, as a
recovery path that re-creates one. Until per-area permissions land,
non-superusers only reach the dashboard and their own password page.

Assisted-by: Claude:opus-5
2026-10-10 08:25:46 +04:00
c2020bf903
admin: dashboard instead of the invoice list
/admin redirected to the invoice list, which stopped making sense once
the admin also ran the bingo. Make it a dashboard with one tile per
area: invoice totals, outstanding amount per currency, overdue count and
the five latest invoices; the bingo score, reserve size, latest incident
and card cells still missing a photo. A shared nav links the sections.
Tiles are per area so they can follow user permissions later.

Assisted-by: Claude:opus-5
2026-10-10 08:23:14 +04:00
e5bd1c1ac7
bingo: planned status and capacity type
All checks were successful
Build and Deploy to Production / test (push) Successful in 23s
Build and Deploy to Production / build (push) Successful in 1m0s
Build and Deploy to Production / deploy (push) Successful in 39s
"MW" on the card mixed IT load, connected power, total site power,
design capacity and even MVA (Oxygen, Sber Yuzhny Port). Record which
one a figure is in a new mw_type column; the dialog tags the number
("64 MW · design") and MVA replaces the unit, while cells keep the bare
figure to stay readable. Add a "planned" status for projects not yet
under construction; like "building", it is left out of the operational
view.

Assisted-by: Claude:opus-5
2026-10-10 06:59:04 +04:00
9e885dd8c1
bingo: operational-only view of the card
The card mixes facilities in service with construction projects (Sber
Balakovo at #1, MOS7, MOS11, Yandex Vladimir-2), which is fine for a
bingo but not when you want to compare what actually runs today. Add a
toggle that builds the card from operational rows only, in the same
ranking, with bingo lines recomputed for that card. The filtered view is
noindex with canonical to the plain locale URL, so it does not compete
with the main page in search.

Assisted-by: Claude:opus-5
2026-10-10 06:58:30 +04:00
61bb1393fe
bingo: fix photo rebuild on UTF-8 tagged S3 bodies
All checks were successful
Build and Deploy to Production / test (push) Successful in 22s
Build and Deploy to Production / build (push) Successful in 1m3s
Build and Deploy to Production / deploy (push) Successful in 39s
aws-sdk returns object bodies from a plain StringIO, so S3.get handed
back a UTF-8 tagged string and Photo.sniff's binary prefix comparison
raised Encoding::CompatibilityError: "Rebuild resized photos" died on
the first photo with a 500. URL imports had the same flaw via a UTF-8
read buffer. Treat image data as binary at the source and in sniff, and
let one failing photo no longer abort the batch (errors are reported in
the admin flash and logged).

Assisted-by: Claude:opus-5
2026-10-10 06:32:06 +04:00
201a70b07b
bingo: SEO pass and resized photos
All checks were successful
Build and Deploy to Production / test (push) Successful in 22s
Build and Deploy to Production / build (push) Successful in 1m1s
Build and Deploy to Production / deploy (push) Successful in 39s
The bingo host fell through to asxp.io's public/, so crawlers got
asxp.io's robots.txt pointing at asxp.io's sitemap (and the keys and
invoice JS were served too). Turn static files off for BingoWeb and give
it its own robots.txt and a sitemap with hreflang alternates.

The card loaded 15.6 MB of full-size photos as cell backgrounds. Derive
480/1200 px WebP variants with ImageMagick on store (memory/area limits,
input coder pinned to the sniffed type), render cells as <img> with lazy
loading past the first two rows, and add an admin action that rebuilds
variants from the stored originals.

Head gets per-locale titles and descriptions carrying the live score,
og:image from the latest hit, Twitter card, og:locale alternates,
JSON-LD WebSite + ItemList, and Vary: Accept-Language on the redirect.

Assisted-by: Claude:opus-5
2026-10-10 06:26:03 +04:00
70f1734c5d
bingo: stop serving bingo.asxp.io
All checks were successful
Build and Deploy to Production / test (push) Successful in 21s
Build and Deploy to Production / build (push) Successful in 52s
Build and Deploy to Production / deploy (push) Successful in 36s
datacenterbingo.xyz resolves now, so the stopgap second hostname is no
longer needed. Drop it from the Traefik rule and the deploy env; the
app-side BINGO_ALT_HOST support stays but is unset.

Assisted-by: Claude:opus-5
2026-10-10 06:07:37 +04:00
e725395825
bingo: add, delete and reorder datacenters from admin
All checks were successful
Build and Deploy to Production / test (push) Successful in 22s
Build and Deploy to Production / build (push) Successful in 52s
Build and Deploy to Production / deploy (push) Successful in 38s
The card was fixed to the 25 seeded rows, so a new site such as
IXcellerate MOS7 (34 MW, under construction) could only be shoehorned in
by overwriting an existing row. Add create and delete (cascading to
incidents), make position an insert that shifts the rest, and keep
positions dense. More than 25 rows are allowed: the card shows the first
25 and the rest wait in reserve. "Sort by MW" re-ranks by the parsed
power figure, unknown last.

Assisted-by: Claude:opus-5
2026-10-10 06:00:46 +04:00
cd04b01e57
bingo: serve bingo.asxp.io too, credit asxp.io in footer
datacenterbingo.xyz is not in the .xyz zone yet, while bingo.asxp.io
already resolves. Serve the card on both (BINGO_ALT_HOST) so it is
reachable now; canonical and hreflang links keep pointing at the .xyz
domain. Footer gets a copyright line linking back to asxp.io.

Assisted-by: Claude:opus-5
2026-10-10 06:00:39 +04:00
90e1bbc9c5
bingo: move to datacenterbingo.xyz
All checks were successful
Build and Deploy to Production / test (push) Successful in 21s
Build and Deploy to Production / build (push) Successful in 52s
Build and Deploy to Production / deploy (push) Successful in 39s
The card gets its own domain instead of a subdomain of the IE site.
www. is routed to the same container and 301-redirected to the apex in
config.ru, so there is one canonical host for links and the cert.

Assisted-by: Claude:opus-5
2026-10-10 05:19:50 +04:00
903dc17a75
bingo: strip photo metadata, identify to Wikimedia
Photos are served byte-for-byte, and the Commons shot of Medvedkovo
carries GPS coordinates in its EXIF, while the card is meant to stay
city-level. Drop Exif/XMP/IPTC/comment segments from JPEG, text/eXIf
chunks from PNG and EXIF/XMP chunks from WebP on store; pixel data is
left alone and malformed files are rejected.

upload.wikimedia.org answered 429 to the generic agent; its policy asks
for a tool name and contact, so send one.

Add a "re-import all" admin action so photos stored before this change
get re-fetched and stripped.

Assisted-by: Claude:opus-5
2026-10-10 05:19:49 +04:00
7820f138f6
ci: read registry token from FJ_TOKEN
All checks were successful
Build and Deploy to Production / test (push) Successful in 19s
Build and Deploy to Production / build (push) Successful in 52s
Build and Deploy to Production / deploy (push) Successful in 1m0s
Forgejo no longer allows creating secrets prefixed FORGEJO_, so the
regenerated registry token (the old one got 401 reqPackageAccess on
push) has to live under a new name.

Assisted-by: Claude:opus-5
2026-10-10 04:56:55 +04:00
bdb3682e18
bingo: serve datacenter bingo on bingo.asxp.io
Some checks failed
Tests / test (push) Successful in 18s
Build and Deploy to Production / test (push) Successful in 18s
Build and Deploy to Production / build (push) Failing after 2s
Build and Deploy to Production / deploy (push) Has been skipped
Add a second public site to the same process: a 5x5 card of the 25
largest Russian datacenters where a cell is crossed out once an incident
with a public source is recorded (Yandex Sasovo and Kaluga drone strikes
on 2026-10-08/09 are seeded). config.ru dispatches by Host, so it rides
the existing container, DB and bucket instead of a new deploy.

The card is localized ru/en/uk with per-locale columns falling back to
ru. Content is managed from /admin/bingo on asxp.io, reusing AdminAuth
and CSRF. Photos go to the existing bucket under bingo/ and are proxied
by the app, so the bucket stays private and storage needs no change.

The seed data was fact-checked against operator sites and press on
2026-10-10; several entries in the original list were wrong or vague.

Assisted-by: Claude:opus-5
2026-10-10 04:51:26 +04:00