Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions public/js/admin-app-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import {
reveal,
setTogDisabled,
swapContent,
} from '/js/admin-shared.js?v=81ab2f92';
} from '/js/admin-shared.js?v=d0ef41ba';
import { createListbox } from '/js/listbox.js?v=11c869c4';
import { MAX_LABELS } from '/js/badge-logic.js?v=9e6d9d4b';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=426a5709';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=61f73e4d';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=cee08df9';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=a9a7e60c';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
import { iconSvg } from '/js/icon-set.js?v=34af798f';

Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { apiGet, apiPost } from '/js/admin-shared.js?v=81ab2f92';
import { apiGet, apiPost } from '/js/admin-shared.js?v=d0ef41ba';
import { initI18n, t } from '/js/i18n.js?v=5579776a';
import { loginErrorKey } from '/js/admin-error.js?v=61f73e4d';
import { loginErrorKey } from '/js/admin-error.js?v=a9a7e60c';
import { pwStrength } from '/js/password-strength.js?v=389e0ed0';
import { el, inp as inpById, qa } from '/js/utils.js?v=fdc0243f';
import { blockingScreenFor } from '/js/config-recovery.js?v=dbe542e1';
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=81ab2f92';
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=d0ef41ba';
import { t } from '/js/i18n.js?v=5579776a';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { qa, q } from '/js/utils.js?v=fdc0243f';
Expand Down
1 change: 1 addition & 0 deletions public/js/admin-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const BY_CODE = Object.freeze({
'blocked.private-address': 'adminError.privateAddress',
'invalid.retype': 'adminError.retype',
'upstream.redirect': 'adminError.redirect',
'upstream.too-large': 'adminError.responseTooLarge',
'network.tls-ignored': 'adminError.tlsIgnored',
'network.tls-untrusted': 'adminError.tlsUntrusted',
'blocked.read-only': 'adminError.readOnly',
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b';
import { snapshotItems } from '/js/admin-save-logic.js?v=ae65f9c8';
import { reorderItems } from '/js/admin-logic.js?v=fc7f0836';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=5e9c57c7';
import { paintIcon } from '/js/admin-shared.js?v=81ab2f92';
import { paintIcon } from '/js/admin-shared.js?v=d0ef41ba';
import { clr, el, focusFirst, initial, qa, setUserText } from '/js/utils.js?v=fdc0243f';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { t } from '/js/i18n.js?v=5579776a';
import { sizeLabel } from '/js/admin-widget-form.js?v=b05766fe';
import { sizeLabel } from '/js/admin-widget-form.js?v=ea4e9101';
import { widgetGlyph } from '/js/widget-glyphs.js?v=648cc374';
import { iconSvg } from '/js/icon-set.js?v=34af798f';

Expand Down
6 changes: 3 additions & 3 deletions public/js/admin-settings.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toast, apiGet, apiPost, errorText, reveal, swapContent } from '/js/admin-shared.js?v=81ab2f92';
import { socketProbeAdvice } from '/js/admin-error.js?v=61f73e4d';
import { toast, apiGet, apiPost, errorText, reveal, swapContent } from '/js/admin-shared.js?v=d0ef41ba';
import { socketProbeAdvice } from '/js/admin-error.js?v=a9a7e60c';
import { pwStrength } from '/js/password-strength.js?v=389e0ed0';
import { t } from '/js/i18n.js?v=5579776a';
import {
Expand All @@ -14,7 +14,7 @@ import { confirmText, promptModal } from '/js/modal.js?v=6b0320bd';
import { el, inp, setUserText } from '/js/utils.js?v=fdc0243f';
import { formatNumber } from '/js/format-number.js?v=349a741d';
import { serialWrites } from '/js/admin-save-logic.js?v=ae65f9c8';
import { renderColorControl } from '/js/admin-color-control.js?v=426a5709';
import { renderColorControl } from '/js/admin-color-control.js?v=cee08df9';
import { BACKDROP } from '/js/background.js?v=abd33088';
import {
ALLOWED_HOSTS_MAX,
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { recoversSession, toastHoldMs } from '/js/admin-logic.js?v=fc7f0836';
import { el, isolate, q } from '/js/utils.js?v=fdc0243f';
import { t } from '/js/i18n.js?v=5579776a';
import { errorAdvice } from '/js/admin-error.js?v=61f73e4d';
import { errorAdvice } from '/js/admin-error.js?v=a9a7e60c';
import { formatNumber } from '/js/format-number.js?v=349a741d';
import { iconChain } from '/js/icons.js?v=9c7b5111';
import { iconSvg } from '/js/icon-set.js?v=34af798f';
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-widget-form.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { state } from '/js/admin-state.js?v=af772a1b';
import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=81ab2f92';
import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=d0ef41ba';
import { createListbox } from '/js/listbox.js?v=11c869c4';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=0e9c05db';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=effb84f9';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=fc7f0836';
import { t } from '/js/i18n.js?v=5579776a';
Expand Down
26 changes: 17 additions & 9 deletions public/js/admin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=a707f78e';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=df3c72ef';
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=c5eecf98';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=c54047e3';
import { recoveryShown } from '/js/config-recovery.js?v=dbe542e1';
import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=6d020ac7';
import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=348d1d69';
import { resolveAdminSection } from '/js/admin-logic.js?v=fc7f0836';
import {
afterImport,
Expand All @@ -19,7 +19,7 @@ import {
settingsDirty,
showBgFields,
showWallpaperFile,
} from '/js/admin-settings.js?v=7fbd9499';
} from '/js/admin-settings.js?v=08a3c4c9';
import {
apiGet,
apiPost,
Expand All @@ -32,9 +32,9 @@ import {
ShownError,
setReauthHandler,
toast,
} from '/js/admin-shared.js?v=81ab2f92';
} from '/js/admin-shared.js?v=d0ef41ba';
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=b05766fe';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=ea4e9101';
import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86';
import { formatNumber } from '/js/format-number.js?v=349a741d';
import { initGlideSelect, syncGlideSelect } from '/js/glide-select.js?v=8b39e9d0';
Expand Down Expand Up @@ -335,6 +335,17 @@ function openModal(idx) {
state.spaths = ex.map(e => (typeof e === 'string' ? e : e.path)).filter(Boolean);
state.slegacySum = state.spaths.length >= 2;
}
/* The save reads a summed or single-path badge's style from the first label.
Without this seed the stored style is lost. */
const custom = item?.monitoring?.activity?.custom;
if (!state.slabels[state.spaths[0]] && state.spaths.length && custom && typeof custom === 'object') {
state.slabels[state.spaths[0]] = {
name: '',
unit: typeof custom.unit === 'string' ? custom.unit : '',
color: typeof custom.color === 'string' && custom.color ? custom.color : 'info',
min: custom.min == null ? '' : String(custom.min),
};
}

const isEdit = idx != null;
const evTitle = el('ev-title');
Expand Down Expand Up @@ -630,9 +641,6 @@ async function doSave(orig) {
actInt: Math.min(3600, Math.max(10, parseInt(inp('f-bint')?.value || '30', 10))),
actParams: serializeKvRows(state._bpar),
actHeaders: serializeKvRows(state._bhdr),
actColor: inp('act-col-val')?.value || '#0289ff',
custUnit: inp('bcust-unit')?.value?.trim() || '',
custMin: parseInt(inp('bcust-min')?.value || '', 10),
staticEn: inp('static-en')?.checked || false,
staticLabel: inp('f-static-label')?.value?.trim() || '',
staticColor: inp('static-col-val')?.value || 'info',
Expand Down
8 changes: 5 additions & 3 deletions public/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { html, setHtml, raw } from '/js/html.js?v=c71f8903';
import { initI18n, t, currentLang } from '/js/i18n.js?v=5579776a';
import { blockingScreenFor, showBlockingScreen } from '/js/config-recovery.js?v=dbe542e1';
import { pwStrength, passwordMismatch } from '/js/password-strength.js?v=389e0ed0';
import { setupErrorKey } from '/js/admin-error.js?v=61f73e4d';
import { setupErrorKey } from '/js/admin-error.js?v=a9a7e60c';
import { sanitizeItemLinks } from '/js/link-url.js?v=54adb40f';
import { DOCK_MAX } from '/js/limits.js?v=31048a24';
import {
Expand Down Expand Up @@ -248,13 +248,15 @@ function bset(id, type, val) {
function folderBadge(folder) {
const children = (folder.children || []).map(id => items.find(i => i.id === id)).filter(Boolean);
let actSum = 0,
hasHealth = false;
hasHealth = false,
stale = false;
const labels = [],
values = [];
for (const c of children) {
const s = badgeState[c.id] || {};
if (s.health) hasHealth = true;
const ca = c.monitoring?.activity;
if (s.activityStale) stale = true;
const own = !ca?.combine && Array.isArray(ca?.labels) && ca.labels.length ? ca.labels : null;
if (own && Array.isArray(s.values)) {
own.forEach((l, n) => {
Expand All @@ -268,7 +270,7 @@ function folderBadge(folder) {
not raise the folder's either. */
if (s.activity >= badgeMinimum(ca?.custom)) actSum += s.activity;
}
return { health: hasHealth, activity: actSum, labels, values };
return { health: hasHealth, activity: actSum, labels, values, activityStale: stale };
}

const mkWrap = (item, sz, r, isz, cls) => _mkWrap(item, sz, r, isz, cls, breg);
Expand Down
6 changes: 3 additions & 3 deletions public/js/widget-config-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import { t } from '/js/i18n.js?v=5579776a';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=81ab2f92';
import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=d0ef41ba';
import { createListbox } from '/js/listbox.js?v=11c869c4';
import { renderColorControl } from '/js/admin-color-control.js?v=426a5709';
import { renderColorControl } from '/js/admin-color-control.js?v=cee08df9';
import {
seedCarried,
applyOptionSet,
Expand All @@ -14,7 +14,7 @@ import {
groupBounds,
visibleFieldFlags,
} from '/js/admin-logic.js?v=fc7f0836';
import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=61f73e4d';
import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=a9a7e60c';
import { qi } from '/js/utils.js?v=fdc0243f';
import { formatNumber } from '/js/format-number.js?v=349a741d';

Expand Down
144 changes: 138 additions & 6 deletions public/js/widget-toolbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,43 @@ export function readableInk(hex, min = 4.5) {
return '#' + rgb.map(v => Math.round(v).toString(16).padStart(2, '0')).join('');
}

/** @param {unknown} value @returns {number[] | null} */
function _rgbOf(value) {
const s = String(value ?? '').trim();
let m = /^#([0-9a-f]{6})$/i.exec(s);
if (m) return [0, 2, 4].map(i => parseInt(m[1].slice(i, i + 2), 16));
m = /^#([0-9a-f]{3})$/i.exec(s);
if (m) return [...m[1]].map(c => parseInt(c + c, 16));
m = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.exec(s);
return m ? m.slice(1, 4).map(v => Math.min(255, Number(v))) : null;
}

const _cardLum = () => (_hostTheme() === 'light' ? 1 : _lum([28, 28, 30]));

/** Contrast of a colour against the card, as a ratio. NaN when it cannot be read.
@param {string} colour @returns {number} */
export function cardContrast(colour) {
const rgb = _rgbOf(colour);
if (!rgb) return NaN;
const a = _lum(rgb) + 0.05;
const b = _cardLum() + 0.05;
return Math.max(a, b) / Math.min(a, b);
}

/** Moves a colour toward black on the light card or toward white on the dark
one, only as far as min:1 against that card needs. For text: a fill keeps
the colour the user picked.
@param {string} colour hex or rgb() @param {number} [min] @returns {string} #rrggbb, or the input when unreadable */
export function contrastInk(colour, min = 4.5) {
let rgb = _rgbOf(colour);
if (!rgb) return colour;
const light = _hostTheme() === 'light';
const card = _cardLum() + 0.05;
const ratio = c => (light ? card / (_lum(c) + 0.05) : (_lum(c) + 0.05) / card);
for (let k = 0; k < 40 && ratio(rgb) < min; k++) rgb = rgb.map(v => (light ? v * 0.95 : v + (255 - v) * 0.08));
return '#' + rgb.map(v => Math.round(v).toString(16).padStart(2, '0')).join('');
}

export function theme() {
return _hostTheme();
}
Expand Down Expand Up @@ -238,24 +275,119 @@ function reducedMotion() {
}
}

/* opts: { color='#0a84ff', track='rgba(255,255,255,0.10)', height=6, radius=3 } */
/** Shrink a figure's type until its text fits its box, down to `min` px. The
element must clip its overflow. Starts again from the stylesheet size.
@param {HTMLElement} el @param {number} [min] */
export function fitText(el, min = 20) {
el.style.fontSize = '';
let size = parseFloat(getComputedStyle(el).fontSize) || min;
while (el.scrollWidth > el.clientWidth && size > min) {
size -= 1;
el.style.fontSize = size + 'px';
}
}

/* opts: { color='#0a84ff', track='rgba(255,255,255,0.10)', height=6, radius=3 }
track: null paints no track, so the page styles .tb-bar per theme. */
export function barFill(percent, opts = {}) {
const pct = Math.max(0, Math.min(100, Number(percent) || 0));
const h = opts.height != null ? opts.height : 6;
const radius = opts.radius != null ? opts.radius : 3;
const track = document.createElement('div');
track.style.cssText =
`position:relative;width:100%;height:${h}px;border-radius:${radius}px;` +
`background:${opts.track || 'rgba(255,255,255,0.10)'};overflow:hidden`;
track.className = 'tb-bar';
track.style.cssText = `position:relative;width:100%;height:${h}px;border-radius:${radius}px;overflow:hidden`;
if (opts.track !== null) track.style.backgroundColor = opts.track || 'rgba(255,255,255,0.10)';
const fill = document.createElement('div');
fill.className = 'tb-bar-fill';
fill.style.cssText =
`position:absolute;left:0;top:0;bottom:0;width:${pct}%;border-radius:${radius}px;` +
`background:${opts.color || '#0a84ff'}` +
`position:absolute;left:0;top:0;bottom:0;width:${pct}%;border-radius:${radius}px` +
(reducedMotion() ? '' : ';transition:width .4s ease');
fill.style.backgroundColor = colorOrFallback(opts.color, '#0a84ff');
track.appendChild(fill);
return track;
}

/** Rounded columns, newest on the right, for a short history at a glance.
Built once; update() moves heights only, so a poll never rebuilds the DOM.
track: null paints no track, so the page styles .tb-col per theme.

@param {{ count?: number, color?: string, track?: string | null, gap?: number, radius?: number }} [opts]
@returns {{ el: HTMLElement, update: (values: unknown[], scale?: { min?: number, max?: number, dim?: (v: number) => boolean }) => void, indexAt: (clientX: number) => number, mark: (index: number | null) => void }} */
export function columns(opts = {}) {
const count = Math.max(1, Math.floor(Number(opts.count) || 24));
const radius = opts.radius != null ? opts.radius : 3;
const color = colorOrFallback(opts.color, '#0a84ff');
const el = document.createElement('div');
el.className = 'tb-cols';
el.style.cssText = `display:flex;align-items:stretch;height:100%;gap:${opts.gap != null ? opts.gap : 3}px`;
/** @type {HTMLElement[]} */
const fills = [];
/** @type {HTMLElement[]} */
const cols = [];
/** @type {HTMLElement | null} */
let marker = null;
for (let i = 0; i < count; i++) {
const col = document.createElement('div');
col.className = 'tb-col';
col.style.cssText = `flex:1 1 0;position:relative;overflow:hidden;border-radius:${radius}px`;
if (opts.track !== null) col.style.backgroundColor = opts.track || 'rgba(255,255,255,0.10)';
const fill = document.createElement('div');
fill.className = 'tb-col-fill';
fill.style.cssText = `position:absolute;left:0;right:0;bottom:0;height:0;border-radius:${radius}px`;
fill.style.backgroundColor = color;
col.appendChild(fill);
el.appendChild(col);
cols.push(col);
fills.push(fill);
}
return {
el,
update(values, scale = {}) {
const list = Array.isArray(values) ? values.slice(-count) : [];
const offset = count - list.length;
const min = Number.isFinite(scale.min) ? Number(scale.min) : 0;
const max = Number.isFinite(scale.max) && Number(scale.max) > min ? Number(scale.max) : min + 100;
for (let i = 0; i < count; i++) {
const v = i < offset ? null : list[i - offset];
const fill = fills[i];
/* Keep the sliver. Without it a zero reading looks like a missing one. */
if (typeof v !== 'number' || !Number.isFinite(v)) {
fill.style.height = '0';
fill.style.opacity = '';
continue;
}
const share = Math.max(0, Math.min(1, (v - min) / (max - min)));
fill.style.height = `max(2px, ${(share * 100).toFixed(2)}%)`;
fill.style.opacity = scale.dim && scale.dim(v) ? '0.45' : '';
}
},
/** The column under a pointer, so the whole plot is the hit target.
@param {number} clientX @returns {number} */
indexAt(clientX) {
const box = el.getBoundingClientRect();
const share = box.width > 0 ? (clientX - box.left) / box.width : 1;
return Math.max(0, Math.min(count - 1, Math.floor(share * count)));
},
/** A thin marker on one column, or none. track: null leaves its colour to
the page as .tb-col-mark.
@param {number | null} index */
mark(index) {
if (index === null || !Number.isInteger(index) || index < 0 || index >= count) {
marker?.remove();
return;
}
if (!marker) {
marker = document.createElement('div');
marker.className = 'tb-col-mark';
marker.style.cssText =
'position:absolute;top:0;bottom:0;left:50%;width:2px;border-radius:1px;transform:translateX(-50%)';
if (opts.track !== null) marker.style.backgroundColor = 'rgba(255,255,255,0.9)';
}
cols[index].appendChild(marker);
},
};
}

/* A widget is an iframe and does not load the i18n module. The language arrives
on the iframe URL. */
const _lang = new URLSearchParams(location.search).get('lang') || 'en';
Expand Down
2 changes: 1 addition & 1 deletion public/widgets/backup/backup.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
<body>
<div class="widget" id="widget" role="group" aria-label="Backup status"></div>
<script type="module">
import { poll, fetchData, openUrl, linkTo, html, setHtml, loadStrings, wt, errorState, errorLine, formatNumber } from '/js/widget-toolbox.js?v=5ecee136';
import { poll, fetchData, openUrl, linkTo, html, setHtml, loadStrings, wt, errorState, errorLine, formatNumber } from '/js/widget-toolbox.js?v=556ee947';
await loadStrings();
document.getElementById('widget').setAttribute('aria-label', wt('ui.backupStatus', 'Backup status'));
const id = new URLSearchParams(location.search).get('id') || '';
Expand Down
2 changes: 1 addition & 1 deletion public/widgets/books/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
<span class="sr" id="sr" aria-live="polite"></span>
</div>
<script type="module">
import { poll, linkTo, loadStrings, wt, formatNumber, errorState } from '/js/widget-toolbox.js?v=5ecee136';
import { poll, linkTo, loadStrings, wt, formatNumber, errorState } from '/js/widget-toolbox.js?v=556ee947';
await loadStrings();
document.getElementById('lbl').textContent = wt('ui.reading', 'Reading');
document.getElementById('card').setAttribute('aria-label', wt('widget.label', 'Books'));
Expand Down
Loading
Loading