@toon-protocol/townhouse 0.1.0-rc5 → 0.1.1

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.
@@ -0,0 +1,625 @@
1
+ import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
+ import {
3
+ formatRelativeTime,
4
+ formatUsdc,
5
+ formatUsdcMicro
6
+ } from "./chunk-JCOFMUPL.js";
7
+ import "./chunk-I2R4CRUX.js";
8
+
9
+ // src/tui/index.ts
10
+ import { createElement } from "react";
11
+ import { render } from "ink";
12
+
13
+ // src/tui/App.tsx
14
+ import { useState as useState4 } from "react";
15
+ import { Box as Box4, Text as Text10, useInput as useInput2 } from "ink";
16
+
17
+ // src/tui/use-earnings.ts
18
+ import { useEffect, useRef, useState } from "react";
19
+
20
+ // src/tui/constants.ts
21
+ var DEFAULT_REFRESH_INTERVAL_MS = 2e3;
22
+ var DEFAULT_API_URL = "http://127.0.0.1:28090";
23
+
24
+ // src/tui/use-earnings.ts
25
+ var EMPTY_EARNINGS = {
26
+ status: "connector_unavailable",
27
+ apex: { routingFees: {} },
28
+ peers: [],
29
+ recentClaims: [],
30
+ eventsRelayed: 0,
31
+ uptimeSeconds: 0
32
+ };
33
+ function useEarnings(opts = {}) {
34
+ const {
35
+ apiUrl = DEFAULT_API_URL,
36
+ refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS,
37
+ fetchImpl = globalThis.fetch
38
+ } = opts;
39
+ const [state, setState] = useState({
40
+ phase: "loading",
41
+ data: null,
42
+ bannerKey: null
43
+ });
44
+ const prevDataRef = useRef(null);
45
+ useEffect(() => {
46
+ let cancelled = false;
47
+ let abortController = null;
48
+ async function doFetch() {
49
+ if (cancelled) return;
50
+ const ac = new AbortController();
51
+ abortController = ac;
52
+ try {
53
+ const res = await fetchImpl(`${apiUrl}/api/earnings`, {
54
+ signal: ac.signal
55
+ });
56
+ if (cancelled) return;
57
+ if (!res.ok) {
58
+ const prev = prevDataRef.current;
59
+ setState({
60
+ phase: "stale",
61
+ data: prev ?? EMPTY_EARNINGS,
62
+ bannerKey: "fetch_failed"
63
+ });
64
+ return;
65
+ }
66
+ const body = await res.json();
67
+ if (cancelled) return;
68
+ if (body.status === "connector_unavailable") {
69
+ const prev = prevDataRef.current;
70
+ setState({
71
+ phase: "stale",
72
+ data: prev ?? EMPTY_EARNINGS,
73
+ bannerKey: "connector_unavailable"
74
+ });
75
+ return;
76
+ }
77
+ prevDataRef.current = body;
78
+ setState({ phase: "ok", data: body, bannerKey: null });
79
+ } catch (err) {
80
+ if (cancelled) return;
81
+ if (err instanceof Error && err.name === "AbortError") return;
82
+ const prev = prevDataRef.current;
83
+ setState({
84
+ phase: "stale",
85
+ data: prev ?? EMPTY_EARNINGS,
86
+ bannerKey: "fetch_failed"
87
+ });
88
+ } finally {
89
+ abortController = null;
90
+ }
91
+ }
92
+ void doFetch();
93
+ const intervalId = setInterval(() => {
94
+ void doFetch();
95
+ }, refreshIntervalMs);
96
+ return () => {
97
+ cancelled = true;
98
+ clearInterval(intervalId);
99
+ if (abortController !== null) {
100
+ abortController.abort();
101
+ }
102
+ };
103
+ }, [apiUrl, refreshIntervalMs, fetchImpl]);
104
+ return state;
105
+ }
106
+
107
+ // src/tui/use-activity-buffer.ts
108
+ import { useState as useState2, useEffect as useEffect2 } from "react";
109
+ var MAX_BUFFER_SIZE = 200;
110
+ function claimKey(c) {
111
+ return `${c.peerId}|${c.at}|${c.amount}|${c.assetCode}|${c.direction}`;
112
+ }
113
+ function sortKey(c) {
114
+ const ms = Date.parse(c.at);
115
+ return Number.isFinite(ms) ? ms : -Infinity;
116
+ }
117
+ function useActivityBuffer(incoming) {
118
+ const [buffer, setBuffer] = useState2([]);
119
+ useEffect2(() => {
120
+ if (!Array.isArray(incoming)) return;
121
+ if (incoming.length === 0 && buffer.length === 0) return;
122
+ const seen = /* @__PURE__ */ new Map();
123
+ for (const c of buffer) seen.set(claimKey(c), c);
124
+ for (const c of incoming) seen.set(claimKey(c), c);
125
+ const merged = Array.from(seen.values());
126
+ merged.sort((a, b) => sortKey(b) - sortKey(a));
127
+ const trimmed = merged.slice(0, MAX_BUFFER_SIZE);
128
+ const same = trimmed.length === buffer.length && trimmed.every(
129
+ (c, i) => buffer[i] !== void 0 && claimKey(c) === claimKey(buffer[i])
130
+ );
131
+ if (!same) setBuffer(trimmed);
132
+ }, [incoming]);
133
+ return buffer;
134
+ }
135
+
136
+ // src/tui/components/HeroBand.tsx
137
+ import { Box, Text as Text3, useStdout } from "ink";
138
+
139
+ // src/tui/components/Sparkline.tsx
140
+ import { Text } from "ink";
141
+ import { jsxs } from "react/jsx-runtime";
142
+ var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
143
+ var PLACEHOLDER = "\xB7\xB7\xB7\xB7\xB7\xB7\xB7";
144
+ function Sparkline({ values, width }) {
145
+ if (width < 60) {
146
+ return null;
147
+ }
148
+ if (values.length === 0) {
149
+ return /* @__PURE__ */ jsxs(Text, { children: [
150
+ PLACEHOLDER,
151
+ " 7d"
152
+ ] });
153
+ }
154
+ const safe = values.filter((v) => Number.isFinite(v)).map((v) => v < 0 ? 0 : v);
155
+ if (safe.length === 0) {
156
+ return /* @__PURE__ */ jsxs(Text, { children: [
157
+ PLACEHOLDER,
158
+ " 7d"
159
+ ] });
160
+ }
161
+ const max = safe.reduce((m, v) => v > m ? v : m, 0);
162
+ const chars = safe.map((v) => {
163
+ if (max === 0) return BLOCKS[0] ?? "\u2581";
164
+ const idx = Math.floor(v / max * (BLOCKS.length - 1));
165
+ return BLOCKS[idx] ?? "\u2581";
166
+ }).join("");
167
+ return /* @__PURE__ */ jsxs(Text, { children: [
168
+ chars,
169
+ " 7d"
170
+ ] });
171
+ }
172
+
173
+ // src/tui/components/Qualifier.tsx
174
+ import { Text as Text2 } from "ink";
175
+
176
+ // src/tui/copy.ts
177
+ var COPY = {
178
+ heroEarly: `you're early`,
179
+ heroEarlyRotation: [
180
+ `you're early`,
181
+ `warming up`,
182
+ `first packet en route`
183
+ ],
184
+ loading: `Fetching earnings\u2026`,
185
+ qualifierPrefix: `MONTH $0.00`,
186
+ qualifierEventsWords: `events relayed`,
187
+ qualifierEvents: (n) => `${n} events relayed`,
188
+ banners: {
189
+ connectorUnavailable: `Connector not reachable \u2014 showing last known values. Retrying in 2s.`,
190
+ fetchFailed: `Last refresh failed \u2014 retrying.`
191
+ },
192
+ apex: {
193
+ routingPrefix: `\u21B3 apex routing: `,
194
+ routingEmpty: `(enable mill to route)`
195
+ },
196
+ peerTable: {
197
+ empty: `no peers yet \u2014 run 'townhouse node add town'`
198
+ },
199
+ activityTicker: {
200
+ prefix: `recent: `,
201
+ empty: `no settlements yet \u2014 press [a] when activity arrives`,
202
+ keybind: ` [a] activity`
203
+ },
204
+ activityOverlay: {
205
+ titlePrefix: `Activity \u2014 last `,
206
+ emptyHint: `(no activity yet)`,
207
+ scrollHint: `j/k to scroll \xB7 q to close`,
208
+ scrollHintEmpty: `q to close`,
209
+ directionInbound: `in`,
210
+ directionOutbound: `out`,
211
+ directionUnknown: `?`
212
+ }
213
+ };
214
+
215
+ // src/tui/components/Qualifier.tsx
216
+ import { jsxs as jsxs2 } from "react/jsx-runtime";
217
+ function Qualifier({ eventsRelayed }) {
218
+ return /* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
219
+ COPY.qualifierPrefix,
220
+ " \xB7 ",
221
+ COPY.qualifierEvents(eventsRelayed),
222
+ " \xB7 ",
223
+ COPY.heroEarly
224
+ ] });
225
+ }
226
+
227
+ // src/tui/components/HeroBand.tsx
228
+ import { jsx, jsxs as jsxs3 } from "react/jsx-runtime";
229
+ var USDC_SCALE = 6;
230
+ var ASSET = "USDC";
231
+ var DECIMAL_RE = /^-?\d+$/;
232
+ var MIN_COL_WIDTH = 8;
233
+ function addDecimalStrings(a, b) {
234
+ if (!DECIMAL_RE.test(b)) return a;
235
+ try {
236
+ return (BigInt(a) + BigInt(b)).toString();
237
+ } catch {
238
+ return a;
239
+ }
240
+ }
241
+ function computeScalars(apex, peers) {
242
+ let today = "0";
243
+ let month = "0";
244
+ let year = "0";
245
+ let lifetime = "0";
246
+ const apexUsdc = apex.routingFees[ASSET];
247
+ if (apexUsdc !== void 0) {
248
+ today = addDecimalStrings(today, apexUsdc.today);
249
+ month = addDecimalStrings(month, apexUsdc.month);
250
+ year = addDecimalStrings(year, apexUsdc.year);
251
+ lifetime = addDecimalStrings(lifetime, apexUsdc.lifetime);
252
+ }
253
+ for (const peer of peers) {
254
+ const peerUsdc = peer.byAsset[ASSET];
255
+ if (peerUsdc !== void 0) {
256
+ today = addDecimalStrings(today, peerUsdc.today);
257
+ month = addDecimalStrings(month, peerUsdc.month);
258
+ year = addDecimalStrings(year, peerUsdc.year);
259
+ lifetime = addDecimalStrings(lifetime, peerUsdc.lifetime);
260
+ }
261
+ }
262
+ return { today, month, year, lifetime };
263
+ }
264
+ function isEmptyState(apex, peers) {
265
+ const apexMonth = apex.routingFees[ASSET]?.month ?? "0";
266
+ if (apexMonth !== "0") return false;
267
+ for (const peer of peers) {
268
+ const peerMonth = peer.byAsset[ASSET]?.month ?? "0";
269
+ if (peerMonth !== "0") return false;
270
+ }
271
+ return true;
272
+ }
273
+ function HeroBand({ apex, peers, eventsRelayed }) {
274
+ const { stdout } = useStdout();
275
+ const columns = stdout?.columns ?? 80;
276
+ const scalars = computeScalars(apex, peers);
277
+ const showQualifier = isEmptyState(apex, peers);
278
+ const todayFmt = formatUsdc(scalars.today, USDC_SCALE);
279
+ const monthFmt = formatUsdc(scalars.month, USDC_SCALE);
280
+ const yearFmt = formatUsdc(scalars.year, USDC_SCALE);
281
+ const lifetimeFmt = formatUsdc(scalars.lifetime, USDC_SCALE);
282
+ const shortLabels = columns < 70;
283
+ const labelLifetime = shortLabels ? "LIFE" : "LIFETIME";
284
+ const colWidth = Math.max(Math.floor(columns / 4), MIN_COL_WIDTH);
285
+ return /* @__PURE__ */ jsxs3(Box, { flexDirection: "column", children: [
286
+ /* @__PURE__ */ jsxs3(Box, { children: [
287
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "TODAY" }) }),
288
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "MONTH" }) }),
289
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "YEAR" }) }),
290
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: labelLifetime }) })
291
+ ] }),
292
+ /* @__PURE__ */ jsxs3(Box, { children: [
293
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { color: scalars.today !== "0" ? "green" : void 0, children: todayFmt }) }),
294
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { color: scalars.month !== "0" ? "green" : void 0, children: monthFmt }) }),
295
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { color: scalars.year !== "0" ? "green" : void 0, children: yearFmt }) }),
296
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { color: scalars.lifetime !== "0" ? "green" : void 0, children: lifetimeFmt }) })
297
+ ] }),
298
+ /* @__PURE__ */ jsx(Sparkline, { values: [], width: columns }),
299
+ showQualifier ? /* @__PURE__ */ jsx(Qualifier, { eventsRelayed }) : null
300
+ ] });
301
+ }
302
+
303
+ // src/tui/components/Banner.tsx
304
+ import { Text as Text4 } from "ink";
305
+ import { jsx as jsx2 } from "react/jsx-runtime";
306
+ function Banner({ bannerKey }) {
307
+ if (bannerKey === null) return null;
308
+ const isError = bannerKey === "fetch_failed";
309
+ const text = isError ? COPY.banners.fetchFailed : COPY.banners.connectorUnavailable;
310
+ return /* @__PURE__ */ jsx2(Text4, { color: isError ? "red" : "yellow", children: text });
311
+ }
312
+
313
+ // src/tui/components/ApexStrip.tsx
314
+ import { Text as Text5 } from "ink";
315
+ import { jsxs as jsxs4 } from "react/jsx-runtime";
316
+ var USDC_SCALE2 = 6;
317
+ var ASSET2 = "USDC";
318
+ var DECIMAL_RE2 = /^-?\d+$/;
319
+ function addDecimalStrings2(a, b) {
320
+ if (!DECIMAL_RE2.test(b)) return a;
321
+ try {
322
+ return (BigInt(a) + BigInt(b)).toString();
323
+ } catch {
324
+ return a;
325
+ }
326
+ }
327
+ function ApexStrip({ apex, peers }) {
328
+ const apexMonth = apex.routingFees[ASSET2]?.month ?? "0";
329
+ const apexValid = DECIMAL_RE2.test(apexMonth);
330
+ const apexMonthBig = apexValid ? BigInt(apexMonth) : 0n;
331
+ let totalMonth = apexMonthBig;
332
+ for (const peer of peers) {
333
+ const peerMonth = peer.byAsset[ASSET2]?.month ?? "0";
334
+ totalMonth = BigInt(addDecimalStrings2(totalMonth.toString(), peerMonth));
335
+ }
336
+ const apexFmt = formatUsdc(apexMonth, USDC_SCALE2);
337
+ const hasMillPeer = peers.some((p) => p.type === "mill");
338
+ if (!apexValid) {
339
+ return /* @__PURE__ */ jsxs4(Text5, { dimColor: true, italic: true, children: [
340
+ COPY.apex.routingPrefix,
341
+ apexFmt
342
+ ] });
343
+ }
344
+ if (apexMonthBig === 0n) {
345
+ const upsell = hasMillPeer ? "" : ` ${COPY.apex.routingEmpty}`;
346
+ return /* @__PURE__ */ jsxs4(Text5, { dimColor: true, italic: true, children: [
347
+ COPY.apex.routingPrefix,
348
+ apexFmt,
349
+ upsell
350
+ ] });
351
+ }
352
+ const pct = totalMonth === 0n ? null : Number(apexMonthBig * 100n / totalMonth);
353
+ return /* @__PURE__ */ jsxs4(Text5, { children: [
354
+ COPY.apex.routingPrefix,
355
+ apexFmt,
356
+ pct !== null ? ` (${pct}%)` : ""
357
+ ] });
358
+ }
359
+
360
+ // src/tui/components/PeerTable.tsx
361
+ import { Box as Box2, Text as Text6, useStdout as useStdout2 } from "ink";
362
+ import { jsx as jsx3, jsxs as jsxs5 } from "react/jsx-runtime";
363
+ var USDC_SCALE3 = 6;
364
+ var MAX_DATA_ROWS = 4;
365
+ var MIN_COL_WIDTH2 = 6;
366
+ function flattenPeers(peers) {
367
+ const out = [];
368
+ for (const peer of peers) {
369
+ const assetCodes = Object.keys(peer.byAsset).sort();
370
+ if (assetCodes.length === 0) continue;
371
+ let isFirst = true;
372
+ for (const assetCode of assetCodes) {
373
+ const perAsset = peer.byAsset[assetCode];
374
+ if (perAsset === void 0) continue;
375
+ out.push({
376
+ peerId: peer.id,
377
+ type: peer.type,
378
+ assetCode,
379
+ perAsset,
380
+ lastClaimAt: peer.lastClaimAt,
381
+ isFirstRowOfPeer: isFirst
382
+ });
383
+ isFirst = false;
384
+ }
385
+ }
386
+ return out;
387
+ }
388
+ function PeerTable({ peers, now = /* @__PURE__ */ new Date(), columns: columnsProp }) {
389
+ const { stdout } = useStdout2();
390
+ const columns = columnsProp ?? (stdout?.columns || 80);
391
+ const rows = flattenPeers(peers).slice(0, MAX_DATA_ROWS);
392
+ if (rows.length === 0) {
393
+ return /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: COPY.peerTable.empty });
394
+ }
395
+ const showLastClaim = columns >= 60;
396
+ const shortType = columns < 70;
397
+ const dropAgoSuffix = columns < 70;
398
+ const totalCols = showLastClaim ? 5 : 4;
399
+ const colWidth = Math.max(Math.floor(columns / totalCols), MIN_COL_WIDTH2);
400
+ const header = /* @__PURE__ */ jsxs5(Box2, { children: [
401
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "PEER" }) }),
402
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "TYPE" }) }),
403
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "ASSET" }) }),
404
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "NET (MONTH)" }) }),
405
+ showLastClaim ? /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "LAST CLAIM" }) }) : null
406
+ ] });
407
+ return /* @__PURE__ */ jsxs5(Box2, { flexDirection: "column", children: [
408
+ header,
409
+ rows.map((row, i) => {
410
+ const peerCell = row.isFirstRowOfPeer ? row.peerId : "";
411
+ const typeRaw = row.isFirstRowOfPeer ? row.type : "";
412
+ const typeCell = shortType && typeRaw.length > 0 ? typeRaw.slice(0, 3) : typeRaw;
413
+ const netFmt = formatUsdc(row.perAsset.month, USDC_SCALE3);
414
+ let lastClaim = formatRelativeTime(row.lastClaimAt, now);
415
+ if (dropAgoSuffix && lastClaim.endsWith(" ago")) {
416
+ lastClaim = lastClaim.slice(0, -" ago".length);
417
+ }
418
+ return /* @__PURE__ */ jsxs5(Box2, { children: [
419
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: peerCell }) }),
420
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: typeCell }) }),
421
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: row.assetCode }) }),
422
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: netFmt }) }),
423
+ showLastClaim ? /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: lastClaim }) }) : null
424
+ ] }, `${row.peerId}-${row.assetCode}-${i}`);
425
+ })
426
+ ] });
427
+ }
428
+
429
+ // src/tui/components/ActivityTicker.tsx
430
+ import { Text as Text7 } from "ink";
431
+ import { jsx as jsx4, jsxs as jsxs6 } from "react/jsx-runtime";
432
+ function sortKey2(c) {
433
+ const ms = Date.parse(c.at);
434
+ return Number.isFinite(ms) ? ms : -Infinity;
435
+ }
436
+ function arrowFor(direction) {
437
+ return direction === "inbound" ? "\u2190" : direction === "outbound" ? "\u2192" : COPY.activityOverlay.directionUnknown;
438
+ }
439
+ function ActivityTicker({ recentClaims, now = /* @__PURE__ */ new Date() }) {
440
+ if (recentClaims.length === 0) {
441
+ return /* @__PURE__ */ jsx4(Text7, { dimColor: true, children: COPY.activityTicker.empty });
442
+ }
443
+ const sorted = [...recentClaims].sort((a, b) => sortKey2(b) - sortKey2(a));
444
+ const claim = sorted[0];
445
+ if (!claim) {
446
+ return /* @__PURE__ */ jsx4(Text7, { dimColor: true, children: COPY.activityTicker.empty });
447
+ }
448
+ const arrow = arrowFor(claim.direction);
449
+ const amount = formatUsdcMicro(claim.amount, claim.assetScale);
450
+ const rel = formatRelativeTime(claim.at, now);
451
+ return /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
452
+ COPY.activityTicker.prefix,
453
+ claim.peerId,
454
+ " ",
455
+ arrow,
456
+ " ",
457
+ amount,
458
+ " ",
459
+ claim.assetCode,
460
+ " \xB7 ",
461
+ rel,
462
+ COPY.activityTicker.keybind
463
+ ] });
464
+ }
465
+
466
+ // src/tui/components/Badge.tsx
467
+ import { Text as Text8 } from "ink";
468
+ import { jsx as jsx5 } from "react/jsx-runtime";
469
+ var USDC_ASSET = "USDC";
470
+ var DECIMAL_RE3 = /^-?\d+$/;
471
+ var LIFETIME_USDC_THRESHOLD = 1000000n;
472
+ var UPTIME_SECONDS_THRESHOLD = 7 * 24 * 60 * 60;
473
+ var ROTATION_INTERVAL_MS = 3e4;
474
+ function parseDecimalOrZero(value) {
475
+ if (value === void 0 || !DECIMAL_RE3.test(value)) return 0n;
476
+ try {
477
+ return BigInt(value);
478
+ } catch {
479
+ return 0n;
480
+ }
481
+ }
482
+ function computeLifetimeUsdc(apex, peers) {
483
+ let total = parseDecimalOrZero(apex.routingFees[USDC_ASSET]?.lifetime);
484
+ for (const peer of peers) {
485
+ total += parseDecimalOrZero(peer.byAsset[USDC_ASSET]?.lifetime);
486
+ }
487
+ return total;
488
+ }
489
+ function Badge({
490
+ apex,
491
+ peers,
492
+ uptimeSeconds,
493
+ now = /* @__PURE__ */ new Date()
494
+ }) {
495
+ const lifetime = computeLifetimeUsdc(apex, peers);
496
+ const lifetimeTriggers = lifetime < LIFETIME_USDC_THRESHOLD;
497
+ const uptimeTriggers = uptimeSeconds < UPTIME_SECONDS_THRESHOLD;
498
+ if (!lifetimeTriggers && !uptimeTriggers) return null;
499
+ const index = Math.floor(now.getTime() / ROTATION_INTERVAL_MS) % COPY.heroEarlyRotation.length;
500
+ const text = COPY.heroEarlyRotation[index] ?? COPY.heroEarlyRotation[0];
501
+ return /* @__PURE__ */ jsx5(Text8, { color: "yellow", bold: true, children: text });
502
+ }
503
+
504
+ // src/tui/components/ActivityOverlay.tsx
505
+ import { Box as Box3, Text as Text9, useStdout as useStdout3, useInput } from "ink";
506
+ import { useEffect as useEffect3, useState as useState3 } from "react";
507
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
508
+ var MIN_OVERLAY_WIDTH = 40;
509
+ var MAX_PEER_ID_WIDTH = 24;
510
+ var DEFAULT_MAX_BUFFER_SIZE = 200;
511
+ function formatTime(iso) {
512
+ const d = new Date(iso);
513
+ if (Number.isNaN(d.getTime())) return "--:--:--";
514
+ return d.toLocaleTimeString("en-GB", { hour12: false });
515
+ }
516
+ function truncatePeerId(id) {
517
+ if (id.length <= MAX_PEER_ID_WIDTH) return id;
518
+ return id.slice(0, MAX_PEER_ID_WIDTH - 1) + "\u2026";
519
+ }
520
+ function arrowFor2(direction) {
521
+ return direction === "inbound" ? "\u2190" : direction === "outbound" ? "\u2192" : COPY.activityOverlay.directionUnknown;
522
+ }
523
+ function directionLabel(direction) {
524
+ return direction === "inbound" ? COPY.activityOverlay.directionInbound : direction === "outbound" ? COPY.activityOverlay.directionOutbound : COPY.activityOverlay.directionUnknown;
525
+ }
526
+ function formatRow(claim) {
527
+ const time = formatTime(claim.at);
528
+ const peer = truncatePeerId(claim.peerId);
529
+ const arrow = arrowFor2(claim.direction);
530
+ const amount = formatUsdcMicro(claim.amount, claim.assetScale);
531
+ const dir = directionLabel(claim.direction);
532
+ return `${time} \xB7 ${peer} \xB7 ${arrow} ${amount} ${claim.assetCode} \xB7 ${dir}`;
533
+ }
534
+ function claimKeyForReact(c) {
535
+ return `${c.peerId}|${c.at}|${c.amount}|${c.assetCode}|${c.direction}`;
536
+ }
537
+ function ActivityOverlay({
538
+ claims,
539
+ onClose,
540
+ columns: columnsProp,
541
+ rows: rowsProp,
542
+ maxBufferSize = DEFAULT_MAX_BUFFER_SIZE
543
+ }) {
544
+ const { stdout } = useStdout3();
545
+ const columns = columnsProp ?? (stdout?.columns || 80);
546
+ const rows = rowsProp ?? (stdout?.rows || 24);
547
+ const modalWidth = Math.max(MIN_OVERLAY_WIDTH, Math.floor(columns * 0.7));
548
+ const visibleRows = Math.max(5, rows - 5);
549
+ const [scroll, setScroll] = useState3(0);
550
+ const maxScroll = Math.max(0, claims.length - visibleRows);
551
+ useEffect3(() => {
552
+ if (scroll > maxScroll) setScroll(maxScroll);
553
+ }, [maxScroll, scroll]);
554
+ useInput((input, key) => {
555
+ if (key.escape) {
556
+ onClose();
557
+ return;
558
+ }
559
+ if (key.ctrl || key.meta) return;
560
+ if (input === "q" || input === "Q") {
561
+ onClose();
562
+ return;
563
+ }
564
+ if (input === "j" || key.downArrow) {
565
+ setScroll((s) => Math.min(maxScroll, s + 1));
566
+ return;
567
+ }
568
+ if (input === "k" || key.upArrow) {
569
+ setScroll((s) => Math.max(0, s - 1));
570
+ }
571
+ });
572
+ const displayedCount = Math.min(claims.length, maxBufferSize);
573
+ const title = `${COPY.activityOverlay.titlePrefix}${displayedCount} of ${maxBufferSize}`;
574
+ const window = claims.slice(scroll, scroll + visibleRows);
575
+ const hint = claims.length === 0 ? COPY.activityOverlay.scrollHintEmpty : COPY.activityOverlay.scrollHint;
576
+ return /* @__PURE__ */ jsx6(Box3, { flexDirection: "column", alignItems: "center", width: columns, children: /* @__PURE__ */ jsxs7(Box3, { flexDirection: "column", borderStyle: "round", width: modalWidth, paddingX: 1, children: [
577
+ /* @__PURE__ */ jsx6(Text9, { bold: true, children: title }),
578
+ claims.length === 0 ? /* @__PURE__ */ jsx6(Text9, { dimColor: true, children: COPY.activityOverlay.emptyHint }) : window.map((c, i) => /* @__PURE__ */ jsx6(Text9, { children: formatRow(c) }, `${claimKeyForReact(c)}-${scroll + i}`)),
579
+ /* @__PURE__ */ jsx6(Text9, { dimColor: true, children: hint })
580
+ ] }) });
581
+ }
582
+
583
+ // src/tui/App.tsx
584
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
585
+ function App(props) {
586
+ const state = useEarnings(props);
587
+ const recentClaims = state.phase !== "loading" ? state.data.recentClaims : void 0;
588
+ const buffer = useActivityBuffer(recentClaims);
589
+ const [overlayOpen, setOverlayOpen] = useState4(false);
590
+ useInput2(
591
+ (input, key) => {
592
+ if (key.ctrl || key.meta) return;
593
+ if (input === "a" || input === "A") setOverlayOpen(true);
594
+ },
595
+ { isActive: !overlayOpen && state.phase !== "loading" }
596
+ );
597
+ if (state.phase === "loading") {
598
+ return /* @__PURE__ */ jsx7(Text10, { children: COPY.loading });
599
+ }
600
+ if (overlayOpen) {
601
+ return /* @__PURE__ */ jsx7(ActivityOverlay, { claims: buffer, onClose: () => setOverlayOpen(false), maxBufferSize: MAX_BUFFER_SIZE });
602
+ }
603
+ const { data } = state;
604
+ const bannerKey = state.phase === "stale" ? state.bannerKey : null;
605
+ return /* @__PURE__ */ jsxs8(Box4, { flexDirection: "column", children: [
606
+ /* @__PURE__ */ jsx7(HeroBand, { apex: data.apex, peers: data.peers, eventsRelayed: data.eventsRelayed }),
607
+ /* @__PURE__ */ jsx7(Badge, { apex: data.apex, peers: data.peers, uptimeSeconds: data.uptimeSeconds }),
608
+ /* @__PURE__ */ jsx7(Banner, { bannerKey }),
609
+ /* @__PURE__ */ jsx7(ApexStrip, { apex: data.apex, peers: data.peers }),
610
+ /* @__PURE__ */ jsx7(PeerTable, { peers: data.peers }),
611
+ /* @__PURE__ */ jsx7(ActivityTicker, { recentClaims: data.recentClaims })
612
+ ] });
613
+ }
614
+
615
+ // src/tui/index.ts
616
+ function mountTui(opts = {}) {
617
+ return render(createElement(App, opts), {
618
+ exitOnCtrlC: true,
619
+ patchConsole: false
620
+ });
621
+ }
622
+ export {
623
+ mountTui
624
+ };
625
+ //# sourceMappingURL=tui-OIFXGBTL.js.map