diff --git a/apps/decodex-app/Sources/DecodexApp/AccountStore.swift b/apps/decodex-app/Sources/DecodexApp/AccountStore.swift index 03e8926eb..d162e1faf 100644 --- a/apps/decodex-app/Sources/DecodexApp/AccountStore.swift +++ b/apps/decodex-app/Sources/DecodexApp/AccountStore.swift @@ -3,6 +3,7 @@ import Foundation import OSLog private let accountStoreLog = Logger(subsystem: "ink.hack.DecodexApp", category: "AccountStore") +private let accountUsageRefreshIntervalNanoseconds: UInt64 = 15_000_000_000 private let operatorSnapshotReconnectInitialDelay: UInt64 = 1_000_000_000 private let operatorSnapshotReconnectMaxDelay: UInt64 = 30_000_000_000 @@ -102,7 +103,7 @@ final class AccountStore: ObservableObject { return } - await refresh() + await refresh(force: true) } func openWebUI() async { @@ -133,7 +134,7 @@ final class AccountStore: ObservableObject { automaticRefreshTask = Task { [weak self] in while Task.isCancelled == false { do { - try await Task.sleep(nanoseconds: 60_000_000_000) + try await Task.sleep(nanoseconds: accountUsageRefreshIntervalNanoseconds) } catch { return } diff --git a/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift b/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift index eeaa0fd20..dca490716 100644 --- a/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift +++ b/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift @@ -251,7 +251,7 @@ final class AccountModelTests: XCTestCase { XCTAssertEqual(lifecycle.buckets.first?.wallSeconds, 1313) } - func testStoppedActiveRunUsesInactiveDurationAndAttentionTone() throws { + func testStoppedCurrentLaneUsesInactiveDurationAndAttentionTone() throws { let payload = """ { "run_id": "pub-1524-attempt-2", @@ -331,7 +331,7 @@ final class AccountModelTests: XCTestCase { ) let payload = """ { - "active_runs": [ + "current_lanes": [ { "run_id": "run-1", "issue_identifier": "XY-445", @@ -378,12 +378,12 @@ final class AccountModelTests: XCTestCase { let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: payload) - XCTAssertEqual(snapshot.activeRuns(for: assignedAccount).map(\.runID), ["run-1"]) - XCTAssertEqual(snapshot.activeRuns(for: otherAssignedAccount).map(\.runID), ["run-2"]) - XCTAssertTrue(snapshot.activeRuns(for: poolOnlyAccount).isEmpty) + XCTAssertEqual(snapshot.currentLanes(for: assignedAccount).map(\.runID), ["run-1"]) + XCTAssertEqual(snapshot.currentLanes(for: otherAssignedAccount).map(\.runID), ["run-2"]) + XCTAssertTrue(snapshot.currentLanes(for: poolOnlyAccount).isEmpty) } - func testOperatorSnapshotKeepsUnassignedActiveRunsVisibleGlobally() throws { + func testOperatorSnapshotKeepsUnassignedCurrentLanesVisibleGlobally() throws { let account = makeAccount( status: "available", email: "pool@example.com", @@ -391,7 +391,7 @@ final class AccountModelTests: XCTestCase { ) let payload = """ { - "active_runs": [ + "current_lanes": [ { "run_id": "run-unassigned", "project_id": "pubfi-platform", @@ -404,18 +404,18 @@ final class AccountModelTests: XCTestCase { let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: payload) - XCTAssertEqual(snapshot.activeRuns.map(\.runID), ["run-unassigned"]) - XCTAssertEqual(snapshot.activeRunCount, 1) - XCTAssertTrue(snapshot.activeRuns(for: account).isEmpty) + XCTAssertEqual(snapshot.currentLanes.map(\.runID), ["run-unassigned"]) + XCTAssertEqual(snapshot.currentLaneCount, 1) + XCTAssertTrue(snapshot.currentLanes(for: account).isEmpty) } - func testOperatorProjectStatusSeparatesActiveAndRunningLaneCounts() throws { + func testOperatorProjectStatusSeparatesCurrentAndRunningLaneCounts() throws { let payload = """ { "projects": [ { "project_id": "pubfi-platform", - "active_run_count": 2, + "current_lane_count": 2, "running_lane_count": 1, "attention_count": 1 } @@ -426,20 +426,20 @@ final class AccountModelTests: XCTestCase { let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: payload) let project = try XCTUnwrap(snapshot.projects.first) - XCTAssertEqual(project.activeRunCount, 2) + XCTAssertEqual(project.currentLaneCount, 2) XCTAssertEqual(project.runningLaneCount, 1) - XCTAssertEqual(snapshot.activeRunCount, 2) + XCTAssertEqual(snapshot.currentLaneCount, 2) XCTAssertEqual(snapshot.runningLaneCount, 1) XCTAssertEqual(snapshot.attentionCount, 1) } - func testOperatorProjectStatusDefaultsRunningLaneCountToActiveRunCount() throws { + func testOperatorProjectStatusDefaultsRunningLaneCountToCurrentLaneCount() throws { let payload = """ { "projects": [ { "project_id": "pubfi-platform", - "active_run_count": 2 + "current_lane_count": 2 } ] } @@ -448,7 +448,7 @@ final class AccountModelTests: XCTestCase { let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: payload) let project = try XCTUnwrap(snapshot.projects.first) - XCTAssertEqual(project.activeRunCount, 2) + XCTAssertEqual(project.currentLaneCount, 2) XCTAssertEqual(project.runningLaneCount, 2) XCTAssertEqual(snapshot.runningLaneCount, 2) } @@ -465,7 +465,7 @@ final class AccountModelTests: XCTestCase { "post_review_lanes": [ { "classification": "ready_to_land", - "shadowed_by_active_run": true + "shadowed_by_current_lane": true } ] } @@ -475,7 +475,7 @@ final class AccountModelTests: XCTestCase { XCTAssertEqual(snapshot.reviewCount, 0) XCTAssertEqual(snapshot.landingCount, 0) - XCTAssertEqual(snapshot.postReviewLanes.first?.shadowedByActiveRun, true) + XCTAssertEqual(snapshot.postReviewLanes.first?.shadowedByCurrentLane, true) } func testOperatorSnapshotAssignsSelectedAccountWhenPrimaryAccountIsMissing() throws { @@ -491,7 +491,7 @@ final class AccountModelTests: XCTestCase { ) let payload = """ { - "active_runs": [ + "current_lanes": [ { "run_id": "run-1", "issue_identifier": "XY-689", @@ -514,8 +514,8 @@ final class AccountModelTests: XCTestCase { let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: payload) - XCTAssertEqual(snapshot.activeRuns(for: assignedAccount).map(\.runID), ["run-1"]) - XCTAssertTrue(snapshot.activeRuns(for: poolOnlyAccount).isEmpty) + XCTAssertEqual(snapshot.currentLanes(for: assignedAccount).map(\.runID), ["run-1"]) + XCTAssertTrue(snapshot.currentLanes(for: poolOnlyAccount).isEmpty) } @MainActor @@ -533,7 +533,7 @@ final class AccountModelTests: XCTestCase { { "snapshotPublishedAtUnixEpoch": 20, "snapshot": { - "active_runs": [ + "current_lanes": [ { "run_id": "run-new", "issue_identifier": "XY-672", @@ -552,8 +552,8 @@ final class AccountModelTests: XCTestCase { payload: """ { "emittedAtUnixEpoch": 10, - "activeRunsComplete": true, - "activeRuns": [ + "currentLanesComplete": true, + "currentLanes": [ { "run_id": "run-old", "issue_identifier": "PUB-1147", @@ -567,11 +567,11 @@ final class AccountModelTests: XCTestCase { """ )) - XCTAssertEqual(store.operatorSnapshot?.activeRuns(for: account).map(\.runID), ["run-old"]) + XCTAssertEqual(store.operatorSnapshot?.currentLanes(for: account).map(\.runID), ["run-old"]) } @MainActor - func testRunActivityBeforeSnapshotCreatesVisibleActiveRuns() throws { + func testRunActivityBeforeSnapshotCreatesVisibleCurrentLanes() throws { let account = makeAccount( status: "available", email: "copy@example.com", @@ -584,8 +584,8 @@ final class AccountModelTests: XCTestCase { payload: """ { "emittedAtUnixEpoch": 30, - "activeRunsComplete": true, - "activeRuns": [ + "currentLanesComplete": true, + "currentLanes": [ { "run_id": "run-live", "issue_identifier": "XY-672", @@ -599,11 +599,11 @@ final class AccountModelTests: XCTestCase { """ )) - XCTAssertEqual(store.operatorSnapshot?.activeRuns.map(\.runID), ["run-live"]) - XCTAssertEqual(store.operatorSnapshot?.activeRuns(for: account).map(\.runID), ["run-live"]) + XCTAssertEqual(store.operatorSnapshot?.currentLanes.map(\.runID), ["run-live"]) + XCTAssertEqual(store.operatorSnapshot?.currentLanes(for: account).map(\.runID), ["run-live"]) } - func testPartialRunActivityPreservesSnapshotActiveRuns() throws { + func testPartialRunActivityPreservesSnapshotCurrentLanes() throws { let account = makeAccount( status: "available", email: "copy@example.com", @@ -611,11 +611,11 @@ final class AccountModelTests: XCTestCase { ) let snapshotPayload = """ { - "active_runs": [ + "current_lanes": [ { "run_id": "run-689", "issue_identifier": "XY-689", - "active_lease": true, + "run_lease": true, "account": { "email": "copy@example.com", "account_fingerprint": "...123456" @@ -624,7 +624,7 @@ final class AccountModelTests: XCTestCase { { "run_id": "run-690", "issue_identifier": "XY-690", - "active_lease": true, + "run_lease": true, "account": { "email": "copy@example.com", "account_fingerprint": "...123456" @@ -635,8 +635,8 @@ final class AccountModelTests: XCTestCase { """.data(using: .utf8)! let activityPayload = """ { - "activeRunsComplete": false, - "activeRuns": [ + "currentLanesComplete": false, + "currentLanes": [ { "run_id": "run-690", "issue_identifier": "XY-690", @@ -653,14 +653,14 @@ final class AccountModelTests: XCTestCase { let event = try JSONDecoder() .decode(OperatorDashboardSocketPayload.self, from: activityPayload) let overlay = OperatorRunActivitySnapshot( - activeRuns: event.activeRuns ?? [], - activeRunsComplete: event.activeRunsComplete ?? true, + currentLanes: event.currentLanes ?? [], + currentLanesComplete: event.currentLanesComplete ?? true, emittedAt: Date(timeIntervalSince1970: 30) ) let merged = overlay.merging(into: snapshot) - XCTAssertEqual(merged.activeRuns.map(\.runID), ["run-689", "run-690"]) - XCTAssertEqual(merged.activeRuns(for: account).map(\.runID), ["run-689", "run-690"]) + XCTAssertEqual(merged.currentLanes.map(\.runID), ["run-689", "run-690"]) + XCTAssertEqual(merged.currentLanes(for: account).map(\.runID), ["run-689", "run-690"]) } func testPartialRunActivityRecomputesProjectRunningLaneCounts() throws { @@ -669,11 +669,11 @@ final class AccountModelTests: XCTestCase { "projects": [ { "project_id": "pubfi-platform", - "active_run_count": 1, + "current_lane_count": 1, "running_lane_count": 1 } ], - "active_runs": [ + "current_lanes": [ { "run_id": "run-stopped", "project_id": "pubfi-platform", @@ -686,21 +686,21 @@ final class AccountModelTests: XCTestCase { """.data(using: .utf8)! let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: snapshotPayload) let overlay = OperatorRunActivitySnapshot( - activeRuns: [], - activeRunsComplete: false, + currentLanes: [], + currentLanesComplete: false, emittedAt: Date(timeIntervalSince1970: 30) ) let merged = overlay.merging(into: snapshot) let project = try XCTUnwrap(merged.projects.first) - XCTAssertEqual(merged.activeRuns.map(\.runID), ["run-stopped"]) - XCTAssertEqual(project.activeRunCount, 1) + XCTAssertEqual(merged.currentLanes.map(\.runID), ["run-stopped"]) + XCTAssertEqual(project.currentLaneCount, 1) XCTAssertEqual(project.runningLaneCount, 0) - XCTAssertEqual(merged.activeRunCount, 1) + XCTAssertEqual(merged.currentLaneCount, 1) XCTAssertEqual(merged.runningLaneCount, 0) } - func testEmptyPartialRunActivityPreservesSnapshotActiveRuns() throws { + func testEmptyPartialRunActivityPreservesSnapshotCurrentLanes() throws { let account = makeAccount( status: "available", email: "copy@example.com", @@ -708,11 +708,11 @@ final class AccountModelTests: XCTestCase { ) let snapshotPayload = """ { - "active_runs": [ + "current_lanes": [ { "run_id": "run-689", "issue_identifier": "XY-689", - "active_lease": true, + "run_lease": true, "account": { "email": "copy@example.com", "account_fingerprint": "...123456" @@ -723,17 +723,17 @@ final class AccountModelTests: XCTestCase { """.data(using: .utf8)! let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: snapshotPayload) let overlay = OperatorRunActivitySnapshot( - activeRuns: [], - activeRunsComplete: false, + currentLanes: [], + currentLanesComplete: false, emittedAt: Date(timeIntervalSince1970: 30) ) let merged = overlay.merging(into: snapshot) - XCTAssertEqual(merged.activeRuns.map(\.runID), ["run-689"]) - XCTAssertEqual(merged.activeRuns(for: account).map(\.runID), ["run-689"]) + XCTAssertEqual(merged.currentLanes.map(\.runID), ["run-689"]) + XCTAssertEqual(merged.currentLanes(for: account).map(\.runID), ["run-689"]) } - func testCompleteRunActivityReplacesSnapshotActiveRuns() throws { + func testCompleteRunActivityReplacesSnapshotCurrentLanes() throws { let account = makeAccount( status: "available", email: "copy@example.com", @@ -741,11 +741,11 @@ final class AccountModelTests: XCTestCase { ) let snapshotPayload = """ { - "active_runs": [ + "current_lanes": [ { "run_id": "run-689", "issue_identifier": "XY-689", - "active_lease": true, + "run_lease": true, "account": { "email": "copy@example.com", "account_fingerprint": "...123456" @@ -754,7 +754,7 @@ final class AccountModelTests: XCTestCase { { "run_id": "run-690", "issue_identifier": "XY-690", - "active_lease": true, + "run_lease": true, "account": { "email": "copy@example.com", "account_fingerprint": "...123456" @@ -765,8 +765,8 @@ final class AccountModelTests: XCTestCase { """.data(using: .utf8)! let activityPayload = """ { - "activeRunsComplete": true, - "activeRuns": [ + "currentLanesComplete": true, + "currentLanes": [ { "run_id": "run-690", "issue_identifier": "XY-690", @@ -783,14 +783,14 @@ final class AccountModelTests: XCTestCase { let event = try JSONDecoder() .decode(OperatorDashboardSocketPayload.self, from: activityPayload) let overlay = OperatorRunActivitySnapshot( - activeRuns: event.activeRuns ?? [], - activeRunsComplete: event.activeRunsComplete ?? true, + currentLanes: event.currentLanes ?? [], + currentLanesComplete: event.currentLanesComplete ?? true, emittedAt: Date(timeIntervalSince1970: 30) ) let merged = overlay.merging(into: snapshot) - XCTAssertEqual(merged.activeRuns.map(\.runID), ["run-690"]) - XCTAssertEqual(merged.activeRuns(for: account).map(\.runID), ["run-690"]) + XCTAssertEqual(merged.currentLanes.map(\.runID), ["run-690"]) + XCTAssertEqual(merged.currentLanes(for: account).map(\.runID), ["run-690"]) } func testNewerEmptyRunActivityClearsSnapshotRuns() throws { @@ -801,7 +801,7 @@ final class AccountModelTests: XCTestCase { ) let snapshotPayload = """ { - "active_runs": [ + "current_lanes": [ { "run_id": "run-old", "issue_identifier": "XY-672", @@ -815,13 +815,13 @@ final class AccountModelTests: XCTestCase { """.data(using: .utf8)! let snapshot = try JSONDecoder().decode(OperatorSnapshotResponse.self, from: snapshotPayload) let overlay = OperatorRunActivitySnapshot( - activeRuns: [], - activeRunsComplete: true, + currentLanes: [], + currentLanesComplete: true, emittedAt: Date(timeIntervalSince1970: 30) ) let merged = overlay.merging(into: snapshot) - XCTAssertTrue(merged.activeRuns(for: account).isEmpty) + XCTAssertTrue(merged.currentLanes(for: account).isEmpty) } @MainActor @@ -839,7 +839,7 @@ final class AccountModelTests: XCTestCase { { "snapshotPublishedAtUnixEpoch": 20, "snapshot": { - "active_runs": [] + "current_lanes": [] } } """ @@ -849,8 +849,8 @@ final class AccountModelTests: XCTestCase { payload: """ { "emittedAtUnixEpoch": 30, - "activeRunsComplete": true, - "activeRuns": [ + "currentLanesComplete": true, + "currentLanes": [ { "run_id": "run-live", "issue_identifier": "XY-672", @@ -869,13 +869,13 @@ final class AccountModelTests: XCTestCase { { "snapshotPublishedAtUnixEpoch": 40, "snapshot": { - "active_runs": [] + "current_lanes": [] } } """ )) - XCTAssertEqual(store.operatorSnapshot?.activeRuns(for: account).map(\.runID), ["run-live"]) + XCTAssertEqual(store.operatorSnapshot?.currentLanes(for: account).map(\.runID), ["run-live"]) } @MainActor @@ -893,7 +893,7 @@ final class AccountModelTests: XCTestCase { { "snapshotPublishedAtUnixEpoch": 20, "snapshot": { - "active_runs": [ + "current_lanes": [ { "run_id": "run-live", "issue_identifier": "XY-672", @@ -912,13 +912,13 @@ final class AccountModelTests: XCTestCase { payload: """ { "emittedAtUnixEpoch": 30, - "activeRunsComplete": true, - "activeRuns": [] + "currentLanesComplete": true, + "currentLanes": [] } """ )) - XCTAssertTrue(store.operatorSnapshot?.activeRuns(for: account).isEmpty ?? false) + XCTAssertTrue(store.operatorSnapshot?.currentLanes(for: account).isEmpty ?? false) } @MainActor @@ -936,7 +936,7 @@ final class AccountModelTests: XCTestCase { { "snapshotPublishedAtUnixEpoch": 20, "snapshot": { - "active_runs": [ + "current_lanes": [ { "run_id": "run-live", "issue_identifier": "XY-672", @@ -955,12 +955,12 @@ final class AccountModelTests: XCTestCase { payload: """ { "emittedAtUnixEpoch": 30, - "activeRunsComplete": true, - "activeRuns": [] + "currentLanesComplete": true, + "currentLanes": [] } """ )) - XCTAssertTrue(store.operatorSnapshot?.activeRuns(for: account).isEmpty ?? false) + XCTAssertTrue(store.operatorSnapshot?.currentLanes(for: account).isEmpty ?? false) try store.applyOperatorDashboardEvent(dashboardEvent( type: "snapshot", @@ -968,7 +968,7 @@ final class AccountModelTests: XCTestCase { { "snapshotPublishedAtUnixEpoch": 40, "snapshot": { - "active_runs": [ + "current_lanes": [ { "run_id": "run-returned", "issue_identifier": "XY-934", @@ -983,7 +983,7 @@ final class AccountModelTests: XCTestCase { """ )) - XCTAssertEqual(store.operatorSnapshot?.activeRuns(for: account).map(\.runID), ["run-returned"]) + XCTAssertEqual(store.operatorSnapshot?.currentLanes(for: account).map(\.runID), ["run-returned"]) } func testOperatorSnapshotWarningSummaryUsesRawWarningToken() throws { diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html index 437843f86..027fca985 100644 --- a/apps/decodex/src/orchestrator/operator_dashboard.html +++ b/apps/decodex/src/orchestrator/operator_dashboard.html @@ -3915,6 +3915,7 @@

Run History

]; const DASHBOARD_WEBSOCKET_ENDPOINT = "/dashboard/control"; const DASHBOARD_LOCAL_CLOCK_INTERVAL_MS = 5_000; + const ACCOUNT_API_REFRESH_INTERVAL_MS = 15_000; const RUN_ATTENTION_IDLE_SECONDS = 60; const RUN_STALE_NO_PROCESS_SECONDS = 300; @@ -3976,7 +3977,6 @@

Run History

let dashboardLiveCurrentLanes = []; let dashboardLiveRunActivitySeen = false; let dashboardLiveCurrentLanesComplete = true; - let dashboardLiveAccounts = null; let dashboardLiveAccountControl = null; let dashboardStreamState = { supported: typeof window.WebSocket === "function", @@ -4880,12 +4880,6 @@

Run History

: []; } - function configuredDashboardAccounts(snapshot) { - return Array.isArray(snapshot?.accounts) - ? [...accountApiAccounts(), ...snapshot.accounts.filter(Boolean)] - : accountApiAccounts(); - } - function noteAccountApiSnapshot(response) { if (!response || !Array.isArray(response.accounts)) { return false; @@ -4901,14 +4895,16 @@

Run History

const now = Date.now(); if ( accountApiRefreshInFlight || - (!force && accountApiSnapshot && now - accountApiRefreshedAt < 15_000) + (!force && + accountApiSnapshot && + now - accountApiRefreshedAt < ACCOUNT_API_REFRESH_INTERVAL_MS) ) { return; } accountApiRefreshInFlight = true; try { - const response = await fetch("/api/accounts", { + const response = await fetch("/api/accounts?refresh=1", { headers: { Accept: "application/json" }, }); if (!response.ok) { @@ -6590,7 +6586,7 @@

Run History

return null; } - const accounts = configuredDashboardAccounts(snapshot); + const accounts = accountApiAccounts(); if (!accounts.length) { return null; } @@ -6686,7 +6682,7 @@

Run History

} return ( - configuredDashboardAccounts(snapshot).find( + accountApiAccounts().find( (candidate) => (identity && codexAccountIdentity(candidate) === identity) || (email && codexAccountEmail(candidate) === email) || @@ -7197,8 +7193,8 @@

Run History

)[0] || null; } - function accountPoolUsageEstimate(snapshot) { - return accountApiSnapshot?.usage_estimate || snapshot?.usage_estimate || null; + function accountPoolUsageEstimate() { + return accountApiSnapshot?.usage_estimate || null; } function accountPoolDayDeltaPercentagePoints(accounts, estimate) { @@ -7952,8 +7948,8 @@

Run History

nodes.accountModeMeta.title = title; } - function renderCodexAccountPoolUsageSummary(accounts, snapshot) { - const estimate = accountPoolUsageEstimate(snapshot); + function renderCodexAccountPoolUsageSummary(accounts) { + const estimate = accountPoolUsageEstimate(); if (!estimate) { return ""; } @@ -8032,13 +8028,13 @@

Run History

`; } - function renderCodexAccountPool(accounts, snapshot) { - if (!accounts.length) { - return ""; - } + function renderCodexAccountPool(accounts, snapshot) { + if (!accounts.length) { + return ""; + } return ` - ${renderCodexAccountPoolUsageSummary(accounts, snapshot)} + ${renderCodexAccountPoolUsageSummary(accounts)}
${ACCOUNT_POOL_SORT_COLUMNS.map(renderCodexAccountPoolGuideCell).join("")} @@ -8103,62 +8099,10 @@

Run History

return renderRunMetaLine(run); } - function codexAccountPoolAccounts(snapshot) { - const accountsByIdentity = new Map(); - const configuredAccounts = configuredDashboardAccounts(snapshot); - const runs = [ - ...(snapshot?.current_lanes ?? []).map((run) => [run, true]), - ...(snapshot?.recent_runs ?? []).map((run) => [run, false]), - ]; - - for (const account of configuredAccounts) { - const identity = codexAccountIdentity(account); - if (!identity) { - continue; - } - - const existing = accountsByIdentity.get(identity); - accountsByIdentity.set(identity, existing ? { ...existing, ...account } : account); - } - - for (const [run, isCurrentLane] of runs) { - const selected = codexAccount(run); - for (const account of codexAccounts(run)) { - const identity = codexAccountIdentity(account); - if (!account?.account_fingerprint) { - continue; - } - - const selectedForRun = - selected && codexAccountIdentity(account) === codexAccountIdentity(selected); - const accountStatus = String(account.status || "").toLowerCase(); - const existing = accountsByIdentity.get(identity); - const candidate = { - ...existing, - ...account, - status: - isCurrentLane && selectedForRun - ? "selected" - : accountStatus === "selected" - ? "available" - : account.status, - }; - if ( - !existing || - codexAccountPoolMergeRank(candidate) >= codexAccountPoolMergeRank(existing) - ) { - accountsByIdentity.set(identity, candidate); - } - } - } - - return sortCodexAccountPoolAccounts([...accountsByIdentity.values()]); - } - - function codexAccountPoolMergeRank(account) { - const status = String(account?.status || "").toLowerCase(); - - return status === "selected" ? 1 : 0; + function codexAccountPoolAccounts() { + return sortCodexAccountPoolAccounts( + accountApiAccounts().map((account) => ({ ...account })), + ); } function codexAccountCreditsSortValue(account) { @@ -11191,6 +11135,7 @@

${escapeHtml(worktree.branch_name)}

return; } + refreshAccountApiSnapshot(); renderDashboardState(lastDashboardRender, { refreshAccounts: false }); } @@ -11398,9 +11343,6 @@

${escapeHtml(worktree.branch_name)}

}; }) : snapshot.projects; - const accounts = Array.isArray(activityPayload.accounts) - ? activityPayload.accounts.filter(Boolean).map((account) => ({ ...account })) - : snapshot.accounts; const accountControl = activityPayload.accountControl && typeof activityPayload.accountControl === "object" ? { ...(snapshot.account_control || {}), ...activityPayload.accountControl } @@ -11409,7 +11351,6 @@

${escapeHtml(worktree.branch_name)}

return { ...snapshot, account_control: accountControl, - accounts, current_lanes: mergedCurrentLanes, recent_runs: recentRuns, projects, @@ -11436,7 +11377,6 @@

${escapeHtml(worktree.branch_name)}

dashboardLiveRunActivitySeen = false; dashboardLiveCurrentLanes = []; dashboardLiveCurrentLanesComplete = true; - dashboardLiveAccounts = null; dashboardLiveAccountControl = null; } } @@ -11452,7 +11392,6 @@

${escapeHtml(worktree.branch_name)}

return mergeDashboardRunActivity(snapshot, dashboardLiveCurrentLanes, { accountControl: dashboardLiveAccountControl, - accounts: dashboardLiveAccounts, currentLanesComplete: dashboardLiveCurrentLanesComplete, }); } @@ -11474,9 +11413,6 @@

${escapeHtml(worktree.branch_name)}

dashboardLiveRunActivitySeen = true; dashboardLiveCurrentLanesComplete = payload.currentLanesComplete !== false && payload.currentLaneScope !== "filtered"; - dashboardLiveAccounts = Array.isArray(payload.accounts) && payload.accounts.length - ? payload.accounts.filter(Boolean).map((account) => ({ ...account })) - : null; dashboardLiveAccountControl = payload.accountControl && typeof payload.accountControl === "object" ? { ...payload.accountControl } @@ -11640,7 +11576,6 @@

${escapeHtml(worktree.branch_name)}

dashboardLiveCurrentLanes = []; dashboardLiveRunActivitySeen = false; dashboardLiveCurrentLanesComplete = true; - dashboardLiveAccounts = null; dashboardLiveAccountControl = null; updateDashboardStreamState({ connected: false, @@ -11652,7 +11587,6 @@

${escapeHtml(worktree.branch_name)}

dashboardLiveCurrentLanes = []; dashboardLiveRunActivitySeen = false; dashboardLiveCurrentLanesComplete = true; - dashboardLiveAccounts = null; dashboardLiveAccountControl = null; updateDashboardStreamState({ connected: false, diff --git a/apps/decodex/src/orchestrator/operator_http.rs b/apps/decodex/src/orchestrator/operator_http.rs index 4cdd5e0a4..7fbdd423b 100644 --- a/apps/decodex/src/orchestrator/operator_http.rs +++ b/apps/decodex/src/orchestrator/operator_http.rs @@ -565,7 +565,6 @@ fn build_operator_run_activity_event( ) -> Result { let now_unix_epoch = OffsetDateTime::now_utc().unix_timestamp(); let account_control = global_codex_account_control_status(); - let mut accounts = Vec::new(); let mut current_lanes = Vec::new(); for registration in state_store.list_projects()? { @@ -606,20 +605,15 @@ fn build_operator_run_activity_event( continue; } - accounts.extend(project_snapshot.accounts); current_lanes.extend(project_snapshot.current_lanes); } - let fingerprint_payload = dashboard_run_activity_fingerprint_payload( - &account_control, - &accounts, - ¤t_lanes, - ); + let fingerprint_payload = + dashboard_run_activity_fingerprint_payload(&account_control, ¤t_lanes); let fingerprint = serde_json::to_vec(&fingerprint_payload)?; let payload = json!({ "emittedAtUnixEpoch": now_unix_epoch, "accountControl": &account_control, - "accounts": &accounts, "currentLanes": ¤t_lanes, "currentLanesComplete": true, "currentLaneScope": "complete", @@ -633,12 +627,10 @@ fn build_operator_run_activity_event( fn dashboard_run_activity_fingerprint_payload( account_control: &OperatorCodexAccountControlStatus, - accounts: &[CodexAccountActivitySummary], current_lanes: &[OperatorRunStatus], ) -> Value { let mut fingerprint_payload = json!({ "accountControl": account_control, - "accounts": accounts, "currentLanes": current_lanes, "currentLanesComplete": true, "currentLaneScope": "complete", diff --git a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs index 873683b9e..af77619c3 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs @@ -654,9 +654,10 @@ fn operator_dashboard_renders_account_usage_controls() { assert!(response.contains("function codexAccountTokenLabel(refreshStatus)")); assert!(response.contains("function codexAccountWindowLabel(seconds)")); assert!(response.contains("function codexAccountStatusTone(account)")); - assert!(response.contains("function renderCodexAccountPoolUsageSummary(accounts, snapshot)")); + assert!(response.contains("function renderCodexAccountPoolUsageSummary(accounts)")); assert!(response.contains("function accountPoolDayDeltaPercentagePoints(accounts, estimate)")); assert!(response.contains("accountApiSnapshot?.usage_estimate")); + assert!(!response.contains("snapshot?.usage_estimate")); assert!(response.contains("Pool used")); assert!(response.contains("Day Δ")); assert!(response.contains("Daily avg")); @@ -668,8 +669,10 @@ fn operator_dashboard_renders_account_usage_controls() { assert!(response.contains("function renderAccountModeControl(snapshot)")); assert!(response.contains("nodes.accountModeMeta.innerHTML = `${escapeHtml(title)}`;")); assert!(response.contains("nodes.accountModeMeta.title = title;")); - assert!(response.contains("function codexAccountPoolAccounts(snapshot)")); - assert!(response.contains("function codexAccountPoolMergeRank(account)")); + assert!(response.contains("function codexAccountPoolAccounts()")); + assert!(response.contains("accountApiAccounts().map((account) => ({ ...account }))")); + assert!(!response.contains("function configuredDashboardAccounts(snapshot)")); + assert!(!response.contains("function codexAccountPoolMergeRank(account)")); assert!(response.contains("function renderCodexAccountPool(accounts, snapshot)")); assert!(!response.contains("function renderCodexAccountPoolHeader(accounts)")); assert!(response.contains( @@ -697,7 +700,7 @@ fn operator_dashboard_renders_account_usage_controls() { assert!(!response.contains("function codexAccountPoolDebugSummary(accounts)")); assert!(response.contains("return \"not captured\";")); assert!(response.contains("function codexAccountHistorySummary(account)")); - assert!(response.contains("snapshot?.accounts")); + assert!(!response.contains("snapshot?.accounts")); assert!(response.contains("account?.account_email || account?.email")); assert!(response.contains("run?.account || run?.codex_account || null")); assert!(response.contains("run?.accounts")); @@ -771,7 +774,7 @@ fn operator_dashboard_account_privacy_controls_use_compact_identities() { assert!(!response.contains("function loadAccountNameOffsets()")); assert!(response.contains("function persistAccountPrivacy(hidden)")); assert!(!response.contains("function persistAccountNameOffsets()")); - assert!(response.contains("function configuredDashboardAccounts(snapshot)")); + assert!(!response.contains("function configuredDashboardAccounts(snapshot)")); assert!(response.contains("function renderAccountPrivacyToggle()")); assert!(response.contains("function codexAccountRandomNameKey(account)")); assert!(response.contains("function codexAccountRandomNameOffset(account)")); @@ -806,7 +809,6 @@ fn operator_dashboard_account_privacy_controls_use_compact_identities() { assert!(response.contains("\"Alex\"")); assert!(response.contains("return `${local.slice(0, 3)}...${local.slice(-3)}${domain}`;")); assert!(response.contains("return ACCOUNT_RANDOM_NAMES[index];")); - assert!(response.contains("return status === \"selected\" ? 1 : 0;")); assert!(response.contains("return accounts;")); assert!(!response.contains("function codexAccountPoolSortKey(account)")); assert!(!response.contains("return codexAccountPoolSortKey(left).localeCompare(codexAccountPoolSortKey(right));")); @@ -1865,7 +1867,7 @@ fn operator_dashboard_run_activity_preserves_snapshot_detail_fields() { assert!(response.contains("let dashboardLiveCurrentLanes = [];")); assert!(response.contains("let dashboardLiveRunActivitySeen = false;")); assert!(response.contains("let dashboardLiveCurrentLanesComplete = true;")); - assert!(response.contains("let dashboardLiveAccounts = null;")); + assert!(!response.contains("let dashboardLiveAccounts = null;")); assert!(response.contains("let dashboardLiveAccountControl = null;")); assert!(response .contains("function dashboardLiveRunActivityHasOverlay({ includeCompletedEmpty = false } = {})")); @@ -1878,7 +1880,9 @@ fn operator_dashboard_run_activity_preserves_snapshot_detail_fields() { assert!(!response.contains("field(\"Author\",")); assert!(!response.contains("\"author\",\n")); assert!(response.contains("activityPayload.accountControl")); - assert!(response.contains("dashboardLiveAccounts = Array.isArray(payload.accounts)")); + assert!(!response.contains("activityPayload.accounts")); + assert!(!response.contains("payload.accounts")); + assert!(!response.contains("dashboardLiveAccounts")); assert!(response.contains("dashboardLiveAccountControl =")); assert!(response.contains("\"child_agent_activity\"")); assert!(response.contains("\"protocol_activity\"")); @@ -1901,7 +1905,7 @@ fn operator_dashboard_run_activity_preserves_snapshot_detail_fields() { )); assert!(response.contains("clearDashboardLiveRunActivityOverlayIfCompleteEmpty();")); assert!(response.contains("account_control: accountControl,")); - assert!(response.contains("accounts,")); + assert!(!response.contains("accounts: dashboardLiveAccounts")); assert!(response.contains("current_lanes: mergedCurrentLanes,")); assert!(!response.contains("current_lanes: currentLaneRows,")); } @@ -1917,6 +1921,10 @@ fn operator_dashboard_uses_websocket_without_http_state_fallback() { assert!(response.contains("if (document.hidden) {\n\t\t\t\t\treturn;\n\t\t\t\t}")); assert!(response.contains("if (!dashboardSocketIsOpen()) {\n\t\t\t\t\tconnectDashboardSocket();")); assert!(response.contains("function renderDashboardLocalClockTick()")); + assert!(response.contains("const ACCOUNT_API_REFRESH_INTERVAL_MS = 15_000;")); + assert!(response.contains("now - accountApiRefreshedAt < ACCOUNT_API_REFRESH_INTERVAL_MS")); + assert!(response.contains("const response = await fetch(\"/api/accounts?refresh=1\"")); + assert!(response.contains("refreshAccountApiSnapshot();")); assert!(response.contains("renderDashboardState(lastDashboardRender, { refreshAccounts: false });")); assert!(response.contains("const shouldRefreshAccounts = options.refreshAccounts !== false;")); assert!(!response.contains("function scheduleDashboardHttpFallback")); diff --git a/apps/decodex/src/orchestrator/tests/operator/status/http.rs b/apps/decodex/src/orchestrator/tests/operator/status/http.rs index 587120c7b..73f09b3d8 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/http.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/http.rs @@ -1084,12 +1084,12 @@ fn operator_dashboard_run_activity_event_summarizes_current_lanes() { assert_eq!(data["accountControl"]["mode"], "balanced"); assert_eq!(data["currentLanesComplete"], true); assert_eq!(data["currentLaneScope"], "complete"); - assert!(data["accounts"].is_array()); + assert!(data.get("accounts").is_none()); assert!(fingerprint.get("emittedAtUnixEpoch").is_none()); assert_eq!(fingerprint["accountControl"]["mode"], "balanced"); assert_eq!(fingerprint["currentLanesComplete"], true); assert_eq!(fingerprint["currentLaneScope"], "complete"); - assert!(fingerprint["accounts"].is_array()); + assert!(fingerprint.get("accounts").is_none()); assert_eq!(fingerprint["currentLanes"][0]["run_id"], "run-1"); assert_eq!( fingerprint["currentLanes"][0]["project_display_name"],