Skip to content

feat(console): let an administrator work a long queue - #151

Merged
TheMeinerLP merged 1 commit into
mainfrom
feat/console-queue-rework
Aug 23, 2026
Merged

feat(console): let an administrator work a long queue#151
TheMeinerLP merged 1 commit into
mainfrom
feat/console-queue-rework

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Contributor

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 priority that 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:

  • "The order this server's work will run in" — a session with jobs still queued or running. Shown in (priority, id), which is JobQueue.claim's own ORDER BY said in TypeScript. Reorderable, and the quick actions sit above it.
  • "Listed, but not in the queue"priority present 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. orderQueueSessions ranks 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 UiPagination and 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_SIZE uses, for two reasons. A row on /recordings is 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 a Grab produces one Placement — 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.

UiDisclosureList is 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 409 with the queue as it now stands attached. That body is the whole point of asking for it, so the request goes out with ignoreResponseError and 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 no order in 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":

Somebody else moved something in this queue while that was in the air, so nothing was written. The list below is the order as it now stands — move it again if it still needs moving.

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 ConsentCard established, 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:

  • it reaches every outstanding session in the server, including any the list was cut short of showing;
  • nothing is moved forward, so it holds work back rather than speeding anything up — and behind every other server's work as well, not only this server's;
  • pressing the other button afterwards does not undo it; it writes an order of its own on top of the numbers this one leaves behind.

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

queueChannelLabel answered Channel 1240377558927872021 and 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 ask channelNaming, 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. queueChannelNote stays, 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 under recordings.* when the Queue borrows it — one absence, one translation — and i18n/README.md records 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 the isQueueStreamFinished check 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 terms admin.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/queue and moves when the sweep reaches it. New strings on that page go through $t from now on. i18n/README.md says so, and the German is impersonal, umlauted and free of Sie.

Checks

Measured on main before 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), and adminQueuePage.spec.ts gains 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.

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
TheMeinerLP force-pushed the feat/console-queue-rework branch from bb581bd to 304bc67 Compare August 23, 2026 15:36
@TheMeinerLP
TheMeinerLP merged commit f32d46f into main Aug 23, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant