tokmon 0.23.1 → 0.23.2

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.
Files changed (24) hide show
  1. package/README.md +2 -1
  2. package/dist/{bootstrap-ink-UD3KDTOB.js → bootstrap-ink-MBN74IBQ.js} +161 -100
  3. package/dist/{chunk-PGQK7MOE.js → chunk-2AFQKH5O.js} +5 -1
  4. package/dist/{chunk-AON6GE7D.js → chunk-VVJF5H4D.js} +69 -20
  5. package/dist/cli.js +3 -3
  6. package/dist/{daemon-QGCI46US.js → daemon-F556JO7S.js} +2 -2
  7. package/dist/{server-4CDZMLH6.js → server-GBB3CR7Y.js} +2 -2
  8. package/dist/web/assets/{Area-DGo9K-U3.js → Area-C-Zb5hGY.js} +1 -1
  9. package/dist/web/assets/{analytics-BOhbhV5Z.js → analytics-kEeE-ogR.js} +2 -2
  10. package/dist/web/assets/{breakdown-CO04nSN-.js → breakdown-BavE2T8a.js} +1 -1
  11. package/dist/web/assets/{chart-bNxdfXxA.js → chart-QgM4f2g9.js} +1 -1
  12. package/dist/web/assets/{explore-sZnmjzAj.js → explore-Buh9XEPv.js} +1 -1
  13. package/dist/web/assets/index-KD8LkaQs.js +83 -0
  14. package/dist/web/assets/{models-Cl6D8VVF.js → models-BRG0KFKx.js} +2 -2
  15. package/dist/web/assets/{overview-C18nvaAK.js → overview-JbfGEJ5u.js} +2 -2
  16. package/dist/web/assets/{panel-DTjMnHaz.js → panel-OcuPqv_k.js} +1 -1
  17. package/dist/web/assets/{primitives-Dj_9YxTF.js → primitives-BUmlNFB0.js} +1 -1
  18. package/dist/web/assets/{settings-sheet-0BGb5Ahv.js → settings-sheet-DVstI_qi.js} +1 -1
  19. package/dist/web/assets/{share-sheet-BHtt-OmT.js → share-sheet-qAODWbhh.js} +1 -1
  20. package/dist/web/assets/{timeline-DmzB8Z3W.js → timeline-DoJ5qjNr.js} +1 -1
  21. package/dist/web/assets/{use-dialog-trap-DeXFtxx8.js → use-dialog-trap-9cAy7Dfu.js} +1 -1
  22. package/dist/web/index.html +1 -1
  23. package/package.json +1 -1
  24. package/dist/web/assets/index-DnDr1VUr.js +0 -83
package/README.md CHANGED
@@ -233,10 +233,11 @@ tokmon runs a small local **daemon** that does all the data collection. The term
233
233
  - Each enabled provider is detected automatically, and its real account identity — email and plan — is read from local auth (e.g. Claude `~/.claude.json`, the Codex `id_token`, Cursor's state DB). Extra accounts, like additional Claude homes, are auto-discovered too.
234
234
 
235
235
  **Limits & billing**
236
- - Rate limits and remaining spend/quota come from each provider's own official API, refreshed on the billing poll interval.
236
+ - Rate limits and remaining spend/quota come from each provider's own official API, refreshed on the billing poll interval and when the terminal regains focus. Opening a dashboard also catches up stale limits immediately.
237
237
 
238
238
  **Responsiveness**
239
239
  - Dashboard summaries and table history load independently and refresh on separate intervals, so the UI stays responsive even on large histories.
240
+ - The TUI shows the usage and limits cadences separately and reconnects automatically if a suspended terminal leaves its live stream stale.
240
241
 
241
242
  Cross-platform: macOS, Linux, Windows. Everything is local and read-only — see [Privacy](#privacy).
242
243
 
@@ -19,7 +19,7 @@ import {
19
19
  time,
20
20
  tokens,
21
21
  withTimeout
22
- } from "./chunk-PGQK7MOE.js";
22
+ } from "./chunk-2AFQKH5O.js";
23
23
  import {
24
24
  glyphs
25
25
  } from "./chunk-RF4GGQGM.js";
@@ -1607,7 +1607,7 @@ function TinyRow({ provider, accounts, stats, width }) {
1607
1607
  import { useEffect as useEffect3, useMemo, useRef as useRef2, useState as useState3 } from "react";
1608
1608
 
1609
1609
  // src/client/daemon-rpc-client.ts
1610
- import { Cause, Context, Duration, Effect, Fiber, Layer, ManagedRuntime, Schedule, Stream } from "effect";
1610
+ import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Schedule, Stream } from "effect";
1611
1611
  import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
1612
1612
  import * as Socket from "effect/unstable/socket/Socket";
1613
1613
  var TokmonRpcClient = class extends Context.Service()(
@@ -1660,8 +1660,13 @@ function createDaemonRpcClient(baseUrl, options = {}) {
1660
1660
  options.onConn?.(state, error);
1661
1661
  };
1662
1662
  const resetSession = (active) => {
1663
+ if (active !== void 0 && active !== null && active !== session) {
1664
+ void active.runtime.dispose().catch(() => {
1665
+ });
1666
+ return;
1667
+ }
1663
1668
  const dead = active ?? session;
1664
- if (active === void 0 || active === session) session = null;
1669
+ session = null;
1665
1670
  sessionPromise = null;
1666
1671
  if (dead) void dead.runtime.dispose().catch(() => {
1667
1672
  });
@@ -1738,12 +1743,16 @@ function createDaemonRpcClient(baseUrl, options = {}) {
1738
1743
  throw error;
1739
1744
  }
1740
1745
  };
1741
- const subscribe = (streamFor, onValue) => {
1746
+ const subscribe = (streamFor, onValue, staleAfterFor) => {
1742
1747
  if (closed) return () => {
1743
1748
  };
1744
1749
  let fiber = null;
1745
1750
  let unsubscribed = false;
1746
1751
  let retryTimer = null;
1752
+ let watchdogTimer = null;
1753
+ let lastValueAt = 0;
1754
+ let staleAfterMs = Number.POSITIVE_INFINITY;
1755
+ let watchdogRestart = false;
1747
1756
  const stopFiber = () => {
1748
1757
  if (!fiber) return;
1749
1758
  const current = fiber;
@@ -1760,16 +1769,31 @@ function createDaemonRpcClient(baseUrl, options = {}) {
1760
1769
  }, options.reconnectBaseDelayMs ?? 250);
1761
1770
  retryTimer.unref?.();
1762
1771
  };
1772
+ if (staleAfterFor) {
1773
+ const checkEveryMs = Math.min(5e3, Math.max(10, (options.snapshotStaleFloorMs ?? 9e4) / 2));
1774
+ watchdogTimer = setInterval(() => {
1775
+ if (!fiber || lastValueAt === 0 || Date.now() - lastValueAt <= staleAfterMs) return;
1776
+ watchdogRestart = true;
1777
+ lastValueAt = Date.now();
1778
+ setConn("reconnecting");
1779
+ stopFiber();
1780
+ }, checkEveryMs);
1781
+ watchdogTimer.unref?.();
1782
+ }
1763
1783
  const start = () => {
1764
1784
  void (async () => {
1765
1785
  try {
1766
1786
  const active = await ensureSession();
1767
1787
  if (closed || unsubscribed) return;
1768
- fiber = active.runtime.runFork(
1788
+ lastValueAt = Date.now();
1789
+ staleAfterMs = options.snapshotStaleFloorMs ?? 9e4;
1790
+ const currentFiber = active.runtime.runFork(
1769
1791
  TokmonRpcClient.use(
1770
1792
  (client) => streamFor(client).pipe(
1771
1793
  Stream.runForEach(
1772
1794
  (value) => Effect.sync(() => {
1795
+ lastValueAt = Date.now();
1796
+ if (staleAfterFor) staleAfterMs = staleAfterFor(value);
1773
1797
  try {
1774
1798
  onValue(value);
1775
1799
  } catch {
@@ -1777,19 +1801,20 @@ function createDaemonRpcClient(baseUrl, options = {}) {
1777
1801
  })
1778
1802
  )
1779
1803
  )
1780
- ).pipe(Effect.catchCause(
1781
- (cause) => Effect.sync(() => {
1782
- if (!closed && !unsubscribed) {
1783
- resetSession(active);
1784
- setConn("error", Cause.squash(cause));
1785
- scheduleRetry();
1786
- }
1787
- })
1788
- ))
1804
+ )
1789
1805
  );
1790
- fibers.add(fiber);
1791
- fiber.addObserver(() => {
1792
- if (fiber) fibers.delete(fiber);
1806
+ fiber = currentFiber;
1807
+ fibers.add(currentFiber);
1808
+ currentFiber.addObserver((exit) => {
1809
+ fibers.delete(currentFiber);
1810
+ if (fiber === currentFiber) fiber = null;
1811
+ if (closed || unsubscribed) return;
1812
+ const stale = watchdogRestart;
1813
+ watchdogRestart = false;
1814
+ resetSession(active);
1815
+ if (stale || Exit.isSuccess(exit)) setConn("reconnecting");
1816
+ else setConn("error", Cause.squash(exit.cause));
1817
+ scheduleRetry();
1793
1818
  });
1794
1819
  } catch (error) {
1795
1820
  if (!closed && !unsubscribed) {
@@ -1807,6 +1832,10 @@ function createDaemonRpcClient(baseUrl, options = {}) {
1807
1832
  clearTimeout(retryTimer);
1808
1833
  retryTimer = null;
1809
1834
  }
1835
+ if (watchdogTimer) {
1836
+ clearInterval(watchdogTimer);
1837
+ watchdogTimer = null;
1838
+ }
1810
1839
  stopFiber();
1811
1840
  };
1812
1841
  };
@@ -1815,7 +1844,11 @@ function createDaemonRpcClient(baseUrl, options = {}) {
1815
1844
  setConfig: (config) => run((client) => client[TOKMON_WS_METHODS.setConfig](config)).then((state) => state),
1816
1845
  refresh: (scope = "all") => run((client) => client[TOKMON_WS_METHODS.refresh]({ scope })),
1817
1846
  browseFs: (path) => run((client) => client[TOKMON_WS_METHODS.browseFs]({ path })),
1818
- subscribeSnapshot: (onSnapshot) => subscribe((client) => client[TOKMON_WS_METHODS.snapshot]({}).pipe(Stream.map((value) => value)), onSnapshot),
1847
+ subscribeSnapshot: (onSnapshot) => subscribe(
1848
+ (client) => client[TOKMON_WS_METHODS.snapshot]({}).pipe(Stream.map((value) => value)),
1849
+ onSnapshot,
1850
+ (snapshot) => Math.max(options.snapshotStaleFloorMs ?? 9e4, snapshot.intervalMs * 3)
1851
+ ),
1819
1852
  subscribeConfig: (onConfig) => subscribe((client) => client[TOKMON_WS_METHODS.config]({}).pipe(Stream.map((value) => value)), onConfig),
1820
1853
  async close() {
1821
1854
  if (closed) return;
@@ -2500,6 +2533,17 @@ function handleNavigation(input, key, ctx) {
2500
2533
  }
2501
2534
 
2502
2535
  // src/ui/keybindings.ts
2536
+ function terminalFocusEvent(input) {
2537
+ if (input === "[I") return "in";
2538
+ if (input === "[O") return "out";
2539
+ return null;
2540
+ }
2541
+ function handleTerminalFocusInput(input, onFocusIn) {
2542
+ const event = terminalFocusEvent(input);
2543
+ if (!event) return false;
2544
+ if (event === "in") onFocusIn();
2545
+ return true;
2546
+ }
2503
2547
  function isRefreshAllShortcut(input, mode) {
2504
2548
  return (input === "r" || input === "R") && !mode.unavailable && !mode.showPicker && !mode.editingAccount && !mode.editingTimezone && !mode.editingSearch;
2505
2549
  }
@@ -3384,6 +3428,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3384
3428
  [connected, snapshot, updatedLocal]
3385
3429
  );
3386
3430
  const intervalLabel = connected && snapshot?.intervalMs ? Math.round(snapshot.intervalMs / 1e3) : cfg.interval;
3431
+ const billingIntervalLabel = connected && snapshot?.billingIntervalMs ? Math.max(1, Math.round(snapshot.billingIntervalMs / 6e4)) : cfg.billingInterval;
3387
3432
  const readyInputFor = useCallback7((id) => {
3388
3433
  if (connected) {
3389
3434
  const wa = snapshot?.accounts.find((a) => a.id === id);
@@ -3604,7 +3649,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3604
3649
  if (webStartingRef.current) return;
3605
3650
  webStartingRef.current = true;
3606
3651
  try {
3607
- const { startWebServer } = await import("./server-4CDZMLH6.js");
3652
+ const { startWebServer } = await import("./server-GBB3CR7Y.js");
3608
3653
  const ctrl = await startWebServer({ config: cfg, log: false });
3609
3654
  webRef.current = ctrl;
3610
3655
  openUrl(ctrl.browserUrl);
@@ -3651,83 +3696,95 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3651
3696
  () => tab === 1 ? sortRows(filterTokenRows(filterRowsByModel(rawTokenRows, activeTableModel), search), sort) : [],
3652
3697
  [tab, rawTokenRows, activeTableModel, search, sort]
3653
3698
  );
3654
- useInput((input, key) => handleKey(input, key, {
3655
- onboarding: {
3656
- show: showPicker,
3657
- providers: pickerProviders,
3658
- cursor: onboardCursor,
3659
- setCursor: setOnboardCursor,
3660
- toggle: toggleOnboard,
3661
- confirm: confirmOnboarding
3662
- },
3663
- accountEditor: {
3664
- form: accountForm,
3665
- setForm: setAccountForm,
3666
- commit: commitAccountForm,
3667
- cycleField: cycleFormField,
3668
- cycleProvider,
3669
- cycleColor
3670
- },
3671
- timezoneEditor: {
3672
- value: tzEdit,
3673
- setValue: setTzEdit,
3674
- setError: setTzError,
3675
- setCaret: setTzCaret,
3676
- valueRef: tzValueRef,
3677
- caretRef: tzCaretRef
3678
- },
3679
- textInput: { isPrintable, insert: insertText },
3680
- settings: {
3681
- show: showSettings,
3682
- setShow: setShowSettings,
3683
- cursor: settingsCursor,
3684
- tab: settingsTab,
3685
- setTab: setSettingsTab,
3686
- setCursor: setSettingsCursor,
3687
- trackedAccounts: trackedAccountRows,
3688
- moveAccount,
3689
- toggleProvider,
3690
- openEditAccount,
3691
- openConfigureAccount,
3692
- deleteAccount,
3693
- openAddAccount
3694
- },
3695
- table: {
3696
- tab,
3697
- searchMode,
3698
- setSearchMode,
3699
- search,
3700
- setSearch,
3701
- setSearchCaret,
3702
- searchValueRef,
3703
- searchCaretRef,
3704
- cycleProvider: cycleTableProvider,
3705
- setExpanded,
3706
- setSort,
3707
- sorts: SORTS_FOR,
3708
- cycleModel: cycleTableModelFilter,
3709
- setView,
3710
- cursor,
3711
- rowCountRef,
3712
- rows,
3713
- setCursor,
3714
- clampRow
3715
- },
3716
- dashboard: { paginated: dashPaginated, pageCount: dashPageCount, setPage: setDashPage },
3717
- global: {
3718
- exit,
3719
- showLoader,
3720
- configReady,
3721
- toggleWeb,
3722
- config: cfg,
3723
- updateConfig,
3724
- cycleAccount,
3725
- setTab,
3726
- resetView,
3727
- slots,
3728
- refreshAll
3729
- }
3730
- }), { isActive: IS_TTY });
3699
+ const lastFocusRefreshRef = useRef8(Date.now());
3700
+ const refreshOnFocus = useCallback7(() => {
3701
+ const now = Date.now();
3702
+ if (now - lastFocusRefreshRef.current < 3e4) return;
3703
+ lastFocusRefreshRef.current = now;
3704
+ const task = connected ? daemonRefreshRef.current("billing") : degradedRefreshRef.current();
3705
+ void task.catch(() => {
3706
+ });
3707
+ }, [connected]);
3708
+ useInput((input, key) => {
3709
+ if (handleTerminalFocusInput(input, refreshOnFocus)) return;
3710
+ handleKey(input, key, {
3711
+ onboarding: {
3712
+ show: showPicker,
3713
+ providers: pickerProviders,
3714
+ cursor: onboardCursor,
3715
+ setCursor: setOnboardCursor,
3716
+ toggle: toggleOnboard,
3717
+ confirm: confirmOnboarding
3718
+ },
3719
+ accountEditor: {
3720
+ form: accountForm,
3721
+ setForm: setAccountForm,
3722
+ commit: commitAccountForm,
3723
+ cycleField: cycleFormField,
3724
+ cycleProvider,
3725
+ cycleColor
3726
+ },
3727
+ timezoneEditor: {
3728
+ value: tzEdit,
3729
+ setValue: setTzEdit,
3730
+ setError: setTzError,
3731
+ setCaret: setTzCaret,
3732
+ valueRef: tzValueRef,
3733
+ caretRef: tzCaretRef
3734
+ },
3735
+ textInput: { isPrintable, insert: insertText },
3736
+ settings: {
3737
+ show: showSettings,
3738
+ setShow: setShowSettings,
3739
+ cursor: settingsCursor,
3740
+ tab: settingsTab,
3741
+ setTab: setSettingsTab,
3742
+ setCursor: setSettingsCursor,
3743
+ trackedAccounts: trackedAccountRows,
3744
+ moveAccount,
3745
+ toggleProvider,
3746
+ openEditAccount,
3747
+ openConfigureAccount,
3748
+ deleteAccount,
3749
+ openAddAccount
3750
+ },
3751
+ table: {
3752
+ tab,
3753
+ searchMode,
3754
+ setSearchMode,
3755
+ search,
3756
+ setSearch,
3757
+ setSearchCaret,
3758
+ searchValueRef,
3759
+ searchCaretRef,
3760
+ cycleProvider: cycleTableProvider,
3761
+ setExpanded,
3762
+ setSort,
3763
+ sorts: SORTS_FOR,
3764
+ cycleModel: cycleTableModelFilter,
3765
+ setView,
3766
+ cursor,
3767
+ rowCountRef,
3768
+ rows,
3769
+ setCursor,
3770
+ clampRow
3771
+ },
3772
+ dashboard: { paginated: dashPaginated, pageCount: dashPageCount, setPage: setDashPage },
3773
+ global: {
3774
+ exit,
3775
+ showLoader,
3776
+ configReady,
3777
+ toggleWeb,
3778
+ config: cfg,
3779
+ updateConfig,
3780
+ cycleAccount,
3781
+ setTab,
3782
+ resetView,
3783
+ slots,
3784
+ refreshAll
3785
+ }
3786
+ });
3787
+ }, { isActive: IS_TTY });
3731
3788
  if (!config) return /* @__PURE__ */ jsx12(Box11, { padding: 1, children: /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Loading..." }) });
3732
3789
  if (resizing) return /* @__PURE__ */ jsx12(ResizingView, { cols: live.cols, rows: live.rows });
3733
3790
  if (showPicker) {
@@ -3760,9 +3817,13 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3760
3817
  /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3761
3818
  " ",
3762
3819
  glyphs().middot,
3763
- " every ",
3820
+ " usage ",
3764
3821
  intervalLabel,
3765
- "s"
3822
+ "s ",
3823
+ glyphs().middot,
3824
+ " limits ",
3825
+ billingIntervalLabel,
3826
+ "m"
3766
3827
  ] })
3767
3828
  ] }),
3768
3829
  /* @__PURE__ */ jsxs12(Box11, { children: [
@@ -3885,11 +3946,11 @@ function leaveAltScreen() {
3885
3946
  }
3886
3947
  }
3887
3948
  function setupInputModes() {
3888
- process.stdout.write("\x1B[?2004h\x1B[?1004l");
3949
+ process.stdout.write("\x1B[?2004h\x1B[?1004h");
3889
3950
  }
3890
3951
  function restoreInputModes() {
3891
3952
  try {
3892
- process.stdout.write("\x1B[?2004l");
3953
+ process.stdout.write("\x1B[?1004l\x1B[?2004l");
3893
3954
  } catch {
3894
3955
  }
3895
3956
  }
@@ -4752,13 +4752,17 @@ var WebAccountSchema = Schema.Struct({
4752
4752
  billing: Schema.NullOr(BillingResultSchema),
4753
4753
  summaryState: Schema.Literals(["pending", "ready", "error"]),
4754
4754
  billingState: Schema.Literals(["pending", "ready", "error"]),
4755
- tableState: Schema.Literals(["pending", "ready", "error"])
4755
+ tableState: Schema.Literals(["pending", "ready", "error"]),
4756
+ summaryUpdatedAt: Schema.optionalKey(Schema.NullOr(NonNegativeIntegerSchema)),
4757
+ billingUpdatedAt: Schema.optionalKey(Schema.NullOr(NonNegativeIntegerSchema)),
4758
+ tableUpdatedAt: Schema.optionalKey(Schema.NullOr(NonNegativeIntegerSchema))
4756
4759
  });
4757
4760
  var WebSnapshotSchema = Schema.Struct({
4758
4761
  version: Schema.String,
4759
4762
  generatedAt: NonNegativeIntegerSchema,
4760
4763
  tz: Schema.String,
4761
4764
  intervalMs: PositiveFiniteSchema,
4765
+ billingIntervalMs: Schema.optionalKey(PositiveFiniteSchema),
4762
4766
  providers: Schema.Array(Schema.Struct({
4763
4767
  id: ProviderIdSchema,
4764
4768
  name: Schema.String,
@@ -21,7 +21,7 @@ import {
21
21
  namedHex,
22
22
  resolveTimezone,
23
23
  withTimeout
24
- } from "./chunk-PGQK7MOE.js";
24
+ } from "./chunk-2AFQKH5O.js";
25
25
  import {
26
26
  cacheDir,
27
27
  expandHome,
@@ -84,7 +84,10 @@ function assembleSnapshot(opts) {
84
84
  billing,
85
85
  summaryState: opts.summaryState?.get(r.account.id) ?? "pending",
86
86
  billingState: opts.billingState?.get(r.account.id) ?? "pending",
87
- tableState: opts.tableState?.get(r.account.id) ?? "pending"
87
+ tableState: opts.tableState?.get(r.account.id) ?? "pending",
88
+ summaryUpdatedAt: opts.summaryUpdatedAt?.get(r.account.id) ?? null,
89
+ billingUpdatedAt: opts.billingUpdatedAt?.get(r.account.id) ?? null,
90
+ tableUpdatedAt: opts.tableUpdatedAt?.get(r.account.id) ?? null
88
91
  };
89
92
  });
90
93
  const seen = /* @__PURE__ */ new Set();
@@ -103,6 +106,7 @@ function assembleSnapshot(opts) {
103
106
  generatedAt: Date.now(),
104
107
  tz: opts.tz,
105
108
  intervalMs: opts.intervalMs,
109
+ billingIntervalMs: opts.billingIntervalMs,
106
110
  providers,
107
111
  accounts,
108
112
  seeded: opts.seeded ?? false,
@@ -247,6 +251,10 @@ var IDLE_PAUSE_MS = 6e4;
247
251
  var SNAPSHOT_CACHE_THROTTLE_MS = 2e4;
248
252
  var REVEAL_THROTTLE_MS = 500;
249
253
  var FETCH_TIMEOUT_MS = 3e4;
254
+ var ATTACH_BILLING_MAX_AGE_MS = 3e4;
255
+ function billingNeedsCatchUp(accounts, updatedAt, now = Date.now(), maxAgeMs = ATTACH_BILLING_MAX_AGE_MS) {
256
+ return accounts.some(({ account }) => now - (updatedAt.get(account.id) ?? 0) >= maxAgeMs);
257
+ }
250
258
  function runInBackground(task) {
251
259
  void task.catch(() => {
252
260
  });
@@ -268,6 +276,9 @@ function createDataEngine(opts) {
268
276
  const summaryState = /* @__PURE__ */ new Map();
269
277
  const billingState = /* @__PURE__ */ new Map();
270
278
  const tableState = /* @__PURE__ */ new Map();
279
+ const summaryUpdatedAt = /* @__PURE__ */ new Map();
280
+ const billingUpdatedAt = /* @__PURE__ */ new Map();
281
+ const tableUpdatedAt = /* @__PURE__ */ new Map();
271
282
  let peak = null;
272
283
  let seeded = false;
273
284
  let current = null;
@@ -296,12 +307,16 @@ function createDataEngine(opts) {
296
307
  version,
297
308
  tz,
298
309
  intervalMs: summaryIntervalMs,
310
+ billingIntervalMs,
299
311
  resolved,
300
312
  usage,
301
313
  billing,
302
314
  summaryState,
303
315
  billingState,
304
316
  tableState,
317
+ summaryUpdatedAt,
318
+ billingUpdatedAt,
319
+ tableUpdatedAt,
305
320
  seeded,
306
321
  peak
307
322
  });
@@ -312,12 +327,19 @@ function createDataEngine(opts) {
312
327
  for (const a of cached.accounts) {
313
328
  if (a.dashboard || a.table) {
314
329
  usage.set(a.id, { dashboard: a.dashboard, table: a.table });
315
- if (a.dashboard) summaryState.set(a.id, "ready");
316
- if (a.table) tableState.set(a.id, "ready");
330
+ if (a.dashboard) {
331
+ summaryState.set(a.id, "ready");
332
+ if (typeof a.summaryUpdatedAt === "number") summaryUpdatedAt.set(a.id, a.summaryUpdatedAt);
333
+ }
334
+ if (a.table) {
335
+ tableState.set(a.id, "ready");
336
+ if (typeof a.tableUpdatedAt === "number") tableUpdatedAt.set(a.id, a.tableUpdatedAt);
337
+ }
317
338
  }
318
339
  if (a.billing) {
319
340
  billing.set(a.id, a.billing);
320
341
  billingState.set(a.id, "ready");
342
+ if (typeof a.billingUpdatedAt === "number") billingUpdatedAt.set(a.id, a.billingUpdatedAt);
321
343
  }
322
344
  }
323
345
  seeded = true;
@@ -362,23 +384,40 @@ function createDataEngine(opts) {
362
384
  async () => {
363
385
  if (stopped) return;
364
386
  const epoch = configEpoch;
365
- const failures = [];
366
- for (const r of opts2.accounts()) {
367
- if (stopped) return;
368
- let value = null;
369
- let ok = true;
387
+ const fetchOne = async (r) => {
370
388
  try {
371
- value = await withTimeout(opts2.fetch(r), FETCH_TIMEOUT_MS);
389
+ return { r, ok: true, value: await withTimeout(opts2.fetch(r), FETCH_TIMEOUT_MS) };
372
390
  } catch (cause) {
373
- ok = false;
374
- failures.push(cause);
391
+ return { r, ok: false, cause };
392
+ }
393
+ };
394
+ const failures = [];
395
+ const applyResult = (result) => {
396
+ const id = result.r.account.id;
397
+ if (result.ok) {
398
+ opts2.apply(id, result.value);
399
+ opts2.state.set(id, "ready");
400
+ opts2.updatedAt.set(id, Date.now());
401
+ } else {
402
+ opts2.state.set(id, "error");
403
+ failures.push(result.cause);
375
404
  }
376
- if (stopped || epoch !== configEpoch) return;
377
- if (ok) {
378
- opts2.apply(r.account.id, value);
379
- opts2.state.set(r.account.id, "ready");
380
- } else opts2.state.set(r.account.id, "error");
381
405
  reveal();
406
+ };
407
+ if (opts2.concurrent) {
408
+ await Promise.all(opts2.accounts().map(async (r) => {
409
+ const result = await fetchOne(r);
410
+ if (stopped || epoch !== configEpoch) return;
411
+ applyResult(result);
412
+ }));
413
+ if (stopped || epoch !== configEpoch) return;
414
+ } else {
415
+ for (const r of opts2.accounts()) {
416
+ if (stopped) return;
417
+ const result = await fetchOne(r);
418
+ if (stopped || epoch !== configEpoch) return;
419
+ applyResult(result);
420
+ }
382
421
  }
383
422
  rebuild();
384
423
  throwIfRefreshFailures(opts2.scope, failures);
@@ -392,7 +431,8 @@ function createDataEngine(opts) {
392
431
  apply: (id, dashboard) => {
393
432
  usageEntry(id).dashboard = dashboard;
394
433
  },
395
- state: summaryState
434
+ state: summaryState,
435
+ updatedAt: summaryUpdatedAt
396
436
  });
397
437
  const refreshTable = makeRefreshLoop({
398
438
  scope: "history",
@@ -401,7 +441,8 @@ function createDataEngine(opts) {
401
441
  apply: (id, table) => {
402
442
  usageEntry(id).table = table;
403
443
  },
404
- state: tableState
444
+ state: tableState,
445
+ updatedAt: tableUpdatedAt
405
446
  });
406
447
  const refreshBilling = makeRefreshLoop({
407
448
  scope: "billing",
@@ -410,7 +451,9 @@ function createDataEngine(opts) {
410
451
  apply: (id, result) => {
411
452
  billing.set(id, result);
412
453
  },
413
- state: billingState
454
+ state: billingState,
455
+ updatedAt: billingUpdatedAt,
456
+ concurrent: true
414
457
  });
415
458
  const refreshPeak = createRefreshQueue(
416
459
  async () => {
@@ -499,6 +542,9 @@ function createDataEngine(opts) {
499
542
  for (const map of [summaryState, billingState, tableState]) {
500
543
  for (const id of [...map.keys()]) if (!survivors.has(id)) map.delete(id);
501
544
  }
545
+ for (const map of [summaryUpdatedAt, billingUpdatedAt, tableUpdatedAt]) {
546
+ for (const id of [...map.keys()]) if (!survivors.has(id)) map.delete(id);
547
+ }
502
548
  rebuild();
503
549
  runInBackground(refreshSummary.run(true));
504
550
  runInBackground(refreshTable.run(true));
@@ -529,6 +575,9 @@ function createDataEngine(opts) {
529
575
  runInBackground(refreshSummary.run(true));
530
576
  runInBackground(refreshTable.run(true));
531
577
  }
578
+ if (billingNeedsCatchUp(billingAccounts, billingUpdatedAt)) {
579
+ runInBackground(refreshBilling.run());
580
+ }
532
581
  return () => {
533
582
  snapshotSubscribers.delete(onSnapshot);
534
583
  };
package/dist/cli.js CHANGED
@@ -41,14 +41,14 @@ function validateServeArgs(serveArgs) {
41
41
  }
42
42
  async function main() {
43
43
  if (subcommand === "__daemon") {
44
- const { runDaemon } = await import("./daemon-QGCI46US.js");
44
+ const { runDaemon } = await import("./daemon-F556JO7S.js");
45
45
  await runDaemon(args.slice(1), { foreground: false });
46
46
  process.exitCode ??= 0;
47
47
  return;
48
48
  }
49
49
  if (subcommand === "serve" || subcommand === "web") {
50
50
  validateServeArgs(args.slice(1));
51
- const { runDaemon } = await import("./daemon-QGCI46US.js");
51
+ const { runDaemon } = await import("./daemon-F556JO7S.js");
52
52
  await runDaemon(args.slice(1), { foreground: true });
53
53
  process.exitCode ??= 0;
54
54
  return;
@@ -103,7 +103,7 @@ async function main() {
103
103
  }));
104
104
  const daemon = await attachOrSpawn();
105
105
  const mode = daemon.kind === "spawned" ? "connected" : "degraded";
106
- const { bootstrapInk } = await import("./bootstrap-ink-UD3KDTOB.js");
106
+ const { bootstrapInk } = await import("./bootstrap-ink-MBN74IBQ.js");
107
107
  await bootstrapInk({ interval, config, daemon, mode });
108
108
  }
109
109
  void main().catch((error) => {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startWebServer
4
- } from "./chunk-AON6GE7D.js";
4
+ } from "./chunk-VVJF5H4D.js";
5
5
  import {
6
6
  acquireLock,
7
7
  isAlive,
@@ -19,7 +19,7 @@ import {
19
19
  } from "./chunk-HXDAOOSI.js";
20
20
  import {
21
21
  flushDisk
22
- } from "./chunk-PGQK7MOE.js";
22
+ } from "./chunk-2AFQKH5O.js";
23
23
  import {
24
24
  loadConfig
25
25
  } from "./chunk-DJPUYMZM.js";
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startWebServer
4
- } from "./chunk-AON6GE7D.js";
4
+ } from "./chunk-VVJF5H4D.js";
5
5
  import "./chunk-HXDAOOSI.js";
6
- import "./chunk-PGQK7MOE.js";
6
+ import "./chunk-2AFQKH5O.js";
7
7
  import "./chunk-DJPUYMZM.js";
8
8
  export {
9
9
  startWebServer