CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 4s
Build images / sign-extension (push) Successful in 5s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 7s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 34s
Build images / build-web (push) Successful in 59s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m19s
Milestone 422 step 4. Rule 27: no UI, no ship. This is where the previous three steps become usable. REACHABLE AT: Settings -> Activity -> Worker lanes, directly under the "Queues + workers" pane. Written from opening the view, not from memory of having built it — lesson #4282, and #3463 is the same trap landing inside milestone 365, where the System page shipped with no navigation to it. Under that pane deliberately, not in the System tab. System answers "is everything running", where every control would be about something broken. This is about something working that should work harder, and it belongs beside the backlog it reacts to: you watch a queue grow and give that lane another slot without leaving the pane. Per lane: queues, pending, busy, a stepper, an enable switch. - PENDING is depth + reserved, not LLEN. Celery prefetches, so LLEN alone reads 0 while a worker holds tasks in memory — the number that would make someone think a buried lane was idle. - NOT ANSWERING, never "stopped". present=false means nothing replied; saying stopped would send the operator looking for a crash that has not happened. - THE CEILING IS ON SCREEN, with "(memory)" on the ML lane. It is the one number here the operator cannot change, so it has to justify itself; a greyed stepper with no explanation reads as a bug. - `busy` is per lane, so adjusting one does not freeze the others. TWO OUTCOMES THAT MUST NOT COLLAPSE INTO ONE MESSAGE. A stored-but-unpushed change (applied:false — the lane is restarting) is information: the value is saved and the reconcile will carry it, so the card says so and invites waiting. A refused value (400) is an error and shows the endpoint's own sentence. Collapsing them would make one of the two invite the wrong action. A BUG CAUGHT BEFORE COMMIT: the card read `e.detail?.detail`, but ApiError puts the parsed body on `.body` and `.message` on the short `error` key. It would have shown the operator the bare word "refused" with no reason — the exact failure that line exists to prevent. The test would not have caught it either: `rejects.toThrow()` passes whether the sentence is reachable or not. It now asserts `err.body.detail` specifically. Also: queueOptions in SystemActivityTab was a fourth hand-kept copy of the queue list and had drifted — `maintenance_long` was missing, so activity on that lane could not be filtered for at all despite four task routes pointing there. Added, and DELIBERATELY left as a written-out list rather than derived like the other three were: this filters task_run HISTORY, so a derived list would hide the filter for any queue that has rows but no longer has a lane — precisely when someone is looking — and would be empty whenever the endpoint is down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
151 lines
4.9 KiB
JavaScript
151 lines
4.9 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
|
|
import { useApi } from '../composables/useApi.js'
|
|
|
|
export const useSystemActivityStore = defineStore('systemActivity', () => {
|
|
const api = useApi()
|
|
|
|
// Live polled state.
|
|
const queues = ref(null) // { queues: {name: depth|null}, fetched_at }
|
|
const workers = ref(null) // { workers: {hostname: {...}}, fetched_at }
|
|
const recentRuns = ref([]) // last-60s rows (for Overview summary)
|
|
const failures = ref(null) // { recent, count_by_type, since }
|
|
|
|
// Worker lanes (milestone 422): the configured slots joined to the live
|
|
// pool. Lives here rather than in its own store because it is the same
|
|
// domain the queues and workers above describe — a second store polling
|
|
// /api/system/* would be two things to keep in step.
|
|
const lanes = ref(null) // { lanes: [...], fetched_at }
|
|
|
|
// Paginated runs (Activity tab "All recent activity" pane).
|
|
const runs = ref([])
|
|
const runsCursor = ref(null)
|
|
const runsHasMore = ref(false)
|
|
const runsFilter = ref({ queue: null, status: null, task: null, limit: 50 })
|
|
|
|
const loading = ref({
|
|
queues: false, workers: false, runs: false, failures: false, lanes: false,
|
|
})
|
|
const lastError = ref(null)
|
|
|
|
async function loadQueues() {
|
|
loading.value.queues = true
|
|
lastError.value = null
|
|
try {
|
|
queues.value = await api.get('/api/system/activity/queues')
|
|
} catch (e) {
|
|
lastError.value = e.message
|
|
} finally {
|
|
loading.value.queues = false
|
|
}
|
|
}
|
|
|
|
async function loadWorkers() {
|
|
loading.value.workers = true
|
|
lastError.value = null
|
|
try {
|
|
workers.value = await api.get('/api/system/activity/workers')
|
|
} catch (e) {
|
|
lastError.value = e.message
|
|
} finally {
|
|
loading.value.workers = false
|
|
}
|
|
}
|
|
|
|
async function loadLanes() {
|
|
loading.value.lanes = true
|
|
lastError.value = null
|
|
try {
|
|
lanes.value = await api.get('/api/system/workers')
|
|
} catch (e) {
|
|
lastError.value = e.message
|
|
} finally {
|
|
loading.value.lanes = false
|
|
}
|
|
}
|
|
|
|
// Change one lane. Returns the endpoint's reply so the caller can tell a
|
|
// stored-but-not-yet-live change (`applied: false`) from a live one — the
|
|
// difference between "saved, the lane is restarting" and "that failed",
|
|
// which the UI must not collapse into one message.
|
|
//
|
|
// Deliberately NOT swallowing the error: a refused value (400) carries the
|
|
// sentence explaining why, and the card shows it. Returning null on failure
|
|
// would leave the operator with a control that silently did nothing.
|
|
async function setLane(name, fields) {
|
|
const reply = await api.post(`/api/system/workers/${name}`, { body: fields })
|
|
await loadLanes()
|
|
return reply
|
|
}
|
|
|
|
async function loadRecentRuns() {
|
|
// Used by the Overview summary card: pull last 60s of runs to compute
|
|
// per-queue ok/err counts. One call covers all queues; UI groups.
|
|
try {
|
|
const body = await api.get('/api/system/activity/runs', {
|
|
params: { limit: 200 },
|
|
})
|
|
recentRuns.value = body.runs || []
|
|
} catch (e) {
|
|
lastError.value = e.message
|
|
}
|
|
}
|
|
|
|
async function loadRuns({ reset = false } = {}) {
|
|
loading.value.runs = true
|
|
lastError.value = null
|
|
try {
|
|
const params = { limit: runsFilter.value.limit }
|
|
if (runsFilter.value.queue) params.queue = runsFilter.value.queue
|
|
if (runsFilter.value.status) params.status = runsFilter.value.status
|
|
if (runsFilter.value.task) params.task = runsFilter.value.task
|
|
if (!reset && runsCursor.value) params.before_id = runsCursor.value
|
|
const body = await api.get('/api/system/activity/runs', { params })
|
|
runs.value = reset ? body.runs : [...runs.value, ...body.runs]
|
|
runsCursor.value = body.next_cursor
|
|
runsHasMore.value = body.next_cursor !== null
|
|
} catch (e) {
|
|
lastError.value = e.message
|
|
} finally {
|
|
loading.value.runs = false
|
|
}
|
|
}
|
|
|
|
async function loadFailures() {
|
|
loading.value.failures = true
|
|
lastError.value = null
|
|
try {
|
|
failures.value = await api.get('/api/system/activity/failures')
|
|
} catch (e) {
|
|
lastError.value = e.message
|
|
} finally {
|
|
loading.value.failures = false
|
|
}
|
|
}
|
|
|
|
function setFilter(filter) {
|
|
runsFilter.value = { ...runsFilter.value, ...filter }
|
|
}
|
|
|
|
// One-call rollup for the always-on TopNav pipeline indicator:
|
|
// { scheduler, queues, queued_total, running, failing }.
|
|
const summary = ref(null)
|
|
async function loadSummary() {
|
|
try {
|
|
summary.value = await api.get('/api/system/activity/summary')
|
|
} catch (e) {
|
|
lastError.value = e.message
|
|
}
|
|
return summary.value
|
|
}
|
|
|
|
return {
|
|
queues, workers, recentRuns, failures, summary, lanes,
|
|
runs, runsCursor, runsHasMore, runsFilter,
|
|
loading, lastError,
|
|
loadQueues, loadWorkers, loadRecentRuns, loadLanes, setLane,
|
|
loadRuns, loadFailures, loadSummary, setFilter,
|
|
}
|
|
})
|