mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
Surface the server's manner-of-death (crashed / clean_exit / silent) as a subordinate qualifier ON the Offline badge (not a 4th liveness state), on both the device list and device-detail. Rides livenessBadge. - Reliability-aware label (contract §10): clean_exit reads plainly on /player (reliable), "(best-effort)" on APK/.wgt. silent = "silent (no signal)". - Honest hover tooltip on every reason (both views), incl. silent = "external/violent: power loss, network, force-stop, or MDM/kill". Never fabricates a reason (no-reason -> plain Offline); state-gated (reason only on Offline); clears on re-online (matches the server). - Filter drill-in: <optgroup> "Offline by reason" -> Offline · silent / crashed / clean exit, matched via a data-offline-reason attribute (Offline·silent = the MDM-killed set — the Bold use case). Existing three-state filter (All/Healthy/Reconnecting/Offline) unchanged. - List label shortened to fit the pill (full text stays on detail; tooltip carries the full honesty both). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
4.3 KiB
JavaScript
73 lines
4.3 KiB
JavaScript
import { t } from './i18n.js';
|
|
|
|
// HTML escape helper — prevents XSS when inserting user data into innerHTML
|
|
export function esc(str) {
|
|
if (str == null) return '';
|
|
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
}
|
|
|
|
// v4 liveness badge. The patch4 server derives a 3-state liveness — 'healthy' / 'degraded'
|
|
// (temporarily reconnecting) / 'offline' — and emits it as `data.liveness` on dashboard:device-status.
|
|
// It is present on SOME emits only (the plain reconnect + disconnect emits, and any device object read
|
|
// from the DB, carry just the binary `status`), so we DEGRADE to the binary status when liveness is
|
|
// absent — nothing ever renders blank. 'provisioning' is a lifecycle state (never-paired), kept
|
|
// distinct from liveness. livenessState() is pure (unit-testable); livenessBadge() adds the i18n label.
|
|
const LIVENESS_LABEL_KEY = {
|
|
healthy: 'device.liveness.healthy',
|
|
degraded: 'device.liveness.degraded',
|
|
offline: 'device.liveness.offline',
|
|
provisioning: 'dashboard.awaiting_pairing',
|
|
};
|
|
export function livenessState(data) {
|
|
const lv = data && data.liveness;
|
|
if (lv === 'healthy' || lv === 'degraded' || lv === 'offline') return lv; // 3-state signal present
|
|
const st = data && data.status; // backward-compat: derive from binary status
|
|
if (st === 'provisioning') return 'provisioning';
|
|
if (st === 'online') return 'healthy';
|
|
if (st === 'offline') return 'offline';
|
|
return 'offline'; // unknown / no data yet -> safe default, never blank
|
|
}
|
|
// Exit-signal contract §8/§10 — honest, reliability-aware manner-of-death sub-label for an Offline
|
|
// device. clean_exit is RELIABLE only on the browser /player (pagehide+sendBeacon); best-effort on
|
|
// APK/.wgt, so we qualify it there rather than overstate certainty. crashed/silent are labeled plainly.
|
|
// Returns null when no reason is known (old data / never went offline) -> plain "Offline".
|
|
// short=true -> concise LIST label (drops the parenthetical qualifiers, which the tooltip still carries);
|
|
// full (default) -> DETAIL label with the reliability qualifier. Honesty is preserved either way: the
|
|
// full meaning lives in the tooltip (both views) and in the detail label.
|
|
function offlineReasonLabel(reason, clientType, short) {
|
|
if (reason === 'crashed') return t('device.exit.crashed');
|
|
if (reason === 'clean_exit') {
|
|
if (short) return t('device.exit.clean'); // list: "clean exit" (tooltip carries best-effort)
|
|
return clientType === 'player' ? t('device.exit.clean') : t('device.exit.clean_besteffort');
|
|
}
|
|
if (reason === 'silent') return short ? t('device.exit.silent_short') : t('device.exit.silent');
|
|
return null;
|
|
}
|
|
// Honest hover explanation of the manner of death — carries the contract's reliability (esp. 'silent'
|
|
// = external/violent, and best-effort clean_exit) so an operator isn't misled by a terse badge label.
|
|
function offlineReasonTip(reason, clientType) {
|
|
if (reason === 'crashed') return t('device.exit.crashed.tip');
|
|
if (reason === 'clean_exit') return clientType === 'player' ? t('device.exit.clean.tip') : t('device.exit.clean_besteffort.tip');
|
|
if (reason === 'silent') return t('device.exit.silent.tip');
|
|
return '';
|
|
}
|
|
export function livenessBadge(data, opts = {}) {
|
|
const state = livenessState(data);
|
|
let label = t(LIVENESS_LABEL_KEY[state]);
|
|
let title = '', reason = '';
|
|
if (state === 'offline') { // annotate Offline with the manner of death, if known
|
|
const r = data && data.offline_reason, ct = data && data.client_type;
|
|
const sub = offlineReasonLabel(r, ct, opts.short);
|
|
if (sub) { label += ' · ' + sub; title = offlineReasonTip(r, ct); reason = r || ''; }
|
|
}
|
|
return { state, label, title, reason }; // reason -> data-offline-reason (filter drill-in); '' unless offline+known
|
|
}
|
|
|
|
// Phase 2.1: the Phase 1 schema migration renamed the legacy 'superadmin'
|
|
// role to 'platform_admin'. Existing frontend checks still match the old
|
|
// string; this helper accepts both so we don't have to splatter the array
|
|
// at every call site. Use everywhere the UI gates on platform-level access.
|
|
export function isPlatformAdmin(user) {
|
|
return !!(user && (user.role === 'superadmin' || user.role === 'platform_admin'));
|
|
}
|