dashboard frontend: consume rfc3339 timestamps from the api
This commit is contained in:
parent
bac2c0a65e
commit
cafef519a9
5 changed files with 46 additions and 33 deletions
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { $, el, form, Panel, appendLinkified } from './common.js';
|
||||
import { themedToast } from './modal.js';
|
||||
import { fmtAgo, fmtDuration } from './util.js';
|
||||
import { epochSec, fmtAgo, fmtDuration } from './util.js';
|
||||
import { questionsState, QUESTION_HISTORY_LIMIT } from './state.js';
|
||||
|
||||
// Registered by the dashboard entry at boot; defaults to a no-op so the
|
||||
|
|
@ -70,7 +70,7 @@ function renderOperatorInbox() {
|
|||
`✓ mark all read (${operatorInbox.length})`);
|
||||
mark.addEventListener('click', markOperatorInboxRead);
|
||||
root.append(el('div', { class: 'inbox-toolbar' }, mark));
|
||||
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
||||
const fmt = (ts) => new Date(ts).toISOString().replace('T', ' ').slice(0, 19);
|
||||
const ul = el('ul', { class: 'inbox' });
|
||||
for (const m of operatorInbox) {
|
||||
const body = el('span', { class: 'msg-body' });
|
||||
|
|
@ -333,11 +333,12 @@ export function renderApprovals() {
|
|||
// Goes amber once it's been pending an hour so a stale request is
|
||||
// obvious at a glance (see docs/web-ui.md::Approval card).
|
||||
if (a.requested_at != null) {
|
||||
const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - a.requested_at));
|
||||
const requestedSec = epochSec(a.requested_at);
|
||||
const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - requestedSec));
|
||||
head.append(el('span', {
|
||||
class: 'approval-ts' + (ageSec >= 3600 ? ' stale' : ''),
|
||||
title: 'requested ' + new Date(a.requested_at * 1000).toLocaleString(),
|
||||
'data-requested-at': String(a.requested_at),
|
||||
title: 'requested ' + new Date(a.requested_at).toLocaleString(),
|
||||
'data-requested-at': String(requestedSec),
|
||||
}, 'requested ' + fmtAgo(a.requested_at)));
|
||||
}
|
||||
li.append(head);
|
||||
|
|
@ -556,7 +557,7 @@ function questionRowFingerprint(q) {
|
|||
// Event listeners attached here (keydown on textarea, submit on form) are
|
||||
// preserved in the reused node — no re-attachment needed.
|
||||
function buildQuestionLi(q) {
|
||||
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
||||
const fmt = (ts) => new Date(ts).toISOString().replace('T', ' ').slice(0, 19);
|
||||
const targetLabel = q.target || 'operator';
|
||||
const li = el('li', { class: 'question' + (q.target ? ' question-peer' : '') });
|
||||
const head = el('div', { class: 'q-head' },
|
||||
|
|
@ -570,10 +571,10 @@ function buildQuestionLi(q) {
|
|||
// Tag the chip with its deadline so the global 1s ticker
|
||||
// can refresh the text without re-rendering the questions section.
|
||||
const ttlEl = el('span', {
|
||||
class: 'q-ttl', 'data-deadline': String(q.deadline_at),
|
||||
class: 'q-ttl', 'data-deadline': String(epochSec(q.deadline_at)),
|
||||
});
|
||||
ttlEl.textContent = formatTtl(
|
||||
q.deadline_at - Math.floor(Date.now() / 1000),
|
||||
epochSec(q.deadline_at) - Math.floor(Date.now() / 1000),
|
||||
);
|
||||
head.append(' ', ttlEl);
|
||||
}
|
||||
|
|
@ -694,7 +695,7 @@ export function renderQuestions() {
|
|||
// <li> nodes (preserving textarea/checkbox state) and only rebuilds
|
||||
// cache-miss rows, so we no longer wipe the DOM at the start.
|
||||
const openDetails = snapshotOpenDetails(root);
|
||||
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
||||
const fmt = (ts) => new Date(ts).toISOString().replace('T', ' ').slice(0, 19);
|
||||
const allPending = questionsState.pending;
|
||||
|
||||
// Filter chips. Always include `all` / `operator` / `peer`; add
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
appendLinkified,
|
||||
openStream, initServerWarnings,
|
||||
} from './common.js';
|
||||
import { epochSec } from './util.js';
|
||||
|
||||
(() => {
|
||||
NOTIF.bind();
|
||||
|
|
@ -92,7 +93,7 @@ import {
|
|||
const flow = $('msgflow');
|
||||
if (!flow) return;
|
||||
flow.replaceChildren();
|
||||
const tsFmt = (n) => new Date(n * 1000).toISOString().slice(11, 19);
|
||||
const tsFmt = (ts) => new Date(ts).toISOString().slice(11, 19);
|
||||
// Pulse the page banner whenever a broker event lands. The
|
||||
// `.banner` element lives in the dashboard's <footer> rather than
|
||||
// in the flow chrome — `pulseBanner` no-ops on /flow.html since
|
||||
|
|
@ -179,7 +180,7 @@ import {
|
|||
const recentSent = new Map(); // broker row id → { row, at }
|
||||
function rememberSent(ev, row) {
|
||||
if (ev.id == null || ev.id <= 0) return;
|
||||
recentSent.set(ev.id, { row, at: ev.at });
|
||||
recentSent.set(ev.id, { row, at: epochSec(ev.at) });
|
||||
// Bound the map — drop the oldest entries once it grows past a
|
||||
// page of un-collapsed sends (insertion order = oldest first).
|
||||
while (recentSent.size > 256) {
|
||||
|
|
@ -190,7 +191,7 @@ import {
|
|||
function collapseDelivered(ev) {
|
||||
if (ev.id == null || ev.id <= 0) return false;
|
||||
const s = recentSent.get(ev.id);
|
||||
if (!s || ev.at - s.at > COLLAPSE_SECS) return false;
|
||||
if (!s || epochSec(ev.at) - s.at > COLLAPSE_SECS) return false;
|
||||
const arrow = s.row.querySelector('.msg-arrow');
|
||||
if (arrow) arrow.textContent = '✓';
|
||||
// Re-style the row as delivered (green ✓) — the collapsed line now
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import {
|
||||
$, el, fmtAgeSecs, openStream, initServerWarnings,
|
||||
} from './common.js';
|
||||
import { epochSec } from './util.js';
|
||||
import { createTabStrip } from '@hive/shared/tabs.js';
|
||||
|
||||
(() => {
|
||||
|
|
@ -170,10 +171,11 @@ import { createTabStrip } from '@hive/shared/tabs.js';
|
|||
let auditTotal = 0;
|
||||
let auditFetching = false;
|
||||
|
||||
// ts_unix is unix seconds — fmtAgeSecs wants an age in seconds.
|
||||
function auditFmtWhen(tsUnix) {
|
||||
if (!tsUnix) return '';
|
||||
const age = Math.floor(Date.now() / 1000) - tsUnix;
|
||||
// ts_unix arrives as an RFC 3339 string — fmtAgeSecs wants an age
|
||||
// in seconds, so normalize via epochSec first.
|
||||
function auditFmtWhen(ts) {
|
||||
if (!ts) return '';
|
||||
const age = Math.floor(Date.now() / 1000) - epochSec(ts);
|
||||
return fmtAgeSecs(Math.max(0, age)) + ' ago';
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +236,7 @@ import { createTabStrip } from '@hive/shared/tabs.js';
|
|||
tbody.append(el('tr', {},
|
||||
el('td', {
|
||||
class: 'audit-when meta',
|
||||
title: e.ts_unix ? new Date(e.ts_unix * 1000).toISOString() : '',
|
||||
title: e.ts_unix ? new Date(e.ts_unix).toISOString() : '',
|
||||
}, auditFmtWhen(e.ts_unix)),
|
||||
el('td', { class: 'audit-agent' }, e.agent || ''),
|
||||
el('td', { class: 'audit-action' }, e.action || ''),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
import { $, el, appendLinkified } from './common.js';
|
||||
import { themedConfirm, themedToast } from './modal.js';
|
||||
import { paintAtomic, fmtAgo, fmtDuration } from './util.js';
|
||||
import { paintAtomic, epochSec, fmtAgo, fmtDuration } from './util.js';
|
||||
import { containersState } from './state.js';
|
||||
|
||||
export async function refreshReminders() {
|
||||
|
|
@ -47,7 +47,7 @@ function renderReminders(rows) {
|
|||
for (const r of rows) {
|
||||
const failed = (r.attempt_count || 0) > 0;
|
||||
const li = el('li', { class: 'reminder-row' + (failed ? ' reminder-failed' : '') });
|
||||
const dueIn = r.due_at - Math.floor(Date.now() / 1000);
|
||||
const dueIn = epochSec(r.due_at) - Math.floor(Date.now() / 1000);
|
||||
const dueLabel = dueIn <= 0
|
||||
? `overdue ${fmtAgo(r.due_at)}`
|
||||
: `in ${fmtDuration(dueIn)}`;
|
||||
|
|
@ -55,8 +55,8 @@ function renderReminders(rows) {
|
|||
el('span', { class: 'agent' }, r.agent), ' ',
|
||||
el('span', {
|
||||
class: 'meta reminder-due',
|
||||
title: new Date(r.due_at * 1000).toISOString(),
|
||||
'data-due-at': String(r.due_at),
|
||||
title: new Date(r.due_at).toISOString(),
|
||||
'data-due-at': String(epochSec(r.due_at)),
|
||||
}, dueLabel),
|
||||
' ',
|
||||
el('span', { class: 'meta' }, `· id ${r.id}`),
|
||||
|
|
@ -394,7 +394,7 @@ function renderSchedulesList() {
|
|||
const aOrd = a.cancelled_at_unix ? 2 : a.paused_at_unix ? 1 : 0;
|
||||
const bOrd = b.cancelled_at_unix ? 2 : b.paused_at_unix ? 1 : 0;
|
||||
if (aOrd !== bOrd) return aOrd - bOrd;
|
||||
return a.next_fire_at_unix - b.next_fire_at_unix;
|
||||
return epochSec(a.next_fire_at_unix) - epochSec(b.next_fire_at_unix);
|
||||
});
|
||||
for (const s of sorted) {
|
||||
tbody.append(renderScheduleRow(s, agents));
|
||||
|
|
@ -657,24 +657,24 @@ function renderScheduleRow(s, agents) {
|
|||
// absolute ISO in the title.
|
||||
const nextCell = el('td', { class: 'meta schedules-table-next-col' });
|
||||
if (cancelled) {
|
||||
nextCell.title = 'cancelled ' + new Date(s.cancelled_at_unix * 1000).toISOString();
|
||||
nextCell.title = 'cancelled ' + new Date(s.cancelled_at_unix).toISOString();
|
||||
nextCell.textContent = 'cancelled';
|
||||
} else if (paused) {
|
||||
nextCell.title = 'paused since '
|
||||
+ new Date(s.paused_at_unix * 1000).toISOString()
|
||||
+ new Date(s.paused_at_unix).toISOString()
|
||||
+ '\nwould fire at '
|
||||
+ new Date(s.next_fire_at_unix * 1000).toISOString();
|
||||
+ new Date(s.next_fire_at_unix).toISOString();
|
||||
nextCell.append(
|
||||
el('span', { class: 'sched-paused-label' }, '⏸ paused'),
|
||||
);
|
||||
} else {
|
||||
const dueIn = s.next_fire_at_unix - Math.floor(Date.now() / 1000);
|
||||
nextCell.title = new Date(s.next_fire_at_unix * 1000).toISOString();
|
||||
const dueIn = epochSec(s.next_fire_at_unix) - Math.floor(Date.now() / 1000);
|
||||
nextCell.title = new Date(s.next_fire_at_unix).toISOString();
|
||||
nextCell.textContent = dueIn <= 0
|
||||
? 'overdue ' + fmtAgo(s.next_fire_at_unix)
|
||||
: fmtDuration(dueIn);
|
||||
nextCell.classList.add('sched-due');
|
||||
nextCell.dataset.dueAt = String(s.next_fire_at_unix);
|
||||
nextCell.dataset.dueAt = String(epochSec(s.next_fire_at_unix));
|
||||
}
|
||||
tr.append(nextCell);
|
||||
|
||||
|
|
@ -841,7 +841,7 @@ function renderScheduleEditForm(s) {
|
|||
});
|
||||
firstFireInput.value = carry.next_fire !== undefined
|
||||
? carry.next_fire
|
||||
: isoForDatetimeLocal(new Date(s.next_fire_at_unix * 1000));
|
||||
: isoForDatetimeLocal(new Date(s.next_fire_at_unix));
|
||||
form_.append(scheduleField('next fire', firstFireInput));
|
||||
|
||||
const intervalCx = buildIntervalComposer({
|
||||
|
|
@ -968,7 +968,7 @@ async function submitEditSchedule(originalSchedule, form_) {
|
|||
if (newDescription !== (s.description || '')) {
|
||||
patch.description = newDescription || null;
|
||||
}
|
||||
if (newNextFireUnix !== s.next_fire_at_unix) {
|
||||
if (newNextFireUnix !== epochSec(s.next_fire_at_unix)) {
|
||||
patch.next_fire_at_unix = newNextFireUnix;
|
||||
}
|
||||
if (newIntervalSeconds !== (s.interval_seconds || null)) {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,18 @@ export function paintAtomic(liveRoot, build) {
|
|||
liveRoot.replaceChildren(buf);
|
||||
}
|
||||
|
||||
// Relative age of a unix timestamp, coarsened to one unit ("5m ago").
|
||||
export function fmtAgo(unixSecs) {
|
||||
const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - unixSecs));
|
||||
// Epoch seconds from an API timestamp. The hive-c0re dashboard API
|
||||
// ships timestamps as RFC 3339 strings; a few feeds (rebuild queue,
|
||||
// meta inputs, turn stats) still carry unix-second numbers, so
|
||||
// numbers pass through unchanged.
|
||||
export function epochSec(ts) {
|
||||
return typeof ts === 'number' ? ts : Math.floor(Date.parse(ts) / 1000);
|
||||
}
|
||||
|
||||
// Relative age of a timestamp (RFC 3339 string or unix seconds),
|
||||
// coarsened to one unit ("5m ago").
|
||||
export function fmtAgo(ts) {
|
||||
const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - epochSec(ts)));
|
||||
if (ageSec < 60) return ageSec + 's ago';
|
||||
if (ageSec < 3600) return Math.floor(ageSec / 60) + 'm ago';
|
||||
if (ageSec < 86400) return Math.floor(ageSec / 3600) + 'h ago';
|
||||
|
|
|
|||
Loading…
Reference in a new issue