feat(console): let an administrator work a long queue - #151
Merged
Conversation
The list of sessions was the one part of this page that was not a section: a bare heading, three loose paragraphs and a stack of cards, growing until there was nothing to navigate by. It is now two sections, split by the one distinction that decides what can be done to a row -- whether the API gave it a priority. Both are paged five rows at a time, because a row here is a paragraph rather than a line and twenty of them is the complaint. The queued half can be reordered. A row is picked up, moved and put down, and the arrow keys and a mouse drag produce the same grab and therefore the same request -- one per gesture, expressed relative to a neighbour. Home and End send a session to the front or the back of the whole queue, across page boundaries a mouse drag cannot cross, and the handle carries that sentence as its own description rather than leaving it to be discovered. The two quick actions say what they will do before they do it: that they reach sessions the list was cut short of showing, that nothing is ever moved forward so this holds work back globally, and that the other button does not undo them. Shortest-first additionally says that an unmeasured recording ranks last, which is why it does very little on a queue of fresh sessions. A 409 is not an error. Somebody else moved something, nothing was written, and the body carries the queue as it now stands -- so the request asks for that body, redraws from it, and says so in a sentence. The rows follow #141 at last: an unresolved channel reads as an absence with its id demoted, rather than eighteen digits set in semibold as a heading.
TheMeinerLP
force-pushed
the
feat/console-queue-rework
branch
from
August 23, 2026 15:36
bb581bd to
304bc67
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the Queue page usable when a server actually has a backlog. The session list becomes two paged sections, the queued half can be reordered by dragging or by keyboard, and two quick actions re-rank the whole server after saying what that will cost.
The design argument
The complaint was that the page lacks sections and gets too long. Both are true, and they are the same fault seen from two angles.
Every part of this page lived in a bordered
<section>under an<h2>— the lifecycle counts, the three figures somebody has to act on, the all-clear state — except the one part that grows without limit. The list of sessions was a bare<h2>, three loose paragraphs and a<div>of twenty cards. So a long queue read as an endless page with nothing to navigate by, and there was nowhere to put a pager or a reordering control without hanging it off a<div>beside the cards it belonged to.That is what "missing sections" turned out to mean, and it is not the answer I expected to find. I read the page and its history — #106 built the list, #117 rewrote the watching half, and neither ever put the list in a container — and compared it against the other five pages, every one of which wraps its lists in a section. The queue page is the outlier, and it is the one page where the list is the point.
The split is not by length, it is by what can be done to a row. #145 sends each session a
prioritythat is present-and-null when nothing is outstanding, and says plainly that a null row must not offer a drag handle. That is not a rendering detail; it is a second kind of row that had been sitting in the same list all along:(priority, id), which isJobQueue.claim's ownORDER BYsaid in TypeScript. Reorderable, and the quick actions sit above it.prioritypresent and null. A meeting being recorded at this moment, or one nothing will move on without a person. Shown in the order that ranks attention, and carrying no handle at all — not a disabled one. A control that cannot exist is not offered greyed out; the section says in prose that there is nothing to move.One list could not have done both.
orderQueueSessionsranks rows by what a reader can still do about them, which is the right order for triage and a meaningless one to drag in — a list sorted by somebody's attention cannot express "this meeting runs before that one", so a session dropped in it lands somewhere the queue does not have.Pagination, and why five
Both sections page at five, through
UiPaginationand therefore through~/utils/paging— the numbers to offer, the gaps and how many pages a total divides into were all decided once for the recordings list and are not decided again.Five rather than the twenty
PAGE_SIZEuses, for two reasons. A row on/recordingsis a line; a row here is a heading, a sentence saying what will happen next without anybody doing anything, four counts and a link. And the API cuts the list at twenty sessions, so a page of twenty is always page one of one — a pager that never appears is not a pager.A keyboard path that is not an afterthought
A control reachable only with a mouse is not a control, so the keyboard is not the fallback path here — it is the same path.
A row is picked up, moved and put down. Enter or Space on the handle picks it up; Up and Down move it; Home sends it to the front and End to the back; Enter puts it down; Escape leaves it where it was. A mouse drag and a keystroke both produce the same
Grab, and aGrabproduces onePlacement— so there is no second implementation to drift, and no move a mouse can make that a keyboard cannot. The reverse is not true: Home and End cross page boundaries, and a mouse drag is confined to the page it started on. The window follows the held row rather than the row being confined to the window, so "put this at the front" is not the one move the keyboard cannot make.One request per gesture, not one per keystroke. Four arrow presses to move a session four places would otherwise be four writes, and the three orders in between are ones nobody asked the queue to be in even briefly. A row dropped where it started sends nothing at all: the server would answer
changed: []and the page would have to word "nothing to do" after the fact — not asking is truer and cheaper.The handle carries the whole instruction as its
aria-describedby, so it is read out on focus and on screen for everybody else, and a live region says what was picked up and where it is now after every keystroke.UiDisclosureListis deliberately not the base. It owns two things this list must not have. A disclosure — and nothing on a queue row is hidden, because the page argues at length that a row's state has to be readable without hovering. And a selection that survives a page change, which is exactly the property that must not be reused: the position a row is being dragged to is meaningless on a page the reader has left. Teaching it to reorder would also push a queue-only concern into a control two other pages share. This is an<ol>, which is what a list whose order is its content already is.The two things the server does that the interface must not hide
Priorities are never lowered. Going first is expressed as everything ahead going second, so a reorder changes the displayed numbers of rows nobody touched. That is the feature working, and it is also exactly what a reorder looks like when it is broken — so the sentence after a move says it out loud: "going first is expressed as everything that was ahead going second, so rows nobody touched carry new numbers too."
A 409 is an ordinary outcome. Two administrators reordering at once serialise and the loser is answered
409with the queue as it now stands attached. That body is the whole point of asking for it, so the request goes out withignoreResponseErrorand the shape of the reply decides what happened: an order is an order whether or not it was accepted, and everything else is a failure with a status to report.{"error": "no such session"}has noorderin it, and reading that as an empty queue would redraw the page as though every session had left at once.It reads as a sentence, in the muted-cyan tone, never as an error and never using the word "failed":
The quick actions say what they will do
Neither is undone by pressing the other, so neither acts on the press. Each opens an inline confirmation where the button is — the shape
ConsentCardestablished, because a panel that appears somewhere the reader is not looking is a panel that gets confirmed without being read — and states three things a button cannot show:Shortest-first carries a fourth, and it is the one that decides whether the rule is worth choosing at all. Null is not zero: a recording nothing has ever measured has no length to rank by, goes after every measured one and keeps the place it had. Most sessions are unmeasured until they have been transcribed once — so "on a queue of fresh recordings this changes very little; it earns its place on a queue of re-queued ones", said where somebody is choosing it rather than discovered afterwards.
The rows follow #141 at last
queueChannelLabelansweredChannel 1240377558927872021and the page set it as a row's heading in semibold — the exact string #141 named as the complaint. Nobody has a meeting called that. It is gone; the rows askchannelNaming, which is the console's single answer to "how is an unresolved snowflake presented": the heading says there is no name, in the muted role and in normal weight and italic, and the id goes underneath as a subordinate line.queueChannelNotestays, because a recordings row shows the absence and an administrator reading a backlog is additionally owed the reason for it.channelNaming's parameter widened to the two fields it actually reads, so there is one implementation rather than two. Its sentence stays keyed underrecordings.*when the Queue borrows it — one absence, one translation — andi18n/README.mdrecords why that exception is a sentence a decision owns rather than one a page owns.What it keeps
Everything #117 settled. The
EventSource, the polling fallback, the honest "which mode am I in" sentence in its live region, and theisQueueStreamFinishedcheck that stopped a dead handle from looking like a working one — untouched, and their tests untouched. The four lifecycle figures with their scope note, the three attention figures with the caveat that makes each honest, the truncation notice (now above both sections, because the cut is made before either exists), the manual Refresh, and the guild switcher with its remembered choice.The page still offers no re-queue control. Whether a redo is safe is decided once, on the recording page.
Where the decisions live
Two new pure modules, and one component that is only the part a browser is needed for.
~/utils/queueReorder— grab, move, drop, and the placement that comes out. Lists of ids and nothing else. Its strongest test asserts the property that ties the two halves together: for every session and every landing position, the order the preview showed and the order the placement produces are the same list.~/utils/queueOrder— the endpoints' answer, what it means, and what each quick action states before it runs.QueueOrderPanel.vue— which row the pointer is over, where focus goes when a row is rebuilt on another page, and when to send the one request a gesture makes.QueueSessionRow.vue— one row, rendered by both sections. Extracted because a row rendered twice from two copies of the same markup stops agreeing with itself, and the first thing to diverge would be the state badge, which is the one thing on the row that decides what somebody does next.i18n
admin.queue.*is now half-populated, on the same termsadmin.consents.*is. Everything the reordering work added is keyed in both files — the sections, the handle and what it announces, the quick actions and their consequences. The page's older prose (the lifecycle notes, the caveats, the per-row state sentences) stays hard-coded English in~/utils/queueand moves when the sweep reaches it. New strings on that page go through$tfrom now on.i18n/README.mdsays so, and the German is impersonal, umlauted and free ofSie.Checks
Measured on
mainbefore touching anything, and again after: lint clean → clean; 51 files / 1 578 tests → 54 files / 1 640 tests, all passing; typecheck clean → clean; build clean → clean.Three new specs (
queueReorder,queueOrder,queueOrderPanel), andadminQueuePage.spec.tsgains the real locale files and three cases about which section a row lands in — including that a null-priority row gets no handle, and that a nameless channel is an absence rather than a heading full of digits.