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
22 changes: 22 additions & 0 deletions api/test/widget-error.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,25 @@ test('no widget reports a user-facing failure with a plain Error', () => {
`Use ctx.fail(message, { kind }) so the message reaches the browser:\n${offenders.join('\n')}`,
);
});

/* One definition of a normalised upstream base URL.

The stats widget carried three spellings of it: a local diskBase helper and
two inline copies, one of which stripped a single trailing slash where
ctx.normalizeBase strips several and also trims. Which one a URL met decided
whether "http://nas:8080/ " reached the service.

connections keeps its own normBase, which is a different rule rather than a
copy of this one: it leaves a trailing slash in place, and one of its three
call sites strips slashes itself because that endpoint needs it. */
const OWN_BASE_RULE = new Set(['connections']);

test('no widget rebuilds the base-URL normalisation ctx already provides', () => {
const offenders = [];
for (const [name, p] of dataFiles) {
if (OWN_BASE_RULE.has(name)) continue;
const src = fs.readFileSync(p, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
if (/includes\(':\/\/'\)\s*\?/.test(src)) offenders.push(`${name}: hand-rolled scheme check`);
}
assert.deepEqual(offenders, [], `Use ctx.normalizeBase:\n${offenders.join('\n')}`);
});
18 changes: 8 additions & 10 deletions ui/widgets/stats/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ module.exports = async function (ctx) {
return systemSummary(ctx);
};

const diskBase = u => (u.includes('://') ? u : `http://${u}`).replace(/\/$/, '');

/* Config-time picker: one option per drive or pool, shared by every bay row. */
function diskDevices(ctx) {
return ctx.dispatchProvider(
Expand All @@ -25,9 +23,9 @@ function diskDevices(ctx) {
}

async function scrutinyDeviceOptions(ctx) {
const { config, fetchJSON } = ctx;
const { config, fetchJSON, normalizeBase } = ctx;
if (!config.scrutinyUrl) ctx.fail('Enter the Scrutiny URL first.', { kind: ctx.KIND.INVALID });
const r = await fetchJSON(diskBase(config.scrutinyUrl) + '/api/summary', { timeout: 8000 });
const r = await fetchJSON(normalizeBase(config.scrutinyUrl) + '/api/summary', { timeout: 8000 });
if (r.status >= 400) ctx.fail('Scrutiny HTTP ' + r.status);
const summary = r.data?.data?.summary || {};
const options = Object.values(summary)
Expand All @@ -48,10 +46,10 @@ function truenasStatus(ctx, r) {
}

async function truenasPoolOptions(ctx) {
const { config, fetchJSON } = ctx;
const { config, fetchJSON, normalizeBase } = ctx;
if (!config.truenasUrl) ctx.fail('Enter the TrueNAS URL first.', { kind: ctx.KIND.INVALID });
if (!config.truenasKey) ctx.fail('Enter the TrueNAS API key first.', { kind: ctx.KIND.INVALID });
const r = await fetchJSON(diskBase(config.truenasUrl) + '/api/v2.0/pool', {
const r = await fetchJSON(normalizeBase(config.truenasUrl) + '/api/v2.0/pool', {
headers: { Authorization: 'Bearer ' + config.truenasKey },
timeout: 8000,
});
Expand Down Expand Up @@ -109,12 +107,12 @@ async function systemSummary({ config, settings, metrics }) {
/* Disk Health (Scrutiny): maps the widget's configured bays (device_id per bay)
onto Scrutiny's SMART summary. */
async function diskHealthScrutiny(ctx) {
const { config, fetchJSON } = ctx;
const { config, fetchJSON, normalizeBase } = ctx;
const url = config.scrutinyUrl;
if (!url) ctx.fail('scrutinyUrl not configured', { kind: ctx.KIND.INVALID });
const bays = config.bays || [];

const base = url.includes('://') ? url.replace(/\/$/, '') : `http://${url.replace(/\/$/, '')}`;
const base = normalizeBase(url);
const r = await fetchJSON(base + '/api/summary', { timeout: 8000 });

const summary = r.data?.data?.summary || {};
Expand Down Expand Up @@ -145,14 +143,14 @@ async function diskHealthScrutiny(ctx) {
matched from /api/v2.0/pool. A pool's `healthy` flag is the per-bay status
(healthy → 0, unhealthy → 2, the same codes the widget uses for Scrutiny). */
async function diskHealthTrueNas(ctx) {
const { config, fetchJSON } = ctx;
const { config, fetchJSON, normalizeBase } = ctx;
const url = config.truenasUrl;
const key = config.truenasKey;
if (!url) ctx.fail('truenasUrl not configured', { kind: ctx.KIND.INVALID });
if (!key) ctx.fail('TrueNAS API key not configured', { kind: ctx.KIND.INVALID });
const bays = config.bays || [];

const base = url.includes('://') ? url.replace(/\/$/, '') : `http://${url.replace(/\/$/, '')}`;
const base = normalizeBase(url);
const r = await fetchJSON(base + '/api/v2.0/pool', {
headers: { Authorization: 'Bearer ' + key },
timeout: 8000,
Expand Down
Loading