codex-autorunner 0.1.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.
Files changed (147) hide show
  1. codex_autorunner/__init__.py +3 -0
  2. codex_autorunner/bootstrap.py +151 -0
  3. codex_autorunner/cli.py +886 -0
  4. codex_autorunner/codex_cli.py +79 -0
  5. codex_autorunner/codex_runner.py +17 -0
  6. codex_autorunner/core/__init__.py +1 -0
  7. codex_autorunner/core/about_car.py +125 -0
  8. codex_autorunner/core/codex_runner.py +100 -0
  9. codex_autorunner/core/config.py +1465 -0
  10. codex_autorunner/core/doc_chat.py +547 -0
  11. codex_autorunner/core/docs.py +37 -0
  12. codex_autorunner/core/engine.py +720 -0
  13. codex_autorunner/core/git_utils.py +206 -0
  14. codex_autorunner/core/hub.py +756 -0
  15. codex_autorunner/core/injected_context.py +9 -0
  16. codex_autorunner/core/locks.py +57 -0
  17. codex_autorunner/core/logging_utils.py +158 -0
  18. codex_autorunner/core/notifications.py +465 -0
  19. codex_autorunner/core/optional_dependencies.py +41 -0
  20. codex_autorunner/core/prompt.py +107 -0
  21. codex_autorunner/core/prompts.py +275 -0
  22. codex_autorunner/core/request_context.py +21 -0
  23. codex_autorunner/core/runner_controller.py +116 -0
  24. codex_autorunner/core/runner_process.py +29 -0
  25. codex_autorunner/core/snapshot.py +576 -0
  26. codex_autorunner/core/state.py +156 -0
  27. codex_autorunner/core/update.py +567 -0
  28. codex_autorunner/core/update_runner.py +44 -0
  29. codex_autorunner/core/usage.py +1221 -0
  30. codex_autorunner/core/utils.py +108 -0
  31. codex_autorunner/discovery.py +102 -0
  32. codex_autorunner/housekeeping.py +423 -0
  33. codex_autorunner/integrations/__init__.py +1 -0
  34. codex_autorunner/integrations/app_server/__init__.py +6 -0
  35. codex_autorunner/integrations/app_server/client.py +1386 -0
  36. codex_autorunner/integrations/app_server/supervisor.py +206 -0
  37. codex_autorunner/integrations/github/__init__.py +10 -0
  38. codex_autorunner/integrations/github/service.py +889 -0
  39. codex_autorunner/integrations/telegram/__init__.py +1 -0
  40. codex_autorunner/integrations/telegram/adapter.py +1401 -0
  41. codex_autorunner/integrations/telegram/commands_registry.py +104 -0
  42. codex_autorunner/integrations/telegram/config.py +450 -0
  43. codex_autorunner/integrations/telegram/constants.py +154 -0
  44. codex_autorunner/integrations/telegram/dispatch.py +162 -0
  45. codex_autorunner/integrations/telegram/handlers/__init__.py +0 -0
  46. codex_autorunner/integrations/telegram/handlers/approvals.py +241 -0
  47. codex_autorunner/integrations/telegram/handlers/callbacks.py +72 -0
  48. codex_autorunner/integrations/telegram/handlers/commands.py +160 -0
  49. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +5262 -0
  50. codex_autorunner/integrations/telegram/handlers/messages.py +477 -0
  51. codex_autorunner/integrations/telegram/handlers/selections.py +545 -0
  52. codex_autorunner/integrations/telegram/helpers.py +2084 -0
  53. codex_autorunner/integrations/telegram/notifications.py +164 -0
  54. codex_autorunner/integrations/telegram/outbox.py +174 -0
  55. codex_autorunner/integrations/telegram/rendering.py +102 -0
  56. codex_autorunner/integrations/telegram/retry.py +37 -0
  57. codex_autorunner/integrations/telegram/runtime.py +270 -0
  58. codex_autorunner/integrations/telegram/service.py +921 -0
  59. codex_autorunner/integrations/telegram/state.py +1223 -0
  60. codex_autorunner/integrations/telegram/transport.py +318 -0
  61. codex_autorunner/integrations/telegram/types.py +57 -0
  62. codex_autorunner/integrations/telegram/voice.py +413 -0
  63. codex_autorunner/manifest.py +150 -0
  64. codex_autorunner/routes/__init__.py +53 -0
  65. codex_autorunner/routes/base.py +470 -0
  66. codex_autorunner/routes/docs.py +275 -0
  67. codex_autorunner/routes/github.py +197 -0
  68. codex_autorunner/routes/repos.py +121 -0
  69. codex_autorunner/routes/sessions.py +137 -0
  70. codex_autorunner/routes/shared.py +137 -0
  71. codex_autorunner/routes/system.py +175 -0
  72. codex_autorunner/routes/terminal_images.py +107 -0
  73. codex_autorunner/routes/voice.py +128 -0
  74. codex_autorunner/server.py +23 -0
  75. codex_autorunner/spec_ingest.py +113 -0
  76. codex_autorunner/static/app.js +95 -0
  77. codex_autorunner/static/autoRefresh.js +209 -0
  78. codex_autorunner/static/bootstrap.js +105 -0
  79. codex_autorunner/static/bus.js +23 -0
  80. codex_autorunner/static/cache.js +52 -0
  81. codex_autorunner/static/constants.js +48 -0
  82. codex_autorunner/static/dashboard.js +795 -0
  83. codex_autorunner/static/docs.js +1514 -0
  84. codex_autorunner/static/env.js +99 -0
  85. codex_autorunner/static/github.js +168 -0
  86. codex_autorunner/static/hub.js +1511 -0
  87. codex_autorunner/static/index.html +622 -0
  88. codex_autorunner/static/loader.js +28 -0
  89. codex_autorunner/static/logs.js +690 -0
  90. codex_autorunner/static/mobileCompact.js +300 -0
  91. codex_autorunner/static/snapshot.js +116 -0
  92. codex_autorunner/static/state.js +87 -0
  93. codex_autorunner/static/styles.css +4966 -0
  94. codex_autorunner/static/tabs.js +50 -0
  95. codex_autorunner/static/terminal.js +21 -0
  96. codex_autorunner/static/terminalManager.js +3535 -0
  97. codex_autorunner/static/todoPreview.js +25 -0
  98. codex_autorunner/static/types.d.ts +8 -0
  99. codex_autorunner/static/utils.js +597 -0
  100. codex_autorunner/static/vendor/LICENSE.xterm +24 -0
  101. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic-ext.woff2 +0 -0
  102. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic.woff2 +0 -0
  103. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-greek.woff2 +0 -0
  104. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin-ext.woff2 +0 -0
  105. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin.woff2 +0 -0
  106. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-vietnamese.woff2 +0 -0
  107. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic-ext.woff2 +0 -0
  108. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic.woff2 +0 -0
  109. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-greek.woff2 +0 -0
  110. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin-ext.woff2 +0 -0
  111. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin.woff2 +0 -0
  112. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-vietnamese.woff2 +0 -0
  113. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic-ext.woff2 +0 -0
  114. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic.woff2 +0 -0
  115. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-greek.woff2 +0 -0
  116. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin-ext.woff2 +0 -0
  117. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin.woff2 +0 -0
  118. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-vietnamese.woff2 +0 -0
  119. codex_autorunner/static/vendor/fonts/jetbrains-mono/OFL.txt +93 -0
  120. codex_autorunner/static/vendor/xterm-addon-fit.js +2 -0
  121. codex_autorunner/static/vendor/xterm.css +209 -0
  122. codex_autorunner/static/vendor/xterm.js +2 -0
  123. codex_autorunner/static/voice.js +591 -0
  124. codex_autorunner/voice/__init__.py +39 -0
  125. codex_autorunner/voice/capture.py +349 -0
  126. codex_autorunner/voice/config.py +167 -0
  127. codex_autorunner/voice/provider.py +66 -0
  128. codex_autorunner/voice/providers/__init__.py +7 -0
  129. codex_autorunner/voice/providers/openai_whisper.py +345 -0
  130. codex_autorunner/voice/resolver.py +36 -0
  131. codex_autorunner/voice/service.py +210 -0
  132. codex_autorunner/web/__init__.py +1 -0
  133. codex_autorunner/web/app.py +1037 -0
  134. codex_autorunner/web/hub_jobs.py +181 -0
  135. codex_autorunner/web/middleware.py +552 -0
  136. codex_autorunner/web/pty_session.py +357 -0
  137. codex_autorunner/web/runner_manager.py +25 -0
  138. codex_autorunner/web/schemas.py +253 -0
  139. codex_autorunner/web/static_assets.py +430 -0
  140. codex_autorunner/web/terminal_sessions.py +78 -0
  141. codex_autorunner/workspace.py +16 -0
  142. codex_autorunner-0.1.0.dist-info/METADATA +240 -0
  143. codex_autorunner-0.1.0.dist-info/RECORD +147 -0
  144. codex_autorunner-0.1.0.dist-info/WHEEL +5 -0
  145. codex_autorunner-0.1.0.dist-info/entry_points.txt +3 -0
  146. codex_autorunner-0.1.0.dist-info/licenses/LICENSE +21 -0
  147. codex_autorunner-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,795 @@
1
+ import { api, flash, statusPill, confirmModal, openModal } from "./utils.js";
2
+ import { subscribe } from "./bus.js";
3
+ import { saveToCache, loadFromCache } from "./cache.js";
4
+ import { renderTodoPreview } from "./todoPreview.js";
5
+ import {
6
+ loadState,
7
+ startRun,
8
+ stopRun,
9
+ resumeRun,
10
+ killRun,
11
+ resetRunner,
12
+ startStatePolling,
13
+ } from "./state.js";
14
+ import { registerAutoRefresh } from "./autoRefresh.js";
15
+ import { CONSTANTS } from "./constants.js";
16
+
17
+ const UPDATE_STATUS_SEEN_KEY = "car_update_status_seen";
18
+ let pendingSummaryOpen = false;
19
+ const usageChartState = {
20
+ segment: "none",
21
+ bucket: "day",
22
+ windowDays: 30,
23
+ };
24
+ let usageSeriesRetryTimer = null;
25
+ let usageSummaryRetryTimer = null;
26
+
27
+ function renderState(state) {
28
+ if (!state) return;
29
+ saveToCache("state", state);
30
+ statusPill(document.getElementById("runner-status"), state.status);
31
+ document.getElementById("last-run-id").textContent = state.last_run_id ?? "–";
32
+ document.getElementById("last-exit-code").textContent =
33
+ state.last_exit_code ?? "–";
34
+ document.getElementById("last-start").textContent =
35
+ state.last_run_started_at ?? "–";
36
+ document.getElementById("last-finish").textContent =
37
+ state.last_run_finished_at ?? "–";
38
+ document.getElementById("todo-count").textContent =
39
+ state.outstanding_count ?? "–";
40
+ document.getElementById("done-count").textContent = state.done_count ?? "–";
41
+ document.getElementById("runner-pid").textContent = `Runner pid: ${
42
+ state.runner_pid ?? "–"
43
+ }`;
44
+ const modelEl = document.getElementById("runner-model");
45
+ if (modelEl) modelEl.textContent = state.codex_model || "auto";
46
+
47
+ // Show "Summary" CTA when TODO is fully complete.
48
+ const summaryBtn = document.getElementById("open-summary");
49
+ if (summaryBtn) {
50
+ const done = Number(state.outstanding_count ?? NaN) === 0;
51
+ summaryBtn.classList.toggle("hidden", !done);
52
+ }
53
+ }
54
+
55
+ function updateTodoPreview(content) {
56
+ renderTodoPreview(content || "");
57
+ if (content !== undefined) {
58
+ saveToCache("todo-doc", content || "");
59
+ }
60
+ }
61
+
62
+ function handleDocsEvent(payload) {
63
+ if (!payload) return;
64
+ if (payload.kind === "todo") {
65
+ updateTodoPreview(payload.content || "");
66
+ return;
67
+ }
68
+ if (typeof payload.todo === "string") {
69
+ updateTodoPreview(payload.todo);
70
+ }
71
+ }
72
+
73
+ async function loadTodoPreview() {
74
+ try {
75
+ const data = await api("/api/docs");
76
+ updateTodoPreview(data?.todo || "");
77
+ } catch (err) {
78
+ flash(err.message || "Failed to load TODO preview", "error");
79
+ }
80
+ }
81
+
82
+ function setUsageLoading(loading) {
83
+ const btn = document.getElementById("usage-refresh");
84
+ if (!btn) return;
85
+ btn.disabled = loading;
86
+ btn.classList.toggle("loading", loading);
87
+ }
88
+
89
+ function formatTokensCompact(val) {
90
+ if (val === null || val === undefined) return "–";
91
+ const num = Number(val);
92
+ if (Number.isNaN(num)) return val;
93
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
94
+ if (num >= 1000) return `${(num / 1000).toFixed(0)}k`;
95
+ return num.toLocaleString();
96
+ }
97
+
98
+ function formatTokensAxis(val) {
99
+ if (val === null || val === undefined) return "0";
100
+ const num = Number(val);
101
+ if (Number.isNaN(num)) return "0";
102
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
103
+ if (num >= 1000) return `${(num / 1000).toFixed(1)}k`;
104
+ return Math.round(num).toString();
105
+ }
106
+
107
+ function renderUsageProgressBar(container, percent, windowMinutes) {
108
+ if (!container) return;
109
+
110
+ const pct = typeof percent === "number" ? Math.min(100, Math.max(0, percent)) : 0;
111
+ const hasData = typeof percent === "number";
112
+
113
+ // Determine color based on percentage
114
+ let barClass = "usage-bar-ok";
115
+ if (pct >= 90) barClass = "usage-bar-critical";
116
+ else if (pct >= 70) barClass = "usage-bar-warning";
117
+
118
+ container.innerHTML = `
119
+ <div class="usage-progress-bar ${hasData ? "" : "usage-progress-bar-empty"}">
120
+ <div class="usage-progress-fill ${barClass}" style="width: ${pct}%"></div>
121
+ </div>
122
+ <span class="usage-progress-label">${hasData ? `${pct}%` : "–"}${windowMinutes ? `/${windowMinutes}m` : ""}</span>
123
+ `;
124
+ }
125
+
126
+ function renderUsage(data) {
127
+ if (data) saveToCache("usage", data);
128
+ const totals = data?.totals || {};
129
+ const events = data?.events ?? 0;
130
+ const rate = data?.latest_rate_limits;
131
+ const codexHome = data?.codex_home || "–";
132
+
133
+ const eventsEl = document.getElementById("usage-events");
134
+ if (eventsEl) {
135
+ eventsEl.textContent = `${events} ev`;
136
+ }
137
+ const totalEl = document.getElementById("usage-total");
138
+ const inputEl = document.getElementById("usage-input");
139
+ const cachedEl = document.getElementById("usage-cached");
140
+ const outputEl = document.getElementById("usage-output");
141
+ const reasoningEl = document.getElementById("usage-reasoning");
142
+ const ratesEl = document.getElementById("usage-rates");
143
+ const metaEl = document.getElementById("usage-meta");
144
+ const primaryBarEl = document.getElementById("usage-rate-primary");
145
+ const secondaryBarEl = document.getElementById("usage-rate-secondary");
146
+
147
+ if (totalEl) totalEl.textContent = formatTokensCompact(totals.total_tokens);
148
+ if (inputEl) inputEl.textContent = formatTokensCompact(totals.input_tokens);
149
+ if (cachedEl)
150
+ cachedEl.textContent = formatTokensCompact(totals.cached_input_tokens);
151
+ if (outputEl)
152
+ outputEl.textContent = formatTokensCompact(totals.output_tokens);
153
+ if (reasoningEl)
154
+ reasoningEl.textContent = formatTokensCompact(
155
+ totals.reasoning_output_tokens
156
+ );
157
+
158
+ // Render progress bars for rate limits
159
+ if (rate) {
160
+ const primary = rate.primary || {};
161
+ const secondary = rate.secondary || {};
162
+
163
+ renderUsageProgressBar(primaryBarEl, primary.used_percent, primary.window_minutes);
164
+ renderUsageProgressBar(secondaryBarEl, secondary.used_percent, secondary.window_minutes);
165
+
166
+ // Also update text fallback
167
+ if (ratesEl) {
168
+ ratesEl.textContent = `${primary.used_percent ?? "–"}%/${
169
+ primary.window_minutes ?? ""
170
+ }m · ${secondary.used_percent ?? "–"}%/${
171
+ secondary.window_minutes ?? ""
172
+ }m`;
173
+ }
174
+ } else {
175
+ renderUsageProgressBar(primaryBarEl, null, null);
176
+ renderUsageProgressBar(secondaryBarEl, null, null);
177
+ if (ratesEl) ratesEl.textContent = "–";
178
+ }
179
+
180
+ if (metaEl) metaEl.textContent = codexHome;
181
+ }
182
+
183
+ function buildUsageSeriesQuery() {
184
+ const params = new URLSearchParams();
185
+ const now = new Date();
186
+ const since = new Date(now.getTime() - usageChartState.windowDays * 86400000);
187
+ const bucket =
188
+ usageChartState.windowDays >= 180 ? "week" : usageChartState.bucket;
189
+ params.set("since", since.toISOString());
190
+ params.set("until", now.toISOString());
191
+ params.set("bucket", bucket);
192
+ params.set("segment", usageChartState.segment);
193
+ return params.toString();
194
+ }
195
+
196
+ function renderUsageChart(data) {
197
+ const container = document.getElementById("usage-chart-canvas");
198
+ if (!container) return;
199
+ const buckets = data?.buckets || [];
200
+ const series = data?.series || [];
201
+ const isLoading = data?.status === "loading";
202
+ if (!buckets.length || !series.length) {
203
+ container.__usageChartBound = false;
204
+ container.innerHTML = isLoading
205
+ ? '<div class="usage-chart-empty">Loading…</div>'
206
+ : '<div class="usage-chart-empty">No data</div>';
207
+ return;
208
+ }
209
+
210
+ const { width, height } = getChartSize(container, 320, 88);
211
+ const padding = 8;
212
+ const chartWidth = width - padding * 2;
213
+ const chartHeight = height - padding * 2;
214
+ const colors = [
215
+ "#6cf5d8",
216
+ "#6ca8ff",
217
+ "#f5b86c",
218
+ "#f56c8a",
219
+ "#84d1ff",
220
+ "#9be26f",
221
+ "#f2a0c5",
222
+ ];
223
+
224
+ const { series: displaySeries } = normalizeSeries(
225
+ limitSeries(series, 4, "rest").series,
226
+ buckets.length
227
+ );
228
+
229
+ let scaleMax = 1;
230
+ const totals = new Array(buckets.length).fill(0);
231
+ displaySeries.forEach((entry) => {
232
+ (entry.values || []).forEach((value, i) => {
233
+ totals[i] += value;
234
+ });
235
+ });
236
+ scaleMax = Math.max(...totals, 1);
237
+
238
+ let svg = `<svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMinYMin meet" role="img" aria-label="Token usage trend">`;
239
+ svg += `
240
+ <defs></defs>
241
+ `;
242
+
243
+ const gridLines = 3;
244
+ for (let i = 1; i <= gridLines; i += 1) {
245
+ const y = padding + (chartHeight / (gridLines + 1)) * i;
246
+ svg += `<line x1="${padding}" y1="${y}" x2="${
247
+ padding + chartWidth
248
+ }" y2="${y}" stroke="rgba(108, 245, 216, 0.12)" stroke-width="1" />`;
249
+ }
250
+
251
+ const maxLabel = formatTokensAxis(scaleMax);
252
+ const midLabel = formatTokensAxis(scaleMax / 2);
253
+ svg += `<text x="${padding}" y="${padding + 10}" fill="rgba(203, 213, 225, 0.7)" font-size="8">${maxLabel}</text>`;
254
+ svg += `<text x="${padding}" y="${
255
+ padding + chartHeight / 2 + 4
256
+ }" fill="rgba(203, 213, 225, 0.6)" font-size="8">${midLabel}</text>`;
257
+ svg += `<text x="${padding}" y="${
258
+ padding + chartHeight + 2
259
+ }" fill="rgba(203, 213, 225, 0.5)" font-size="8">0</text>`;
260
+
261
+ const count = buckets.length;
262
+ const barWidth = count ? chartWidth / count : chartWidth;
263
+ const gap = Math.max(1, Math.round(barWidth * 0.2));
264
+ const usableWidth = Math.max(1, barWidth - gap);
265
+ if (usageChartState.segment === "none") {
266
+ const values = displaySeries[0]?.values || [];
267
+ values.forEach((value, i) => {
268
+ const x = padding + i * barWidth + gap / 2;
269
+ const h = (value / scaleMax) * chartHeight;
270
+ const y = padding + chartHeight - h;
271
+ svg += `<rect x="${x}" y="${y}" width="${usableWidth}" height="${h}" fill="#6cf5d8" opacity="0.75" rx="2" />`;
272
+ });
273
+ } else {
274
+ const accum = new Array(count).fill(0);
275
+ displaySeries.forEach((entry, idx) => {
276
+ const color = colors[idx % colors.length];
277
+ const values = entry.values || [];
278
+ values.forEach((value, i) => {
279
+ if (!value) return;
280
+ const base = accum[i];
281
+ accum[i] += value;
282
+ const h = (value / scaleMax) * chartHeight;
283
+ const y = padding + chartHeight - (base / scaleMax) * chartHeight - h;
284
+ const x = padding + i * barWidth + gap / 2;
285
+ svg += `<rect x="${x}" y="${y}" width="${usableWidth}" height="${h}" fill="${color}" opacity="0.55" rx="2" />`;
286
+ });
287
+ });
288
+ }
289
+
290
+ svg += "</svg>";
291
+ container.__usageChartBound = false;
292
+ container.innerHTML = svg;
293
+ attachUsageChartInteraction(container, {
294
+ buckets,
295
+ series: displaySeries,
296
+ segment: usageChartState.segment,
297
+ scaleMax,
298
+ width,
299
+ height,
300
+ padding,
301
+ chartWidth,
302
+ chartHeight,
303
+ });
304
+ }
305
+
306
+ function getChartSize(container, fallbackWidth, fallbackHeight) {
307
+ const rect = container.getBoundingClientRect();
308
+ const width = Math.max(1, Math.round(rect.width || fallbackWidth));
309
+ const height = Math.max(1, Math.round(rect.height || fallbackHeight));
310
+ return { width, height };
311
+ }
312
+
313
+ function limitSeries(series, maxSeries, restKey) {
314
+ if (series.length <= maxSeries) return { series };
315
+ const sorted = [...series].sort((a, b) => (b.total || 0) - (a.total || 0));
316
+ const top = sorted.slice(0, maxSeries).filter((entry) => (entry.total || 0) > 0);
317
+ const rest = sorted.slice(maxSeries);
318
+ if (!rest.length) return { series: top };
319
+ const values = new Array((top[0]?.values || []).length).fill(0);
320
+ rest.forEach((entry) => {
321
+ (entry.values || []).forEach((value, i) => {
322
+ values[i] += value;
323
+ });
324
+ });
325
+ const total = values.reduce((sum, value) => sum + value, 0);
326
+ if (total > 0) {
327
+ top.push({ key: restKey, model: null, token_type: null, total, values });
328
+ }
329
+ return { series: top.length ? top : series };
330
+ }
331
+
332
+ function normalizeSeries(series, length) {
333
+ const normalized = series.map((entry) => {
334
+ const values = (entry.values || []).slice(0, length);
335
+ while (values.length < length) values.push(0);
336
+ return { ...entry, values, total: values.reduce((sum, v) => sum + v, 0) };
337
+ });
338
+ return { series: normalized };
339
+ }
340
+
341
+ function setChartLoading(container, loading) {
342
+ if (!container) return;
343
+ container.classList.toggle("loading", loading);
344
+ }
345
+
346
+ function attachUsageChartInteraction(container, state) {
347
+ container.__usageChartState = state;
348
+ if (container.__usageChartBound) return;
349
+ container.__usageChartBound = true;
350
+
351
+ const focus = document.createElement("div");
352
+ focus.className = "usage-chart-focus";
353
+ const dot = document.createElement("div");
354
+ dot.className = "usage-chart-dot";
355
+ const tooltip = document.createElement("div");
356
+ tooltip.className = "usage-chart-tooltip";
357
+ container.appendChild(focus);
358
+ container.appendChild(dot);
359
+ container.appendChild(tooltip);
360
+
361
+ const updateTooltip = (event) => {
362
+ const chartState = container.__usageChartState;
363
+ if (!chartState) return;
364
+ const rect = container.getBoundingClientRect();
365
+ const x = event.clientX - rect.left;
366
+ const normalizedX = (x / rect.width) * chartState.width;
367
+ const count = chartState.buckets.length;
368
+ const usableWidth = chartState.chartWidth;
369
+ const localX = Math.min(
370
+ Math.max(normalizedX - chartState.padding, 0),
371
+ usableWidth
372
+ );
373
+ const barWidth = count ? usableWidth / count : usableWidth;
374
+ const index = Math.floor(localX / barWidth);
375
+ const clampedIndex = Math.max(
376
+ 0,
377
+ Math.min(chartState.buckets.length - 1, index)
378
+ );
379
+ const xPos =
380
+ chartState.padding + clampedIndex * barWidth + barWidth / 2;
381
+
382
+ const totals = chartState.series.reduce((sum, entry) => {
383
+ return sum + (entry.values?.[clampedIndex] || 0);
384
+ }, 0);
385
+ const yPos =
386
+ chartState.padding +
387
+ chartState.chartHeight -
388
+ (totals / chartState.scaleMax) * chartState.chartHeight;
389
+
390
+ focus.style.opacity = "1";
391
+ dot.style.opacity = "1";
392
+ focus.style.left = `${(xPos / chartState.width) * 100}%`;
393
+ dot.style.left = `${(xPos / chartState.width) * 100}%`;
394
+ dot.style.top = `${(yPos / chartState.height) * 100}%`;
395
+
396
+ const bucketLabel = chartState.buckets[clampedIndex];
397
+ const rows = [];
398
+ rows.push(
399
+ `<div class="usage-chart-tooltip-row"><span>Total</span><span>${formatTokensCompact(
400
+ totals
401
+ )}</span></div>`
402
+ );
403
+
404
+ if (chartState.segment !== "none") {
405
+ const ranked = chartState.series
406
+ .map((entry) => ({
407
+ key: entry.key,
408
+ value: entry.values?.[clampedIndex] || 0,
409
+ }))
410
+ .filter((entry) => entry.value > 0)
411
+ .sort((a, b) => b.value - a.value)
412
+ .slice(0, 4);
413
+ ranked.forEach((entry) => {
414
+ rows.push(
415
+ `<div class="usage-chart-tooltip-row"><span>${entry.key}</span><span>${formatTokensCompact(
416
+ entry.value
417
+ )}</span></div>`
418
+ );
419
+ });
420
+ }
421
+
422
+ tooltip.innerHTML = `<div class="usage-chart-tooltip-title">${bucketLabel}</div>${rows.join(
423
+ ""
424
+ )}`;
425
+
426
+ const tooltipRect = tooltip.getBoundingClientRect();
427
+ let tooltipLeft = x + 10;
428
+ if (tooltipLeft + tooltipRect.width > rect.width) {
429
+ tooltipLeft = x - tooltipRect.width - 10;
430
+ }
431
+ tooltipLeft = Math.max(6, tooltipLeft);
432
+ const tooltipTop = 6;
433
+ tooltip.style.opacity = "1";
434
+ tooltip.style.transform = `translate(${tooltipLeft}px, ${tooltipTop}px)`;
435
+ };
436
+
437
+ container.addEventListener("pointermove", updateTooltip);
438
+ container.addEventListener("pointerleave", () => {
439
+ focus.style.opacity = "0";
440
+ dot.style.opacity = "0";
441
+ tooltip.style.opacity = "0";
442
+ });
443
+ }
444
+
445
+ async function loadUsageSeries() {
446
+ const container = document.getElementById("usage-chart-canvas");
447
+ try {
448
+ const data = await api(`/api/usage/series?${buildUsageSeriesQuery()}`);
449
+ setChartLoading(container, data?.status === "loading");
450
+ renderUsageChart(data);
451
+ if (data?.status === "loading") {
452
+ scheduleUsageSeriesRetry();
453
+ } else {
454
+ clearUsageSeriesRetry();
455
+ }
456
+ } catch (err) {
457
+ setChartLoading(container, false);
458
+ renderUsageChart(null);
459
+ clearUsageSeriesRetry();
460
+ }
461
+ }
462
+
463
+ function scheduleUsageSeriesRetry() {
464
+ clearUsageSeriesRetry();
465
+ usageSeriesRetryTimer = setTimeout(() => {
466
+ loadUsageSeries();
467
+ }, 1500);
468
+ }
469
+
470
+ function clearUsageSeriesRetry() {
471
+ if (usageSeriesRetryTimer) {
472
+ clearTimeout(usageSeriesRetryTimer);
473
+ usageSeriesRetryTimer = null;
474
+ }
475
+ }
476
+
477
+ function scheduleUsageSummaryRetry() {
478
+ clearUsageSummaryRetry();
479
+ usageSummaryRetryTimer = setTimeout(() => {
480
+ loadUsage();
481
+ }, 1500);
482
+ }
483
+
484
+ function clearUsageSummaryRetry() {
485
+ if (usageSummaryRetryTimer) {
486
+ clearTimeout(usageSummaryRetryTimer);
487
+ usageSummaryRetryTimer = null;
488
+ }
489
+ }
490
+
491
+ async function loadUsage() {
492
+ setUsageLoading(true);
493
+ try {
494
+ const data = await api("/api/usage");
495
+ const cachedUsage = loadFromCache("usage");
496
+ const hasSummary = data && data.totals && typeof data.events === "number";
497
+ if (data?.status === "loading") {
498
+ if (hasSummary) {
499
+ renderUsage(data);
500
+ } else if (cachedUsage) {
501
+ renderUsage(cachedUsage);
502
+ } else {
503
+ renderUsage(data);
504
+ }
505
+ scheduleUsageSummaryRetry();
506
+ } else {
507
+ renderUsage(data);
508
+ clearUsageSummaryRetry();
509
+ }
510
+ loadUsageSeries();
511
+ } catch (err) {
512
+ const cachedUsage = loadFromCache("usage");
513
+ if (cachedUsage) {
514
+ renderUsage(cachedUsage);
515
+ } else {
516
+ renderUsage(null);
517
+ }
518
+ flash(err.message || "Failed to load usage", "error");
519
+ clearUsageSummaryRetry();
520
+ } finally {
521
+ setUsageLoading(false);
522
+ }
523
+ }
524
+
525
+ const UPDATE_TARGET_LABELS = {
526
+ both: "web + Telegram",
527
+ web: "web only",
528
+ telegram: "Telegram only",
529
+ };
530
+
531
+ function normalizeUpdateTarget(value) {
532
+ if (!value) return "both";
533
+ if (value === "both" || value === "web" || value === "telegram") return value;
534
+ return "both";
535
+ }
536
+
537
+ function getUpdateTarget(selectId) {
538
+ const select = selectId ? document.getElementById(selectId) : null;
539
+ return normalizeUpdateTarget(select ? select.value : "both");
540
+ }
541
+
542
+ function describeUpdateTarget(target) {
543
+ return UPDATE_TARGET_LABELS[target] || UPDATE_TARGET_LABELS.both;
544
+ }
545
+
546
+ async function handleSystemUpdate(btnId, targetSelectId) {
547
+ const btn = document.getElementById(btnId);
548
+ if (!btn) return;
549
+
550
+ const originalText = btn.textContent;
551
+ btn.disabled = true;
552
+ btn.textContent = "Checking...";
553
+ const updateTarget = getUpdateTarget(targetSelectId);
554
+ const targetLabel = describeUpdateTarget(updateTarget);
555
+
556
+ let check;
557
+ try {
558
+ check = await api("/system/update/check");
559
+ } catch (err) {
560
+ check = { update_available: true, message: err.message || "Unable to check for updates." };
561
+ }
562
+
563
+ if (!check?.update_available) {
564
+ flash(check?.message || "No update available.");
565
+ btn.disabled = false;
566
+ btn.textContent = originalText;
567
+ return;
568
+ }
569
+
570
+ const restartNotice =
571
+ updateTarget === "telegram"
572
+ ? "The Telegram bot will restart."
573
+ : "The service will restart.";
574
+ const confirmed = await confirmModal(
575
+ `${check?.message || "Update available."} Update Codex Autorunner (${targetLabel})? ${restartNotice}`
576
+ );
577
+ if (!confirmed) {
578
+ btn.disabled = false;
579
+ btn.textContent = originalText;
580
+ return;
581
+ }
582
+
583
+ btn.textContent = "Updating...";
584
+
585
+ try {
586
+ const res = await api("/system/update", {
587
+ method: "POST",
588
+ body: { target: updateTarget },
589
+ });
590
+ flash(res.message || `Update started (${targetLabel}).`, "success");
591
+ if (updateTarget === "telegram") {
592
+ btn.disabled = false;
593
+ btn.textContent = originalText;
594
+ return;
595
+ }
596
+ // Disable interaction
597
+ document.body.style.pointerEvents = "none";
598
+ // Wait for restart (approx 5-10s) then reload
599
+ setTimeout(() => {
600
+ const url = new URL(window.location.href);
601
+ url.searchParams.set("v", String(Date.now()));
602
+ window.location.replace(url.toString());
603
+ }, 8000);
604
+ } catch (err) {
605
+ flash(err.message || "Update failed", "error");
606
+ btn.disabled = false;
607
+ btn.textContent = originalText;
608
+ }
609
+ }
610
+
611
+ function initSettings() {
612
+ const settingsBtn = document.getElementById("repo-settings");
613
+ const modal = document.getElementById("repo-settings-modal");
614
+ const closeBtn = document.getElementById("repo-settings-close");
615
+ const updateBtn = document.getElementById("repo-update-btn");
616
+ const updateTarget = document.getElementById("repo-update-target");
617
+ let closeModal = null;
618
+
619
+ const hideModal = () => {
620
+ if (closeModal) {
621
+ const close = closeModal;
622
+ closeModal = null;
623
+ close();
624
+ }
625
+ };
626
+
627
+ if (settingsBtn && modal) {
628
+ settingsBtn.addEventListener("click", () => {
629
+ const triggerEl = document.activeElement;
630
+ hideModal();
631
+ closeModal = openModal(modal, {
632
+ initialFocus: closeBtn || updateBtn || modal,
633
+ returnFocusTo: triggerEl,
634
+ onRequestClose: hideModal,
635
+ });
636
+ });
637
+ }
638
+
639
+ if (closeBtn && modal) {
640
+ closeBtn.addEventListener("click", () => {
641
+ hideModal();
642
+ });
643
+ }
644
+
645
+ if (updateBtn) {
646
+ updateBtn.addEventListener("click", () =>
647
+ handleSystemUpdate("repo-update-btn", updateTarget ? updateTarget.id : null)
648
+ );
649
+ }
650
+ }
651
+
652
+ function initUsageChartControls() {
653
+ const segmentSelect = document.getElementById("usage-chart-segment");
654
+ const rangeSelect = document.getElementById("usage-chart-range");
655
+ if (segmentSelect) {
656
+ segmentSelect.value = usageChartState.segment;
657
+ segmentSelect.addEventListener("change", () => {
658
+ usageChartState.segment = segmentSelect.value;
659
+ loadUsageSeries();
660
+ });
661
+ }
662
+ if (rangeSelect) {
663
+ rangeSelect.value = String(usageChartState.windowDays);
664
+ rangeSelect.addEventListener("change", () => {
665
+ const value = Number(rangeSelect.value);
666
+ usageChartState.windowDays = Number.isNaN(value)
667
+ ? usageChartState.windowDays
668
+ : value;
669
+ loadUsageSeries();
670
+ });
671
+ }
672
+ }
673
+
674
+ function bindAction(buttonId, action) {
675
+ const btn = document.getElementById(buttonId);
676
+ btn.addEventListener("click", async () => {
677
+ btn.disabled = true;
678
+ btn.classList.add("loading");
679
+ try {
680
+ await action();
681
+ } catch (err) {
682
+ flash(err.message);
683
+ } finally {
684
+ btn.disabled = false;
685
+ btn.classList.remove("loading");
686
+ }
687
+ });
688
+ }
689
+
690
+ function isDocsReady() {
691
+ return document.body?.dataset?.docsReady === "true";
692
+ }
693
+
694
+ function openSummaryDoc() {
695
+ const summaryChip = document.querySelector('.chip[data-doc="summary"]');
696
+ if (summaryChip) summaryChip.click();
697
+ }
698
+
699
+ export function initDashboard() {
700
+ initSettings();
701
+ initUsageChartControls();
702
+ subscribe("state:update", renderState);
703
+ subscribe("docs:updated", handleDocsEvent);
704
+ subscribe("docs:loaded", handleDocsEvent);
705
+ subscribe("docs:ready", () => {
706
+ if (!isDocsReady()) {
707
+ document.body.dataset.docsReady = "true";
708
+ }
709
+ if (pendingSummaryOpen) {
710
+ pendingSummaryOpen = false;
711
+ openSummaryDoc();
712
+ }
713
+ });
714
+ bindAction("start-run", () => startRun(false));
715
+ bindAction("start-once", () => startRun(true));
716
+ bindAction("stop-run", stopRun);
717
+ bindAction("resume-run", resumeRun);
718
+ bindAction("kill-run", killRun);
719
+ bindAction("reset-runner", async () => {
720
+ const confirmed = await confirmModal(
721
+ "Reset runner? This will clear all logs and reset run ID to 1."
722
+ );
723
+ if (confirmed) await resetRunner();
724
+ });
725
+ bindAction("refresh-state", loadState);
726
+ bindAction("usage-refresh", loadUsage);
727
+ bindAction("refresh-preview", loadTodoPreview);
728
+ // Try loading from cache first
729
+ const cachedState = loadFromCache("state");
730
+ if (cachedState) renderState(cachedState);
731
+
732
+ const cachedUsage = loadFromCache("usage");
733
+ if (cachedUsage) renderUsage(cachedUsage);
734
+
735
+ const cachedTodo = loadFromCache("todo-doc");
736
+ if (typeof cachedTodo === "string") {
737
+ updateTodoPreview(cachedTodo);
738
+ }
739
+
740
+ const summaryBtn = document.getElementById("open-summary");
741
+ if (summaryBtn) {
742
+ summaryBtn.addEventListener("click", () => {
743
+ const docsTab = document.querySelector('.tab[data-target="docs"]');
744
+ if (docsTab) docsTab.click();
745
+ if (isDocsReady()) {
746
+ requestAnimationFrame(openSummaryDoc);
747
+ } else {
748
+ pendingSummaryOpen = true;
749
+ }
750
+ });
751
+ }
752
+
753
+ // Initial load
754
+ loadUsage();
755
+ loadTodoPreview();
756
+ loadVersion();
757
+ checkUpdateStatus();
758
+ startStatePolling();
759
+
760
+ // Register auto-refresh for usage data (every 60s, only when dashboard tab is active)
761
+ registerAutoRefresh("dashboard-usage", {
762
+ callback: loadUsage,
763
+ tabId: "dashboard",
764
+ interval: CONSTANTS.UI.AUTO_REFRESH_USAGE_INTERVAL,
765
+ refreshOnActivation: true,
766
+ immediate: false, // Already called loadUsage() above
767
+ });
768
+ }
769
+
770
+ async function loadVersion() {
771
+ const versionEl = document.getElementById("repo-version");
772
+ if (!versionEl) return;
773
+ try {
774
+ const data = await api("/api/version", { method: "GET" });
775
+ const version = data?.asset_version || "";
776
+ versionEl.textContent = version ? `v${version}` : "v–";
777
+ } catch (_err) {
778
+ versionEl.textContent = "v–";
779
+ }
780
+ }
781
+
782
+ async function checkUpdateStatus() {
783
+ try {
784
+ const data = await api("/system/update/status", { method: "GET" });
785
+ if (!data || !data.status) return;
786
+ const stamp = data.at ? String(data.at) : "";
787
+ if (stamp && sessionStorage.getItem(UPDATE_STATUS_SEEN_KEY) === stamp) return;
788
+ if (data.status === "rollback" || data.status === "error") {
789
+ flash(data.message || "Update failed; rollback attempted.", "error");
790
+ }
791
+ if (stamp) sessionStorage.setItem(UPDATE_STATUS_SEEN_KEY, stamp);
792
+ } catch (_err) {
793
+ // ignore
794
+ }
795
+ }