@moneysiren/app 0.1.0-alpha.10

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dist/apps/cli/src/cli.d.ts +59 -0
  4. package/dist/apps/cli/src/cli.js +199 -0
  5. package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
  6. package/dist/apps/cli/src/commands/dashboard.js +239 -0
  7. package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
  8. package/dist/apps/cli/src/commands/doctor.js +25 -0
  9. package/dist/apps/cli/src/commands/init.d.ts +3 -0
  10. package/dist/apps/cli/src/commands/init.js +18 -0
  11. package/dist/apps/cli/src/commands/install.d.ts +3 -0
  12. package/dist/apps/cli/src/commands/install.js +244 -0
  13. package/dist/apps/cli/src/commands/modes.d.ts +3 -0
  14. package/dist/apps/cli/src/commands/modes.js +73 -0
  15. package/dist/apps/cli/src/commands/notify.d.ts +3 -0
  16. package/dist/apps/cli/src/commands/notify.js +430 -0
  17. package/dist/apps/cli/src/commands/report.d.ts +3 -0
  18. package/dist/apps/cli/src/commands/report.js +206 -0
  19. package/dist/apps/cli/src/commands/runtime.d.ts +10 -0
  20. package/dist/apps/cli/src/commands/runtime.js +499 -0
  21. package/dist/apps/cli/src/commands/shared.d.ts +9 -0
  22. package/dist/apps/cli/src/commands/shared.js +29 -0
  23. package/dist/apps/cli/src/commands/summary.d.ts +3 -0
  24. package/dist/apps/cli/src/commands/summary.js +15 -0
  25. package/dist/apps/cli/src/commands/sync.d.ts +3 -0
  26. package/dist/apps/cli/src/commands/sync.js +393 -0
  27. package/dist/apps/cli/src/commands/theme.d.ts +3 -0
  28. package/dist/apps/cli/src/commands/theme.js +181 -0
  29. package/dist/apps/cli/src/desktop-runtime.d.ts +54 -0
  30. package/dist/apps/cli/src/desktop-runtime.js +720 -0
  31. package/dist/apps/cli/src/home.d.ts +7 -0
  32. package/dist/apps/cli/src/home.js +124 -0
  33. package/dist/apps/cli/src/index.d.ts +3 -0
  34. package/dist/apps/cli/src/index.js +14 -0
  35. package/dist/apps/cli/src/install-profile.d.ts +35 -0
  36. package/dist/apps/cli/src/install-profile.js +124 -0
  37. package/dist/apps/cli/src/install-selector.d.ts +10 -0
  38. package/dist/apps/cli/src/install-selector.js +66 -0
  39. package/dist/apps/cli/src/interactive.d.ts +3 -0
  40. package/dist/apps/cli/src/interactive.js +32 -0
  41. package/dist/apps/cli/src/postinstall.d.ts +3 -0
  42. package/dist/apps/cli/src/postinstall.js +42 -0
  43. package/dist/apps/cli/src/release-installer.d.ts +57 -0
  44. package/dist/apps/cli/src/release-installer.js +432 -0
  45. package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
  46. package/dist/apps/cli/src/runtime-adapter.js +185 -0
  47. package/dist/apps/cli/src/slash.d.ts +15 -0
  48. package/dist/apps/cli/src/slash.js +229 -0
  49. package/dist/apps/cli/src/summary-model.d.ts +51 -0
  50. package/dist/apps/cli/src/summary-model.js +136 -0
  51. package/dist/apps/cli/src/theme.d.ts +18 -0
  52. package/dist/apps/cli/src/theme.js +118 -0
  53. package/dist/apps/cli/src/version.d.ts +2 -0
  54. package/dist/apps/cli/src/version.js +2 -0
  55. package/dist/packages/config/src/index.d.ts +3 -0
  56. package/dist/packages/config/src/index.js +3 -0
  57. package/dist/packages/config/src/load.d.ts +3 -0
  58. package/dist/packages/config/src/load.js +80 -0
  59. package/dist/packages/config/src/schema.d.ts +49 -0
  60. package/dist/packages/config/src/schema.js +28 -0
  61. package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
  62. package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
  63. package/dist/packages/connectors/aws/src/index.d.ts +35 -0
  64. package/dist/packages/connectors/aws/src/index.js +67 -0
  65. package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
  66. package/dist/packages/connectors/aws/src/normalize.js +141 -0
  67. package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
  68. package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
  69. package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
  70. package/dist/packages/connectors/cloudflare/src/client.js +107 -0
  71. package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
  72. package/dist/packages/connectors/cloudflare/src/index.js +81 -0
  73. package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
  74. package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
  75. package/dist/packages/connectors/mock/src/index.d.ts +58 -0
  76. package/dist/packages/connectors/mock/src/index.js +66 -0
  77. package/dist/packages/connectors/openai/src/index.d.ts +55 -0
  78. package/dist/packages/connectors/openai/src/index.js +169 -0
  79. package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
  80. package/dist/packages/connectors/openai/src/normalize.js +180 -0
  81. package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
  82. package/dist/packages/connectors/supabase/src/client.js +132 -0
  83. package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
  84. package/dist/packages/connectors/supabase/src/index.js +87 -0
  85. package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
  86. package/dist/packages/connectors/supabase/src/normalize.js +266 -0
  87. package/dist/packages/core/src/collector.d.ts +12 -0
  88. package/dist/packages/core/src/collector.js +68 -0
  89. package/dist/packages/core/src/index.d.ts +5 -0
  90. package/dist/packages/core/src/index.js +4 -0
  91. package/dist/packages/core/src/provider.d.ts +18 -0
  92. package/dist/packages/core/src/provider.js +2 -0
  93. package/dist/packages/core/src/risk-engine.d.ts +9 -0
  94. package/dist/packages/core/src/risk-engine.js +4 -0
  95. package/dist/packages/core/src/snapshots.d.ts +49 -0
  96. package/dist/packages/core/src/snapshots.js +9 -0
  97. package/dist/packages/db/src/client.d.ts +11 -0
  98. package/dist/packages/db/src/client.js +14 -0
  99. package/dist/packages/db/src/index.d.ts +6 -0
  100. package/dist/packages/db/src/index.js +6 -0
  101. package/dist/packages/db/src/local-store.d.ts +161 -0
  102. package/dist/packages/db/src/local-store.js +623 -0
  103. package/dist/packages/db/src/migrate.d.ts +17 -0
  104. package/dist/packages/db/src/migrate.js +35 -0
  105. package/dist/packages/db/src/schema.d.ts +5 -0
  106. package/dist/packages/db/src/schema.js +120 -0
  107. package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
  108. package/dist/packages/db/src/sqlite-bin.js +16 -0
  109. package/dist/packages/local-api/src/index.d.ts +2 -0
  110. package/dist/packages/local-api/src/index.js +2 -0
  111. package/dist/packages/local-api/src/server.d.ts +36 -0
  112. package/dist/packages/local-api/src/server.js +310 -0
  113. package/dist/packages/report/src/daily.d.ts +24 -0
  114. package/dist/packages/report/src/daily.js +9 -0
  115. package/dist/packages/report/src/index.d.ts +4 -0
  116. package/dist/packages/report/src/index.js +4 -0
  117. package/dist/packages/report/src/korean.d.ts +3 -0
  118. package/dist/packages/report/src/korean.js +62 -0
  119. package/dist/packages/report/src/slack.d.ts +34 -0
  120. package/dist/packages/report/src/slack.js +134 -0
  121. package/dist/packages/runtime/src/index.d.ts +2 -0
  122. package/dist/packages/runtime/src/index.js +2 -0
  123. package/dist/packages/runtime/src/runtime.d.ts +26 -0
  124. package/dist/packages/runtime/src/runtime.js +182 -0
  125. package/dist/packages/view-model/src/hud-model.d.ts +74 -0
  126. package/dist/packages/view-model/src/hud-model.js +295 -0
  127. package/dist/packages/view-model/src/index.d.ts +6 -0
  128. package/dist/packages/view-model/src/index.js +6 -0
  129. package/dist/packages/view-model/src/notification-preferences-model.d.ts +75 -0
  130. package/dist/packages/view-model/src/notification-preferences-model.js +400 -0
  131. package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
  132. package/dist/packages/view-model/src/notification-preferences.js +36 -0
  133. package/dist/packages/view-model/src/sync-state.d.ts +47 -0
  134. package/dist/packages/view-model/src/sync-state.js +140 -0
  135. package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
  136. package/dist/packages/view-model/src/usage-progress.js +57 -0
  137. package/dist/packages/view-model/src/view-model.d.ts +215 -0
  138. package/dist/packages/view-model/src/view-model.js +826 -0
  139. package/package.json +40 -0
  140. package/scripts/postinstall.mjs +69 -0
@@ -0,0 +1,826 @@
1
+ import { DEFAULT_NOTIFICATION_PREFERENCES, } from "./notification-preferences.js";
2
+ const DEFAULT_CURRENCY = "USD";
3
+ const OPENAI_PROVIDER_KEY = "openai";
4
+ const AWS_PROVIDER_KEY = "aws";
5
+ const SUPABASE_PROVIDER_KEY = "supabase";
6
+ const CLOUDFLARE_PROVIDER_KEY = "cloudflare";
7
+ const CODEX_APP_PROVIDER_KEY = "codex-app";
8
+ const CODEX_CLI_PROVIDER_KEY = "codex-cli";
9
+ const CLAUDE_CLI_PROVIDER_KEY = "claude-cli";
10
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
11
+ const SENSITIVE_TEXT_PATTERN = /(https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/_-]+|\b(?:sk|sbp|xox[baprs])[-_][A-Za-z0-9_-]+\b|\bacct[_-][A-Za-z0-9_-]+\b|\b(?:proj|project)[_-][A-Za-z0-9_-]+\b|\b(?:in|invoice)[_-][A-Za-z0-9_-]+\b|[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})/gi;
12
+ const EMPTY_STORE = {
13
+ providers: [],
14
+ usageSnapshots: [],
15
+ billingSnapshots: [],
16
+ serviceHealthSnapshots: [],
17
+ costEstimates: [],
18
+ alerts: [],
19
+ };
20
+ export async function readOperationsOverview(options = {}) {
21
+ const store = await resolveStore(options);
22
+ const now = options.now?.() ?? new Date();
23
+ return buildOperationsOverview(store, {
24
+ generatedAt: now.toISOString(),
25
+ });
26
+ }
27
+ export function buildOperationsOverview(store, options) {
28
+ const providerNames = new Map(store.providers.map((provider) => [provider.key, safeText(provider.displayName)]));
29
+ const providers = store.providers.map((provider) => {
30
+ const providerKey = safeText(provider.key);
31
+ const billingSnapshots = store.billingSnapshots.filter((snapshot) => snapshot.providerKey === provider.key);
32
+ const costEstimates = store.costEstimates.filter((estimate) => estimate.providerKey === provider.key);
33
+ const usageSnapshots = store.usageSnapshots.filter((snapshot) => snapshot.providerKey === provider.key);
34
+ const healthSnapshots = store.serviceHealthSnapshots.filter((snapshot) => snapshot.providerKey === provider.key);
35
+ const alerts = store.alerts.filter((alert) => alert.providerKey === provider.key);
36
+ const healthStatus = summarizeHealth(healthSnapshots.map((snapshot) => snapshot.status));
37
+ return {
38
+ providerKey,
39
+ displayName: safeText(provider.displayName),
40
+ estimatedAmountMinor: sum(costEstimates.map((estimate) => estimate.estimatedAmountMinor)),
41
+ billingAmountMinor: sum(billingSnapshots.map((snapshot) => snapshot.amountMinor)),
42
+ currency: summarizeCurrency([
43
+ ...costEstimates.map((estimate) => estimate.currency),
44
+ ...billingSnapshots.map((snapshot) => snapshot.currency),
45
+ ]),
46
+ usageSnapshotCount: usageSnapshots.length,
47
+ costEstimateCount: costEstimates.length,
48
+ latestCollectedAt: latestIso([
49
+ ...billingSnapshots.map((snapshot) => snapshot.collectedAt),
50
+ ...costEstimates.map((estimate) => estimate.collectedAt),
51
+ ...usageSnapshots.map((snapshot) => snapshot.collectedAt),
52
+ ...healthSnapshots.map((snapshot) => snapshot.collectedAt),
53
+ ]),
54
+ healthStatus,
55
+ riskLevel: summarizeRisk(alerts.map((alert) => alert.severity), healthStatus),
56
+ alertCount: alerts.length,
57
+ };
58
+ });
59
+ const alerts = store.alerts
60
+ .map((alert) => {
61
+ const providerKey = alert.providerKey === undefined ? null : safeText(alert.providerKey);
62
+ return {
63
+ providerKey,
64
+ displayName: alert.providerKey === undefined
65
+ ? null
66
+ : providerNames.get(alert.providerKey) ?? safeText(alert.providerKey),
67
+ severity: alert.severity,
68
+ category: safeText(alert.category),
69
+ title: safeText(alert.title),
70
+ message: safeText(alert.message),
71
+ createdAt: alert.createdAt,
72
+ };
73
+ })
74
+ .sort((first, second) => second.createdAt.localeCompare(first.createdAt))
75
+ .slice(0, 5);
76
+ const summaryCurrency = summarizeCurrency([
77
+ ...store.costEstimates.map((estimate) => estimate.currency),
78
+ ...store.billingSnapshots.map((snapshot) => snapshot.currency),
79
+ ]);
80
+ return {
81
+ generatedAt: options.generatedAt,
82
+ localOnly: true,
83
+ secretsReturned: false,
84
+ source: store.providers.length === 0 ? "empty" : "sqlite",
85
+ summary: {
86
+ providerCount: store.providers.length,
87
+ connectedProviderCount: providers.filter((provider) => provider.latestCollectedAt !== null).length,
88
+ totalEstimatedAmountMinor: sum(store.costEstimates.map((estimate) => estimate.estimatedAmountMinor)),
89
+ totalBillingAmountMinor: sum(store.billingSnapshots.map((snapshot) => snapshot.amountMinor)),
90
+ currency: summaryCurrency,
91
+ usageSnapshotCount: store.usageSnapshots.length,
92
+ costEstimateCount: store.costEstimates.length,
93
+ alertCount: store.alerts.length,
94
+ criticalAlertCount: store.alerts.filter((alert) => alert.severity === "critical").length,
95
+ healthStatus: summarizeHealth(providers.map((provider) => provider.healthStatus)),
96
+ },
97
+ providers,
98
+ alerts,
99
+ };
100
+ }
101
+ export async function readTodayLiveView(options = {}) {
102
+ const now = options.now?.() ?? new Date();
103
+ const store = await resolveStore(options);
104
+ const buildOptions = {
105
+ generatedAt: now.toISOString(),
106
+ now,
107
+ timezone: options.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC",
108
+ ...(options.liveProviders === undefined ? {} : { liveProviders: options.liveProviders }),
109
+ };
110
+ return buildTodayLiveView(store, buildOptions);
111
+ }
112
+ export function buildTodayLiveView(store, options) {
113
+ const dateKey = dateKeyInTimezone(options.now, options.timezone);
114
+ const providers = options.liveProviders === undefined
115
+ ? todayProvidersFromStore(store, dateKey)
116
+ : options.liveProviders.map((provider) => ({
117
+ ...provider,
118
+ providerKey: safeText(provider.providerKey),
119
+ displayName: safeText(provider.displayName),
120
+ ...(provider.expiresAt === undefined ? {} : { expiresAt: provider.expiresAt }),
121
+ ...(provider.lastAttemptAt === undefined ? {} : { lastAttemptAt: provider.lastAttemptAt }),
122
+ ...(provider.lastSuccessAt === undefined ? {} : { lastSuccessAt: provider.lastSuccessAt }),
123
+ ...(provider.freshUntil === undefined ? {} : { freshUntil: provider.freshUntil }),
124
+ ...(provider.staleUntil === undefined ? {} : { staleUntil: provider.staleUntil }),
125
+ ...(provider.lastRefreshFailed === undefined ? {} : { lastRefreshFailed: provider.lastRefreshFailed }),
126
+ ...(provider.revision === undefined ? {} : { revision: provider.revision }),
127
+ ...(provider.message === undefined ? {} : { message: safeText(provider.message) }),
128
+ metrics: (provider.metrics ?? []).map((metric) => ({
129
+ key: safeText(metric.key),
130
+ value: metric.value,
131
+ unit: safeText(metric.unit),
132
+ ...(metric.resetAt === undefined ? {} : { resetAt: safeText(metric.resetAt) }),
133
+ ...(metric.resetAtLatest === undefined ? {} : { resetAtLatest: safeText(metric.resetAtLatest) }),
134
+ ...(metric.itemKey === undefined ? {} : { itemKey: safeText(metric.itemKey) }),
135
+ ...(metric.accuracy === undefined ? {} : { accuracy: metric.accuracy }),
136
+ ...(metric.source === undefined ? {} : { source: safeText(metric.source) }),
137
+ })),
138
+ }));
139
+ const includedProviders = providers.filter((provider) => provider.included && provider.todayLiveAmountMinor !== null);
140
+ const currency = singleCurrency(includedProviders.map((provider) => provider.currency)) ?? DEFAULT_CURRENCY;
141
+ const canSum = includedProviders.length > 0 && includedProviders.every((provider) => provider.currency === currency);
142
+ return {
143
+ generatedAt: options.generatedAt,
144
+ localOnly: true,
145
+ secretsReturned: false,
146
+ timezone: options.timezone,
147
+ dateKey,
148
+ cacheState: providers.length === 0
149
+ ? "empty"
150
+ : providers.some((provider) => provider.freshness === "live")
151
+ ? "fresh"
152
+ : "stale",
153
+ summary: {
154
+ providerCount: providers.length,
155
+ includedProviderCount: includedProviders.length,
156
+ todayLiveAmountMinor: canSum ? sum(includedProviders.map((provider) => provider.todayLiveAmountMinor ?? 0)) : null,
157
+ currency,
158
+ },
159
+ providers,
160
+ };
161
+ }
162
+ export async function readNotificationDigest(options = {}) {
163
+ const overview = options.overview ?? await readOperationsOverview(options);
164
+ const todayLive = options.todayLive ?? await readTodayLiveView(options);
165
+ return buildNotificationDigest(overview, todayLive, options.notificationPreferences);
166
+ }
167
+ export function buildNotificationDigest(overview, todayLive, preferences = DEFAULT_NOTIFICATION_PREFERENCES) {
168
+ const criticalAlerts = overview.summary.criticalAlertCount;
169
+ const warningAlerts = overview.summary.alertCount - criticalAlerts;
170
+ const status = criticalAlerts > 0
171
+ ? "critical"
172
+ : warningAlerts > 0 || overview.summary.healthStatus !== "ok"
173
+ ? "attention"
174
+ : "ok";
175
+ const items = buildDigestItems(overview, todayLive, {
176
+ criticalAlerts,
177
+ warningAlerts,
178
+ });
179
+ return {
180
+ generatedAt: overview.generatedAt,
181
+ localOnly: true,
182
+ secretsReturned: false,
183
+ title: "MoneySiren",
184
+ status,
185
+ suppressedReason: preferences.enabled
186
+ ? preferences.digestEnabled
187
+ ? null
188
+ : "digest_disabled"
189
+ : "notifications_disabled",
190
+ items: preferences.enabled && preferences.digestEnabled
191
+ ? filterDigestItems(items, preferences.selectedWidgets)
192
+ : [],
193
+ };
194
+ }
195
+ export async function readTrayMenuModel(options = {}) {
196
+ const digest = options.digest ?? await readNotificationDigest(options);
197
+ return buildTrayMenuModel(digest);
198
+ }
199
+ function filterDigestItems(items, selectedWidgets) {
200
+ const itemsByWidget = new Map(items.map((item) => [item.widgetKey, item]));
201
+ return selectedWidgets.flatMap((widgetKey) => {
202
+ const item = itemsByWidget.get(widgetKey);
203
+ return item === undefined ? [] : [item];
204
+ });
205
+ }
206
+ export function buildTrayMenuModel(digest) {
207
+ const summaryItem = digest.items.find((item) => item.kind === "summary");
208
+ const statusLabel = digest.status === "ok"
209
+ ? digest.suppressedReason === null
210
+ ? "All monitored providers OK"
211
+ : "Notifications paused"
212
+ : digest.status === "critical"
213
+ ? "Critical alert needs attention"
214
+ : "Provider attention needed";
215
+ return {
216
+ generatedAt: digest.generatedAt,
217
+ localOnly: true,
218
+ secretsReturned: false,
219
+ title: digest.title,
220
+ subtitle: summaryItem?.value ?? "No local data",
221
+ status: digest.status,
222
+ items: [
223
+ {
224
+ id: "status",
225
+ label: digest.suppressedReason === null ? statusLabel : `${statusLabel}: ${digest.suppressedReason}`,
226
+ enabled: false,
227
+ kind: "status",
228
+ },
229
+ ...digest.items.map((item) => ({
230
+ id: `widget-${item.widgetKey}`,
231
+ label: `${item.label}: ${item.value}`,
232
+ enabled: false,
233
+ kind: "status",
234
+ ...(item.clickPath === undefined ? {} : { urlPath: item.clickPath }),
235
+ })),
236
+ ...(digest.items.length === 0
237
+ ? []
238
+ : [{
239
+ id: "separator-widgets",
240
+ label: "",
241
+ enabled: false,
242
+ kind: "separator",
243
+ }]),
244
+ {
245
+ id: "show-hud",
246
+ label: "Show HUD",
247
+ enabled: true,
248
+ kind: "command",
249
+ action: "show_hud",
250
+ urlPath: "/hud?locale=ko",
251
+ },
252
+ {
253
+ id: "open-dashboard",
254
+ label: "Open Dashboard",
255
+ enabled: true,
256
+ kind: "command",
257
+ action: "open_url",
258
+ urlPath: "/ko/dashboard/overview",
259
+ },
260
+ {
261
+ id: "open-notification-settings",
262
+ label: "Notification Settings",
263
+ enabled: true,
264
+ kind: "command",
265
+ action: "open_url",
266
+ urlPath: "/ko/settings/notifications",
267
+ },
268
+ {
269
+ id: "refresh-now",
270
+ label: "Refresh Now",
271
+ enabled: true,
272
+ kind: "command",
273
+ action: "refresh_live",
274
+ },
275
+ {
276
+ id: "separator-main",
277
+ label: "",
278
+ enabled: false,
279
+ kind: "separator",
280
+ },
281
+ {
282
+ id: "quit",
283
+ label: "Quit",
284
+ enabled: true,
285
+ kind: "command",
286
+ action: "quit",
287
+ },
288
+ ],
289
+ };
290
+ }
291
+ function buildDigestItems(overview, todayLive, alerts) {
292
+ const riskCount = alerts.criticalAlerts + alerts.warningAlerts;
293
+ const staleConnectionCount = todayLive.providers.filter((provider) => provider.freshness === "stale" ||
294
+ provider.freshness === "error" ||
295
+ provider.freshness === "locked" ||
296
+ provider.freshness === "unavailable").length;
297
+ const awsOverview = findOverviewProvider(overview, AWS_PROVIDER_KEY);
298
+ const openAiToday = amountFromTodayProviders(todayLive, OPENAI_PROVIDER_KEY);
299
+ const openAiTokens = tokenTotalFromProviders(todayProviders(todayLive, OPENAI_PROVIDER_KEY));
300
+ const supabaseOverview = findOverviewProvider(overview, SUPABASE_PROVIDER_KEY);
301
+ const supabaseToday = firstTodayProvider(todayLive, SUPABASE_PROVIDER_KEY);
302
+ const cloudflareOverview = findOverviewProvider(overview, CLOUDFLARE_PROVIDER_KEY);
303
+ const cloudflareToday = amountFromTodayProviders(todayLive, CLOUDFLARE_PROVIDER_KEY);
304
+ return [
305
+ {
306
+ widgetKey: "month_forecast",
307
+ kind: "summary",
308
+ severity: "info",
309
+ label: "Month estimate",
310
+ value: formatMinorAmount(overview.summary.totalEstimatedAmountMinor, overview.summary.currency),
311
+ clickPath: "/ko/dashboard/forecast",
312
+ },
313
+ {
314
+ widgetKey: "today_live_cost",
315
+ kind: "live",
316
+ severity: "info",
317
+ label: "Today live",
318
+ value: todayLive.summary.todayLiveAmountMinor === null
319
+ ? "Not available"
320
+ : formatMinorAmount(todayLive.summary.todayLiveAmountMinor, todayLive.summary.currency),
321
+ clickPath: "/ko/dashboard/today",
322
+ },
323
+ {
324
+ widgetKey: "risk_high_count",
325
+ kind: "risk",
326
+ severity: alerts.criticalAlerts > 0 ? "critical" : alerts.warningAlerts > 0 ? "warning" : "info",
327
+ label: "High risks",
328
+ value: String(riskCount),
329
+ clickPath: "/ko/dashboard/risks",
330
+ },
331
+ {
332
+ widgetKey: "stale_connection_count",
333
+ kind: "risk",
334
+ severity: staleConnectionCount > 0 ? "warning" : "info",
335
+ label: "Stale connections",
336
+ value: String(staleConnectionCount),
337
+ clickPath: "/ko/settings/connections",
338
+ },
339
+ {
340
+ widgetKey: "aws_month_forecast",
341
+ kind: "summary",
342
+ severity: providerRiskSeverity(awsOverview),
343
+ label: "AWS month estimate",
344
+ value: awsOverview === undefined
345
+ ? "Not available"
346
+ : formatMinorAmount(awsOverview.estimatedAmountMinor, awsOverview.currency),
347
+ clickPath: "/ko/services/aws",
348
+ },
349
+ {
350
+ widgetKey: "openai_today_cost",
351
+ kind: "live",
352
+ severity: "info",
353
+ label: "OpenAI today",
354
+ value: openAiToday === null ? "Not available" : formatMinorAmount(openAiToday.amountMinor, openAiToday.currency),
355
+ clickPath: "/ko/services/openai",
356
+ },
357
+ {
358
+ widgetKey: "openai_today_tokens",
359
+ kind: "usage",
360
+ severity: "info",
361
+ label: "OpenAI tokens",
362
+ value: openAiTokens === null ? "Not available" : formatTokens(openAiTokens),
363
+ clickPath: "/ko/services/openai",
364
+ },
365
+ cliRemainingPercentItem({
366
+ widgetKey: "claude_five_hour_percent",
367
+ label: "Claude 5h remaining",
368
+ providerKey: CLAUDE_CLI_PROVIDER_KEY,
369
+ todayLive,
370
+ usedPercentMetricKey: "five_hour_limit_percent",
371
+ remainingTokensMetricKey: "five_hour_remaining_tokens",
372
+ usedTokensMetricKey: "five_hour_tokens",
373
+ clickPath: "/ko/services/claude-cli",
374
+ }),
375
+ cliRemainingPercentItem({
376
+ widgetKey: "claude_weekly_percent",
377
+ label: "Claude weekly remaining",
378
+ providerKey: CLAUDE_CLI_PROVIDER_KEY,
379
+ todayLive,
380
+ usedPercentMetricKey: "weekly_limit_percent",
381
+ remainingTokensMetricKey: "weekly_remaining_tokens",
382
+ usedTokensMetricKey: "weekly_tokens",
383
+ clickPath: "/ko/services/claude-cli",
384
+ }),
385
+ cliRemainingPercentItem({
386
+ widgetKey: "codex_five_hour_percent",
387
+ label: "Codex 5h remaining",
388
+ providerKey: CODEX_CLI_PROVIDER_KEY,
389
+ todayLive,
390
+ usedPercentMetricKey: "five_hour_limit_percent",
391
+ remainingTokensMetricKey: "five_hour_remaining_tokens",
392
+ usedTokensMetricKey: "five_hour_tokens",
393
+ clickPath: "/ko/services/codex-cli",
394
+ }),
395
+ cliRemainingPercentItem({
396
+ widgetKey: "codex_weekly_percent",
397
+ label: "Codex weekly remaining",
398
+ providerKey: CODEX_CLI_PROVIDER_KEY,
399
+ todayLive,
400
+ usedPercentMetricKey: "weekly_limit_percent",
401
+ remainingTokensMetricKey: "weekly_remaining_tokens",
402
+ usedTokensMetricKey: "weekly_tokens",
403
+ clickPath: "/ko/services/codex-cli",
404
+ }),
405
+ codexResetCreditCountItem(todayLive),
406
+ codexResetCreditExpiryItem(todayLive),
407
+ {
408
+ widgetKey: "supabase_usage_health",
409
+ kind: "health",
410
+ severity: healthSeverity(supabaseOverview?.healthStatus),
411
+ label: "Supabase health",
412
+ value: providerHealthValue(supabaseOverview, supabaseToday),
413
+ ...(supabaseToday === undefined
414
+ ? {}
415
+ : {
416
+ freshness: supabaseToday.freshness,
417
+ confidence: supabaseToday.confidence,
418
+ }),
419
+ clickPath: "/ko/services/supabase",
420
+ },
421
+ {
422
+ widgetKey: "cloudflare_month_to_date",
423
+ kind: "summary",
424
+ severity: providerRiskSeverity(cloudflareOverview),
425
+ label: "Cloudflare MTD",
426
+ value: cloudflareOverview === undefined
427
+ ? cloudflareToday === null
428
+ ? "Not available"
429
+ : formatMinorAmount(cloudflareToday.amountMinor, cloudflareToday.currency)
430
+ : formatMinorAmount(cloudflareOverview.estimatedAmountMinor, cloudflareOverview.currency),
431
+ clickPath: "/ko/services/cloudflare",
432
+ },
433
+ ];
434
+ }
435
+ function cliRemainingPercentItem(options) {
436
+ const providers = todayProviders(options.todayLive, options.providerKey);
437
+ const percent = remainingPercentFromMetrics(providers, options.remainingTokensMetricKey, options.usedTokensMetricKey, options.usedPercentMetricKey);
438
+ const firstProvider = providers[0];
439
+ return {
440
+ widgetKey: options.widgetKey,
441
+ kind: "usage",
442
+ severity: remainingPercentSeverity(percent),
443
+ label: options.label,
444
+ value: percent === null ? "Not available" : formatPercent(percent),
445
+ ...(percent === null ? {} : {
446
+ numericValue: percent,
447
+ unit: "percent",
448
+ remainingPercent: percent,
449
+ usedPercent: clampPercent(100 - percent),
450
+ }),
451
+ providerKey: options.providerKey,
452
+ ...(firstProvider === undefined
453
+ ? {}
454
+ : {
455
+ freshness: firstProvider.freshness,
456
+ confidence: firstProvider.confidence,
457
+ }),
458
+ clickPath: options.clickPath,
459
+ };
460
+ }
461
+ function codexResetCreditCountItem(todayLive) {
462
+ const providers = [
463
+ ...todayProviders(todayLive, CODEX_APP_PROVIDER_KEY),
464
+ ...todayProviders(todayLive, CODEX_CLI_PROVIDER_KEY),
465
+ ];
466
+ const firstProvider = providers[0];
467
+ const metricEntry = firstMetricEntry(providers, "usage_reset_credits");
468
+ const count = metricEntry?.metric.value ?? null;
469
+ const clickProviderKey = metricEntry?.provider.providerKey ?? CODEX_CLI_PROVIDER_KEY;
470
+ return {
471
+ widgetKey: "codex_reset_credit_count",
472
+ kind: "usage",
473
+ severity: "info",
474
+ label: "Codex reset credits",
475
+ value: count === null ? "Not available" : formatCount(count),
476
+ ...(count === null ? {} : {
477
+ numericValue: count,
478
+ unit: "count",
479
+ }),
480
+ ...(metricEntry === undefined ? {} : { providerKey: metricEntry.provider.providerKey }),
481
+ ...(firstProvider === undefined
482
+ ? {}
483
+ : {
484
+ freshness: firstProvider.freshness,
485
+ confidence: firstProvider.confidence,
486
+ }),
487
+ clickPath: `/ko/services/${clickProviderKey}`,
488
+ };
489
+ }
490
+ function codexResetCreditExpiryItem(todayLive) {
491
+ const providers = [
492
+ ...todayProviders(todayLive, CODEX_APP_PROVIDER_KEY),
493
+ ...todayProviders(todayLive, CODEX_CLI_PROVIDER_KEY),
494
+ ];
495
+ const firstProvider = providers[0];
496
+ const exactExpiry = earliestMetricResetAt(providers, "usage_reset_credit");
497
+ const estimatedExpiry = earliestMetricResetAt(providers, "usage_reset_credit_estimate");
498
+ const earliestExpiry = exactExpiry ?? estimatedExpiry;
499
+ const metric = earliestResetCreditMetric(providers, exactExpiry === null ? "usage_reset_credit_estimate" : "usage_reset_credit", earliestExpiry);
500
+ const metricProviderKey = metric === undefined ? undefined : providerForMetric(providers, metric);
501
+ const daysUntil = earliestExpiry === null
502
+ ? null
503
+ : Math.ceil((Date.parse(earliestExpiry) - Date.parse(todayLive.generatedAt)) / MS_PER_DAY);
504
+ return {
505
+ widgetKey: "codex_reset_credit_expiry",
506
+ kind: "usage",
507
+ severity: resetCreditExpirySeverity(daysUntil),
508
+ label: "Codex reset credit expiry",
509
+ value: resetCreditExpiryValue(daysUntil),
510
+ ...(earliestExpiry === null ? {} : { resetAt: earliestExpiry }),
511
+ ...(metric?.resetAtLatest === undefined ? {} : { resetAtLatest: metric.resetAtLatest }),
512
+ ...(metric?.accuracy === undefined ? {} : { accuracy: metric.accuracy }),
513
+ ...(metricProviderKey === undefined ? {} : { providerKey: metricProviderKey }),
514
+ ...(firstProvider === undefined
515
+ ? {}
516
+ : {
517
+ freshness: firstProvider.freshness,
518
+ confidence: firstProvider.confidence,
519
+ }),
520
+ clickPath: "/ko/services/codex-cli",
521
+ };
522
+ }
523
+ async function resolveStore(options) {
524
+ if (options.store !== undefined) {
525
+ return options.store;
526
+ }
527
+ return options.readStore === undefined ? EMPTY_STORE : options.readStore();
528
+ }
529
+ function todayProvidersFromStore(store, dateKey) {
530
+ const providerNames = new Map(store.providers.map((provider) => [provider.key, safeText(provider.displayName)]));
531
+ const estimatesByProvider = new Map();
532
+ for (const estimate of store.costEstimates) {
533
+ if (!dateKeyMatchesIso(estimate.collectedAt, dateKey)) {
534
+ continue;
535
+ }
536
+ estimatesByProvider.set(estimate.providerKey, [
537
+ ...(estimatesByProvider.get(estimate.providerKey) ?? []),
538
+ estimate,
539
+ ]);
540
+ }
541
+ return [...estimatesByProvider.entries()]
542
+ .map(([providerKey, estimates]) => {
543
+ const currency = singleCurrency(estimates.map((estimate) => estimate.currency)) ?? DEFAULT_CURRENCY;
544
+ const canInclude = estimates.length > 0 && estimates.every((estimate) => estimate.currency === currency);
545
+ return {
546
+ providerKey: safeText(providerKey),
547
+ displayName: providerNames.get(providerKey) ?? safeText(providerKey),
548
+ checkedAt: latestIso(estimates.map((estimate) => estimate.collectedAt)),
549
+ freshness: "stale",
550
+ confidence: highestConfidence(estimates.map((estimate) => estimate.confidence)),
551
+ todayLiveAmountMinor: canInclude ? sum(estimates.map((estimate) => estimate.estimatedAmountMinor)) : null,
552
+ currency,
553
+ included: canInclude,
554
+ metrics: [],
555
+ };
556
+ })
557
+ .sort((first, second) => first.providerKey.localeCompare(second.providerKey));
558
+ }
559
+ function findOverviewProvider(overview, providerKey) {
560
+ return overview.providers.find((provider) => provider.providerKey === providerKey);
561
+ }
562
+ function todayProviders(todayLive, providerKey) {
563
+ return todayLive.providers.filter((provider) => provider.providerKey === providerKey);
564
+ }
565
+ function firstTodayProvider(todayLive, providerKey) {
566
+ return todayProviders(todayLive, providerKey)[0];
567
+ }
568
+ function amountFromTodayProviders(todayLive, providerKey) {
569
+ const providers = todayProviders(todayLive, providerKey).filter((provider) => provider.included && provider.todayLiveAmountMinor !== null);
570
+ if (providers.length === 0) {
571
+ return null;
572
+ }
573
+ const currency = singleCurrency(providers.map((provider) => provider.currency));
574
+ if (currency === null) {
575
+ return null;
576
+ }
577
+ return {
578
+ amountMinor: sum(providers.map((provider) => provider.todayLiveAmountMinor ?? 0)),
579
+ currency,
580
+ };
581
+ }
582
+ function tokenTotalFromProviders(providers) {
583
+ const totalTokens = metricSum(providers, "total_tokens");
584
+ if (totalTokens !== null) {
585
+ return totalTokens;
586
+ }
587
+ const componentTokens = sumNullable([
588
+ metricSum(providers, "input_tokens"),
589
+ metricSum(providers, "output_tokens"),
590
+ metricSum(providers, "cache_tokens"),
591
+ metricSum(providers, "reasoning_tokens"),
592
+ ]);
593
+ return componentTokens === 0 ? null : componentTokens;
594
+ }
595
+ function metricSum(providers, metricKey) {
596
+ let found = false;
597
+ let total = 0;
598
+ for (const provider of providers) {
599
+ for (const metric of provider.metrics) {
600
+ if (metric.key !== metricKey) {
601
+ continue;
602
+ }
603
+ found = true;
604
+ total += metric.value;
605
+ }
606
+ }
607
+ return found ? total : null;
608
+ }
609
+ function metricFirst(providers, metricKey) {
610
+ for (const provider of providers) {
611
+ const metric = provider.metrics.find((item) => item.key === metricKey);
612
+ if (metric !== undefined) {
613
+ return metric.value;
614
+ }
615
+ }
616
+ return null;
617
+ }
618
+ function firstMetricEntry(providers, metricKey) {
619
+ for (const provider of providers) {
620
+ const metric = provider.metrics.find((item) => item.key === metricKey);
621
+ if (metric !== undefined) {
622
+ return { provider, metric };
623
+ }
624
+ }
625
+ return undefined;
626
+ }
627
+ function earliestMetricResetAt(providers, metricKey) {
628
+ const values = providers.flatMap((provider) => provider.metrics
629
+ .filter((metric) => metric.key === metricKey && metric.resetAt !== undefined)
630
+ .map((metric) => metric.resetAt)
631
+ .filter((value) => Number.isFinite(Date.parse(value))));
632
+ return values.sort((first, second) => Date.parse(first) - Date.parse(second))[0] ?? null;
633
+ }
634
+ function earliestResetCreditMetric(providers, metricKey, resetAt) {
635
+ if (resetAt === null) {
636
+ return undefined;
637
+ }
638
+ return providers
639
+ .flatMap((provider) => provider.metrics)
640
+ .find((metric) => metric.key === metricKey && metric.resetAt === resetAt);
641
+ }
642
+ function providerForMetric(providers, target) {
643
+ return providers.find((provider) => provider.metrics.some((metric) => metric === target))?.providerKey;
644
+ }
645
+ function remainingPercentFromMetrics(providers, remainingTokensMetricKey, usedTokensMetricKey, usedPercentMetricKey) {
646
+ const remainingTokens = metricSum(providers, remainingTokensMetricKey);
647
+ const usedTokens = metricSum(providers, usedTokensMetricKey);
648
+ if (remainingTokens !== null && usedTokens !== null) {
649
+ const totalTokens = remainingTokens + usedTokens;
650
+ if (totalTokens > 0) {
651
+ return clampPercent((remainingTokens / totalTokens) * 100);
652
+ }
653
+ }
654
+ const usedPercent = metricFirst(providers, usedPercentMetricKey);
655
+ return usedPercent === null ? null : clampPercent(100 - usedPercent);
656
+ }
657
+ function resetCreditExpirySeverity(daysUntil) {
658
+ if (daysUntil === null) {
659
+ return "info";
660
+ }
661
+ if (daysUntil <= 1) {
662
+ return "critical";
663
+ }
664
+ if (daysUntil <= 7) {
665
+ return "warning";
666
+ }
667
+ return "info";
668
+ }
669
+ function resetCreditExpiryValue(daysUntil) {
670
+ if (daysUntil === null) {
671
+ return "Not available";
672
+ }
673
+ if (daysUntil <= 0) {
674
+ return "May expire now";
675
+ }
676
+ if (daysUntil === 1) {
677
+ return "May expire within 1 day";
678
+ }
679
+ return `May expire within ${daysUntil} days`;
680
+ }
681
+ function providerRiskSeverity(provider) {
682
+ if (provider?.riskLevel === "critical") {
683
+ return "critical";
684
+ }
685
+ if (provider?.riskLevel === "warning") {
686
+ return "warning";
687
+ }
688
+ return "info";
689
+ }
690
+ function healthSeverity(status) {
691
+ if (status === "down") {
692
+ return "critical";
693
+ }
694
+ if (status === "degraded" || status === "unknown") {
695
+ return "warning";
696
+ }
697
+ return "info";
698
+ }
699
+ function remainingPercentSeverity(percent) {
700
+ if (percent === null) {
701
+ return "info";
702
+ }
703
+ if (percent <= 10) {
704
+ return "critical";
705
+ }
706
+ if (percent <= 25) {
707
+ return "warning";
708
+ }
709
+ return "info";
710
+ }
711
+ function providerHealthValue(overviewProvider, todayProvider) {
712
+ if (overviewProvider === undefined && todayProvider === undefined) {
713
+ return "Not available";
714
+ }
715
+ const values = [
716
+ overviewProvider === undefined ? null : `health ${overviewProvider.healthStatus}`,
717
+ todayProvider === undefined ? null : `live ${todayProvider.freshness}`,
718
+ ].filter((value) => value !== null);
719
+ return values.join(" / ");
720
+ }
721
+ function summarizeHealth(statuses) {
722
+ if (statuses.includes("down")) {
723
+ return "down";
724
+ }
725
+ if (statuses.includes("degraded")) {
726
+ return "degraded";
727
+ }
728
+ if (statuses.includes("unknown") || statuses.length === 0) {
729
+ return "unknown";
730
+ }
731
+ return "ok";
732
+ }
733
+ function summarizeRisk(severities, healthStatus) {
734
+ if (severities.includes("critical") || healthStatus === "down") {
735
+ return "critical";
736
+ }
737
+ if (severities.includes("warning") || healthStatus === "degraded" || healthStatus === "unknown") {
738
+ return "warning";
739
+ }
740
+ return "low";
741
+ }
742
+ function highestConfidence(values) {
743
+ if (values.includes("high")) {
744
+ return "high";
745
+ }
746
+ if (values.includes("medium")) {
747
+ return "medium";
748
+ }
749
+ if (values.includes("low")) {
750
+ return "low";
751
+ }
752
+ return "none";
753
+ }
754
+ function summarizeCurrency(currencies) {
755
+ const normalized = new Set(currencies
756
+ .map((currency) => safeText(currency.toUpperCase()))
757
+ .filter((currency) => currency.length > 0));
758
+ if (normalized.size === 0) {
759
+ return DEFAULT_CURRENCY;
760
+ }
761
+ if (normalized.size > 1) {
762
+ return "MIXED";
763
+ }
764
+ return [...normalized][0] ?? DEFAULT_CURRENCY;
765
+ }
766
+ function singleCurrency(currencies) {
767
+ const normalized = currencies
768
+ .map((currency) => safeText(currency.toUpperCase()))
769
+ .filter((currency) => currency.length > 0);
770
+ const values = new Set(normalized);
771
+ return values.size === 1 ? normalized[0] ?? null : null;
772
+ }
773
+ function latestIso(values) {
774
+ return values.length === 0
775
+ ? null
776
+ : [...values].sort((first, second) => second.localeCompare(first))[0] ?? null;
777
+ }
778
+ function dateKeyMatchesIso(value, dateKey) {
779
+ return value.startsWith(dateKey);
780
+ }
781
+ function dateKeyInTimezone(date, timezone) {
782
+ const parts = new Intl.DateTimeFormat("en-US", {
783
+ timeZone: timezone,
784
+ year: "numeric",
785
+ month: "2-digit",
786
+ day: "2-digit",
787
+ }).formatToParts(date);
788
+ return [
789
+ parts.find((part) => part.type === "year")?.value ?? "1970",
790
+ parts.find((part) => part.type === "month")?.value ?? "01",
791
+ parts.find((part) => part.type === "day")?.value ?? "01",
792
+ ].join("-");
793
+ }
794
+ function formatMinorAmount(amountMinor, currency) {
795
+ return `${currency} ${(amountMinor / 100).toFixed(2)}`;
796
+ }
797
+ function formatTokens(tokens) {
798
+ return new Intl.NumberFormat("en-US", {
799
+ maximumFractionDigits: 0,
800
+ }).format(tokens);
801
+ }
802
+ function formatCount(value) {
803
+ return new Intl.NumberFormat("en-US", {
804
+ maximumFractionDigits: 0,
805
+ }).format(value);
806
+ }
807
+ function formatPercent(percent) {
808
+ const rounded = Number.isInteger(percent) ? percent.toFixed(0) : percent.toFixed(1);
809
+ return `${rounded}%`;
810
+ }
811
+ function sum(values) {
812
+ return values.reduce((total, value) => total + value, 0);
813
+ }
814
+ function sumNullable(values) {
815
+ return values.reduce((total, value) => total + (value ?? 0), 0);
816
+ }
817
+ function clampPercent(value) {
818
+ if (!Number.isFinite(value)) {
819
+ return 0;
820
+ }
821
+ return Math.max(0, Math.min(100, value));
822
+ }
823
+ function safeText(value) {
824
+ return value.replace(SENSITIVE_TEXT_PATTERN, "[redacted]");
825
+ }
826
+ //# sourceMappingURL=view-model.js.map