Commit Graph

2 Commits

Author SHA1 Message Date
Niklas Ye 07914d5cdb Give the Admin tab sub-sections of its own
Administration was one scrolling page with three cards on it: the teams,
the people, and the settings. There was no way to link somebody to the
settings, no way back to the top of the user list but scrolling, and the
poll loop refetched all three endpoints every tick however little of the
page you were looking at.

Each is now a route -- /admin/teams, /admin/users, /admin/settings --
reached from a strip across the top, with /admin an overview that says
how many of each there are. The three cards themselves are untouched;
they are simply rendered one at a time, so a tab fetches only what it
shows. The Users page is the exception and fetches the teams too, since
its invite form has to offer a team to invite somebody into.

The strip is ordinary links rather than chips. Chips filter what a page
already shows, here and in the queue, and these four go somewhere: the
browser's Back walks them, a reload lands where you were, and the click
is intercepted by the same handler every other link in the app uses.
The current one is marked with aria-current="page", the convention the
tab bar has used since it existed, so the state lives on the attribute
and not in a class.

admin.js owns the table of the four routes, because it also builds the
strip that links to them; app.js parses against that table rather than
keeping a second list to drift from it. Adding a fifth sub-section is
one line.

The bottom tab bar still has six items. 56b8191 made it count-agnostic
when Admin arriving pushed it past four, and the note there records that
six at 420px already leaves 55-65px each -- so the sub-sections went
inside the Admin page rather than beside it.

A person's page keeps its own route at /admin/users/{id}; its back link
now returns to the user list rather than to the top of everything.

Nobody has looked at this in a browser, the same caveat ac9af8e carried.
What is checked is the wiring: the module graph evaluates at every admin
URL, all four tabs render against live server responses with one
aria-current each and the fetches the table above describes, the
non-administrator branch still refuses without fetching, and the server
serves index.html for each new path so a reload survives. The strip's
appearance at phone and desktop width is not checked.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
2026-09-21 21:11:08 +02:00
Niklas Ye ac9af8e4f5 Manage a person's account and teams from one page
The Admin tab could make somebody an administrator and disable them, and
nothing else. Setting a first password, deleting an account and seeing
which teams a person is in all meant curl, and the last one meant opening
each team in turn — the Team tab answers "who is in this team", which is
the wrong way round when the question is about a person.

A name in the user list now opens /admin/users/{id}: their email and when
they joined, where their notifications go, the administrator and disabled
flags, the teams they are in with their role in each, a password field
for a first or forgotten one, and deletion. A section of its own rather
than an expanding row, because memberships and the account actions
together are more than a table row can hold and still be read on a phone.

Adding somebody mints an invite link into a chosen team rather than
creating a bare account. POST /api/users makes a user with no password
and no team, who can sign in nowhere and would see nothing if they did;
the invite machinery from #7 already solves both, and the password is
chosen by the person it belongs to instead of passing through an
administrator.

One new endpoint, GET /api/users/{id}/teams, self or admin. /api/teams is
always about the caller and cannot be asked about anybody else. It 404s
for a user who does not exist, so the page can tell "in no teams" from
"no such person" — an empty list is a real answer and needed to stay one.

No authorisation changed, and the interesting part is why it did not.
requireTeamOwner has accepted the administrator flag since a4fbd60, with
the reason in its own comment: somebody has to be able to repair a team
whose owner has left. It guards nine call sites, so an administrator has
always been able to configure any team on this server — while #1's
decision table and this README both said an admin "is not implicitly in
every team", full stop. The code was right and the prose was wrong in the
safe-sounding direction, which is the worse way round to have it.

So the documentation moved to meet the code. The Teams table marks owner
as owner-or-admin, and the Authentication section states the two
directions separately: an administrator configures any team, and reads
none, because callerTeamIDs is built from real memberships only. Joining
a team to see its queue is a membership change and shows as one.

TestAdmin_ConfiguresATeamTheyAreNotIn pins both halves — the admin
renames, invites, adds and removes on a team they are not in, then sees
zero of its incidents. Nothing tested this from v0.12.0 to here, which is
why four releases of prose could contradict it quietly.

The UI has not been opened in a browser. Its wiring is checked — every
cross-module import resolves, every api.* call exists, every CSS class
has a rule, and the deep link serves index.html — but nobody has clicked
through it, least of all at phone width.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
2026-09-21 12:55:42 +02:00