tokmon 0.9.2 → 0.11.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 +1 -0
- package/dist/cli.js +287 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ Then just run `tokmon`. Press `q` to quit.
|
|
|
73
73
|
- **Today / This Week / This Month** — cost and token summaries
|
|
74
74
|
- **Burn rate** — current $/hr
|
|
75
75
|
- **Rate Limits** — real-time session (5h), weekly (7d), and Sonnet utilization with reset countdowns, fetched from Anthropic's OAuth API
|
|
76
|
+
- **Peak / Off-Peak badge** — shown in the header, fetched from [promoclock.co](https://promoclock.co) (peak hours drain session limits faster)
|
|
76
77
|
|
|
77
78
|
### Table
|
|
78
79
|
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { MouseProvider } from "@zenobius/ink-mouse";
|
|
|
8
8
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
9
9
|
import { join } from "path";
|
|
10
10
|
import { homedir } from "os";
|
|
11
|
-
var DEFAULTS = { interval: 2, billingInterval: 5, clearScreen: true };
|
|
11
|
+
var DEFAULTS = { interval: 2, billingInterval: 5, clearScreen: true, timezone: null };
|
|
12
12
|
function configDir() {
|
|
13
13
|
if (process.platform === "win32") {
|
|
14
14
|
return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "tokmon");
|
|
@@ -45,6 +45,117 @@ import { createReadStream } from "fs";
|
|
|
45
45
|
import { createInterface } from "readline";
|
|
46
46
|
import { join as join2 } from "path";
|
|
47
47
|
import { homedir as homedir2 } from "os";
|
|
48
|
+
|
|
49
|
+
// src/tz.ts
|
|
50
|
+
function systemTimezone() {
|
|
51
|
+
try {
|
|
52
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
53
|
+
} catch {
|
|
54
|
+
return "UTC";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function isValidTimezone(tz) {
|
|
58
|
+
try {
|
|
59
|
+
new Intl.DateTimeFormat("en-CA", { timeZone: tz });
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function resolveTimezone(cfg) {
|
|
66
|
+
if (!cfg) return systemTimezone();
|
|
67
|
+
return isValidTimezone(cfg) ? cfg : systemTimezone();
|
|
68
|
+
}
|
|
69
|
+
var dayFmtCache = /* @__PURE__ */ new Map();
|
|
70
|
+
function dayFmt(tz) {
|
|
71
|
+
let f = dayFmtCache.get(tz);
|
|
72
|
+
if (!f) {
|
|
73
|
+
f = new Intl.DateTimeFormat("en-CA", {
|
|
74
|
+
timeZone: tz,
|
|
75
|
+
year: "numeric",
|
|
76
|
+
month: "2-digit",
|
|
77
|
+
day: "2-digit"
|
|
78
|
+
});
|
|
79
|
+
dayFmtCache.set(tz, f);
|
|
80
|
+
}
|
|
81
|
+
return f;
|
|
82
|
+
}
|
|
83
|
+
function dayKey(ts, tz) {
|
|
84
|
+
return dayFmt(tz).format(new Date(ts));
|
|
85
|
+
}
|
|
86
|
+
function monthKey(ts, tz) {
|
|
87
|
+
return dayKey(ts, tz).slice(0, 7);
|
|
88
|
+
}
|
|
89
|
+
var partsFmtCache = /* @__PURE__ */ new Map();
|
|
90
|
+
function partsFmt(tz) {
|
|
91
|
+
let f = partsFmtCache.get(tz);
|
|
92
|
+
if (!f) {
|
|
93
|
+
f = new Intl.DateTimeFormat("en-US", {
|
|
94
|
+
timeZone: tz,
|
|
95
|
+
hourCycle: "h23",
|
|
96
|
+
year: "numeric",
|
|
97
|
+
month: "2-digit",
|
|
98
|
+
day: "2-digit",
|
|
99
|
+
hour: "2-digit",
|
|
100
|
+
minute: "2-digit",
|
|
101
|
+
second: "2-digit",
|
|
102
|
+
weekday: "short"
|
|
103
|
+
});
|
|
104
|
+
partsFmtCache.set(tz, f);
|
|
105
|
+
}
|
|
106
|
+
return f;
|
|
107
|
+
}
|
|
108
|
+
var WEEKDAY_MAP = {
|
|
109
|
+
Sun: 0,
|
|
110
|
+
Mon: 1,
|
|
111
|
+
Tue: 2,
|
|
112
|
+
Wed: 3,
|
|
113
|
+
Thu: 4,
|
|
114
|
+
Fri: 5,
|
|
115
|
+
Sat: 6
|
|
116
|
+
};
|
|
117
|
+
function tzParts(ts, tz) {
|
|
118
|
+
const parts = partsFmt(tz).formatToParts(new Date(ts));
|
|
119
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
120
|
+
return {
|
|
121
|
+
y: Number(get("year")),
|
|
122
|
+
m: Number(get("month")),
|
|
123
|
+
d: Number(get("day")),
|
|
124
|
+
hh: Number(get("hour")),
|
|
125
|
+
mm: Number(get("minute")),
|
|
126
|
+
ss: Number(get("second")),
|
|
127
|
+
weekday: WEEKDAY_MAP[get("weekday")] ?? 0
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function instantFromTz(y, m, d, hh, mm, ss, tz) {
|
|
131
|
+
const guess = Date.UTC(y, m - 1, d, hh, mm, ss);
|
|
132
|
+
const r = tzParts(guess, tz);
|
|
133
|
+
const rendered = Date.UTC(r.y, r.m - 1, r.d, r.hh, r.mm, r.ss);
|
|
134
|
+
const offset = rendered - guess;
|
|
135
|
+
return guess - offset;
|
|
136
|
+
}
|
|
137
|
+
function startOfDay(ts, tz) {
|
|
138
|
+
const p = tzParts(ts, tz);
|
|
139
|
+
return instantFromTz(p.y, p.m, p.d, 0, 0, 0, tz);
|
|
140
|
+
}
|
|
141
|
+
function startOfMonth(ts, tz) {
|
|
142
|
+
const p = tzParts(ts, tz);
|
|
143
|
+
return instantFromTz(p.y, p.m, 1, 0, 0, 0, tz);
|
|
144
|
+
}
|
|
145
|
+
function startOfWeek(ts, tz) {
|
|
146
|
+
const p = tzParts(ts, tz);
|
|
147
|
+
const offset = p.weekday === 0 ? 6 : p.weekday - 1;
|
|
148
|
+
return instantFromTz(p.y, p.m, p.d - offset, 0, 0, 0, tz);
|
|
149
|
+
}
|
|
150
|
+
function monthsAgoStart(ts, months, tz) {
|
|
151
|
+
const p = tzParts(ts, tz);
|
|
152
|
+
return instantFromTz(p.y, p.m - months, 1, 0, 0, 0, tz);
|
|
153
|
+
}
|
|
154
|
+
function weekKey(ts, tz) {
|
|
155
|
+
return dayKey(startOfWeek(ts, tz), tz);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/data.ts
|
|
48
159
|
var PRICING = {
|
|
49
160
|
"claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
|
|
50
161
|
"claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
|
|
@@ -207,24 +318,11 @@ function groupBy(entries, keyFn) {
|
|
|
207
318
|
}
|
|
208
319
|
return rows.sort((a, b) => a.label.localeCompare(b.label));
|
|
209
320
|
}
|
|
210
|
-
function
|
|
211
|
-
const d = new Date(ts);
|
|
212
|
-
const day = d.getDay();
|
|
213
|
-
const mondayOffset = day === 0 ? 6 : day - 1;
|
|
214
|
-
const monday = new Date(d);
|
|
215
|
-
monday.setDate(d.getDate() - mondayOffset);
|
|
216
|
-
return monday.toISOString().slice(0, 10);
|
|
217
|
-
}
|
|
218
|
-
function monthLabel(ts) {
|
|
219
|
-
return new Date(ts).toISOString().slice(0, 7);
|
|
220
|
-
}
|
|
221
|
-
async function fetchDashboard() {
|
|
222
|
-
const d = /* @__PURE__ */ new Date();
|
|
223
|
-
const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
|
|
224
|
-
const todayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
225
|
-
const weekDay = d.getDay();
|
|
226
|
-
const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (weekDay === 0 ? 6 : weekDay - 1)).getTime();
|
|
321
|
+
async function fetchDashboard(tz) {
|
|
227
322
|
const now = Date.now();
|
|
323
|
+
const monthStart = startOfMonth(now, tz);
|
|
324
|
+
const todayStart = startOfDay(now, tz);
|
|
325
|
+
const weekStart = startOfWeek(now, tz);
|
|
228
326
|
const entries = await loadEntries(monthStart);
|
|
229
327
|
const todayEntries = entries.filter((e) => e.ts >= todayStart);
|
|
230
328
|
let burnRate = 0;
|
|
@@ -245,14 +343,13 @@ async function fetchDashboard() {
|
|
|
245
343
|
burnRate
|
|
246
344
|
};
|
|
247
345
|
}
|
|
248
|
-
async function fetchTable() {
|
|
249
|
-
const
|
|
250
|
-
const lookback = new Date(d.getFullYear(), d.getMonth() - 6, 1).getTime();
|
|
346
|
+
async function fetchTable(tz) {
|
|
347
|
+
const lookback = monthsAgoStart(Date.now(), 6, tz);
|
|
251
348
|
const entries = await loadEntries(lookback);
|
|
252
349
|
return {
|
|
253
|
-
daily: groupBy(entries, (e) =>
|
|
254
|
-
weekly: groupBy(entries, (e) =>
|
|
255
|
-
monthly: groupBy(entries, (e) =>
|
|
350
|
+
daily: groupBy(entries, (e) => dayKey(e.ts, tz)),
|
|
351
|
+
weekly: groupBy(entries, (e) => weekKey(e.ts, tz)),
|
|
352
|
+
monthly: groupBy(entries, (e) => monthKey(e.ts, tz))
|
|
256
353
|
};
|
|
257
354
|
}
|
|
258
355
|
|
|
@@ -297,10 +394,18 @@ async function getAccessToken() {
|
|
|
297
394
|
}
|
|
298
395
|
return readCredentialsFile();
|
|
299
396
|
}
|
|
300
|
-
var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, error: null };
|
|
397
|
+
var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, peak: null, error: null };
|
|
301
398
|
async function fetchBilling() {
|
|
302
399
|
const token = await getAccessToken();
|
|
303
400
|
if (!token) return { ...EMPTY, error: "No OAuth token \u2014 run claude and log in" };
|
|
401
|
+
const [usageRes, peak] = await Promise.all([
|
|
402
|
+
fetchUsage(token),
|
|
403
|
+
fetchPeakStatus()
|
|
404
|
+
]);
|
|
405
|
+
if ("error" in usageRes) return { ...EMPTY, peak, error: usageRes.error };
|
|
406
|
+
return { ...usageRes.data, peak, error: null };
|
|
407
|
+
}
|
|
408
|
+
async function fetchUsage(token) {
|
|
304
409
|
try {
|
|
305
410
|
const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
306
411
|
headers: {
|
|
@@ -310,31 +415,54 @@ async function fetchBilling() {
|
|
|
310
415
|
},
|
|
311
416
|
signal: AbortSignal.timeout(1e4)
|
|
312
417
|
});
|
|
313
|
-
if (res.status === 429) return {
|
|
314
|
-
if (res.status === 401) return {
|
|
315
|
-
if (!res.ok) return {
|
|
418
|
+
if (res.status === 429) return { error: "Rate limited \u2014 retrying next poll" };
|
|
419
|
+
if (res.status === 401) return { error: "Token expired \u2014 restart Claude Code" };
|
|
420
|
+
if (!res.ok) return { error: `API ${res.status}` };
|
|
316
421
|
const data = await res.json();
|
|
317
422
|
return {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
423
|
+
data: {
|
|
424
|
+
session: data.five_hour ? {
|
|
425
|
+
utilization: data.five_hour.utilization,
|
|
426
|
+
resetsAt: formatReset(data.five_hour.resets_at)
|
|
427
|
+
} : null,
|
|
428
|
+
weekly: data.seven_day ? {
|
|
429
|
+
utilization: data.seven_day.utilization,
|
|
430
|
+
resetsAt: formatReset(data.seven_day.resets_at)
|
|
431
|
+
} : null,
|
|
432
|
+
sonnet: data.seven_day_sonnet ? {
|
|
433
|
+
utilization: data.seven_day_sonnet.utilization,
|
|
434
|
+
resetsAt: formatReset(data.seven_day_sonnet.resets_at)
|
|
435
|
+
} : null,
|
|
436
|
+
extraUsage: data.extra_usage?.is_enabled ? {
|
|
437
|
+
limit: data.extra_usage.monthly_limit / 100,
|
|
438
|
+
used: data.extra_usage.used_credits / 100
|
|
439
|
+
} : null
|
|
440
|
+
}
|
|
335
441
|
};
|
|
336
442
|
} catch {
|
|
337
|
-
return {
|
|
443
|
+
return { error: "Network error" };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function fetchPeakStatus() {
|
|
447
|
+
try {
|
|
448
|
+
const res = await fetch("https://promoclock.co/api/status", {
|
|
449
|
+
headers: { "Accept": "application/json", "User-Agent": "tokmon" },
|
|
450
|
+
signal: AbortSignal.timeout(3e3)
|
|
451
|
+
});
|
|
452
|
+
if (!res.ok) return null;
|
|
453
|
+
const data = await res.json();
|
|
454
|
+
let state;
|
|
455
|
+
if (data.isPeak === true || data.status === "peak") state = "peak";
|
|
456
|
+
else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
|
|
457
|
+
else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
|
|
458
|
+
else return null;
|
|
459
|
+
return {
|
|
460
|
+
state,
|
|
461
|
+
label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
|
|
462
|
+
minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
|
|
463
|
+
};
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
338
466
|
}
|
|
339
467
|
}
|
|
340
468
|
function formatReset(iso) {
|
|
@@ -362,11 +490,12 @@ function tokens(value) {
|
|
|
362
490
|
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
363
491
|
return String(value);
|
|
364
492
|
}
|
|
365
|
-
function time(date) {
|
|
493
|
+
function time(date, tz) {
|
|
366
494
|
return date.toLocaleTimeString(void 0, {
|
|
367
495
|
hour: "2-digit",
|
|
368
496
|
minute: "2-digit",
|
|
369
|
-
second: "2-digit"
|
|
497
|
+
second: "2-digit",
|
|
498
|
+
timeZone: tz
|
|
370
499
|
});
|
|
371
500
|
}
|
|
372
501
|
var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
@@ -387,7 +516,8 @@ var VIEWS = ["Daily", "Weekly", "Monthly"];
|
|
|
387
516
|
var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
|
|
388
517
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
389
518
|
var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
390
|
-
var DEFAULT_CONFIG = { interval: 2, billingInterval: 5, clearScreen: true };
|
|
519
|
+
var DEFAULT_CONFIG = { interval: 2, billingInterval: 5, clearScreen: true, timezone: null };
|
|
520
|
+
var SETTINGS_ROWS = 4;
|
|
391
521
|
var IS_TTY = process.stdin.isTTY === true;
|
|
392
522
|
function App({ interval: cliInterval }) {
|
|
393
523
|
const [dashboard, setDashboard] = useState(null);
|
|
@@ -400,10 +530,12 @@ function App({ interval: cliInterval }) {
|
|
|
400
530
|
const [view, setView] = useState(0);
|
|
401
531
|
const [cursor, setCursor] = useState(0);
|
|
402
532
|
const [expanded, setExpanded] = useState(-1);
|
|
403
|
-
const [sort, setSort] = useState(
|
|
533
|
+
const [sort, setSort] = useState(1);
|
|
404
534
|
const [showSettings, setShowSettings] = useState(false);
|
|
405
535
|
const [config2, setConfig] = useState(null);
|
|
406
536
|
const [settingsCursor, setSettingsCursor] = useState(0);
|
|
537
|
+
const [tzEdit, setTzEdit] = useState(null);
|
|
538
|
+
const [tzError, setTzError] = useState(null);
|
|
407
539
|
const tableLoadedOnce = useRef(false);
|
|
408
540
|
const { stdout } = useStdout();
|
|
409
541
|
const { exit } = useApp();
|
|
@@ -411,6 +543,7 @@ function App({ interval: cliInterval }) {
|
|
|
411
543
|
const cols = stdout?.columns ?? 80;
|
|
412
544
|
const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
|
|
413
545
|
const cfg = config2 ?? DEFAULT_CONFIG;
|
|
546
|
+
const tz = resolveTimezone(cfg.timezone);
|
|
414
547
|
useEffect(() => {
|
|
415
548
|
loadConfig().then((c) => {
|
|
416
549
|
if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
|
|
@@ -421,7 +554,7 @@ function App({ interval: cliInterval }) {
|
|
|
421
554
|
let active = true;
|
|
422
555
|
const load = async () => {
|
|
423
556
|
try {
|
|
424
|
-
const result = await fetchDashboard();
|
|
557
|
+
const result = await fetchDashboard(tz);
|
|
425
558
|
if (active) {
|
|
426
559
|
setDashboard(result);
|
|
427
560
|
setError(null);
|
|
@@ -437,7 +570,7 @@ function App({ interval: cliInterval }) {
|
|
|
437
570
|
active = false;
|
|
438
571
|
clearInterval(id);
|
|
439
572
|
};
|
|
440
|
-
}, [interval2]);
|
|
573
|
+
}, [interval2, tz]);
|
|
441
574
|
const billingMs = cfg.billingInterval * 6e4;
|
|
442
575
|
useEffect(() => {
|
|
443
576
|
let active = true;
|
|
@@ -452,12 +585,16 @@ function App({ interval: cliInterval }) {
|
|
|
452
585
|
clearInterval(id);
|
|
453
586
|
};
|
|
454
587
|
}, [billingMs]);
|
|
588
|
+
useEffect(() => {
|
|
589
|
+
tableLoadedOnce.current = false;
|
|
590
|
+
setTable(null);
|
|
591
|
+
}, [tz]);
|
|
455
592
|
useEffect(() => {
|
|
456
593
|
if (tab !== 1) return;
|
|
457
594
|
if (tableLoadedOnce.current && table) return;
|
|
458
595
|
let active = true;
|
|
459
596
|
setTableLoading(true);
|
|
460
|
-
fetchTable().then((result) => {
|
|
597
|
+
fetchTable(tz).then((result) => {
|
|
461
598
|
if (active) {
|
|
462
599
|
setTable(result);
|
|
463
600
|
setTableLoading(false);
|
|
@@ -469,13 +606,13 @@ function App({ interval: cliInterval }) {
|
|
|
469
606
|
return () => {
|
|
470
607
|
active = false;
|
|
471
608
|
};
|
|
472
|
-
}, [tab]);
|
|
609
|
+
}, [tab, tz]);
|
|
473
610
|
useEffect(() => {
|
|
474
611
|
if (tab !== 1 || !tableLoadedOnce.current) return;
|
|
475
612
|
let active = true;
|
|
476
613
|
const id = setInterval(async () => {
|
|
477
614
|
try {
|
|
478
|
-
const result = await fetchTable();
|
|
615
|
+
const result = await fetchTable(tz);
|
|
479
616
|
if (active) setTable(result);
|
|
480
617
|
} catch {
|
|
481
618
|
}
|
|
@@ -484,7 +621,7 @@ function App({ interval: cliInterval }) {
|
|
|
484
621
|
active = false;
|
|
485
622
|
clearInterval(id);
|
|
486
623
|
};
|
|
487
|
-
}, [tab, interval2]);
|
|
624
|
+
}, [tab, interval2, tz]);
|
|
488
625
|
const resetView = useCallback(() => {
|
|
489
626
|
setCursor(0);
|
|
490
627
|
setExpanded(-1);
|
|
@@ -504,10 +641,47 @@ function App({ interval: cliInterval }) {
|
|
|
504
641
|
};
|
|
505
642
|
}, [tab]);
|
|
506
643
|
useInput((input, key) => {
|
|
644
|
+
if (showSettings && tzEdit !== null) {
|
|
645
|
+
if (key.escape) {
|
|
646
|
+
setTzEdit(null);
|
|
647
|
+
setTzError(null);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (key.return) {
|
|
651
|
+
const val = tzEdit.trim();
|
|
652
|
+
if (val === "" || val.toLowerCase() === "system") {
|
|
653
|
+
updateConfig((c) => ({ ...c, timezone: null }));
|
|
654
|
+
setTzEdit(null);
|
|
655
|
+
setTzError(null);
|
|
656
|
+
} else if (isValidTimezone(val)) {
|
|
657
|
+
updateConfig((c) => ({ ...c, timezone: val }));
|
|
658
|
+
setTzEdit(null);
|
|
659
|
+
setTzError(null);
|
|
660
|
+
} else {
|
|
661
|
+
setTzError(`Invalid: ${val}`);
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (key.backspace || key.delete) {
|
|
666
|
+
setTzEdit((s) => (s ?? "").slice(0, -1));
|
|
667
|
+
setTzError(null);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (input && !key.ctrl && !key.meta) {
|
|
671
|
+
setTzEdit((s) => (s ?? "") + input);
|
|
672
|
+
setTzError(null);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (input === "q") {
|
|
678
|
+
exit();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
507
681
|
if (showSettings) {
|
|
508
682
|
if (key.escape || input === "s") setShowSettings(false);
|
|
509
683
|
if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
|
|
510
|
-
if (key.downArrow) setSettingsCursor((c) => Math.min(
|
|
684
|
+
if (key.downArrow) setSettingsCursor((c) => Math.min(SETTINGS_ROWS - 1, c + 1));
|
|
511
685
|
if (settingsCursor === 0) {
|
|
512
686
|
if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
|
|
513
687
|
if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
|
|
@@ -519,10 +693,15 @@ function App({ interval: cliInterval }) {
|
|
|
519
693
|
if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
|
|
520
694
|
updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
|
|
521
695
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
696
|
+
if (settingsCursor === 3) {
|
|
697
|
+
if (key.return) {
|
|
698
|
+
setTzEdit(cfg.timezone ?? "");
|
|
699
|
+
setTzError(null);
|
|
700
|
+
}
|
|
701
|
+
if (key.leftArrow || key.rightArrow) {
|
|
702
|
+
updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
526
705
|
return;
|
|
527
706
|
}
|
|
528
707
|
if (input === "s") {
|
|
@@ -631,9 +810,15 @@ function App({ interval: cliInterval }) {
|
|
|
631
810
|
"s"
|
|
632
811
|
] })
|
|
633
812
|
] }),
|
|
634
|
-
/* @__PURE__ */
|
|
813
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
814
|
+
billing?.peak && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
815
|
+
/* @__PURE__ */ jsx(PeakBadge, { peak: billing.peak }),
|
|
816
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " })
|
|
817
|
+
] }),
|
|
818
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated, tz) })
|
|
819
|
+
] })
|
|
635
820
|
] }),
|
|
636
|
-
showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
821
|
+
showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor, tzEdit, tzError, resolvedTz: tz }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
637
822
|
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
638
823
|
/* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
|
|
639
824
|
setTab(i);
|
|
@@ -716,7 +901,9 @@ function sortRows(rows, sortIdx) {
|
|
|
716
901
|
return sorted;
|
|
717
902
|
}
|
|
718
903
|
}
|
|
719
|
-
function SettingsView({ config: config2, cursor }) {
|
|
904
|
+
function SettingsView({ config: config2, cursor, tzEdit, tzError, resolvedTz }) {
|
|
905
|
+
const editing = tzEdit !== null;
|
|
906
|
+
const tzDisplay = config2.timezone === null ? `System (${resolvedTz})` : config2.timezone;
|
|
720
907
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
721
908
|
/* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
|
|
722
909
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: configLocation() }),
|
|
@@ -767,8 +954,25 @@ function SettingsView({ config: config2, cursor }) {
|
|
|
767
954
|
/* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
|
|
768
955
|
/* @__PURE__ */ jsx(Text, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" })
|
|
769
956
|
] }),
|
|
957
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
958
|
+
/* @__PURE__ */ jsxs(Text, { color: cursor === 3 ? "green" : void 0, children: [
|
|
959
|
+
cursor === 3 ? "\u25B8" : " ",
|
|
960
|
+
" "
|
|
961
|
+
] }),
|
|
962
|
+
/* @__PURE__ */ jsx(Text, { children: "Timezone " }),
|
|
963
|
+
editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
964
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[" }),
|
|
965
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: tzEdit }),
|
|
966
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "_" }),
|
|
967
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "]" })
|
|
968
|
+
] }) : /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: tzDisplay })
|
|
969
|
+
] }),
|
|
970
|
+
cursor === 3 && tzError && /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
971
|
+
" ",
|
|
972
|
+
tzError
|
|
973
|
+
] }),
|
|
770
974
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
771
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust s/Esc close" })
|
|
975
|
+
editing ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "type IANA name (e.g. Europe/London) \xB7 empty = System \xB7 Enter save \xB7 Esc cancel" }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust Enter edit tz s/Esc close" })
|
|
772
976
|
] });
|
|
773
977
|
}
|
|
774
978
|
function DashboardView({ data, billing }) {
|
|
@@ -836,6 +1040,24 @@ function DashboardView({ data, billing }) {
|
|
|
836
1040
|
)
|
|
837
1041
|
] });
|
|
838
1042
|
}
|
|
1043
|
+
function PeakBadge({ peak }) {
|
|
1044
|
+
const color = peak.state === "peak" ? "red" : "green";
|
|
1045
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
1046
|
+
/* @__PURE__ */ jsx(Text, { color, children: "\u25CF " }),
|
|
1047
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
|
|
1048
|
+
peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1049
|
+
" (",
|
|
1050
|
+
fmtMinutes(peak.minutesUntilChange),
|
|
1051
|
+
")"
|
|
1052
|
+
] })
|
|
1053
|
+
] });
|
|
1054
|
+
}
|
|
1055
|
+
function fmtMinutes(mins) {
|
|
1056
|
+
if (mins < 60) return `${mins}m`;
|
|
1057
|
+
const h = Math.floor(mins / 60);
|
|
1058
|
+
const m = mins % 60;
|
|
1059
|
+
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
|
1060
|
+
}
|
|
839
1061
|
function LimitBar({ label, pct, resets }) {
|
|
840
1062
|
const width = 30;
|
|
841
1063
|
const filled = Math.round(pct / 100 * width);
|