@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 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`, `TableFilter`, `SortHeader` / `PageControls` /
14
- `TableShell`, `ThemeToggle`, `ErrorBoundary`, `AvatarPicker`, `DebugPanel`,
15
- `QuoteScroller`, `useSession`, `useTopUp`, `useTheme`, `useDebugLog`
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
- zone the site stores under `<prefix>:timezone`, if any.
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
- try {
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;
@@ -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;