Commit graph

83 commits

Author SHA1 Message Date
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
1f059dc2f6
remove contact form
All checks were successful
Build and Deploy to Production / test (push) Successful in 45s
Build and Deploy to Production / build (push) Successful in 1m6s
Build and Deploy to Production / deploy (push) Successful in 46s
Tests / test (push) Successful in 43s
2026-08-04 01:25:50 +04:00
a7dbe55694
docs: brand assets, fonts and tokens, page structure
All checks were successful
Build and Deploy to Production / test (push) Successful in 45s
Build and Deploy to Production / build (push) Successful in 6m2s
Build and Deploy to Production / deploy (push) Successful in 37s
2026-07-16 20:01:26 +04:00
0cafc484c7
invoices: em-dash cleanup in admin form and PDF copy 2026-07-16 20:01:26 +04:00
82fc1c6e17
site: multi-page structure with shared nav; rework services, keys, contact 2026-07-16 20:01:26 +04:00
03f38ab908
layout: drop font CDNs, OG image meta, refreshed JSON-LD 2026-07-16 20:01:25 +04:00
0aec9f9320
assets: OG card and logo-derived favicons 2026-07-16 20:01:25 +04:00
89700368ef
styles: design tokens and self-hosted IBM Plex fonts 2026-07-16 20:01:25 +04:00
0fbb6ee809
invoices: multi-crypto payment support
All checks were successful
Build and Deploy to Production / test (push) Successful in 18s
Build and Deploy to Production / build (push) Successful in 49s
Build and Deploy to Production / deploy (push) Successful in 31s
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.
2026-07-03 02:01:57 +04:00
f9c7001e7a
invoices: issuer contact ie@asxp.io on PDF 2026-07-02 18:49:03 +04:00
f7319a08a9
fmt: shared number formatting for HTML and PDF
All checks were successful
Build and Deploy to Production / test (push) Successful in 2m28s
Build and Deploy to Production / build (push) Successful in 52s
Build and Deploy to Production / deploy (push) Successful in 30s
2026-07-02 18:40:20 +04:00
9e85082758
app: enable host authorization in production 2026-07-02 18:40:20 +04:00
15cff895af
ci: cache gems and persist nix store between runs 2026-07-02 18:39:37 +04:00
67263cb75c
ci: gate build on tests, deploy waits for container health
Some checks failed
Build and Deploy to Production / build (push) Blocked by required conditions
Build and Deploy to Production / deploy (push) Blocked by required conditions
Build and Deploy to Production / test (push) Failing after 11m37s
2026-07-02 18:32:53 +04:00
d1d82d90ff
docker: HEALTHCHECK via /healthz, freeze bundle to lockfile 2026-07-02 18:32:53 +04:00
afde73e772
app: add /healthz liveness endpoint 2026-07-02 18:32:52 +04:00
9485b6b72a
ci: drop git install — image ships git-minimal, full git conflicts
All checks were successful
Build and Deploy to Production / build (push) Successful in 1m2s
Tests / test (push) Successful in 1m1s
Build and Deploy to Production / deploy (push) Successful in 26s
2026-07-02 18:09:39 +04:00
5dbd1d4dc8
ci: use service hostname — runner networking fixed to per-workflow bridge
Some checks failed
Tests / test (push) Failing after 18s
Build and Deploy to Production / build (push) Successful in 51s
Build and Deploy to Production / deploy (push) Successful in 24s
2026-07-02 18:02:02 +04:00
6807d41aa4
ci: publish postgres service port — job containers use host network
Some checks failed
Tests / test (push) Failing after 1s
Build and Deploy to Production / build (push) Successful in 53s
Build and Deploy to Production / deploy (push) Successful in 25s
2026-07-02 17:57:25 +04:00
9ff394c75f
ci: run test suite on push
Some checks failed
Build and Deploy to Production / build (push) Successful in 1m0s
Build and Deploy to Production / deploy (push) Successful in 43s
Tests / test (push) Failing after 1s
2026-07-02 17:38:35 +04:00
43bed98329
tests: minitest + rack-test suite, bin/test with ephemeral Postgres 2026-07-02 17:32:14 +04:00
8468b3d099
deps: declare rexml — no longer a default gem on Ruby 3.4, aws-sdk needs it 2026-07-02 17:31:44 +04:00
187910f69b
admin form: disable submit button to prevent duplicate invoices 2026-07-02 17:31:20 +04:00
fb62176358
admin: constant-time credential comparison 2026-07-02 17:31:19 +04:00
a6a155ee46
rate limit: prune stale keys so @hits cannot grow unbounded 2026-07-02 17:31:19 +04:00
ef3cb2a304
invoices: numeric number allocation, retry create on collision 2026-07-02 17:31:19 +04:00
052ff68616
invoices: reject non-positive gel_rate and non-numeric line items 2026-07-02 17:30:43 +04:00
16680be309
invoices: rework of PDF
All checks were successful
Build and Deploy to Production / build (push) Successful in 50s
Build and Deploy to Production / deploy (push) Successful in 27s
2026-06-09 03:52:12 +03:00
cb52388bf1
invoices: make duplicate
All checks were successful
Build and Deploy to Production / build (push) Successful in 50s
Build and Deploy to Production / deploy (push) Successful in 26s
2026-06-09 03:28:42 +03:00
7ed1a674aa
main page: SP -> Logo
All checks were successful
Build and Deploy to Production / build (push) Successful in 48s
Build and Deploy to Production / deploy (push) Successful in 27s
2026-06-09 02:22:28 +03:00
7dbeb0dd49
nbg and auto fill GEL exchange rate
All checks were successful
Build and Deploy to Production / build (push) Successful in 47s
Build and Deploy to Production / deploy (push) Successful in 27s
2026-06-08 19:29:25 +03:00
37a491cf79
fix: pending/paid invoices 2026-06-08 19:00:03 +03:00
d37f1d5dfe
ltc address secret
All checks were successful
Build and Deploy to Production / build (push) Successful in 49s
Build and Deploy to Production / deploy (push) Successful in 26s
2026-06-08 18:47:58 +03:00
c9bcbe7ff5
ltc payments 2026-06-08 18:43:01 +03:00