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
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
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
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
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
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
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
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
One asset per invoice from a registry (lib/crypto_asset.rb): BTC, LTC,
ETH, XMR, SOL, ALGO, USDT/USDC on ERC-20/TRC-20/BEP-20/Solana/Algorand.
Migration 003 generalizes the ltc_* columns to crypto_* + crypto_coin
(existing LTC invoices backfilled). QR payload adapts per asset: BIP21
amount URIs where supported, bare address for tokens with a network
hint on the PDF. Default addresses come from the CRYPTO_ADDRESSES
secret (JSON code=>address); LTC_ADDRESS still works as legacy.
Updates CLAUDE.md to reflect what the site actually is now (entity page +
invoicing, not just contact form):
- "What this is" expanded to two responsibilities.
- New "Storage stack dependency" section explaining the Postgres + MinIO
split, the storage repo, and the failure mode if storage is down.
- Stack list gains Sequel/pg, Prawn, aws-sdk-s3, AdminAuth, the two
external Docker networks, and postgres in the dev shell.
- New "Invoicing" section: routes (admin + public), storage model with
the uuid mass-assignment caveat, PDF rendering + Noto Georgian fallback,
frontend touch, and the open caveats (single-process number allocator,
UUID == access).
- Secrets table extended with ADMIN_*, ASXPIO_DB_PASSWORD,
ASXPIO_S3_ACCESS_KEY, ASXPIO_S3_SECRET_KEY and their rotation rules
(dual-repo update, push storage first).
- Records the URL-safe-password lesson from the deploy that broke prod
twice, with a generator one-liner.
- Recovery snippet uses a <prod-host> placeholder rather than the real
internal IP, since this file lives in a public repo.
Also drops the global-prefs lines from Hard Rules — those belong in the
user-level CLAUDE.md, not the project file.
Sequel guards primary keys against mass assignment, so passing uuid: to
Invoice.new raised MassAssignmentRestriction and turned every invoice
creation into a 500. Build the instance with the rest of the fields, then
set uuid as an attribute before returning.
start_with?('/admin') also matched /admin-invoice-form.js, returning 401
for the public JS asset that the New Invoice form depends on. Without it,
the "+ add line" button did nothing.
Require either the bare /admin path or a /admin/-prefixed one so sibling
assets in public/ that happen to share the prefix stay reachable.
Extends the .env heredoc the workflow drops on the prod host with:
- ADMIN_USER / ADMIN_PASSWORD — gate /admin/*
- DATABASE_URL — points at the storage stack's Postgres
- S3_* (endpoint, bucket, creds) — MinIO; S3_PUBLIC_ENDPOINT is the
s3.asxp.io hostname used only for
presigning so browser-facing URLs
resolve over Traefik+TLS.
Non-secret values stay hardcoded in the workflow; secrets come from
Forgejo. ASXPIO_DB_PASSWORD / S3 creds must match the corresponding
secrets on the storage repo or the app can't authenticate.
Extends the existing dark palette to cover the invoicing UI:
- .invoice-list: zebra-free table with right-aligned totals.
- .badge / .badge-pending / .badge-paid: small status chip.
- .items-table: per-row inputs for description/qty/unit price, .remove-row
button styling.
- .invoice-grid: two-column layout for the terms block (issued/due,
currency/GEL rate).
- .link-button: subdued "+ add line" affordance.
- .download-button: the prominent CTA on the public landing page.
- color-scheme: dark on date inputs so the native picker matches the
rest of the page.
No changes to existing rules.
Three admin templates:
- index: invoice list with status badge and link to detail.
- new: client + terms fields, dynamic line-item rows backed by
admin-invoice-form.js (add/remove + re-index of items[N][...] names).
- show: invoice detail, public URL to share, toggle-paid button.
One public template, invoice_public.erb, shown at /i/:uuid: client name,
number, total, status, and a single big download button to /i/:uuid/pdf.
All admin pages are noindex; same for the public landing (it's a
client-specific URL, not search-discoverable content).
Mounts AdminAuth (HTTP Basic on /admin/*) and adds two route groups:
- Admin (/admin/invoices): list, new form, create, show, toggle-paid.
POST /admin/invoices builds the Invoice, renders the PDF, uploads to
MinIO, then persists the row with the resulting pdf_key.
- Public (/i/:uuid): an HTML landing page showing client name, number,
total, and status badge, plus /i/:uuid/pdf which 302s to a short-lived
MinIO presigned URL. Anyone with the UUID can fetch the PDF — that's
the whole point of the link-based delivery.
The Invoice model is required only when DATABASE_URL is set, so the
contact-form site still boots in dev without Postgres. Invoicing routes
return 503 in that mode rather than crashing on first request.
S3 module wraps aws-sdk-s3 against MinIO. Two clients on purpose:
- The bytes-mover client points at S3_ENDPOINT (internal minio:9000 over
the storage Docker network).
- The presign-only client points at S3_PUBLIC_ENDPOINT (https://s3.asxp.io
via Traefik) so the URLs it mints resolve from a browser.
force_path_style on both because subdomain-style URLs would require wildcard
DNS under s3.asxp.io.
AdminAuth is a tiny Rack middleware that 401s any request under /admin
unless HTTP Basic credentials match ADMIN_USER / ADMIN_PASSWORD from env.
Fails closed if either env var is missing.