23 Commits
Author SHA1 Message Date
michilisandClaude Fable 5.1 ae7664fbe0 Stop a listing page from rewriting the next one's grid after navigation.
Switching between /mints, /fedimints and /lnurl-mints showed the right list
for a moment and then flashed back to the previous ecosystem's mints. The
client router keeps every page's script alive, and onReady re-runs each
setup on every arrival, so a visited page's setup also ran on the next page.
All three grids were marked with the same bare data-mint-grid, so the stale
setup found the new grid, fetched its own type and overwrote it.

Mark and query each grid by ecosystem (data-mint-grid="cashu" etc.), the way
the home page already scopes data-home-grid, so a stale setup finds nothing
and bails. Skip the render in hydrateMintGrid when the grid has already been
detached by the router. Document the re-run-everywhere contract on onReady,
and add a static wiring test so a bare marker cannot come back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 22:36:26 +02:00
michilisandCursor 70b35f4ccc Show a brief publishing note that fades out after a few seconds.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 22:42:00 +02:00
michilisandClaude Opus 5 1eade490c8 Order the latest-reviews strip by created_at alone.
The home page carousel floated reviews whose author had a kind 0 ahead of
everything else, capped at 90 days old, so a named review from two months ago
sat between two reviews from this week. Under a heading that says "Latest
reviews", with each card's foot printing the very timestamp being overruled,
that reads as broken rather than as curation.

Strict recency now, newest first, as the last thing that happens to the list,
with the event id breaking ties so two builds of the same events agree. Names
and avatars are still resolved and still shown; they no longer decide the
order.

The candidate scan stops at the limit rather than gathering twelve times it:
the scan already runs newest first, so nothing further down can outrank what
it has, and the build resolves ten profiles per locale instead of a hundred
and twenty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:23:22 +02:00
michilisandClaude Opus 5 860a4de009 Check in the dynamic-mint-data analysis it was already sitting on.
It was untracked in the working tree. web/src/lib/mint-cards.ts cites it for the
reasoning behind hydrating rather than moving the list to SSR, and a comment
pointing at a file nobody else has is worse than no comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:34:54 +02:00
michilisandClaude Opus 5 24fe2003b6 Drop the nightly rebuild timer.
The timer was load-bearing while the mint list was a build-time snapshot: a
rebuild was the only way a new mint, a new review count or a changed status ever
reached /mints. The list hydrates now, so all three arrive within a second of
load, in every language, and rebuilding 2,000 pages at 03:30 to refresh numbers
that refresh themselves is twenty minutes of CPU for nothing.

cashumints-web.service stays exactly as it is — it is the deploy-time publish
step, and now the only thing that starts it is a deploy. A build still produces
what only a build can: the prerendered HTML a crawler reads, a social card per
mint, the sitemap and hreflang set, and a /mint/{host} page for every mint known
at build time.

The one thing that gets staler is that last item. A mint indexed since the last
deploy has no prerendered page: /mint/newhost is a 404, whose resolver looks the
address up against the live API and renders it — readable, reviewable, noindex
until a deploy gives it a real page. That was already true between nightly
builds; this only lengthens the window.

README documents the one-time host commands to remove the installed timer, and
gains a "Live lists" section describing what replaced it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:29:09 +02:00
michilisandClaude Opus 5 14548179a0 Hydrate the mint lists from the live API after paint.
/mints was a snapshot of whatever the API held when `astro build` ran, and stayed
that until the next build: a mint indexed at noon was reviewable at once — the 404
resolver saw to that — and simply had no card until 03:30. Every card's rating,
review count and status were as stale as the page.

The three index pages and the home page's three top-six strips now refetch
`GET /api/mints?type=…` once, after paint, and rebuild their grids. The
prerendered cards stay: they are the first paint, what a crawler indexes, and the
whole page without JavaScript. Hydration only ever replaces them with something
newer, and never with nothing — neither a failed fetch nor a well-formed empty
array touches a grid that has cards in it.

To make that affordable, the list payload grew the facts a chip is drawn from:
`nuts`, `capabilities`, and the two probed LNURL fields. /mints and /lnurl-mints
were fetching `GET /api/mints/:host` once per mint at build time to read two
booleans off each; that N+1 is gone from both, which takes the build from
fifty-six requests to one and is what makes the same read possible in a browser.
Additive: `MintDetail` already had all four.

web/src/lib/mint-cards.ts is MintCard.astro's parallel renderer, the same
relationship review-cards.ts has with the reviews panel. Same classes, same
data-* attributes — the sort, the search, the rank chips and the shared-element
view transitions all read the DOM — and the same i18n, through the page's own
inlined catalog rather than a build-time one.

Base.astro gained `clientNamespaces`, so the home page can inline the `home.`
catalog its strips need to rewrite "All 60 mints →" without putting 2KB of
marketing copy on 1,300 mint pages. check-i18n reads the prop off the page, so
the two cannot disagree.

Verified in Chromium against the built site: 60 prerendered cards become 61
including a mint inserted after the build; sort, search and hide-offline operate
on the new cards; /es/mints renders "En línea", "54 reseñas", "4,9" and "Solo
fundir"; JavaScript disabled still shows all 60; an aborted or empty API leaves
the grid alone; and a navigation away and back re-hydrates. 2016 pages build,
link and hreflang checks pass, 30 web tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:27:33 +02:00
michilisandClaude Opus 5 060c7f1a59 Refuse to publish a site built from a hollow index.
Health answering 200 and the index being complete are different claims. A year of
~31-event backfills left a perfectly healthy API serving a real, correct, complete
list of eight mints. A build against that succeeds — it prerenders eight cards —
and rsync --delete-after then replaces fifty-five with eight.

cashumints-web.service gains a second ExecStartPre after the health wait: count
/api/mints, and exit non-zero below MIN_MINTS_FOR_BUILD (default 20, overridable
with `systemctl edit`). A refusal aborts the unit before `pnpm build`, and
publishing is ExecStartPost, so the previously published site is untouched; the
OnFailure alert added in the last commit says why.

Counted by the "host": key rather than by counting braces, because the list
payload is about to carry a nested object per mint. A curl that fails at all
counts as zero, which is below every floor — so an API that fell over between the
health check and this line refuses the build instead of sailing through it.

Verified against three live APIs: 73 mints passes, a doctored 8-mint database
fails with the reason, and a dead port fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:13:16 +02:00
michilisandClaude Opus 5 0ebc8ada54 Make a crash loop reach somebody instead of scrolling past.
`Restart=on-failure` with no start limit is an infinite loop by definition: the
unit never reaches `failed`, `systemctl status` stays active (auto-restart), and
the only evidence is a journal moving at four lines a second. That is how 464
restarts over fifteen hours went unnoticed.

All three units now stop after five failures in 120s and run
OnFailure=cashumints-alert@%n.service. The window is 120s and not 60s because
RestartSec=5s plus a process that takes a few seconds to die can spread five
failures past a sixty second window, reset the counter, and loop forever anyway.

cashumints-alert@.service is a oneshot that takes the failed unit's name as its
instance. Configuration is /etc/cashumints/alert.env: NTFY_URL gets a plain-text
body, WEBHOOK_URL gets JSON carrying `content` so one payload fits Discord and
Slack-compatible endpoints. With neither set — or the file absent — it still
writes to the journal at ERROR via a `<3>` syslog prefix, so `journalctl -p err -t
cashumints-alert` is a complete history on a host nobody configured.

It cannot become a second thing to debug: each curl is bounded at 10s, each
failure falls back to a journal line, and the shell ends in `true`, so the alerter
always exits 0. Verified with systemd-analyze verify and by running the ExecStart
body against a local sink — the JSON parses, and every branch exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:10:01 +02:00
michilisandClaude Opus 5 9ffa53094d Make a starved discovery cycle say so, in the log and on /api/health.
For about a year the production RELAYS list did not include the relay carrying
the kind 38000/38172 archive. Every backfill read about thirty events, wrote them
faithfully, reported ok=true, and the nightly build republished an index of eight
mints. Nothing measured the difference between "the cycle completed" and "the
cycle read anything", so nothing went red.

Three signals now do:

  - Per-relay attribution. queryRelays() replaces pool.querySync(), which merges
    every relay into one deduplicated array and throws away who sent what. It
    keeps one subscription per relay over the pool's existing sockets and shares
    a single alreadyHaveEvent across them, so an event five relays carry is still
    verified once; receivedEvent fires before that check, which is what makes the
    per-relay count mean "what this relay contributed". The deadline moved out of
    each Subscription's own EOSE timer so `eose` means a frame arrived rather than
    something timed out.

  - A WARN naming any relay that will not connect, on every cycle, and any relay
    that connected and sent nothing, on backfills only. An incremental cycle is
    supposed to come back empty.

  - BACKFILL_MIN_EVENTS, default 200. Under it, ERROR discovery starvation
    suspected and a flag health reports as discovery_starved, forcing 503. Sticky
    across incremental cycles so an hourly cycle finding four events cannot clear
    what a backfill diagnosed; stored in the database so a restart cannot either.

A fresh database is starved until its first backfill lands. That is intended: it
holds the build's health gate rather than publishing a site made from nothing.

Verified against the live relay set — 1528 events, five relays connected, EOSE on
all five, health 200 — and against an unreachable list, which produces the two
WARN lines, the ERROR, and 503.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:07:01 +02:00
michilisandClaude Opus 5 65307ba278 Compile the API instead of running its TypeScript in production.
The unit's ExecStart named src/index.ts, so every start depended on the host
having Node 22.18 or newer for native type stripping. A deploy onto a host with
Node 20 met ERR_UNKNOWN_FILE_EXTENSION, exited in under a second, and was
restarted 464 times over fifteen hours with nothing anywhere going red.

api/tsconfig.json now emits to api/dist. The source keeps its explicit .ts import
specifiers, which is what makes `node --watch src/index.ts` work in development;
rewriteRelativeImportExtensions turns them into .js on the way out, so what runs
in production is ordinary ESM that any Node from 20.18 up will start.

`pnpm build` builds shared, then api, then web. `pnpm dev` is unchanged.

deploy/ is tracked rather than ignored: the unit files are the thing an operator
copies to /etc/systemd/system, and the alert unit added next has to live
somewhere a deploy can find it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:58:53 +02:00
michilisandCursor 06ba3d35e7 Clarify the empty-WEB_ROOT hint to point at cashumints-web.
A bare pnpm build only fills dist; production needs the publish unit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 06:46:23 +02:00
michilisandCursor 79a115be38 Serve the prerendered site from Node instead of nginx root.
Avoids www-data traversing the cashumints tree and keeps rebuilds from blanking a live root.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 06:27:27 +02:00
michilisandCursor 301679d340 Wire locale catalogs, repair mint terminology, and confirm login with a toast.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 22:35:43 +02:00
michilisandCursor b95aab2bcd Improve write-review flow and require Node 22.18 for native TS stripping.
Gate login, reveal the form after a rating, and wire inline Rate this mint; drop --experimental-strip-types.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 21:52:11 +02:00
michilisandCursor c74c7fc187 Ship LNURL mint pages, indexing UI, and reviews rewrite.
Add lnurl list/detail routes, OG fixtures, i18n strings, and the write/
index client flows so the site surfaces the new mint type end to end.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 03:44:43 +02:00
michilisandCursor 2a9444942b Index, probe, and announce LNURL mints in the API.
Wire discovery and probing for LNURL mints, add rate-limited POST /api/index
for user submissions, and optionally announce confirmed state to relays.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 03:44:35 +02:00
michilisandCursor c97b44018d Add shared LNURL types, indexing helpers, and warnings.
Introduce lnurl as a first-class mint type with probe/announcement fields
and shared helpers the API and web can both rely on.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 03:44:27 +02:00
michilisandCursor 36c01861f5 Document LNURL mint kind and live probe notes.
Capture the NIP kind contract and wire-level NOTES so indexing and probing
can follow observed LNURL mint behavior rather than outdated assumptions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 03:44:20 +02:00
michilisandCursor be322cb0d8 Add configurable Plausible analytics
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 06:11:04 +02:00
michilisandCursor 47e6537dde Expand i18n locales and improve reviews UI
Add many new language packs with RTL support, refresh brand assets, and harden review rendering with tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 05:06:58 +02:00
michilisandCursor 1c5df18e81 Improve SEO and social card wiring
Add SearchAction and optional Product JSON-LD, per-page OG images/alts, and document the og build and cache headers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 02:17:35 +02:00
michilis 6f17b572b1 Expand ecash explorer capabilities
Add Fedimint discovery, dual SQLite/Postgres storage, richer review handling, and generated social imagery.
2026-08-21 02:10:48 +02:00
michilisandCursor aa1771ea20 first commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 22:41:25 +02:00