From 18471be4aa6f1fbb2d837db520c9ef82851b2952 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:41:03 -0400 Subject: [PATCH] Document Live Activity polling and credential changes Badges share one reading per poll interval and treat an error answer as a failed poll. Changing Allow self-signed certificate or the port map clears a stored credential. Add the message for a response too large to read, and sync widget and script copies from the app. --- public/js/admin-app-form.js | 6 +- public/js/admin-auth.js | 4 +- public/js/admin-color-control.js | 2 +- public/js/admin-error.js | 1 + public/js/admin-list.js | 4 +- public/js/admin-settings.js | 6 +- public/js/admin-shared.js | 2 +- public/js/admin-widget-form.js | 4 +- public/js/admin.js | 26 +- public/js/dashboard.js | 8 +- public/js/widget-config-form.js | 6 +- public/js/widget-toolbox.js | 144 +++- public/widgets/backup/backup.html | 2 +- public/widgets/books/index.html | 2 +- public/widgets/clock/analog.html | 2 +- public/widgets/clock/digital.html | 178 +--- .../widgets/connections/connections-map.html | 2 +- .../widgets/connections/connections-vpn.html | 2 +- public/widgets/dashboard-switch/index.html | 2 +- public/widgets/disk-health/index.html | 2 +- public/widgets/dns/index.html | 2 +- public/widgets/github/contributions.html | 351 +++++--- public/widgets/github/demo.js | 28 +- public/widgets/github/i18n/de.json | 4 +- public/widgets/github/i18n/en.json | 4 +- public/widgets/github/i18n/es.json | 4 +- public/widgets/github/i18n/fa.json | 4 +- public/widgets/github/i18n/fr.json | 4 +- public/widgets/github/i18n/zh-Hans.json | 4 +- public/widgets/github/pullrequests.html | 364 +++----- public/widgets/github/widget.json | 4 +- public/widgets/nowplaying/index.html | 4 +- public/widgets/nowplaying/widget.json | 1 + public/widgets/system-summary/data.js | 4 +- public/widgets/system-summary/i18n/de.json | 8 + public/widgets/system-summary/i18n/en.json | 8 + public/widgets/system-summary/i18n/es.json | 8 + public/widgets/system-summary/i18n/fa.json | 8 + public/widgets/system-summary/i18n/fr.json | 8 + .../widgets/system-summary/i18n/zh-Hans.json | 8 + public/widgets/system-summary/index.html | 777 ++++++++++-------- public/widgets/system-summary/widget.json | 26 + public/widgets/weather/index.html | 2 +- src/content/docs/docs/badges.md | 4 +- src/content/docs/docs/security.md | 2 +- src/content/docs/docs/troubleshooting.md | 3 +- 46 files changed, 1132 insertions(+), 917 deletions(-) diff --git a/public/js/admin-app-form.js b/public/js/admin-app-form.js index 9a8f92b..b4ba93a 100644 --- a/public/js/admin-app-form.js +++ b/public/js/admin-app-form.js @@ -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'; diff --git a/public/js/admin-auth.js b/public/js/admin-auth.js index 34491d2..7450111 100644 --- a/public/js/admin-auth.js +++ b/public/js/admin-auth.js @@ -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'; diff --git a/public/js/admin-color-control.js b/public/js/admin-color-control.js index d9a8e39..57c0e2d 100644 --- a/public/js/admin-color-control.js +++ b/public/js/admin-color-control.js @@ -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'; diff --git a/public/js/admin-error.js b/public/js/admin-error.js index b369c8c..98cd58c 100644 --- a/public/js/admin-error.js +++ b/public/js/admin-error.js @@ -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', diff --git a/public/js/admin-list.js b/public/js/admin-list.js index cf31cc1..ef55965 100644 --- a/public/js/admin-list.js +++ b/public/js/admin-list.js @@ -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'; diff --git a/public/js/admin-settings.js b/public/js/admin-settings.js index ffec9b3..695d0c7 100644 --- a/public/js/admin-settings.js +++ b/public/js/admin-settings.js @@ -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 { @@ -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, diff --git a/public/js/admin-shared.js b/public/js/admin-shared.js index 8a18743..84eddb8 100644 --- a/public/js/admin-shared.js +++ b/public/js/admin-shared.js @@ -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'; diff --git a/public/js/admin-widget-form.js b/public/js/admin-widget-form.js index 192f2a7..9009848 100644 --- a/public/js/admin-widget-form.js +++ b/public/js/admin-widget-form.js @@ -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'; diff --git a/public/js/admin.js b/public/js/admin.js index dcdc0d3..e3d9938 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -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, @@ -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, @@ -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'; @@ -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'); @@ -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', diff --git a/public/js/dashboard.js b/public/js/dashboard.js index a0a4dca..a53a29a 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -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 { @@ -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) => { @@ -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); diff --git a/public/js/widget-config-form.js b/public/js/widget-config-form.js index d20ad70..7ce9b73 100644 --- a/public/js/widget-config-form.js +++ b/public/js/widget-config-form.js @@ -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, @@ -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'; diff --git a/public/js/widget-toolbox.js b/public/js/widget-toolbox.js index 34bbd1b..7914af0 100644 --- a/public/js/widget-toolbox.js +++ b/public/js/widget-toolbox.js @@ -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(); } @@ -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'; diff --git a/public/widgets/backup/backup.html b/public/widgets/backup/backup.html index b8486e7..7d1744e 100644 --- a/public/widgets/backup/backup.html +++ b/public/widgets/backup/backup.html @@ -123,7 +123,7 @@
-
- -
+
+ +
diff --git a/public/widgets/github/demo.js b/public/widgets/github/demo.js index 3f85fd8..b71df97 100644 --- a/public/widgets/github/demo.js +++ b/public/widgets/github/demo.js @@ -4,7 +4,6 @@ let _cal = null; function githubCalendar() { - const COLORS = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39']; let seed = 1337, total = 0; const rnd = () => { @@ -12,20 +11,19 @@ function githubCalendar() { return seed / 0x7fffffff; }; const weeks = []; - const start = new Date(); - start.setDate(start.getDate() - 52 * 7); - for (let w = 0; w < 53; w++) { - const days = []; - for (let d = 0; d < 7; d++) { - const r = rnd(); - const count = r < 0.45 ? 0 : Math.floor(rnd() * 14) + 1; - total += count; - const lvl = count === 0 ? 0 : count < 3 ? 1 : count < 6 ? 2 : count < 10 ? 3 : 4; - const date = new Date(start); - date.setDate(start.getDate() + w * 7 + d); - days.push({ contributionCount: count, date: date.toISOString().slice(0, 10), color: COLORS[lvl] }); - } - weeks.push({ contributionDays: days }); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const day = new Date(today); + day.setDate(day.getDate() - 52 * 7 - today.getDay()); + const iso = d => + `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + while (day <= today) { + if (day.getDay() === 0) weeks.push({ contributionDays: [] }); + const r = rnd(); + const count = r < 0.45 ? 0 : Math.floor(rnd() * 14) + 1; + total += count; + weeks[weeks.length - 1].contributionDays.push({ contributionCount: count, date: iso(day) }); + day.setDate(day.getDate() + 1); } return { view: 'contributions', weeks, totalContributions: total }; } diff --git a/public/widgets/github/i18n/de.json b/public/widgets/github/i18n/de.json index 2e7aa84..e29fac3 100644 --- a/public/widgets/github/i18n/de.json +++ b/public/widgets/github/i18n/de.json @@ -18,7 +18,9 @@ "githubPrFilters.opt.mentioned": "Erwähnt", "githubPrFilters.opt.review-requested": "Review angefragt", "ui.prAria": "GitHub-Pull-Requests", - "ui.noPrs": "Keine Pull Requests", + "ui.pullRequests": "Pull Requests", + "ui.lastYear": "Letztes Jahr", + "ui.noPrs": "Keine offenen Pull Requests", "ui.contributionsAria": "GitHub-Beiträge: {total} im letzten Jahr", "ui.viewAll": "alle anzeigen" } diff --git a/public/widgets/github/i18n/en.json b/public/widgets/github/i18n/en.json index df6739d..c719b66 100644 --- a/public/widgets/github/i18n/en.json +++ b/public/widgets/github/i18n/en.json @@ -18,7 +18,9 @@ "githubPrFilters.opt.mentioned": "Mentioned", "githubPrFilters.opt.review-requested": "Review requested", "ui.prAria": "GitHub pull requests", - "ui.noPrs": "No pull requests", + "ui.pullRequests": "Pull requests", + "ui.lastYear": "Last year", + "ui.noPrs": "No open pull requests", "ui.contributionsAria": "GitHub contributions: {total} in the last year", "ui.viewAll": "view all" } diff --git a/public/widgets/github/i18n/es.json b/public/widgets/github/i18n/es.json index 0ffc482..8a2c67c 100644 --- a/public/widgets/github/i18n/es.json +++ b/public/widgets/github/i18n/es.json @@ -18,7 +18,9 @@ "githubPrFilters.opt.mentioned": "Mencionado", "githubPrFilters.opt.review-requested": "Revisión solicitada", "ui.prAria": "Pull requests de GitHub", - "ui.noPrs": "No hay pull requests", + "ui.pullRequests": "Pull requests", + "ui.lastYear": "Último año", + "ui.noPrs": "No hay pull requests abiertas", "ui.contributionsAria": "Contribuciones de GitHub: {total} en el último año", "ui.viewAll": "ver todo" } diff --git a/public/widgets/github/i18n/fa.json b/public/widgets/github/i18n/fa.json index 4257232..675a1b7 100644 --- a/public/widgets/github/i18n/fa.json +++ b/public/widgets/github/i18n/fa.json @@ -18,7 +18,9 @@ "githubPrFilters.opt.mentioned": "نام‌برده‌شده", "githubPrFilters.opt.review-requested": "درخواست بازبینی", "ui.prAria": "درخواست‌های ادغام GitHub", - "ui.noPrs": "درخواست ادغامی نیست", + "ui.pullRequests": "درخواست‌های ادغام", + "ui.lastYear": "سال گذشته", + "ui.noPrs": "درخواست ادغام بازی نیست", "ui.contributionsAria": "مشارکت‌های GitHub: {total} در سال گذشته", "ui.viewAll": "مشاهدهٔ همه" } diff --git a/public/widgets/github/i18n/fr.json b/public/widgets/github/i18n/fr.json index 064700f..9dafdc1 100644 --- a/public/widgets/github/i18n/fr.json +++ b/public/widgets/github/i18n/fr.json @@ -18,7 +18,9 @@ "githubPrFilters.opt.mentioned": "Mentionné", "githubPrFilters.opt.review-requested": "Revue demandée", "ui.prAria": "Pull requests GitHub", - "ui.noPrs": "Aucune pull request", + "ui.pullRequests": "Pull requests", + "ui.lastYear": "Dernière année", + "ui.noPrs": "Aucune pull request ouverte", "ui.contributionsAria": "Contributions GitHub : {total} sur la dernière année", "ui.viewAll": "tout afficher" } diff --git a/public/widgets/github/i18n/zh-Hans.json b/public/widgets/github/i18n/zh-Hans.json index 0a0bf10..bc1b323 100644 --- a/public/widgets/github/i18n/zh-Hans.json +++ b/public/widgets/github/i18n/zh-Hans.json @@ -18,7 +18,9 @@ "githubPrFilters.opt.mentioned": "提到我的", "githubPrFilters.opt.review-requested": "请求我评审的", "ui.prAria": "GitHub 拉取请求", - "ui.noPrs": "没有拉取请求", + "ui.pullRequests": "拉取请求", + "ui.lastYear": "过去一年", + "ui.noPrs": "没有打开的拉取请求", "ui.contributionsAria": "GitHub 贡献:过去一年 {total} 次", "ui.viewAll": "查看全部" } diff --git a/public/widgets/github/pullrequests.html b/public/widgets/github/pullrequests.html index 24f2c79..f1923d4 100644 --- a/public/widgets/github/pullrequests.html +++ b/public/widgets/github/pullrequests.html @@ -4,262 +4,166 @@ - + GitHub Pull Requests
+

+ counts and charts: content with its own direction. Mirroring it runs the + charts' time axis right to left. --> +