Skip to content

feat(console): let an administrator set a guild up without leaving the browser - #154

Open
TheMeinerLP wants to merge 1 commit into
mainfrom
feat/console-onboarding-ui
Open

feat(console): let an administrator set a guild up without leaving the browser#154
TheMeinerLP wants to merge 1 commit into
mainfrom
feat/console-onboarding-ui

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Contributor

Adds Server Setup (/admin/onboarding), the page that drives the three routes #149 shipped. Until now a guild was set up by running /setup in Discord or by calling GET /api/invite, POST /api/guilds/{id}/setup and GET /api/guilds/{id}/setup by hand — the API existed and nothing in the console reached it.

What it does

Four steps, in the order somebody actually performs them.

1. Put the bot in the server. The authorize link from GET /api/invite, opened in a new tab. When url is null — a deployment with no STURNUS_DISCORD_CLIENT_ID — the page says that is a configuration fact rather than a fault and hands over the two values the API still sends, which are exactly what somebody ticks in Discord's own URL generator instead. No dead link is ever offered in place of a working one.

2. Choose the server. The same guild switcher Bot Settings, Destinations and the Queue use, remembered choice and stored key included, so switching servers on one admin page carries to the others. An empty list is not a dead end here the way it is on every other admin page — it is the ordinary middle of this flow, so it says a server appears once the bot has joined and swept it, and offers a way to look again.

3. Say what to record. A channel picker over the mirror, grouped by kind, plus the consent role's name. Both go into one POST, which answers 202 with the guild's setup state.

4. What came of it. The request panel: a badge word, a heading, the decided sentences under it, who asked and when, what was asked for, and — on a failure only — the bot's own text verbatim and multi-line, because it names which channel, which permission and what to do about it, and no status word this console could invent would say that.

Why it is shaped this way

The console does not write guild_config. api holds no Discord token and never will (Spec 13.2), so it writes an intent and the bot's ten-second tick makes it true through the same planner /setup uses. The page's real job is therefore not a form; it is expressing three properties of that mechanism that are invisible in the payload, each of which looks on screen exactly like the state it must not be confused with.

A failure is terminal, so the page says so and offers another ask. One attempt settles an intent whichever way it went — the tick runs six times a minute forever, and an intent left unapplied after failing would retry a permission error against Discord's rate limiter just as often. There is nothing running behind a failed request and no back-off to wait out, so the panel says that outright and says that asking again is a new request rather than a resumption. A page that showed "failed" beside a spinner would have somebody waiting on a retry that does not exist.

The newest ask wins, so a superseded request is never red. Nothing went wrong to one: it was replaced before the bot reached it and never acted on. It is rendered in the neutral tone with the word "replaced", and the sentence explains why the newest wins outright — applying both in order would finish on the older list, which is a correction being overwritten by the mistake it corrected. There is a second half to this that only shows up in use: GET answers with the guild's newest request, which after a colleague pressed the button thirty seconds later is theirs. The page keeps the id it saw when this browser asked and, when a later poll answers with a different one, says so before anything else on the panel. Reading somebody else's applied request as your own is how an administrator concludes their channel list is in force when another one is.

pending past a tick means the bot is not there, and bot.has_arrived says which. The picker draws four states, not one empty list: nobody has looked yet while nothing has been mirrored, this server has no voice channels once something has, the names could not be read when the directory call failed, and the picker itself. The first two are one empty array on the wire and opposite instructions on screen — one says wait ten seconds, the other says go and create a channel — and has_arrived is asked before the directory failure, because a directory that answered nothing for a guild nothing has been mirrored for answered correctly. A pending request in that state gets its own tone and its own sentence rather than a slower spinner.

Where the role-position caveat lives. Manage Roles in the invitation is not sufficient by itself: the bot's own role must sit above the consent role in Server Settings → Roles or Discord refuses the edit, and no bitmask expresses that. It sits under step 1, marked, because it is a precondition and the drag can be done in the same visit to Discord that the invitation already requires. It is not repeated as a diagnosis on every failure, because request.error already carries the bot's own words — which name exactly that when it is the cause — and a fixed sentence pinned under every failure would be noise over the one place a person should be reading.

Two decisions that came from the applier rather than from the API's shape. A setup request adds to voice_channel_ids and never removes from it, so unticking a channel this server already records would do nothing at all; those rows are ticked, disabled, and labelled in a word, with one sentence pointing at voice_channel_ids on Bot Settings, which is where removal actually lives. And a stored channel the mirror cannot resolve is deliberately left out of every request: apply_setup_intents refuses a channel it cannot see, that refusal is a problem, and one problem settles the whole intent as failed — so a guild with a single deleted room in its configured list would otherwise have every request it ever made fail over a channel nobody asked about. The page names those ids and says what to do with them instead.

What it deliberately refuses to do

  • It never disables the form while a request is pending. The API accepts a second request over a first on purpose — refusing one would leave somebody who mistyped a channel waiting out a tick before they could correct it, and would lock a guild whose bot has not arrived out of being set up at all. So the button stays live and one sentence says what pressing it does: it replaces, it does not queue.
  • It never builds a second dropdown, a second channel picker, or a second answer to an unresolved snowflake. The guild switcher is UiSelect; the grouping, the kind headings and the bare-id fallback are ~/utils/directory's, which is where this console already decided how a snowflake with no row is presented.
  • It never offers an action it knows will fail. A request naming no channel is refused by the API, and a role name over a hundred characters is refused by Discord; both are said beside a disabled button while somebody is still typing, rather than arriving as a 400 a tick later.
  • It never treats a snowflake as a number. Every id is a string end to end, and the parser stringifies rather than refuses one that arrives as a JSON number.
  • It never polls forever. Three seconds against a ten-second tick, stopped on settlement, stopped on unmount, and given up after five minutes — the wait that bound guards is a human one (somebody has to open Discord), not the tick, and a tab left on that state would otherwise poll for as long as the browser runs. When it gives up it says so and offers a button, rather than pretending to still be watching.
  • It never blanks the panel it is watching. The skeleton is keyed off having no answer yet rather than off status === 'pending', because a page that re-reads itself every three seconds and keys a skeleton off the status alone erases what somebody is reading, four times a minute.

Colour is never the only thing that says a state

Every tone carries a badge word — waiting, not started, done, failed, replaced — and an outcome this build has never heard of renders as itself rather than being narrowed away, because outcome is text and not a database enum precisely so a newer bot's word is a row a reader can ignore. The two states that change on their own are aria-live="polite", the already-recorded rows say so in words as well as by being disabled, and the picker's channels are checkboxes in fieldsets under kind legends.

Translation

admin.onboarding.*, 84 keys, complete in both languages from the start — nothing on this page was ever hard-coded English, so the sweep that converts the older admin pages has nothing to reach here. nav.onboarding is the one key outside the namespace. i18n/README.md gains the namespace row and a note on why three of its sentences render the API's own prose beside a translated sentence rather than inside one.

docs/operations.md §6.2.14 gains a pointer to the page; the section itself is unchanged, since it is the contract this was built against.

Where Server Setup sits in the navigation

First in the Admin View, ahead of Bot Settings, which breaks that list's stated frequency ordering on purpose. Everything else there is read by somebody who already has a working server; this page is read by somebody who has none, and it is the prerequisite of every other entry — there is no configuration, no consent roster, no queue and no report until a server has been set up. An entry needed exactly once, by the person who knows this console least, cannot be fifth in a list of five. The ordering test's comment is amended to say so rather than left to disagree with the code.

One thing worth flagging

The brief described channelNaming as the console's single answer to how an unresolved snowflake is presented. That function (in ~/utils/recordings) is about a recording session's channel; the picker-side answer is resolveChoice / channelChoices in ~/utils/directory, and that is what this page reuses. No second answer to either question was written.

Checks

Measured on eed19d7 (v0.16.0) before and on this branch after, in console/:

vitest run 1784 passed in 58 files (was 1719 in 56 — 65 new: 47 in onboarding.spec.ts, 16 in adminOnboardingPage.spec.ts, and 2 that the new .vue file adds to the per-component sweeps in stylesheets.spec.ts and motion.spec.ts, both of which it passes) · eslint . clean · nuxt typecheck clean · nuxt build clean — all four green before and after.

…e browser

PR #149 shipped the routes and nothing rendered them: setting a guild up
meant running /setup in Discord or calling three endpoints by hand. This
is Server Setup, first entry of the Admin View, walking the invitation,
the server, what to record, and what came of it.

The console writes an intent rather than configuration -- api holds no
Discord token and never will -- so the page's job is to express three
properties of that mechanism that the payload does not state on its own.
A failure is terminal, so the page says there is no retry and offers
another ask instead of a wait. The newest ask wins, so a superseded
request is never drawn in the failure colour and a request that is not
the one this browser sent says so. And bot.has_arrived is what separates
"this server has no voice channels" from "nobody has looked yet", so the
picker draws four states rather than one empty list.

Two things reading the applier decided rather than the brief. Setting up
adds to voice_channel_ids and never removes from it, so already-recorded
channels are ticked and disabled instead of offering a control that would
do nothing. And a stored channel the mirror cannot resolve is never
carried into a request: the applier refuses a channel it cannot see, one
refusal settles the whole intent as failed, and a guild with one deleted
room would otherwise have every request it ever made fail over it.

Every decision is in app/utils/onboarding.ts and tested there; the page
keeps layout, request plumbing and the three-second poll, which stops on
settlement, on unmount, and after five minutes of a bot that never
arrives.
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