b2c3868619
format.js defined isoWeek twice. 9d1df2b added a second copy for the
week numbers on the rota without noticing the first, which already
exported the same function. A duplicate function declaration is legal in
a plain script but an early SyntaxError in an ES module, so the browser
refused format.js, every module importing it, and with them app.js. Its
boot() is what hides the spinner, so nothing ever did.
Keeps the first definition, whose comment explains the Thursday rule, and
drops the second. Both give ISO 8601 numbers; the one kept was checked
against 2026-01-01 (week 1), 2026-09-28 (40), 2026-12-31 (53) and
2024-12-30 (1). oncall.js and team.js import the name unchanged.
The suite could not see this: it has no JS, and node --check reads a .js
file as a script, where the redeclaration passes. Checking each file as a
module (.mjs) does catch it.
112 lines
3.4 KiB
JavaScript
112 lines
3.4 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 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) || '?';
|
|
}
|