91f03c21e8
Addresses the screenshot-review feedback in #26. No framework or build step added — all of this stays within the existing plain HTML/CSS/ vanilla-JS + go:embed architecture. - Nav: re-enable the bottom tab bar that was already built and switched off (Queue/On-call/Alerts/Team + a "More" sheet for Stats/Admin/Account), replacing the hamburger on phone width. - Queue: chip counts, a scroll fade on the filter row, a "Triggered Xh ago" + severity label per row, a chevron on the team switcher so it reads as a dropdown. - On-call: collapse repeated same-person days into shift bars (week view and "your shifts" both), show the week as a date range with the ISO week number as secondary text, split "Current shift" out from "Next shifts" with "ends in Nd", a pill badge + row highlight for "you". - Incident detail: fix the actual bug behind the duplicate "acknowledged" timeline entries (acknowledgeIncident's UPDATE had no guard on the incident's current status, so acknowledging an already-acknowledged incident silently re-logged the event — now idempotent, with regression tests on both the authenticated route and the ntfy ack-button route). Relabel escalation re-pages so they don't look like the same page landing twice. Copy the primary action up near the top. Label the "···" button. Group the timeline by phase (triggered/acknowledged/resolved). Add an "at a glance" summary row (duration/severity/responsible) and collapse the group labels by default. - Team overview: reword the vague copy ("One owner." etc.) into plain labels. - Empty states: fill in missing icons/one-liners across queue, alerts, stats and the incident timeline. - CSS: fix card padding bugs, verify link contrast already passes AA, introduce a --fs-* type-scale token set and migrate the few genuinely isolated cases onto it (left sizes tied to a fixed shape, a deliberately prominent display, or a non-negotiable constraint like the iOS-zoom-prevention input size as documented exceptions rather than guess at a render this change can't see). Verified with the full fmt/lint/test/helm-lint gate, plus a live instance against the test DB with seeded incidents and schedule data to trace the on-call grouping and timeline phase-splitting logic against real API responses. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
67 lines
2.8 KiB
JavaScript
67 lines
2.8 KiB
JavaScript
// The global team selector: a small control, once per layout (the desktop
|
|
// sidebar and the mobile topbar each have their own button in index.html),
|
|
// showing the current team's colour and name — or "All teams" — and opening a
|
|
// sheet to switch. Shown only once there is more than one team to choose
|
|
// between, the same rule every other team-aware control in this app follows;
|
|
// see state.js's currentTeam() for why nobody with just one ever has to.
|
|
|
|
import { h, icon, openSheet, closeSheet } from './ui.js';
|
|
import { state, currentTeam, setSelectedTeam, onTeamChange } from './state.js';
|
|
import { teamColorClass } from './format.js';
|
|
|
|
// Not a real team id (ids are positive), so it can never collide with one —
|
|
// the value closeSheet resolves with for "All teams", distinct from the null
|
|
// a dismissed sheet resolves with.
|
|
const ALL_TEAMS = '__all__';
|
|
|
|
const buttons = () => [
|
|
document.getElementById('team-selector'),
|
|
document.getElementById('team-selector-mobile'),
|
|
].filter(Boolean);
|
|
|
|
// init wires the buttons once, at boot. render (below) is what actually fills
|
|
// them in and is called again by state.js whenever the selection changes.
|
|
export function init() {
|
|
for (const btn of buttons()) btn.addEventListener('click', open);
|
|
onTeamChange(render);
|
|
}
|
|
|
|
export function render() {
|
|
const multiTeam = (state.teams || []).length > 1;
|
|
const team = currentTeam();
|
|
const label = team ? team.name : 'All teams';
|
|
const dotClass = team ? `team-dot ${teamColorClass(team.id)}` : 'team-dot';
|
|
for (const btn of buttons()) {
|
|
btn.hidden = !multiTeam;
|
|
btn.replaceChildren(
|
|
h('span', { class: dotClass }),
|
|
h('span', { class: 'team-selector-label', text: label }),
|
|
// Without this the pill reads as a tag rather than something you can
|
|
// open; the chevron is the only thing that says "dropdown" here.
|
|
icon('chevronDown', 'icon team-selector-chevron'),
|
|
);
|
|
}
|
|
}
|
|
|
|
function open() {
|
|
const teams = state.teams || [];
|
|
openSheet(() => [
|
|
h('h2', { class: 'sheet-title', text: 'Switch team' }),
|
|
h('ul', { class: 'menu', role: 'menu' },
|
|
h('li', {}, h('button', {
|
|
class: 'menu-item', type: 'button', role: 'menuitemradio',
|
|
'aria-checked': String(state.selectedTeamID == null),
|
|
onclick: () => closeSheet(ALL_TEAMS),
|
|
}, h('span', { class: 'team-dot' }), ' All teams')),
|
|
teams.map((t) => h('li', {}, h('button', {
|
|
class: 'menu-item', type: 'button', role: 'menuitemradio',
|
|
'aria-checked': String(t.id === state.selectedTeamID),
|
|
onclick: () => closeSheet(t.id),
|
|
}, h('span', { class: `team-dot ${teamColorClass(t.id)}` }), ' ' + t.name))),
|
|
),
|
|
]).then((choice) => {
|
|
if (choice == null) return; // dismissed: backdrop, escape, or cancel
|
|
setSelectedTeam(choice === ALL_TEAMS ? null : choice);
|
|
});
|
|
}
|