@tollbooth-dpyc/web 1.3.0 → 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.d.ts +3 -3
- package/dist/react/TableFilter.js +36 -18
- 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/panelPlacement.d.ts +26 -0
- package/dist/react/panelPlacement.js +44 -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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The patron's display time zone, and the conversions every clock on a page
|
|
3
|
+
* needs. Storage stays UTC; this converts at the edges — rendering an instant,
|
|
4
|
+
* reading an <input type="datetime-local">, turning a day filter into bounds.
|
|
5
|
+
*
|
|
6
|
+
* The preference is "auto" (the browser's own zone) or an IANA name, kept under
|
|
7
|
+
* `<prefix>:timezone`. Never a fixed offset: an offset is right for half the
|
|
8
|
+
* year. A stored name the browser does not know reads as "auto".
|
|
9
|
+
*
|
|
10
|
+
* Pure apart from the preference's storage, so it is tested without a browser.
|
|
11
|
+
* Lifted from eXcalibur's `lib/timezone.ts` (roastify held a copy).
|
|
12
|
+
*/
|
|
13
|
+
export type TimezonePref = "auto" | (string & {});
|
|
14
|
+
/** A starter set of major cities, west to east, plus UTC. Values are IANA names. */
|
|
15
|
+
export declare const TIMEZONE_OPTIONS: readonly {
|
|
16
|
+
value: string;
|
|
17
|
+
label: string;
|
|
18
|
+
}[];
|
|
19
|
+
/** The browser's own zone, or UTC when it will not say. */
|
|
20
|
+
export declare function detectBrowserTimeZone(): string;
|
|
21
|
+
/** True when `tz` is a zone name this browser knows. An offset ("+05:00") is not. */
|
|
22
|
+
export declare function isValidTimeZone(tz: string): boolean;
|
|
23
|
+
/** "auto", or the name when the browser knows it; anything else is "auto". */
|
|
24
|
+
export declare function normalizeTimezonePref(pref: string | null | undefined): TimezonePref;
|
|
25
|
+
/** The IANA zone a preference means right now. */
|
|
26
|
+
export declare function resolveTimeZone(pref: TimezonePref): string;
|
|
27
|
+
/** `<prefix>:timezone` — for a `storage` event listener. */
|
|
28
|
+
export declare function timezoneStorageKey(): string;
|
|
29
|
+
export declare function readTimezonePref(): TimezonePref;
|
|
30
|
+
/** Store the preference (normalized) and tell every listener on the page. */
|
|
31
|
+
export declare function writeTimezonePref(next: string): TimezonePref;
|
|
32
|
+
/** Hear every `writeTimezonePref` on this page. Returns an unsubscribe. */
|
|
33
|
+
export declare function onTimezoneChange(fn: (pref: TimezonePref) => void): () => void;
|
|
34
|
+
/** The zone every clock should use: the stored preference, resolved. */
|
|
35
|
+
export declare function displayTimeZone(): string;
|
|
36
|
+
export interface ZonedParts {
|
|
37
|
+
year: number;
|
|
38
|
+
/** 1–12 */
|
|
39
|
+
month: number;
|
|
40
|
+
day: number;
|
|
41
|
+
/** 0–23 */
|
|
42
|
+
hour: number;
|
|
43
|
+
minute: number;
|
|
44
|
+
second: number;
|
|
45
|
+
}
|
|
46
|
+
/** The calendar and clock of an instant as seen in `timeZone`. */
|
|
47
|
+
export declare function getZonedParts(date: Date, timeZone: string): ZonedParts;
|
|
48
|
+
/** The zone's offset from UTC at an instant, in ms (New York in winter: −5 h). */
|
|
49
|
+
export declare function getTimeZoneOffsetMs(date: Date, timeZone: string): number;
|
|
50
|
+
/**
|
|
51
|
+
* A wall time in `timeZone` as a UTC instant (epoch ms).
|
|
52
|
+
*
|
|
53
|
+
* Around a DST change a wall time can be missing or doubled. A missing one
|
|
54
|
+
* (02:30 on New York's spring-forward day) is read with the offset from before
|
|
55
|
+
* the change, so it lands an hour later (03:30); a doubled one (01:30 on the
|
|
56
|
+
* fall-back day) is the earlier of the two. Temporal calls this "compatible".
|
|
57
|
+
*/
|
|
58
|
+
export declare function zonedWallTimeToUtcMs(year: number, month: number, day: number, hour: number, minute: number, second: number, timeZone: string): number;
|
|
59
|
+
export declare function formatDateTime(iso: string | null | undefined, timeZone: string, opts?: Intl.DateTimeFormatOptions): string;
|
|
60
|
+
export declare function formatDate(iso: string | null | undefined, timeZone: string, opts?: Intl.DateTimeFormatOptions): string;
|
|
61
|
+
export declare function formatTime(iso: string | null | undefined, timeZone: string, opts?: Intl.DateTimeFormatOptions): string;
|
|
62
|
+
/** An hour of the day (0–23) as the patron's locale writes it: "9 AM", "21". */
|
|
63
|
+
export declare function formatHourLabel(hour: number, timeZone: string): string;
|
|
64
|
+
/** The hour (0–23) of an instant in `timeZone`, or null for a bad instant. */
|
|
65
|
+
export declare function hourInZone(iso: string, timeZone: string): number | null;
|
|
66
|
+
/** A UTC instant → the value for a datetime-local input showing `timeZone`. */
|
|
67
|
+
export declare function isoToDatetimeLocalValue(iso: string, timeZone: string): string;
|
|
68
|
+
/** A datetime-local value, read as a wall time in `timeZone` → UTC ISO. */
|
|
69
|
+
export declare function datetimeLocalValueToIso(value: string, timeZone: string): string | null;
|
|
70
|
+
/** The patron's midnight starting `day`, as UTC ISO (an inclusive lower bound). */
|
|
71
|
+
export declare function startOfLocalDayIso(day: string, timeZone: string): string | null;
|
|
72
|
+
/** The patron's midnight after `day`, as UTC ISO (an exclusive upper bound). */
|
|
73
|
+
export declare function startOfNextLocalDayIso(day: string, timeZone: string): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* A from/to day filter as the instants a server can compare: from the patron's
|
|
76
|
+
* midnight starting `dateFrom` up to (not including) the one after `dateTo`.
|
|
77
|
+
* An empty or malformed day is left out.
|
|
78
|
+
*/
|
|
79
|
+
export declare function localDateFilterBounds(dateFrom: string, dateTo: string, timeZone: string): {
|
|
80
|
+
dateFrom?: string;
|
|
81
|
+
dateTo?: string;
|
|
82
|
+
};
|
package/dist/timezone.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The patron's display time zone, and the conversions every clock on a page
|
|
3
|
+
* needs. Storage stays UTC; this converts at the edges — rendering an instant,
|
|
4
|
+
* reading an <input type="datetime-local">, turning a day filter into bounds.
|
|
5
|
+
*
|
|
6
|
+
* The preference is "auto" (the browser's own zone) or an IANA name, kept under
|
|
7
|
+
* `<prefix>:timezone`. Never a fixed offset: an offset is right for half the
|
|
8
|
+
* year. A stored name the browser does not know reads as "auto".
|
|
9
|
+
*
|
|
10
|
+
* Pure apart from the preference's storage, so it is tested without a browser.
|
|
11
|
+
* Lifted from eXcalibur's `lib/timezone.ts` (roastify held a copy).
|
|
12
|
+
*/
|
|
13
|
+
import { readStored, writeStored, key } from "./storage.js";
|
|
14
|
+
const PREF = "timezone";
|
|
15
|
+
/** A starter set of major cities, west to east, plus UTC. Values are IANA names. */
|
|
16
|
+
export const TIMEZONE_OPTIONS = [
|
|
17
|
+
{ value: "Pacific/Honolulu", label: "Honolulu" },
|
|
18
|
+
{ value: "America/Anchorage", label: "Anchorage" },
|
|
19
|
+
{ value: "America/Los_Angeles", label: "Los Angeles" },
|
|
20
|
+
{ value: "America/Denver", label: "Denver" },
|
|
21
|
+
{ value: "America/Chicago", label: "Chicago" },
|
|
22
|
+
{ value: "America/New_York", label: "New York" },
|
|
23
|
+
{ value: "America/Sao_Paulo", label: "São Paulo" },
|
|
24
|
+
{ value: "Europe/London", label: "London" },
|
|
25
|
+
{ value: "Europe/Berlin", label: "Berlin" },
|
|
26
|
+
{ value: "Europe/Athens", label: "Athens" },
|
|
27
|
+
{ value: "Asia/Dubai", label: "Dubai" },
|
|
28
|
+
{ value: "Asia/Kolkata", label: "Mumbai" },
|
|
29
|
+
{ value: "Asia/Singapore", label: "Singapore" },
|
|
30
|
+
{ value: "Asia/Tokyo", label: "Tokyo" },
|
|
31
|
+
{ value: "Australia/Sydney", label: "Sydney" },
|
|
32
|
+
{ value: "Pacific/Auckland", label: "Auckland" },
|
|
33
|
+
{ value: "UTC", label: "UTC" },
|
|
34
|
+
];
|
|
35
|
+
// ── The zone itself ─────────────────────────────────────────────────────────
|
|
36
|
+
/** The browser's own zone, or UTC when it will not say. */
|
|
37
|
+
export function detectBrowserTimeZone() {
|
|
38
|
+
try {
|
|
39
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
40
|
+
if (tz)
|
|
41
|
+
return tz;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* an engine without time-zone data */
|
|
45
|
+
}
|
|
46
|
+
return "UTC";
|
|
47
|
+
}
|
|
48
|
+
/** True when `tz` is a zone name this browser knows. An offset ("+05:00") is not. */
|
|
49
|
+
export function isValidTimeZone(tz) {
|
|
50
|
+
if (!tz || /^[+-]/.test(tz))
|
|
51
|
+
return false;
|
|
52
|
+
try {
|
|
53
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** "auto", or the name when the browser knows it; anything else is "auto". */
|
|
61
|
+
export function normalizeTimezonePref(pref) {
|
|
62
|
+
return pref && pref !== "auto" && isValidTimeZone(pref) ? pref : "auto";
|
|
63
|
+
}
|
|
64
|
+
/** The IANA zone a preference means right now. */
|
|
65
|
+
export function resolveTimeZone(pref) {
|
|
66
|
+
const p = normalizeTimezonePref(pref);
|
|
67
|
+
return p === "auto" ? detectBrowserTimeZone() : p;
|
|
68
|
+
}
|
|
69
|
+
// ── The preference, stored ─────────────────────────────────────────────────
|
|
70
|
+
/** `<prefix>:timezone` — for a `storage` event listener. */
|
|
71
|
+
export function timezoneStorageKey() {
|
|
72
|
+
return key(PREF);
|
|
73
|
+
}
|
|
74
|
+
export function readTimezonePref() {
|
|
75
|
+
return normalizeTimezonePref(readStored(PREF));
|
|
76
|
+
}
|
|
77
|
+
const listeners = new Set();
|
|
78
|
+
/** Store the preference (normalized) and tell every listener on the page. */
|
|
79
|
+
export function writeTimezonePref(next) {
|
|
80
|
+
const value = normalizeTimezonePref(next);
|
|
81
|
+
writeStored(PREF, value);
|
|
82
|
+
listeners.forEach((fn) => fn(value));
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
/** Hear every `writeTimezonePref` on this page. Returns an unsubscribe. */
|
|
86
|
+
export function onTimezoneChange(fn) {
|
|
87
|
+
listeners.add(fn);
|
|
88
|
+
return () => {
|
|
89
|
+
listeners.delete(fn);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** The zone every clock should use: the stored preference, resolved. */
|
|
93
|
+
export function displayTimeZone() {
|
|
94
|
+
return resolveTimeZone(readTimezonePref());
|
|
95
|
+
}
|
|
96
|
+
const partsFormats = new Map();
|
|
97
|
+
function partsFormat(timeZone) {
|
|
98
|
+
let f = partsFormats.get(timeZone);
|
|
99
|
+
if (!f) {
|
|
100
|
+
f = new Intl.DateTimeFormat("en-US", {
|
|
101
|
+
timeZone,
|
|
102
|
+
year: "numeric",
|
|
103
|
+
month: "2-digit",
|
|
104
|
+
day: "2-digit",
|
|
105
|
+
hour: "2-digit",
|
|
106
|
+
minute: "2-digit",
|
|
107
|
+
second: "2-digit",
|
|
108
|
+
hourCycle: "h23",
|
|
109
|
+
});
|
|
110
|
+
partsFormats.set(timeZone, f);
|
|
111
|
+
}
|
|
112
|
+
return f;
|
|
113
|
+
}
|
|
114
|
+
function pad2(n) {
|
|
115
|
+
return n < 10 ? `0${n}` : String(n);
|
|
116
|
+
}
|
|
117
|
+
/** The calendar and clock of an instant as seen in `timeZone`. */
|
|
118
|
+
export function getZonedParts(date, timeZone) {
|
|
119
|
+
const map = {};
|
|
120
|
+
for (const p of partsFormat(timeZone).formatToParts(date)) {
|
|
121
|
+
if (p.type !== "literal")
|
|
122
|
+
map[p.type] = p.value;
|
|
123
|
+
}
|
|
124
|
+
const hour = Number(map.hour ?? "0");
|
|
125
|
+
return {
|
|
126
|
+
year: Number(map.year),
|
|
127
|
+
month: Number(map.month),
|
|
128
|
+
day: Number(map.day),
|
|
129
|
+
hour: hour === 24 ? 0 : hour, // some engines still say 24 at midnight
|
|
130
|
+
minute: Number(map.minute ?? "0"),
|
|
131
|
+
second: Number(map.second ?? "0"),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** The zone's offset from UTC at an instant, in ms (New York in winter: −5 h). */
|
|
135
|
+
export function getTimeZoneOffsetMs(date, timeZone) {
|
|
136
|
+
const t = date.getTime();
|
|
137
|
+
const whole = t - (((t % 1000) + 1000) % 1000); // the parts carry no milliseconds
|
|
138
|
+
const p = getZonedParts(date, timeZone);
|
|
139
|
+
return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - whole;
|
|
140
|
+
}
|
|
141
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
142
|
+
/**
|
|
143
|
+
* A wall time in `timeZone` as a UTC instant (epoch ms).
|
|
144
|
+
*
|
|
145
|
+
* Around a DST change a wall time can be missing or doubled. A missing one
|
|
146
|
+
* (02:30 on New York's spring-forward day) is read with the offset from before
|
|
147
|
+
* the change, so it lands an hour later (03:30); a doubled one (01:30 on the
|
|
148
|
+
* fall-back day) is the earlier of the two. Temporal calls this "compatible".
|
|
149
|
+
*/
|
|
150
|
+
export function zonedWallTimeToUtcMs(year, month, day, hour, minute, second, timeZone) {
|
|
151
|
+
const wall = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
152
|
+
const before = getTimeZoneOffsetMs(new Date(wall - DAY_MS), timeZone);
|
|
153
|
+
const after = getTimeZoneOffsetMs(new Date(wall + DAY_MS), timeZone);
|
|
154
|
+
const fits = [wall - before, wall - after].filter((t) => getTimeZoneOffsetMs(new Date(t), timeZone) === wall - t);
|
|
155
|
+
return fits.length ? Math.min(...fits) : wall - before;
|
|
156
|
+
}
|
|
157
|
+
// ── Rendering an instant ────────────────────────────────────────────────────
|
|
158
|
+
function render(iso, draw) {
|
|
159
|
+
if (!iso)
|
|
160
|
+
return "—";
|
|
161
|
+
const t = Date.parse(iso);
|
|
162
|
+
return Number.isNaN(t) ? String(iso) : draw(new Date(t));
|
|
163
|
+
}
|
|
164
|
+
export function formatDateTime(iso, timeZone, opts) {
|
|
165
|
+
return render(iso, (d) => d.toLocaleString(undefined, { timeZone, ...opts }));
|
|
166
|
+
}
|
|
167
|
+
export function formatDate(iso, timeZone, opts) {
|
|
168
|
+
return render(iso, (d) => d.toLocaleDateString(undefined, { timeZone, ...opts }));
|
|
169
|
+
}
|
|
170
|
+
export function formatTime(iso, timeZone, opts) {
|
|
171
|
+
return render(iso, (d) => d.toLocaleTimeString(undefined, { timeZone, ...opts }));
|
|
172
|
+
}
|
|
173
|
+
/** An hour of the day (0–23) as the patron's locale writes it: "9 AM", "21". */
|
|
174
|
+
export function formatHourLabel(hour, timeZone) {
|
|
175
|
+
// A fixed winter date, so only the hour matters.
|
|
176
|
+
const utc = zonedWallTimeToUtcMs(2024, 1, 15, hour, 0, 0, timeZone);
|
|
177
|
+
return new Date(utc).toLocaleTimeString(undefined, { timeZone, hour: "numeric" });
|
|
178
|
+
}
|
|
179
|
+
/** The hour (0–23) of an instant in `timeZone`, or null for a bad instant. */
|
|
180
|
+
export function hourInZone(iso, timeZone) {
|
|
181
|
+
const t = Date.parse(iso);
|
|
182
|
+
return Number.isNaN(t) ? null : getZonedParts(new Date(t), timeZone).hour;
|
|
183
|
+
}
|
|
184
|
+
// ── <input type="datetime-local"> ↔ ISO ─────────────────────────────────────
|
|
185
|
+
/** A UTC instant → the value for a datetime-local input showing `timeZone`. */
|
|
186
|
+
export function isoToDatetimeLocalValue(iso, timeZone) {
|
|
187
|
+
const t = Date.parse(iso);
|
|
188
|
+
if (Number.isNaN(t))
|
|
189
|
+
return "";
|
|
190
|
+
const p = getZonedParts(new Date(t), timeZone);
|
|
191
|
+
return `${p.year}-${pad2(p.month)}-${pad2(p.day)}T${pad2(p.hour)}:${pad2(p.minute)}`;
|
|
192
|
+
}
|
|
193
|
+
/** A datetime-local value, read as a wall time in `timeZone` → UTC ISO. */
|
|
194
|
+
export function datetimeLocalValueToIso(value, timeZone) {
|
|
195
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?/.exec(value.trim());
|
|
196
|
+
if (!m)
|
|
197
|
+
return null;
|
|
198
|
+
const ms = zonedWallTimeToUtcMs(Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]), m[6] ? Number(m[6]) : 0, timeZone);
|
|
199
|
+
return new Date(ms).toISOString();
|
|
200
|
+
}
|
|
201
|
+
// ── A day filter's bounds (YYYY-MM-DD in the zone → UTC ISO) ────────────────
|
|
202
|
+
function ymd(value) {
|
|
203
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
|
|
204
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
205
|
+
}
|
|
206
|
+
/** The patron's midnight starting `day`, as UTC ISO (an inclusive lower bound). */
|
|
207
|
+
export function startOfLocalDayIso(day, timeZone) {
|
|
208
|
+
const d = ymd(day);
|
|
209
|
+
return d ? new Date(zonedWallTimeToUtcMs(d[0], d[1], d[2], 0, 0, 0, timeZone)).toISOString() : null;
|
|
210
|
+
}
|
|
211
|
+
/** The patron's midnight after `day`, as UTC ISO (an exclusive upper bound). */
|
|
212
|
+
export function startOfNextLocalDayIso(day, timeZone) {
|
|
213
|
+
const d = ymd(day);
|
|
214
|
+
if (!d)
|
|
215
|
+
return null;
|
|
216
|
+
// The next calendar day, counted on UTC noon so no zone can move it.
|
|
217
|
+
const next = new Date(Date.UTC(d[0], d[1] - 1, d[2], 12) + DAY_MS);
|
|
218
|
+
const ms = zonedWallTimeToUtcMs(next.getUTCFullYear(), next.getUTCMonth() + 1, next.getUTCDate(), 0, 0, 0, timeZone);
|
|
219
|
+
return new Date(ms).toISOString();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* A from/to day filter as the instants a server can compare: from the patron's
|
|
223
|
+
* midnight starting `dateFrom` up to (not including) the one after `dateTo`.
|
|
224
|
+
* An empty or malformed day is left out.
|
|
225
|
+
*/
|
|
226
|
+
export function localDateFilterBounds(dateFrom, dateTo, timeZone) {
|
|
227
|
+
const out = {};
|
|
228
|
+
const from = dateFrom ? startOfLocalDayIso(dateFrom, timeZone) : null;
|
|
229
|
+
const to = dateTo ? startOfNextLocalDayIso(dateTo, timeZone) : null;
|
|
230
|
+
if (from)
|
|
231
|
+
out.dateFrom = from;
|
|
232
|
+
if (to)
|
|
233
|
+
out.dateTo = to;
|
|
234
|
+
return out;
|
|
235
|
+
}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An `account_statement` read as a usage card: the account's totals, what the
|
|
3
|
+
* last N days spent and bought, and where the sats went. Framework-free, so it
|
|
4
|
+
* is tested without React or a server.
|
|
5
|
+
*
|
|
6
|
+
* The union of what the fleet's "Last 30 days" cards showed — balance,
|
|
7
|
+
* deposited, consumed (eXcalibur, cypher, Roastify), expired and per-tool
|
|
8
|
+
* calls and sats (Optionality) — with the period figures taken from the
|
|
9
|
+
* statement's own daily log rather than the all-time summary.
|
|
10
|
+
*/
|
|
11
|
+
import type { AccountStatementResult } from "./standardTools.ts";
|
|
12
|
+
export interface ToolSpend {
|
|
13
|
+
tool: string;
|
|
14
|
+
calls: number;
|
|
15
|
+
sats: number;
|
|
16
|
+
}
|
|
17
|
+
export interface UsageFacts {
|
|
18
|
+
/** The period the server reported on, in days. */
|
|
19
|
+
days: number;
|
|
20
|
+
balance: number;
|
|
21
|
+
deposited: number;
|
|
22
|
+
consumed: number;
|
|
23
|
+
expired: number;
|
|
24
|
+
/** Paid calls in the period. */
|
|
25
|
+
calls: number;
|
|
26
|
+
/** api_sats spent in the period. */
|
|
27
|
+
spent: number;
|
|
28
|
+
/** api_sats credited by top-ups settled in the period. */
|
|
29
|
+
credited: number;
|
|
30
|
+
/** The period's tools, most sats first, at most `topTools` of them. */
|
|
31
|
+
tools: ToolSpend[];
|
|
32
|
+
}
|
|
33
|
+
/** The facts a usage card shows, or null when the statement carries none. */
|
|
34
|
+
export declare function usageFacts(stmt: AccountStatementResult, days: number, topTools?: number): UsageFacts | null;
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An `account_statement` read as a usage card: the account's totals, what the
|
|
3
|
+
* last N days spent and bought, and where the sats went. Framework-free, so it
|
|
4
|
+
* is tested without React or a server.
|
|
5
|
+
*
|
|
6
|
+
* The union of what the fleet's "Last 30 days" cards showed — balance,
|
|
7
|
+
* deposited, consumed (eXcalibur, cypher, Roastify), expired and per-tool
|
|
8
|
+
* calls and sats (Optionality) — with the period figures taken from the
|
|
9
|
+
* statement's own daily log rather than the all-time summary.
|
|
10
|
+
*/
|
|
11
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
/** The facts a usage card shows, or null when the statement carries none. */
|
|
13
|
+
export function usageFacts(stmt, days, topTools = 5) {
|
|
14
|
+
const summary = stmt.account_summary;
|
|
15
|
+
if (!summary)
|
|
16
|
+
return null;
|
|
17
|
+
const period = stmt.statement_period_days ?? days;
|
|
18
|
+
let calls = 0;
|
|
19
|
+
let spent = 0;
|
|
20
|
+
const byTool = new Map();
|
|
21
|
+
for (const day of stmt.daily_usage ?? []) {
|
|
22
|
+
calls += day.total_calls;
|
|
23
|
+
spent += day.total_api_sats;
|
|
24
|
+
for (const [tool, u] of Object.entries(day.tools ?? {})) {
|
|
25
|
+
const row = byTool.get(tool) ?? { tool, calls: 0, sats: 0 };
|
|
26
|
+
row.calls += u.calls;
|
|
27
|
+
row.sats += u.api_sats;
|
|
28
|
+
byTool.set(tool, row);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const end = Date.parse(stmt.generated_at ?? "");
|
|
32
|
+
const since = (Number.isNaN(end) ? Date.now() : end) - period * DAY_MS;
|
|
33
|
+
let credited = 0;
|
|
34
|
+
for (const inv of stmt.purchase_history ?? []) {
|
|
35
|
+
const at = Date.parse(inv.settled_at ?? "");
|
|
36
|
+
if (!Number.isNaN(at) && at >= since)
|
|
37
|
+
credited += inv.api_sats_credited;
|
|
38
|
+
}
|
|
39
|
+
const tools = [...byTool.values()]
|
|
40
|
+
.sort((a, b) => b.sats - a.sats || b.calls - a.calls || a.tool.localeCompare(b.tool))
|
|
41
|
+
.slice(0, Math.max(0, topTools));
|
|
42
|
+
return {
|
|
43
|
+
days: period,
|
|
44
|
+
balance: summary.balance_api_sats,
|
|
45
|
+
deposited: summary.total_deposited_api_sats,
|
|
46
|
+
consumed: summary.total_consumed_api_sats,
|
|
47
|
+
expired: summary.total_expired_api_sats,
|
|
48
|
+
calls,
|
|
49
|
+
spent,
|
|
50
|
+
credited,
|
|
51
|
+
tools,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tollbooth-dpyc/web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Shared browser SDK for Tollbooth DPYC operator front ends: npub sign-in, proofs, MCP client, profile and wallet",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Lonnie VanZandt",
|