Files
terdut-server/internal/web/static/js/format.js
T
Niklas Ye 9d1df2b611 Show week numbers on the rota, and assign a whole week from them
The rota grid starts each row with its ISO week number, and for an owner
the number is a button: one tap opens a sheet for that week, showing who
holds each of its seven days, and puts one person on all of them. A rota
is usually handed out by the week, and seven taps on seven days was the
only way to do it short of the range form.

ISO 8601 numbering, because the grid already runs Monday to Sunday: week
1 is the one holding the year's first Thursday, which is taken from the
Thursday of the row so the year boundaries come out right (2025-12-29 is
week 1 of 2026, 2020-12-31 is week 53).

Days already past are left alone. Who was on call last Tuesday is a fact,
and "the whole week" should not rewrite it, so a half-elapsed week covers
the days still to come and the sheet says so; a week that is entirely
over has nothing to assign. By default the assignment replaces whoever
holds those days, as the day sheet does and the sheet states, and a
checkbox limits it to the days nobody has yet. The overhang into the
neighbouring month is part of the same week and is included.

Web UI only: the existing schedule endpoint already takes a list of
dates and a replace flag, so nothing changed on the server and there is
nothing to mirror in terdut-tui.
2026-09-26 09:19:35 +02:00

121 lines
3.7 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;
}
// ISO 8601 week number.
export function isoWeek(d) {
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
const day = t.getUTCDay() || 7;
t.setUTCDate(t.getUTCDate() + 4 - day);
const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
return Math.ceil(((t - yearStart) / DAY + 1) / 7);
}
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) || '?';
}