@tokenbuddy/tb-admin 1.0.15 → 1.0.28
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/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +323 -14
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +12 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +12 -8
- package/dist/src/client.js.map +1 -1
- package/dist/src/display-format.d.ts +39 -0
- package/dist/src/display-format.d.ts.map +1 -0
- package/dist/src/display-format.js +354 -0
- package/dist/src/display-format.js.map +1 -0
- package/dist/src/server-cmd.d.ts +3 -0
- package/dist/src/server-cmd.d.ts.map +1 -1
- package/dist/src/server-cmd.js +32 -9
- package/dist/src/server-cmd.js.map +1 -1
- package/dist/src/ui-actions.d.ts +2 -0
- package/dist/src/ui-actions.d.ts.map +1 -1
- package/dist/src/ui-actions.js +123 -63
- package/dist/src/ui-actions.js.map +1 -1
- package/dist/src/ui-command.js +1 -1
- package/dist/src/ui-command.js.map +1 -1
- package/dist/src/ui-server.d.ts +0 -1
- package/dist/src/ui-server.d.ts.map +1 -1
- package/dist/src/ui-server.js +25 -9
- package/dist/src/ui-server.js.map +1 -1
- package/dist/src/ui-state.d.ts +7 -1
- package/dist/src/ui-state.d.ts.map +1 -1
- package/dist/src/ui-state.js +55 -24
- package/dist/src/ui-state.js.map +1 -1
- package/dist/src/ui-static.d.ts.map +1 -1
- package/dist/src/ui-static.js +371 -46
- package/dist/src/ui-static.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +367 -14
- package/src/client.ts +13 -8
- package/src/display-format.ts +398 -0
- package/src/server-cmd.ts +35 -9
- package/src/ui-actions.ts +129 -72
- package/src/ui-command.ts +1 -1
- package/src/ui-server.ts +24 -10
- package/src/ui-state.ts +64 -25
- package/src/ui-static.ts +374 -46
- package/tests/admin.test.ts +590 -41
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// Shared display formatters for TokenBuddy UI surfaces.
|
|
2
|
+
//
|
|
3
|
+
// This module is a portable copy of `packages/tb-ui/src/lib/display-format.ts`
|
|
4
|
+
// and the canonical formatter functions declared in `DESIGN.md`
|
|
5
|
+
// ("Formatter Implementation" section). Keep both files in sync — the
|
|
6
|
+
// rule from DESIGN.md is:
|
|
7
|
+
// "Do not add new per-page formatting helpers for these domains.
|
|
8
|
+
// If a page needs a different display, add an explicit option
|
|
9
|
+
// to the shared formatter."
|
|
10
|
+
//
|
|
11
|
+
// Differences vs. the React copy:
|
|
12
|
+
// 1. No React-only imports.
|
|
13
|
+
// 2. Adds `formatBalanceAmount` and `formatSellerStatus` for the
|
|
14
|
+
// admin surface, because the admin UI surfaces live balance
|
|
15
|
+
// numbers and node-status dots that the buyer UI never shows.
|
|
16
|
+
|
|
17
|
+
export const UNKNOWN_VALUE = "—";
|
|
18
|
+
|
|
19
|
+
const PAD2 = (n: number) => (n < 10 ? `0${n}` : `${n}`);
|
|
20
|
+
|
|
21
|
+
export function formatTokenCount(
|
|
22
|
+
value: number | undefined | null,
|
|
23
|
+
options: { compact?: boolean } = {}
|
|
24
|
+
): string {
|
|
25
|
+
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
26
|
+
const numeric = value as number;
|
|
27
|
+
if (options.compact === false) return Math.round(numeric).toLocaleString("en-US");
|
|
28
|
+
if (numeric < 10_000) return Math.round(numeric).toLocaleString("en-US");
|
|
29
|
+
if (numeric < 1_000_000) return `${(numeric / 1_000).toFixed(1)}K`;
|
|
30
|
+
if (numeric < 1_000_000_000) return `${(numeric / 1_000_000).toFixed(1)}M`;
|
|
31
|
+
return `${(numeric / 1_000_000_000).toFixed(2)}B`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatTokenPair(
|
|
35
|
+
input: number | undefined | null,
|
|
36
|
+
output: number | undefined | null,
|
|
37
|
+
options: { compact?: boolean; separator?: string } = {}
|
|
38
|
+
): string {
|
|
39
|
+
const separator = options.separator ?? " / ";
|
|
40
|
+
return `In ${formatTokenCount(input, options)}${separator}Out ${formatTokenCount(output, options)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatCount(value: number | undefined | null): string {
|
|
44
|
+
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
45
|
+
return Math.round(value as number).toLocaleString("en-US");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatMoney(
|
|
49
|
+
micros: number | undefined | null,
|
|
50
|
+
options: { digits?: number; ledger?: boolean; signed?: boolean } = {}
|
|
51
|
+
): string {
|
|
52
|
+
if (!Number.isFinite(micros)) return UNKNOWN_VALUE;
|
|
53
|
+
const amount = Math.abs((micros as number) / 1_000_000);
|
|
54
|
+
const digits = options.digits ?? (options.ledger && amount < 0.01 ? 6 : 4);
|
|
55
|
+
const formatted = `$${((micros as number) / 1_000_000).toFixed(digits)}`;
|
|
56
|
+
return options.signed && (micros as number) >= 0 ? `+${formatted}` : formatted;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatMoneyPair(
|
|
60
|
+
actualMicros: number | undefined | null,
|
|
61
|
+
referenceMicros: number | undefined | null,
|
|
62
|
+
options: { digits?: number } = {}
|
|
63
|
+
): string {
|
|
64
|
+
if (!Number.isFinite(actualMicros) && !Number.isFinite(referenceMicros)) return UNKNOWN_VALUE;
|
|
65
|
+
return `${formatMoney(actualMicros, options)} / ${formatMoney(referenceMicros, options)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatDuration(valueMs: number | undefined | null): string {
|
|
69
|
+
if (!Number.isFinite(valueMs)) return UNKNOWN_VALUE;
|
|
70
|
+
const ms = Math.max(0, Math.round(valueMs as number));
|
|
71
|
+
if (ms < 1000) return `${ms}ms`;
|
|
72
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatPercent(value: number | undefined | null): string {
|
|
76
|
+
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
77
|
+
return `${Math.round((value as number) * 100)}%`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatDiscountRatio(discountRatio: number | undefined | null): string {
|
|
81
|
+
if (!Number.isFinite(discountRatio)) return UNKNOWN_VALUE;
|
|
82
|
+
const discount = Math.max(0, 1 - (discountRatio as number));
|
|
83
|
+
return formatPercent(discount);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatPriceMicrosPerMillion(value: number | undefined | null): string {
|
|
87
|
+
return formatMoney(value, { digits: 4 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function formatPricePair(
|
|
91
|
+
inputMicros: number | undefined | null,
|
|
92
|
+
outputMicros: number | undefined | null
|
|
93
|
+
): string {
|
|
94
|
+
return formatMoneyPair(inputMicros, outputMicros, { digits: 4 });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function normalizeStatusLabel(status: string | undefined | null): string {
|
|
98
|
+
if (!status) return UNKNOWN_VALUE;
|
|
99
|
+
return status.trim().toLowerCase().replaceAll("_", " ");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type StatusTone = "green" | "amber" | "red" | "blue" | "gray";
|
|
103
|
+
|
|
104
|
+
export function statusTone(status: string | undefined | null): StatusTone {
|
|
105
|
+
const normalized = normalizeStatusLabel(status);
|
|
106
|
+
if (
|
|
107
|
+
normalized === "ok" ||
|
|
108
|
+
normalized === "online" ||
|
|
109
|
+
normalized === "configured" ||
|
|
110
|
+
normalized === "settled" ||
|
|
111
|
+
normalized === "completed" ||
|
|
112
|
+
normalized === "success" ||
|
|
113
|
+
normalized === "active" ||
|
|
114
|
+
normalized === "healthy"
|
|
115
|
+
) {
|
|
116
|
+
return "green";
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
normalized === "fallback" ||
|
|
120
|
+
normalized === "pending" ||
|
|
121
|
+
normalized === "degraded" ||
|
|
122
|
+
normalized === "preview" ||
|
|
123
|
+
normalized === "draining" ||
|
|
124
|
+
normalized === "busy capacity" ||
|
|
125
|
+
normalized === "auth unknown"
|
|
126
|
+
) {
|
|
127
|
+
return "amber";
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
normalized === "failed" ||
|
|
131
|
+
normalized === "error" ||
|
|
132
|
+
normalized === "canceled" ||
|
|
133
|
+
normalized === "unhealthy" ||
|
|
134
|
+
normalized === "offline"
|
|
135
|
+
) {
|
|
136
|
+
return "red";
|
|
137
|
+
}
|
|
138
|
+
if (normalized === "running") return "blue";
|
|
139
|
+
return "gray";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function formatHash(value: string | undefined | null, length = 32): string {
|
|
143
|
+
if (!value) return UNKNOWN_VALUE;
|
|
144
|
+
return value.length > length ? `${value.slice(0, length)}...` : value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function formatSellerId(value: string | undefined | null): string {
|
|
148
|
+
if (!value) return UNKNOWN_VALUE;
|
|
149
|
+
if (value.startsWith("tbs-") && value.length > 10) return value.slice(0, 10);
|
|
150
|
+
if (value.length <= 12) return value;
|
|
151
|
+
return value.slice(0, 12);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function formatTimeCompact(iso: string | undefined | null): string {
|
|
155
|
+
if (!iso) return UNKNOWN_VALUE;
|
|
156
|
+
const d = new Date(iso);
|
|
157
|
+
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
158
|
+
const now = new Date();
|
|
159
|
+
const sameDay =
|
|
160
|
+
d.getFullYear() === now.getFullYear() &&
|
|
161
|
+
d.getMonth() === now.getMonth() &&
|
|
162
|
+
d.getDate() === now.getDate();
|
|
163
|
+
const time = `${PAD2(d.getHours())}:${PAD2(d.getMinutes())}`;
|
|
164
|
+
if (sameDay) return time;
|
|
165
|
+
return `${PAD2(d.getMonth() + 1)}/${PAD2(d.getDate())} ${time}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function formatTimeFull(iso: string | undefined | null): string {
|
|
169
|
+
if (!iso) return UNKNOWN_VALUE;
|
|
170
|
+
const d = new Date(iso);
|
|
171
|
+
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
172
|
+
return (
|
|
173
|
+
`${d.getFullYear()}-${PAD2(d.getMonth() + 1)}-${PAD2(d.getDate())} ` +
|
|
174
|
+
`${PAD2(d.getHours())}:${PAD2(d.getMinutes())}:${PAD2(d.getSeconds())}`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function formatTimeLedger(iso: string | undefined | null): string {
|
|
179
|
+
if (!iso) return UNKNOWN_VALUE;
|
|
180
|
+
const d = new Date(iso);
|
|
181
|
+
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
182
|
+
return (
|
|
183
|
+
`${d.getFullYear()}/${PAD2(d.getMonth() + 1)}/${PAD2(d.getDate())} ` +
|
|
184
|
+
`${PAD2(d.getHours())}:${PAD2(d.getMinutes())}:${PAD2(d.getSeconds())}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function formatRouteSwitchTime(iso: string | undefined | null): string {
|
|
189
|
+
return formatTimeCompact(iso);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- admin-only extensions ---------------------------------------------------
|
|
193
|
+
|
|
194
|
+
export function formatBalanceAmount(
|
|
195
|
+
usdMicros: number | undefined | null,
|
|
196
|
+
currency: string | undefined | null
|
|
197
|
+
): string {
|
|
198
|
+
if (!Number.isFinite(usdMicros)) return UNKNOWN_VALUE;
|
|
199
|
+
const amount = (usdMicros as number) / 1_000_000;
|
|
200
|
+
const digits = Math.abs(amount) >= 100 ? 0 : 2;
|
|
201
|
+
const code = (currency || "USD").toUpperCase();
|
|
202
|
+
return `${code} ${amount.toFixed(digits)}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function formatSellerCapacity(
|
|
206
|
+
used: number | undefined | null,
|
|
207
|
+
limit: number | undefined | null
|
|
208
|
+
): string {
|
|
209
|
+
if (!Number.isFinite(used) && !Number.isFinite(limit)) return UNKNOWN_VALUE;
|
|
210
|
+
const u = Number.isFinite(used) ? Math.round(used as number) : UNKNOWN_VALUE;
|
|
211
|
+
const l = Number.isFinite(limit) ? Math.round(limit as number) : UNKNOWN_VALUE;
|
|
212
|
+
return `${u} / ${l}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function formatSpeed(value: number | undefined | null): string {
|
|
216
|
+
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
217
|
+
return `${(value as number).toFixed(1)} tok/s`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type CanonicalStatus =
|
|
221
|
+
| "ok"
|
|
222
|
+
| "online"
|
|
223
|
+
| "configured"
|
|
224
|
+
| "pending"
|
|
225
|
+
| "degraded"
|
|
226
|
+
| "error"
|
|
227
|
+
| "unknown";
|
|
228
|
+
|
|
229
|
+
const SELLER_STATUS_MAP: Record<string, CanonicalStatus> = {
|
|
230
|
+
active: "ok",
|
|
231
|
+
healthy: "ok",
|
|
232
|
+
online: "online",
|
|
233
|
+
configured: "configured",
|
|
234
|
+
pending: "pending",
|
|
235
|
+
draining: "degraded",
|
|
236
|
+
degraded: "degraded",
|
|
237
|
+
busy_capacity: "degraded",
|
|
238
|
+
offline: "error",
|
|
239
|
+
unhealthy: "error",
|
|
240
|
+
error: "error",
|
|
241
|
+
auth_unknown: "unknown",
|
|
242
|
+
unknown: "unknown"
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export function formatSellerStatus(status: string | undefined | null): CanonicalStatus {
|
|
246
|
+
const key = String(status || "unknown")
|
|
247
|
+
.trim()
|
|
248
|
+
.toLowerCase()
|
|
249
|
+
.replace(/-/g, "_");
|
|
250
|
+
return SELLER_STATUS_MAP[key] || "unknown";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function sellerStatusTone(status: string | undefined | null): StatusTone {
|
|
254
|
+
return statusTone(formatSellerStatus(status));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Inlined browser bundle — emits a self-invoking function that
|
|
258
|
+
// attaches the same formatter API to `window.__tbFmt`. The HTML
|
|
259
|
+
// served by `tb-admin ui` includes this bundle verbatim so the
|
|
260
|
+
// page can use the shared spec-compliant helpers without an
|
|
261
|
+
// extra <script src> round-trip. The JS bodies below MUST stay
|
|
262
|
+
// in lockstep with the TS implementations above.
|
|
263
|
+
export function displayFormatBundle(): string {
|
|
264
|
+
return `(() => {
|
|
265
|
+
const UNKNOWN_VALUE = ${JSON.stringify(UNKNOWN_VALUE)};
|
|
266
|
+
const PAD2 = (n) => (n < 10 ? "0" + n : "" + n);
|
|
267
|
+
function formatTokenCount(value, options) {
|
|
268
|
+
options = options || {};
|
|
269
|
+
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
270
|
+
const numeric = value;
|
|
271
|
+
if (options.compact === false) return Math.round(numeric).toLocaleString("en-US");
|
|
272
|
+
if (numeric < 10000) return Math.round(numeric).toLocaleString("en-US");
|
|
273
|
+
if (numeric < 1000000) return (numeric / 1000).toFixed(1) + "K";
|
|
274
|
+
if (numeric < 1000000000) return (numeric / 1000000).toFixed(1) + "M";
|
|
275
|
+
return (numeric / 1000000000).toFixed(2) + "B";
|
|
276
|
+
}
|
|
277
|
+
function formatTokenPair(input, output, options) {
|
|
278
|
+
options = options || {};
|
|
279
|
+
const separator = options.separator == null ? " / " : options.separator;
|
|
280
|
+
return "In " + formatTokenCount(input, options) + separator + "Out " + formatTokenCount(output, options);
|
|
281
|
+
}
|
|
282
|
+
function formatCount(value) {
|
|
283
|
+
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
284
|
+
return Math.round(value).toLocaleString("en-US");
|
|
285
|
+
}
|
|
286
|
+
function formatMoney(micros, options) {
|
|
287
|
+
options = options || {};
|
|
288
|
+
if (!Number.isFinite(micros)) return UNKNOWN_VALUE;
|
|
289
|
+
const amount = Math.abs(micros / 1000000);
|
|
290
|
+
const digits = options.digits != null ? options.digits : (options.ledger && amount < 0.01 ? 6 : 4);
|
|
291
|
+
const formatted = "$" + (micros / 1000000).toFixed(digits);
|
|
292
|
+
return options.signed && micros >= 0 ? "+" + formatted : formatted;
|
|
293
|
+
}
|
|
294
|
+
function formatMoneyPair(actualMicros, referenceMicros, options) {
|
|
295
|
+
options = options || {};
|
|
296
|
+
if (!Number.isFinite(actualMicros) && !Number.isFinite(referenceMicros)) return UNKNOWN_VALUE;
|
|
297
|
+
return formatMoney(actualMicros, options) + " / " + formatMoney(referenceMicros, options);
|
|
298
|
+
}
|
|
299
|
+
function formatDuration(valueMs) {
|
|
300
|
+
if (!Number.isFinite(valueMs)) return UNKNOWN_VALUE;
|
|
301
|
+
const ms = Math.max(0, Math.round(valueMs));
|
|
302
|
+
if (ms < 1000) return ms + "ms";
|
|
303
|
+
return (ms / 1000).toFixed(2) + "s";
|
|
304
|
+
}
|
|
305
|
+
function formatPercent(value) {
|
|
306
|
+
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
307
|
+
return Math.round(value * 100) + "%";
|
|
308
|
+
}
|
|
309
|
+
function formatDiscountRatio(discountRatio) {
|
|
310
|
+
if (!Number.isFinite(discountRatio)) return UNKNOWN_VALUE;
|
|
311
|
+
const discount = Math.max(0, 1 - discountRatio);
|
|
312
|
+
return formatPercent(discount);
|
|
313
|
+
}
|
|
314
|
+
function formatPriceMicrosPerMillion(value) { return formatMoney(value, { digits: 4 }); }
|
|
315
|
+
function formatPricePair(inputMicros, outputMicros) { return formatMoneyPair(inputMicros, outputMicros, { digits: 4 }); }
|
|
316
|
+
function normalizeStatusLabel(status) {
|
|
317
|
+
if (!status) return UNKNOWN_VALUE;
|
|
318
|
+
return String(status).trim().toLowerCase().replaceAll("_", " ");
|
|
319
|
+
}
|
|
320
|
+
function statusTone(status) {
|
|
321
|
+
const n = normalizeStatusLabel(status);
|
|
322
|
+
if (n === "ok" || n === "online" || n === "configured" || n === "settled" || n === "completed" || n === "success" || n === "active" || n === "healthy") return "green";
|
|
323
|
+
if (n === "fallback" || n === "pending" || n === "degraded" || n === "preview" || n === "draining" || n === "busy capacity" || n === "auth unknown") return "amber";
|
|
324
|
+
if (n === "failed" || n === "error" || n === "canceled" || n === "unhealthy" || n === "offline") return "red";
|
|
325
|
+
if (n === "running") return "blue";
|
|
326
|
+
return "gray";
|
|
327
|
+
}
|
|
328
|
+
function formatHash(value, length) {
|
|
329
|
+
if (length == null) length = 32;
|
|
330
|
+
if (!value) return UNKNOWN_VALUE;
|
|
331
|
+
return value.length > length ? value.slice(0, length) + "..." : value;
|
|
332
|
+
}
|
|
333
|
+
function formatSellerId(value) {
|
|
334
|
+
if (!value) return UNKNOWN_VALUE;
|
|
335
|
+
if (value.startsWith("tbs-") && value.length > 10) return value.slice(0, 10);
|
|
336
|
+
if (value.length <= 12) return value;
|
|
337
|
+
return value.slice(0, 12);
|
|
338
|
+
}
|
|
339
|
+
function formatTimeCompact(iso) {
|
|
340
|
+
if (!iso) return UNKNOWN_VALUE;
|
|
341
|
+
const d = new Date(iso);
|
|
342
|
+
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
343
|
+
const now = new Date();
|
|
344
|
+
const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
|
|
345
|
+
const time = PAD2(d.getHours()) + ":" + PAD2(d.getMinutes());
|
|
346
|
+
if (sameDay) return time;
|
|
347
|
+
return PAD2(d.getMonth() + 1) + "/" + PAD2(d.getDate()) + " " + time;
|
|
348
|
+
}
|
|
349
|
+
function formatTimeFull(iso) {
|
|
350
|
+
if (!iso) return UNKNOWN_VALUE;
|
|
351
|
+
const d = new Date(iso);
|
|
352
|
+
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
353
|
+
return d.getFullYear() + "-" + PAD2(d.getMonth() + 1) + "-" + PAD2(d.getDate()) + " " + PAD2(d.getHours()) + ":" + PAD2(d.getMinutes()) + ":" + PAD2(d.getSeconds());
|
|
354
|
+
}
|
|
355
|
+
function formatTimeLedger(iso) {
|
|
356
|
+
if (!iso) return UNKNOWN_VALUE;
|
|
357
|
+
const d = new Date(iso);
|
|
358
|
+
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
359
|
+
return d.getFullYear() + "/" + PAD2(d.getMonth() + 1) + "/" + PAD2(d.getDate()) + " " + PAD2(d.getHours()) + ":" + PAD2(d.getMinutes()) + ":" + PAD2(d.getSeconds());
|
|
360
|
+
}
|
|
361
|
+
function formatRouteSwitchTime(iso) { return formatTimeCompact(iso); }
|
|
362
|
+
function formatBalanceAmount(usdMicros, currency) {
|
|
363
|
+
if (!Number.isFinite(usdMicros)) return UNKNOWN_VALUE;
|
|
364
|
+
const amount = usdMicros / 1000000;
|
|
365
|
+
const digits = Math.abs(amount) >= 100 ? 0 : 2;
|
|
366
|
+
const code = (currency || "USD").toUpperCase();
|
|
367
|
+
return code + " " + amount.toFixed(digits);
|
|
368
|
+
}
|
|
369
|
+
function formatSellerCapacity(used, limit) {
|
|
370
|
+
if (!Number.isFinite(used) && !Number.isFinite(limit)) return UNKNOWN_VALUE;
|
|
371
|
+
const u = Number.isFinite(used) ? Math.round(used) : UNKNOWN_VALUE;
|
|
372
|
+
const l = Number.isFinite(limit) ? Math.round(limit) : UNKNOWN_VALUE;
|
|
373
|
+
return u + " / " + l;
|
|
374
|
+
}
|
|
375
|
+
function formatSpeed(value) {
|
|
376
|
+
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
377
|
+
return value.toFixed(1) + " tok/s";
|
|
378
|
+
}
|
|
379
|
+
const SELLER_STATUS_MAP = {
|
|
380
|
+
active: "ok", healthy: "ok", online: "online", configured: "configured",
|
|
381
|
+
pending: "pending", draining: "degraded", degraded: "degraded",
|
|
382
|
+
busy_capacity: "degraded", offline: "error", unhealthy: "error",
|
|
383
|
+
error: "error", auth_unknown: "unknown", unknown: "unknown"
|
|
384
|
+
};
|
|
385
|
+
function formatSellerStatus(status) {
|
|
386
|
+
const key = String(status || "unknown").trim().toLowerCase().replace(/-/g, "_");
|
|
387
|
+
return SELLER_STATUS_MAP[key] || "unknown";
|
|
388
|
+
}
|
|
389
|
+
function sellerStatusTone(status) { return statusTone(formatSellerStatus(status)); }
|
|
390
|
+
window.__tbFmt = {
|
|
391
|
+
UNKNOWN_VALUE, formatTokenCount, formatTokenPair, formatCount, formatMoney, formatMoneyPair,
|
|
392
|
+
formatDuration, formatPercent, formatDiscountRatio, formatPriceMicrosPerMillion, formatPricePair,
|
|
393
|
+
normalizeStatusLabel, statusTone, formatHash, formatSellerId, formatTimeCompact, formatTimeFull,
|
|
394
|
+
formatTimeLedger, formatRouteSwitchTime, formatBalanceAmount, formatSellerCapacity, formatSpeed,
|
|
395
|
+
formatSellerStatus, sellerStatusTone
|
|
396
|
+
};
|
|
397
|
+
})();`;
|
|
398
|
+
}
|
package/src/server-cmd.ts
CHANGED
|
@@ -141,6 +141,32 @@ export class FlyProvider {
|
|
|
141
141
|
return this.providerConfig?.flyctl_path || "flyctl";
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
private flyExecOptions(options: Parameters<typeof execSync>[1] = {}): Parameters<typeof execSync>[1] {
|
|
145
|
+
return {
|
|
146
|
+
...options,
|
|
147
|
+
env: this.flyEnv(options.env)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private flySpawnOptions(options: Parameters<typeof spawnSync>[2] = {}): Parameters<typeof spawnSync>[2] {
|
|
152
|
+
return {
|
|
153
|
+
...options,
|
|
154
|
+
env: this.flyEnv(options.env)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private flyEnv(env: NodeJS.ProcessEnv | undefined): NodeJS.ProcessEnv {
|
|
159
|
+
const configuredToken = this.providerConfig?.token;
|
|
160
|
+
const merged = {
|
|
161
|
+
...process.env,
|
|
162
|
+
...(env || {})
|
|
163
|
+
};
|
|
164
|
+
if (configuredToken && !merged.FLY_API_TOKEN) {
|
|
165
|
+
merged.FLY_API_TOKEN = configuredToken;
|
|
166
|
+
}
|
|
167
|
+
return merged;
|
|
168
|
+
}
|
|
169
|
+
|
|
144
170
|
/**
|
|
145
171
|
* List apps on Fly.io
|
|
146
172
|
*/
|
|
@@ -148,7 +174,7 @@ export class FlyProvider {
|
|
|
148
174
|
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
149
175
|
throw new Error(`\`${this.flyctl}\` is not installed on your system PATH.`);
|
|
150
176
|
}
|
|
151
|
-
return this.runtime.execSync(`${this.flyctl} apps list`, { encoding: "utf8" }) as string;
|
|
177
|
+
return this.runtime.execSync(`${this.flyctl} apps list`, this.flyExecOptions({ encoding: "utf8" })) as string;
|
|
152
178
|
}
|
|
153
179
|
|
|
154
180
|
/**
|
|
@@ -213,14 +239,14 @@ export class FlyProvider {
|
|
|
213
239
|
requirePublishedDockerImage(targetImage, this.runtime.imageInspector);
|
|
214
240
|
|
|
215
241
|
console.log(`[Fly.io] Creating app ${appName}...`);
|
|
216
|
-
this.runtime.execSync(`${this.flyctl} apps create ${appName} --machines`, { stdio: "inherit" });
|
|
242
|
+
this.runtime.execSync(`${this.flyctl} apps create ${appName} --machines`, this.flyExecOptions({ stdio: "inherit" }));
|
|
217
243
|
|
|
218
244
|
console.log(`[Fly.io] Setting secrets...`);
|
|
219
245
|
this.importCreateSecrets(appName, operatorSecret, initialConfigPath);
|
|
220
246
|
|
|
221
247
|
console.log(`[Fly.io] Deploying image ${targetImage}...`);
|
|
222
248
|
const deployCmd = `${this.flyctl} deploy -c ${flyConfig} --image ${targetImage} --primary-region ${targetRegion} --app ${appName} --now`;
|
|
223
|
-
this.runtime.execSync(deployCmd, { stdio: "inherit" });
|
|
249
|
+
this.runtime.execSync(deployCmd, this.flyExecOptions({ stdio: "inherit" }));
|
|
224
250
|
|
|
225
251
|
return `Successfully deployed ${appName} on Fly.io`;
|
|
226
252
|
}
|
|
@@ -234,10 +260,10 @@ export class FlyProvider {
|
|
|
234
260
|
const configContent = fs.readFileSync(initialConfigPath, "utf8");
|
|
235
261
|
lines.push(`TOKENBUDDY_SELLER_CONFIG_B64=${Buffer.from(configContent, "utf8").toString("base64")}`);
|
|
236
262
|
}
|
|
237
|
-
const result = this.runtime.spawnSync(this.flyctl, ["secrets", "import", "--stage", "--app", appName], {
|
|
263
|
+
const result = this.runtime.spawnSync(this.flyctl, ["secrets", "import", "--stage", "--app", appName], this.flySpawnOptions({
|
|
238
264
|
input: `${lines.join("\n")}\n`,
|
|
239
265
|
stdio: ["pipe", "inherit", "inherit"]
|
|
240
|
-
});
|
|
266
|
+
}));
|
|
241
267
|
if (result.error) {
|
|
242
268
|
throw result.error;
|
|
243
269
|
}
|
|
@@ -263,7 +289,7 @@ export class FlyProvider {
|
|
|
263
289
|
}
|
|
264
290
|
|
|
265
291
|
console.log(`[Fly.io] Destroying app ${appName}...`);
|
|
266
|
-
this.runtime.execSync(`${this.flyctl} apps destroy ${appName} --yes`, { stdio: "inherit" });
|
|
292
|
+
this.runtime.execSync(`${this.flyctl} apps destroy ${appName} --yes`, this.flyExecOptions({ stdio: "inherit" }));
|
|
267
293
|
|
|
268
294
|
return `Successfully destroyed ${appName} on Fly.io`;
|
|
269
295
|
}
|
|
@@ -276,7 +302,7 @@ export class FlyProvider {
|
|
|
276
302
|
if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
|
|
277
303
|
throw new Error(`\`${this.flyctl}\` is not installed.`);
|
|
278
304
|
}
|
|
279
|
-
return this.runtime.execSync(`${this.flyctl} status --app ${appName}`, { encoding: "utf8" }) as string;
|
|
305
|
+
return this.runtime.execSync(`${this.flyctl} status --app ${appName}`, this.flyExecOptions({ encoding: "utf8" })) as string;
|
|
280
306
|
}
|
|
281
307
|
|
|
282
308
|
/**
|
|
@@ -310,14 +336,14 @@ export class FlyProvider {
|
|
|
310
336
|
throw new Error(`\`${this.flyctl}\` is not installed.`);
|
|
311
337
|
}
|
|
312
338
|
|
|
313
|
-
const machinesJson = this.runtime.execSync(`${this.flyctl} machines list --app ${app} --json`, { encoding: "utf8" }) as string;
|
|
339
|
+
const machinesJson = this.runtime.execSync(`${this.flyctl} machines list --app ${app} --json`, this.flyExecOptions({ encoding: "utf8" })) as string;
|
|
314
340
|
const machineIds = parseFlyMachineIds(machinesJson, app);
|
|
315
341
|
|
|
316
342
|
console.log(`[Fly.io] Updating ${app} image on ${machineIds.length} machine(s)...`);
|
|
317
343
|
for (const machineId of machineIds) {
|
|
318
344
|
this.runtime.execSync(
|
|
319
345
|
`${this.flyctl} machine update ${machineId} --app ${app} --image ${targetImage} --yes`,
|
|
320
|
-
{ stdio: "inherit" }
|
|
346
|
+
this.flyExecOptions({ stdio: "inherit" })
|
|
321
347
|
);
|
|
322
348
|
}
|
|
323
349
|
return `Successfully updated ${app} image`;
|