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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`ALLOW_PRIVATE_IPS=true` as the setting that allows it. The advice was added
in 1.6.0 but never appeared.

- A widget settings Fetch marks a blocked private address as a warning, the
same as a badge test, instead of a hard error.

## [1.6.0] - 2026-08-13

### Added
Expand Down
1 change: 1 addition & 0 deletions ui/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ html{background:var(--bg-color,var(--bg-outer))}
.row-status{font-size:13px;margin-inline-end:8px;color:var(--dm)}
.row-status.ok{color:var(--ok)}
.row-status.err{color:var(--danger)}
.row-status.warn{color:var(--warning)}

/* ── Textarea row (Add to URL / Add to Header) ── */
.kv-hdr{padding-bottom:4px}
Expand Down
1 change: 1 addition & 0 deletions ui/css/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
--accent: var(--sy-teal);
--accent-strong: var(--sy-teal-hi);
--danger: var(--sy-red);
--warning: var(--sy-orange);
--success: var(--sy-green);

--sy-radius-lg:14px;
Expand Down
2 changes: 1 addition & 1 deletion ui/js/admin-app-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ async function fetchBadge() {
/* Branch on the error's `kind`, never on words inside its message. */
const advice = badgeErrorAdvice(e);
if (st) {
st.style.cssText = 'margin-top:4px;color:' + (advice.tone === TONE.WARN ? '#ff9f0a' : '#ff453a');
st.style.cssText = 'margin-top:4px;color:' + (advice.tone === TONE.WARN ? 'var(--warning)' : 'var(--danger)');
st.textContent = advice.tone === TONE.WARN ? advice.message : '✗ ' + advice.message;
}
if (advice.openAuth) {
Expand Down
12 changes: 8 additions & 4 deletions ui/js/admin-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,14 @@ export function badgeErrorAdvice(e) {
};
}

export function optionsErrorText(e) {
/* Same shape as badgeErrorAdvice: a settings Fetch and a badge test report the
same failures and must not disagree about the tone. */
export function optionsErrorAdvice(e) {
const read = readError(e);
const { kind, message } = read;
if (kind === KIND.INVALID && message) return message;
if (blockedForPrivateAddress(read)) return `${message} ${PRIVATE_ADDRESS_ADVICE}`;
return 'Fetch failed: ' + (message || 'Request failed.');
if (blockedForPrivateAddress(read)) {
return { tone: TONE.WARN, message: `${message} ${PRIVATE_ADDRESS_ADVICE}` };
}
if (kind === KIND.INVALID && message) return { tone: TONE.ERROR, message };
return { tone: TONE.ERROR, message: 'Fetch failed: ' + (message || 'Request failed.') };
}
12 changes: 7 additions & 5 deletions ui/js/widget-config-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
groupBounds,
visibleFieldFlags,
} from '/js/admin-logic.js?v=056a11e9';
import { optionsErrorText } from '/js/admin-error.js?v=af729113';
import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=af729113';
import { qi } from '/js/utils.js?v=84d58686';

const PE =
Expand Down Expand Up @@ -269,8 +269,9 @@ function _picklist(field, value, ctx, size) {
: 'No options found';
status.className = 'row-status ok';
} catch (e) {
status.textContent = optionsErrorText(e);
status.className = 'row-status err';
const advice = optionsErrorAdvice(e);
status.textContent = advice.message;
status.className = 'row-status ' + (advice.tone === TONE.WARN ? 'warn' : 'err');
} finally {
btn.disabled = false;
}
Expand Down Expand Up @@ -353,8 +354,9 @@ function _select(field, value, ctx, config = {}) {
: 'No options found';
status.className = 'row-status ok';
} catch (e) {
status.textContent = optionsErrorText(e);
status.className = 'row-status err';
const advice = optionsErrorAdvice(e);
status.textContent = advice.message;
status.className = 'row-status ' + (advice.tone === TONE.WARN ? 'warn' : 'err');
} finally {
btn.disabled = false;
}
Expand Down
27 changes: 15 additions & 12 deletions ui/test/admin-error.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { badgeErrorAdvice, optionsErrorText, KIND, TONE } from '../js/admin-error.js';
import { badgeErrorAdvice, optionsErrorAdvice, KIND, TONE } from '../js/admin-error.js';

/* api/src/api-error.js is CommonJS (the server half of the codebase is), so it
needs createRequire rather than a plain import. */
Expand Down Expand Up @@ -84,8 +84,11 @@ test('a block without the reason code gets no private-address advice', () => {

test('the widget options Fetch gives the same advice', () => {
const blocked = { kind: KIND.BLOCKED, message: 'The request was blocked.', detail: { reason: 'private-address' } };
assert.match(optionsErrorText(blocked), /ALLOW_PRIVATE_IPS=true/);
assert.match(optionsErrorText({ kind: KIND.BLOCKED, message: 'The request was blocked.' }), /^Fetch failed: /);
assert.match(optionsErrorAdvice(blocked).message, /ALLOW_PRIVATE_IPS=true/);
assert.equal(optionsErrorAdvice(blocked).tone, TONE.WARN, 'a fixable setting is not a hard error');
const plain = optionsErrorAdvice({ kind: KIND.BLOCKED, message: 'The request was blocked.' });
assert.match(plain.message, /^Fetch failed: /);
assert.equal(plain.tone, TONE.ERROR);
});

test('other blocked reasons are still shown verbatim', () => {
Expand All @@ -109,22 +112,22 @@ test('an unknown future kind degrades instead of crashing an older frontend', ()
assert.equal(a.message, 'Too many requests.');
});

/* ── optionsErrorText (P11-2) ─────────────────────────────────────────────── */
/* ── optionsErrorAdvice (P11-2) ───────────────────────────────────────────── */

test('the retype instruction is shown on its own, without a failure prefix', async () => {
const { optionsErrorText } = await import('../js/admin-error.js');
const { optionsErrorAdvice } = await import('../js/admin-error.js');
const msg =
'This configuration has changed since it was saved, so the stored credential was not used. Enter the credential to test these settings.';
assert.equal(optionsErrorText({ kind: KIND.INVALID, message: msg }), msg);
assert.equal(optionsErrorAdvice({ kind: KIND.INVALID, message: msg }).message, msg);
});

test('any other failure keeps the Fetch failed prefix', async () => {
const { optionsErrorText } = await import('../js/admin-error.js');
assert.equal(optionsErrorText({ kind: KIND.NETWORK, message: 'boom' }), 'Fetch failed: boom');
assert.equal(optionsErrorText(new Error('boom')), 'Fetch failed: boom');
const { optionsErrorAdvice } = await import('../js/admin-error.js');
assert.equal(optionsErrorAdvice({ kind: KIND.NETWORK, message: 'boom' }).message, 'Fetch failed: boom');
assert.equal(optionsErrorAdvice(new Error('boom')).message, 'Fetch failed: boom');
});

test('optionsErrorText tolerates an error with no message', async () => {
const { optionsErrorText } = await import('../js/admin-error.js');
assert.equal(optionsErrorText({ kind: KIND.INVALID }), 'Fetch failed: Request failed.');
test('optionsErrorAdvice tolerates an error with no message', async () => {
const { optionsErrorAdvice } = await import('../js/admin-error.js');
assert.equal(optionsErrorAdvice({ kind: KIND.INVALID }).message, 'Fetch failed: Request failed.');
});
Loading