codex-autorunner 1.1.0__py3-none-any.whl → 1.2.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 (127) hide show
  1. codex_autorunner/agents/opencode/client.py +113 -4
  2. codex_autorunner/agents/opencode/supervisor.py +4 -0
  3. codex_autorunner/agents/registry.py +17 -7
  4. codex_autorunner/bootstrap.py +219 -1
  5. codex_autorunner/core/__init__.py +17 -1
  6. codex_autorunner/core/about_car.py +114 -1
  7. codex_autorunner/core/app_server_threads.py +6 -0
  8. codex_autorunner/core/config.py +236 -1
  9. codex_autorunner/core/context_awareness.py +38 -0
  10. codex_autorunner/core/docs.py +0 -122
  11. codex_autorunner/core/filebox.py +265 -0
  12. codex_autorunner/core/flows/controller.py +71 -1
  13. codex_autorunner/core/flows/reconciler.py +4 -1
  14. codex_autorunner/core/flows/runtime.py +22 -0
  15. codex_autorunner/core/flows/store.py +61 -9
  16. codex_autorunner/core/flows/transition.py +23 -16
  17. codex_autorunner/core/flows/ux_helpers.py +18 -3
  18. codex_autorunner/core/flows/worker_process.py +32 -6
  19. codex_autorunner/core/hub.py +198 -41
  20. codex_autorunner/core/lifecycle_events.py +253 -0
  21. codex_autorunner/core/path_utils.py +2 -1
  22. codex_autorunner/core/pma_audit.py +224 -0
  23. codex_autorunner/core/pma_context.py +496 -0
  24. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  25. codex_autorunner/core/pma_lifecycle.py +527 -0
  26. codex_autorunner/core/pma_queue.py +367 -0
  27. codex_autorunner/core/pma_safety.py +221 -0
  28. codex_autorunner/core/pma_state.py +115 -0
  29. codex_autorunner/core/ports/agent_backend.py +2 -5
  30. codex_autorunner/core/ports/run_event.py +1 -4
  31. codex_autorunner/core/prompt.py +0 -80
  32. codex_autorunner/core/prompts.py +56 -172
  33. codex_autorunner/core/redaction.py +0 -4
  34. codex_autorunner/core/review_context.py +11 -9
  35. codex_autorunner/core/runner_controller.py +35 -33
  36. codex_autorunner/core/runner_state.py +147 -0
  37. codex_autorunner/core/runtime.py +829 -0
  38. codex_autorunner/core/sqlite_utils.py +13 -4
  39. codex_autorunner/core/state.py +7 -10
  40. codex_autorunner/core/state_roots.py +5 -0
  41. codex_autorunner/core/templates/__init__.py +39 -0
  42. codex_autorunner/core/templates/git_mirror.py +234 -0
  43. codex_autorunner/core/templates/provenance.py +56 -0
  44. codex_autorunner/core/templates/scan_cache.py +120 -0
  45. codex_autorunner/core/ticket_linter_cli.py +17 -0
  46. codex_autorunner/core/ticket_manager_cli.py +154 -92
  47. codex_autorunner/core/time_utils.py +11 -0
  48. codex_autorunner/core/types.py +18 -0
  49. codex_autorunner/core/utils.py +34 -6
  50. codex_autorunner/flows/review/service.py +23 -25
  51. codex_autorunner/flows/ticket_flow/definition.py +43 -1
  52. codex_autorunner/integrations/agents/__init__.py +2 -0
  53. codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
  54. codex_autorunner/integrations/agents/codex_backend.py +19 -8
  55. codex_autorunner/integrations/agents/runner.py +3 -8
  56. codex_autorunner/integrations/agents/wiring.py +8 -0
  57. codex_autorunner/integrations/telegram/doctor.py +228 -6
  58. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  59. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  60. codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
  61. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  62. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
  63. codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
  64. codex_autorunner/integrations/telegram/handlers/messages.py +26 -1
  65. codex_autorunner/integrations/telegram/helpers.py +1 -3
  66. codex_autorunner/integrations/telegram/runtime.py +9 -4
  67. codex_autorunner/integrations/telegram/service.py +30 -0
  68. codex_autorunner/integrations/telegram/state.py +38 -0
  69. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
  70. codex_autorunner/integrations/telegram/transport.py +10 -3
  71. codex_autorunner/integrations/templates/__init__.py +27 -0
  72. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  73. codex_autorunner/server.py +2 -2
  74. codex_autorunner/static/agentControls.js +21 -5
  75. codex_autorunner/static/app.js +115 -11
  76. codex_autorunner/static/chatUploads.js +137 -0
  77. codex_autorunner/static/docChatCore.js +185 -13
  78. codex_autorunner/static/fileChat.js +68 -40
  79. codex_autorunner/static/fileboxUi.js +159 -0
  80. codex_autorunner/static/hub.js +46 -81
  81. codex_autorunner/static/index.html +303 -24
  82. codex_autorunner/static/messages.js +82 -4
  83. codex_autorunner/static/notifications.js +255 -0
  84. codex_autorunner/static/pma.js +1167 -0
  85. codex_autorunner/static/settings.js +3 -0
  86. codex_autorunner/static/streamUtils.js +57 -0
  87. codex_autorunner/static/styles.css +9125 -6742
  88. codex_autorunner/static/templateReposSettings.js +225 -0
  89. codex_autorunner/static/ticketChatActions.js +165 -3
  90. codex_autorunner/static/ticketChatStream.js +17 -119
  91. codex_autorunner/static/ticketEditor.js +41 -13
  92. codex_autorunner/static/ticketTemplates.js +798 -0
  93. codex_autorunner/static/tickets.js +69 -19
  94. codex_autorunner/static/turnEvents.js +27 -0
  95. codex_autorunner/static/turnResume.js +33 -0
  96. codex_autorunner/static/utils.js +28 -0
  97. codex_autorunner/static/workspace.js +258 -44
  98. codex_autorunner/static/workspaceFileBrowser.js +6 -4
  99. codex_autorunner/surfaces/cli/cli.py +1465 -155
  100. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  101. codex_autorunner/surfaces/web/app.py +253 -49
  102. codex_autorunner/surfaces/web/routes/__init__.py +4 -0
  103. codex_autorunner/surfaces/web/routes/analytics.py +29 -22
  104. codex_autorunner/surfaces/web/routes/file_chat.py +317 -36
  105. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  106. codex_autorunner/surfaces/web/routes/flows.py +219 -29
  107. codex_autorunner/surfaces/web/routes/messages.py +70 -39
  108. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  109. codex_autorunner/surfaces/web/routes/repos.py +1 -1
  110. codex_autorunner/surfaces/web/routes/shared.py +0 -3
  111. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  112. codex_autorunner/surfaces/web/runner_manager.py +2 -2
  113. codex_autorunner/surfaces/web/schemas.py +70 -18
  114. codex_autorunner/tickets/agent_pool.py +27 -0
  115. codex_autorunner/tickets/files.py +33 -16
  116. codex_autorunner/tickets/lint.py +50 -0
  117. codex_autorunner/tickets/models.py +3 -0
  118. codex_autorunner/tickets/outbox.py +41 -5
  119. codex_autorunner/tickets/runner.py +350 -69
  120. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/METADATA +15 -19
  121. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
  122. codex_autorunner/core/adapter_utils.py +0 -21
  123. codex_autorunner/core/engine.py +0 -3302
  124. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  125. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  126. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  127. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,159 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ import { api, confirmModal, escapeHtml, flash, resolvePath } from "./utils.js";
3
+ function formatBytes(size) {
4
+ if (!size && size !== 0)
5
+ return "";
6
+ const units = ["B", "KB", "MB", "GB"];
7
+ let val = size;
8
+ let idx = 0;
9
+ while (val >= 1024 && idx < units.length - 1) {
10
+ val /= 1024;
11
+ idx += 1;
12
+ }
13
+ const formatted = idx === 0 ? String(val) : val.toFixed(1).replace(/\.0$/, "");
14
+ return `${formatted}${units[idx]}`;
15
+ }
16
+ function pathPrefix(config) {
17
+ if (config.scope === "repo") {
18
+ return config.basePath || "/api/filebox";
19
+ }
20
+ if (config.scope === "pma") {
21
+ return config.basePath || "/hub/pma/files";
22
+ }
23
+ if (!config.repoId) {
24
+ throw new Error("repoId is required for hub filebox");
25
+ }
26
+ const base = config.basePath || "/hub/filebox";
27
+ return `${base}/${encodeURIComponent(config.repoId)}`;
28
+ }
29
+ async function listFileBox(config) {
30
+ const prefix = pathPrefix(config);
31
+ const res = (await api(prefix));
32
+ return {
33
+ inbox: Array.isArray(res?.inbox) ? res.inbox : [],
34
+ outbox: Array.isArray(res?.outbox) ? res.outbox : [],
35
+ };
36
+ }
37
+ async function uploadFiles(config, box, files) {
38
+ const prefix = pathPrefix(config);
39
+ const form = new FormData();
40
+ const names = [];
41
+ Array.from(files).forEach((file) => {
42
+ form.append(file.name, file);
43
+ names.push(file.name);
44
+ });
45
+ await api(`${prefix}/${box}`, {
46
+ method: "POST",
47
+ body: form,
48
+ });
49
+ return names;
50
+ }
51
+ async function deleteFile(config, box, name) {
52
+ const prefix = pathPrefix(config);
53
+ await api(`${prefix}/${box}/${encodeURIComponent(name)}`, { method: "DELETE" });
54
+ }
55
+ export function createFileBoxWidget(opts) {
56
+ const uploadBox = opts.uploadBox || "inbox";
57
+ let listing = { inbox: [], outbox: [] };
58
+ const renderList = (box, el) => {
59
+ if (!el)
60
+ return;
61
+ const files = listing[box] || [];
62
+ if (!files.length) {
63
+ el.innerHTML = opts.emptyMessage
64
+ ? `<div class="filebox-empty muted small">${escapeHtml(opts.emptyMessage)}</div>`
65
+ : "";
66
+ return;
67
+ }
68
+ el.innerHTML = files
69
+ .map((entry) => {
70
+ const href = entry.url ? resolvePath(entry.url) : "#";
71
+ const meta = entry.modified_at ? new Date(entry.modified_at).toLocaleString() : "";
72
+ const size = formatBytes(entry.size);
73
+ const source = entry.source && entry.source !== "filebox" ? ` • ${escapeHtml(entry.source || "")}` : "";
74
+ return `
75
+ <div class="filebox-item">
76
+ <div class="filebox-row">
77
+ <a class="filebox-link" href="${escapeHtml(href)}" download>${escapeHtml(entry.name)}</a>
78
+ <button class="ghost sm icon-btn filebox-delete" data-box="${box}" data-file="${escapeHtml(entry.name)}" title="Delete">×</button>
79
+ </div>
80
+ <div class="filebox-meta muted small">${escapeHtml(size || "")}${source}${meta ? ` • ${escapeHtml(meta)}` : ""}</div>
81
+ </div>
82
+ `;
83
+ })
84
+ .join("");
85
+ el.querySelectorAll(".filebox-delete").forEach((btn) => {
86
+ btn.addEventListener("click", async (evt) => {
87
+ const target = evt.currentTarget;
88
+ const boxName = (target.dataset.box || "");
89
+ const file = target.dataset.file || "";
90
+ if (!boxName || !file)
91
+ return;
92
+ const confirmed = await confirmModal(`Delete ${file}?`);
93
+ if (!confirmed)
94
+ return;
95
+ try {
96
+ await deleteFile(opts, boxName, file);
97
+ await refresh();
98
+ }
99
+ catch (err) {
100
+ const msg = err.message || "Delete failed";
101
+ flash(msg, "error");
102
+ opts.onError?.(msg);
103
+ }
104
+ });
105
+ });
106
+ };
107
+ const render = () => {
108
+ renderList("inbox", opts.inboxEl);
109
+ renderList("outbox", opts.outboxEl);
110
+ };
111
+ async function refresh() {
112
+ try {
113
+ listing = await listFileBox(opts);
114
+ render();
115
+ opts.onChange?.(listing);
116
+ }
117
+ catch (err) {
118
+ const msg = err.message || "Failed to load FileBox";
119
+ flash(msg, "error");
120
+ opts.onError?.(msg);
121
+ }
122
+ return listing;
123
+ }
124
+ const handleUpload = async (files) => {
125
+ if (!files || !files.length)
126
+ return;
127
+ const names = Array.from(files).map((f) => f.name);
128
+ try {
129
+ await uploadFiles(opts, uploadBox, files);
130
+ opts.onUpload?.(names);
131
+ await refresh();
132
+ }
133
+ catch (err) {
134
+ const msg = err.message || "Upload failed";
135
+ flash(msg, "error");
136
+ opts.onError?.(msg);
137
+ }
138
+ finally {
139
+ if (opts.uploadInput)
140
+ opts.uploadInput.value = "";
141
+ }
142
+ };
143
+ if (opts.uploadBtn && opts.uploadInput) {
144
+ opts.uploadBtn.addEventListener("click", () => opts.uploadInput?.click());
145
+ opts.uploadInput.addEventListener("change", () => void handleUpload(opts.uploadInput?.files));
146
+ }
147
+ if (opts.refreshBtn) {
148
+ opts.refreshBtn.addEventListener("click", () => void refresh());
149
+ }
150
+ return {
151
+ refresh,
152
+ snapshot() {
153
+ return {
154
+ inbox: [...listing.inbox],
155
+ outbox: [...listing.outbox],
156
+ };
157
+ },
158
+ };
159
+ }
@@ -5,7 +5,6 @@ import { HUB_BASE } from "./env.js";
5
5
  import { preserveScroll } from "./preserve.js";
6
6
  let hubData = { repos: [], last_scan_at: null };
7
7
  const prefetchedUrls = new Set();
8
- let hubInboxHydrated = false;
9
8
  const HUB_CACHE_TTL_MS = 30000;
10
9
  const HUB_CACHE_KEY = `car:hub:${HUB_BASE || "/"}`;
11
10
  const HUB_USAGE_CACHE_KEY = `car:hub-usage:${HUB_BASE || "/"}`;
@@ -14,6 +13,7 @@ const HUB_REFRESH_IDLE_MS = 30000;
14
13
  let lastHubAutoRefreshAt = 0;
15
14
  const repoListEl = document.getElementById("hub-repo-list");
16
15
  const lastScanEl = document.getElementById("hub-last-scan");
16
+ const pmaLastScanEl = document.getElementById("pma-last-scan");
17
17
  const totalEl = document.getElementById("hub-count-total");
18
18
  const runningEl = document.getElementById("hub-count-running");
19
19
  const missingEl = document.getElementById("hub-count-missing");
@@ -23,8 +23,7 @@ const hubUsageChartCanvas = document.getElementById("hub-usage-chart-canvas");
23
23
  const hubUsageChartRange = document.getElementById("hub-usage-chart-range");
24
24
  const hubUsageChartSegment = document.getElementById("hub-usage-chart-segment");
25
25
  const hubVersionEl = document.getElementById("hub-version");
26
- const hubInboxList = document.getElementById("hub-inbox-list");
27
- const hubInboxRefresh = document.getElementById("hub-inbox-refresh");
26
+ const pmaVersionEl = document.getElementById("pma-version");
28
27
  const UPDATE_STATUS_SEEN_KEY = "car_update_status_seen";
29
28
  const HUB_JOB_POLL_INTERVAL_MS = 1200;
30
29
  const HUB_JOB_TIMEOUT_MS = 180000;
@@ -84,9 +83,7 @@ function formatLastActivity(repo) {
84
83
  }
85
84
  function setButtonLoading(scanning) {
86
85
  const buttons = [
87
- document.getElementById("hub-scan"),
88
86
  document.getElementById("hub-quick-scan"),
89
- document.getElementById("hub-refresh"),
90
87
  ];
91
88
  buttons.forEach((btn) => {
92
89
  if (!btn)
@@ -156,6 +153,9 @@ function renderSummary(repos) {
156
153
  if (lastScanEl) {
157
154
  lastScanEl.textContent = formatTimeCompact(hubData.last_scan_at);
158
155
  }
156
+ if (pmaLastScanEl) {
157
+ pmaLastScanEl.textContent = formatTimeCompact(hubData.last_scan_at);
158
+ }
159
159
  }
160
160
  function formatTokensCompact(val) {
161
161
  if (val === null || val === undefined)
@@ -182,17 +182,10 @@ function formatTokensAxis(val) {
182
182
  function getRepoUsage(repoId) {
183
183
  const usage = hubUsageIndex[repoId];
184
184
  if (!usage)
185
- return { label: "—", meta: "", hasData: false };
185
+ return { label: "—", hasData: false };
186
186
  const totals = usage.totals || {};
187
- const cached = totals.cached_input_tokens || 0;
188
- const input = totals.input_tokens || 0;
189
- const cachePercent = input ? Math.round((cached / input) * 100) : 0;
190
- const meta = usage.events === undefined
191
- ? ""
192
- : `${usage.events}ev${input ? ` · ${cachePercent}%↻` : ""}`;
193
187
  return {
194
188
  label: formatTokensCompact(totals.total_tokens),
195
- meta,
196
189
  hasData: true,
197
190
  };
198
191
  }
@@ -616,7 +609,7 @@ async function handleSystemUpdate(btnId, targetSelectId) {
616
609
  }
617
610
  }
618
611
  function initHubSettings() {
619
- const settingsBtn = document.getElementById("hub-settings");
612
+ const settingsBtns = Array.from(document.querySelectorAll("#hub-settings, #pma-settings"));
620
613
  const modal = document.getElementById("hub-settings-modal");
621
614
  const closeBtn = document.getElementById("hub-settings-close");
622
615
  const updateBtn = document.getElementById("hub-update-btn");
@@ -629,14 +622,16 @@ function initHubSettings() {
629
622
  close();
630
623
  }
631
624
  };
632
- if (settingsBtn && modal) {
633
- settingsBtn.addEventListener("click", () => {
634
- const triggerEl = document.activeElement;
635
- hideModal();
636
- closeModal = openModal(modal, {
637
- initialFocus: closeBtn || updateBtn || modal,
638
- returnFocusTo: triggerEl,
639
- onRequestClose: hideModal,
625
+ if (modal && settingsBtns.length > 0) {
626
+ settingsBtns.forEach((settingsBtn) => {
627
+ settingsBtn.addEventListener("click", () => {
628
+ const triggerEl = document.activeElement;
629
+ hideModal();
630
+ closeModal = openModal(modal, {
631
+ initialFocus: closeBtn || updateBtn || modal,
632
+ returnFocusTo: triggerEl,
633
+ onRequestClose: hideModal,
634
+ });
640
635
  });
641
636
  });
642
637
  }
@@ -815,10 +810,26 @@ function renderRepos(repos) {
815
810
  <span class="pill pill-small hub-usage-pill">
816
811
  ${escapeHtml(usageInfo.label)}
817
812
  </span>
818
- ${usageInfo.meta
819
- ? `<span class="hub-usage-pill-meta">${escapeHtml(usageInfo.meta)}</span>`
820
- : ""}
821
813
  </div>`;
814
+ // Ticket flow progress line
815
+ let ticketFlowLine = "";
816
+ const tf = repo.ticket_flow;
817
+ if (tf && tf.total_count > 0) {
818
+ const percent = Math.round((tf.done_count / tf.total_count) * 100);
819
+ const isActive = tf.status === "running" || tf.status === "paused";
820
+ const statusSuffix = tf.status === "paused"
821
+ ? " · paused"
822
+ : tf.current_step
823
+ ? ` · step ${tf.current_step}`
824
+ : "";
825
+ ticketFlowLine = `
826
+ <div class="hub-repo-flow-line${isActive ? " active" : ""}">
827
+ <div class="hub-flow-bar">
828
+ <div class="hub-flow-fill" style="width:${percent}%"></div>
829
+ </div>
830
+ <span class="hub-flow-text">${tf.done_count}/${tf.total_count}${statusSuffix}</span>
831
+ </div>`;
832
+ }
822
833
  card.innerHTML = `
823
834
  <div class="hub-repo-row">
824
835
  <div class="hub-repo-left">
@@ -833,6 +844,7 @@ function renderRepos(repos) {
833
844
  ${infoLine}
834
845
  </div>
835
846
  ${usageLine}
847
+ ${ticketFlowLine}
836
848
  </div>
837
849
  <div class="hub-repo-right">
838
850
  ${actions || ""}
@@ -903,7 +915,6 @@ async function refreshHub() {
903
915
  saveSessionCache(HUB_CACHE_KEY, hubData);
904
916
  renderSummary(data.repos || []);
905
917
  renderReposWithScroll(data.repos || []);
906
- await loadHubInbox().catch(() => { });
907
918
  loadHubUsage({ silent: true }).catch(() => { });
908
919
  }
909
920
  catch (err) {
@@ -913,46 +924,6 @@ async function refreshHub() {
913
924
  setButtonLoading(false);
914
925
  }
915
926
  }
916
- async function loadHubInbox(ctx) {
917
- if (!hubInboxList)
918
- return;
919
- if (!hubInboxHydrated || ctx?.reason === "manual") {
920
- hubInboxList.textContent = "Loading…";
921
- }
922
- try {
923
- const payload = (await api("/hub/messages", { method: "GET" }));
924
- const items = payload?.items || [];
925
- const html = !items.length
926
- ? '<div class="muted">No paused runs</div>'
927
- : items
928
- .map((item) => {
929
- const title = item.message?.title || item.message?.mode || "Message";
930
- const excerpt = item.message?.body ? item.message.body.slice(0, 160) : "";
931
- const repoLabel = item.repo_display_name || item.repo_id;
932
- const href = item.open_url || `/repos/${item.repo_id}/?tab=messages&run_id=${item.run_id}`;
933
- return `
934
- <a class="hub-inbox-item" href="${escapeHtml(resolvePath(href))}">
935
- <div class="hub-inbox-item-header">
936
- <span class="hub-inbox-repo">${escapeHtml(repoLabel)}</span>
937
- <span class="pill pill-small pill-warn">paused</span>
938
- </div>
939
- <div class="hub-inbox-title">${escapeHtml(title)}</div>
940
- <div class="hub-inbox-excerpt muted small">${escapeHtml(excerpt)}</div>
941
- </a>
942
- `;
943
- })
944
- .join("");
945
- preserveScroll(hubInboxList, () => {
946
- hubInboxList.innerHTML = html;
947
- }, { restoreOnNextFrame: true });
948
- hubInboxHydrated = true;
949
- }
950
- catch (_err) {
951
- preserveScroll(hubInboxList, () => {
952
- hubInboxList.innerHTML = "";
953
- }, { restoreOnNextFrame: true });
954
- }
955
- }
956
927
  async function triggerHubScan() {
957
928
  setButtonLoading(true);
958
929
  try {
@@ -1167,22 +1138,14 @@ async function handleRepoAction(repoId, action) {
1167
1138
  }
1168
1139
  function attachHubHandlers() {
1169
1140
  initHubSettings();
1170
- const scanBtn = document.getElementById("hub-scan");
1171
- const refreshBtn = document.getElementById("hub-refresh");
1172
1141
  const quickScanBtn = document.getElementById("hub-quick-scan");
1173
1142
  const newRepoBtn = document.getElementById("hub-new-repo");
1174
1143
  const createCancelBtn = document.getElementById("create-repo-cancel");
1175
1144
  const createSubmitBtn = document.getElementById("create-repo-submit");
1176
1145
  const createRepoId = document.getElementById("create-repo-id");
1177
- if (scanBtn) {
1178
- scanBtn.addEventListener("click", () => triggerHubScan());
1179
- }
1180
1146
  if (quickScanBtn) {
1181
1147
  quickScanBtn.addEventListener("click", () => triggerHubScan());
1182
1148
  }
1183
- if (refreshBtn) {
1184
- refreshBtn.addEventListener("click", () => refreshHub());
1185
- }
1186
1149
  if (hubUsageRefresh) {
1187
1150
  hubUsageRefresh.addEventListener("click", () => loadHubUsage());
1188
1151
  }
@@ -1282,15 +1245,20 @@ async function dynamicRefreshHub() {
1282
1245
  await silentRefreshHub();
1283
1246
  }
1284
1247
  async function loadHubVersion() {
1285
- if (!hubVersionEl)
1286
- return;
1287
1248
  try {
1288
1249
  const data = await api("/hub/version", { method: "GET" });
1289
1250
  const version = data.asset_version || "";
1290
- hubVersionEl.textContent = version ? `v${version}` : "v–";
1251
+ const formatted = version ? `v${version}` : "v–";
1252
+ if (hubVersionEl)
1253
+ hubVersionEl.textContent = formatted;
1254
+ if (pmaVersionEl)
1255
+ pmaVersionEl.textContent = formatted;
1291
1256
  }
1292
1257
  catch (_err) {
1293
- hubVersionEl.textContent = "v–";
1258
+ if (hubVersionEl)
1259
+ hubVersionEl.textContent = "v–";
1260
+ if (pmaVersionEl)
1261
+ pmaVersionEl.textContent = "v–";
1294
1262
  }
1295
1263
  }
1296
1264
  async function checkUpdateStatus() {
@@ -1322,9 +1290,6 @@ export function initHub() {
1322
1290
  return;
1323
1291
  attachHubHandlers();
1324
1292
  initHubUsageChartControls();
1325
- hubInboxRefresh?.addEventListener("click", () => {
1326
- void loadHubInbox({ reason: "manual" });
1327
- });
1328
1293
  const cachedHub = loadSessionCache(HUB_CACHE_KEY, HUB_CACHE_TTL_MS);
1329
1294
  if (cachedHub) {
1330
1295
  hubData = cachedHub;