Expand ecash explorer capabilities

Add Fedimint discovery, dual SQLite/Postgres storage, richer review handling, and generated social imagery.
This commit is contained in:
michilis
2026-08-21 02:10:48 +02:00
parent aa1771ea20
commit 6f17b572b1
80 changed files with 7580 additions and 704 deletions
+81 -4
View File
@@ -1,6 +1,24 @@
/** Shapes returned by the API. The web app builds against these. */
export type MintStatus = 'online' | 'degraded' | 'offline' | 'unknown';
/**
* Statuses a listed thing can be in.
*
* `announced` is the Fedimint addition, and it is deliberately not a synonym for
* `unknown`. `unknown` means "we have not checked yet"; `announced` means "there is no
* check we can run" — the federation exists on Nostr, nothing has confirmed it since,
* and nothing here will claim otherwise. A Cashu mint never carries it.
*/
export type MintStatus = 'online' | 'degraded' | 'offline' | 'unknown' | 'announced';
/**
* Which ecosystem a listing belongs to.
*
* Deliberately open rather than a closed union: `mints.type` is a stored TEXT column
* with no CHECK constraint, so an older build reading a database written by a newer one
* has to be able to hold a value it does not have a page for. Compare against
* `ANNOUNCEMENT_KINDS` rather than switching exhaustively on this.
*/
export type MintType = 'cashu' | 'fedimint' | (string & {});
/** One item of `GET /api/mints`. */
export interface MintListItem {
@@ -8,6 +26,8 @@ export interface MintListItem {
host: string;
name: string | null;
icon: string | null;
/** 'cashu' or 'fedimint'. Rows written before the column existed read as 'cashu'. */
type: MintType;
status: MintStatus;
last_online: number | null;
review_count: number;
@@ -25,8 +45,13 @@ export interface ProbeSample {
export type RatingDistribution = Record<'1' | '2' | '3' | '4' | '5', number>;
/** `GET /api/mints/:host`. */
export interface MintDetail extends MintListItem {
/**
* `GET /api/mints/:host`.
*
* The Fedimint keys are optional and absent on a Cashu mint, which is what keeps the
* Cashu payload unchanged. Read them through `FedimintDetail` after checking `type`.
*/
export interface MintDetail extends MintListItem, Partial<FedimintFields> {
description: string | null;
pubkey: string | null;
info: MintInfo | null;
@@ -41,7 +66,49 @@ export interface MintDetail extends MintListItem {
probes_recent: ProbeSample[];
}
/** `GET /api/stats`. */
/**
* What `ecosystem_json` holds for a Fedimint row, and what `GET /api/mints/:host`
* spreads across the detail payload for one.
*
* Flattened rather than nested so a Cashu payload is byte for byte what it always was:
* these keys are simply absent on one. A third ecosystem adds its own interface here
* and its own optional keys below; nothing existing has to move.
*/
export interface FedimintFields {
/** The `d` tag of the announcement: 64 hex characters. `host` is derived from it. */
federation_id: string;
/** Every `u` tag that was a usable invite code, in announcement order. */
invite_codes: string[];
/** The `modules` tag, split: `["ln", "mint", "wallet", "lnv2", "meta"]`. */
modules: string[];
/** The `n` tag, normalized — `bitcoin` and `mainnet` both arrive as `mainnet`. */
network: string | null;
/** `created_at` of the newest announcement seen for this federation. */
announced_at: number | null;
/** Who published that announcement. The `a` tag of a review points back at them. */
announcer_pubkey: string | null;
/**
* Who confirmed the status, when anything did: `"fedimint.observer"` today.
*
* null means nothing has, and then `status` is `announced` and never `online`. The
* page prints this next to the status, because "someone else says it is up" is a
* different claim from "we checked".
*/
status_source: string | null;
}
/** `GET /api/mints/:host` for a Fedimint federation: the detail plus its own fields. */
export type FedimintDetail = MintDetail & FedimintFields;
/**
* `GET /api/stats`.
*
* The four `mints_*` fields count Cashu mints and only Cashu mints, exactly as they did
* before federations were indexed: they are read by the pulse ticker, the /mints
* description and the home page, and a number that silently changed meaning would be
* worse than a new field. `cashu_total` is the same number under a name that says so,
* and every ecosystem added later gets its own `*_total` beside `fedimint_total`.
*/
export interface Stats {
mints_total: number;
mints_online: number;
@@ -50,6 +117,16 @@ export interface Stats {
reviews_total: number;
last_review_at: number | null;
updated_at: number;
/** Same value as `mints_total`, named for the ecosystem it counts. */
cashu_total: number;
fedimint_total: number;
/** Federations a real check confirmed were up. Never inferred from an announcement. */
fedimint_online: number;
fedimint_offline: number;
/** Federations announced on Nostr that no check has ever confirmed. */
fedimint_announced: number;
/** Reviews of federations (`k` = 38173), included in `reviews_total`. */
fedimint_reviews: number;
}
/** `GET /api/health`. */