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
49 changes: 24 additions & 25 deletions public/js/admin-app-form.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { clr, el, focusFirst, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=9a9bfb54';
import { clr, el, focusFirst, initial, inp as inpById, isolate, q as qSel, qa, qi, tgt } from '/js/utils.js?v=fdc0243f';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { loadLocalIcons, resolveIcon, iconChain, cdnIconRef, splitIconRef } from '/js/icons.js?v=9c8c550c';
import { loadLocalIcons, resolveIcon, iconChain, cdnIconRef, splitIconRef } from '/js/icons.js?v=9c7b5111';
import { state } from '/js/admin-state.js?v=af772a1b';
import { DOCK_MAX } from '/js/limits.js?v=31048a24';
import { formatNumber } from '/js/format-number.js?v=349a741d';
import {
isDockBlocked,
clearsStoredSecret,
Expand All @@ -11,9 +12,10 @@ import {
nextActiveIndex,
sameIconName,
} from '/js/admin-logic.js?v=fc7f0836';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { t } from '/js/i18n.js?v=5579776a';
import {
toast,
errorText,
responseError,
apiGet,
apiPost,
Expand All @@ -22,11 +24,11 @@ import {
reveal,
setTogDisabled,
swapContent,
} from '/js/admin-shared.js?v=f5551857';
import { createListbox } from '/js/listbox.js?v=30c1b9d1';
} from '/js/admin-shared.js?v=81ab2f92';
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=233683ad';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=f82486b2';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=426a5709';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=61f73e4d';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
import { iconSvg } from '/js/icon-set.js?v=34af798f';

Expand Down Expand Up @@ -119,7 +121,7 @@ export function buildAppForm(body, item) {
<div class="grp" id="ipw">
<div class="icon-src-anchor">
<div class="row icon-src-row">
<span class="icon-prev" id="ipv">${state.siurl ? html`<img src="${resolveIcon(state.siurl)}" alt="" id="ipv-img">` : html`<span>${(item?.label || '?')[0]?.toUpperCase() || '?'}</span>`}</span>
<span class="icon-prev" id="ipv">${state.siurl ? html`<img src="${resolveIcon(state.siurl)}" alt="" id="ipv-img">` : html`<span>${initial(item?.label)}</span>`}</span>
<input class="icon-srch" id="ip-in" type="text" autocomplete="off" role="combobox" aria-expanded="false" aria-controls="iprs" aria-autocomplete="list" aria-describedby="ip-status" aria-label="${t('app.icon')}" placeholder="${t('app.iconPh')}" value="${state.siurl}">
<button type="button" class="row-btn" id="ip-upload-lbl">${t('app.upload')}</button>
<input type="file" id="ip-upload" class="file-hidden" tabindex="-1" aria-hidden="true" accept=".svg,.png,.ico,image/svg+xml,image/png,image/x-icon">
Expand All @@ -134,7 +136,7 @@ export function buildAppForm(body, item) {
<div class="grp">
<div class="row"><span class="rl">${t('app.showInDock')}</span>${tog('f-dock', !!item?.dock, t('app.showInDock'))}</div>
</div>
${dockBlocked ? html`<p class="grp-tip" id="dock-full-tip">${t('app.dockFull', { max: DOCK_MAX })}</p>` : ''}
${dockBlocked ? html`<p class="grp-tip" id="dock-full-tip">${t('app.dockFull', { max: formatNumber(DOCK_MAX) })}</p>` : ''}

<p class="grp-hdr" role="heading" aria-level="2">${t('app.badge')}</p>
<div class="grp">
Expand Down Expand Up @@ -169,7 +171,7 @@ export function buildAppForm(body, item) {
<div id="auth-row-wrap">
<div class="row"><span class="rl">${t('app.authentication')}</span>${tog('auth-en', !!(act.params || act.headers), t('app.authentication'))}</div>
<div id="auth-sub" class="reveal${act.params?.length || act.headers?.length ? ' open' : ''}"><div class="reveal-in">
<div class="row kv-hdr"><span class="rl">${t('app.addToUrl')} <span class="rl-sub">(query params)</span></span></div>
<div class="row kv-hdr"><span class="rl">${t('app.addToUrl')} <span class="rl-sub">${t('app.queryParams')}</span></span></div>
<div id="bpar-rows" class="kv-rows"></div>
<div class="row kv-hdr"><span class="rl">${t('app.addToHeader')}</span></div>
<div id="bhdr-rows" class="kv-rows"></div>
Expand Down Expand Up @@ -229,8 +231,8 @@ export function buildAppForm(body, item) {
});
state._bpar = normKvRows(act.params);
state._bhdr = normKvRows(act.headers);
renderKvRows(el('bpar-rows'), state._bpar, 'key=value');
renderKvRows(el('bhdr-rows'), state._bhdr, 'X-Api-Key=…');
renderKvRows(el('bpar-rows'), state._bpar, t('app.value'));
renderKvRows(el('bhdr-rows'), state._bhdr, '…');

wireIcon();
if (state.siurl) updPrev();
Expand Down Expand Up @@ -366,7 +368,7 @@ function renderActLabels(host) {
hdr.dataset.idx = String(i);
setHtml(
hdr,
html`<span class="albl-grip" aria-hidden="true">${raw(GRIP_SVG)}</span><span role="heading" aria-level="3">${t('app.labelN', { n: i + 1 })}</span>`,
html`<span class="albl-grip" aria-hidden="true">${raw(GRIP_SVG)}</span><span role="heading" aria-level="3">${t('app.labelN', { n: formatNumber(i + 1) })}</span>`,
);
const ctl = document.createElement('span');
ctl.className = 'albl-ctl';
Expand Down Expand Up @@ -727,7 +729,7 @@ function wireIcon() {
const form = new FormData();
form.append('icon', file, file.name);
const r = await fetch('/api/icons/upload', { method: 'POST', body: form });
if (!r.ok) throw new Error(await responseError(r));
if (!r.ok) throw await responseError(r);
const d = await r.json();
await loadLocalIcons();
state.siurl = d.filename;
Expand All @@ -736,9 +738,9 @@ function wireIcon() {
renderIconVariants([]);
ipClose();
updPrev();
toast(t('toast.uploaded', { name: d.filename }));
toast(t('toast.uploaded', { name: isolate(d.filename) }));
} catch (e) {
toast(t('toast.uploadFailed', { err: e.message }), 'err');
toast(t('toast.uploadFailed', { err: errorText(e) }), 'err');
} finally {
upBtn.textContent = origText;
upInput.value = '';
Expand Down Expand Up @@ -840,7 +842,7 @@ function showIPRes(list, rawInput) {
function setInitialGlyph(p) {
const l = inpById('f-lbl')?.value || '?';
const s = document.createElement('span');
s.textContent = (l[0] || '?').toUpperCase();
s.textContent = initial(l);
p.replaceChildren(s);
}
/* Several attempts are in flight at once and the half-typed ones finish last.
Expand Down Expand Up @@ -887,11 +889,10 @@ async function testPing() {
const skipTls = inpById('f-skip-tls')?.checked || false;
try {
const r = await apiPost('/api/ping', { url, skipTls });
st.textContent = r.ok
? '✓ ' + t('app.reachable', { status: r.status })
: '✗ ' + t('app.httpError', { status: r.status });
if (r.ok) st.textContent = '✓ ' + t('app.reachable', { status: r.status });
else st.textContent = '✗ ' + (r.status ? t('app.httpError', { status: r.status }) : errorText(r));
} catch (e) {
st.textContent = '✗ ' + e.message;
st.textContent = '✗ ' + errorText(e);
}
}

Expand Down Expand Up @@ -941,12 +942,10 @@ function renderKvRows(host, rows, ph) {
host.appendChild(add);
}

const defaultValuePlaceholder = ph => ph.split('=')[1] || 'value';

function kvRowEl(host, rows, row, ph) {
const rowEl = document.createElement('div');
rowEl.className = 'kv-row';
const valPh = row.secret && row.valueSet && row.value === '' ? 'Configured' : defaultValuePlaceholder(ph);
const valPh = row.secret && row.valueSet && row.value === '' ? t('common.configured') : ph;
setHtml(
rowEl,
html`
Expand Down Expand Up @@ -975,7 +974,7 @@ function kvRowEl(host, rows, row, ph) {
if (clearsStoredSecret(row, cEl.checked)) {
row.valueSet = false;
vEl.value = '';
vEl.placeholder = defaultValuePlaceholder(ph);
vEl.placeholder = ph;
}
};
dEl.onclick = () => {
Expand Down
13 changes: 8 additions & 5 deletions public/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { apiGet, apiPost } from '/js/admin-shared.js?v=f5551857';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { apiGet, apiPost } from '/js/admin-shared.js?v=81ab2f92';
import { initI18n, t } from '/js/i18n.js?v=5579776a';
import { loginErrorKey } from '/js/admin-error.js?v=61f73e4d';
import { pwStrength } from '/js/password-strength.js?v=389e0ed0';
import { el, inp as inpById, qa } from '/js/utils.js?v=9a9bfb54';
import { blockingScreenFor } from '/js/config-recovery.js?v=706fc9a7';
import { el, inp as inpById, qa } from '/js/utils.js?v=fdc0243f';
import { blockingScreenFor } from '/js/config-recovery.js?v=dbe542e1';

export async function checkAuth(onLogin) {
try {
const d = await apiGet('/api/auth/check');
if (!d.enabled || d.authenticated) return true;
await initI18n(d.language || 'en');
document.title = t('nav.pageTitle');
showLoginScreen(onLogin);
return false;
} catch (e) {
Expand Down Expand Up @@ -67,7 +70,7 @@ function showLoginScreen(onLogin) {
onLogin?.();
} catch (e) {
if (err) {
err.textContent = e.message || t('login.incorrect');
err.textContent = t(loginErrorKey(e));
err.style.display = 'block';
}
if (pw) {
Expand Down
6 changes: 3 additions & 3 deletions public/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=f5551857';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=81ab2f92';
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=9a9bfb54';
import { qa, q } from '/js/utils.js?v=fdc0243f';
import { iconSvg } from '/js/icon-set.js?v=34af798f';
import { HUE_NAMES, ROLE_NAMES, TILE_KEYWORDS, pageTheme, tileColor } from '/js/palette.js?v=3fb8ae43';

Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-drag.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76';
import { state } from '/js/admin-state.js?v=af772a1b';
import { snapshotItems } from '/js/admin-save-logic.js?v=8389782f';
import { qa } from '/js/utils.js?v=9a9bfb54';
import { snapshotItems } from '/js/admin-save-logic.js?v=ae65f9c8';
import { qa } from '/js/utils.js?v=fdc0243f';

/** @type {(before: unknown) => void} */
let _save = () => {};
Expand Down
68 changes: 67 additions & 1 deletion public/js/admin-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ const BY_CODE = Object.freeze({
'upstream.redirect': 'adminError.redirect',
'network.tls-ignored': 'adminError.tlsIgnored',
'network.tls-untrusted': 'adminError.tlsUntrusted',
'blocked.read-only': 'adminError.readOnly',
'blocked.rate-limit': 'toast.tooManyAttempts',
'invalid.too-large': 'toast.imageTooLarge',
'invalid.file-type': 'adminError.fileType',
'invalid.url': 'adminError.invalidUrl',
'upstream.refused': 'adminError.socketRefused',
'upstream.not-docker': 'adminError.notSocketProxy',
'blocked.unresolved': 'adminError.unreachable',
'invalid.duplicate-id': 'adminError.duplicateId',
'invalid.missing-children': 'adminError.missingChildren',
'invalid.unsafe-link': 'adminError.unsafeLink',
'invalid.dock-full': 'app.dockFull',
'network.refused': 'adminError.refused',
'network.not-found': 'adminError.hostNotFound',
'network.unreachable': 'adminError.hostUnreachable',
'network.reset': 'adminError.connectionReset',
'network.not-http': 'adminError.notHttp',
'network.self-signed': 'adminError.selfSigned',
'network.self-signed-public': 'adminError.selfSignedPublic',
'network.tls-expired': 'adminError.tlsExpired',
'timeout.no-answer': 'adminError.timedOut',
'blocked.demo': 'adminError.demoOutbound',
});

const BY_KIND = Object.freeze({
Expand Down Expand Up @@ -52,16 +74,43 @@ export function readError(e) {
return { kind, code, detail };
}

/* Placeholders only. A value is an id, a status or a limit, never a sentence. */
/** @param {Record<string, unknown> | null} detail @returns {Record<string, unknown> | null} */
function detailVars(detail) {
if (!detail) return null;
/** @type {Record<string, unknown>} */
const vars = {};
for (const [k, v] of Object.entries(detail)) {
if (k === 'reason') continue;
if (typeof v === 'number' || (typeof v === 'string' && v.length <= 200)) vars[k] = v;
}
return Object.keys(vars).length ? vars : null;
}

function adviceFor(read) {
const { kind, code, detail } = read;
const status = detail && typeof detail.status === 'number' ? detail.status : null;
if (code === 'upstream.status' && status !== null) return { key: statusKey(status), vars: { status } };
if (BY_CODE[code]) {
return status !== null ? { key: BY_CODE[code], vars: { status } } : { key: BY_CODE[code] };
const vars = detailVars(detail);
return vars ? { key: BY_CODE[code], vars } : { key: BY_CODE[code] };
}
return { key: BY_KIND[kind] || BY_KIND[KIND.INTERNAL] };
}

/** What to show for a failed request to the API's own routes.
@param {unknown} e @returns {{ key: string, vars?: Record<string, unknown> }} */
export function errorAdvice(e) {
return adviceFor(readError(e));
}

/* The probe's hint already says why nothing answered and what to change. */
/** @param {unknown} probe @returns {{ key: string, vars?: Record<string, unknown> }} */
export function socketProbeAdvice(probe) {
const read = readError(probe);
return read.code === KIND.NETWORK ? { key: 'adminError.noConnection' } : adviceFor(read);
}

export function badgeErrorAdvice(e) {
const read = readError(e);
const { kind, code, detail } = read;
Expand All @@ -83,3 +132,20 @@ export function optionsErrorAdvice(e) {
const { tone, key, vars, code } = badgeErrorAdvice(e);
return vars ? { tone, code, key, vars } : { tone, code, key };
}

/** @param {unknown} e @returns {string} */
export function loginErrorKey(e) {
const { kind, code } = readError(e);
if (code === 'blocked.rate-limit') return 'toast.tooManyAttempts';
if (kind === KIND.AUTH) return 'login.incorrect';
return 'adminError.genericInternal';
}

/* A first-run password is refused as auth or as changed when another tab or
device set one first. */
/** @param {unknown} e @returns {string} */
export function setupErrorKey(e) {
const { kind, code } = readError(e);
if (kind === KIND.AUTH || code === 'invalid.password-changed') return 'toast.passwordChangedElsewhere';
return 'setup.failed';
}
23 changes: 10 additions & 13 deletions public/js/admin-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
reads and writes them too. */

import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b';
import { snapshotItems } from '/js/admin-save-logic.js?v=8389782f';
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=444456dd';
import { paintIcon } from '/js/admin-shared.js?v=f5551857';
import { clr, el, focusFirst, qa, setUserText } from '/js/utils.js?v=9a9bfb54';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=5e9c57c7';
import { paintIcon } from '/js/admin-shared.js?v=81ab2f92';
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=1f1ea9c1';
import { sizeLabel } from '/js/admin-widget-form.js?v=53ceafd3';
import { t } from '/js/i18n.js?v=5579776a';
import { sizeLabel } from '/js/admin-widget-form.js?v=b05766fe';
import { widgetGlyph } from '/js/widget-glyphs.js?v=648cc374';
import { iconSvg } from '/js/icon-set.js?v=34af798f';

Expand Down Expand Up @@ -61,9 +61,6 @@ function moveRow(item, dir, opts = {}) {
export function mkRow(item, idx, { indent = false, childIdx = null, folderId = null } = {}) {
const row = document.createElement('div');
row.className = 'row drow';
if (indent)
row.style.cssText =
'padding-left:28px;background:rgba(255,255,255,.02);border-left:2px solid var(--bd);margin-left:8px;border-radius:0 var(--rs) var(--rs) 0;';
const _filtering = !!(filter.q || filter.type !== 'all');
row.draggable = !_filtering;
row.dataset.itemId = item.id;
Expand Down Expand Up @@ -102,8 +99,8 @@ export function mkRow(item, idx, { indent = false, childIdx = null, folderId = n
const glyph = widgetGlyph(state._widgetReg?.[item.widgetType]?.glyph);
ico.appendChild(svgNode(glyph || SIZE_ICONS[item.widgetSize] || SIZE_ICONS.medium));
} else if (item.iconUrl) {
paintIcon(ico, item.iconUrl, (item.label || '?')[0].toUpperCase(), 'width:28px;height:28px;object-fit:contain;');
} else ico.textContent = (item.label || item.id || '?')[0].toUpperCase();
paintIcon(ico, item.iconUrl, initial(item.label), 'width:28px;height:28px;object-fit:contain;');
} else ico.textContent = initial(item.label || item.id);
const inf = document.createElement('div');
inf.className = 'rinf';
const isFolderRow = item.type === 'folder';
Expand All @@ -121,7 +118,7 @@ export function mkRow(item, idx, { indent = false, childIdx = null, folderId = n
chevron.textContent = '▼';
chevron.style.transform = collapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
chevron.id = 'chev-' + item.id;
nm.append(chevron, document.createTextNode(item.label));
nm.append(chevron, setUserText(document.createElement('span'), item.label));
nm.onclick = e => {
e.stopPropagation();
if (collapsedFolders.has(item.id)) {
Expand All @@ -138,7 +135,7 @@ export function mkRow(item, idx, { indent = false, childIdx = null, folderId = n
mt.className = 'rmt';
if (item.type === 'widget') {
const wt = item.widgetType || 'custom';
const wtLabel = state._widgetReg?.[wt]?.label || 'Custom';
const wtLabel = state._widgetReg?.[wt]?.label || t('widgetCfg.typeCustom');
mt.textContent = t('widgetCfg.meta', { type: wtLabel, size: sizeLabel(item.widgetSize || 'medium') });
} else if (item.type === 'folder') mt.textContent = t('folder.appsCount', { count: (item.children || []).length });
else if (item.system === 'settings') mt.textContent = t('home.opensSettings');
Expand Down
Loading
Loading