Commit Graph

3 Commits

Author SHA1 Message Date
Niklas Ye 3cdd5aee1f Give the Team tab sub-sections of its own
The Team tab was five cards stacked on one page: the rota, the escalation
ladder, the alert sources, the dead man's switches and the membership.
07914d5 split the Admin tab for three reasons, and all three were sharper
here. There was no way to link somebody to the escalation ladder, which is
the thing a team owner most often has to be talked through. There was no
way to the switches but scrolling past a month of rota -- and the rota
became a month grid in v0.18.0, which made the page taller rather than
shorter. And the poll loop refetched six endpoints every tick however
little of the page you were looking at.

Each is now a route: /team/rota, /team/members, /team/escalation,
/team/sources, /team/deadman, reached from the same strip of links the
Admin tab uses, with /team an overview. A page fetches only what it shows,
so the switches are one GET and the sources are one, where every tick used
to be six.

Three of the five fetch the member list besides their own endpoint, and
for the same reason each time: a rota entry, a ladder target and a role are
all a person, and the page has to be able to name them. The overview is the
one that fetches everything, because saying how much of each there is means
asking each of them -- who is on call today, how many members and owners,
how many ladder levels and whether a fallback follows them, how many keys
and how many never used, how many switches. That is what it is for; a strip
that already links to the five does not need a second menu that repeats it.

team.js owns the table of its six routes, as admin.js owns its four, and
app.js parses against both rather than keeping a third list to drift from
them. The table carries a title beside the label where the strip's word is
too thin to name a page on its own: "Sources" is a fine tab and a poor
browser tab, so that page titles as Alert sources and the switches keep
their apostrophe in the top bar.

menuItem left admin.js for ui.js as menuCard, since both tabs now open on
one, and its CSS went from .admin-menu* to .overview-*. That is the rename
.user-link -> .row-link was in v0.18.0, for the same reason: the class was
named after the first page that used it rather than after what it is.

The read-only notice a member sees is now on the overview only. It explains
why the controls further down are missing, and a page that is nothing but
the rota grid has no controls to explain. The team picker sits above the
strip, because it changes the subject of all five, and it drops the ladder
draft when it moves -- an unsaved edit belongs to the team it was started
in.

No server change. Extensionless paths already fall back to index.html, so
/team/rota survives a reload the way /admin/users/{id} does, and no
endpoint, payload or permission moved.

Nobody has looked at this in a browser, the caveat 07914d5 and a6fa673
carried. What is checked is the wiring, and rather more of it than last
time: every sub-page was rendered against a stub fetch and a pocket DOM,
each with exactly one aria-current and fetching only the endpoints named
above; and app.js itself was booted the same way and walked through all
seventeen URLs the app has, which resolve to one section each with the
right title -- the six new ones, the four Admin ones, both subject pages,
and /incidents/42 and /nonsense still falling to the queue. Whether six
entries scroll cleanly at phone width is not checked.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
2026-09-22 12:23:11 +02:00
Niklas Ye 67d68ce058 Show the rota as a month rather than a list of dates
The Team tab printed the next thirty days as thirty rows of date, name and
a Clear button. That is a rota spelled out one day at a time, and it is the
one shape the question cannot be read in: what anybody wants from a rota is
who holds which stretch, and thirty names down a column hides a handover
between two rows that look the same. It was also the longest thing on the
page by a wide margin, so the escalation ladder and the alert sources sat
below a screen of dates.

It is a month now, Monday to Sunday, one coloured initial per day. A shift
becomes a run of one colour, which is the shape the answer actually has; a
gap becomes a hole you can see. The legend underneath says whose colour is
whose, and one line says how many days are left uncovered, counting only
from today -- an empty Tuesday last week is history, not a hole somebody
still has to fill.

Laid out like the on-call page's week, deliberately: heading and arrows
outside the card, days inside it. It is the same rota, and two pages
showing it two ways would be two things to learn.

Colours come from a person's place in the member list, so they hold still
as you page between months, and six of them repeat -- the initial inside
still tells two people apart, and a legend that has to explain nine hues is
not a legend. They are not the severity palette: nothing on a rota is
critical, and a red Thursday would read as one. --teal and --pink are new
in both themes for the two the palette was short.

The per-row Clear button had nowhere left to live, so a day opens the sheet
the app already uses for confirmations: who holds it, a picker, Assign and
Clear. That assign sends replace=true where the range form still asks
first, and the difference is the point -- the sheet has just named whoever
holds the day, so taking it from them is the thing that was asked for
rather than something to warn about. The range form is unchanged and folded
into a details, since filling a whole shift is what it is for; it opens on
the month above it rather than on today, so paging to March to fill March
does not hand you September.

The server is untouched. The month drawn is the month fetched -- the grid's
Monday overhang and its trailing days are real days and are fetched with
it -- so paging is one GET /api/teams/{id}/schedule per month with from and
to, where it used to be one fixed thirty-day window. No new endpoint, no
change to what the API returns, and terdut-tui is unaffected.

Nobody has looked at this in a browser either. What is checked is the
rendering: team.js's own refresh() was run against a stub fetch and a
pocket DOM for September 2026, and it produces 35 cells for a month whose
1st is a Tuesday, the right from/to on the schedule call, today marked on
the 22nd, three people in the legend with "you" on the viewer, the gap
count over a five-day hole, and -- as a member rather than an owner -- the
same grid as plain divs with no sheet and no range form. How it looks at
phone width, and whether the six colours hold up in dark mode, are not
checked.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
2026-09-22 09:13:36 +02:00
Niklas Ye d728af53b1 Put a team's own settings in the web UI
CI / chart (pull_request) Successful in 1s
CI / security (pull_request) Successful in 13s
CI / test (pull_request) Successful in 2m19s
Closes #17. Everything a team owner configures was API-only: escalation,
integrations, dead man's switches, membership, and the rota -- which the
on-call view still described as the TUI's job, and the TUI has been broken
against this server since teams landed. Setting up the feature this whole
line of work exists for meant using curl.

A Team tab now holds all of it, one team at a time, with a picker for
somebody in more than one. An owner edits; a member sees the same page
without the controls, because the server refuses their writes anyway --
hiding a button is a courtesy to the reader, not the thing enforcing
anything.

The escalation editor holds a draft and sends the whole ladder, because
the API replaces it wholesale: the levels are an order, and patching one
rung leaves the numbering of the others undecided. Adding a level
defaults to five minutes and the rota, which is the shape almost every
ladder starts as.

An integration key is returned exactly once, so creating one opens a
panel that says so, shows the URL large with a copy button, and renders
the Alertmanager receiver snippet with the URL already in it -- the next
thing anybody does with that key is paste it into a config. The panel
stays until it is dismissed rather than disappearing on the next
re-render.

The incident view gains where an incident is on the ladder and when the
next page is due, which is the question somebody looking at an
unacknowledged incident actually has. The API carries it: the incident
payload now includes escalation_level and escalation_due_at, the latter
computed in the incident SELECT by joining the level's timeout, so a list
costs no extra queries.

Verified against a live server by making every call the page makes,
including the writes: the six reads the Team tab issues, a two-level
ladder saved and read back, an integration created and its key returned
once, three days of rota assigned, switches set, and an incident showing
level 1 with a due time five minutes out.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
2026-09-20 21:22:24 +02:00