@moneysiren/app 0.1.0-alpha.9
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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/apps/cli/src/cli.d.ts +59 -0
- package/dist/apps/cli/src/cli.js +199 -0
- package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
- package/dist/apps/cli/src/commands/dashboard.js +239 -0
- package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
- package/dist/apps/cli/src/commands/doctor.js +25 -0
- package/dist/apps/cli/src/commands/init.d.ts +3 -0
- package/dist/apps/cli/src/commands/init.js +18 -0
- package/dist/apps/cli/src/commands/install.d.ts +3 -0
- package/dist/apps/cli/src/commands/install.js +244 -0
- package/dist/apps/cli/src/commands/modes.d.ts +3 -0
- package/dist/apps/cli/src/commands/modes.js +73 -0
- package/dist/apps/cli/src/commands/notify.d.ts +3 -0
- package/dist/apps/cli/src/commands/notify.js +430 -0
- package/dist/apps/cli/src/commands/report.d.ts +3 -0
- package/dist/apps/cli/src/commands/report.js +206 -0
- package/dist/apps/cli/src/commands/runtime.d.ts +10 -0
- package/dist/apps/cli/src/commands/runtime.js +499 -0
- package/dist/apps/cli/src/commands/shared.d.ts +9 -0
- package/dist/apps/cli/src/commands/shared.js +29 -0
- package/dist/apps/cli/src/commands/summary.d.ts +3 -0
- package/dist/apps/cli/src/commands/summary.js +15 -0
- package/dist/apps/cli/src/commands/sync.d.ts +3 -0
- package/dist/apps/cli/src/commands/sync.js +393 -0
- package/dist/apps/cli/src/commands/theme.d.ts +3 -0
- package/dist/apps/cli/src/commands/theme.js +181 -0
- package/dist/apps/cli/src/desktop-runtime.d.ts +54 -0
- package/dist/apps/cli/src/desktop-runtime.js +720 -0
- package/dist/apps/cli/src/home.d.ts +7 -0
- package/dist/apps/cli/src/home.js +124 -0
- package/dist/apps/cli/src/index.d.ts +3 -0
- package/dist/apps/cli/src/index.js +14 -0
- package/dist/apps/cli/src/install-profile.d.ts +35 -0
- package/dist/apps/cli/src/install-profile.js +124 -0
- package/dist/apps/cli/src/install-selector.d.ts +10 -0
- package/dist/apps/cli/src/install-selector.js +66 -0
- package/dist/apps/cli/src/interactive.d.ts +3 -0
- package/dist/apps/cli/src/interactive.js +32 -0
- package/dist/apps/cli/src/postinstall.d.ts +3 -0
- package/dist/apps/cli/src/postinstall.js +42 -0
- package/dist/apps/cli/src/release-installer.d.ts +57 -0
- package/dist/apps/cli/src/release-installer.js +432 -0
- package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
- package/dist/apps/cli/src/runtime-adapter.js +185 -0
- package/dist/apps/cli/src/slash.d.ts +15 -0
- package/dist/apps/cli/src/slash.js +229 -0
- package/dist/apps/cli/src/summary-model.d.ts +51 -0
- package/dist/apps/cli/src/summary-model.js +136 -0
- package/dist/apps/cli/src/theme.d.ts +18 -0
- package/dist/apps/cli/src/theme.js +118 -0
- package/dist/apps/cli/src/version.d.ts +2 -0
- package/dist/apps/cli/src/version.js +2 -0
- package/dist/packages/config/src/index.d.ts +3 -0
- package/dist/packages/config/src/index.js +3 -0
- package/dist/packages/config/src/load.d.ts +3 -0
- package/dist/packages/config/src/load.js +80 -0
- package/dist/packages/config/src/schema.d.ts +49 -0
- package/dist/packages/config/src/schema.js +28 -0
- package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
- package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
- package/dist/packages/connectors/aws/src/index.d.ts +35 -0
- package/dist/packages/connectors/aws/src/index.js +67 -0
- package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
- package/dist/packages/connectors/aws/src/normalize.js +141 -0
- package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
- package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
- package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
- package/dist/packages/connectors/cloudflare/src/client.js +107 -0
- package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
- package/dist/packages/connectors/cloudflare/src/index.js +81 -0
- package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
- package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
- package/dist/packages/connectors/mock/src/index.d.ts +58 -0
- package/dist/packages/connectors/mock/src/index.js +66 -0
- package/dist/packages/connectors/openai/src/index.d.ts +55 -0
- package/dist/packages/connectors/openai/src/index.js +169 -0
- package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
- package/dist/packages/connectors/openai/src/normalize.js +180 -0
- package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
- package/dist/packages/connectors/supabase/src/client.js +132 -0
- package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
- package/dist/packages/connectors/supabase/src/index.js +87 -0
- package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
- package/dist/packages/connectors/supabase/src/normalize.js +266 -0
- package/dist/packages/core/src/collector.d.ts +12 -0
- package/dist/packages/core/src/collector.js +68 -0
- package/dist/packages/core/src/index.d.ts +5 -0
- package/dist/packages/core/src/index.js +4 -0
- package/dist/packages/core/src/provider.d.ts +18 -0
- package/dist/packages/core/src/provider.js +2 -0
- package/dist/packages/core/src/risk-engine.d.ts +9 -0
- package/dist/packages/core/src/risk-engine.js +4 -0
- package/dist/packages/core/src/snapshots.d.ts +49 -0
- package/dist/packages/core/src/snapshots.js +9 -0
- package/dist/packages/db/src/client.d.ts +11 -0
- package/dist/packages/db/src/client.js +14 -0
- package/dist/packages/db/src/index.d.ts +6 -0
- package/dist/packages/db/src/index.js +6 -0
- package/dist/packages/db/src/local-store.d.ts +161 -0
- package/dist/packages/db/src/local-store.js +623 -0
- package/dist/packages/db/src/migrate.d.ts +17 -0
- package/dist/packages/db/src/migrate.js +35 -0
- package/dist/packages/db/src/schema.d.ts +5 -0
- package/dist/packages/db/src/schema.js +120 -0
- package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
- package/dist/packages/db/src/sqlite-bin.js +16 -0
- package/dist/packages/local-api/src/index.d.ts +2 -0
- package/dist/packages/local-api/src/index.js +2 -0
- package/dist/packages/local-api/src/server.d.ts +36 -0
- package/dist/packages/local-api/src/server.js +310 -0
- package/dist/packages/report/src/daily.d.ts +24 -0
- package/dist/packages/report/src/daily.js +9 -0
- package/dist/packages/report/src/index.d.ts +4 -0
- package/dist/packages/report/src/index.js +4 -0
- package/dist/packages/report/src/korean.d.ts +3 -0
- package/dist/packages/report/src/korean.js +62 -0
- package/dist/packages/report/src/slack.d.ts +34 -0
- package/dist/packages/report/src/slack.js +134 -0
- package/dist/packages/runtime/src/index.d.ts +2 -0
- package/dist/packages/runtime/src/index.js +2 -0
- package/dist/packages/runtime/src/runtime.d.ts +26 -0
- package/dist/packages/runtime/src/runtime.js +182 -0
- package/dist/packages/view-model/src/hud-model.d.ts +74 -0
- package/dist/packages/view-model/src/hud-model.js +295 -0
- package/dist/packages/view-model/src/index.d.ts +6 -0
- package/dist/packages/view-model/src/index.js +6 -0
- package/dist/packages/view-model/src/notification-preferences-model.d.ts +75 -0
- package/dist/packages/view-model/src/notification-preferences-model.js +400 -0
- package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
- package/dist/packages/view-model/src/notification-preferences.js +36 -0
- package/dist/packages/view-model/src/sync-state.d.ts +47 -0
- package/dist/packages/view-model/src/sync-state.js +140 -0
- package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
- package/dist/packages/view-model/src/usage-progress.js +57 -0
- package/dist/packages/view-model/src/view-model.d.ts +215 -0
- package/dist/packages/view-model/src/view-model.js +826 -0
- package/package.json +40 -0
- 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
|