@tollbooth-dpyc/web 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -4
- package/dist/buildInfo.d.ts +21 -0
- package/dist/buildInfo.js +35 -0
- package/dist/debugLog.js +3 -12
- package/dist/funding.d.ts +73 -0
- package/dist/funding.js +160 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/react/BuildInfoPanel.d.ts +50 -0
- package/dist/react/BuildInfoPanel.js +49 -0
- package/dist/react/CouponsPanel.js +1 -4
- package/dist/react/FundingStatus.d.ts +67 -0
- package/dist/react/FundingStatus.js +120 -0
- package/dist/react/PagedTable.js +1 -4
- package/dist/react/TableFilter.js +1 -4
- package/dist/react/TimezonePicker.d.ts +30 -0
- package/dist/react/TimezonePicker.js +19 -0
- package/dist/react/UsageSummary.d.ts +55 -0
- package/dist/react/UsageSummary.js +53 -0
- package/dist/react/WalletPage.js +1 -4
- package/dist/react/cx.d.ts +2 -0
- package/dist/react/cx.js +5 -0
- package/dist/react/index.d.ts +5 -0
- package/dist/react/index.js +5 -0
- package/dist/react/useTimezone.d.ts +8 -0
- package/dist/react/useTimezone.js +34 -0
- package/dist/timezone.d.ts +82 -0
- package/dist/timezone.js +235 -0
- package/dist/usage.d.ts +34 -0
- package/dist/usage.js +53 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,9 +10,13 @@ features. It is the peer of the `tollbooth-dpyc` Python wheel.
|
|
|
10
10
|
- **MCP client** — one shared connection to the operator, typed wrappers for
|
|
11
11
|
the standard tools (balance, top-up, statement, price, profile)
|
|
12
12
|
- **React components** — `NpubGate`, `NostrProfilePanel`, `SessionKeyClaim`, `WalletCard`,
|
|
13
|
-
`WalletPage`, `CouponsPanel`, `
|
|
14
|
-
`
|
|
15
|
-
`
|
|
13
|
+
`WalletPage`, `CouponsPanel`, `UsageSummary`, `PatronFundingStatus` /
|
|
14
|
+
`OperatorFundingStatus`, `BuildInfoPanel`, `TimezonePicker`, `TableFilter`,
|
|
15
|
+
`SortHeader` / `PageControls` / `TableShell`, `ThemeToggle`, `ErrorBoundary`,
|
|
16
|
+
`AvatarPicker`, `DebugPanel`, `QuoteScroller`, `useSession`, `useTopUp`,
|
|
17
|
+
`useTheme`, `useTimezone`, `useDebugLog`
|
|
18
|
+
- **Time zone** — the patron's display zone and the conversions every clock
|
|
19
|
+
needs (render, datetime-local, day-filter bounds), DST-correct
|
|
16
20
|
- **Mechanics, not looks** — the newer components take `classNames` per part
|
|
17
21
|
and add no typography or colour of their own; actions are chips
|
|
18
22
|
- **Debug log** — every call, result and error from `callTool`, scrubbed of
|
|
@@ -113,6 +117,63 @@ wrapper takes the same optional `CallOptions` as `callTool`, last
|
|
|
113
117
|
`useTopUp` is the wallet's top-up on its own (purchase_credits → invoice →
|
|
114
118
|
check_payment, polled while the tab is visible) for a site drawing its own.
|
|
115
119
|
|
|
120
|
+
### Time zone
|
|
121
|
+
|
|
122
|
+
The patron picks a display zone once — "auto" (the browser's) or an IANA name,
|
|
123
|
+
never a fixed offset — and every clock follows. It is kept under
|
|
124
|
+
`<prefix>:timezone`; `useTimezone()` re-renders on any change, from this page
|
|
125
|
+
or another tab. Storage stays UTC; convert at the edges:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { datetimeLocalValueToIso, formatDateTime, isoToDatetimeLocalValue, localDateFilterBounds } from "@tollbooth-dpyc/web";
|
|
129
|
+
import { TimezonePicker, useTimezone } from "@tollbooth-dpyc/web/react";
|
|
130
|
+
|
|
131
|
+
const [, zone] = useTimezone();
|
|
132
|
+
formatDateTime(post.sent_at, zone); // render
|
|
133
|
+
isoToDatetimeLocalValue(post.publish_at, zone); // → <input type="datetime-local">
|
|
134
|
+
datetimeLocalValueToIso(value, zone); // ← back to UTC ISO
|
|
135
|
+
localDateFilterBounds("2026-03-08", "2026-03-08", zone); // the patron's midnights, as instants
|
|
136
|
+
|
|
137
|
+
<TimezonePicker label="Display time zone" classNames={{ select: "field" }} />
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Around a DST change a missing wall time (02:30 on a spring-forward morning)
|
|
141
|
+
moves an hour on and a doubled one (01:30 on a fall-back night) is the
|
|
142
|
+
earlier; day bounds give the 23- and 25-hour days their true length.
|
|
143
|
+
`displayTimeZone()` is the zone for code outside React.
|
|
144
|
+
|
|
145
|
+
### Usage, funding, build
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { BuildInfoPanel, OperatorFundingStatus, PatronFundingStatus, UsageSummary } from "@tollbooth-dpyc/web/react";
|
|
149
|
+
|
|
150
|
+
<UsageSummary days={30} figures={["balance", "deposited", "consumed"]} classNames={{ root: "card p-5", figures: "grid grid-cols-3" }} />
|
|
151
|
+
|
|
152
|
+
<PatronFundingStatus thresholds={{ low: 100 }} siteRows={xConnectionRows} classNames={{ root: "card p-5", ok: "text-green-600" }} />
|
|
153
|
+
<OperatorFundingStatus authorityThresholds={{ low: 500 }} />
|
|
154
|
+
|
|
155
|
+
<BuildInfoPanel
|
|
156
|
+
status={status}
|
|
157
|
+
frontend={{ version: __APP_VERSION__, commit: __BUILD_COMMIT__, builtAt: __BUILD_TIME__, source: "https://github.com/you/site" }}
|
|
158
|
+
intro={<p>…the site's own words…</p>}
|
|
159
|
+
/>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `UsageSummary` reads `account_statement`: balance and lifetime totals, the
|
|
163
|
+
period's spend, calls and top-ups (from its daily log), and its top tools.
|
|
164
|
+
`figures` picks the numbers, `renderRow` draws a tool row.
|
|
165
|
+
- `PatronFundingStatus`: sign-in proof and credit balance, with tranche expiry;
|
|
166
|
+
`OperatorFundingStatus`: credentials still to deliver, the Authority balance,
|
|
167
|
+
the database and background jobs. Every row is read fresh and stamped with
|
|
168
|
+
when, in the patron's zone. **The operator panel renders nothing — and calls
|
|
169
|
+
nothing operator-only — unless the signed-in npub equals the `operator_npub`
|
|
170
|
+
that `session_status` reports.** That is display only; the server enforces
|
|
171
|
+
its own ACL. `siteRows` adds a site's own checks.
|
|
172
|
+
- `BuildInfoPanel`: the front end's version (yours, passed in), the MCP
|
|
173
|
+
server's version, wheel and commit linked to its repository (from
|
|
174
|
+
`service_status`; only https links), the Tollbooth-DPYC™ links, the licence
|
|
175
|
+
and the patent notice. `children` adds rows after the MCP server section.
|
|
176
|
+
|
|
116
177
|
### Debug panel
|
|
117
178
|
|
|
118
179
|
`callTool` logs to a shared, scrubbed ring buffer; `DebugPanel` shows it as a
|
|
@@ -136,7 +197,7 @@ configureDebugLog({ persist: true }); // optional: outlive a reload; max defau
|
|
|
136
197
|
```
|
|
137
198
|
|
|
138
199
|
A site's own `debugPush` lines are scrubbed the same way. Stamps follow the
|
|
139
|
-
|
|
200
|
+
patron's display time zone (see Time zone).
|
|
140
201
|
|
|
141
202
|
### Quote scroller
|
|
142
203
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `service_status` says about the running build: the server's version,
|
|
3
|
+
* the tollbooth-dpyc wheel under it, and the commit it was deployed from,
|
|
4
|
+
* linked to its repository. Framework-free, so it is tested without React.
|
|
5
|
+
*
|
|
6
|
+
* Hrefs come from the server, so only an https URL becomes a link; anything
|
|
7
|
+
* else is shown as text.
|
|
8
|
+
*/
|
|
9
|
+
import type { ServiceStatus } from "./standardTools.ts";
|
|
10
|
+
export interface BuildFacts {
|
|
11
|
+
version: string | null;
|
|
12
|
+
sdkVersion: string | null;
|
|
13
|
+
/** The deployed commit, short (7 characters). */
|
|
14
|
+
commit: string | null;
|
|
15
|
+
/** The commit on its host, when the repository is on GitHub. */
|
|
16
|
+
commitHref: string | null;
|
|
17
|
+
/** The repository without its scheme, e.g. "github.com/lonniev/excalibur-mcp". */
|
|
18
|
+
repo: string | null;
|
|
19
|
+
repoHref: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function buildFacts(status: ServiceStatus | null | undefined): BuildFacts;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `service_status` says about the running build: the server's version,
|
|
3
|
+
* the tollbooth-dpyc wheel under it, and the commit it was deployed from,
|
|
4
|
+
* linked to its repository. Framework-free, so it is tested without React.
|
|
5
|
+
*
|
|
6
|
+
* Hrefs come from the server, so only an https URL becomes a link; anything
|
|
7
|
+
* else is shown as text.
|
|
8
|
+
*/
|
|
9
|
+
function httpsUrl(raw) {
|
|
10
|
+
if (!raw)
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
const u = new URL(raw);
|
|
14
|
+
return u.protocol === "https:" ? u : null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function buildFacts(status) {
|
|
21
|
+
const info = status?.build_info ?? {};
|
|
22
|
+
const sha = /^[0-9a-f]{7,40}$/i.test(info.fastmcp_cloud_git_commit_sha ?? "")
|
|
23
|
+
? info.fastmcp_cloud_git_commit_sha
|
|
24
|
+
: null;
|
|
25
|
+
const repoUrl = httpsUrl(info.fastmcp_cloud_git_repo);
|
|
26
|
+
const repoHref = repoUrl ? repoUrl.href.replace(/\/+$/, "") : null;
|
|
27
|
+
return {
|
|
28
|
+
version: status?.version || null,
|
|
29
|
+
sdkVersion: status?.tollbooth_dpyc_version || null,
|
|
30
|
+
commit: sha ? sha.slice(0, 7) : null,
|
|
31
|
+
commitHref: sha && repoUrl?.hostname === "github.com" ? `${repoHref}/commit/${sha}` : null,
|
|
32
|
+
repo: repoHref ? repoHref.replace(/^https:\/\//, "") : (info.fastmcp_cloud_git_repo || null),
|
|
33
|
+
repoHref,
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/debugLog.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* optionality, roastify, taxsort). None of them scrubbed; this one does.
|
|
16
16
|
*/
|
|
17
17
|
import { tollboothConfig } from "./config.js";
|
|
18
|
+
import { displayTimeZone } from "./timezone.js";
|
|
18
19
|
const DEFAULT_MAX = 200;
|
|
19
20
|
let max = DEFAULT_MAX;
|
|
20
21
|
let persist = false;
|
|
@@ -108,19 +109,9 @@ function hydrate() {
|
|
|
108
109
|
return [];
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
/**
|
|
112
|
-
* The patron's display zone, if the site stores one under `<prefix>:timezone`
|
|
113
|
-
* ("auto" or absent means the browser's own), so stamps match the rest of the UI.
|
|
114
|
-
*/
|
|
112
|
+
/** In the patron's display zone (`timezone.ts`), so stamps match the rest of the UI. */
|
|
115
113
|
function stamp(now) {
|
|
116
|
-
|
|
117
|
-
const pref = readLocal("timezone");
|
|
118
|
-
const zone = pref && pref !== "auto" ? pref : undefined;
|
|
119
|
-
return now.toLocaleTimeString(undefined, zone ? { timeZone: zone } : undefined);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return now.toLocaleTimeString();
|
|
123
|
-
}
|
|
114
|
+
return now.toLocaleTimeString(undefined, { timeZone: displayTimeZone() });
|
|
124
115
|
}
|
|
125
116
|
function emit() {
|
|
126
117
|
save();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funding and credential health as uniform rows, for a patron and for the
|
|
3
|
+
* operator. Framework-free, so every rule is tested without React or a server.
|
|
4
|
+
*
|
|
5
|
+
* Lifted from eXcalibur's `lib/fundingStatus.ts`, keeping its three rules:
|
|
6
|
+
* 1. No prices — balances and expiries only.
|
|
7
|
+
* 2. No warning outlives its cause — every row is derived fresh from the
|
|
8
|
+
* latest tool answers, and carries the time it was checked.
|
|
9
|
+
* 3. Compose, don't invent — every input is a field a standard tool already
|
|
10
|
+
* returns (check_balance, check_proof_status, session_status,
|
|
11
|
+
* service_status, get_operator_onboarding_status, check_authority_balance).
|
|
12
|
+
*
|
|
13
|
+
* Three states: ok (working now), warning (will stop on a known date, or
|
|
14
|
+
* incomplete but not fatal), blocked (stopping work now).
|
|
15
|
+
*/
|
|
16
|
+
import type { CheckBalanceResult, OperatorOnboardingStatus, ServiceStatus, SessionStatusResult } from "./standardTools.ts";
|
|
17
|
+
export type StatusLevel = "ok" | "warning" | "blocked";
|
|
18
|
+
export interface StatusRow {
|
|
19
|
+
/** Stable key for lists and tests. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** What the row is about: "Sign-in proof", "Authority balance", … */
|
|
22
|
+
dependency: string;
|
|
23
|
+
state: StatusLevel;
|
|
24
|
+
/** One line; never a price. */
|
|
25
|
+
detail: string;
|
|
26
|
+
/** ISO time of the read that produced the row. */
|
|
27
|
+
checked_at: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* When a balance reads as trouble: at or below `empty` it is blocked, at or
|
|
31
|
+
* below `low` a warning. Default both 0 — only an empty balance is flagged.
|
|
32
|
+
*/
|
|
33
|
+
export interface BalanceThresholds {
|
|
34
|
+
low?: number;
|
|
35
|
+
empty?: number;
|
|
36
|
+
}
|
|
37
|
+
/** How this tab proves who it is. */
|
|
38
|
+
export type ProofKind = {
|
|
39
|
+
kind: "session_nsec";
|
|
40
|
+
} | {
|
|
41
|
+
kind: "dm_proof";
|
|
42
|
+
status: "valid" | "expired" | "unknown";
|
|
43
|
+
expiresInSec?: number | null;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "none";
|
|
46
|
+
};
|
|
47
|
+
/** A tranche or proof ending within this window is a warning. */
|
|
48
|
+
export declare const WARNING_WITHIN_MS: number;
|
|
49
|
+
/** The worst state across rows — for a heading's summary mark. */
|
|
50
|
+
export declare function worstState(rows: readonly StatusRow[]): StatusLevel;
|
|
51
|
+
/** "3d", "5h", "12m", "now". */
|
|
52
|
+
export declare function durationText(sec: number): string;
|
|
53
|
+
/** The sign-in proof: a lapsed one drops the patron mid-session. */
|
|
54
|
+
export declare function composeProofRow(proof: ProofKind, checkedAt: string): StatusRow;
|
|
55
|
+
/** The patron's balance and the expiry of its tranches — unused credits evaporate. */
|
|
56
|
+
export declare function composeCreditsRow(bal: CheckBalanceResult, checkedAt: string, thresholds?: BalanceThresholds, nowMs?: number): StatusRow;
|
|
57
|
+
/**
|
|
58
|
+
* Is the signed-in npub this operator? The rule every copy used: the patron's
|
|
59
|
+
* npub must equal the operator npub the server reports (`session_status`).
|
|
60
|
+
* It only decides what a page shows — the server still enforces its own ACL.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isOperator(npub: string, status: Pick<SessionStatusResult, "operator_npub"> | null | undefined): boolean;
|
|
63
|
+
/** The credentials the operator still has to deliver by Secure Courier. */
|
|
64
|
+
export declare function composeOnboardingRow(onb: OperatorOnboardingStatus, checkedAt: string): StatusRow;
|
|
65
|
+
/** The operator's balance at its Authority — what certifies patron top-ups. */
|
|
66
|
+
export declare function composeAuthorityRow(bal: CheckBalanceResult, checkedAt: string, thresholds?: BalanceThresholds): StatusRow;
|
|
67
|
+
/** The operator's database, from `session_status`'s lifecycle and the vault. */
|
|
68
|
+
export declare function composePersistenceRow(life: SessionStatusResult, svc: ServiceStatus, onb: OperatorOnboardingStatus, checkedAt: string): StatusRow;
|
|
69
|
+
/**
|
|
70
|
+
* Background-job dispatch, for an operator that runs any: null when
|
|
71
|
+
* `service_status` carries no job diagnostics (the server runs none).
|
|
72
|
+
*/
|
|
73
|
+
export declare function composeDurableJobsRow(svc: ServiceStatus, checkedAt: string): StatusRow | null;
|
package/dist/funding.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funding and credential health as uniform rows, for a patron and for the
|
|
3
|
+
* operator. Framework-free, so every rule is tested without React or a server.
|
|
4
|
+
*
|
|
5
|
+
* Lifted from eXcalibur's `lib/fundingStatus.ts`, keeping its three rules:
|
|
6
|
+
* 1. No prices — balances and expiries only.
|
|
7
|
+
* 2. No warning outlives its cause — every row is derived fresh from the
|
|
8
|
+
* latest tool answers, and carries the time it was checked.
|
|
9
|
+
* 3. Compose, don't invent — every input is a field a standard tool already
|
|
10
|
+
* returns (check_balance, check_proof_status, session_status,
|
|
11
|
+
* service_status, get_operator_onboarding_status, check_authority_balance).
|
|
12
|
+
*
|
|
13
|
+
* Three states: ok (working now), warning (will stop on a known date, or
|
|
14
|
+
* incomplete but not fatal), blocked (stopping work now).
|
|
15
|
+
*/
|
|
16
|
+
/** A tranche or proof ending within this window is a warning. */
|
|
17
|
+
export const WARNING_WITHIN_MS = 48 * 60 * 60 * 1000;
|
|
18
|
+
const RANK = { ok: 0, warning: 1, blocked: 2 };
|
|
19
|
+
/** The worst state across rows — for a heading's summary mark. */
|
|
20
|
+
export function worstState(rows) {
|
|
21
|
+
return rows.reduce((w, r) => (RANK[r.state] > RANK[w] ? r.state : w), "ok");
|
|
22
|
+
}
|
|
23
|
+
/** "3d", "5h", "12m", "now". */
|
|
24
|
+
export function durationText(sec) {
|
|
25
|
+
if (sec <= 0)
|
|
26
|
+
return "now";
|
|
27
|
+
const h = Math.floor(sec / 3600);
|
|
28
|
+
if (h >= 48)
|
|
29
|
+
return `${Math.floor(h / 24)}d`;
|
|
30
|
+
if (h >= 1)
|
|
31
|
+
return `${h}h`;
|
|
32
|
+
return `${Math.max(1, Math.floor(sec / 60))}m`;
|
|
33
|
+
}
|
|
34
|
+
function row(id, dependency, state, detail, checked_at) {
|
|
35
|
+
return { id, dependency, state, detail, checked_at };
|
|
36
|
+
}
|
|
37
|
+
function sats(n) {
|
|
38
|
+
return `${n.toLocaleString()} sats`;
|
|
39
|
+
}
|
|
40
|
+
// ── Patron ──────────────────────────────────────────────────────────────────
|
|
41
|
+
/** The sign-in proof: a lapsed one drops the patron mid-session. */
|
|
42
|
+
export function composeProofRow(proof, checkedAt) {
|
|
43
|
+
const r = (state, detail) => row("npub-proof", "Sign-in proof", state, detail, checkedAt);
|
|
44
|
+
if (proof.kind === "session_nsec")
|
|
45
|
+
return r("ok", "This tab's session key signs a fresh proof on every call — no expiry.");
|
|
46
|
+
if (proof.kind === "none")
|
|
47
|
+
return r("blocked", "No proof on hand — sign in again.");
|
|
48
|
+
if (proof.status === "expired")
|
|
49
|
+
return r("blocked", "Proof has expired — sign in again to resume paid calls.");
|
|
50
|
+
if (proof.status === "unknown")
|
|
51
|
+
return r("warning", "No server-side proof record — the next paid call may ask you to sign in again.");
|
|
52
|
+
const sec = proof.expiresInSec;
|
|
53
|
+
if (sec != null && sec * 1000 < WARNING_WITHIN_MS)
|
|
54
|
+
return r("warning", `Proof valid — expires in about ${durationText(sec)}.`);
|
|
55
|
+
return r("ok", sec != null && sec > 0 ? `Proof valid — expires in about ${durationText(sec)}.` : "Proof valid.");
|
|
56
|
+
}
|
|
57
|
+
/** The patron's balance and the expiry of its tranches — unused credits evaporate. */
|
|
58
|
+
export function composeCreditsRow(bal, checkedAt, thresholds = {}, nowMs = Date.now()) {
|
|
59
|
+
const r = (state, detail) => row("credits", "Credit balance", state, detail, checkedAt);
|
|
60
|
+
if (bal.vault_unavailable)
|
|
61
|
+
return r("warning", bal.warning || "The ledger is unavailable — the balance may be stale. Retry shortly.");
|
|
62
|
+
if (bal.error)
|
|
63
|
+
return r("warning", bal.error);
|
|
64
|
+
const { low = 0, empty = 0 } = thresholds;
|
|
65
|
+
const balance = bal.balance_api_sats ?? 0;
|
|
66
|
+
if (balance <= empty)
|
|
67
|
+
return r("blocked", `${balance > 0 ? `Only ${sats(balance)} left` : "Balance is empty"} — top up before paid calls will succeed.`);
|
|
68
|
+
const next = Date.parse(bal.next_expiration_iso ?? "");
|
|
69
|
+
const expiresInMs = Number.isNaN(next) ? null : next - nowMs;
|
|
70
|
+
if (expiresInMs != null && expiresInMs <= 0)
|
|
71
|
+
return r("warning", `${sats(balance)} on hand — a tranche is at or past expiry.`);
|
|
72
|
+
const expiring = bal.expiring_within_24h_sats ?? 0;
|
|
73
|
+
if ((expiresInMs != null && expiresInMs < WARNING_WITHIN_MS) || expiring > 0) {
|
|
74
|
+
const when = expiresInMs != null ? `next expiry in about ${durationText(Math.floor(expiresInMs / 1000))}` : "tranches expire within 24h";
|
|
75
|
+
const howMuch = expiring > 0 ? `${sats(expiring)} expiring; ` : "";
|
|
76
|
+
return r("warning", `${sats(balance)} on hand — ${howMuch}${when}. Unused credits evaporate.`);
|
|
77
|
+
}
|
|
78
|
+
if (balance <= low)
|
|
79
|
+
return r("warning", `${sats(balance)} on hand — running low.`);
|
|
80
|
+
const tranches = bal.active_tranches ?? 0;
|
|
81
|
+
return r("ok", `${sats(balance)} · ${tranches} active tranche${tranches === 1 ? "" : "s"}.`);
|
|
82
|
+
}
|
|
83
|
+
// ── Operator ────────────────────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Is the signed-in npub this operator? The rule every copy used: the patron's
|
|
86
|
+
* npub must equal the operator npub the server reports (`session_status`).
|
|
87
|
+
* It only decides what a page shows — the server still enforces its own ACL.
|
|
88
|
+
*/
|
|
89
|
+
export function isOperator(npub, status) {
|
|
90
|
+
return !!npub && npub === status?.operator_npub;
|
|
91
|
+
}
|
|
92
|
+
/** The credentials the operator still has to deliver by Secure Courier. */
|
|
93
|
+
export function composeOnboardingRow(onb, checkedAt) {
|
|
94
|
+
const r = (state, detail) => row("credentials", "Operator credentials", state, detail, checkedAt);
|
|
95
|
+
if (onb.vault_situation)
|
|
96
|
+
return r("warning", onb.vault_situation);
|
|
97
|
+
if (onb.error)
|
|
98
|
+
return r("warning", onb.error);
|
|
99
|
+
const missing = (onb.missing ?? []).map((m) => m.field);
|
|
100
|
+
if (missing.length)
|
|
101
|
+
return r("blocked", `Missing via Secure Courier: ${missing.join(", ")}.`);
|
|
102
|
+
const optional = (onb.optional_missing ?? []).map((m) => m.field);
|
|
103
|
+
if (optional.length)
|
|
104
|
+
return r("warning", `Optional, not delivered: ${optional.join(", ")}.`);
|
|
105
|
+
if (onb.ready)
|
|
106
|
+
return r("ok", "Every required credential is delivered.");
|
|
107
|
+
return r("warning", onb.summary || "Couldn't tell which credentials are delivered.");
|
|
108
|
+
}
|
|
109
|
+
/** The operator's balance at its Authority — what certifies patron top-ups. */
|
|
110
|
+
export function composeAuthorityRow(bal, checkedAt, thresholds = {}) {
|
|
111
|
+
const r = (state, detail) => row("authority-balance", "Authority balance", state, detail, checkedAt);
|
|
112
|
+
if (bal.success === false || bal.error)
|
|
113
|
+
return r("warning", bal.error || "Authority balance check failed.");
|
|
114
|
+
const balance = bal.balance_api_sats;
|
|
115
|
+
if (balance == null)
|
|
116
|
+
return r("warning", "The Authority did not return a balance.");
|
|
117
|
+
const { low = 0, empty = 0 } = thresholds;
|
|
118
|
+
if (balance <= empty)
|
|
119
|
+
return r("blocked", "Tax balance is empty — patron top-ups cannot be certified until you fund the Authority.");
|
|
120
|
+
if (balance <= low)
|
|
121
|
+
return r("warning", `${sats(balance)} left to certify patron purchases — running low.`);
|
|
122
|
+
return r("ok", `${sats(balance)} available to certify patron purchases.`);
|
|
123
|
+
}
|
|
124
|
+
/** The operator's database, from `session_status`'s lifecycle and the vault. */
|
|
125
|
+
export function composePersistenceRow(life, svc, onb, checkedAt) {
|
|
126
|
+
const r = (state, detail) => row("persistence", "Neon persistence", state, detail, checkedAt);
|
|
127
|
+
const lc = life.lifecycle;
|
|
128
|
+
if (lc === "quota_exceeded")
|
|
129
|
+
return r("blocked", life.message || "Database quota exceeded — paid tools are locked until capacity is restored.");
|
|
130
|
+
if (lc === "misconfigured")
|
|
131
|
+
return r("blocked", life.message || "Persistence misconfigured — repair the database before paid tools will work.");
|
|
132
|
+
if (lc === "warming_up" || lc === "not_registered" || lc === "no_identity")
|
|
133
|
+
return r("warning", life.message || `Operator lifecycle: ${lc}.`);
|
|
134
|
+
if (svc.vault_configured === false || onb.vault_ok === false)
|
|
135
|
+
return r("blocked", "The vault is not configured — the operator cannot serve paid calls.");
|
|
136
|
+
return r("ok", lc ? `Lifecycle ${lc} — vault reachable.` : "Vault reachable.");
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Background-job dispatch, for an operator that runs any: null when
|
|
140
|
+
* `service_status` carries no job diagnostics (the server runs none).
|
|
141
|
+
*/
|
|
142
|
+
export function composeDurableJobsRow(svc, checkedAt) {
|
|
143
|
+
const dj = svc.durable_jobs;
|
|
144
|
+
const aj = svc.async_jobs;
|
|
145
|
+
if (!dj && !aj)
|
|
146
|
+
return null;
|
|
147
|
+
const r = (state, detail) => row("durable-jobs", "Durable job dispatch", state, detail, checkedAt);
|
|
148
|
+
if (dj?.detached_executor_error)
|
|
149
|
+
return r("blocked", `Detached executor error: ${dj.detached_executor_error}`);
|
|
150
|
+
const active = !!dj?.detached_executor_active;
|
|
151
|
+
const resolved = dj?.detached_executor_resolved !== false;
|
|
152
|
+
const durable = aj?.durable_across_recycles;
|
|
153
|
+
if (active && resolved && durable !== false)
|
|
154
|
+
return r("ok", `Detached executor active${aj?.backend ? ` · docket ${aj.backend}` : ""}.`);
|
|
155
|
+
if (active && durable === false)
|
|
156
|
+
return r("warning", "The executor is up, but the job backend does not survive a recycle — long jobs may be lost.");
|
|
157
|
+
if (!active)
|
|
158
|
+
return r("warning", "In-process jobs only — a long job may be cut off by a recycle.");
|
|
159
|
+
return r("warning", "Detached executor present but not fully resolved.");
|
|
160
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export { isTransportFailure } from "./networkError.ts";
|
|
|
15
15
|
export * from "./wallet.ts";
|
|
16
16
|
export * from "./table.ts";
|
|
17
17
|
export * from "./theme.ts";
|
|
18
|
+
export * from "./timezone.ts";
|
|
19
|
+
export { usageFacts, type ToolSpend, type UsageFacts } from "./usage.ts";
|
|
20
|
+
export { buildFacts, type BuildFacts } from "./buildInfo.ts";
|
|
21
|
+
export * from "./funding.ts";
|
|
18
22
|
export { errorReport } from "./errorReport.ts";
|
|
19
23
|
export { loadQuotes, peekQuotes, shuffle, validQuotes, type Quote } from "./quotes.ts";
|
|
20
24
|
export { captureGlobalErrors, clearDebug, configureDebugLog, debugEntries, debugLogText, debugPush, debugSeverity, onDebug, redact, type DebugEntry, type DebugLogOptions, type DebugSeverity, } from "./debugLog.ts";
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,10 @@ export { isTransportFailure } from "./networkError.js";
|
|
|
14
14
|
export * from "./wallet.js";
|
|
15
15
|
export * from "./table.js";
|
|
16
16
|
export * from "./theme.js";
|
|
17
|
+
export * from "./timezone.js";
|
|
18
|
+
export { usageFacts } from "./usage.js";
|
|
19
|
+
export { buildFacts } from "./buildInfo.js";
|
|
20
|
+
export * from "./funding.js";
|
|
17
21
|
export { errorReport } from "./errorReport.js";
|
|
18
22
|
export { loadQuotes, peekQuotes, shuffle, validQuotes } from "./quotes.js";
|
|
19
23
|
export { captureGlobalErrors, clearDebug, configureDebugLog, debugEntries, debugLogText, debugPush, debugSeverity, onDebug, redact, } from "./debugLog.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build & license: what is running and under what terms — the front end's
|
|
3
|
+
* version and commit (the site's, passed in, since only its bundler knows
|
|
4
|
+
* them), the MCP server's version, wheel and deployed commit linked to its
|
|
5
|
+
* repository (from `service_status`), the Tollbooth-DPYC™ links, the licence,
|
|
6
|
+
* and the patent and trademark notice.
|
|
7
|
+
*
|
|
8
|
+
* Mechanics only: every visual choice is the site's, through `classNames`;
|
|
9
|
+
* `intro` carries the site's own words and `children` any rows of its own,
|
|
10
|
+
* drawn after the MCP server section. Pass `status` when the site already
|
|
11
|
+
* holds a `service_status` answer; without it the panel asks once.
|
|
12
|
+
*/
|
|
13
|
+
import { type ReactNode } from "react";
|
|
14
|
+
import { type ServiceStatus } from "../standardTools.ts";
|
|
15
|
+
export interface BuildInfoFrontend {
|
|
16
|
+
version?: string;
|
|
17
|
+
commit?: string;
|
|
18
|
+
builtAt?: string;
|
|
19
|
+
/** The front end's repository, an https URL. */
|
|
20
|
+
source?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface BuildInfoLicence {
|
|
23
|
+
name: string;
|
|
24
|
+
href?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface BuildInfoPanelClassNames {
|
|
27
|
+
root?: string;
|
|
28
|
+
heading?: string;
|
|
29
|
+
intro?: string;
|
|
30
|
+
/** Each section's label: Frontend, MCP server, … */
|
|
31
|
+
section?: string;
|
|
32
|
+
row?: string;
|
|
33
|
+
label?: string;
|
|
34
|
+
value?: string;
|
|
35
|
+
/** Added to a value that is a link. */
|
|
36
|
+
link?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface BuildInfoPanelProps {
|
|
39
|
+
/** A `service_status` answer the site already has; omitted, the panel fetches one. */
|
|
40
|
+
status?: ServiceStatus | null;
|
|
41
|
+
frontend?: BuildInfoFrontend;
|
|
42
|
+
/** Default "Build & license". */
|
|
43
|
+
heading?: ReactNode;
|
|
44
|
+
intro?: ReactNode;
|
|
45
|
+
/** Default Apache 2.0, the licence of every DPYC repository. */
|
|
46
|
+
licence?: BuildInfoLicence;
|
|
47
|
+
children?: ReactNode;
|
|
48
|
+
classNames?: BuildInfoPanelClassNames;
|
|
49
|
+
}
|
|
50
|
+
export default function BuildInfoPanel({ status, frontend, heading, intro, licence, children, classNames: c, }: BuildInfoPanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Build & license: what is running and under what terms — the front end's
|
|
4
|
+
* version and commit (the site's, passed in, since only its bundler knows
|
|
5
|
+
* them), the MCP server's version, wheel and deployed commit linked to its
|
|
6
|
+
* repository (from `service_status`), the Tollbooth-DPYC™ links, the licence,
|
|
7
|
+
* and the patent and trademark notice.
|
|
8
|
+
*
|
|
9
|
+
* Mechanics only: every visual choice is the site's, through `classNames`;
|
|
10
|
+
* `intro` carries the site's own words and `children` any rows of its own,
|
|
11
|
+
* drawn after the MCP server section. Pass `status` when the site already
|
|
12
|
+
* holds a `service_status` answer; without it the panel asks once.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { buildFacts } from "../buildInfo.js";
|
|
16
|
+
import { serviceStatus } from "../standardTools.js";
|
|
17
|
+
import { cx } from "./cx.js";
|
|
18
|
+
const APACHE = { name: "Apache 2.0", href: "https://www.apache.org/licenses/LICENSE-2.0" };
|
|
19
|
+
function bare(href) {
|
|
20
|
+
return href.replace(/^https:\/\//, "").replace(/\/+$/, "");
|
|
21
|
+
}
|
|
22
|
+
function isHttps(href) {
|
|
23
|
+
return !!href && href.startsWith("https://");
|
|
24
|
+
}
|
|
25
|
+
function Section({ c, children }) {
|
|
26
|
+
return _jsx("div", { className: c.section, children: children });
|
|
27
|
+
}
|
|
28
|
+
function Row({ c, label, value, href }) {
|
|
29
|
+
return (_jsxs("div", { className: c.row, children: [_jsx("span", { className: c.label, children: label }), " ", href ? (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: cx(c.value, c.link), children: value })) : (_jsx("span", { className: c.value, children: value }))] }));
|
|
30
|
+
}
|
|
31
|
+
export default function BuildInfoPanel({ status, frontend, heading = "Build & license", intro, licence = APACHE, children, classNames: c = {}, }) {
|
|
32
|
+
const [fetched, setFetched] = useState(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (status !== undefined)
|
|
35
|
+
return;
|
|
36
|
+
let live = true;
|
|
37
|
+
serviceStatus()
|
|
38
|
+
.then((s) => live && setFetched(s))
|
|
39
|
+
.catch(() => {
|
|
40
|
+
/* the rows read "—" */
|
|
41
|
+
});
|
|
42
|
+
return () => {
|
|
43
|
+
live = false;
|
|
44
|
+
};
|
|
45
|
+
}, [status]);
|
|
46
|
+
const f = buildFacts(status === undefined ? fetched : status);
|
|
47
|
+
const feVersion = [frontend?.version, frontend?.commit].filter(Boolean).join(" · ");
|
|
48
|
+
return (_jsxs("section", { className: c.root, children: [heading && _jsx("div", { className: c.heading, children: heading }), intro && _jsx("div", { className: c.intro, children: intro }), frontend && (_jsxs(_Fragment, { children: [_jsx(Section, { c: c, children: "Frontend" }), _jsx(Row, { c: c, label: "Version", value: feVersion || "—" }), frontend.builtAt && _jsx(Row, { c: c, label: "Built at", value: frontend.builtAt }), isHttps(frontend.source) && _jsx(Row, { c: c, label: "Source", value: bare(frontend.source), href: frontend.source })] })), _jsx(Section, { c: c, children: "MCP server" }), _jsx(Row, { c: c, label: "Version", value: f.version ?? "—" }), _jsx(Row, { c: c, label: "Commit", value: f.commit ?? "—", href: f.commitHref }), _jsx(Row, { c: c, label: "tollbooth-dpyc", value: f.sdkVersion ? `wheel ${f.sdkVersion}` : "—" }), f.repo && _jsx(Row, { c: c, label: "Source", value: f.repo, href: f.repoHref }), children, _jsx(Section, { c: c, children: "Tollbooth-DPYC\u2122" }), _jsx(Row, { c: c, label: "Marketing", value: "tollbooth-dpyc.com", href: "https://tollbooth-dpyc.com" }), _jsx(Row, { c: c, label: "Community", value: "github.com/lonniev/dpyc-community", href: "https://github.com/lonniev/dpyc-community" }), _jsx(Row, { c: c, label: "Wheel source", value: "github.com/lonniev/tollbooth-dpyc", href: "https://github.com/lonniev/tollbooth-dpyc" }), _jsx(Section, { c: c, children: "License" }), _jsx(Row, { c: c, label: licence.name, value: licence.href ? bare(licence.href) : licence.name, href: isHttps(licence.href) ? licence.href : null }), _jsx(Section, { c: c, children: "Patent & trademarks" }), _jsx(Row, { c: c, label: "Patent", value: "Patent Pending \u2014 US Provisional 64/045,999" }), _jsx(Row, { c: c, label: "Filing", value: "dpyc-community/docs/patent", href: "https://github.com/lonniev/dpyc-community/tree/main/docs/patent" }), _jsx(Row, { c: c, label: "Trademarks", value: "DPYC\u2122 \u00B7 Tollbooth DPYC\u2122 \u00B7 Don't Pester Your Customer\u2122" })] }));
|
|
49
|
+
}
|
|
@@ -12,16 +12,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
12
12
|
import { useCallback, useEffect, useState } from "react";
|
|
13
13
|
import { forgetCoupon, listMyCoupons, redeemCoupon } from "../standardTools.js";
|
|
14
14
|
import { couponExpiryText, couponStatusLabel, couponUsesText, redeemedText } from "../wallet.js";
|
|
15
|
+
import { cx } from "./cx.js";
|
|
15
16
|
const DEFAULT_INTRO = "Redeem an operator's code once. The discount applies on its own to later paid calls until its uses or its window run out.";
|
|
16
17
|
const DEFAULT_EMPTY = "No coupons yet. Paste a code above to claim its discount.";
|
|
17
18
|
function defaultConfirm() {
|
|
18
19
|
return (typeof window === "undefined" ||
|
|
19
20
|
window.confirm("Remove this coupon from your list? You can redeem the code again while its window allows."));
|
|
20
21
|
}
|
|
21
|
-
function cx(...parts) {
|
|
22
|
-
const s = parts.filter(Boolean).join(" ");
|
|
23
|
-
return s || undefined;
|
|
24
|
-
}
|
|
25
22
|
export default function CouponsPanel({ heading = "My coupons", intro = DEFAULT_INTRO, empty = DEFAULT_EMPTY, formHeading, listHeading, placeholder = "Coupon code", redeemLabel = "Redeem", forgetLabel = "Remove", confirmForget = defaultConfirm, classNames: c = {}, renderCoupon, }) {
|
|
26
23
|
const [coupons, setCoupons] = useState([]);
|
|
27
24
|
const [loading, setLoading] = useState(true);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funding and credential health, as a list of rows that are read fresh and
|
|
3
|
+
* stamped with when: `PatronFundingStatus` (sign-in proof, credit balance and
|
|
4
|
+
* tranche expiry) and `OperatorFundingStatus` (credentials, Authority balance,
|
|
5
|
+
* database, background jobs).
|
|
6
|
+
*
|
|
7
|
+
* `OperatorFundingStatus` renders nothing — and asks nothing operator-only —
|
|
8
|
+
* unless the signed-in npub is the operator npub `session_status` reports, the
|
|
9
|
+
* rule the fleet's copies used. That decides only what the page shows; the
|
|
10
|
+
* server enforces its own ACL.
|
|
11
|
+
*
|
|
12
|
+
* Mechanics only: the calls, the rules (`funding.ts`) and the states. Every
|
|
13
|
+
* visual choice is the site's, through `classNames`, with `ok` / `warning` /
|
|
14
|
+
* `blocked` added to a row, its state word and the heading's summary mark.
|
|
15
|
+
* `siteRows` adds the site's own checks (an upstream OAuth connection, say);
|
|
16
|
+
* `renderRow` draws a row yourself. Refresh is a chip.
|
|
17
|
+
*/
|
|
18
|
+
import { type ReactNode } from "react";
|
|
19
|
+
import { type BalanceThresholds, type StatusLevel, type StatusRow } from "../funding.ts";
|
|
20
|
+
export interface FundingStatusClassNames {
|
|
21
|
+
root?: string;
|
|
22
|
+
/** The heading row: title, summary mark and the Refresh chip. */
|
|
23
|
+
header?: string;
|
|
24
|
+
heading?: string;
|
|
25
|
+
/** The summary mark: the worst state across rows. */
|
|
26
|
+
overall?: string;
|
|
27
|
+
chip?: string;
|
|
28
|
+
intro?: string;
|
|
29
|
+
list?: string;
|
|
30
|
+
row?: string;
|
|
31
|
+
dependency?: string;
|
|
32
|
+
state?: string;
|
|
33
|
+
detail?: string;
|
|
34
|
+
/** The "checked …" stamp. */
|
|
35
|
+
checked?: string;
|
|
36
|
+
loading?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
/** Added to a row, its state word and the summary mark, by state. */
|
|
39
|
+
ok?: string;
|
|
40
|
+
warning?: string;
|
|
41
|
+
blocked?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface FundingStatusProps {
|
|
44
|
+
heading?: ReactNode;
|
|
45
|
+
intro?: ReactNode;
|
|
46
|
+
/** The site's own checks, given the read time; drawn before the package's rows. */
|
|
47
|
+
siteRows?: (checkedAt: string) => Promise<StatusRow[]>;
|
|
48
|
+
/** The words for each state. Default "ok", "warning", "blocked". */
|
|
49
|
+
stateLabels?: Partial<Record<StatusLevel, ReactNode>>;
|
|
50
|
+
/** Draw a row yourself; `checked` is its read time in the patron's zone. */
|
|
51
|
+
renderRow?: (row: StatusRow, checked: string) => ReactNode;
|
|
52
|
+
/** Content of the Refresh chip; null for none. Default "Refresh". */
|
|
53
|
+
refreshLabel?: ReactNode;
|
|
54
|
+
classNames?: FundingStatusClassNames;
|
|
55
|
+
}
|
|
56
|
+
export interface PatronFundingStatusProps extends FundingStatusProps {
|
|
57
|
+
/** When the patron's balance reads as low or empty. */
|
|
58
|
+
thresholds?: BalanceThresholds;
|
|
59
|
+
}
|
|
60
|
+
export interface OperatorFundingStatusProps extends FundingStatusProps {
|
|
61
|
+
/** When the operator's Authority balance reads as low or empty. */
|
|
62
|
+
authorityThresholds?: BalanceThresholds;
|
|
63
|
+
}
|
|
64
|
+
/** The patron's own health: sign-in proof and credit balance. */
|
|
65
|
+
export declare function PatronFundingStatus({ thresholds, siteRows, heading, ...rest }: PatronFundingStatusProps): import("react").JSX.Element;
|
|
66
|
+
/** What only the operator can fix. Renders nothing for anyone else. */
|
|
67
|
+
export declare function OperatorFundingStatus({ authorityThresholds, siteRows, heading, ...rest }: OperatorFundingStatusProps): import("react").JSX.Element | null;
|