total-recall-brain 3.41.4 → 3.41.6

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 (147) hide show
  1. package/bin/total-recall.mjs +10 -6
  2. package/docs/reference/cli-reference.md +18 -0
  3. package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
  4. package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
  5. package/frontend/dist/index.html +2 -2
  6. package/frontend/package.json +11 -0
  7. package/frontend/src/App.tsx +47 -122
  8. package/frontend/src/components/plugins/plugin-frame.ts +1 -0
  9. package/frontend/src/index.css +152 -86
  10. package/frontend/src/pages/HelpPage.tsx +13 -10
  11. package/frontend/src/pages/MemoryPage.tsx +28 -30
  12. package/frontend/src/pages/NetworkPage.tsx +3 -3
  13. package/frontend/src/pages/PluginsPage.tsx +6 -5
  14. package/frontend/src/pages/RulesPage.tsx +16 -12
  15. package/package.json +4 -3
  16. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
  17. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +4 -1
  18. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
  19. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +4 -1
  20. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
  21. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +4 -1
  22. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
  23. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +4 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3127 -49
  25. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -2
  26. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
  27. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
  28. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
  29. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
  30. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +5 -2
  31. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
  32. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
  33. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
  34. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
  36. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +40 -14
  38. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
  39. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +304 -46
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +5 -2
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +33 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
  76. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
  79. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3097 -81
  83. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +4 -1
  84. package/src/cli/agent.mjs +12 -1
  85. package/src/cli/compact.mjs +108 -0
  86. package/src/cli/connect.mjs +56 -1
  87. package/src/cli/context.mjs +55 -23
  88. package/src/cli/doctor.mjs +5 -2
  89. package/src/cli/forget.mjs +2 -0
  90. package/src/cli/harness.mjs +62 -50
  91. package/src/cli/memory-entry.mjs +2 -2
  92. package/src/cli/preflight.mjs +152 -16
  93. package/src/cli/release.mjs +386 -0
  94. package/src/cli/remember.mjs +19 -1
  95. package/src/cli/rules.mjs +54 -12
  96. package/src/cli/startup.mjs +1 -1
  97. package/src/cli/status.mjs +12 -2
  98. package/src/core/claude-hooks.mjs +41 -3
  99. package/src/core/cli-commands.mjs +2 -0
  100. package/src/core/cli-semantic-index.mjs +3 -2
  101. package/src/core/conflict-detector.mjs +23 -12
  102. package/src/core/context-compactor.mjs +145 -0
  103. package/src/core/context-policy.mjs +20 -0
  104. package/src/core/duplicate-guard.mjs +350 -0
  105. package/src/core/meta-harness.mjs +31 -11
  106. package/src/core/preflight-gate.mjs +2 -2
  107. package/src/core/preflight-receipt.mjs +29 -1
  108. package/src/core/scaffold-plugins.mjs +7 -0
  109. package/src/core/stdio-flush.mjs +23 -0
  110. package/src/core/surface.mjs +33 -10
  111. package/src/server/invoke-durability.mjs +39 -0
  112. package/src/server/invoke-write-child.mjs +25 -0
  113. package/src/server/routes/invoke.mjs +7 -0
  114. package/src/server/routes/plugins.mjs +2 -2
  115. package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
  116. package/frontend/dist/assets/index-CRzjQLu7.js +0 -5631
  117. package/frontend/src/components/DaemonLogsTab.tsx +0 -54
  118. package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
  119. package/frontend/src/components/TaskQueueTab.tsx +0 -193
  120. package/frontend/src/components/VoiceInput.tsx +0 -112
  121. package/frontend/src/pages/ChatPage.tsx +0 -1059
  122. package/frontend/src/pages/CollabPage.tsx +0 -470
  123. package/frontend/src/pages/DesignDocsPage.tsx +0 -621
  124. package/frontend/src/pages/FilesPage.tsx +0 -393
  125. package/frontend/src/pages/HealthPage.tsx +0 -359
  126. package/frontend/src/pages/InboxPage.tsx +0 -83
  127. package/frontend/src/pages/InstructionsPage.tsx +0 -583
  128. package/frontend/src/pages/IntegrationsPage.tsx +0 -761
  129. package/frontend/src/pages/MeshPage.css +0 -58
  130. package/frontend/src/pages/MeshPage.tsx +0 -1219
  131. package/frontend/src/pages/OkfPage.tsx +0 -535
  132. package/frontend/src/pages/SandboxPage.tsx +0 -113
  133. package/frontend/src/pages/SkillsPage.tsx +0 -1994
  134. package/frontend/src/pages/TasksPage.tsx +0 -427
  135. package/frontend/src/pages/UsagePage.tsx +0 -702
  136. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
  137. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
  138. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
  139. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
  140. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
  141. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
  142. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
  143. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
  144. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
  145. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
  146. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
  147. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +0 -575
@@ -1,702 +0,0 @@
1
- import { useState, useEffect, useMemo } from 'react';
2
- import { fetchUsageStats, fetchConfigJson, fetchProviderUsage } from '../api';
3
- import type { ProviderUsage } from '../api';
4
- import type { ConfigJson, UsageData } from '../types';
5
- import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
6
-
7
- export default function UsagePage() {
8
- const [usage, setUsage] = useState<UsageData | null>(null);
9
- const [config, setConfig] = useState<ConfigJson | null>(null);
10
- const [loading, setLoading] = useState(true);
11
- const [error, setError] = useState<string | null>(null);
12
- const [providerUsage, setProviderUsage] = useState<ProviderUsage | null>(null);
13
- const [providerLoading, setProviderLoading] = useState(false);
14
- const [providerError, setProviderError] = useState<string | null>(null);
15
-
16
- const fetchUsageAndConfig = async () => {
17
- try {
18
- const stats = await fetchUsageStats();
19
- setUsage(stats);
20
-
21
- const configData = await fetchConfigJson();
22
- setConfig(configData);
23
- setError(null);
24
- } catch (err: unknown) {
25
- setError((err as Error).message || 'Failed to sync budget consumption stats.');
26
- } finally {
27
- setLoading(false);
28
- }
29
- };
30
-
31
- const loadProviderUsage = async (refresh = false) => {
32
- setProviderLoading(true);
33
- try {
34
- setProviderUsage(await fetchProviderUsage(refresh));
35
- setProviderError(null);
36
- } catch (err: unknown) {
37
- setProviderError((err as Error).message || 'Failed to read provider billing data.');
38
- } finally {
39
- setProviderLoading(false);
40
- }
41
- };
42
-
43
- useEffect(() => {
44
- // eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate statistics sync on mount
45
- void fetchUsageAndConfig();
46
- const interval = setInterval(fetchUsageAndConfig, 5000);
47
- return () => clearInterval(interval);
48
- }, []);
49
-
50
- useEffect(() => {
51
- // Deliberately not on the 5s ledger interval — this hits live provider
52
- // billing APIs. The server caches hourly; refreshing is a manual action.
53
- // eslint-disable-next-line react-hooks/set-state-in-effect -- initial fetch on mount
54
- void loadProviderUsage();
55
- }, []);
56
-
57
-
58
-
59
- const dailyCap = config?.budget?.budget?.daily_cap_usd ?? 5.0;
60
- const weeklyCap = config?.budget?.budget?.weekly_cap_usd ?? 25.0;
61
- const budgetEnabled = config?.budget?.budget?.enabled !== false;
62
-
63
- const dailyPercent = Math.min(100, ((usage?.dailyUsd || 0) / dailyCap) * 100);
64
- const weeklyPercent = Math.min(100, ((usage?.weeklyUsd || 0) / weeklyCap) * 100);
65
-
66
- const getProgressColor = (percent: number) => {
67
- if (percent > 85) return 'var(--error)';
68
- if (percent > 60) return 'var(--warning)';
69
- return 'var(--success)';
70
- };
71
-
72
-
73
- const timeseriesData = useMemo(() => {
74
- if ((usage as unknown as Record<string, unknown>)?.timeseries && Object.keys((usage as unknown as Record<string, unknown>).timeseries as Record<string, unknown>).length > 0) {
75
- const ts = (usage as unknown as Record<string, unknown>).timeseries as Record<string, Record<string, { provider: string; cost: number }>>;
76
- return Object.entries(ts).map(([date, models]) => {
77
- const entry: Record<string, unknown> = { date: new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) };
78
- let geminiCost = 0, claudeCost = 0, openrouterCost = 0, codexCost = 0;
79
- for (const [, info] of Object.entries(models)) {
80
- const m = info as { provider: string; cost: number };
81
- if (m.provider === 'gemini') geminiCost += m.cost;
82
- else if (m.provider === 'claude') claudeCost += m.cost;
83
- else if (m.provider === 'openrouter') openrouterCost += m.cost;
84
- else codexCost += m.cost;
85
- }
86
- entry.gemini = +geminiCost.toFixed(3);
87
- entry.claude = +claudeCost.toFixed(3);
88
- entry.openrouter = +openrouterCost.toFixed(3);
89
- entry.codex = +codexCost.toFixed(3);
90
- return entry;
91
- });
92
- }
93
- return [];
94
- }, [usage]);
95
-
96
- if (loading) {
97
- return (
98
- <div className="page" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
99
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2.5" style={{ animation: 'spin 1s linear infinite', marginBottom: 12 }}>
100
- <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
101
- </svg>
102
- <div style={{ color: 'var(--text-secondary)' }}>Aggregating token expenditures...</div>
103
- </div>
104
- );
105
- }
106
-
107
- return (
108
- <div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflowY: 'auto' }}>
109
- <div className="page-header" style={{ marginBottom: 24 }}>
110
- <h1>Token & Budget Statistics</h1>
111
- <p>Monitor your token cost expenditure in real time against safety budget limits</p>
112
- </div>
113
-
114
- {/* Subscription Tier Banner */}
115
- <div style={{
116
- padding: '16px 20px',
117
- marginBottom: 24,
118
- background: 'rgba(18, 18, 26, 0.6)',
119
- backdropFilter: 'blur(12px)',
120
- border: '1px solid var(--border)',
121
- borderLeft: '3px solid var(--accent)',
122
- borderRadius: 10,
123
- display: 'flex',
124
- alignItems: 'center',
125
- gap: 14,
126
- }}>
127
- <span style={{ fontSize: 24 }}>🛡️</span>
128
- <div>
129
- <div style={{ fontSize: 14, fontWeight: 700, color: '#fff', letterSpacing: 0.3 }}>
130
- Local-first · Self-hosted
131
- </div>
132
- <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 2 }}>
133
- Memory and processing stay on your machine. No vendor lock-in.
134
- </div>
135
- </div>
136
- </div>
137
-
138
- {error && (
139
- <div className="badge badge-error" style={{ marginBottom: 20, display: 'inline-flex', padding: '6px 12px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #ef4444', color: '#f87171' }}>
140
- ⚠️ {error}
141
- </div>
142
- )}
143
-
144
- {/* Overview dollar cards */}
145
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(340px, 1fr))', gap: 24, marginBottom: 24 }}>
146
-
147
- {/* Daily Cap progress card */}
148
- <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
149
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
150
- <div>
151
- <h3 style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>Daily Expenditure</h3>
152
- <div style={{ fontSize: 28, fontWeight: 700, marginTop: 4, color: '#fff' }}>
153
- ${usage?.dailyUsd?.toFixed(4) ?? '0.0000'}
154
- </div>
155
- </div>
156
- <span className="badge" style={{ background: 'var(--bg-elevated)', color: 'var(--text-secondary)', border: '1px solid var(--border)', fontSize: 11 }}>
157
- Cap: ${dailyCap.toFixed(2)}
158
- </span>
159
- </div>
160
-
161
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8 }}>
162
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--text-secondary)' }}>
163
- <span>Consumption Ratio</span>
164
- <span>{dailyPercent.toFixed(1)}%</span>
165
- </div>
166
- <div style={{ height: 6, background: 'var(--bg-elevated)', borderRadius: 3, overflow: 'hidden' }}>
167
- <div style={{
168
- height: '100%',
169
- width: `${dailyPercent}%`,
170
- background: getProgressColor(dailyPercent),
171
- boxShadow: `0 0 8px ${getProgressColor(dailyPercent)}`,
172
- transition: 'width 0.4s ease'
173
- }} />
174
- </div>
175
- </div>
176
-
177
- <div style={{ fontSize: 10, color: 'var(--text-tertiary)', borderTop: '1px solid var(--border)', paddingTop: 10, marginTop: 4 }}>
178
- Status: {budgetEnabled ? '🛡️ Budget supervision active' : '⚠️ Limits unmonitored (disabled)'}
179
- </div>
180
- </div>
181
-
182
- {/* Weekly Cap progress card */}
183
- <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
184
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
185
- <div>
186
- <h3 style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>Weekly Expenditure</h3>
187
- <div style={{ fontSize: 28, fontWeight: 700, marginTop: 4, color: '#fff' }}>
188
- ${usage?.weeklyUsd?.toFixed(4) ?? '0.0000'}
189
- </div>
190
- </div>
191
- <span className="badge" style={{ background: 'var(--bg-elevated)', color: 'var(--text-secondary)', border: '1px solid var(--border)', fontSize: 11 }}>
192
- Cap: ${weeklyCap.toFixed(2)}
193
- </span>
194
- </div>
195
-
196
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8 }}>
197
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--text-secondary)' }}>
198
- <span>Consumption Ratio</span>
199
- <span>{weeklyPercent.toFixed(1)}%</span>
200
- </div>
201
- <div style={{ height: 6, background: 'var(--bg-elevated)', borderRadius: 3, overflow: 'hidden' }}>
202
- <div style={{
203
- height: '100%',
204
- width: `${weeklyPercent}%`,
205
- background: getProgressColor(weeklyPercent),
206
- boxShadow: `0 0 8px ${getProgressColor(weeklyPercent)}`,
207
- transition: 'width 0.4s ease'
208
- }} />
209
- </div>
210
- </div>
211
-
212
- <div style={{ fontSize: 10, color: 'var(--text-tertiary)', borderTop: '1px solid var(--border)', paddingTop: 10, marginTop: 4 }}>
213
- Rolling window: Last 7 days in local system time
214
- </div>
215
- </div>
216
-
217
- </div>
218
-
219
- {/* Per Model breakdown list */}
220
- <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', border: '1px solid var(--border)' }}>
221
- <h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, borderBottom: '1px solid var(--border)', paddingBottom: 10, marginBottom: 16 }}>
222
- Model Engine Spend breakdown
223
- </h3>
224
-
225
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 20 }}>
226
-
227
- {/* Gemini */}
228
- <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
229
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
230
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
231
- <span style={{ fontSize: 18 }}>♊</span>
232
- <div style={{ display: 'flex', flexDirection: 'column' }}>
233
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Gemini CLI</span>
234
- <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>~/.gemini/tmp</span>
235
- </div>
236
- </div>
237
- <span className="badge" style={{ background: 'rgba(59, 130, 246, 0.1)', color: '#60a5fa', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
238
- Google API
239
- </span>
240
- </div>
241
-
242
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
243
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
244
- <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
245
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.gemini?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
246
- </div>
247
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
248
- <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
249
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.gemini?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
250
- </div>
251
- <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
252
- <span>Rolling Tokens:</span>
253
- <span>{usage?.breakdown?.gemini?.dailyTokens?.toLocaleString() || 0} (24h)</span>
254
- </div>
255
- </div>
256
- </div>
257
-
258
- {/* Claude */}
259
- <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
260
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
261
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
262
- <span style={{ fontSize: 18 }}>🍊</span>
263
- <div style={{ display: 'flex', flexDirection: 'column' }}>
264
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Claude Code CLI</span>
265
- <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>~/.claude</span>
266
- </div>
267
- </div>
268
- <span className="badge" style={{ background: 'rgba(239, 107, 107, 0.1)', color: '#f87171', border: '1px solid rgba(239, 107, 107, 0.2)' }}>
269
- Anthropic API
270
- </span>
271
- </div>
272
-
273
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
274
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
275
- <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
276
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.claude?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
277
- </div>
278
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
279
- <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
280
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.claude?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
281
- </div>
282
- <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
283
- <span>Rolling Tokens:</span>
284
- <span>{usage?.breakdown?.claude?.dailyTokens?.toLocaleString() || 0} (24h)</span>
285
- </div>
286
- </div>
287
- </div>
288
-
289
- {/* Codex */}
290
- <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
291
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
292
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
293
- <span style={{ fontSize: 18 }}>💻</span>
294
- <div style={{ display: 'flex', flexDirection: 'column' }}>
295
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Codex CLI</span>
296
- <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>~/.codex/sessions</span>
297
- </div>
298
- </div>
299
- <span className="badge" style={{ background: 'rgba(16, 185, 129, 0.1)', color: '#34d399', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
300
- OpenAI API
301
- </span>
302
- </div>
303
-
304
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
305
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
306
- <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
307
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.codex?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
308
- </div>
309
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
310
- <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
311
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.codex?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
312
- </div>
313
- <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
314
- <span>Rolling Tokens:</span>
315
- <span>{usage?.breakdown?.codex?.dailyTokens?.toLocaleString() || 0} (24h)</span>
316
- </div>
317
- </div>
318
- </div>
319
- {/* OpenRouter */}
320
- <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
321
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
322
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
323
- <span style={{ fontSize: 18 }}>🌐</span>
324
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>OpenRouter</span>
325
- </div>
326
- <span className="badge" style={{ background: 'rgba(236, 72, 153, 0.1)', color: '#f472b6', border: '1px solid rgba(236, 72, 153, 0.2)' }}>
327
- OR API
328
- </span>
329
- </div>
330
-
331
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
332
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
333
- <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
334
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.openrouter?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
335
- </div>
336
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
337
- <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
338
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.openrouter?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
339
- </div>
340
- <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
341
- <span>Rolling Tokens:</span>
342
- <span>{usage?.breakdown?.openrouter?.dailyTokens?.toLocaleString() || 0} (24h)</span>
343
- </div>
344
- </div>
345
- </div>
346
-
347
- {/* Tavily */}
348
- <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
349
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
350
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
351
- <span style={{ fontSize: 18 }}>🔎</span>
352
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Tavily Search</span>
353
- </div>
354
- <span className="badge" style={{ background: 'rgba(234, 179, 8, 0.1)', color: '#eab308', border: '1px solid rgba(234, 179, 8, 0.2)' }}>
355
- Web API
356
- </span>
357
- </div>
358
-
359
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
360
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
361
- <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
362
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.tavily?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
363
- </div>
364
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
365
- <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
366
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.tavily?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
367
- </div>
368
- <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
369
- <span>Rolling Tokens:</span>
370
- <span>{usage?.breakdown?.tavily?.dailyTokens?.toLocaleString() || 0} (24h)</span>
371
- </div>
372
- </div>
373
- </div>
374
-
375
- {/* Brave */}
376
- <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
377
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
378
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
379
- <span style={{ fontSize: 18 }}>🦁</span>
380
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Brave Search</span>
381
- </div>
382
- <span className="badge" style={{ background: 'rgba(234, 179, 8, 0.1)', color: '#eab308', border: '1px solid rgba(234, 179, 8, 0.2)' }}>
383
- Web API
384
- </span>
385
- </div>
386
-
387
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
388
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
389
- <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
390
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.brave?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
391
- </div>
392
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
393
- <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
394
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.brave?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
395
- </div>
396
- <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
397
- <span>Rolling Tokens:</span>
398
- <span>{usage?.breakdown?.brave?.dailyTokens?.toLocaleString() || 0} (24h)</span>
399
- </div>
400
- </div>
401
- </div>
402
-
403
- {/* Exa */}
404
- <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
405
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
406
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
407
- <span style={{ fontSize: 18 }}>✨</span>
408
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Exa Search</span>
409
- </div>
410
- <span className="badge" style={{ background: 'rgba(234, 179, 8, 0.1)', color: '#eab308', border: '1px solid rgba(234, 179, 8, 0.2)' }}>
411
- Web API
412
- </span>
413
- </div>
414
-
415
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
416
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
417
- <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
418
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.exa?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
419
- </div>
420
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
421
- <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
422
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.exa?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
423
- </div>
424
- <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
425
- <span>Rolling Tokens:</span>
426
- <span>{usage?.breakdown?.exa?.dailyTokens?.toLocaleString() || 0} (24h)</span>
427
- </div>
428
- </div>
429
- </div>
430
-
431
- {/* Serper */}
432
- <div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
433
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
434
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
435
- <span style={{ fontSize: 18 }}>🔍</span>
436
- <span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Serper Search</span>
437
- </div>
438
- <span className="badge" style={{ background: 'rgba(234, 179, 8, 0.1)', color: '#eab308', border: '1px solid rgba(234, 179, 8, 0.2)' }}>
439
- Web API
440
- </span>
441
- </div>
442
-
443
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
444
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
445
- <span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
446
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.serper?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
447
- </div>
448
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
449
- <span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
450
- <span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown?.serper?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
451
- </div>
452
- <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
453
- <span>Rolling Tokens:</span>
454
- <span>{usage?.breakdown?.serper?.dailyTokens?.toLocaleString() || 0} (24h)</span>
455
- </div>
456
- </div>
457
- </div>
458
-
459
- </div>
460
- </div>
461
-
462
- {/* ── Timeseries Cost Chart ── */}
463
- <div className="card" style={{ padding: 24, marginTop: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
464
- <h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, borderBottom: '1px solid var(--border)', paddingBottom: 10, marginBottom: 20 }}>
465
- 30-Day Cost Trend
466
- </h3>
467
- {timeseriesData.length === 0 ? (
468
- <div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-secondary)' }}>
469
- No usage data available for the last 30 days.
470
- </div>
471
- ) : (
472
- <ResponsiveContainer width="100%" height={300}>
473
- <AreaChart data={timeseriesData} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
474
- <defs>
475
- <linearGradient id="gradGemini" x1="0" y1="0" x2="0" y2="1">
476
- <stop offset="5%" stopColor="#3b82f6" stopOpacity={0.4} />
477
- <stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
478
- </linearGradient>
479
- <linearGradient id="gradClaude" x1="0" y1="0" x2="0" y2="1">
480
- <stop offset="5%" stopColor="#e17055" stopOpacity={0.4} />
481
- <stop offset="95%" stopColor="#e17055" stopOpacity={0} />
482
- </linearGradient>
483
- <linearGradient id="gradOpenRouter" x1="0" y1="0" x2="0" y2="1">
484
- <stop offset="5%" stopColor="#74b9ff" stopOpacity={0.4} />
485
- <stop offset="95%" stopColor="#74b9ff" stopOpacity={0} />
486
- </linearGradient>
487
- <linearGradient id="gradCodex" x1="0" y1="0" x2="0" y2="1">
488
- <stop offset="5%" stopColor="#00cec9" stopOpacity={0.4} />
489
- <stop offset="95%" stopColor="#00cec9" stopOpacity={0} />
490
- </linearGradient>
491
- </defs>
492
- <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
493
- <XAxis dataKey="date" tick={{ fontSize: 11, fill: 'var(--text-secondary)' }} axisLine={{ stroke: 'var(--border)' }} tickLine={false} />
494
- <YAxis tick={{ fontSize: 11, fill: 'var(--text-secondary)' }} axisLine={{ stroke: 'var(--border)' }} tickLine={false} tickFormatter={(v: number) => `$${v.toFixed(2)}`} />
495
- <Tooltip
496
- contentStyle={{ background: '#1a1a2e', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, fontSize: 12, color: '#e8e8e8' }}
497
- labelStyle={{ color: '#fff', fontWeight: 600, marginBottom: 4 }}
498
- formatter={(value: unknown, name: unknown) => [`$${Number(value).toFixed(3)}`, String(name).charAt(0).toUpperCase() + String(name).slice(1)]}
499
- />
500
- <Legend wrapperStyle={{ fontSize: 12, color: 'var(--text-secondary)' }} />
501
- <Area type="monotone" dataKey="gemini" stackId="1" stroke="#3b82f6" fill="url(#gradGemini)" strokeWidth={2} />
502
- <Area type="monotone" dataKey="claude" stackId="1" stroke="#e17055" fill="url(#gradClaude)" strokeWidth={2} />
503
- <Area type="monotone" dataKey="openrouter" stackId="1" stroke="#74b9ff" fill="url(#gradOpenRouter)" strokeWidth={2} />
504
- <Area type="monotone" dataKey="codex" stackId="1" stroke="#00cec9" fill="url(#gradCodex)" strokeWidth={2} />
505
- </AreaChart>
506
- </ResponsiveContainer>
507
- )}
508
- </div>
509
-
510
- {/* ── Provider Surface Cards ── */}
511
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16, marginTop: 24 }}>
512
- {([
513
- { name: 'Gemini', color: '#3b82f6', emoji: '♊', key: 'gemini' as const },
514
- { name: 'Claude', color: '#e17055', emoji: '🍊', key: 'claude' as const },
515
- { name: 'OpenRouter', color: '#74b9ff', emoji: '🌐', key: 'openrouter' as const },
516
- { name: 'Codex', color: '#00cec9', emoji: '💻', key: 'codex' as const },
517
- { name: 'Tavily', color: '#eab308', emoji: '🔎', key: 'tavily' as const },
518
- { name: 'Brave', color: '#eab308', emoji: '🦁', key: 'brave' as const },
519
- { name: 'Exa', color: '#eab308', emoji: '✨', key: 'exa' as const },
520
- { name: 'Serper', color: '#eab308', emoji: '🔍', key: 'serper' as const },
521
- ] as const).map((provider) => {
522
- const data = usage?.breakdown?.[provider.key as keyof typeof usage.breakdown];
523
- return (
524
- <div key={provider.key} style={{
525
- background: 'rgba(18, 18, 26, 0.6)',
526
- backdropFilter: 'blur(12px)',
527
- border: '1px solid var(--border)',
528
- borderTop: `4px solid ${provider.color}`,
529
- borderRadius: 10,
530
- padding: 20,
531
- display: 'flex',
532
- flexDirection: 'column',
533
- gap: 12,
534
- transition: 'transform 0.2s ease, box-shadow 0.2s ease',
535
- }}
536
- onMouseEnter={(e) => { (e.currentTarget as HTMLDivElement).style.transform = 'translateY(-2px)'; (e.currentTarget as HTMLDivElement).style.boxShadow = `0 8px 24px ${provider.color}22`; }}
537
- onMouseLeave={(e) => { (e.currentTarget as HTMLDivElement).style.transform = 'translateY(0)'; (e.currentTarget as HTMLDivElement).style.boxShadow = 'none'; }}
538
- >
539
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
540
- <span style={{ fontSize: 20 }}>{provider.emoji}</span>
541
- <span style={{ fontSize: 14, fontWeight: 700, color: '#fff' }}>{provider.name}</span>
542
- </div>
543
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
544
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
545
- <span style={{ color: 'var(--text-secondary)' }}>Daily</span>
546
- <span style={{ fontWeight: 600, color: provider.color }}>${data?.dailyUsd?.toFixed(4) ?? '0.0000'}</span>
547
- </div>
548
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
549
- <span style={{ color: 'var(--text-secondary)' }}>Weekly</span>
550
- <span style={{ fontWeight: 600, color: provider.color }}>${data?.weeklyUsd?.toFixed(4) ?? '0.0000'}</span>
551
- </div>
552
- {data?.dailyTokens != null && (
553
- <div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 2, fontSize: 11, color: 'var(--text-tertiary)' }}>
554
- <span>Tokens (24h)</span>
555
- <span>{data.dailyTokens.toLocaleString()}</span>
556
- </div>
557
- )}
558
- </div>
559
- </div>
560
- );
561
- })}
562
- </div>
563
-
564
- {/* ── Per-Model Token Breakdown Table ── */}
565
- <div className="card" style={{ padding: 24, marginTop: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
566
- <h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, borderBottom: '1px solid var(--border)', paddingBottom: 10, marginBottom: 16 }}>
567
- Per-Model Breakdown
568
- </h3>
569
- <div style={{ overflowX: 'auto' }}>
570
- <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
571
- <thead>
572
- <tr style={{ borderBottom: '1px solid var(--border)' }}>
573
- {['Provider', 'Daily ($)', 'Weekly ($)', 'Daily Tokens', 'Weekly Tokens'].map((h) => (
574
- <th key={h} style={{ textAlign: 'left', padding: '10px 12px', fontSize: 11, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>{h}</th>
575
- ))}
576
- </tr>
577
- </thead>
578
- <tbody>
579
- {([
580
- { name: 'Gemini', key: 'gemini' as const, color: '#3b82f6' },
581
- { name: 'Claude', key: 'claude' as const, color: '#e17055' },
582
- { name: 'Codex', key: 'codex' as const, color: '#00cec9' },
583
- { name: 'OpenRouter', key: 'openrouter' as const, color: '#74b9ff' },
584
- ] as const).map((row, idx) => {
585
- const d = row.key === 'openrouter' ? usage?.breakdown?.openrouter : usage?.breakdown?.[row.key];
586
- return (
587
- <tr key={row.key} style={{ background: idx % 2 === 1 ? 'rgba(255,255,255,0.02)' : 'transparent', borderBottom: '1px solid var(--border)' }}>
588
- <td style={{ padding: '10px 12px', fontWeight: 600, color: row.color }}>{row.name}</td>
589
- <td style={{ padding: '10px 12px', color: '#fff' }}>${d?.dailyUsd?.toFixed(4) ?? '0.0000'}</td>
590
- <td style={{ padding: '10px 12px', color: '#fff' }}>${d?.weeklyUsd?.toFixed(4) ?? '0.0000'}</td>
591
- <td style={{ padding: '10px 12px', color: 'var(--text-secondary)' }}>{d?.dailyTokens?.toLocaleString() || '—'}</td>
592
- <td style={{ padding: '10px 12px', color: 'var(--text-secondary)' }}>{d?.weeklyTokens?.toLocaleString() || '—'}</td>
593
- </tr>
594
- );
595
- })}
596
- {/* Totals row */}
597
- <tr style={{ borderTop: '2px solid var(--border)', background: 'rgba(108, 92, 231, 0.05)' }}>
598
- <td style={{ padding: '10px 12px', fontWeight: 700, color: '#fff' }}>Total</td>
599
- <td style={{ padding: '10px 12px', fontWeight: 700, color: '#fff' }}>
600
- ${(
601
- (usage?.breakdown?.gemini?.dailyUsd || 0) +
602
- (usage?.breakdown?.claude?.dailyUsd || 0) +
603
- (usage?.breakdown?.codex?.dailyUsd || 0) +
604
- (usage?.breakdown?.openrouter?.dailyUsd || 0)
605
- ).toFixed(4)}
606
- </td>
607
- <td style={{ padding: '10px 12px', fontWeight: 700, color: '#fff' }}>
608
- ${(
609
- (usage?.breakdown?.gemini?.weeklyUsd || 0) +
610
- (usage?.breakdown?.claude?.weeklyUsd || 0) +
611
- (usage?.breakdown?.codex?.weeklyUsd || 0) +
612
- (usage?.breakdown?.openrouter?.weeklyUsd || 0)
613
- ).toFixed(4)}
614
- </td>
615
- <td style={{ padding: '10px 12px', fontWeight: 700, color: 'var(--text-secondary)' }}>
616
- {(
617
- (usage?.breakdown?.gemini?.dailyTokens || 0) +
618
- (usage?.breakdown?.claude?.dailyTokens || 0) +
619
- (usage?.breakdown?.codex?.dailyTokens || 0) +
620
- (usage?.breakdown?.openrouter?.dailyTokens || 0)
621
- ).toLocaleString()}
622
- </td>
623
- <td style={{ padding: '10px 12px', fontWeight: 700, color: 'var(--text-secondary)' }}>
624
- {(
625
- (usage?.breakdown?.gemini?.weeklyTokens || 0) +
626
- (usage?.breakdown?.claude?.weeklyTokens || 0) +
627
- (usage?.breakdown?.codex?.weeklyTokens || 0) +
628
- (usage?.breakdown?.openrouter?.weeklyTokens || 0)
629
- ).toLocaleString()}
630
- </td>
631
- </tr>
632
- </tbody>
633
- </table>
634
- </div>
635
- </div>
636
-
637
- {/* ── Provider-Reported Spend ──
638
- Everything above is estimated locally: observed tokens × a hardcoded
639
- price table. This panel is what the providers themselves bill. */}
640
- <div className="card" style={{ padding: 24, marginTop: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
641
- <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', borderBottom: '1px solid var(--border)', paddingBottom: 10, marginBottom: 16 }}>
642
- <h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, margin: 0 }}>
643
- Reported by Provider
644
- </h3>
645
- <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
646
- {providerUsage?.fetched_at && (
647
- <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
648
- {new Date(providerUsage.fetched_at).toLocaleString()}
649
- {providerUsage.from_cache ? ' (cached)' : ''}
650
- </span>
651
- )}
652
- <button
653
- type="button"
654
- onClick={() => { void loadProviderUsage(true); }}
655
- disabled={providerLoading}
656
- style={{ fontSize: 11, padding: '4px 10px', borderRadius: 6, border: '1px solid var(--border)', background: 'transparent', color: 'var(--text-secondary)', cursor: providerLoading ? 'wait' : 'pointer' }}
657
- >
658
- {providerLoading ? 'Refreshing…' : 'Refresh'}
659
- </button>
660
- </div>
661
- </div>
662
-
663
- {providerError && (
664
- <div style={{ fontSize: 12, color: 'var(--error)', marginBottom: 12 }}>{providerError}</div>
665
- )}
666
-
667
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 16 }}>
668
- {Object.entries(providerUsage?.providers || {}).map(([id, p]) => (
669
- <div key={id} style={{
670
- border: '1px solid var(--border)',
671
- borderLeft: `3px solid ${p.state === 'ok' ? 'var(--success)' : p.state === 'error' ? 'var(--error)' : 'var(--warning)'}`,
672
- borderRadius: 8,
673
- padding: 16,
674
- }}>
675
- <div style={{ fontSize: 13, fontWeight: 700, color: '#fff', marginBottom: 8 }}>{p.label}</div>
676
- {p.state === 'ok' ? (
677
- <>
678
- <div style={{ fontSize: 22, fontWeight: 700, color: '#fff' }}>
679
- ${(p.total_cost ?? 0).toFixed(2)}
680
- </div>
681
- <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>
682
- {/* Never label a lifetime figure as a windowed one. */}
683
- {p.lifetime ? 'lifetime to date' : `last ${providerUsage?.window_days ?? 30} days`}
684
- {typeof p.balance === 'number' ? ` · $${p.balance.toFixed(2)} remaining` : ''}
685
- </div>
686
- </>
687
- ) : (
688
- <div style={{ fontSize: 11, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
689
- {p.reason}
690
- </div>
691
- )}
692
- </div>
693
- ))}
694
- {!providerUsage && !providerError && (
695
- <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Loading provider billing data…</div>
696
- )}
697
- </div>
698
- </div>
699
-
700
- </div>
701
- );
702
- }