(MOT-4357) feat(queue): queues and dead-letters page as injectable console UI - #738
(MOT-4357) feat(queue): queues and dead-letters page as injectable console UI#738rohitg00 wants to merge 3 commits into
Conversation
…nsole UI The queue worker ships its own console page at #/ext/queues, through the shared iii-console-ui crate — queues are queue-worker data, so the page appears when the worker connects and leaves with it. Every function the page drives already existed (engine::queue::* reads, iii::durable::publish, iii::queue::redrive / redrive_message / discard_message); this is the missing surface, not new plumbing. The list is a stats table, not a name list: depth, delivered, failed, dead and subscriber counts per topic with alert coloring, fifo topics carrying their ordering key inline. The page auto-selects on open — the topic with dead letters first, else the busiest — so it never opens empty. Topic detail leads with what a topic IS: - the delivery policy from the worker's queue_configs: standard vs fifo with its message-group field (harness-turn is fifo per session_id — the agent turn queue, finally visible), concurrency, retry budget and backoff, timeout, and redeliver-on-engine-restart, with drop rendered as a warning - live subscribers from durable:subscriber registrations, each with its own retry config and condition - an activity tab reading the trace store: publishes onto the topic (attributed via the recorded payload) and deliveries into each subscriber, with duration and age - publish, behind a confirm — it goes through the real queue, so subscribers receive it and retry/DLQ rules apply - dead letters: grouped by error first (forty identical failures are one fact), then paged instances with payload, retries, failure time and size, per-message redrive and discard, redrive-all behind a confirm - adapter gating: redis keeps no DLQ and the tab says so; a builtin store that is not file-backed shows a volatile-store warning in the header, because queued jobs die with a worker restart Live without polling: a stream subscription on the all-spans feed reloads whenever a queue function or a subscriber of the selected topic executes anywhere on the bus, debounced; a refresh control covers the rest. Verified against a running engine end to end: a deliberately failing subscriber dead-lettered real publishes with max_retries honored; redrive sent a message back through the pipeline where it failed again honestly; discard removed it with the engine's dlq_topics agreeing; publish from the page updated the dead-letter count live off the traffic tick.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe queue worker now includes a console UI for topic inspection, publishing, activity monitoring, and dead-letter message management. The UI is bundled by esbuild, embedded into the Rust worker, and registered during worker startup. ChangesQueue console UI
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant ConsoleHost
participant QueuesPage
participant QueueApi
participant QueueWorker
ConsoleHost->>QueuesPage: register queues page
QueuesPage->>QueueApi: load queue data
QueueApi->>QueueWorker: invoke queue functions and trace APIs
QueueWorker-->>QueueApi: return queue data and activity
QueueApi-->>QueuesPage: provide normalized models
QueuesPage->>QueueApi: publish or manage DLQ messages
QueueApi->>QueueWorker: execute queue operation
QueueWorker-->>QueuesPage: return operation result
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
skill-check — worker0 verified, 56 skipped (no docs/).
Four for four. Nicely done. |
Seven findings from review, each verified against the code first. Activity feed: delivery traces were fetched with no topic filter, so a subscriber bound to several topics showed every topic's deliveries in each one's feed. Delivery inputs are the bare message data — the envelope is gone — so the filter is lenient: a span is kept unless its payload carries a `topic` field naming a DIFFERENT topic (publishes keep their exact envelope filter). The per-subscriber trace reads now run through the same 4-wide pool statsForAll uses instead of all at once, and one unreadable subscriber no longer blanks the feed. Live reload: the traffic handler JSON.stringify'd every stream frame to substring-match it, and its trailing debounce reset on every matching frame — continuous traffic postponed the reload forever, exactly when the page matters most. Frames now have their span names read directly off the envelope, and the debounce carries a 2.5s ceiling. Selection: the load fallback re-selected the first topic on EVERY reload, overriding a deliberately cleared selection on the next traffic tick. The dead-letters-else-first pick now happens once, on the first populated load. Smaller: activity rows key on the index too, so same-instant events from one function render distinctly; subtree_older_than checks the directory's own mtime, catching deletions the per-entry walk missed; the build script declares rerun-if-env-changed for SKIP_UI_BUILD and PNPM alongside the file directives.
…nnected consumers On engine 0.22.1 `list_topics.subscriber_count` (and topic_stats `consumer_count`) read 0 for idle durable subscribers, so every topic showed subs 0 while the detail panel — which counts durable:subscriber REGISTRATIONS — showed the same topic with live subscribers. Registrations are what an operator means by "does anything consume this topic": the table now overlays a per-topic registration count (one registered-triggers read per load, shared with the detail panel's fetch) and shows whichever of the two figures is higher, so a future engine that reports consumers again is not undercut. The other zeros after a restart are honest: the builtin store was running in memory, and the VOLATILE STORE badge exists for exactly that conversation.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (10)
queue/ui/styles.css (1)
125-132: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThese selectors match nothing.
queue/ui/src/page/index.tsxrenders no.metaor.deadelement inside.queue-ui-row. The row contains.c-name,.name,.fifo, and.c-nonly..metaappears under.queue-ui-dead-head, which Line 241 already styles. Remove both rules.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/styles.css` around lines 125 - 132, Remove the unused .meta and .dead rules from the queue row styles in the stylesheet; queue/ui/src/page/index.tsx renders neither class within .queue-ui-row, and .meta is already styled by the existing queue-ui-dead-head rule.queue/ui/src/page/index.tsx (7)
444-447: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
defaultValueapplies only on the first mount.
TopicDetailhas nokeyat the call site (Line 342), so it does not remount when the operator selects another topic.Tabskeeps the tab that was already open and ignores the newdeadCount > 0 ? 'dead' : 'overview'value. A topic with dead letters therefore does not open on the dead-letter tab after the first selection. If the intent is to re-apply the rule per topic, addkey={topic}toTopicDetail.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/src/page/index.tsx` around lines 444 - 447, Add key={topic} to the TopicDetail component usage so it remounts when the selected topic changes, allowing Tabs defaultValue to reapply the deadCount-based initial tab for each topic.
870-872: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueDisable the
redrive alltrigger while an action runs.Every other action button checks
busyId !== null. This button does not. The operator can open the confirm step while a per-message redrive is in flight.♻️ Proposed change
- <Button size="sm" onClick={() => setConfirmingAll(true)}> + <Button + size="sm" + disabled={busyId !== null} + onClick={() => setConfirmingAll(true)} + > redrive all </Button>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/src/page/index.tsx` around lines 870 - 872, Update the “redrive all” Button near setConfirmingAll to be disabled whenever busyId is not null, matching the other action buttons and preventing the confirmation flow from opening during an active per-message redrive.
619-633: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSubscriber array identity churn re-runs the activity read on every detail reload.
loaddepends onsubscribers.TopicDetailcallssetSubscribers(subs)on everyloadDetailrun, andsubscribersForalways builds a new array. The identity therefore changes even when the subscriber set is unchanged. Each change recreatesloadand re-firesuseEffect(load), which issues oneengine::traces::listcall per subscriber plus the publish read.loadDetailitself runs on every debounced traffic burst, so activity reads multiply.Depend on a content-derived key instead, or skip the state update in
TopicDetailwhen the subscriber list is unchanged.♻️ Proposed change
+ const subsKey = useMemo( + () => subscribers.map((s) => s.functionId).join('\u0000'), + [subscribers], + ) + const subsRef = useRef(subscribers) + subsRef.current = subscribers const load = useCallback(() => { - recentActivity(host, topic, subscribers).then( + recentActivity(host, topic, subsRef.current).then( (rows) => { setEvents(rows) setError(null) }, (err: unknown) => setError(errorMessage(err)), ) - }, [host, topic, subscribers]) + }, [host, topic, subsKey])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/src/page/index.tsx` around lines 619 - 633, Prevent subscriber array identity churn from recreating the activity loader: update the load callback and related memoization in TopicDetail to depend on a stable content-derived subscriber key, or avoid calling setSubscribers when the list is unchanged. Preserve subscriber-driven watched IDs while ensuring repeated loadDetail runs do not re-fire recentActivity unnecessarily.
191-219: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConcurrent
loadcalls can commit stale results.
loadruns on mount, on the refresh button, and on every debounced traffic burst. Nothing cancels an in-flight run. If an earlier run resolves after a later run, it overwritestopics,dlq,setup, andstatsByTopicwith older data. Theawait statsForAll(...)on Line 213 widens the window, because that step issues one request per topic before it commits.Add a sequence guard.
♻️ Proposed change
const pickedRef = useRef(false) + const loadSeq = useRef(0) const load = useCallback(() => { + const seq = ++loadSeq.current Promise.all([ listTopics(host), dlqTopics(host), queueSetup(host), subscriberCounts(host).catch(() => new Map<string, number>()), ]).then( async ([topicRows, dlqRows, setupValue, counts]) => { + if (seq !== loadSeq.current) return setTopics(topicRows)Apply the same check again after the
statsForAllawait, beforesetStatsByTopic.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/src/page/index.tsx` around lines 191 - 219, Update the load callback to use a monotonically increasing request sequence and capture the current sequence for each invocation. Before committing topics, DLQ, setup, counts, or error results, ignore the result when its sequence is no longer current; apply the same guard after the await statsForAll call and before setStatsByTopic so stale statistics cannot overwrite newer data.
566-567: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
functionIdmay not be unique per topic.
subscribersForreturns one entry per registration. If the same function registers twice for the topic, two entries sharesub.functionIdand React reports duplicate keys. Include the index or the worker name in the key.♻️ Proposed change
- subscribers.map((sub) => ( - <div key={sub.functionId} className="queue-ui-subscriber"> + subscribers.map((sub, i) => ( + <div key={`${sub.functionId}-${sub.worker}-${i}`} className="queue-ui-subscriber">🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/src/page/index.tsx` around lines 566 - 567, Update the subscriber list rendering in the subscribers.map callback to use a key that is unique for each registration, rather than relying solely on sub.functionId. Include the map index or another available unique worker/registration identifier while preserving the existing subscriber content and rendering behavior.
852-860: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReport the redrive-all count.
redriveAllreturns the number of messages that went back onto the main queue, butactdiscards it and shows a fixed label. Show the count so the operator can confirm the effect.♻️ Proposed change
onClick={() => - act('redrove all messages', () => redriveAll(host, topic), null) + act( + (n) => `redrove ${n} messages`, + () => redriveAll(host, topic), + null, + ) }Change
actto accept a label builder that receives the resolved value.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/src/page/index.tsx` around lines 852 - 860, Update the redrive-all action around redriveAll and act so the resolved count is preserved and displayed in the button or action feedback instead of using a fixed label. Change act to accept a label builder receiving redriveAll’s returned count, and format the message to report how many messages were returned to the main queue.
107-163: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffFour components subscribe to the same span stream independently.
QueuesPage(Line 219),TopicDetail(Line 402),ActivityPanel(Line 633), andDlqPanel(Line 786) each calluseQueueTraffic. Each call registers its owniii:devtools:all-spansstream trigger and runs its own debounce timer. A single burst of queue traffic therefore triggers four independent reload chains, and theQueuesPagechain alone issueslistTopics,dlqTopics,queueSetup,subscriberCounts, plus onetopic_statscall per topic.Consider one shared subscription at the page level that fans out to registered listeners through a context.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/src/page/index.tsx` around lines 107 - 163, Refactor useQueueTraffic into a page-level shared subscription that registers one iii:devtools:all-spans trigger and one debounced reload chain, then expose listener registration through context for QueuesPage, TopicDetail, ActivityPanel, and DlqPanel. Update each component to consume the shared context instead of independently invoking useQueueTraffic, preserving their existing traffic filtering and reload behavior.queue/ui/src/page/api.ts (2)
380-400: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueMake lenient matching order independent.
In lenient mode
matchescan flip back tofalseafter an exact hit. If a span carries more than oneiii.invocation.inputevent, or more than oneiii.payload.jsonattribute, and a later payload names a different topic, the span is dropped even though an earlier payload matchedtopicFilter. Stop the scan once an exact match is found.♻️ Proposed change
const eventsAttr = Array.isArray(span.events) ? span.events : [] let matches = lenient + let exact = false for (const entry of eventsAttr) { if (!isRecord(entry) || entry.name !== 'iii.invocation.input') continue for (const attr of Array.isArray(entry.attributes) ? entry.attributes : []) { if (!Array.isArray(attr) || attr[0] !== 'iii.payload.json') continue try { const payload = JSON.parse(String(attr[1])) if (!isRecord(payload)) continue if (payload.topic === topicFilter) { matches = true + exact = true } else if (lenient && typeof payload.topic === 'string') { matches = false } } catch { // Unparseable payload: not a match. } + if (exact) break } + if (exact) break }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/src/page/api.ts` around lines 380 - 400, Update the matching loops around the `matches` variable so an exact `payload.topic === topicFilter` match is terminal: stop scanning further attributes and events once found, and prevent later non-matching payloads from resetting `matches` to false. Preserve lenient fallback behavior when no exact match exists.
224-226: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueConsider surfacing a failed configuration read.
The catch treats every failure as "worker absent". A transport error then leaves
adapter: 'builtin'and nostoreMethod, which makesindex.tsxrender the "volatile store" warning badge for an adapter that was never read. Returning an optionalunknown: trueflag would let the page suppress the badge when the configuration is unknown.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@queue/ui/src/page/api.ts` around lines 224 - 226, Update the configuration-read catch handling in the surrounding API function to distinguish an absent configuration worker from a failed read, returning an optional unknown: true flag for transport or other read errors while preserving builtin defaults. Update the consuming logic in index.tsx to suppress the volatile-store warning badge whenever the configuration is marked unknown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@queue/ui/src/page/api.ts`:
- Around line 429-436: Update the publishRead request to pass the topic filter
directly to engine::traces::list using the activity-read filter supported by the
engine, while retaining the existing name, limit, and include_internal options.
Keep spanEvents focused on transforming the already-filtered results rather than
relying on client-side topic filtering.
In `@queue/ui/src/page/index.tsx`:
- Around line 444-447: In queue/ui/src/page/index.tsx, add key={selected} to the
TopicDetail rendered around lines 342 so the detail subtree remounts whenever
the topic changes; this root fix covers both affected sites: the Tabs
defaultValue at lines 444-447 and DlqPanel state around lines 776-786 require no
direct changes.
- Around line 300-310: Update the topic row button in the selection handler
around selected and topic.name to include aria-pressed={selected ===
topic.name}, preserving the existing data-selected styling and toggle behavior
so assistive technology can detect the active topic.
In `@queue/ui/styles.css`:
- Around line 73-89: Add explicit :focus-visible styles for the queue row button
selector .queue-ui-row and the dead-head selector .queue-ui-dead-head, using a
clearly visible focus indicator while preserving their existing layout and hover
styling.
---
Nitpick comments:
In `@queue/ui/src/page/api.ts`:
- Around line 380-400: Update the matching loops around the `matches` variable
so an exact `payload.topic === topicFilter` match is terminal: stop scanning
further attributes and events once found, and prevent later non-matching
payloads from resetting `matches` to false. Preserve lenient fallback behavior
when no exact match exists.
- Around line 224-226: Update the configuration-read catch handling in the
surrounding API function to distinguish an absent configuration worker from a
failed read, returning an optional unknown: true flag for transport or other
read errors while preserving builtin defaults. Update the consuming logic in
index.tsx to suppress the volatile-store warning badge whenever the
configuration is marked unknown.
In `@queue/ui/src/page/index.tsx`:
- Around line 444-447: Add key={topic} to the TopicDetail component usage so it
remounts when the selected topic changes, allowing Tabs defaultValue to reapply
the deadCount-based initial tab for each topic.
- Around line 870-872: Update the “redrive all” Button near setConfirmingAll to
be disabled whenever busyId is not null, matching the other action buttons and
preventing the confirmation flow from opening during an active per-message
redrive.
- Around line 619-633: Prevent subscriber array identity churn from recreating
the activity loader: update the load callback and related memoization in
TopicDetail to depend on a stable content-derived subscriber key, or avoid
calling setSubscribers when the list is unchanged. Preserve subscriber-driven
watched IDs while ensuring repeated loadDetail runs do not re-fire
recentActivity unnecessarily.
- Around line 191-219: Update the load callback to use a monotonically
increasing request sequence and capture the current sequence for each
invocation. Before committing topics, DLQ, setup, counts, or error results,
ignore the result when its sequence is no longer current; apply the same guard
after the await statsForAll call and before setStatsByTopic so stale statistics
cannot overwrite newer data.
- Around line 566-567: Update the subscriber list rendering in the
subscribers.map callback to use a key that is unique for each registration,
rather than relying solely on sub.functionId. Include the map index or another
available unique worker/registration identifier while preserving the existing
subscriber content and rendering behavior.
- Around line 852-860: Update the redrive-all action around redriveAll and act
so the resolved count is preserved and displayed in the button or action
feedback instead of using a fixed label. Change act to accept a label builder
receiving redriveAll’s returned count, and format the message to report how many
messages were returned to the main queue.
- Around line 107-163: Refactor useQueueTraffic into a page-level shared
subscription that registers one iii:devtools:all-spans trigger and one debounced
reload chain, then expose listener registration through context for QueuesPage,
TopicDetail, ActivityPanel, and DlqPanel. Update each component to consume the
shared context instead of independently invoking useQueueTraffic, preserving
their existing traffic filtering and reload behavior.
In `@queue/ui/styles.css`:
- Around line 125-132: Remove the unused .meta and .dead rules from the queue
row styles in the stylesheet; queue/ui/src/page/index.tsx renders neither class
within .queue-ui-row, and .meta is already styled by the existing
queue-ui-dead-head rule.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 87718ad0-6466-45ff-8306-d04132a47ce1
⛔ Files ignored due to path filters (2)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlqueue/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (13)
pnpm-workspace.yamlqueue/Cargo.tomlqueue/build.rsqueue/src/boot.rsqueue/src/lib.rsqueue/src/ui.rsqueue/ui/build.mjsqueue/ui/package.jsonqueue/ui/page.tsxqueue/ui/src/page/api.tsqueue/ui/src/page/index.tsxqueue/ui/styles.cssqueue/ui/tsconfig.json
| const publishRead = host.iii | ||
| .trigger('engine::traces::list', { | ||
| name: 'execute iii::durable::publish', | ||
| limit: 60, | ||
| include_internal: true, | ||
| }) | ||
| .then((out) => spanEvents(out, 'publish', topic)) | ||
| .catch((): QueueEvent[] => []) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check which filter parameters engine::traces::list accepts.
rg -n -C 10 'traces::list' --iglob '*.rs'Repository: iii-hq/workers
Length of output: 152
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repository files matching api.ts:"
fd -a 'api\.ts$' . || true
echo
echo "Git tree files under queue/ui/src/page:"
git ls-files queue/ui/src/page 2>/dev/null || true
echo
echo "Search trace API identifiers across tracked files:"
rg -n -C 6 'engine::traces::list|traces::list|spanEvents|ii::traces|traces' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true
echo
echo "Find host.iii definitions/usages:"
rg -n -C 4 '\.iii|Host|host|IHost|h\.iii|host\.iii' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || trueRepository: iii-hq/workers
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Exact tracked references to traces::list (case sensitive):"
rg -n 'traces::list' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true
echo
echo "Exact tracked references to engine::traces::list (case sensitive):"
rg -n 'engine::traces::list' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true
echo
echo "Exact tracked references to spanEvents:"
rg -n 'spanEvents' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true
echo
echo "Files matching api.ts:"
fd -a 'api\.ts$' . | sed -n '1,20p'
echo
echo "Search only by file paths matching api.ts and inspect relevant files if any:"
while IFS= read -r file; do
echo "--- $file ---"
wc -l "$file"
rg -n -C 8 'traces::list|spanEvents|limit: 60|limit: 40|topic' "$file" || true
done < <(fd 'api\.ts$' .)Repository: iii-hq/workers
Length of output: 4457
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "queue/ui/src/page/api.ts: spanEvents and recentActivity sections:"
sed -n '340,470p' queue/ui/src/page/api.ts
echo
echo "queue/ui/src/page/index.tsx traces read/readError sections:"
sed -n '580,650p' queue/ui/src/page/index.tsx
echo
echo "TracesV2 harness canned engine::traces::list support:"
sed -n '45,85p' console/web/src/pages/TracesV2/stories/harness.tsx
echo
echo "TracesV2 api filters:"
sed -n '120,150p' console/web/src/pages/TracesV2/api/traces.tsRepository: iii-hq/workers
Length of output: 8637
Move the topic filter into the traces request.
engine::traces::list returns the newest spans for the given name; this call does not filter spans by topic, then spanEvents(out, 'publish', topic) filters client-side. A busy engine can return spans from other topics before this one, so low-traffic topics can show no publishes. Add a topic filter if the engine supports it for this activity read.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@queue/ui/src/page/api.ts` around lines 429 - 436, Update the publishRead
request to pass the topic filter directly to engine::traces::list using the
activity-read filter supported by the engine, while retaining the existing name,
limit, and include_internal options. Keep spanEvents focused on transforming the
already-filtered results rather than relying on client-side topic filtering.
| <button | ||
| key={topic.name} | ||
| type="button" | ||
| className="queue-ui-row" | ||
| data-selected={selected === topic.name} | ||
| onClick={() => | ||
| setSelected((prev) => | ||
| prev === topic.name ? null : topic.name, | ||
| ) | ||
| } | ||
| > |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Expose the row selection state to assistive technology.
The row button conveys selection only through data-selected, which styles the row but is not announced. Add aria-pressed={selected === topic.name} so screen reader users can tell which topic is open.
♻️ Proposed change
type="button"
className="queue-ui-row"
data-selected={selected === topic.name}
+ aria-pressed={selected === topic.name}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| key={topic.name} | |
| type="button" | |
| className="queue-ui-row" | |
| data-selected={selected === topic.name} | |
| onClick={() => | |
| setSelected((prev) => | |
| prev === topic.name ? null : topic.name, | |
| ) | |
| } | |
| > | |
| <button | |
| key={topic.name} | |
| type="button" | |
| className="queue-ui-row" | |
| data-selected={selected === topic.name} | |
| aria-pressed={selected === topic.name} | |
| onClick={() => | |
| setSelected((prev) => | |
| prev === topic.name ? null : topic.name, | |
| ) | |
| } | |
| > |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@queue/ui/src/page/index.tsx` around lines 300 - 310, Update the topic row
button in the selection handler around selected and topic.name to include
aria-pressed={selected === topic.name}, preserving the existing data-selected
styling and toggle behavior so assistive technology can detect the active topic.
| <Tabs | ||
| defaultValue={deadCount > 0 ? 'dead' : 'overview'} | ||
| className="queue-ui-tabs" | ||
| > |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Per-topic child state persists across topic switches in queue/ui/src/page/index.tsx. QueuesPage renders TopicDetail at Line 342 without a key. React reuses the same component instances when selected changes, so uncontrolled child state carries over from the previous topic. Adding key={selected} to TopicDetail resets the whole detail subtree per topic and fixes both sites.
queue/ui/src/page/index.tsx#L444-L447: theTabsdefaultValueruledeadCount > 0 ? 'dead' : 'overview'never re-applies, so a topic with dead letters does not open on the dead-letter tab after the first selection.queue/ui/src/page/index.tsx#L776-L786:DlqPanelkeeps its previouspage, so a topic with fewer dead letters renders an empty panel with only pagination controls, because the empty-state branch on Line 827 requirespage === 0.
📍 Affects 1 file
queue/ui/src/page/index.tsx#L444-L447(this comment)queue/ui/src/page/index.tsx#L776-L786
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@queue/ui/src/page/index.tsx` around lines 444 - 447, In
queue/ui/src/page/index.tsx, add key={selected} to the TopicDetail rendered
around lines 342 so the detail subtree remounts whenever the topic changes; this
root fix covers both affected sites: the Tabs defaultValue at lines 444-447 and
DlqPanel state around lines 776-786 require no direct changes.
| [data-iii-ui="queue"] .queue-ui-thead, | ||
| [data-iii-ui="queue"] .queue-ui-row { | ||
| display: grid; | ||
| grid-template-columns: minmax(0, 1fr) 64px 76px 64px 56px 52px; | ||
| align-items: baseline; | ||
| gap: 8px; | ||
| width: 100%; | ||
| padding: 8px 10px; | ||
| border: 0; | ||
| border-radius: 6px; | ||
| background: transparent; | ||
| color: var(--color-ink); | ||
| font: inherit; | ||
| font-size: 13px; | ||
| text-align: left; | ||
| cursor: pointer; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a visible focus style for the row button.
.queue-ui-row is a <button> with border: 0 and a transparent background. Only :hover is styled. Keyboard users get whatever the console default outline provides, and the same applies to .queue-ui-dead-head at Line 216. Add an explicit :focus-visible rule so keyboard navigation stays visible.
♻️ Proposed change
[data-iii-ui="queue"] .queue-ui-row:hover {
background: var(--color-surface-hover, rgba(0, 0, 0, 0.08));
}
+[data-iii-ui="queue"] .queue-ui-row:focus-visible,
+[data-iii-ui="queue"] .queue-ui-dead-head:focus-visible {
+ outline: 2px solid var(--color-rule-focus, var(--color-accent));
+ outline-offset: -2px;
+}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@queue/ui/styles.css` around lines 73 - 89, Add explicit :focus-visible styles
for the queue row button selector .queue-ui-row and the dead-head selector
.queue-ui-dead-head, using a clearly visible focus indicator while preserving
their existing layout and hover styling.
What
The queue worker ships its own console page at
#/ext/queuesthrough the sharediii-console-uicrate: topics, delivery policies, subscribers, live movement, publish, and the dead-letter queue with redrive/discard. Every function the page drives already existed on the worker — this is the missing surface, not new plumbing.The page
List — a stats table, not a name list: depth, delivered, failed, dead, subscribers per topic with alert coloring; fifo topics carry their ordering key inline (
fifo · session_id). The page auto-selects on open — dead-lettered topic first, else the busiest — so it never opens empty.Overview — what a topic IS:
queue_configs: standard vs fifo with its message-group field (harness-turnis fifo persession_id— the agent turn queue, now visible), concurrency, retry budget and backoff, timeout, redeliver-on-engine-restart (drop renders as a warning)durable:subscriberregistrations, each with its own retry config and conditionActivity — movement from the trace store: publishes onto the topic (attributed via the recorded payload) and deliveries into each subscriber, with duration and age.
Publish — behind a confirm; it goes through the real queue, so every subscriber receives it and retry/DLQ rules apply.
Dead letters — grouped by error first (forty identical failures are one fact), then paged instances with payload, retries, failure time, size; per-message redrive and discard, redrive-all behind a confirm.
Honest gating — redis keeps no DLQ and the tab says so instead of rendering an empty table; a builtin store that is not file-backed shows a
volatile storeheader warning, because queued jobs die with a worker restart.Live, no polling
A stream subscription on
iii:devtools:all-spansreloads whenever a queue function or a subscriber of the selected topic executes anywhere on the bus — a publish from chat, a redrive from the CLI, a consumer failing — debounced; a refresh control covers the rest.Verification
End to end against a running engine: a deliberately failing
durable:subscriberdead-lettered real publishes withmax_retrieshonored; redrive sent a message back through the pipeline where the consumer failed it again (failed count ticked up — the real path, not a UI trick); discard removed it withengine::queue::dlq_topicsagreeing; publish from the page moved the dead-letter badge live off the traffic tick. Both themes.cargo fmt --check,cargo clippy --all-targets -D warnings,cargo test(4 UI asset tests pin the page id and driven function ids), biome,tsc+ esbuild green.Wire notes for review:
engine::queue::list_topicsanddlq_topicsreturn bare arrays, not envelopes; the write functions live underiii::queue::*/iii::durable::*.Linear: MOT-4357.
Summary by CodeRabbit