33356ca978
state.js's currentTeam() was hard-coded to teams[0] and never really meant "the team currently selected" — team.js's settings page and queue.js's filter chips each kept their own separate, unsynchronized notion of "which team" instead, so picking one on one page had no effect on the other. Replaces both with a single state.selectedTeamID, set only through the new setSelectedTeam (persisted in localStorage, unlike the queue's old per-tab sessionStorage filter) and broadcast to listeners via onTeamChange. A new teamselector.js control — a coloured dot plus the team's name, or "All teams" — sits at the top of both the desktop sidebar and the mobile topbar, opening the existing bottom-sheet menu to switch. Shown only once someone is in more than one team, matching every other team-aware control in this app. Colours come from a new teamColorClass() in format.js, hashing a team's id into the six-colour rc1..rc6 palette already used for the rota's per-person chips, so no schema or API change is needed. The queue's team filter chips pick up the same colours.
120 lines
3.8 KiB
JavaScript
120 lines
3.8 KiB
JavaScript
// Formatting of times, durations and labels.
|
|
|
|
const MIN = 60 * 1000;
|
|
const HOUR = 60 * MIN;
|
|
const DAY = 24 * HOUR;
|
|
|
|
// Compact age for list rows: "now", "4m", "3h", "2d".
|
|
export function age(iso, now = Date.now()) {
|
|
const ms = Math.max(0, now - Date.parse(iso));
|
|
if (ms < MIN) return 'now';
|
|
if (ms < HOUR) return `${Math.floor(ms / MIN)}m`;
|
|
if (ms < DAY) return `${Math.floor(ms / HOUR)}h`;
|
|
return `${Math.floor(ms / DAY)}d`;
|
|
}
|
|
|
|
// "4 min ago", "3 h ago", "yesterday"-free: stays unambiguous at 3am.
|
|
export function ago(iso, now = Date.now()) {
|
|
const a = age(iso, now);
|
|
return a === 'now' ? 'just now' : `${a} ago`;
|
|
}
|
|
|
|
// Time remaining until iso, e.g. "1h 20m".
|
|
export function until(iso, now = Date.now()) {
|
|
return duration(Date.parse(iso) - now);
|
|
}
|
|
|
|
export function duration(ms) {
|
|
ms = Math.max(0, ms);
|
|
if (ms < MIN) return '<1m';
|
|
const d = Math.floor(ms / DAY);
|
|
const h = Math.floor((ms % DAY) / HOUR);
|
|
const m = Math.floor((ms % HOUR) / MIN);
|
|
if (d) return h ? `${d}d ${h}h` : `${d}d`;
|
|
if (h) return m ? `${h}h ${m}m` : `${h}h`;
|
|
return `${m}m`;
|
|
}
|
|
|
|
const timeFmt = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
const dayTimeFmt = new Intl.DateTimeFormat(undefined, {
|
|
weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit',
|
|
});
|
|
|
|
// Local timestamp; the date is dropped when it is today.
|
|
export function when(iso) {
|
|
const d = new Date(iso);
|
|
const today = new Date();
|
|
if (d.toDateString() === today.toDateString()) return timeFmt.format(d);
|
|
return dayTimeFmt.format(d);
|
|
}
|
|
|
|
export function isFuture(iso) {
|
|
return iso != null && Date.parse(iso) > Date.now();
|
|
}
|
|
|
|
// Local calendar dates, as the schedule stores them (YYYY-MM-DD).
|
|
export function isoDate(d) {
|
|
const y = d.getFullYear();
|
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
return `${y}-${m}-${day}`;
|
|
}
|
|
|
|
export function mondayOf(d) {
|
|
const r = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
r.setDate(r.getDate() - ((r.getDay() + 6) % 7));
|
|
return r;
|
|
}
|
|
|
|
// ISO 8601 week number: weeks start on Monday and week 1 is the one holding the
|
|
// year's first Thursday, which is what a rota that runs Monday to Sunday means
|
|
// by "week 40". Taken from the Thursday of d's week, whose year is the week's.
|
|
export function isoWeek(d) {
|
|
const thu = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
thu.setDate(thu.getDate() + 3 - ((thu.getDay() + 6) % 7));
|
|
const jan4 = new Date(thu.getFullYear(), 0, 4);
|
|
return 1 + Math.round(((thu - jan4) / 86400000 - 3 + ((jan4.getDay() + 6) % 7)) / 7);
|
|
}
|
|
|
|
export function addDays(d, n) {
|
|
const r = new Date(d);
|
|
r.setDate(r.getDate() + n);
|
|
return r;
|
|
}
|
|
|
|
export const STATUS_LABEL = {
|
|
triggered: 'Triggered',
|
|
acknowledged: 'Acknowledged',
|
|
resolved: 'Resolved',
|
|
snoozed: 'Snoozed',
|
|
firing: 'Firing',
|
|
};
|
|
|
|
export function severityClass(sev) {
|
|
const s = (sev || '').toLowerCase();
|
|
if (s === 'critical' || s === 'page' || s === 'error') return 'sev-critical';
|
|
if (s === 'warning' || s === 'warn') return 'sev-warning';
|
|
if (s) return 'sev-info';
|
|
return '';
|
|
}
|
|
|
|
// A stable identity colour for a team, so the same team always reads the same
|
|
// colour without the server needing to store one. Teams have no colour field;
|
|
// this hashes the id into the six-colour rcN palette app.css already has for
|
|
// the rota's per-person chips (a team is not a status, so never severity).
|
|
export function teamColorClass(teamID) {
|
|
return `rc${(((teamID % 6) + 6) % 6) + 1}`;
|
|
}
|
|
|
|
// A one-line summary of the group labels, without the one the title already shows.
|
|
export function labelSummary(labels, skip = 'alertname') {
|
|
return Object.entries(labels || {})
|
|
.filter(([k]) => k !== skip)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(' · ');
|
|
}
|
|
|
|
export function initial(name) {
|
|
return (name || '?').trim().charAt(0) || '?';
|
|
}
|