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
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
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
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
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
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
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
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
/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
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.
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.