Commit graph

25 commits

Author SHA1 Message Date
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
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
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
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
0cafc484c7
invoices: em-dash cleanup in admin form and PDF copy 2026-07-16 20:01:26 +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
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
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
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
c9bcbe7ff5
ltc payments 2026-06-08 18:43:01 +03:00
8f3afcfcd2
logo in pdf
All checks were successful
Build and Deploy to Production / build (push) Successful in 1m2s
Build and Deploy to Production / deploy (push) Successful in 39s
2026-06-08 18:15:23 +03:00
a34b8cd33d
fix: assign uuid after Invoice.new to avoid mass-assignment restriction
All checks were successful
Build and Deploy to Production / build (push) Successful in 47s
Build and Deploy to Production / deploy (push) Successful in 25s
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.
2026-05-26 18:34:45 +03:00
c7fe108ecf
fix: AdminAuth prefix matched too greedily
All checks were successful
Build and Deploy to Production / build (push) Successful in 46s
Build and Deploy to Production / deploy (push) Successful in 25s
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.
2026-05-26 18:31:32 +03:00
0dc31fbafe
storage: S3 client wrapper and admin Basic-auth middleware
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.
2026-05-26 17:43:48 +03:00
a605ccdc52
invoicing: Prawn PDF renderer with Noto fonts
InvoicePdf.render(invoice) returns A4 PDF bytes. Layout: title + number,
horizontal rule, two-column FROM/BILL TO, meta strip (issued/due/currency/
status) bracketed by rules, line items table, right-aligned totals with
optional GEL equivalent, payment block (Bank of Georgia IBAN), optional
notes, repeating footer with issuer ID and page number.

Both party columns capture pdf.y after rendering and the global cursor jumps
to whichever ran longer — avoids the bbox cursor-restore footgun that was
collapsing the meta strip into the FROM column.

Noto Sans Georgian is registered as a fallback family so the Georgian legal
name renders without needing a separate text run.

Issuer details (legal name, tax ID, registration, address, bank) are
hardcoded as a constant — these are public on the rendered site already and
change rarely enough that a config file would be overkill.
2026-05-26 17:43:38 +03:00
1220f37d5f
invoicing: Invoice model with number allocation and totals
Sequel::Model wrapper over the invoices table. Three responsibilities:

- Invoice.build takes form params (client, currency, GEL rate, items array)
  and returns an unsaved Invoice with a freshly-allocated number, normalized
  line items, and computed subtotal.
- Invoice.allocate_number scans for the highest INV-<year>-NNNN and
  increments. Single-process deploy means no contention; if that changes
  this needs a DB-side sequence per year.
- Instance methods: paid?, status, total, total_gel for the PDF renderer
  and the public landing page.

Items are stored as JSONB; qty and unit_price are kept as strings on the
way in/out so BigDecimal arithmetic stays exact through the JSON round-trip.
2026-05-26 17:43:28 +03:00
3fdb0f36b3
db: connection helper and invoices migration
DB.connect! opens a single Sequel connection from DATABASE_URL; DB.migrate!
runs Sequel migrations from db/migrations/.

The first migration creates the invoices table: uuid PK, unique invoice
number, client + currency fields, JSONB for line items, GEL conversion rate
captured at issue time, paid_at for the status toggle, pdf_key for the
MinIO object.
2026-05-26 17:43:18 +03:00
7608cf15fc
useful icon and links
All checks were successful
Build and Deploy to Production / build (push) Successful in 45s
Build and Deploy to Production / deploy (push) Successful in 32s
2026-05-05 00:57:11 +04:00
1ab09043ee
ruby rework with contact form
All checks were successful
Build and Deploy to Production / build (push) Successful in 51s
Build and Deploy to Production / deploy (push) Successful in 34s
2026-05-05 00:45:27 +04:00