From aa1771ea20c962636ff0d07f3e9a708e1c650dc6 Mon Sep 17 00:00:00 2001 From: michilis Date: Thu, 20 Aug 2026 22:41:25 +0200 Subject: [PATCH] first commit Co-authored-by: Cursor --- .env.example | 115 + .gitignore | 7 + NOTES-PAGES.md | 186 + NOTES.md | 215 + README.md | 680 +++ api/package.json | 27 + api/src/check-offline.ts | 102 + api/src/check-warnings.ts | 289 ++ api/src/check.ts | 215 + api/src/config.ts | 28 + api/src/db.ts | 101 + api/src/discovery.ts | 257 + api/src/icons.ts | 65 + api/src/index.ts | 95 + api/src/log.ts | 21 + api/src/mints.ts | 94 + api/src/probe.ts | 169 + api/src/queries.ts | 275 + api/src/seed.ts | 120 + api/src/server.ts | 48 + api/tsconfig.json | 18 + package.json | 21 + pnpm-lock.yaml | 4519 +++++++++++++++++ pnpm-workspace.yaml | 4 + shared/fixtures/warnings/degraded.json | 90 + shared/fixtures/warnings/frozen.json | 37 + shared/fixtures/warnings/healthy-cdk.json | 136 + .../fixtures/warnings/healthy-nutshell.json | 90 + shared/fixtures/warnings/melt-disabled.json | 90 + shared/fixtures/warnings/melt-only.json | 73 + .../warnings/no-lightning-methods.json | 86 + shared/fixtures/warnings/no-nut04-nut05.json | 63 + .../warnings/offline-gone-melt-disabled.json | 90 + shared/fixtures/warnings/offline-gone.json | 90 + .../warnings/offline-long-melt-only.json | 73 + shared/fixtures/warnings/offline-long.json | 90 + shared/fixtures/warnings/offline-never.json | 7 + .../offline-recent-melt-disabled.json | 90 + shared/fixtures/warnings/offline-recent.json | 90 + shared/package.json | 15 + shared/src/index.ts | 6 + shared/src/normalize.ts | 101 + shared/src/nostr.ts | 313 ++ shared/src/nuts.ts | 148 + shared/src/score.ts | 72 + shared/src/types.ts | 109 + shared/src/warnings.ts | 419 ++ shared/tsconfig.json | 18 + web/astro.config.mjs | 79 + web/boneyard.config.json | 5 + web/package.json | 27 + web/public/apple-touch-icon.png | Bin 0 -> 2004 bytes web/public/favicon-32.png | Bin 0 -> 888 bytes web/public/favicon.ico | Bin 0 -> 910 bytes web/public/favicon.svg | 1 + web/public/fonts/OFL.txt | 303 ++ web/public/icon-192.png | Bin 0 -> 2168 bytes web/public/icon-512.png | Bin 0 -> 2745 bytes web/public/og.png | Bin 0 -> 125422 bytes web/public/site.webmanifest | 14 + web/scripts/bones.mjs | 88 + web/scripts/check-hreflang.mjs | 300 ++ web/scripts/check-i18n.mjs | 341 ++ web/scripts/check-links.mjs | 154 + web/scripts/i18n-build-check.mjs | 29 + web/scripts/load-env.mjs | 48 + web/scripts/make-assets.mjs | 183 + web/src/assets/fonts/inter-latin-ext.woff2 | Bin 0 -> 85068 bytes web/src/assets/fonts/inter-latin.woff2 | Bin 0 -> 48256 bytes .../fonts/jetbrains-mono-latin-ext.woff2 | Bin 0 -> 11624 bytes .../assets/fonts/jetbrains-mono-latin.woff2 | Bin 0 -> 31432 bytes .../fonts/space-grotesk-latin-ext.woff2 | Bin 0 -> 18940 bytes .../assets/fonts/space-grotesk-latin.woff2 | Bin 0 -> 22288 bytes web/src/bones/mint-page.bones.json | 326 ++ web/src/bones/reviews-panel.bones.json | 407 ++ web/src/components/ColorBends.astro | 122 + web/src/components/CopyableId.astro | 43 + web/src/components/Footer.astro | 68 + web/src/components/LanguageSwitcher.astro | 225 + web/src/components/LoginDialog.astro | 173 + web/src/components/MintCard.astro | 177 + web/src/components/MintLive.astro | 144 + web/src/components/MintQr.astro | 142 + web/src/components/Moai.astro | 26 + web/src/components/PulseTicker.astro | 175 + web/src/components/Reviews.astro | 991 ++++ web/src/components/Sparkline.astro | 123 + web/src/components/Topbar.astro | 83 + web/src/i18n/GLOSSARY.md | 73 + web/src/i18n/client.ts | 74 + web/src/i18n/config.ts | 83 + web/src/i18n/en.json | 550 ++ web/src/i18n/es.json | 554 ++ web/src/i18n/format.ts | 175 + web/src/i18n/index.ts | 87 + web/src/i18n/locales.mjs | 14 + web/src/i18n/mint.ts | 59 + web/src/i18n/nl.json | 550 ++ web/src/i18n/paths.ts | 80 + web/src/i18n/routing.ts | 107 + web/src/i18n/translate.ts | 138 + web/src/layouts/Base.astro | 329 ++ web/src/lib/api.ts | 45 + web/src/lib/auth-ui.ts | 467 ++ web/src/lib/client.ts | 204 + web/src/lib/feed-client.ts | 135 + web/src/lib/format.ts | 76 + web/src/lib/mint-state.ts | 179 + web/src/lib/modal.ts | 86 + web/src/lib/nostr-build.ts | 397 ++ web/src/lib/nostr-signer.ts | 545 ++ web/src/lib/profiles.ts | 225 + web/src/lib/qr.ts | 79 + web/src/lib/review-cards.ts | 316 ++ web/src/lib/reviews-client.ts | 314 ++ web/src/lib/schema.ts | 252 + web/src/lib/seo.ts | 92 + web/src/lib/site.ts | 27 + web/src/lib/skeleton-fixtures.ts | 107 + web/src/lib/skeleton.ts | 209 + web/src/pages/[...locale]/404.astro | 221 + web/src/pages/[...locale]/about.astro | 125 + web/src/pages/[...locale]/disclaimer.astro | 87 + web/src/pages/[...locale]/index.astro | 658 +++ web/src/pages/[...locale]/mint/[host].astro | 719 +++ web/src/pages/[...locale]/mints.astro | 398 ++ web/src/pages/[...locale]/privacy.astro | 111 + web/src/pages/[...locale]/reviews.astro | 547 ++ web/src/pages/[...locale]/terms.astro | 87 + web/src/pages/[...locale]/wallets.astro | 124 + web/src/pages/robots.txt.ts | 35 + web/src/pages/sitemap.xml.ts | 141 + web/src/scripts/color-bends.ts | 484 ++ web/src/scripts/reveal.ts | 358 ++ web/src/styles/global.css | 1471 ++++++ web/tsconfig.json | 11 + 136 files changed, 27069 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 NOTES-PAGES.md create mode 100644 NOTES.md create mode 100644 README.md create mode 100644 api/package.json create mode 100644 api/src/check-offline.ts create mode 100644 api/src/check-warnings.ts create mode 100644 api/src/check.ts create mode 100644 api/src/config.ts create mode 100644 api/src/db.ts create mode 100644 api/src/discovery.ts create mode 100644 api/src/icons.ts create mode 100644 api/src/index.ts create mode 100644 api/src/log.ts create mode 100644 api/src/mints.ts create mode 100644 api/src/probe.ts create mode 100644 api/src/queries.ts create mode 100644 api/src/seed.ts create mode 100644 api/src/server.ts create mode 100644 api/tsconfig.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 shared/fixtures/warnings/degraded.json create mode 100644 shared/fixtures/warnings/frozen.json create mode 100644 shared/fixtures/warnings/healthy-cdk.json create mode 100644 shared/fixtures/warnings/healthy-nutshell.json create mode 100644 shared/fixtures/warnings/melt-disabled.json create mode 100644 shared/fixtures/warnings/melt-only.json create mode 100644 shared/fixtures/warnings/no-lightning-methods.json create mode 100644 shared/fixtures/warnings/no-nut04-nut05.json create mode 100644 shared/fixtures/warnings/offline-gone-melt-disabled.json create mode 100644 shared/fixtures/warnings/offline-gone.json create mode 100644 shared/fixtures/warnings/offline-long-melt-only.json create mode 100644 shared/fixtures/warnings/offline-long.json create mode 100644 shared/fixtures/warnings/offline-never.json create mode 100644 shared/fixtures/warnings/offline-recent-melt-disabled.json create mode 100644 shared/fixtures/warnings/offline-recent.json create mode 100644 shared/package.json create mode 100644 shared/src/index.ts create mode 100644 shared/src/normalize.ts create mode 100644 shared/src/nostr.ts create mode 100644 shared/src/nuts.ts create mode 100644 shared/src/score.ts create mode 100644 shared/src/types.ts create mode 100644 shared/src/warnings.ts create mode 100644 shared/tsconfig.json create mode 100644 web/astro.config.mjs create mode 100644 web/boneyard.config.json create mode 100644 web/package.json create mode 100644 web/public/apple-touch-icon.png create mode 100644 web/public/favicon-32.png create mode 100644 web/public/favicon.ico create mode 100644 web/public/favicon.svg create mode 100644 web/public/fonts/OFL.txt create mode 100644 web/public/icon-192.png create mode 100644 web/public/icon-512.png create mode 100644 web/public/og.png create mode 100644 web/public/site.webmanifest create mode 100644 web/scripts/bones.mjs create mode 100644 web/scripts/check-hreflang.mjs create mode 100644 web/scripts/check-i18n.mjs create mode 100644 web/scripts/check-links.mjs create mode 100644 web/scripts/i18n-build-check.mjs create mode 100644 web/scripts/load-env.mjs create mode 100644 web/scripts/make-assets.mjs create mode 100644 web/src/assets/fonts/inter-latin-ext.woff2 create mode 100644 web/src/assets/fonts/inter-latin.woff2 create mode 100644 web/src/assets/fonts/jetbrains-mono-latin-ext.woff2 create mode 100644 web/src/assets/fonts/jetbrains-mono-latin.woff2 create mode 100644 web/src/assets/fonts/space-grotesk-latin-ext.woff2 create mode 100644 web/src/assets/fonts/space-grotesk-latin.woff2 create mode 100644 web/src/bones/mint-page.bones.json create mode 100644 web/src/bones/reviews-panel.bones.json create mode 100644 web/src/components/ColorBends.astro create mode 100644 web/src/components/CopyableId.astro create mode 100644 web/src/components/Footer.astro create mode 100644 web/src/components/LanguageSwitcher.astro create mode 100644 web/src/components/LoginDialog.astro create mode 100644 web/src/components/MintCard.astro create mode 100644 web/src/components/MintLive.astro create mode 100644 web/src/components/MintQr.astro create mode 100644 web/src/components/Moai.astro create mode 100644 web/src/components/PulseTicker.astro create mode 100644 web/src/components/Reviews.astro create mode 100644 web/src/components/Sparkline.astro create mode 100644 web/src/components/Topbar.astro create mode 100644 web/src/i18n/GLOSSARY.md create mode 100644 web/src/i18n/client.ts create mode 100644 web/src/i18n/config.ts create mode 100644 web/src/i18n/en.json create mode 100644 web/src/i18n/es.json create mode 100644 web/src/i18n/format.ts create mode 100644 web/src/i18n/index.ts create mode 100644 web/src/i18n/locales.mjs create mode 100644 web/src/i18n/mint.ts create mode 100644 web/src/i18n/nl.json create mode 100644 web/src/i18n/paths.ts create mode 100644 web/src/i18n/routing.ts create mode 100644 web/src/i18n/translate.ts create mode 100644 web/src/layouts/Base.astro create mode 100644 web/src/lib/api.ts create mode 100644 web/src/lib/auth-ui.ts create mode 100644 web/src/lib/client.ts create mode 100644 web/src/lib/feed-client.ts create mode 100644 web/src/lib/format.ts create mode 100644 web/src/lib/mint-state.ts create mode 100644 web/src/lib/modal.ts create mode 100644 web/src/lib/nostr-build.ts create mode 100644 web/src/lib/nostr-signer.ts create mode 100644 web/src/lib/profiles.ts create mode 100644 web/src/lib/qr.ts create mode 100644 web/src/lib/review-cards.ts create mode 100644 web/src/lib/reviews-client.ts create mode 100644 web/src/lib/schema.ts create mode 100644 web/src/lib/seo.ts create mode 100644 web/src/lib/site.ts create mode 100644 web/src/lib/skeleton-fixtures.ts create mode 100644 web/src/lib/skeleton.ts create mode 100644 web/src/pages/[...locale]/404.astro create mode 100644 web/src/pages/[...locale]/about.astro create mode 100644 web/src/pages/[...locale]/disclaimer.astro create mode 100644 web/src/pages/[...locale]/index.astro create mode 100644 web/src/pages/[...locale]/mint/[host].astro create mode 100644 web/src/pages/[...locale]/mints.astro create mode 100644 web/src/pages/[...locale]/privacy.astro create mode 100644 web/src/pages/[...locale]/reviews.astro create mode 100644 web/src/pages/[...locale]/terms.astro create mode 100644 web/src/pages/[...locale]/wallets.astro create mode 100644 web/src/pages/robots.txt.ts create mode 100644 web/src/pages/sitemap.xml.ts create mode 100644 web/src/scripts/color-bends.ts create mode 100644 web/src/scripts/reveal.ts create mode 100644 web/src/styles/global.css create mode 100644 web/tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..08e4928 --- /dev/null +++ b/.env.example @@ -0,0 +1,115 @@ +# cashumints.space configuration. +# +# Copy to .env and edit. Every value below is the built-in default, so an empty .env +# behaves exactly like no .env at all. +# +# cp .env.example .env +# +# One file at the repo root serves all three packages. It is loaded by: +# - api/ via node --env-file-if-exists=../.env (see api/package.json) +# - web/ via web/scripts/load-env.mjs, imported from astro.config.mjs +# +# Real environment variables always win over this file, so a systemd unit or a +# one-off `PORT=9000 pnpm dev:api` still overrides it. .env is gitignored. + +# ─── Ports ─────────────────────────────────────────────────────────────────── +# The two servers `pnpm dev` starts. Change PORT and API_URL together: the web +# build and the dev proxy reach the API at API_URL, so they must agree. + +# API HTTP port. +PORT=8787 + +# Astro dev server / preview port. +WEB_PORT=4321 + +# ─── Web ───────────────────────────────────────────────────────────────────── + +# Where the build and the dev proxy reach the API. A build-machine address; it is +# never written into the markup a visitor downloads. +API_URL=http://127.0.0.1:8787 + +# Browser-facing API origin, baked into the markup and the islands. Empty means +# same origin: icons resolve to /icons and islands fetch /api, which the dev proxy +# and nginx forward to API_URL. Deliberately does NOT fall back to API_URL — a +# visitor's browser cannot reach 127.0.0.1. Set it only when the API answers on its +# own origin, e.g. https://api.cashumints.space +PUBLIC_API_URL= + +# Canonical origin for canonical links, OpenGraph tags and the sitemap. +SITE_URL=https://cashumints.space + +# ─── API storage ───────────────────────────────────────────────────────────── +# Unset means api/data/, resolved from the source tree regardless of where you run +# from. Set these and they are taken as given: a relative path resolves against the +# working directory (api/ under every pnpm script), so prefer absolute paths. In +# production point both at a directory the service user owns. + +# SQLite file. +# DB_PATH=/var/lib/cashumints/cashumints.db + +# Cached mint icons, served at /icons/*. +# ICON_DIR=/var/lib/cashumints/icons + +# ─── Nostr relays ──────────────────────────────────────────────────────────── +# Three comma separated lists, all optional. Each one, unset or empty, falls back +# to the list of the same name in shared/src/nostr.ts; the values below ARE those +# defaults, spelled out so the relays this site talks to are visible in one place. +# Setting one replaces the default pool entirely — it does not add to it. + +# Review and mint-announcement relays: kind 38000 and 38172. Read by the API +# indexer (probe/discovery cycles) and by the build-time "latest reviews" fetch. +# The union of the old site's read pool and its publish pool — the two differed, +# so reviews the old site published to snort/primal were invisible to it. +RELAYS=wss://relay.cashumints.space,wss://nos.lol,wss://relay.azzamo.net,wss://relay.snort.social,wss://relay.primal.net + +# Profile relays for the BUILD (kind 0, prerendered reviewer names on the home +# page). A wider pool than RELAYS on purpose: relay.cashumints.space holds no kind +# 0 at all and snort/primal hold almost none, so the two aggregators below are what +# turn npubs into names. Read the PROFILE_RELAYS comment in shared/src/nostr.ts +# before trimming this — relay.nostr.band was measured and deliberately excluded. +PROFILE_RELAYS=wss://relay.cashumints.space,wss://nos.lol,wss://relay.azzamo.net,wss://relay.snort.social,wss://relay.primal.net,wss://purplepag.es,wss://relay.nostr.net + +# Profile relays for the BROWSER: the same kind 0 lookup, done client side for +# pubkeys the build did not resolve. PUBLIC_ means it is baked into the shipped +# markup and a visitor's browser connects to these directly, so list only relays +# that accept public websocket connections. Normally kept equal to PROFILE_RELAYS. +PUBLIC_PROFILE_RELAYS=wss://relay.cashumints.space,wss://nos.lol,wss://relay.azzamo.net,wss://relay.snort.social,wss://relay.primal.net,wss://purplepag.es,wss://relay.nostr.net + +# Review relays for the BROWSER: where the reviews panel reads from and where a +# newly signed review is published. Defaults to RELAYS above when unset, which is +# what production wants. Point it at a local relay to exercise the publish path in +# development without putting test reviews on the public network. +# PUBLIC_REVIEW_RELAYS= + +# Relays used to reach a NIP-46 remote signer (the "Remote signer" and "Primal" +# login routes). Not review relays: these carry small encrypted RPC between this +# browser and the reader's signer app, so they have to be relays both ends can +# reach. Defaults to relay.nsec.app and relay.primal.net when unset. +# PUBLIC_CONNECT_RELAYS= + +# ─── Indexer timing ────────────────────────────────────────────────────────── +# All are positive integers; anything unparseable falls back to the default. + +# Minutes between probe cycles (every mint's /v1/info). +PROBE_INTERVAL_MIN=10 + +# Minutes between discovery cycles (relay scan for new mints and reviews). +DISCOVERY_INTERVAL_MIN=60 + +# Mints probed in parallel. +PROBE_CONCURRENCY=8 + +# Per-mint request timeout, milliseconds. +PROBE_TIMEOUT_MS=5000 + +# ─── Ranking ───────────────────────────────────────────────────────────────── + +# Bayesian prior mean C. The neutral 3 is intentional — read the "Ranking" section +# of README.md before changing it. `global` uses the observed global mean instead, +# which is the literal BACKEND.md behaviour and ranks noticeably worse. +SCORE_PRIOR_MEAN=3 + +# ─── Tooling ───────────────────────────────────────────────────────────────── + +# Dev server Boneyard captures against (`pnpm bones`). Defaults to WEB_PORT. +# BONES_URL=http://localhost:4321 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5da4566 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.astro/ +api/data/ +*.log +.DS_Store +.env diff --git a/NOTES-PAGES.md b/NOTES-PAGES.md new file mode 100644 index 0000000..818a8b5 --- /dev/null +++ b/NOTES-PAGES.md @@ -0,0 +1,186 @@ +# NOTES-PAGES.md: page audit + +An audit of every internal link in `web/src` against every route `astro build` actually +emits, plus a crawl of the built `dist/` for links that resolve to nothing and anchors +that point at ids no page has. Findings first, then what was built to close them. + +Audit run against 58 indexed mints, API up. Re-run it with: + +```bash +pnpm build && node web/scripts/check-links.mjs web/dist +``` + +`LIST_TARGETS=1` on that command prints every distinct internal target and which pages +link to it. The checker exits non-zero on a problem, so it can gate a deploy. + +## 1. Findings + +Routes are listed once each. "Linked from" is where the link lives in the source, not +every page that renders the component. + +| Route | Linked from | Existed before | Action | +|---|---|---|---| +| `/` | Topbar brand, 404 | yes | none | +| `/mints` | Topbar nav, Footer, home hero + CTA + section links, mint page breadcrumb, 404 | yes | none | +| `/mints?sort=reviews` `?sort=rating` `?sort=recent` | home quick filters | yes (handled by the island) | none | +| `/mints?nut=17` | home quick filter "Supports WebSockets" | route yes, filter **no** | **not fixed**, see below | +| `/mints#mint-search` | Topbar search affordance | id exists, link never renders | **not fixed**, see below | +| `/mint/[host]` | MintCard, home review cards | yes, 58 prerendered | none | +| `/about` | Topbar nav, Footer, home CTA | yes | none | +| `/about#nip87` | home "Signed, not submitted" card | yes | none | +| `/about#nuts` | mint page NUT panel | yes | none | +| `/wallets` | Topbar nav, Footer | yes | none | +| `/#latest-reviews` | Topbar nav "Reviews" | anchor existed | **changed**: nav now points at `/reviews` | +| `/reviews` | nothing linked to it | **no** | **built** | +| `/terms` | nothing linked to it | **no** | **built**, linked from the footer | +| `/privacy` | nothing linked to it | **no** | **built**, linked from the footer | +| `/disclaimer` | nothing linked to it | **no** | **built**, linked from the footer and every mint page | +| `/404` | (served by nginx on a miss) | yes | **reworked**, see below | +| `/sitemap.xml` | nothing | **no** | **built** | +| `/robots.txt` | nothing | **no** | **built** | +| `/api/*`, `/icons/*` | mint icons, islands | not in `dist` by design | none: nginx proxies both to the API | + +The link crawl over the built site found **zero** broken internal links and **zero** +dead anchors, before and after. Nothing pointed at the missing pages, which is exactly +why they were easy to miss: the gap was between the page inventory and the navigation, +not inside the navigation. + +### Findings not closed by this task + +**`/mints?nut=17` does nothing.** The home page offers "Supports WebSockets" as a quick +filter and links to `/mints?nut=17`. The `/mints` island reads `q` and `sort` from the +query string and ignores everything else, so the link lands on an unfiltered list. The +fix is not one line: `MintListItem` (the payload `/api/mints` returns and the only data +`/mints` has) carries no `nuts` array, so `MintCard` has nothing to put in a +`data-nuts` attribute. Either the API adds `nuts` to the list payload, or `/mints` +fetches all 58 mint details at build time the way `/mint/[host]` already does. Worth +doing, out of scope for a page audit. + +**`/mints#mint-search` never renders.** `Topbar` shows a "Search N mints" affordance +only when a `mintCount` prop is passed. Nothing passes a number: the mint page passes +`mintCount={undefined}` explicitly and no other page passes it at all, so the anchor +is never in the emitted HTML. The knock-on is in `Base.astro`: the `/` key handler +falls back to "no search box on this page, so follow the topbar search link", and that +branch is unreachable, so `/` does nothing on the mint pages, `/about`, `/wallets` and +the three new legal pages. Either pass the real count from `/mints` and the mint pages, +or drop the affordance and the fallback branch. + +**Islands and the view transitions router.** `Base.astro` now renders `` +(added by concurrent work on the site, not by this task). Under it, a same-site +navigation swaps the document without re-running page scripts, and no island on the +site re-initialises on `astro:page-load`: `/mints` search and sort, the mint page +reviews panel, the pulse ticker and the new `/reviews` feed all run once, on the first +full page load. Every island needs the same `astro:page-load` treatment the reveal +script in `Base.astro` already has. Flagged rather than fixed here, because it is one +cross-cutting change across every island and it belongs with the router work. + +## 2. What was built, and what was already there + +### Built + +- **`/reviews`** (`web/src/pages/reviews.astro`). The global feed: every mint's reviews + in one list, newest first. + - Build time: the 30 most recent written reviews are read from the relays and + prerendered, so the page has real content for a crawler and for a reader with + JavaScript off. The controls hide themselves through a `