Commit graph

52 commits

Author SHA1 Message Date
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
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
2c1d66dce5
docs: storage stack, invoicing routes, secrets table
All checks were successful
Build and Deploy to Production / build (push) Successful in 48s
Build and Deploy to Production / deploy (push) Successful in 25s
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.
2026-05-26 18:51:26 +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
dc40faf5df
deploy: write invoicing env vars to .env
All checks were successful
Build and Deploy to Production / build (push) Successful in 46s
Build and Deploy to Production / deploy (push) Successful in 24s
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.
2026-05-26 18:07:15 +03:00
96a0f55741
css: invoice list, badges, item form, download button
All checks were successful
Build and Deploy to Production / build (push) Successful in 1m9s
Build and Deploy to Production / deploy (push) Successful in 30s
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.
2026-05-26 17:54:48 +03:00
226ccb8192
invoicing: admin and public views
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).
2026-05-26 17:54:40 +03:00
77724e1ee2
invoicing: admin and public routes
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.
2026-05-26 17:54:31 +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
bf4683a973
fonts: vendor Noto Sans + Noto Sans Georgian
Prawn needs explicit TTFs to render anything beyond the built-in Latin
glyphs. Bundling Noto Sans (Latin/Cyrillic) and Noto Sans Georgian lets the
invoice PDF show both Latin and Georgian forms of the legal name.

OFL.txt is the upstream SIL Open Font License covering all five files.
2026-05-26 17:29:30 +03:00
2ea46b12e6
gems: add pg, sequel, prawn, aws-sdk-s3 for invoicing
- pg + sequel: connect to the storage stack's Postgres.
- prawn + prawn-table: render invoice PDFs.
- aws-sdk-s3: upload PDFs to MinIO and mint presigned download URLs.

Also pulls libpq-dev into the Docker build (for the pg gem) and the
postgresql client lib into the Nix dev shell.
2026-05-26 17:29:21 +03:00
1e4bded4ae
compose: join external storage network
The new asxpio/storage stack owns a Docker network named "storage" carrying
Postgres and MinIO. The app joins it (alongside traefik) so it can reach
postgres:5432 and minio:9000 by service name.
2026-05-26 17:29:09 +03:00
c3f37e6cb0
more SEO: org -> person
All checks were successful
Build and Deploy to Production / build (push) Successful in 47s
Build and Deploy to Production / deploy (push) Successful in 32s
2026-05-05 01:45:18 +04:00
7296ffc944
SEO optimization
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 01:13:59 +04: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
90402ae14a
ie sergei poljanski georgia
All checks were successful
Build and Deploy to Production / build (push) Successful in 16s
Build and Deploy to Production / deploy (push) Successful in 40s
2026-05-05 00:02:26 +04:00
0cc457c1fa
hedgehog
All checks were successful
Build and Deploy to Production / build (push) Successful in 6s
Build and Deploy to Production / deploy (push) Successful in 19s
2025-11-25 05:27:31 +02:00
9071cad3e5 Merge pull request 'ci/cd' (#1) from fix/pipeline into main
Some checks failed
Build and Deploy to Production / build (push) Failing after 7s
Build and Deploy to Production / deploy (push) Has been skipped
Reviewed-on: https://git.asxp.io/asxpi/asxpio/pulls/1
2025-11-25 03:25:25 +00:00
13f6db18f2
ci/cd 2025-11-25 05:24:04 +02:00
20123ffa98
quality of life changes, removed old icon 2024-04-13 23:35:44 +03:00
31edd556c6
SSH and PGP keys added, changed icon 2024-04-13 22:32:32 +03:00
8a4f8449a8
Added links v4 2023-08-25 05:11:15 +03:00
734b339701
Added links v3 2023-08-25 05:08:17 +03:00
6c2dc72ddc
Added links v2 2023-08-25 05:05:00 +03:00
11f354b6d0
Added links 2023-08-25 05:00:34 +03:00
0f250c84ed
Added site icon 2023-08-21 04:14:42 +03:00
4584843b70 font for footer 2023-08-03 00:33:39 +03:00