codex-usage-tracking 0.3.0__py3-none-any.whl
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.
- codex_usage_tracker/__init__.py +7 -0
- codex_usage_tracker/__main__.py +6 -0
- codex_usage_tracker/allowance.py +759 -0
- codex_usage_tracker/api_payloads.py +90 -0
- codex_usage_tracker/cli.py +1326 -0
- codex_usage_tracker/context.py +410 -0
- codex_usage_tracker/costing.py +176 -0
- codex_usage_tracker/dashboard.py +389 -0
- codex_usage_tracker/diagnostics.py +624 -0
- codex_usage_tracker/formatting.py +225 -0
- codex_usage_tracker/json_contracts.py +350 -0
- codex_usage_tracker/mcp_server.py +371 -0
- codex_usage_tracker/models.py +92 -0
- codex_usage_tracker/parser.py +491 -0
- codex_usage_tracker/paths.py +18 -0
- codex_usage_tracker/plugin_data/__init__.py +1 -0
- codex_usage_tracker/plugin_data/assets/icon.svg +8 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard.css +954 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard.js +1833 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +155 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_format.js +132 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_state.js +157 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_template.html +141 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-calls.png +0 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-details.png +0 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-insights.png +0 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-threads.png +0 -0
- codex_usage_tracker/plugin_data/docs/dashboard-guide.html +136 -0
- codex_usage_tracker/plugin_data/rate_cards/codex-credit-rates.json +69 -0
- codex_usage_tracker/plugin_data/skills/codex-usage-api/SKILL.md +62 -0
- codex_usage_tracker/plugin_data/skills/codex-usage-tracker/SKILL.md +47 -0
- codex_usage_tracker/plugin_installer.py +312 -0
- codex_usage_tracker/pricing.py +57 -0
- codex_usage_tracker/pricing_config.py +223 -0
- codex_usage_tracker/pricing_estimates.py +44 -0
- codex_usage_tracker/pricing_openai.py +253 -0
- codex_usage_tracker/projects.py +347 -0
- codex_usage_tracker/recommendations.py +270 -0
- codex_usage_tracker/reports.py +637 -0
- codex_usage_tracker/schema.py +71 -0
- codex_usage_tracker/server.py +400 -0
- codex_usage_tracker/store.py +666 -0
- codex_usage_tracker/support.py +147 -0
- codex_usage_tracker/threads.py +183 -0
- codex_usage_tracking-0.3.0.dist-info/METADATA +278 -0
- codex_usage_tracking-0.3.0.dist-info/RECORD +50 -0
- codex_usage_tracking-0.3.0.dist-info/WHEEL +5 -0
- codex_usage_tracking-0.3.0.dist-info/entry_points.txt +2 -0
- codex_usage_tracking-0.3.0.dist-info/licenses/LICENSE +21 -0
- codex_usage_tracking-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1833 @@
|
|
|
1
|
+
const dashboardFormat = window.CodexUsageDashboardFormat;
|
|
2
|
+
const dashboardData = window.CodexUsageDashboardData;
|
|
3
|
+
const {
|
|
4
|
+
number,
|
|
5
|
+
money,
|
|
6
|
+
credits,
|
|
7
|
+
pct,
|
|
8
|
+
short,
|
|
9
|
+
escapeHtml,
|
|
10
|
+
truncate,
|
|
11
|
+
formatTimestamp,
|
|
12
|
+
formatTimestampTitle,
|
|
13
|
+
renderTimeCell,
|
|
14
|
+
defaultSortDirection,
|
|
15
|
+
textValue,
|
|
16
|
+
compareValues,
|
|
17
|
+
sortLabel,
|
|
18
|
+
} = dashboardFormat;
|
|
19
|
+
const {
|
|
20
|
+
payloadRows,
|
|
21
|
+
payloadLimit,
|
|
22
|
+
limitValue,
|
|
23
|
+
optionValueExists,
|
|
24
|
+
clamp,
|
|
25
|
+
usageCreditValue,
|
|
26
|
+
usageCreditStatusText,
|
|
27
|
+
sumUsageCredits,
|
|
28
|
+
creditCoverageRatio,
|
|
29
|
+
isAutoReview,
|
|
30
|
+
isSubagent,
|
|
31
|
+
sourceLabel,
|
|
32
|
+
resolvedParentThreadName,
|
|
33
|
+
resolvedParentSessionUpdatedAt,
|
|
34
|
+
resolveThreadAttachment,
|
|
35
|
+
chronological,
|
|
36
|
+
compactListSummary,
|
|
37
|
+
threadModelSummary,
|
|
38
|
+
} = dashboardData;
|
|
39
|
+
const initialPayload = JSON.parse(document.getElementById('usage-data').textContent);
|
|
40
|
+
const stateManager = window.CodexUsageDashboardState;
|
|
41
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
42
|
+
const initialState = stateManager ? stateManager.read(urlParams) : {};
|
|
43
|
+
let data = payloadRows(initialPayload);
|
|
44
|
+
let pricingConfigured = Boolean(initialPayload.pricing_configured);
|
|
45
|
+
let pricingSource = initialPayload.pricing_source || {};
|
|
46
|
+
let pricingSnapshotWarning = initialPayload.pricing_snapshot_warning || '';
|
|
47
|
+
let allowanceConfigured = Boolean(initialPayload.allowance_configured);
|
|
48
|
+
let allowanceSource = initialPayload.allowance_source || {};
|
|
49
|
+
let allowanceWindows = Array.isArray(initialPayload.allowance_windows) ? initialPayload.allowance_windows : [];
|
|
50
|
+
let allowanceError = initialPayload.allowance_error || '';
|
|
51
|
+
let rateCardError = initialPayload.rate_card_error || '';
|
|
52
|
+
let projectMetadataPrivacy = initialPayload.project_metadata_privacy || { mode: initialPayload.privacy_mode || 'normal' };
|
|
53
|
+
let parserDiagnostics = initialPayload.parser_diagnostics || {};
|
|
54
|
+
let apiToken = initialPayload.api_token || '';
|
|
55
|
+
let contextApiEnabled = Boolean(initialPayload.context_api_enabled);
|
|
56
|
+
let actionThresholds = initialPayload.action_thresholds || {};
|
|
57
|
+
let totalAvailableRows = Number(initialPayload.total_available_rows || data.length);
|
|
58
|
+
let activeAvailableRows = Number(initialPayload.active_available_rows || data.length);
|
|
59
|
+
let allHistoryAvailableRows = Number(initialPayload.all_history_available_rows || totalAvailableRows);
|
|
60
|
+
let archivedAvailableRows = Number(initialPayload.archived_available_rows || Math.max(allHistoryAvailableRows - activeAvailableRows, 0));
|
|
61
|
+
let loadedLimit = payloadLimit(initialPayload);
|
|
62
|
+
const rowsEl = document.getElementById('rows');
|
|
63
|
+
const detailEl = document.getElementById('detail');
|
|
64
|
+
const searchEl = document.getElementById('search');
|
|
65
|
+
const modelEl = document.getElementById('model');
|
|
66
|
+
const effortEl = document.getElementById('effort');
|
|
67
|
+
const pricingStatusEl = document.getElementById('pricingStatus');
|
|
68
|
+
const datePresetEl = document.getElementById('datePreset');
|
|
69
|
+
const dateStartEl = document.getElementById('dateStart');
|
|
70
|
+
const dateEndEl = document.getElementById('dateEnd');
|
|
71
|
+
const dateRangeStatusEl = document.getElementById('dateRangeStatus');
|
|
72
|
+
const sortEl = document.getElementById('sort');
|
|
73
|
+
const tableTitleEl = document.getElementById('tableTitle');
|
|
74
|
+
const tableCaptionEl = document.getElementById('tableCaption');
|
|
75
|
+
const insightsViewEl = document.getElementById('insightsView');
|
|
76
|
+
const callsViewEl = document.getElementById('callsView');
|
|
77
|
+
const threadsViewEl = document.getElementById('threadsView');
|
|
78
|
+
const insightsPanelEl = document.getElementById('insightsPanel');
|
|
79
|
+
const insightCardsEl = document.getElementById('insightCards');
|
|
80
|
+
const presetListEl = document.getElementById('presetList');
|
|
81
|
+
const presetStatusEl = document.getElementById('presetStatus');
|
|
82
|
+
const clearPresetEl = document.getElementById('clearPreset');
|
|
83
|
+
const refreshDashboardEl = document.getElementById('refreshDashboard');
|
|
84
|
+
const autoRefreshEl = document.getElementById('autoRefresh');
|
|
85
|
+
const loadLimitEl = document.getElementById('loadLimit');
|
|
86
|
+
const historyScopeEl = document.getElementById('historyScope');
|
|
87
|
+
const liveStatusEl = document.getElementById('liveStatus');
|
|
88
|
+
const copyViewLinkEl = document.getElementById('copyViewLink');
|
|
89
|
+
const exportVisibleEl = document.getElementById('exportVisible');
|
|
90
|
+
const actionStatusEl = document.getElementById('actionStatus');
|
|
91
|
+
const prevPageEl = document.getElementById('prevPage');
|
|
92
|
+
const nextPageEl = document.getElementById('nextPage');
|
|
93
|
+
const pageStatusEl = document.getElementById('pageStatus');
|
|
94
|
+
const pagerEl = document.getElementById('pager');
|
|
95
|
+
const toTopEl = document.getElementById('toTop');
|
|
96
|
+
let rowByRecordId = new Map();
|
|
97
|
+
let threadAttachmentByRecordId = new Map();
|
|
98
|
+
const expandedThreads = new Set();
|
|
99
|
+
const liveRefreshSupported = window.location.protocol !== 'file:';
|
|
100
|
+
const initialPayloadIncludeArchived = Boolean(initialPayload.include_archived);
|
|
101
|
+
let includeArchived = initialPayloadIncludeArchived;
|
|
102
|
+
if (liveRefreshSupported && initialState.historyScope === 'all') includeArchived = true;
|
|
103
|
+
const needsInitialHistoryRefresh = liveRefreshSupported && includeArchived !== initialPayloadIncludeArchived;
|
|
104
|
+
const liveRefreshIntervalMs = 10000;
|
|
105
|
+
const pageSize = 500;
|
|
106
|
+
const datePresetLabels = {
|
|
107
|
+
all: 'All time',
|
|
108
|
+
today: 'Today',
|
|
109
|
+
'this-week': 'This week',
|
|
110
|
+
'last-7-days': 'Last 7 days',
|
|
111
|
+
'this-month': 'This month',
|
|
112
|
+
custom: 'Custom range',
|
|
113
|
+
};
|
|
114
|
+
const allowedDatePresets = new Set(Object.keys(datePresetLabels));
|
|
115
|
+
let activeView = ['calls', 'threads', 'insights'].includes(initialState.view) ? initialState.view : 'insights';
|
|
116
|
+
let sortKey = optionValueExists(sortEl, initialState.sort) ? initialState.sort : sortEl.value || 'attention';
|
|
117
|
+
let sortDirection = ['asc', 'desc'].includes(initialState.direction) ? initialState.direction : defaultSortDirection(sortKey);
|
|
118
|
+
let activePreset = '';
|
|
119
|
+
let selectedRecordId = initialState.record || '';
|
|
120
|
+
let selectedThreadKey = initialState.thread || '';
|
|
121
|
+
let refreshInFlight = false;
|
|
122
|
+
let autoRefreshTimer = null;
|
|
123
|
+
let currentPage = 1;
|
|
124
|
+
let initialThreadExpansionApplied = false;
|
|
125
|
+
let initialDetailApplied = false;
|
|
126
|
+
const presetDefinitions = [
|
|
127
|
+
{
|
|
128
|
+
key: 'highest-cost',
|
|
129
|
+
label: 'Highest-cost threads',
|
|
130
|
+
description: 'Threads sorted by estimated spend, with subagents attached.',
|
|
131
|
+
view: 'threads',
|
|
132
|
+
sort: 'cost',
|
|
133
|
+
direction: 'desc',
|
|
134
|
+
caption: 'Highest-cost threads preset',
|
|
135
|
+
matches: () => true,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: 'context-bloat',
|
|
139
|
+
label: 'Context bloat',
|
|
140
|
+
description: 'Calls over 60% context use or with very high cumulative tokens.',
|
|
141
|
+
view: 'calls',
|
|
142
|
+
sort: 'context',
|
|
143
|
+
direction: 'desc',
|
|
144
|
+
caption: 'Context bloat preset',
|
|
145
|
+
matches: row => Number(row.context_window_percent || 0) >= threshold('high_context_percent', 0.6) || Number(row.cumulative_total_tokens || 0) >= threshold('large_cumulative_tokens', 200000),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: 'cache-misses',
|
|
149
|
+
label: 'Cache misses',
|
|
150
|
+
description: 'Low cache-ratio calls grouped by cwd, model, and thread.',
|
|
151
|
+
view: 'calls',
|
|
152
|
+
sort: 'cache',
|
|
153
|
+
direction: 'asc',
|
|
154
|
+
caption: 'Cache misses preset',
|
|
155
|
+
matches: row => Number(row.input_tokens || 0) > 0 && Number(row.cache_ratio || 0) < threshold('low_cache_ratio', 0.3),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: 'pricing-gaps',
|
|
159
|
+
label: 'Pricing gaps',
|
|
160
|
+
description: 'Unpriced usage that makes estimated cost totals incomplete.',
|
|
161
|
+
view: 'calls',
|
|
162
|
+
sort: 'total',
|
|
163
|
+
direction: 'desc',
|
|
164
|
+
pricingStatus: 'unpriced',
|
|
165
|
+
caption: 'Pricing gaps preset',
|
|
166
|
+
matches: row => !row.pricing_model,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: 'estimated-review',
|
|
170
|
+
label: 'Estimated-price review',
|
|
171
|
+
description: 'Usage priced with marked best-guess estimates.',
|
|
172
|
+
view: 'calls',
|
|
173
|
+
sort: 'cost',
|
|
174
|
+
direction: 'desc',
|
|
175
|
+
pricingStatus: 'estimated',
|
|
176
|
+
caption: 'Estimated-price review preset',
|
|
177
|
+
matches: row => Boolean(row.pricing_estimated),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
key: 'usage-credits',
|
|
181
|
+
label: 'Highest Codex credits',
|
|
182
|
+
description: 'Calls sorted by estimated impact on Codex usage allowance.',
|
|
183
|
+
view: 'calls',
|
|
184
|
+
sort: 'usage',
|
|
185
|
+
direction: 'desc',
|
|
186
|
+
caption: 'Highest Codex credits preset',
|
|
187
|
+
matches: row => Number(row.usage_credits || 0) > 0,
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
function directional(compareResult) {
|
|
191
|
+
return sortDirection === 'asc' ? compareResult : -compareResult;
|
|
192
|
+
}
|
|
193
|
+
function setSort(key, direction = null) {
|
|
194
|
+
sortKey = key;
|
|
195
|
+
sortDirection = direction || defaultSortDirection(key);
|
|
196
|
+
sortEl.value = key;
|
|
197
|
+
currentPage = 1;
|
|
198
|
+
render();
|
|
199
|
+
}
|
|
200
|
+
function handleHeaderSort(key) {
|
|
201
|
+
if (sortKey === key) {
|
|
202
|
+
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
203
|
+
} else {
|
|
204
|
+
sortKey = key;
|
|
205
|
+
sortDirection = defaultSortDirection(key);
|
|
206
|
+
}
|
|
207
|
+
sortEl.value = key;
|
|
208
|
+
currentPage = 1;
|
|
209
|
+
render();
|
|
210
|
+
}
|
|
211
|
+
function updateSortControls() {
|
|
212
|
+
sortEl.value = sortKey;
|
|
213
|
+
document.querySelectorAll('[data-sort-header]').forEach(header => {
|
|
214
|
+
const active = header.dataset.sortHeader === sortKey;
|
|
215
|
+
header.dataset.sortActive = active ? 'true' : 'false';
|
|
216
|
+
header.setAttribute('aria-sort', active ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none');
|
|
217
|
+
});
|
|
218
|
+
document.querySelectorAll('[data-sort-indicator]').forEach(indicator => {
|
|
219
|
+
indicator.textContent = indicator.dataset.sortIndicator === sortKey ? (sortDirection === 'asc' ? '▲' : '▼') : '';
|
|
220
|
+
});
|
|
221
|
+
tableCaptionEl.dataset.sortDescription = `${sortLabel(sortKey)} ${sortDirection === 'asc' ? 'ascending' : 'descending'}`;
|
|
222
|
+
}
|
|
223
|
+
function loadedRowsDescription() {
|
|
224
|
+
const loaded = number.format(data.length);
|
|
225
|
+
const available = number.format(totalAvailableRows || data.length);
|
|
226
|
+
const capped = loadedLimit !== null && totalAvailableRows > data.length;
|
|
227
|
+
return capped ? `${loaded} of ${available} calls loaded` : `${loaded} calls loaded`;
|
|
228
|
+
}
|
|
229
|
+
function historyRowsDescription() {
|
|
230
|
+
const archived = Number(archivedAvailableRows || 0);
|
|
231
|
+
if (includeArchived) {
|
|
232
|
+
return archived
|
|
233
|
+
? `All history includes ${number.format(archived)} archived calls`
|
|
234
|
+
: 'All history selected; no archived calls are indexed yet';
|
|
235
|
+
}
|
|
236
|
+
return archived
|
|
237
|
+
? `Active sessions only; ${number.format(archived)} archived calls hidden`
|
|
238
|
+
: 'Active sessions only';
|
|
239
|
+
}
|
|
240
|
+
function updateHistoryScopeControl() {
|
|
241
|
+
historyScopeEl.value = includeArchived ? 'all' : 'active';
|
|
242
|
+
const detail = historyRowsDescription();
|
|
243
|
+
historyScopeEl.title = detail;
|
|
244
|
+
historyScopeEl.parentElement.title = `${detail}. Archived sessions are scanned only when All history is selected during live refresh.`;
|
|
245
|
+
}
|
|
246
|
+
function updateLoadLimitControl() {
|
|
247
|
+
const value = limitValue(loadedLimit);
|
|
248
|
+
const existing = new Set(Array.from(loadLimitEl.options).map(option => option.value));
|
|
249
|
+
if (!existing.has(value)) {
|
|
250
|
+
const option = document.createElement('option');
|
|
251
|
+
option.value = value;
|
|
252
|
+
option.textContent = `${number.format(loadedLimit)} calls`;
|
|
253
|
+
loadLimitEl.insertBefore(option, loadLimitEl.lastElementChild);
|
|
254
|
+
}
|
|
255
|
+
loadLimitEl.value = value;
|
|
256
|
+
}
|
|
257
|
+
function rebuildDashboardIndexes() {
|
|
258
|
+
rowByRecordId = new Map(data.map(row => [row.record_id, row]));
|
|
259
|
+
threadAttachmentByRecordId = new Map(data.map(row => [row.record_id, resolveThreadAttachment(row)]));
|
|
260
|
+
}
|
|
261
|
+
function usageCreditsWithStatus(row) {
|
|
262
|
+
const value = usageCreditValue(row);
|
|
263
|
+
return value === null ? 'No mapped rate' : `${credits(value)} credits · ${usageCreditStatusText(row)}`;
|
|
264
|
+
}
|
|
265
|
+
function costUsageCell(costText, creditValue) {
|
|
266
|
+
const usage = creditValue === null || creditValue === undefined ? 'No credit rate' : `${credits(creditValue)} cr`;
|
|
267
|
+
return `<span class="metric-stack"><span>${escapeHtml(costText)}</span><span class="metric-sub">${escapeHtml(usage)}</span></span>`;
|
|
268
|
+
}
|
|
269
|
+
function allowanceWindowText(totalCredits, mode = 'impact') {
|
|
270
|
+
if (!allowanceWindows.length) return '';
|
|
271
|
+
const labels = allowanceWindows.map(window => {
|
|
272
|
+
const label = short(window.label || window.key, 'Window');
|
|
273
|
+
const total = Number(window.total_credits || 0);
|
|
274
|
+
const remainingCredits = window.remaining_credits === null || window.remaining_credits === undefined ? null : Number(window.remaining_credits);
|
|
275
|
+
const remainingPercent = window.remaining_percent === null || window.remaining_percent === undefined ? null : Number(window.remaining_percent);
|
|
276
|
+
if (mode === 'remaining-card' && remainingPercent !== null && Number.isFinite(remainingPercent)) {
|
|
277
|
+
return `${label} ${pct(remainingPercent)}`;
|
|
278
|
+
}
|
|
279
|
+
if (mode === 'remaining-card' && remainingCredits !== null && Number.isFinite(remainingCredits)) {
|
|
280
|
+
return `${label} ${credits(remainingCredits)} cr left`;
|
|
281
|
+
}
|
|
282
|
+
if (mode === 'impact' && total > 0) {
|
|
283
|
+
return `${label} ${pct(totalCredits / total)} of allowance`;
|
|
284
|
+
}
|
|
285
|
+
if (mode === 'impact' && remainingCredits !== null && Number.isFinite(remainingCredits)) {
|
|
286
|
+
return `${label} ${credits(totalCredits)} used vs ${credits(remainingCredits)} remaining`;
|
|
287
|
+
}
|
|
288
|
+
if (remainingPercent !== null && Number.isFinite(remainingPercent)) {
|
|
289
|
+
return `${label} ${pct(remainingPercent)} remaining`;
|
|
290
|
+
}
|
|
291
|
+
if (remainingCredits !== null && Number.isFinite(remainingCredits)) {
|
|
292
|
+
return `${label} ${credits(remainingCredits)} credits remaining`;
|
|
293
|
+
}
|
|
294
|
+
if (total > 0) {
|
|
295
|
+
return `${label} ${credits(totalCredits)} of ${credits(total)} credits`;
|
|
296
|
+
}
|
|
297
|
+
return `${label} configured`;
|
|
298
|
+
});
|
|
299
|
+
return labels.join(mode === 'remaining-card' ? '\n' : ' · ');
|
|
300
|
+
}
|
|
301
|
+
function allowanceImpactText(totalCredits) {
|
|
302
|
+
const windowImpact = allowanceWindowText(totalCredits, 'remaining-card') || allowanceWindowText(totalCredits, 'impact');
|
|
303
|
+
if (windowImpact) return windowImpact;
|
|
304
|
+
if (allowanceError) return 'Allowance config error';
|
|
305
|
+
return allowanceConfigured ? 'Allowance configured' : 'Set limits';
|
|
306
|
+
}
|
|
307
|
+
function rowAllowanceImpact(row) {
|
|
308
|
+
const value = usageCreditValue(row);
|
|
309
|
+
if (value === null) return 'No mapped Codex credit rate';
|
|
310
|
+
const impact = allowanceWindowText(value, 'impact');
|
|
311
|
+
return impact || `${credits(value)} credits counted toward Codex usage limits`;
|
|
312
|
+
}
|
|
313
|
+
function updateAllowanceSourceLine() {
|
|
314
|
+
const sourceEl = document.getElementById('allowanceSource');
|
|
315
|
+
const sourceName = allowanceSource.name || 'Codex credit rates';
|
|
316
|
+
const coverage = creditCoverageRatio(data);
|
|
317
|
+
sourceEl.textContent = 'Credits';
|
|
318
|
+
sourceEl.dataset.state = coverage > 0 ? 'ready' : 'missing';
|
|
319
|
+
sourceEl.title = [
|
|
320
|
+
allowanceSource.url ? `Source: ${allowanceSource.url}` : '',
|
|
321
|
+
allowanceSource.fetched_at ? `rate card snapshot ${allowanceSource.fetched_at}` : '',
|
|
322
|
+
`Credit rates: ${sourceName}.`,
|
|
323
|
+
`Credit coverage ${pct(coverage)} of loaded tokens.`,
|
|
324
|
+
allowanceWindows.length ? `Allowance windows: ${allowanceWindows.map(window => short(window.label || window.key)).join(', ')}` : 'Run codex-usage-tracker init-allowance to add remaining usage windows.',
|
|
325
|
+
allowanceWindows.some(window => window.reset_at) ? `Resets: ${allowanceWindows.map(window => window.reset_at ? `${short(window.label || window.key)} ${formatTimestamp(window.reset_at, window.reset_at)}` : '').filter(Boolean).join('; ')}` : '',
|
|
326
|
+
allowanceError ? `Allowance config error: ${allowanceError}` : '',
|
|
327
|
+
rateCardError ? `Rate-card error: ${rateCardError}` : '',
|
|
328
|
+
].filter(Boolean).join(' ');
|
|
329
|
+
}
|
|
330
|
+
function rebuildSelectOptions(select, values, label) {
|
|
331
|
+
const previous = select.value;
|
|
332
|
+
select.textContent = '';
|
|
333
|
+
const allOption = document.createElement('option');
|
|
334
|
+
allOption.value = '';
|
|
335
|
+
allOption.textContent = label;
|
|
336
|
+
select.appendChild(allOption);
|
|
337
|
+
[...new Set(values.filter(Boolean))].sort().forEach(value => {
|
|
338
|
+
const option = document.createElement('option');
|
|
339
|
+
option.value = value;
|
|
340
|
+
option.textContent = value;
|
|
341
|
+
select.appendChild(option);
|
|
342
|
+
});
|
|
343
|
+
const valuesSet = new Set(Array.from(select.options).map(option => option.value));
|
|
344
|
+
select.value = valuesSet.has(previous) ? previous : '';
|
|
345
|
+
}
|
|
346
|
+
function rebuildFilterOptions() {
|
|
347
|
+
rebuildSelectOptions(modelEl, data.map(row => row.model), 'All models');
|
|
348
|
+
rebuildSelectOptions(effortEl, data.map(row => row.effort), 'All efforts');
|
|
349
|
+
}
|
|
350
|
+
function applyInitialState() {
|
|
351
|
+
if (!initialState) return;
|
|
352
|
+
searchEl.value = initialState.search || '';
|
|
353
|
+
if (optionValueExists(modelEl, initialState.model)) modelEl.value = initialState.model;
|
|
354
|
+
if (optionValueExists(effortEl, initialState.effort)) effortEl.value = initialState.effort;
|
|
355
|
+
if (optionValueExists(pricingStatusEl, initialState.confidence)) pricingStatusEl.value = initialState.confidence;
|
|
356
|
+
const initialDatePreset = allowedDatePresets.has(initialState.datePreset) ? initialState.datePreset : '';
|
|
357
|
+
const initialDateStart = cleanDateInput(initialState.dateStart);
|
|
358
|
+
const initialDateEnd = cleanDateInput(initialState.dateEnd);
|
|
359
|
+
if (initialDatePreset && initialDatePreset !== 'custom' && initialDatePreset !== 'all') {
|
|
360
|
+
datePresetEl.value = initialDatePreset;
|
|
361
|
+
syncDatePresetInputs();
|
|
362
|
+
} else if (initialDateStart || initialDateEnd) {
|
|
363
|
+
datePresetEl.value = 'custom';
|
|
364
|
+
dateStartEl.value = initialDateStart;
|
|
365
|
+
dateEndEl.value = initialDateEnd;
|
|
366
|
+
} else if (initialDatePreset) {
|
|
367
|
+
datePresetEl.value = initialDatePreset;
|
|
368
|
+
}
|
|
369
|
+
if (optionValueExists(sortEl, initialState.sort)) {
|
|
370
|
+
sortKey = initialState.sort;
|
|
371
|
+
sortEl.value = sortKey;
|
|
372
|
+
}
|
|
373
|
+
if (['asc', 'desc'].includes(initialState.direction)) sortDirection = initialState.direction;
|
|
374
|
+
if (presetDefinitions.some(preset => preset.key === initialState.preset)) activePreset = initialState.preset;
|
|
375
|
+
if (initialState.page && Number(initialState.page) > 1) currentPage = Number(initialState.page);
|
|
376
|
+
if (Array.isArray(initialState.expandedThreads)) {
|
|
377
|
+
initialState.expandedThreads.forEach(key => expandedThreads.add(key));
|
|
378
|
+
}
|
|
379
|
+
if (initialState.expandedThreads && initialState.expandedThreads.length) {
|
|
380
|
+
initialThreadExpansionApplied = true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function updatePricingSourceLine() {
|
|
384
|
+
const sourceEl = document.getElementById('pricingSource');
|
|
385
|
+
if (pricingConfigured && pricingSource.url) {
|
|
386
|
+
const sourceParts = [
|
|
387
|
+
pricingSource.name || 'Pricing source',
|
|
388
|
+
pricingSource.tier ? `${pricingSource.tier} tier` : '',
|
|
389
|
+
pricingSource.fetched_at ? `fetched ${formatTimestamp(pricingSource.fetched_at)}` : '',
|
|
390
|
+
pricingSource.pinned ? 'pinned snapshot' : '',
|
|
391
|
+
].filter(Boolean);
|
|
392
|
+
sourceEl.textContent = 'Costs';
|
|
393
|
+
sourceEl.dataset.state = 'ready';
|
|
394
|
+
sourceEl.title = pricingSource.fetched_at
|
|
395
|
+
? `${sourceParts.join(' · ')}. Fetched from ${pricingSource.url} at ${formatTimestampTitle(pricingSource.fetched_at)}. Internal Codex labels may use marked best-guess estimates.${pricingSnapshotWarning ? ` ${pricingSnapshotWarning}` : ''}`
|
|
396
|
+
: `${sourceParts.join(' · ')}. Internal Codex labels may use marked best-guess estimates.${pricingSnapshotWarning ? ` ${pricingSnapshotWarning}` : ''}`;
|
|
397
|
+
} else {
|
|
398
|
+
sourceEl.textContent = pricingConfigured ? 'Costs' : 'No costs';
|
|
399
|
+
sourceEl.dataset.state = pricingConfigured ? 'ready' : 'missing';
|
|
400
|
+
sourceEl.title = pricingConfigured ? (pricingSnapshotWarning || '') : 'Run codex-usage-tracker update-pricing to configure estimated costs.';
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function updateParserDiagnosticsLine() {
|
|
404
|
+
const sourceEl = document.getElementById('parserDiagnostics');
|
|
405
|
+
const entries = Object.entries(parserDiagnostics || {}).filter(([, value]) => Number(value || 0) > 0);
|
|
406
|
+
if (!entries.length) {
|
|
407
|
+
sourceEl.hidden = true;
|
|
408
|
+
sourceEl.textContent = '';
|
|
409
|
+
sourceEl.title = '';
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const total = entries.reduce((sum, [, value]) => sum + Number(value || 0), 0);
|
|
413
|
+
sourceEl.hidden = false;
|
|
414
|
+
sourceEl.textContent = 'Parser warnings';
|
|
415
|
+
sourceEl.dataset.state = 'missing';
|
|
416
|
+
sourceEl.title = `Latest refresh reported ${number.format(total)} parser diagnostics: ${entries.map(([key, value]) => `${key}=${value}`).join(', ')}. Run codex-usage-tracker inspect-log <path> to investigate schema drift.`;
|
|
417
|
+
}
|
|
418
|
+
function updatePrivacyModeLine() {
|
|
419
|
+
const sourceEl = document.getElementById('privacyMode');
|
|
420
|
+
const mode = projectMetadataPrivacy.mode || 'normal';
|
|
421
|
+
sourceEl.textContent = mode === 'normal' ? 'Metadata normal' : `Metadata ${mode}`;
|
|
422
|
+
sourceEl.dataset.state = mode === 'normal' ? 'ready' : 'missing';
|
|
423
|
+
sourceEl.title = mode === 'normal'
|
|
424
|
+
? 'Project metadata is shown with local cwd, project, branch, and configured labels.'
|
|
425
|
+
: [
|
|
426
|
+
`Project metadata privacy mode: ${mode}.`,
|
|
427
|
+
projectMetadataPrivacy.cwd_redacted ? 'Raw cwd paths are redacted.' : '',
|
|
428
|
+
projectMetadataPrivacy.project_names_redacted ? 'Unnamed projects use stable hashed labels.' : '',
|
|
429
|
+
projectMetadataPrivacy.git_remote_label_hidden ? 'Git remote labels are hidden.' : '',
|
|
430
|
+
projectMetadataPrivacy.relative_cwd_hidden ? 'Relative cwd is hidden.' : '',
|
|
431
|
+
projectMetadataPrivacy.git_branch_hidden ? 'Git branch is hidden.' : '',
|
|
432
|
+
projectMetadataPrivacy.tags_hidden ? 'Project tags are hidden.' : '',
|
|
433
|
+
projectMetadataPrivacy.aliases_preserved ? 'Configured project aliases are treated as explicit display opt-ins.' : '',
|
|
434
|
+
].filter(Boolean).join(' ');
|
|
435
|
+
}
|
|
436
|
+
function padDatePart(value) {
|
|
437
|
+
return String(value).padStart(2, '0');
|
|
438
|
+
}
|
|
439
|
+
function localDateKey(date) {
|
|
440
|
+
return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`;
|
|
441
|
+
}
|
|
442
|
+
function localDay(value = new Date()) {
|
|
443
|
+
return new Date(value.getFullYear(), value.getMonth(), value.getDate());
|
|
444
|
+
}
|
|
445
|
+
function addDays(date, days) {
|
|
446
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
|
|
447
|
+
}
|
|
448
|
+
function parseDateInput(value) {
|
|
449
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return null;
|
|
450
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
451
|
+
const date = new Date(year, month - 1, day);
|
|
452
|
+
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day ? date : null;
|
|
453
|
+
}
|
|
454
|
+
function cleanDateInput(value) {
|
|
455
|
+
const date = parseDateInput(value);
|
|
456
|
+
return date ? localDateKey(date) : '';
|
|
457
|
+
}
|
|
458
|
+
function weekStart(date) {
|
|
459
|
+
const day = date.getDay();
|
|
460
|
+
const offset = day === 0 ? -6 : 1 - day;
|
|
461
|
+
return addDays(date, offset);
|
|
462
|
+
}
|
|
463
|
+
function presetDateRange(preset) {
|
|
464
|
+
const today = localDay();
|
|
465
|
+
if (preset === 'today') {
|
|
466
|
+
return { start: today, endExclusive: addDays(today, 1) };
|
|
467
|
+
}
|
|
468
|
+
if (preset === 'this-week') {
|
|
469
|
+
const start = weekStart(today);
|
|
470
|
+
return { start, endExclusive: addDays(start, 7) };
|
|
471
|
+
}
|
|
472
|
+
if (preset === 'last-7-days') {
|
|
473
|
+
return { start: addDays(today, -6), endExclusive: addDays(today, 1) };
|
|
474
|
+
}
|
|
475
|
+
if (preset === 'this-month') {
|
|
476
|
+
return {
|
|
477
|
+
start: new Date(today.getFullYear(), today.getMonth(), 1),
|
|
478
|
+
endExclusive: new Date(today.getFullYear(), today.getMonth() + 1, 1),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return { start: null, endExclusive: null };
|
|
482
|
+
}
|
|
483
|
+
function syncDatePresetInputs() {
|
|
484
|
+
const preset = datePresetEl.value;
|
|
485
|
+
if (preset === 'custom') return;
|
|
486
|
+
if (preset === 'all') {
|
|
487
|
+
dateStartEl.value = '';
|
|
488
|
+
dateEndEl.value = '';
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const range = presetDateRange(preset);
|
|
492
|
+
dateStartEl.value = range.start ? localDateKey(range.start) : '';
|
|
493
|
+
dateEndEl.value = range.endExclusive ? localDateKey(addDays(range.endExclusive, -1)) : '';
|
|
494
|
+
}
|
|
495
|
+
function formatDateRangeLabel(prefix, start, end) {
|
|
496
|
+
const startLabel = start ? localDateKey(start) : '';
|
|
497
|
+
const endLabel = end ? localDateKey(end) : '';
|
|
498
|
+
if (startLabel && endLabel && startLabel === endLabel) return `${prefix} ${startLabel}`;
|
|
499
|
+
if (startLabel && endLabel) return `${prefix} ${startLabel} to ${endLabel}`;
|
|
500
|
+
if (startLabel) return `${prefix} from ${startLabel}`;
|
|
501
|
+
if (endLabel) return `${prefix} through ${endLabel}`;
|
|
502
|
+
return prefix;
|
|
503
|
+
}
|
|
504
|
+
function currentDateRange() {
|
|
505
|
+
const preset = allowedDatePresets.has(datePresetEl.value) ? datePresetEl.value : 'all';
|
|
506
|
+
if (preset !== 'custom' && preset !== 'all') {
|
|
507
|
+
const range = presetDateRange(preset);
|
|
508
|
+
return {
|
|
509
|
+
active: true,
|
|
510
|
+
invalid: false,
|
|
511
|
+
start: range.start,
|
|
512
|
+
endExclusive: range.endExclusive,
|
|
513
|
+
label: formatDateRangeLabel(datePresetLabels[preset], range.start, addDays(range.endExclusive, -1)),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const start = parseDateInput(dateStartEl.value);
|
|
517
|
+
const end = parseDateInput(dateEndEl.value);
|
|
518
|
+
if (start && end && start > end) {
|
|
519
|
+
return {
|
|
520
|
+
active: true,
|
|
521
|
+
invalid: true,
|
|
522
|
+
start,
|
|
523
|
+
endExclusive: addDays(end, 1),
|
|
524
|
+
label: 'Invalid date range',
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (start || end) {
|
|
528
|
+
return {
|
|
529
|
+
active: true,
|
|
530
|
+
invalid: false,
|
|
531
|
+
start,
|
|
532
|
+
endExclusive: end ? addDays(end, 1) : null,
|
|
533
|
+
label: formatDateRangeLabel('Custom', start, end),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return { active: false, invalid: false, start: null, endExclusive: null, label: 'All time' };
|
|
537
|
+
}
|
|
538
|
+
function rowMatchesDateRange(row, range) {
|
|
539
|
+
if (range.invalid) return false;
|
|
540
|
+
if (!range.active) return true;
|
|
541
|
+
const timestamp = row.event_timestamp ? new Date(row.event_timestamp) : null;
|
|
542
|
+
if (!timestamp || Number.isNaN(timestamp.getTime())) return false;
|
|
543
|
+
if (range.start && timestamp < range.start) return false;
|
|
544
|
+
if (range.endExclusive && timestamp >= range.endExclusive) return false;
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
function updateDateFilterControls() {
|
|
548
|
+
const range = currentDateRange();
|
|
549
|
+
const showStatus = range.active || range.invalid;
|
|
550
|
+
dateRangeStatusEl.hidden = !showStatus;
|
|
551
|
+
dateRangeStatusEl.textContent = showStatus ? range.label : '';
|
|
552
|
+
dateRangeStatusEl.dataset.state = range.invalid ? 'error' : range.active ? 'active' : 'idle';
|
|
553
|
+
return range;
|
|
554
|
+
}
|
|
555
|
+
function dateCaptionPrefix(range = currentDateRange()) {
|
|
556
|
+
return range.active || range.invalid ? `${range.label}. ` : '';
|
|
557
|
+
}
|
|
558
|
+
function filtered(dateRange = currentDateRange()) {
|
|
559
|
+
const term = searchEl.value.trim().toLowerCase();
|
|
560
|
+
const model = modelEl.value;
|
|
561
|
+
const effort = effortEl.value;
|
|
562
|
+
const pricingStatus = pricingStatusEl.value;
|
|
563
|
+
const rows = data.filter(row => {
|
|
564
|
+
const haystack = [
|
|
565
|
+
rowThreadLabel(row),
|
|
566
|
+
row.cwd,
|
|
567
|
+
row.project_name,
|
|
568
|
+
row.project_relative_cwd,
|
|
569
|
+
Array.isArray(row.project_tags) ? row.project_tags.join(' ') : '',
|
|
570
|
+
row.git_branch,
|
|
571
|
+
row.git_remote_label,
|
|
572
|
+
row.model,
|
|
573
|
+
row.effort,
|
|
574
|
+
row.session_id,
|
|
575
|
+
row.turn_id,
|
|
576
|
+
row.thread_source,
|
|
577
|
+
row.subagent_type,
|
|
578
|
+
row.agent_role,
|
|
579
|
+
row.agent_nickname,
|
|
580
|
+
row.parent_session_id,
|
|
581
|
+
row.parent_thread_name,
|
|
582
|
+
row.resolved_parent_thread_name,
|
|
583
|
+
].join(' ').toLowerCase();
|
|
584
|
+
const statusMatches = !pricingStatus
|
|
585
|
+
|| (pricingStatus === 'official' && row.pricing_model && !row.pricing_estimated)
|
|
586
|
+
|| (pricingStatus === 'estimated' && row.pricing_estimated)
|
|
587
|
+
|| (pricingStatus === 'unpriced' && !row.pricing_model)
|
|
588
|
+
|| (pricingStatus === 'credit-exact' && row.usage_credit_confidence === 'exact')
|
|
589
|
+
|| (pricingStatus === 'credit-estimated' && row.usage_credit_confidence === 'estimated')
|
|
590
|
+
|| (pricingStatus === 'credit-override' && row.usage_credit_confidence === 'user_override')
|
|
591
|
+
|| (pricingStatus === 'credit-missing' && row.usage_credit_confidence === 'unpriced');
|
|
592
|
+
return (!term || haystack.includes(term)) && (!model || row.model === model) && (!effort || row.effort === effort) && statusMatches && rowMatchesDateRange(row, dateRange) && presetMatchesRow(row);
|
|
593
|
+
});
|
|
594
|
+
rows.sort(compareCalls);
|
|
595
|
+
return rows;
|
|
596
|
+
}
|
|
597
|
+
function currentDashboardState() {
|
|
598
|
+
return {
|
|
599
|
+
view: activeView,
|
|
600
|
+
search: searchEl.value.trim(),
|
|
601
|
+
model: modelEl.value,
|
|
602
|
+
effort: effortEl.value,
|
|
603
|
+
confidence: pricingStatusEl.value,
|
|
604
|
+
datePreset: datePresetEl.value,
|
|
605
|
+
dateStart: datePresetEl.value === 'custom' ? dateStartEl.value : '',
|
|
606
|
+
dateEnd: datePresetEl.value === 'custom' ? dateEndEl.value : '',
|
|
607
|
+
historyScope: includeArchived ? 'all' : 'active',
|
|
608
|
+
sort: sortKey,
|
|
609
|
+
direction: sortDirection,
|
|
610
|
+
preset: activePreset,
|
|
611
|
+
page: currentPage,
|
|
612
|
+
record: selectedRecordId,
|
|
613
|
+
thread: selectedThreadKey,
|
|
614
|
+
expandedThreads: Array.from(expandedThreads),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function syncUrlState() {
|
|
618
|
+
if (!stateManager) return;
|
|
619
|
+
stateManager.replace(currentDashboardState());
|
|
620
|
+
}
|
|
621
|
+
function showActionStatus(message) {
|
|
622
|
+
actionStatusEl.textContent = message;
|
|
623
|
+
if (!message) return;
|
|
624
|
+
window.setTimeout(() => {
|
|
625
|
+
if (actionStatusEl.textContent === message) actionStatusEl.textContent = '';
|
|
626
|
+
}, 2200);
|
|
627
|
+
}
|
|
628
|
+
async function copyCurrentViewLink() {
|
|
629
|
+
if (!stateManager) return;
|
|
630
|
+
const url = stateManager.urlFor(currentDashboardState());
|
|
631
|
+
try {
|
|
632
|
+
await stateManager.copyText(url);
|
|
633
|
+
showActionStatus('Copied');
|
|
634
|
+
} catch (error) {
|
|
635
|
+
showActionStatus('Copy failed');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function exportCurrentRows() {
|
|
639
|
+
if (!stateManager) return;
|
|
640
|
+
const rows = filtered();
|
|
641
|
+
const columns = [
|
|
642
|
+
{ label: 'timestamp', field: 'event_timestamp' },
|
|
643
|
+
{ label: 'thread', field: row => rowThreadLabel(row) },
|
|
644
|
+
{ label: 'project', field: 'project_name' },
|
|
645
|
+
{ label: 'model', field: 'model' },
|
|
646
|
+
{ label: 'effort', field: 'effort' },
|
|
647
|
+
{ label: 'total_tokens', field: 'total_tokens' },
|
|
648
|
+
{ label: 'input_tokens', field: 'input_tokens' },
|
|
649
|
+
{ label: 'cached_input_tokens', field: 'cached_input_tokens' },
|
|
650
|
+
{ label: 'uncached_input_tokens', field: 'uncached_input_tokens' },
|
|
651
|
+
{ label: 'output_tokens', field: 'output_tokens' },
|
|
652
|
+
{ label: 'reasoning_output_tokens', field: 'reasoning_output_tokens' },
|
|
653
|
+
{ label: 'estimated_cost_usd', field: 'estimated_cost_usd' },
|
|
654
|
+
{ label: 'usage_credits', field: 'usage_credits' },
|
|
655
|
+
{ label: 'cache_ratio', field: 'cache_ratio' },
|
|
656
|
+
{ label: 'context_window_percent', field: 'context_window_percent' },
|
|
657
|
+
{ label: 'pricing_model', field: 'pricing_model' },
|
|
658
|
+
{ label: 'usage_credit_confidence', field: 'usage_credit_confidence' },
|
|
659
|
+
{ label: 'recommendation', field: row => row.recommended_action || recommendationSummary(row) },
|
|
660
|
+
{ label: 'record_id', field: 'record_id' },
|
|
661
|
+
];
|
|
662
|
+
const csv = stateManager.toCsv(rows, columns);
|
|
663
|
+
const suffix = activeView === 'threads' ? 'thread-filtered-calls' : `${activeView}-calls`;
|
|
664
|
+
stateManager.downloadText(`codex-usage-${suffix}.csv`, csv, 'text/csv;charset=utf-8');
|
|
665
|
+
showActionStatus(`Exported ${number.format(rows.length)}`);
|
|
666
|
+
}
|
|
667
|
+
function activePresetDefinition() {
|
|
668
|
+
return presetDefinitions.find(preset => preset.key === activePreset) || null;
|
|
669
|
+
}
|
|
670
|
+
function presetMatchesRow(row) {
|
|
671
|
+
const preset = activePresetDefinition();
|
|
672
|
+
return preset ? preset.matches(row) : true;
|
|
673
|
+
}
|
|
674
|
+
function applyPreset(key) {
|
|
675
|
+
const preset = presetDefinitions.find(candidate => candidate.key === key);
|
|
676
|
+
if (!preset) return;
|
|
677
|
+
activePreset = preset.key;
|
|
678
|
+
activeView = preset.view;
|
|
679
|
+
pricingStatusEl.value = preset.pricingStatus || '';
|
|
680
|
+
sortKey = preset.sort;
|
|
681
|
+
sortDirection = preset.direction || defaultSortDirection(preset.sort);
|
|
682
|
+
sortEl.value = preset.sort;
|
|
683
|
+
currentPage = 1;
|
|
684
|
+
render();
|
|
685
|
+
}
|
|
686
|
+
function clearPreset() {
|
|
687
|
+
activePreset = '';
|
|
688
|
+
pricingStatusEl.value = '';
|
|
689
|
+
sortKey = 'attention';
|
|
690
|
+
sortDirection = defaultSortDirection(sortKey);
|
|
691
|
+
sortEl.value = sortKey;
|
|
692
|
+
currentPage = 1;
|
|
693
|
+
render();
|
|
694
|
+
}
|
|
695
|
+
function threshold(key, fallback) {
|
|
696
|
+
const value = Number(actionThresholds[key]);
|
|
697
|
+
return Number.isFinite(value) ? value : fallback;
|
|
698
|
+
}
|
|
699
|
+
function topRecommendation(row) {
|
|
700
|
+
return Array.isArray(row.action_recommendations) && row.action_recommendations.length
|
|
701
|
+
? row.action_recommendations[0]
|
|
702
|
+
: null;
|
|
703
|
+
}
|
|
704
|
+
function recommendationSummary(row) {
|
|
705
|
+
const recommendation = topRecommendation(row);
|
|
706
|
+
return recommendation ? `${recommendation.title}: ${recommendation.why}` : 'No aggregate action is flagged.';
|
|
707
|
+
}
|
|
708
|
+
function signalCount(row) {
|
|
709
|
+
return Array.isArray(row.efficiency_flags) ? row.efficiency_flags.length : 0;
|
|
710
|
+
}
|
|
711
|
+
function rowAttentionScore(row) {
|
|
712
|
+
const costScore = clamp(Number(row.estimated_cost_usd || 0) * 24, 0, 60);
|
|
713
|
+
const tokenScore = clamp(Number(row.total_tokens || 0) / 2500, 0, 36);
|
|
714
|
+
const lowCacheScore = Number(row.input_tokens || 0) > 0 ? clamp((0.5 - Number(row.cache_ratio || 0)) * 70, 0, 35) : 0;
|
|
715
|
+
const contextScore = clamp(Number(row.context_window_percent || 0) * 42, 0, 42);
|
|
716
|
+
const pricingScore = row.pricing_model ? (row.pricing_estimated ? 12 : 0) : 30;
|
|
717
|
+
const usageScore = clamp(Number(row.usage_credits || 0) * 2.5, 0, 48);
|
|
718
|
+
return costScore + usageScore + tokenScore + lowCacheScore + contextScore + pricingScore + signalCount(row) * 12;
|
|
719
|
+
}
|
|
720
|
+
function threadAttentionScore(group) {
|
|
721
|
+
const costScore = clamp(Number(group.estimatedCost || 0) * 24, 0, 72);
|
|
722
|
+
const tokenScore = clamp(Number(group.totalTokens || 0) / 3500, 0, 42);
|
|
723
|
+
const lowCacheScore = clamp((0.55 - Number(group.cacheRatio || 0)) * 70, 0, 38);
|
|
724
|
+
const contextScore = clamp(Number(group.maxContextUse || 0) * 45, 0, 45);
|
|
725
|
+
const pricingScore = group.pricingStatus === 'No price' ? 36 : group.pricingStatus === 'Estimated' || group.pricingStatus === 'Mixed' ? 18 : 0;
|
|
726
|
+
const usageScore = clamp(Number(group.usageCredits || 0) * 2.4, 0, 72);
|
|
727
|
+
const relationScore = (group.subagentCount || 0) * 4 + (group.autoReviewCount || 0) * 6 + (group.attachedCount || 0) * 3;
|
|
728
|
+
return costScore + usageScore + tokenScore + lowCacheScore + contextScore + pricingScore + relationScore + Number(group.signalCount || 0) * 10;
|
|
729
|
+
}
|
|
730
|
+
function severityForScore(score, hasPricingGap = false) {
|
|
731
|
+
if (score >= 95) return 'high';
|
|
732
|
+
if (score >= 48) return 'medium';
|
|
733
|
+
return hasPricingGap ? 'review' : 'review';
|
|
734
|
+
}
|
|
735
|
+
function callSortValue(row, key) {
|
|
736
|
+
if (key === 'attention') return rowAttentionScore(row);
|
|
737
|
+
if (key === 'cache') return Number(row.cache_ratio || 0);
|
|
738
|
+
if (key === 'context') return Number(row.context_window_percent || 0);
|
|
739
|
+
if (key === 'cost') return Number(row.estimated_cost_usd || 0);
|
|
740
|
+
if (key === 'effort') return textValue(row.effort);
|
|
741
|
+
if (key === 'model') return textValue(row.model);
|
|
742
|
+
if (key === 'signals') return Array.isArray(row.efficiency_flags) ? row.efficiency_flags.length : 0;
|
|
743
|
+
if (key === 'thread') return textValue(rowThreadLabel(row));
|
|
744
|
+
if (key === 'time') return String(row.event_timestamp || '');
|
|
745
|
+
if (key === 'usage') return Number(row.usage_credits || 0);
|
|
746
|
+
return Number(row.total_tokens || 0);
|
|
747
|
+
}
|
|
748
|
+
function compareCalls(a, b) {
|
|
749
|
+
const primary = directional(compareValues(callSortValue(a, sortKey), callSortValue(b, sortKey)));
|
|
750
|
+
if (primary !== 0) return primary;
|
|
751
|
+
const timeFallback = String(b.event_timestamp || '').localeCompare(String(a.event_timestamp || ''));
|
|
752
|
+
if (timeFallback !== 0) return timeFallback;
|
|
753
|
+
return String(a.record_id || '').localeCompare(String(b.record_id || ''));
|
|
754
|
+
}
|
|
755
|
+
function rowAttachment(row) {
|
|
756
|
+
return threadAttachmentByRecordId.get(row.record_id) || resolveThreadAttachment(row);
|
|
757
|
+
}
|
|
758
|
+
function rowThreadLabel(row) {
|
|
759
|
+
return rowAttachment(row).label;
|
|
760
|
+
}
|
|
761
|
+
function sortThreads(groups) {
|
|
762
|
+
groups.sort(compareThreads);
|
|
763
|
+
return groups;
|
|
764
|
+
}
|
|
765
|
+
function threadSortValue(group, key) {
|
|
766
|
+
if (key === 'attention') return group.attentionScore;
|
|
767
|
+
if (key === 'cache') return group.cacheRatio;
|
|
768
|
+
if (key === 'context') return group.maxContextUse;
|
|
769
|
+
if (key === 'cost') return group.estimatedCost;
|
|
770
|
+
if (key === 'effort') return textValue(group.effortSummary);
|
|
771
|
+
if (key === 'model') return textValue(group.modelSummary);
|
|
772
|
+
if (key === 'signals') return group.signalCount;
|
|
773
|
+
if (key === 'thread') return textValue(group.label);
|
|
774
|
+
if (key === 'time') return String(group.latestActivity || '');
|
|
775
|
+
if (key === 'usage') return group.usageCredits;
|
|
776
|
+
return group.totalTokens;
|
|
777
|
+
}
|
|
778
|
+
function compareThreads(a, b) {
|
|
779
|
+
const primary = directional(compareValues(threadSortValue(a, sortKey), threadSortValue(b, sortKey)));
|
|
780
|
+
if (primary !== 0) return primary;
|
|
781
|
+
const timeFallback = String(b.latestActivity || '').localeCompare(String(a.latestActivity || ''));
|
|
782
|
+
if (timeFallback !== 0) return timeFallback;
|
|
783
|
+
return String(a.label || '').localeCompare(String(b.label || ''));
|
|
784
|
+
}
|
|
785
|
+
function relationshipTime(group) {
|
|
786
|
+
return String(group.relationshipLatestActivity || group.latestActivity || '');
|
|
787
|
+
}
|
|
788
|
+
function compareTopLevelThreads(a, b) {
|
|
789
|
+
if (sortKey === 'time' && sortDirection === 'desc') {
|
|
790
|
+
const relationshipCompare = relationshipTime(b).localeCompare(relationshipTime(a));
|
|
791
|
+
if (relationshipCompare !== 0) return relationshipCompare;
|
|
792
|
+
}
|
|
793
|
+
return compareThreads(a, b);
|
|
794
|
+
}
|
|
795
|
+
function fitModelPills() {
|
|
796
|
+
document.querySelectorAll('.model-pill').forEach(pill => {
|
|
797
|
+
pill.style.fontSize = '';
|
|
798
|
+
const maxSize = 12;
|
|
799
|
+
const minSize = 9;
|
|
800
|
+
let size = maxSize;
|
|
801
|
+
while (size > minSize && pill.scrollWidth > pill.clientWidth) {
|
|
802
|
+
size -= 0.5;
|
|
803
|
+
pill.style.fontSize = `${size}px`;
|
|
804
|
+
}
|
|
805
|
+
pill.title = pill.dataset.fullLabel || pill.textContent || '';
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
function dominantParentThread(calls, ownLabel) {
|
|
809
|
+
const counts = new Map();
|
|
810
|
+
for (const row of calls) {
|
|
811
|
+
const parent = resolvedParentThreadName(row);
|
|
812
|
+
if (!parent || parent === ownLabel) continue;
|
|
813
|
+
counts.set(parent, (counts.get(parent) || 0) + 1);
|
|
814
|
+
}
|
|
815
|
+
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
816
|
+
return ranked.length ? ranked[0][0] : '';
|
|
817
|
+
}
|
|
818
|
+
function arrangeThreadGroups(groups) {
|
|
819
|
+
const byLabel = new Map(groups.map(group => [group.label, group]));
|
|
820
|
+
for (const group of groups) {
|
|
821
|
+
group.childThreadCount = 0;
|
|
822
|
+
group.childCallCount = 0;
|
|
823
|
+
group.relationshipLatestActivity = group.latestActivity;
|
|
824
|
+
group.parentVisible = Boolean(group.parentThreadLabel && byLabel.has(group.parentThreadLabel));
|
|
825
|
+
group.renderAsChild = false;
|
|
826
|
+
}
|
|
827
|
+
for (const group of groups) {
|
|
828
|
+
if (!group.parentVisible) continue;
|
|
829
|
+
const parent = byLabel.get(group.parentThreadLabel);
|
|
830
|
+
parent.childThreadCount += 1;
|
|
831
|
+
parent.childCallCount += group.callCount;
|
|
832
|
+
if (String(group.latestActivity || '') > String(parent.relationshipLatestActivity || '')) {
|
|
833
|
+
parent.relationshipLatestActivity = group.latestActivity;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (sortKey !== 'time' || sortDirection !== 'desc') {
|
|
837
|
+
return sortThreads(groups);
|
|
838
|
+
}
|
|
839
|
+
const childrenByParent = new Map();
|
|
840
|
+
for (const group of groups) {
|
|
841
|
+
if (!group.parentVisible) continue;
|
|
842
|
+
if (!childrenByParent.has(group.parentThreadLabel)) childrenByParent.set(group.parentThreadLabel, []);
|
|
843
|
+
childrenByParent.get(group.parentThreadLabel).push(group);
|
|
844
|
+
}
|
|
845
|
+
const display = [];
|
|
846
|
+
const topLevel = groups.filter(group => !group.parentVisible).sort(compareTopLevelThreads);
|
|
847
|
+
const displayed = new Set();
|
|
848
|
+
function appendGroup(group, renderAsChild = false) {
|
|
849
|
+
if (displayed.has(group.key)) return;
|
|
850
|
+
displayed.add(group.key);
|
|
851
|
+
group.renderAsChild = renderAsChild;
|
|
852
|
+
display.push(group);
|
|
853
|
+
const children = (childrenByParent.get(group.label) || []).sort(compareThreads);
|
|
854
|
+
for (const child of children) appendGroup(child, true);
|
|
855
|
+
}
|
|
856
|
+
for (const group of topLevel) {
|
|
857
|
+
appendGroup(group, false);
|
|
858
|
+
}
|
|
859
|
+
return display;
|
|
860
|
+
}
|
|
861
|
+
function pricingStatusFor(rows) {
|
|
862
|
+
const priced = rows.filter(row => row.pricing_model);
|
|
863
|
+
const estimated = rows.filter(row => row.pricing_estimated);
|
|
864
|
+
if (priced.length === 0) return 'No price';
|
|
865
|
+
if (estimated.length === rows.length) return 'Estimated';
|
|
866
|
+
if (estimated.length > 0 || priced.length < rows.length) return 'Mixed';
|
|
867
|
+
return 'Configured';
|
|
868
|
+
}
|
|
869
|
+
function creditStatusFor(rows) {
|
|
870
|
+
const rated = rows.filter(row => usageCreditValue(row) !== null);
|
|
871
|
+
const estimated = rows.filter(row => row.usage_credit_confidence === 'estimated');
|
|
872
|
+
if (rated.length === 0) return 'No mapped rate';
|
|
873
|
+
if (estimated.length === rows.length) return 'Estimated mapping';
|
|
874
|
+
if (estimated.length > 0 || rated.length < rows.length) return 'Mixed';
|
|
875
|
+
return 'Official rate-card match';
|
|
876
|
+
}
|
|
877
|
+
function threadLifecycle(calls) {
|
|
878
|
+
const highCost = threshold('high_cost_usd', 1);
|
|
879
|
+
const highContext = threshold('high_context_percent', 0.6);
|
|
880
|
+
let largestJump = 0;
|
|
881
|
+
let largestJumpRow = null;
|
|
882
|
+
for (let index = 1; index < calls.length; index += 1) {
|
|
883
|
+
const previous = Number(calls[index - 1].cumulative_total_tokens || 0);
|
|
884
|
+
const current = Number(calls[index].cumulative_total_tokens || 0);
|
|
885
|
+
const jump = Math.max(current - previous, Number(calls[index].total_tokens || 0), 0);
|
|
886
|
+
if (jump > largestJump) {
|
|
887
|
+
largestJump = jump;
|
|
888
|
+
largestJumpRow = calls[index];
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const firstExpensiveIndex = calls.findIndex(row => Number(row.estimated_cost_usd || 0) >= highCost || Number(row.context_window_percent || 0) >= highContext);
|
|
892
|
+
const firstExpensiveRow = firstExpensiveIndex >= 0 ? calls[firstExpensiveIndex] : null;
|
|
893
|
+
const first = calls[0] || {};
|
|
894
|
+
const last = calls[calls.length - 1] || {};
|
|
895
|
+
const cacheTrend = Number(last.cache_ratio || 0) - Number(first.cache_ratio || 0);
|
|
896
|
+
const contextTrend = Number(last.context_window_percent || 0) - Number(first.context_window_percent || 0);
|
|
897
|
+
const spikeIndex = largestJumpRow ? calls.indexOf(largestJumpRow) : -1;
|
|
898
|
+
const subagentBeforeSpike = spikeIndex > 0 && calls.slice(0, spikeIndex).some(row => isSubagent(row) || isAutoReview(row));
|
|
899
|
+
const topAction = calls.map(topRecommendation).filter(Boolean)[0];
|
|
900
|
+
let action = topAction
|
|
901
|
+
? topAction.action
|
|
902
|
+
: 'Expand calls or select a row for call-level recommendations.';
|
|
903
|
+
if (contextTrend >= 0.15 || Number(last.context_window_percent || 0) >= highContext) {
|
|
904
|
+
action = 'Review where context growth begins and consider starting a fresh thread.';
|
|
905
|
+
} else if (cacheTrend <= -0.25) {
|
|
906
|
+
action = 'Check for reintroduced files or tool output after cache reuse dropped.';
|
|
907
|
+
} else if (subagentBeforeSpike) {
|
|
908
|
+
action = 'Compare attached subagent or review calls before changing the parent workflow.';
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
firstExpensiveRow,
|
|
912
|
+
firstExpensiveIndex,
|
|
913
|
+
largestJump,
|
|
914
|
+
largestJumpRow,
|
|
915
|
+
cacheTrend,
|
|
916
|
+
contextTrend,
|
|
917
|
+
subagentBeforeSpike,
|
|
918
|
+
action,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function groupThreads(rows) {
|
|
922
|
+
const map = new Map();
|
|
923
|
+
for (const row of rows) {
|
|
924
|
+
const attachment = rowAttachment(row);
|
|
925
|
+
const key = attachment.key;
|
|
926
|
+
if (!map.has(key)) {
|
|
927
|
+
map.set(key, { key, label: attachment.label, rows: [] });
|
|
928
|
+
}
|
|
929
|
+
map.get(key).rows.push(row);
|
|
930
|
+
}
|
|
931
|
+
const groups = [...map.values()].map(group => {
|
|
932
|
+
const calls = group.rows.slice().sort(chronological);
|
|
933
|
+
const totalTokens = calls.reduce((sum, row) => sum + Number(row.total_tokens || 0), 0);
|
|
934
|
+
const inputTokens = calls.reduce((sum, row) => sum + Number(row.input_tokens || 0), 0);
|
|
935
|
+
const cachedTokens = calls.reduce((sum, row) => sum + Number(row.cached_input_tokens || 0), 0);
|
|
936
|
+
const estimatedCost = calls.reduce((sum, row) => sum + Number(row.estimated_cost_usd || 0), 0);
|
|
937
|
+
const usageCredits = sumUsageCredits(calls);
|
|
938
|
+
const signalCount = calls.reduce((sum, row) => sum + (Array.isArray(row.efficiency_flags) ? row.efficiency_flags.length : 0), 0);
|
|
939
|
+
const latestActivity = calls.reduce((latest, row) => String(row.event_timestamp || '') > latest ? String(row.event_timestamp || '') : latest, '');
|
|
940
|
+
const maxContextUse = calls.reduce((max, row) => Math.max(max, Number(row.context_window_percent || 0)), 0);
|
|
941
|
+
const subagentCount = calls.filter(isSubagent).length;
|
|
942
|
+
const autoReviewCount = calls.filter(isAutoReview).length;
|
|
943
|
+
const attachedCount = calls.filter(row => rowAttachment(row).relation !== 'direct' && rowAttachment(row).relation !== 'session').length;
|
|
944
|
+
const modelSummary = threadModelSummary(calls);
|
|
945
|
+
const effortSummary = compactListSummary(calls.map(row => row.effort), 'efforts');
|
|
946
|
+
const parentThreadLabel = dominantParentThread(calls, group.label);
|
|
947
|
+
const lifecycle = threadLifecycle(calls);
|
|
948
|
+
return {
|
|
949
|
+
key: group.key,
|
|
950
|
+
label: group.label,
|
|
951
|
+
calls,
|
|
952
|
+
callCount: calls.length,
|
|
953
|
+
latestActivity,
|
|
954
|
+
parentThreadLabel,
|
|
955
|
+
modelSummary,
|
|
956
|
+
effortSummary,
|
|
957
|
+
totalTokens,
|
|
958
|
+
estimatedCost,
|
|
959
|
+
usageCredits,
|
|
960
|
+
cacheRatio: inputTokens ? cachedTokens / inputTokens : 0,
|
|
961
|
+
maxContextUse,
|
|
962
|
+
pricingStatus: pricingStatusFor(calls),
|
|
963
|
+
creditStatus: creditStatusFor(calls),
|
|
964
|
+
signalCount,
|
|
965
|
+
subagentCount,
|
|
966
|
+
autoReviewCount,
|
|
967
|
+
attachedCount,
|
|
968
|
+
lifecycle,
|
|
969
|
+
attentionScore: 0,
|
|
970
|
+
};
|
|
971
|
+
});
|
|
972
|
+
for (const group of groups) {
|
|
973
|
+
group.attentionScore = threadAttentionScore(group);
|
|
974
|
+
}
|
|
975
|
+
return arrangeThreadGroups(groups);
|
|
976
|
+
}
|
|
977
|
+
function paginate(items) {
|
|
978
|
+
const pageCount = Math.max(1, Math.ceil(items.length / pageSize));
|
|
979
|
+
currentPage = Math.min(Math.max(currentPage, 1), pageCount);
|
|
980
|
+
const start = (currentPage - 1) * pageSize;
|
|
981
|
+
const end = Math.min(start + pageSize, items.length);
|
|
982
|
+
return {
|
|
983
|
+
items: items.slice(start, end),
|
|
984
|
+
start,
|
|
985
|
+
end,
|
|
986
|
+
total: items.length,
|
|
987
|
+
pageCount,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function updatePager(page, itemLabel = 'rows') {
|
|
991
|
+
const shouldShowPager = page.pageCount > 1;
|
|
992
|
+
pagerEl.hidden = !shouldShowPager;
|
|
993
|
+
prevPageEl.disabled = currentPage <= 1;
|
|
994
|
+
nextPageEl.disabled = currentPage >= page.pageCount;
|
|
995
|
+
if (!page.total) {
|
|
996
|
+
pageStatusEl.textContent = 'No rows';
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
pageStatusEl.textContent = `${number.format(page.start + 1)}-${number.format(page.end)} of ${number.format(page.total)} ${itemLabel} · page ${number.format(currentPage)}/${number.format(page.pageCount)}`;
|
|
1000
|
+
}
|
|
1001
|
+
function buildInsights(rows) {
|
|
1002
|
+
const groups = groupThreads(rows);
|
|
1003
|
+
const insights = [];
|
|
1004
|
+
const topCostGroup = groups.filter(group => group.estimatedCost > 0).sort((a, b) => b.estimatedCost - a.estimatedCost || b.attentionScore - a.attentionScore)[0];
|
|
1005
|
+
if (topCostGroup) {
|
|
1006
|
+
insights.push({
|
|
1007
|
+
title: 'Costliest thread',
|
|
1008
|
+
value: pricingConfigured ? money(topCostGroup.estimatedCost) : 'Not configured',
|
|
1009
|
+
body: `${topCostGroup.label} has ${number.format(topCostGroup.callCount)} calls and ${number.format(topCostGroup.totalTokens)} tokens.`,
|
|
1010
|
+
severity: severityForScore(topCostGroup.attentionScore),
|
|
1011
|
+
action: 'Open thread timeline',
|
|
1012
|
+
preset: 'highest-cost',
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
const lowCacheLimit = threshold('low_cache_ratio', 0.3);
|
|
1016
|
+
const lowCacheRows = rows.filter(row => Number(row.input_tokens || 0) > 0 && Number(row.cache_ratio || 0) < lowCacheLimit);
|
|
1017
|
+
if (lowCacheRows.length) {
|
|
1018
|
+
const lowest = lowCacheRows.slice().sort((a, b) => Number(a.cache_ratio || 0) - Number(b.cache_ratio || 0))[0];
|
|
1019
|
+
insights.push({
|
|
1020
|
+
title: 'Low cache reuse',
|
|
1021
|
+
value: pct(lowest.cache_ratio),
|
|
1022
|
+
body: `${number.format(lowCacheRows.length)} calls are under ${pct(lowCacheLimit)} cache reuse. Start with ${rowThreadLabel(lowest)}.`,
|
|
1023
|
+
severity: 'medium',
|
|
1024
|
+
action: 'Apply cache-misses preset',
|
|
1025
|
+
preset: 'cache-misses',
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
const highContextLimit = threshold('high_context_percent', 0.6);
|
|
1029
|
+
const highContextRows = rows.filter(row => Number(row.context_window_percent || 0) >= highContextLimit);
|
|
1030
|
+
if (highContextRows.length) {
|
|
1031
|
+
const highest = highContextRows.slice().sort((a, b) => Number(b.context_window_percent || 0) - Number(a.context_window_percent || 0))[0];
|
|
1032
|
+
insights.push({
|
|
1033
|
+
title: 'Context bloat',
|
|
1034
|
+
value: pct(highest.context_window_percent),
|
|
1035
|
+
body: `${number.format(highContextRows.length)} calls are at or above ${pct(highContextLimit)} context use.`,
|
|
1036
|
+
severity: severityForScore(rowAttentionScore(highest)),
|
|
1037
|
+
action: 'Apply context-bloat preset',
|
|
1038
|
+
preset: 'context-bloat',
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
const usageCredits = sumUsageCredits(rows);
|
|
1042
|
+
if (usageCredits > 0) {
|
|
1043
|
+
const creditCoverage = creditCoverageRatio(rows);
|
|
1044
|
+
insights.push({
|
|
1045
|
+
title: 'Codex allowance usage',
|
|
1046
|
+
value: `${credits(usageCredits)} credits`,
|
|
1047
|
+
body: allowanceWindowText(usageCredits, 'impact') || allowanceWindowText(usageCredits, 'remaining') || `${pct(creditCoverage)} of visible tokens map to Codex credit rates.`,
|
|
1048
|
+
severity: severityForScore(clamp(usageCredits * 2.4, 0, 140)),
|
|
1049
|
+
action: 'Review highest-credit calls',
|
|
1050
|
+
preset: 'usage-credits',
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
const unpricedTokens = rows.reduce((sum, row) => sum + (!row.pricing_model ? Number(row.total_tokens || 0) : 0), 0);
|
|
1054
|
+
if (unpricedTokens) {
|
|
1055
|
+
insights.push({
|
|
1056
|
+
title: 'Unpriced usage',
|
|
1057
|
+
value: number.format(unpricedTokens),
|
|
1058
|
+
body: 'These tokens are omitted from estimated cost totals until pricing is configured.',
|
|
1059
|
+
severity: 'review',
|
|
1060
|
+
action: 'Review pricing gaps',
|
|
1061
|
+
preset: 'pricing-gaps',
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
const estimatedTokens = rows.reduce((sum, row) => sum + (row.pricing_estimated ? Number(row.total_tokens || 0) : 0), 0);
|
|
1065
|
+
if (estimatedTokens) {
|
|
1066
|
+
insights.push({
|
|
1067
|
+
title: 'Estimated pricing',
|
|
1068
|
+
value: number.format(estimatedTokens),
|
|
1069
|
+
body: 'Marked best-guess prices are included, but should be reviewed separately.',
|
|
1070
|
+
severity: 'review',
|
|
1071
|
+
action: 'Review estimates',
|
|
1072
|
+
preset: 'estimated-review',
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
const reasoningRows = rows.filter(row => Number(row.reasoning_output_tokens || 0) > 0).sort((a, b) => Number(b.reasoning_output_tokens || 0) - Number(a.reasoning_output_tokens || 0));
|
|
1076
|
+
if (reasoningRows[0]) {
|
|
1077
|
+
insights.push({
|
|
1078
|
+
title: 'Reasoning output spike',
|
|
1079
|
+
value: number.format(reasoningRows[0].reasoning_output_tokens || 0),
|
|
1080
|
+
body: `${rowThreadLabel(reasoningRows[0])} has the largest reasoning-output call in the current filter.`,
|
|
1081
|
+
severity: severityForScore(rowAttentionScore(reasoningRows[0])),
|
|
1082
|
+
action: 'Inspect selected call',
|
|
1083
|
+
view: 'calls',
|
|
1084
|
+
sort: 'signals',
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
return insights.slice(0, 6);
|
|
1088
|
+
}
|
|
1089
|
+
function renderInsightPanel(rows) {
|
|
1090
|
+
if (activeView !== 'insights' && !activePreset) {
|
|
1091
|
+
insightsPanelEl.hidden = true;
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
insightsPanelEl.hidden = false;
|
|
1095
|
+
renderPresetControls();
|
|
1096
|
+
const insights = buildInsights(rows);
|
|
1097
|
+
if (!insights.length) {
|
|
1098
|
+
insightCardsEl.innerHTML = '<div class="empty-state">No attention signals match the current filters.</div>';
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
insightCardsEl.innerHTML = insights.map((insight, index) => {
|
|
1102
|
+
const severity = insight.severity || 'review';
|
|
1103
|
+
return `
|
|
1104
|
+
<article class="insight-card" data-severity="${escapeHtml(severity)}">
|
|
1105
|
+
<div class="insight-card-header">
|
|
1106
|
+
<h3>${escapeHtml(insight.title)}</h3>
|
|
1107
|
+
<span class="severity-chip ${escapeHtml(severity)}">${escapeHtml(severity === 'high' ? 'High' : severity === 'medium' ? 'Medium' : 'Review')}</span>
|
|
1108
|
+
</div>
|
|
1109
|
+
<strong>${escapeHtml(insight.value)}</strong>
|
|
1110
|
+
<p>${escapeHtml(insight.body)}</p>
|
|
1111
|
+
<button class="insight-action" type="button" data-insight-index="${index}">${escapeHtml(insight.action)}</button>
|
|
1112
|
+
</article>
|
|
1113
|
+
`;
|
|
1114
|
+
}).join('');
|
|
1115
|
+
insightCardsEl.querySelectorAll('[data-insight-index]').forEach(button => {
|
|
1116
|
+
const insight = insights[Number(button.dataset.insightIndex)];
|
|
1117
|
+
button.addEventListener('click', () => {
|
|
1118
|
+
if (insight.preset) {
|
|
1119
|
+
applyPreset(insight.preset);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
activeView = insight.view || 'calls';
|
|
1123
|
+
if (insight.sort) {
|
|
1124
|
+
sortKey = insight.sort;
|
|
1125
|
+
sortDirection = defaultSortDirection(insight.sort);
|
|
1126
|
+
sortEl.value = sortKey;
|
|
1127
|
+
}
|
|
1128
|
+
currentPage = 1;
|
|
1129
|
+
render();
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
function renderPresetControls() {
|
|
1134
|
+
const preset = activePresetDefinition();
|
|
1135
|
+
clearPresetEl.hidden = !preset;
|
|
1136
|
+
presetStatusEl.textContent = preset ? `${preset.caption}: ${preset.description}` : 'No preset applied.';
|
|
1137
|
+
presetListEl.innerHTML = presetDefinitions.map(candidate => `
|
|
1138
|
+
<button class="preset-card" type="button" data-preset="${escapeHtml(candidate.key)}" aria-pressed="${candidate.key === activePreset ? 'true' : 'false'}">
|
|
1139
|
+
<span class="preset-copy"><b>${escapeHtml(candidate.label)}</b><span>${escapeHtml(candidate.description)}</span></span>
|
|
1140
|
+
<span class="preset-chip">Run</span>
|
|
1141
|
+
</button>
|
|
1142
|
+
`).join('');
|
|
1143
|
+
presetListEl.querySelectorAll('[data-preset]').forEach(button => {
|
|
1144
|
+
button.addEventListener('click', () => applyPreset(button.dataset.preset));
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
function render() {
|
|
1148
|
+
const dateRange = updateDateFilterControls();
|
|
1149
|
+
const rows = filtered(dateRange);
|
|
1150
|
+
rowsEl.textContent = '';
|
|
1151
|
+
updateSortControls();
|
|
1152
|
+
const totalTokens = rows.reduce((sum, row) => sum + Number(row.total_tokens || 0), 0);
|
|
1153
|
+
const cachedInputTokens = rows.reduce((sum, row) => sum + Number(row.cached_input_tokens || 0), 0);
|
|
1154
|
+
const uncachedInputTokens = rows.reduce((sum, row) => sum + Number(row.uncached_input_tokens || 0), 0);
|
|
1155
|
+
const reasoningOutputTokens = rows.reduce((sum, row) => sum + Number(row.reasoning_output_tokens || 0), 0);
|
|
1156
|
+
document.getElementById('visibleCalls').textContent = number.format(rows.length);
|
|
1157
|
+
document.getElementById('totalTokens').textContent = number.format(totalTokens);
|
|
1158
|
+
document.getElementById('cachedTokens').textContent = number.format(cachedInputTokens);
|
|
1159
|
+
document.getElementById('uncachedTokens').textContent = number.format(uncachedInputTokens);
|
|
1160
|
+
document.getElementById('reasoningTokens').textContent = number.format(reasoningOutputTokens);
|
|
1161
|
+
const estimatedCost = rows.reduce((sum, row) => sum + Number(row.estimated_cost_usd || 0), 0);
|
|
1162
|
+
const usageCredits = sumUsageCredits(rows);
|
|
1163
|
+
document.getElementById('estimatedCost').textContent = pricingConfigured ? money(estimatedCost) : 'Not configured';
|
|
1164
|
+
document.getElementById('usageCredits').textContent = credits(usageCredits);
|
|
1165
|
+
document.getElementById('allowanceImpact').textContent = allowanceImpactText(usageCredits);
|
|
1166
|
+
document.getElementById('allowanceImpact').title = allowanceWindowText(usageCredits, 'remaining') || 'Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.';
|
|
1167
|
+
insightsViewEl.setAttribute('aria-pressed', activeView === 'insights' ? 'true' : 'false');
|
|
1168
|
+
callsViewEl.setAttribute('aria-pressed', activeView === 'calls' ? 'true' : 'false');
|
|
1169
|
+
threadsViewEl.setAttribute('aria-pressed', activeView === 'threads' ? 'true' : 'false');
|
|
1170
|
+
renderInsightPanel(rows);
|
|
1171
|
+
if (activeView === 'threads') {
|
|
1172
|
+
renderThreads(rows);
|
|
1173
|
+
} else if (activeView === 'insights') {
|
|
1174
|
+
renderThreads(rows, 'insights');
|
|
1175
|
+
} else {
|
|
1176
|
+
renderCalls(rows);
|
|
1177
|
+
}
|
|
1178
|
+
fitModelPills();
|
|
1179
|
+
syncUrlState();
|
|
1180
|
+
}
|
|
1181
|
+
function renderCalls(rows) {
|
|
1182
|
+
const page = paginate(rows);
|
|
1183
|
+
updatePager(page, 'calls');
|
|
1184
|
+
tableTitleEl.textContent = 'Model Calls';
|
|
1185
|
+
const preset = activePresetDefinition();
|
|
1186
|
+
const prefix = preset ? `${preset.caption}. ` : '';
|
|
1187
|
+
tableCaptionEl.textContent = `${prefix}${dateCaptionPrefix()}Showing individual model calls sorted by ${tableCaptionEl.dataset.sortDescription}. ${loadedRowsDescription()}.`;
|
|
1188
|
+
for (const row of page.items) {
|
|
1189
|
+
const tr = document.createElement('tr');
|
|
1190
|
+
const flags = Array.isArray(row.efficiency_flags) ? row.efficiency_flags : [];
|
|
1191
|
+
tr.className = 'call-row';
|
|
1192
|
+
tr.tabIndex = 0;
|
|
1193
|
+
tr.setAttribute('role', 'button');
|
|
1194
|
+
tr.setAttribute('aria-label', `Inspect ${rowThreadLabel(row)} usage`);
|
|
1195
|
+
tr.innerHTML = `
|
|
1196
|
+
<td>${renderTimeCell(row.event_timestamp)}</td>
|
|
1197
|
+
<td title="${escapeHtml(short(row.session_id))}">${escapeHtml(truncate(rowThreadLabel(row)))}</td>
|
|
1198
|
+
<td><span class="pill model-pill" data-full-label="${escapeHtml(short(row.model))}">${escapeHtml(short(row.model))}</span></td>
|
|
1199
|
+
<td>${escapeHtml(short(row.effort))}</td>
|
|
1200
|
+
<td class="num">${number.format(row.total_tokens || 0)}</td>
|
|
1201
|
+
<td class="num">${costUsageCell(row.pricing_estimated ? `${money(row.estimated_cost_usd)}*` : money(row.estimated_cost_usd), usageCreditValue(row))}</td>
|
|
1202
|
+
<td class="num">${pct(row.cache_ratio)}</td>
|
|
1203
|
+
<td><div class="flags">${flags.slice(0, 2).map(flag => `<span class="flag">${escapeHtml(flag)}</span>`).join('')}</div></td>
|
|
1204
|
+
`;
|
|
1205
|
+
tr.addEventListener('mouseenter', () => showDetail(row));
|
|
1206
|
+
tr.addEventListener('click', () => selectRow(row));
|
|
1207
|
+
tr.addEventListener('keydown', event => {
|
|
1208
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1209
|
+
event.preventDefault();
|
|
1210
|
+
selectRow(row);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
rowsEl.appendChild(tr);
|
|
1214
|
+
}
|
|
1215
|
+
if (!initialDetailApplied && selectedRecordId) {
|
|
1216
|
+
const selected = rows.find(row => row.record_id === selectedRecordId);
|
|
1217
|
+
if (selected) {
|
|
1218
|
+
initialDetailApplied = true;
|
|
1219
|
+
showDetail(selected);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (!initialDetailApplied && urlParams.get('detail') === 'first' && page.items[0]) {
|
|
1223
|
+
initialDetailApplied = true;
|
|
1224
|
+
showDetail(page.items[0]);
|
|
1225
|
+
}
|
|
1226
|
+
if (!rows.length) {
|
|
1227
|
+
rowsEl.innerHTML = '<tr><td class="empty-state" colspan="8">No calls match the current filters.</td></tr>';
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function renderThreads(rows, mode = 'threads') {
|
|
1231
|
+
const groups = groupThreads(rows);
|
|
1232
|
+
if (!initialThreadExpansionApplied && (activeView === 'threads' || activeView === 'insights')) {
|
|
1233
|
+
const expansion = urlParams.get('expand');
|
|
1234
|
+
if (expansion === 'all') {
|
|
1235
|
+
groups.forEach(group => expandedThreads.add(group.key));
|
|
1236
|
+
} else if (expansion === 'first' && groups[0]) {
|
|
1237
|
+
expandedThreads.add(groups[0].key);
|
|
1238
|
+
}
|
|
1239
|
+
initialThreadExpansionApplied = true;
|
|
1240
|
+
}
|
|
1241
|
+
const page = paginate(groups);
|
|
1242
|
+
updatePager(page, 'threads');
|
|
1243
|
+
tableTitleEl.textContent = mode === 'insights' ? 'Top Threads by Attention Score' : 'Threads';
|
|
1244
|
+
const preset = activePresetDefinition();
|
|
1245
|
+
const prefix = preset ? `${preset.caption}. ` : '';
|
|
1246
|
+
tableCaptionEl.textContent = `${prefix}${dateCaptionPrefix()}Showing ${number.format(groups.length)} threads from ${number.format(rows.length)} filtered calls, sorted by ${tableCaptionEl.dataset.sortDescription}. ${loadedRowsDescription()}. Click a thread to expand its calls.`;
|
|
1247
|
+
for (const group of page.items) {
|
|
1248
|
+
const tr = document.createElement('tr');
|
|
1249
|
+
const expanded = expandedThreads.has(group.key);
|
|
1250
|
+
const threadNotes = [
|
|
1251
|
+
`${number.format(group.callCount)} calls`,
|
|
1252
|
+
group.pricingStatus,
|
|
1253
|
+
group.parentThreadLabel ? `spawned from ${group.parentThreadLabel}` : '',
|
|
1254
|
+
group.childThreadCount ? `${number.format(group.childThreadCount)} spawned threads` : '',
|
|
1255
|
+
group.subagentCount ? `${number.format(group.subagentCount)} subagent` : '',
|
|
1256
|
+
group.autoReviewCount ? `${number.format(group.autoReviewCount)} auto-review` : '',
|
|
1257
|
+
group.attachedCount ? 'attached' : '',
|
|
1258
|
+
].filter(Boolean).join(' - ');
|
|
1259
|
+
tr.className = `thread-row${group.parentThreadLabel ? ' spawned-thread' : ''}`;
|
|
1260
|
+
tr.tabIndex = 0;
|
|
1261
|
+
tr.setAttribute('role', 'button');
|
|
1262
|
+
tr.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|
1263
|
+
tr.setAttribute('aria-label', `${expanded ? 'Collapse' : 'Expand'} ${group.label} calls. Attention score ${Math.round(group.attentionScore)}.`);
|
|
1264
|
+
tr.innerHTML = `
|
|
1265
|
+
<td>${renderTimeCell(group.latestActivity)}</td>
|
|
1266
|
+
<td>
|
|
1267
|
+
<div class="thread-title">
|
|
1268
|
+
<span class="thread-toggle" aria-hidden="true">${expanded ? '-' : '+'}</span>
|
|
1269
|
+
<span class="thread-meta">
|
|
1270
|
+
<span class="thread-name">${group.renderAsChild ? '<span class="thread-relation">spawned</span> ' : ''}${escapeHtml(truncate(group.label, 72))}</span>
|
|
1271
|
+
<span class="thread-subtle">${escapeHtml(threadNotes)} · attention ${number.format(Math.round(group.attentionScore))}</span>
|
|
1272
|
+
</span>
|
|
1273
|
+
</div>
|
|
1274
|
+
</td>
|
|
1275
|
+
<td><span class="pill model-pill" data-full-label="${escapeHtml(short(group.modelSummary))}">${escapeHtml(short(group.modelSummary))}</span></td>
|
|
1276
|
+
<td>${escapeHtml(truncate(group.effortSummary, 28))}</td>
|
|
1277
|
+
<td class="num">${number.format(group.totalTokens)}</td>
|
|
1278
|
+
<td class="num">${costUsageCell(pricingConfigured ? money(group.estimatedCost) : 'Not configured', group.usageCredits)}</td>
|
|
1279
|
+
<td class="num">${pct(group.cacheRatio)}</td>
|
|
1280
|
+
<td class="num">${number.format(group.signalCount)}</td>
|
|
1281
|
+
`;
|
|
1282
|
+
tr.addEventListener('click', () => {
|
|
1283
|
+
if (expandedThreads.has(group.key)) {
|
|
1284
|
+
expandedThreads.delete(group.key);
|
|
1285
|
+
} else {
|
|
1286
|
+
expandedThreads.add(group.key);
|
|
1287
|
+
}
|
|
1288
|
+
selectThread(group);
|
|
1289
|
+
render();
|
|
1290
|
+
});
|
|
1291
|
+
tr.addEventListener('keydown', event => {
|
|
1292
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1293
|
+
event.preventDefault();
|
|
1294
|
+
tr.click();
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
tr.addEventListener('mouseenter', () => showThreadDetail(group));
|
|
1298
|
+
rowsEl.appendChild(tr);
|
|
1299
|
+
if (expanded) {
|
|
1300
|
+
rowsEl.appendChild(renderThreadCalls(group));
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (!groups.length) {
|
|
1304
|
+
rowsEl.innerHTML = '<tr><td class="empty-state" colspan="8">No threads match the current filters.</td></tr>';
|
|
1305
|
+
}
|
|
1306
|
+
if (!initialDetailApplied && selectedThreadKey) {
|
|
1307
|
+
const selected = groups.find(group => group.key === selectedThreadKey);
|
|
1308
|
+
if (selected) {
|
|
1309
|
+
initialDetailApplied = true;
|
|
1310
|
+
showThreadDetail(selected);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (!initialDetailApplied && urlParams.get('detail') === 'first' && page.items[0]) {
|
|
1314
|
+
initialDetailApplied = true;
|
|
1315
|
+
showThreadDetail(page.items[0]);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function renderThreadCalls(group) {
|
|
1319
|
+
const tr = document.createElement('tr');
|
|
1320
|
+
tr.className = 'thread-child-row';
|
|
1321
|
+
const calls = group.calls.map(row => {
|
|
1322
|
+
const flags = Array.isArray(row.efficiency_flags) ? row.efficiency_flags : [];
|
|
1323
|
+
return `
|
|
1324
|
+
<tr class="thread-call-row" tabindex="0" role="button" data-record-id="${escapeHtml(row.record_id || '')}">
|
|
1325
|
+
<td>${renderTimeCell(row.event_timestamp)}</td>
|
|
1326
|
+
<td><span class="pill model-pill" data-full-label="${escapeHtml(short(row.model))}">${escapeHtml(short(row.model))}</span></td>
|
|
1327
|
+
<td>${escapeHtml(short(row.effort))}</td>
|
|
1328
|
+
<td>${escapeHtml(sourceLabel(row))}</td>
|
|
1329
|
+
<td class="num">${number.format(row.total_tokens || 0)}</td>
|
|
1330
|
+
<td class="num">${costUsageCell(row.pricing_estimated ? `${money(row.estimated_cost_usd)}*` : money(row.estimated_cost_usd), usageCreditValue(row))}</td>
|
|
1331
|
+
<td class="num">${pct(row.cache_ratio)}</td>
|
|
1332
|
+
<td><div class="flags">${flags.slice(0, 2).map(flag => `<span class="flag">${escapeHtml(flag)}</span>`).join('')}</div></td>
|
|
1333
|
+
</tr>
|
|
1334
|
+
`;
|
|
1335
|
+
}).join('');
|
|
1336
|
+
tr.innerHTML = `
|
|
1337
|
+
<td class="child-cell" colspan="8">
|
|
1338
|
+
<table class="thread-call-table" aria-label="${escapeHtml(group.label)} calls">
|
|
1339
|
+
<thead><tr><th>Time</th><th>Model</th><th>Effort</th><th>Source</th><th class="num">Last Call</th><th class="num">Cost</th><th class="num">Cache</th><th>Signals</th></tr></thead>
|
|
1340
|
+
<tbody>${calls}</tbody>
|
|
1341
|
+
</table>
|
|
1342
|
+
</td>
|
|
1343
|
+
`;
|
|
1344
|
+
return tr;
|
|
1345
|
+
}
|
|
1346
|
+
function contextControls(row) {
|
|
1347
|
+
const fileMode = window.location.protocol === 'file:';
|
|
1348
|
+
const apiUnavailable = !contextApiEnabled || !apiToken;
|
|
1349
|
+
const disabled = fileMode || apiUnavailable ? ' disabled' : '';
|
|
1350
|
+
const hint = fileMode
|
|
1351
|
+
? 'Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.'
|
|
1352
|
+
: apiUnavailable
|
|
1353
|
+
? 'Context loading is disabled for this dashboard server. Restart with --context-api explicit to enable explicit row actions.'
|
|
1354
|
+
: 'Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.';
|
|
1355
|
+
return `
|
|
1356
|
+
<div class="context-actions">
|
|
1357
|
+
<button class="context-button" type="button" data-context-load${disabled}>Load context</button>
|
|
1358
|
+
<button class="context-button secondary" type="button" data-context-load-output${disabled}>Include tool output</button>
|
|
1359
|
+
</div>
|
|
1360
|
+
<div id="contextResult" class="context-result"><p class="context-note">${escapeHtml(hint)}</p></div>
|
|
1361
|
+
`;
|
|
1362
|
+
}
|
|
1363
|
+
function bindContextButtons(row) {
|
|
1364
|
+
const loadButton = detailEl.querySelector('[data-context-load]');
|
|
1365
|
+
const outputButton = detailEl.querySelector('[data-context-load-output]');
|
|
1366
|
+
if (loadButton) loadButton.addEventListener('click', () => loadContext(row, false));
|
|
1367
|
+
if (outputButton) outputButton.addEventListener('click', () => loadContext(row, true));
|
|
1368
|
+
}
|
|
1369
|
+
async function loadContext(row, includeToolOutput) {
|
|
1370
|
+
const target = document.getElementById('contextResult');
|
|
1371
|
+
if (!target) return;
|
|
1372
|
+
if (!row.record_id) {
|
|
1373
|
+
target.innerHTML = '<p class="context-note">This row has no record id for context lookup.</p>';
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
target.innerHTML = '<p class="context-note">Loading local context...</p>';
|
|
1377
|
+
const params = new URLSearchParams({ record_id: row.record_id });
|
|
1378
|
+
if (includeToolOutput) params.set('include_tool_output', '1');
|
|
1379
|
+
try {
|
|
1380
|
+
const response = await fetch(`/api/context?${params.toString()}`, {
|
|
1381
|
+
headers: {
|
|
1382
|
+
'Accept': 'application/json',
|
|
1383
|
+
'X-Codex-Usage-Token': apiToken,
|
|
1384
|
+
},
|
|
1385
|
+
cache: 'no-store',
|
|
1386
|
+
});
|
|
1387
|
+
if (!response.ok) {
|
|
1388
|
+
const errorText = response.status === 404
|
|
1389
|
+
? 'Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.'
|
|
1390
|
+
: `Context API returned HTTP ${response.status}.`;
|
|
1391
|
+
throw new Error(errorText);
|
|
1392
|
+
}
|
|
1393
|
+
const payload = await response.json();
|
|
1394
|
+
target.innerHTML = renderContext(payload);
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
target.innerHTML = `<p class="context-note">${escapeHtml(error.message || String(error))}</p>`;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
function renderContext(payload) {
|
|
1400
|
+
const entries = Array.isArray(payload.entries) ? payload.entries : [];
|
|
1401
|
+
const source = payload.source || {};
|
|
1402
|
+
const omitted = payload.omitted || {};
|
|
1403
|
+
const note = [
|
|
1404
|
+
'Loaded on demand from local JSONL.',
|
|
1405
|
+
payload.raw_context_persisted === false ? 'Not persisted to SQLite or dashboard HTML.' : '',
|
|
1406
|
+
payload.include_tool_output ? 'Tool output included with redaction and size limits.' : 'Tool output omitted by default.',
|
|
1407
|
+
source.file ? `Source: ${source.file}:${source.line_number || ''}` : '',
|
|
1408
|
+
omitted.older_entries ? `${number.format(omitted.older_entries)} older entries omitted.` : '',
|
|
1409
|
+
omitted.over_budget_chars ? `${number.format(omitted.over_budget_chars)} chars over budget omitted.` : '',
|
|
1410
|
+
].filter(Boolean).join(' ');
|
|
1411
|
+
const body = entries.map(entry => `
|
|
1412
|
+
<div class="context-entry">
|
|
1413
|
+
<div class="context-entry-header">
|
|
1414
|
+
<span>${escapeHtml(entry.label || entry.type || 'entry')}</span>
|
|
1415
|
+
<span>${escapeHtml([formatTimestamp(entry.timestamp, ''), entry.line_number ? `line ${entry.line_number}` : ''].filter(Boolean).join(' - '))}</span>
|
|
1416
|
+
</div>
|
|
1417
|
+
<pre>${escapeHtml(entry.text || '')}</pre>
|
|
1418
|
+
</div>
|
|
1419
|
+
`).join('');
|
|
1420
|
+
return `<p class="context-note">${escapeHtml(note)}</p>${body || '<p class="context-note">No context entries found for this call.</p>'}`;
|
|
1421
|
+
}
|
|
1422
|
+
function pricingStatusText(row) {
|
|
1423
|
+
if (!row.pricing_model) return 'No configured price';
|
|
1424
|
+
return row.pricing_estimated ? 'Best-guess estimate' : 'Configured price';
|
|
1425
|
+
}
|
|
1426
|
+
function nextActionForRow(row) {
|
|
1427
|
+
if (row.recommended_action) return row.recommended_action;
|
|
1428
|
+
if (!row.pricing_model) return 'Configure pricing before trusting cost totals.';
|
|
1429
|
+
if (Number(row.cache_ratio || 0) < 0.3 && Number(row.input_tokens || 0) > 0) return 'Compare fresh input with the previous turn before continuing.';
|
|
1430
|
+
if (Number(row.context_window_percent || 0) >= 0.6) return 'Inspect the thread timeline and consider starting a fresh thread.';
|
|
1431
|
+
if (Number(row.reasoning_output_tokens || 0) > Number(row.output_tokens || 0)) return 'Review whether reasoning effort is appropriate for this task.';
|
|
1432
|
+
return 'Use the aggregate fields first; load context only if the signal is still unclear.';
|
|
1433
|
+
}
|
|
1434
|
+
function fieldsList(fields, className = 'detail-kv') {
|
|
1435
|
+
return `<dl class="${className}">${fields.map(([key, value]) => `<dt>${escapeHtml(key)}</dt><dd>${escapeHtml(short(value))}</dd>`).join('')}</dl>`;
|
|
1436
|
+
}
|
|
1437
|
+
function detailCollapse(title, fields) {
|
|
1438
|
+
return `
|
|
1439
|
+
<details class="detail-collapse">
|
|
1440
|
+
<summary>${escapeHtml(title)}</summary>
|
|
1441
|
+
<div class="detail-collapse-body">${fieldsList(fields)}</div>
|
|
1442
|
+
</details>
|
|
1443
|
+
`;
|
|
1444
|
+
}
|
|
1445
|
+
function timelineSeverity(value) {
|
|
1446
|
+
if (value >= 0.65) return 'high';
|
|
1447
|
+
if (value >= 0.35) return 'medium';
|
|
1448
|
+
return 'low';
|
|
1449
|
+
}
|
|
1450
|
+
function timelineWidth(value) {
|
|
1451
|
+
return `${Math.round(clamp(Number(value || 0), 0, 1) * 100)}%`;
|
|
1452
|
+
}
|
|
1453
|
+
function renderThreadTimeline(group) {
|
|
1454
|
+
const calls = group.calls.slice(-5);
|
|
1455
|
+
if (!calls.length) return '<p>No calls in this thread.</p>';
|
|
1456
|
+
return `<div class="timeline-list">${calls.map(row => {
|
|
1457
|
+
const contextUse = Number(row.context_window_percent || 0);
|
|
1458
|
+
return `
|
|
1459
|
+
<div class="timeline-item">
|
|
1460
|
+
<div class="timeline-time">${escapeHtml(formatTimestamp(row.event_timestamp, 'Unknown'))}</div>
|
|
1461
|
+
<div>
|
|
1462
|
+
<div class="timeline-title">${escapeHtml(sourceLabel(row))} · ${escapeHtml(short(row.model))}</div>
|
|
1463
|
+
<div class="timeline-meta">${escapeHtml(number.format(row.total_tokens || 0))} tokens · ${escapeHtml(money(row.estimated_cost_usd))} · ${escapeHtml(usageCreditValue(row) === null ? 'no credit rate' : `${credits(usageCreditValue(row))} credits`)} · cache ${escapeHtml(pct(row.cache_ratio))}</div>
|
|
1464
|
+
<div class="timeline-meta">${escapeHtml(recommendationSummary(row))}</div>
|
|
1465
|
+
<div class="signal-strip">
|
|
1466
|
+
<span class="flag">context ${escapeHtml(pct(contextUse))}</span>
|
|
1467
|
+
<span class="flag">${escapeHtml(pricingStatusText(row))}</span>
|
|
1468
|
+
</div>
|
|
1469
|
+
<div class="mini-bar" title="Context use ${escapeHtml(pct(contextUse))}"><span class="${timelineSeverity(contextUse)}" style="width: ${timelineWidth(contextUse)}"></span></div>
|
|
1470
|
+
</div>
|
|
1471
|
+
</div>
|
|
1472
|
+
`;
|
|
1473
|
+
}).join('')}</div>`;
|
|
1474
|
+
}
|
|
1475
|
+
function selectRow(row) {
|
|
1476
|
+
selectedRecordId = row.record_id || '';
|
|
1477
|
+
selectedThreadKey = '';
|
|
1478
|
+
showDetail(row);
|
|
1479
|
+
syncUrlState();
|
|
1480
|
+
}
|
|
1481
|
+
function selectThread(group) {
|
|
1482
|
+
selectedThreadKey = group.key || '';
|
|
1483
|
+
selectedRecordId = '';
|
|
1484
|
+
showThreadDetail(group);
|
|
1485
|
+
syncUrlState();
|
|
1486
|
+
}
|
|
1487
|
+
function showDetail(row) {
|
|
1488
|
+
const attachment = rowAttachment(row);
|
|
1489
|
+
const flags = Array.isArray(row.efficiency_flags) && row.efficiency_flags.length ? row.efficiency_flags.join(', ') : 'None';
|
|
1490
|
+
const whyFlagged = Array.isArray(row.flag_explanations) && row.flag_explanations.length ? row.flag_explanations.join(' ') : recommendationSummary(row);
|
|
1491
|
+
detailEl.innerHTML = `
|
|
1492
|
+
<div class="detail-stack">
|
|
1493
|
+
<div class="detail-card primary">
|
|
1494
|
+
<h3>Cost, usage, and context</h3>
|
|
1495
|
+
${fieldsList([
|
|
1496
|
+
['Estimated cost', money(row.estimated_cost_usd)],
|
|
1497
|
+
['Codex credits', usageCreditsWithStatus(row)],
|
|
1498
|
+
['Allowance impact', rowAllowanceImpact(row)],
|
|
1499
|
+
['Cache ratio', pct(row.cache_ratio)],
|
|
1500
|
+
['Uncached input', number.format(row.uncached_input_tokens || 0)],
|
|
1501
|
+
['Context use', pct(row.context_window_percent)],
|
|
1502
|
+
['Pricing status', pricingStatusText(row)],
|
|
1503
|
+
['Next action', nextActionForRow(row)],
|
|
1504
|
+
['Why flagged', whyFlagged],
|
|
1505
|
+
])}
|
|
1506
|
+
</div>
|
|
1507
|
+
<div class="detail-card">
|
|
1508
|
+
<h3>Thread narrative</h3>
|
|
1509
|
+
${fieldsList([
|
|
1510
|
+
['Thread', attachment.label],
|
|
1511
|
+
['Project', row.project_name || 'Unknown project'],
|
|
1512
|
+
['Project tags', Array.isArray(row.project_tags) && row.project_tags.length ? row.project_tags.join(', ') : 'None'],
|
|
1513
|
+
['Thread attachment', attachment.relation],
|
|
1514
|
+
['Source', sourceLabel(row)],
|
|
1515
|
+
['Parent thread', resolvedParentThreadName(row) || 'None'],
|
|
1516
|
+
['Timestamp', formatTimestamp(row.event_timestamp)],
|
|
1517
|
+
])}
|
|
1518
|
+
</div>
|
|
1519
|
+
<div class="detail-card">
|
|
1520
|
+
<h3>Token and pricing breakdown</h3>
|
|
1521
|
+
${fieldsList([
|
|
1522
|
+
['Last call total', number.format(row.total_tokens || 0)],
|
|
1523
|
+
['Last call input', number.format(row.input_tokens || 0)],
|
|
1524
|
+
['Cached input', number.format(row.cached_input_tokens || 0)],
|
|
1525
|
+
['Output', number.format(row.output_tokens || 0)],
|
|
1526
|
+
['Reasoning output', number.format(row.reasoning_output_tokens || 0)],
|
|
1527
|
+
['Session cumulative', number.format(row.cumulative_total_tokens || 0)],
|
|
1528
|
+
['Pricing model', row.pricing_model || 'No configured price'],
|
|
1529
|
+
['Credit model', row.usage_credit_model || 'No mapped rate'],
|
|
1530
|
+
['Credit confidence', usageCreditStatusText(row)],
|
|
1531
|
+
['Credit source', row.usage_credit_source || 'None'],
|
|
1532
|
+
['Credit source fetched', row.usage_credit_fetched_at || 'Unknown'],
|
|
1533
|
+
['Credit tier', row.usage_credit_tier || 'Unknown'],
|
|
1534
|
+
['Cache savings', money(row.estimated_cache_savings_usd)],
|
|
1535
|
+
['Efficiency signals', flags],
|
|
1536
|
+
])}
|
|
1537
|
+
</div>
|
|
1538
|
+
${detailCollapse('Raw aggregate identifiers', [
|
|
1539
|
+
['Session', row.session_id],
|
|
1540
|
+
['Turn', row.turn_id],
|
|
1541
|
+
['Thread source', row.thread_source || 'user'],
|
|
1542
|
+
['Subagent type', row.subagent_type || 'None'],
|
|
1543
|
+
['Agent role', row.agent_role || 'None'],
|
|
1544
|
+
['Agent nickname', row.agent_nickname || 'None'],
|
|
1545
|
+
['Credit note', row.usage_credit_note || 'None'],
|
|
1546
|
+
['Parent session', row.parent_session_id || 'None'],
|
|
1547
|
+
['Parent updated', resolvedParentSessionUpdatedAt(row) ? formatTimestamp(resolvedParentSessionUpdatedAt(row)) : 'None'],
|
|
1548
|
+
['Cwd', row.cwd],
|
|
1549
|
+
['Project cwd', row.project_relative_cwd || '.'],
|
|
1550
|
+
['Git branch', row.git_branch || 'Unknown'],
|
|
1551
|
+
['Remote label', row.git_remote_label || 'None'],
|
|
1552
|
+
['Remote hash', row.git_remote_hash || 'None'],
|
|
1553
|
+
])}
|
|
1554
|
+
${detailCollapse('Source file and line', [
|
|
1555
|
+
['Source line', `${row.source_file}:${row.line_number}`],
|
|
1556
|
+
['Context window', number.format(row.model_context_window || 0)],
|
|
1557
|
+
])}
|
|
1558
|
+
${contextControls(row)}
|
|
1559
|
+
</div>
|
|
1560
|
+
`;
|
|
1561
|
+
bindContextButtons(row);
|
|
1562
|
+
}
|
|
1563
|
+
function showThreadDetail(group) {
|
|
1564
|
+
const lifecycle = group.lifecycle || {};
|
|
1565
|
+
detailEl.innerHTML = `
|
|
1566
|
+
<div class="detail-stack">
|
|
1567
|
+
<div class="detail-card primary">
|
|
1568
|
+
<h3>Thread attention summary</h3>
|
|
1569
|
+
${fieldsList([
|
|
1570
|
+
['Estimated cost', pricingConfigured ? money(group.estimatedCost) : 'Not configured'],
|
|
1571
|
+
['Codex credits', `${credits(group.usageCredits)} credits · ${group.creditStatus}`],
|
|
1572
|
+
['Allowance impact', allowanceWindowText(group.usageCredits, 'impact') || allowanceWindowText(group.usageCredits, 'remaining') || `${credits(group.usageCredits)} credits counted toward Codex usage limits`],
|
|
1573
|
+
['Attention score', number.format(Math.round(group.attentionScore))],
|
|
1574
|
+
['Cache ratio', pct(group.cacheRatio)],
|
|
1575
|
+
['Max context use', pct(group.maxContextUse)],
|
|
1576
|
+
['Pricing status', group.pricingStatus],
|
|
1577
|
+
['Next action', lifecycle.action || (group.maxContextUse >= threshold('high_context_percent', 0.6) || group.cacheRatio < threshold('low_cache_ratio', 0.3) ? 'Inspect the timeline before continuing this thread.' : 'Expand calls or select a row for call-level details.')],
|
|
1578
|
+
])}
|
|
1579
|
+
</div>
|
|
1580
|
+
<div class="detail-card">
|
|
1581
|
+
<h3>Thread lifecycle</h3>
|
|
1582
|
+
${fieldsList([
|
|
1583
|
+
['First expensive turn', lifecycle.firstExpensiveRow ? `${formatTimestamp(lifecycle.firstExpensiveRow.event_timestamp)} · call ${number.format((lifecycle.firstExpensiveIndex || 0) + 1)}` : 'None above thresholds'],
|
|
1584
|
+
['Largest cumulative jump', lifecycle.largestJumpRow ? `${number.format(lifecycle.largestJump)} tokens at ${formatTimestamp(lifecycle.largestJumpRow.event_timestamp)}` : 'None'],
|
|
1585
|
+
['Cache trend', `${lifecycle.cacheTrend >= 0 ? '+' : ''}${pct(lifecycle.cacheTrend || 0)}`],
|
|
1586
|
+
['Context trend', `${lifecycle.contextTrend >= 0 ? '+' : ''}${pct(lifecycle.contextTrend || 0)}`],
|
|
1587
|
+
['Subagent before spike', lifecycle.subagentBeforeSpike ? 'Yes' : 'No'],
|
|
1588
|
+
])}
|
|
1589
|
+
</div>
|
|
1590
|
+
<div class="detail-card">
|
|
1591
|
+
<h3>Thread timeline</h3>
|
|
1592
|
+
${renderThreadTimeline(group)}
|
|
1593
|
+
</div>
|
|
1594
|
+
<div class="detail-card">
|
|
1595
|
+
<h3>Relationships</h3>
|
|
1596
|
+
${fieldsList([
|
|
1597
|
+
['Thread', group.label],
|
|
1598
|
+
['Calls', number.format(group.callCount)],
|
|
1599
|
+
['Subagent calls', number.format(group.subagentCount)],
|
|
1600
|
+
['Auto-review calls', number.format(group.autoReviewCount)],
|
|
1601
|
+
['Attached calls', number.format(group.attachedCount)],
|
|
1602
|
+
['Spawned from', group.parentThreadLabel || 'None'],
|
|
1603
|
+
['Spawned threads', number.format(group.childThreadCount || 0)],
|
|
1604
|
+
['Spawned child calls', number.format(group.childCallCount || 0)],
|
|
1605
|
+
])}
|
|
1606
|
+
</div>
|
|
1607
|
+
${detailCollapse('Secondary thread fields', [
|
|
1608
|
+
['Latest activity', formatTimestamp(group.latestActivity)],
|
|
1609
|
+
['Total tokens', number.format(group.totalTokens)],
|
|
1610
|
+
['Efficiency signals', number.format(group.signalCount)],
|
|
1611
|
+
['Model mix', group.modelSummary],
|
|
1612
|
+
['Reasoning mix', group.effortSummary],
|
|
1613
|
+
])}
|
|
1614
|
+
</div>
|
|
1615
|
+
`;
|
|
1616
|
+
}
|
|
1617
|
+
function setView(view) {
|
|
1618
|
+
activeView = view;
|
|
1619
|
+
currentPage = 1;
|
|
1620
|
+
render();
|
|
1621
|
+
}
|
|
1622
|
+
function updateLiveStatus(label, detail = '') {
|
|
1623
|
+
liveStatusEl.textContent = label;
|
|
1624
|
+
liveStatusEl.title = detail || label;
|
|
1625
|
+
liveStatusEl.dataset.state = label.toLowerCase().includes('error') ? 'error' : 'ready';
|
|
1626
|
+
}
|
|
1627
|
+
function updateToTopVisibility() {
|
|
1628
|
+
toTopEl.dataset.visible = window.scrollY > 320 ? 'true' : 'false';
|
|
1629
|
+
}
|
|
1630
|
+
function applyDashboardPayload(nextPayload) {
|
|
1631
|
+
data = payloadRows(nextPayload);
|
|
1632
|
+
pricingConfigured = Boolean(nextPayload.pricing_configured);
|
|
1633
|
+
pricingSource = nextPayload.pricing_source || {};
|
|
1634
|
+
pricingSnapshotWarning = nextPayload.pricing_snapshot_warning || '';
|
|
1635
|
+
allowanceConfigured = Boolean(nextPayload.allowance_configured);
|
|
1636
|
+
allowanceSource = nextPayload.allowance_source || {};
|
|
1637
|
+
allowanceWindows = Array.isArray(nextPayload.allowance_windows) ? nextPayload.allowance_windows : [];
|
|
1638
|
+
allowanceError = nextPayload.allowance_error || '';
|
|
1639
|
+
rateCardError = nextPayload.rate_card_error || '';
|
|
1640
|
+
parserDiagnostics = nextPayload.parser_diagnostics || {};
|
|
1641
|
+
projectMetadataPrivacy = nextPayload.project_metadata_privacy || { mode: nextPayload.privacy_mode || 'normal' };
|
|
1642
|
+
apiToken = nextPayload.api_token || apiToken;
|
|
1643
|
+
contextApiEnabled = Boolean(nextPayload.context_api_enabled);
|
|
1644
|
+
actionThresholds = nextPayload.action_thresholds || actionThresholds;
|
|
1645
|
+
totalAvailableRows = Number(nextPayload.total_available_rows || data.length);
|
|
1646
|
+
activeAvailableRows = Number(nextPayload.active_available_rows || data.length);
|
|
1647
|
+
allHistoryAvailableRows = Number(nextPayload.all_history_available_rows || totalAvailableRows);
|
|
1648
|
+
archivedAvailableRows = Number(nextPayload.archived_available_rows || Math.max(allHistoryAvailableRows - activeAvailableRows, 0));
|
|
1649
|
+
includeArchived = Boolean(nextPayload.include_archived);
|
|
1650
|
+
loadedLimit = payloadLimit(nextPayload);
|
|
1651
|
+
rebuildDashboardIndexes();
|
|
1652
|
+
rebuildFilterOptions();
|
|
1653
|
+
updatePricingSourceLine();
|
|
1654
|
+
updateAllowanceSourceLine();
|
|
1655
|
+
updatePrivacyModeLine();
|
|
1656
|
+
updateParserDiagnosticsLine();
|
|
1657
|
+
updateLoadLimitControl();
|
|
1658
|
+
updateHistoryScopeControl();
|
|
1659
|
+
render();
|
|
1660
|
+
}
|
|
1661
|
+
async function refreshDashboardData(manual = false) {
|
|
1662
|
+
if (!liveRefreshSupported) {
|
|
1663
|
+
updateLiveStatus('Reloading', 'Reloading static dashboard snapshot...');
|
|
1664
|
+
window.location.reload();
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (refreshInFlight) return;
|
|
1668
|
+
refreshInFlight = true;
|
|
1669
|
+
refreshDashboardEl.disabled = true;
|
|
1670
|
+
updateLiveStatus(manual ? 'Refreshing' : 'Checking', manual ? 'Refreshing local usage index...' : 'Checking for new usage...');
|
|
1671
|
+
try {
|
|
1672
|
+
const params = new URLSearchParams({
|
|
1673
|
+
refresh: '1',
|
|
1674
|
+
limit: loadLimitEl.value,
|
|
1675
|
+
include_archived: includeArchived ? '1' : '0',
|
|
1676
|
+
_: String(Date.now()),
|
|
1677
|
+
});
|
|
1678
|
+
const response = await fetch(`/api/usage?${params.toString()}`, {
|
|
1679
|
+
headers: {
|
|
1680
|
+
'Accept': 'application/json',
|
|
1681
|
+
'X-Codex-Usage-Token': apiToken,
|
|
1682
|
+
},
|
|
1683
|
+
cache: 'no-store',
|
|
1684
|
+
});
|
|
1685
|
+
if (!response.ok) {
|
|
1686
|
+
throw new Error(`HTTP ${response.status}`);
|
|
1687
|
+
}
|
|
1688
|
+
const nextPayload = await response.json();
|
|
1689
|
+
if (nextPayload.error) throw new Error(nextPayload.error);
|
|
1690
|
+
applyDashboardPayload(nextPayload);
|
|
1691
|
+
const result = nextPayload.refresh_result || {};
|
|
1692
|
+
const indexed = result.inserted_or_updated_events === undefined
|
|
1693
|
+
? ''
|
|
1694
|
+
: ` Indexed ${number.format(result.inserted_or_updated_events)} aggregate rows from ${number.format(result.scanned_files || 0)} logs.`;
|
|
1695
|
+
const skipped = result.skipped_events
|
|
1696
|
+
? ` Skipped ${number.format(result.skipped_events)} malformed token-count events.`
|
|
1697
|
+
: '';
|
|
1698
|
+
updateLiveStatus(autoRefreshEl.checked ? 'Live' : 'Updated', `Updated ${formatTimestamp(nextPayload.refreshed_at)}. ${loadedRowsDescription()}. ${historyRowsDescription()}.${indexed}${skipped}`);
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
const message = error.message || String(error);
|
|
1701
|
+
updateLiveStatus('Refresh error', `Live refresh unavailable: ${message}${manual ? '. Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.' : ''}`);
|
|
1702
|
+
if (manual && message === 'HTTP 404') window.location.reload();
|
|
1703
|
+
} finally {
|
|
1704
|
+
refreshInFlight = false;
|
|
1705
|
+
refreshDashboardEl.disabled = false;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
function scheduleAutoRefresh() {
|
|
1709
|
+
if (autoRefreshTimer) window.clearInterval(autoRefreshTimer);
|
|
1710
|
+
autoRefreshTimer = null;
|
|
1711
|
+
if (!autoRefreshEl.checked || !liveRefreshSupported) return;
|
|
1712
|
+
autoRefreshTimer = window.setInterval(() => {
|
|
1713
|
+
if (document.visibilityState === 'visible') refreshDashboardData(false);
|
|
1714
|
+
}, liveRefreshIntervalMs);
|
|
1715
|
+
}
|
|
1716
|
+
insightsViewEl.addEventListener('click', () => setView('insights'));
|
|
1717
|
+
callsViewEl.addEventListener('click', () => setView('calls'));
|
|
1718
|
+
threadsViewEl.addEventListener('click', () => setView('threads'));
|
|
1719
|
+
clearPresetEl.addEventListener('click', clearPreset);
|
|
1720
|
+
copyViewLinkEl.addEventListener('click', copyCurrentViewLink);
|
|
1721
|
+
exportVisibleEl.addEventListener('click', exportCurrentRows);
|
|
1722
|
+
refreshDashboardEl.addEventListener('click', () => refreshDashboardData(true));
|
|
1723
|
+
loadLimitEl.addEventListener('change', () => {
|
|
1724
|
+
currentPage = 1;
|
|
1725
|
+
if (liveRefreshSupported) {
|
|
1726
|
+
refreshDashboardData(true);
|
|
1727
|
+
} else {
|
|
1728
|
+
updateLiveStatus('Static', 'Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.');
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
historyScopeEl.addEventListener('change', () => {
|
|
1732
|
+
includeArchived = historyScopeEl.value === 'all';
|
|
1733
|
+
currentPage = 1;
|
|
1734
|
+
updateHistoryScopeControl();
|
|
1735
|
+
syncUrlState();
|
|
1736
|
+
if (liveRefreshSupported) {
|
|
1737
|
+
refreshDashboardData(true);
|
|
1738
|
+
} else {
|
|
1739
|
+
updateLiveStatus('Static', 'Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.');
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
autoRefreshEl.addEventListener('change', () => {
|
|
1743
|
+
scheduleAutoRefresh();
|
|
1744
|
+
updateLiveStatus(autoRefreshEl.checked ? 'Live' : 'Paused', `${autoRefreshEl.checked ? `Live refresh every ${liveRefreshIntervalMs / 1000}s` : 'Live refresh paused'}. ${loadedRowsDescription()}. ${historyRowsDescription()}`);
|
|
1745
|
+
if (autoRefreshEl.checked) refreshDashboardData(false);
|
|
1746
|
+
});
|
|
1747
|
+
document.addEventListener('visibilitychange', () => {
|
|
1748
|
+
if (document.visibilityState === 'visible' && autoRefreshEl.checked) refreshDashboardData(false);
|
|
1749
|
+
});
|
|
1750
|
+
document.addEventListener('keydown', event => {
|
|
1751
|
+
const target = event.target;
|
|
1752
|
+
const inEditable = target && target.closest && target.closest('input, select, textarea, button, [contenteditable="true"]');
|
|
1753
|
+
if (inEditable) return;
|
|
1754
|
+
if (event.key === '/') {
|
|
1755
|
+
event.preventDefault();
|
|
1756
|
+
searchEl.focus();
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (event.key === '1') setView('insights');
|
|
1760
|
+
if (event.key === '2') setView('calls');
|
|
1761
|
+
if (event.key === '3') setView('threads');
|
|
1762
|
+
});
|
|
1763
|
+
window.addEventListener('scroll', updateToTopVisibility, { passive: true });
|
|
1764
|
+
toTopEl.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
|
1765
|
+
prevPageEl.addEventListener('click', () => {
|
|
1766
|
+
currentPage = Math.max(1, currentPage - 1);
|
|
1767
|
+
render();
|
|
1768
|
+
});
|
|
1769
|
+
nextPageEl.addEventListener('click', () => {
|
|
1770
|
+
currentPage += 1;
|
|
1771
|
+
render();
|
|
1772
|
+
});
|
|
1773
|
+
document.querySelectorAll('[data-sort-key]').forEach(button => {
|
|
1774
|
+
button.addEventListener('click', () => handleHeaderSort(button.dataset.sortKey));
|
|
1775
|
+
});
|
|
1776
|
+
rowsEl.addEventListener('mouseover', event => {
|
|
1777
|
+
const callRow = event.target.closest('.thread-call-row');
|
|
1778
|
+
if (!callRow || !rowsEl.contains(callRow)) return;
|
|
1779
|
+
const row = rowByRecordId.get(callRow.dataset.recordId);
|
|
1780
|
+
if (row) selectRow(row);
|
|
1781
|
+
});
|
|
1782
|
+
rowsEl.addEventListener('click', event => {
|
|
1783
|
+
const callRow = event.target.closest('.thread-call-row');
|
|
1784
|
+
if (!callRow || !rowsEl.contains(callRow)) return;
|
|
1785
|
+
const row = rowByRecordId.get(callRow.dataset.recordId);
|
|
1786
|
+
if (row) showDetail(row);
|
|
1787
|
+
});
|
|
1788
|
+
rowsEl.addEventListener('keydown', event => {
|
|
1789
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
1790
|
+
const callRow = event.target.closest('.thread-call-row');
|
|
1791
|
+
if (!callRow || !rowsEl.contains(callRow)) return;
|
|
1792
|
+
event.preventDefault();
|
|
1793
|
+
const row = rowByRecordId.get(callRow.dataset.recordId);
|
|
1794
|
+
if (row) selectRow(row);
|
|
1795
|
+
});
|
|
1796
|
+
datePresetEl.addEventListener('input', () => {
|
|
1797
|
+
syncDatePresetInputs();
|
|
1798
|
+
currentPage = 1;
|
|
1799
|
+
render();
|
|
1800
|
+
});
|
|
1801
|
+
[dateStartEl, dateEndEl].forEach(el => el.addEventListener('input', () => {
|
|
1802
|
+
if (datePresetEl.value !== 'custom') datePresetEl.value = 'custom';
|
|
1803
|
+
el.value = cleanDateInput(el.value) || el.value;
|
|
1804
|
+
currentPage = 1;
|
|
1805
|
+
render();
|
|
1806
|
+
}));
|
|
1807
|
+
[searchEl, modelEl, effortEl, pricingStatusEl].forEach(el => el.addEventListener('input', () => {
|
|
1808
|
+
currentPage = 1;
|
|
1809
|
+
render();
|
|
1810
|
+
}));
|
|
1811
|
+
sortEl.addEventListener('input', () => setSort(sortEl.value, defaultSortDirection(sortEl.value)));
|
|
1812
|
+
rebuildDashboardIndexes();
|
|
1813
|
+
rebuildFilterOptions();
|
|
1814
|
+
applyInitialState();
|
|
1815
|
+
updatePricingSourceLine();
|
|
1816
|
+
updateAllowanceSourceLine();
|
|
1817
|
+
updatePrivacyModeLine();
|
|
1818
|
+
updateParserDiagnosticsLine();
|
|
1819
|
+
updateLoadLimitControl();
|
|
1820
|
+
updateHistoryScopeControl();
|
|
1821
|
+
if (!liveRefreshSupported) {
|
|
1822
|
+
autoRefreshEl.checked = false;
|
|
1823
|
+
autoRefreshEl.disabled = true;
|
|
1824
|
+
loadLimitEl.disabled = true;
|
|
1825
|
+
historyScopeEl.disabled = true;
|
|
1826
|
+
updateLiveStatus('Static', `Static snapshot. ${loadedRowsDescription()}. ${historyRowsDescription()}`);
|
|
1827
|
+
} else {
|
|
1828
|
+
updateLiveStatus('Live', `Live refresh every ${liveRefreshIntervalMs / 1000}s. ${loadedRowsDescription()}. ${historyRowsDescription()}`);
|
|
1829
|
+
scheduleAutoRefresh();
|
|
1830
|
+
if (needsInitialHistoryRefresh) refreshDashboardData(false);
|
|
1831
|
+
}
|
|
1832
|
+
updateToTopVisibility();
|
|
1833
|
+
render();
|