v0.34.6
This commit is contained in:
1 parent
e037190c14
commit
5cb3b37627
37 files changed
+1031
-138
No files matched your search
@@ -7,7 +7,8 @@ const assert = require('node:assert/strict');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const active = read('src/admin/web/style.css');
|
||||
const unused = read('src/admin/web/unused.css');
|
||||
// Review-only archives are intentionally optional in reproducible source ZIPs.
|
||||
const unused = fs.existsSync(path.join(root, 'src/admin/web/unused.css')) ? read('src/admin/web/unused.css') : null;
|
||||
const docs = read('src/admin/web/docs.css');
|
||||
const archived = [
|
||||
'auth-ip-card', 'certificate-dialog', 'config-columns', 'overview-grid',
|
||||
@@ -18,7 +19,7 @@ const archived = [
|
||||
for (const name of archived) {
|
||||
const cssClass = new RegExp(`\\.${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![-\\w])`);
|
||||
assert.ok(!cssClass.test(active), `${name} must not remain in the active stylesheet`);
|
||||
assert.ok(cssClass.test(unused), `${name} must be reviewable in unused.css`);
|
||||
if (unused !== null) assert.ok(cssClass.test(unused), `${name} must be reviewable in unused.css`);
|
||||
}
|
||||
for (const name of ['vh-setting-section', 'dashboard-overview-grid', 'vh-override-fields',
|
||||
'refresh-settings-grid', 'docker-backend-ip-settings', 'acl-profile-layout']) {
|
||||
@@ -29,7 +30,7 @@ for (const file of [
|
||||
'src/admin/panel_assets.rs', 'src/admin/documentation.rs',
|
||||
]) assert.ok(!read(file).includes('unused.css'), `${file} must not load or serve unused.css`);
|
||||
assert.ok(!/@import[^;]*unused\.css/.test(active));
|
||||
assert.match(unused, /NOT INCLUDED by the admin panel/);
|
||||
if (unused !== null) assert.match(unused, /NOT INCLUDED by the admin panel/);
|
||||
assert.match(docs, /\.swagger-ui \.opblock/);
|
||||
for (const style of ['square', 'subtle', 'rounded', 'extra']) {
|
||||
assert.ok(active.includes(`html[data-corner-style=${style}]`));
|
||||
@@ -37,10 +38,10 @@ for (const style of ['square', 'subtle', 'rounded', 'extra']) {
|
||||
}
|
||||
for (const name of ['app-startup-spin', 'app-startup-reveal']) {
|
||||
assert.ok(active.includes(`@keyframes ${name}`));
|
||||
assert.ok(!unused.includes(`@keyframes ${name}`));
|
||||
if (unused !== null) assert.ok(!unused.includes(`@keyframes ${name}`));
|
||||
}
|
||||
assert.ok(!active.includes('--backdrop-soft'));
|
||||
assert.ok(!active.includes('var(--panel-strong)'));
|
||||
assert.ok(!active.includes('var(--border)'));
|
||||
assert.match(read('CHANGELOG.md'), /CSS maintenance:/);
|
||||
console.log('CSS used/unused segregation, asset isolation and corner-style regression: PASS');
|
||||
// The review-only CSS history is not a runtime or packaging dependency.
|
||||
console.log('CSS active/optional archive isolation and corner-style regression: PASS');
|
||||
@@ -0,0 +1,187 @@
|
||||
// Isolated DOM regression for per-Docker-vhost live proxy logs and Publish switch.
|
||||
// No native RustProxy process or browser is required for this check.
|
||||
'use strict';
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const read = name => fs.readFileSync(path.join(root, name), 'utf8');
|
||||
const docker = read('src/admin/web/docker.js');
|
||||
const css = read('src/admin/web/style.css');
|
||||
const management = read('src/admin/web/management.js');
|
||||
const en = JSON.parse(read('langs/en_US.json')).translations;
|
||||
const pl = JSON.parse(read('langs/pl_pl.json')).translations;
|
||||
for (const pack of [en, pl]) {
|
||||
assert.ok(pack.dockerPublish && pack.liveLogs && pack.dockerLiveAccessLogsHelp);
|
||||
}
|
||||
assert.match(docker, /publish\.setAttribute\("role", "switch"\)/);
|
||||
assert.match(css, /\.docker-publish-field \.docker-publish:checked::before/);
|
||||
assert.match(css, /\.docker-publish-field \.docker-publish:focus-visible/);
|
||||
assert.match(css, /\.docker-service-quick-actions/);
|
||||
assert.match(management, /if \(selected\) ids\.add\(selected\)/);
|
||||
assert.match(read('Cargo.toml'), /^version = "0\.34\.6"$/m);
|
||||
assert.match(read('Cargo.lock'), /name = "rust-proxy"\nversion = "0\.34\.6"/);
|
||||
|
||||
class Node {
|
||||
constructor(tag) {
|
||||
this.tag = tag; this.children = []; this.parentElement = null; this.dataset = {};
|
||||
this.className = ''; this.listeners = {}; this.attributes = {};
|
||||
this.open = false; this.hidden = false; this.disabled = false;
|
||||
this.checked = false; this.value = ''; this._text = ''; this.isConnected = true;
|
||||
const classes = new Set();
|
||||
this.classList = {
|
||||
add: token => classes.add(token), remove: token => classes.delete(token),
|
||||
toggle: (token, state) => { const wanted = state === undefined ? !classes.has(token) : state; if (wanted) classes.add(token); else classes.delete(token); return wanted; },
|
||||
contains: token => classes.has(token),
|
||||
};
|
||||
}
|
||||
append(...children) { this.children.push(...children); children.forEach(child => { if (child instanceof Node) child.parentElement = this; }); }
|
||||
replaceChildren(...children) { this.children = []; this._text = ''; this.append(...children); }
|
||||
get textContent() { return this._text + this.children.map(child => child.textContent ?? String(child)).join(''); }
|
||||
set textContent(value) { this.replaceChildren(); this._text = String(value); }
|
||||
setAttribute(key, value) { this.attributes[key] = String(value); }
|
||||
getAttribute(key) { return this.attributes[key]; }
|
||||
addEventListener(name, fn) { (this.listeners[name] ||= []).push(fn); }
|
||||
async emit(name) { for (const fn of this.listeners[name] || []) await fn({target:this}); }
|
||||
click() { return this.emit('click'); }
|
||||
}
|
||||
const controls = new Map();
|
||||
const visits = [], saves = [];
|
||||
let saveResult = true;
|
||||
const context = {
|
||||
window: {rustProxyLogs: {open(id) { visits.push(id); }}, rustProxyAclPicker: {}, rustProxyPreferences: {}},
|
||||
document: {createElementNS: (_ns,tag) => new Node(tag), querySelectorAll: () => []},
|
||||
$: id => controls.get(id) || null,
|
||||
el(tag, content, cls) { const node = new Node(tag); if (content !== undefined) node.textContent = content; if (cls) node.className = cls; return node; },
|
||||
t: key => en[key] || key,
|
||||
locale: () => 'en-US',
|
||||
localStorage: {getItem:() => null},
|
||||
rustProxyRefresh: {register(){}, sync(){}},
|
||||
envelope: {config: {docker_agents:{agents:[]}, listeners:{endpoints:[
|
||||
{id:'http', enabled:true, binding:{address:'0.0.0.0:80',tls:false}},
|
||||
{id:'https', enabled:true, binding:{address:'0.0.0.0:8443',tls:true}},
|
||||
]}}},
|
||||
latestStatus:null, performance:{now:()=>0}, authenticated:false, currentPanel:'overview',
|
||||
toast(message) { throw new Error('Unexpected toast: ' + message); },
|
||||
};
|
||||
vm.createContext(context);
|
||||
assert.ok(docker.trimEnd().endsWith('})();'));
|
||||
const injected = docker.trimEnd().slice(0, -5) + `
|
||||
// Replace the unrelated configuration editors and persistence with controlled stubs.
|
||||
aliasEditor = () => ({node:el('div'), input:el('input'), read:()=>[], count:()=>0});
|
||||
targetOptions = () => [];
|
||||
aclEditor = timeoutEditor = healthEditor = passiveHealthEditor = () => el('details');
|
||||
saveAssignment = async (agentId, row, enabled) => window._saveAssignment(agentId, row, enabled);
|
||||
window._dockerActions = {assignmentRow, proxiedUrl, setRoutes(routes) { poolRoutes = routes; }};
|
||||
})();`;
|
||||
context.window._saveAssignment = async (agentId, row, published) => { saves.push([agentId,row.dataset.key,published]); return saveResult; };
|
||||
vm.runInContext(injected, context);
|
||||
const actions = context.window._dockerActions;
|
||||
const agent = {id:'docker-1',name:'Docker 1',base_domain:'example.test',listener_ids:['http','https'],enabled:true};
|
||||
const service = {key:'container:app:8080/tcp',technical_name:'app-8080',container_name:'app',image:'myapp:v1',private_port:8080,bindings:[{bind_ip:'0.0.0.0',public_port:8080}]};
|
||||
const assignment = {service_key:service.key,hostname:'app',enabled:true};
|
||||
const logId = 'docker-agent-41a1b2c3d4e5';
|
||||
actions.setRoutes([{agent_id:agent.id,service_key:service.key,pool_id:logId}]);
|
||||
const row = actions.assignmentRow(agent,null,service,assignment,false);
|
||||
const [head,editor,metrics] = row.children;
|
||||
assert.equal(editor.tag,'details'); assert.equal(metrics.tag,'details', 'published route retains collapsed metrics');
|
||||
const [publishLabel,,headActions] = head.children;
|
||||
const publish = publishLabel.children[1];
|
||||
assert.equal(publishLabel.className,'docker-publish-field');
|
||||
assert.equal(publish.getAttribute('role'),'switch');
|
||||
assert.equal(publish.checked,true);
|
||||
assert.equal(publish.getAttribute('aria-checked'),'true');
|
||||
const [runtime,quickActions] = headActions.children;
|
||||
assert.equal(runtime.className, 'docker-runtime');
|
||||
const [openApp,openLogs] = quickActions.children;
|
||||
assert.equal(openApp.textContent,en.dockerOpenApp);
|
||||
assert.equal(openApp.href,'https://app.example.test:8443/', 'Open app honors nested listener TLS and port');
|
||||
assert.equal(openLogs.textContent,en.liveLogs);
|
||||
assert.equal(openLogs.title,en.dockerLiveAccessLogsHelp);
|
||||
assert.equal(openLogs.disabled,false);
|
||||
assert.equal(openLogs.hidden,false);
|
||||
function testLiveLogSelection() {
|
||||
// Execute the actual live log dialog controller independently from other admin modules.
|
||||
const start = management.indexOf('(() => {\n const dialog = $("logs-dialog");');
|
||||
assert.ok(start > 0);
|
||||
const end = management.indexOf('\n})();', start) + '\n})();'.length;
|
||||
const logController = management.slice(start, end);
|
||||
class LogNode extends Node {
|
||||
constructor(tag) { super(tag); this.value = ''; }
|
||||
replaceChildren(...children) {
|
||||
super.replaceChildren(...children);
|
||||
if (this.tag === 'select') this.value = this.children[0]?.value || '';
|
||||
}
|
||||
querySelectorAll() { return []; }
|
||||
show() { this.open = true; }
|
||||
showModal() { this.open = true; }
|
||||
close() { this.open = false; }
|
||||
}
|
||||
const logControls = new Map();
|
||||
const $ = id => {
|
||||
if (!logControls.has(id)) {
|
||||
const tag = ['log-vhost','log-method','log-status','log-outcome'].includes(id) ? 'select' : 'div';
|
||||
logControls.set(id, new LogNode(tag));
|
||||
}
|
||||
return logControls.get(id);
|
||||
};
|
||||
$('log-outcome').value = 'all';
|
||||
const logContext = {
|
||||
document:{createElement:tag => new LogNode(tag)},
|
||||
window:{rustProxyPreferences:{load:async()=>({live_log_columns:['time','vhost']})}},
|
||||
$, t:key => en[key] || key, el(tag,value,cls) {const node=new LogNode(tag); if(value!==undefined) node.textContent=value; if(cls)node.className=cls; return node;},
|
||||
authenticated:true, envelope:{config:{vhosts:[{id:'ordinary-vhost'}]}}, lastLogs:{entries:[]},
|
||||
formatTime:time=>String(time), toast(){throw Error('unexpected toast')},
|
||||
};
|
||||
vm.createContext(logContext);
|
||||
vm.runInContext(logController,logContext);
|
||||
logContext.window.rustProxyLogs.open('docker-agent-41a1b2c3d4e5');
|
||||
assert.equal($('log-vhost').value, 'docker-agent-41a1b2c3d4e5');
|
||||
const entry = (vhost) => ({vhost,time_ms:123,method:'GET',path:'/',host:'app.example.test',status:200,cache:'MISS',outcome:'ok'});
|
||||
// A refresh containing only requests from another Vhost must not remove the chosen ID.
|
||||
logContext.lastLogs = {entries:[entry('ordinary-vhost')]};
|
||||
logContext.window.rustProxyLogs.render();
|
||||
assert.equal($('log-vhost').value,'docker-agent-41a1b2c3d4e5');
|
||||
assert.match($('log-count').textContent, /^0 \/ 1/);
|
||||
logContext.lastLogs = {entries:[entry('ordinary-vhost'),entry('docker-agent-41a1b2c3d4e5')]};
|
||||
logContext.window.rustProxyLogs.render();
|
||||
assert.match($('log-count').textContent, /^1 \/ 2/);
|
||||
assert.equal($('log-vhost').value,'docker-agent-41a1b2c3d4e5');
|
||||
$('log-vhost').value = '';
|
||||
logContext.window.rustProxyLogs.render();
|
||||
assert.match($('log-count').textContent, /^2 \/ 2/, 'manual All vhosts selection still works');
|
||||
}
|
||||
|
||||
(async () => {
|
||||
testLiveLogSelection();
|
||||
await openLogs.click();
|
||||
assert.deepEqual(visits,[logId], 'logs open for the returned generated ID, not the display hostname');
|
||||
publish.checked = false;
|
||||
await publish.emit('change');
|
||||
assert.deepEqual(saves.at(-1),[agent.id,service.key,false]);
|
||||
assert.equal(publish.getAttribute('aria-checked'),'false');
|
||||
assert.equal(openLogs.hidden,true);
|
||||
assert.equal(openApp.hidden,true);
|
||||
assert.equal(publish.disabled,false);
|
||||
publish.checked = true;
|
||||
await publish.emit('change');
|
||||
assert.deepEqual(saves.at(-1),[agent.id,service.key,true]);
|
||||
assert.equal(openLogs.hidden,false);
|
||||
assert.equal(openApp.hidden,false);
|
||||
assert.equal(publish.getAttribute('aria-checked'),'true');
|
||||
saveResult = false;
|
||||
publish.checked = false;
|
||||
await publish.emit('change');
|
||||
assert.equal(publish.checked,true, 'failed unpublish reverts state');
|
||||
assert.equal(publish.getAttribute('aria-checked'),'true', 'ARIA state rolls back');
|
||||
assert.equal(openLogs.hidden,false, 'related action rolls back');
|
||||
saveResult = true;
|
||||
actions.setRoutes([]);
|
||||
const missingRoute = actions.assignmentRow(agent,null,service,assignment,false);
|
||||
const missingLogs = missingRoute.children[0].children[2].children[1].children[1];
|
||||
assert.equal(missingLogs.disabled,true, 'no generated route ID disables logs rather than showing all vhosts');
|
||||
await missingLogs.click();
|
||||
assert.deepEqual(visits,[logId], 'missing ID never opens unfiltered logs');
|
||||
console.log('Docker publish switch, rollback, live log targeting, app links: PASS');
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,139 @@
|
||||
// Run with: node tests/docker_vhost_metrics.cjs
|
||||
// Source contracts and isolated DOM/metric sampling regression for Docker vhosts.
|
||||
'use strict';
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const source = name => fs.readFileSync(path.join(root, name), 'utf8');
|
||||
const docker = source('src/admin/web/docker.js');
|
||||
const core = source('src/admin/web/app/core.js');
|
||||
const html = source('src/admin/web/index.html');
|
||||
const css = source('src/admin/web/style.css');
|
||||
const en = JSON.parse(source('langs/en_US.json')).translations;
|
||||
const pl = JSON.parse(source('langs/pl_pl.json')).translations;
|
||||
|
||||
assert.match(html, /id="docker-metrics-toggle-all"/);
|
||||
assert.match(docker, /if \(agent\.enabled !== false && assignment && assignment\.enabled !== false\) row\.append\(statisticsSection\(agent, key\)\)/);
|
||||
assert.match(docker, /"docker-service-editor docker-service-metrics"/);
|
||||
assert.ok(docker.indexOf('row.append(editor);') < docker.indexOf('row.append(statisticsSection(agent, key))'));
|
||||
for (const key of ['status', 'vhRequests', 'vhActive', 'vhErrors', 'vhLatency', 'vhTraffic', 'vhLifetimeChart']) {
|
||||
assert.ok(docker.includes(`"${key}"`), `missing ordinary Vhost metric: ${key}`);
|
||||
}
|
||||
for (const key of ['dockerTrafficStatistics', 'dockerExpandAllMetrics', 'dockerCollapseAllMetrics']) {
|
||||
assert.ok(en[key] && pl[key], `missing translated label: ${key}`);
|
||||
}
|
||||
for (const className of ['vhost-live-grid', 'vhost-live-metric', 'vhost-chart', 'vhost-chart-line']) {
|
||||
assert.ok(css.includes(`.${className}`), `existing ordinary Vhost style missing: ${className}`);
|
||||
}
|
||||
assert.match(docker, /status\?\.vhost_metrics/);
|
||||
assert.match(docker, /rustProxyRateSampling\.difference/);
|
||||
assert.match(docker, /docker-service-editor:not\(\.docker-service-metrics\)\[open\]/);
|
||||
assert.match(docker, /expandedMetrics\.clear\(\); metricHistory\.clear\(\)/);
|
||||
assert.match(core, /window\.rustProxyDocker\?\.renderStatus\(s\)/);
|
||||
|
||||
// Keep the production module intact. Expose only local functions for this
|
||||
// self-contained DOM stub; no browser, network, or Rust server is involved.
|
||||
class Node {
|
||||
constructor(tag) { this.tag = tag; this.children = []; this.dataset = {}; this.className = ''; this.listeners = {}; this.attributes = {}; this.open = false; this.disabled = false; this._text = ''; }
|
||||
append(...children) { this.children.push(...children); }
|
||||
replaceChildren(...children) { this.children = [...children]; this._text = ''; }
|
||||
get textContent() { return this._text + this.children.map(node => node.textContent).join(''); }
|
||||
set textContent(value) { this.replaceChildren(); this._text = String(value); }
|
||||
setAttribute(name, value) { this.attributes[name] = String(value); }
|
||||
getAttribute(name) { return this.attributes[name]; }
|
||||
addEventListener(name, callback) { (this.listeners[name] ||= []).push(callback); }
|
||||
dispatch(name) { for (const callback of this.listeners[name] || []) callback({target:this}); }
|
||||
click() { this.dispatch('click'); }
|
||||
}
|
||||
const controls = new Map([['docker-metrics-toggle-all', new Node('button')], ['docker-discovery-list', new Node('div')]]);
|
||||
let sections = [], now = 1000;
|
||||
const document = {
|
||||
hidden: false,
|
||||
createElementNS(ns, tag) { return new Node(tag); },
|
||||
querySelectorAll(selector) { if (selector === '#docker-discovery-list .docker-agent-card') return []; assert.equal(selector, '#docker-discovery-list .docker-service-metrics'); return sections; },
|
||||
addEventListener() { },
|
||||
};
|
||||
const context = {
|
||||
window: {addEventListener(){}, rustProxyPreferences:{}, rustProxyAclPicker:{}},
|
||||
document, performance: {now:() => now},
|
||||
localStorage:{getItem(){return null;}},
|
||||
$: id => controls.get(id) || null,
|
||||
el(tag, value, classes) { const node = new Node(tag); if (value !== undefined) node.textContent = value; if (classes) node.className = classes; return node; },
|
||||
t: key => en[key] || key,
|
||||
fmt: value => Number(value).toLocaleString('en-US', {maximumFractionDigits:1}),
|
||||
rateBytes: value => `${(Number(value)/1024).toFixed(1)} KiB`,
|
||||
locale: () => 'en-US',
|
||||
rustProxyRefresh: {register(){}, current(){return {overview_seconds:3};}},
|
||||
latestStatus:null, authenticated:false, currentPanel:'overview',
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(source('src/admin/web/app/rate-sampling.js'), context);
|
||||
const marker = '})();';
|
||||
assert.ok(docker.trimEnd().endsWith(marker));
|
||||
const injected = docker.trimEnd().slice(0, -marker.length) + `
|
||||
window._metricsTest = {
|
||||
statisticsSection, renderDockerMetrics, syncMetricsToggle,
|
||||
setRuntime(routes, pools) { poolRoutes = routes; poolIndex = new Map(pools.map(pool => [pool.id, pool])); },
|
||||
};
|
||||
})();`;
|
||||
vm.runInContext(injected, context);
|
||||
const metrics = context.window._metricsTest;
|
||||
const agent = {id:'docker-1'};
|
||||
const first = metrics.statisticsSection(agent, 'service/a');
|
||||
const second = metrics.statisticsSection(agent, 'service-b');
|
||||
sections = [first, second];
|
||||
const button = controls.get('docker-metrics-toggle-all');
|
||||
metrics.syncMetricsToggle();
|
||||
assert.ok(sections.every(section => !section.open), 'default collapsed');
|
||||
assert.equal(button.textContent, en.dockerExpandAllMetrics);
|
||||
assert.equal(button.disabled, false);
|
||||
assert.equal(first.className, 'docker-service-editor docker-service-metrics');
|
||||
assert.equal(first.children[0].tag, 'summary');
|
||||
assert.equal(first.children[0].children[0].textContent, en.dockerTrafficStatistics);
|
||||
assert.equal(first.children[1].className, 'vhost-live-grid');
|
||||
assert.equal(first.children[1].children.length, 6);
|
||||
button.click();
|
||||
assert.ok(sections.every(section => section.open), 'one click expands every host');
|
||||
assert.equal(button.textContent, en.dockerCollapseAllMetrics);
|
||||
const refreshed = metrics.statisticsSection(agent, 'service/a');
|
||||
assert.equal(refreshed.open, true, 'expanded state survives re-render');
|
||||
button.click();
|
||||
assert.ok(sections.every(section => !section.open), 'second click collapses all');
|
||||
first.open = true; first.dispatch('toggle');
|
||||
assert.equal(button.textContent, en.dockerExpandAllMetrics, 'partial expansion still offers expand all');
|
||||
|
||||
metrics.setRuntime([
|
||||
{agent_id:'docker-1',service_key:'service/a',pool_id:'docker-generated-a'},
|
||||
{agent_id:'docker-1',service_key:'service-b',pool_id:'docker-generated-b'},
|
||||
], [
|
||||
{id:'docker-generated-a',health_mode:'http',members:[{status:'up'}]},
|
||||
{id:'docker-generated-b',health_mode:'tcp',members:[{status:'down'}]},
|
||||
]);
|
||||
const base = {requests:26288, active:7,errors:129,ewma_latency_ms:20.4,bytes_in:15000,bytes_out:2000};
|
||||
metrics.renderDockerMetrics({vhost_metrics:{'docker-generated-a':base,'docker-generated-b':{...base,requests:22}}});
|
||||
assert.equal(first.dockerMetricView.fields.status.textContent, '1/1 Up');
|
||||
assert.equal(first.dockerMetricView.fields.vhRequests.textContent, '26,288');
|
||||
assert.equal(first.dockerMetricView.fields.vhActive.textContent, '7');
|
||||
assert.equal(first.dockerMetricView.fields.vhErrors.textContent, '129');
|
||||
assert.equal(first.dockerMetricView.fields.vhLatency.textContent, '20.4 ms');
|
||||
assert.match(first.dockerMetricView.chartValue.textContent, /req\/s .* peak/);
|
||||
now += 3000;
|
||||
const increased = {...base, requests:26300, bytes_out:32720};
|
||||
metrics.renderDockerMetrics({vhost_metrics:{'docker-generated-a':increased,'docker-generated-b':{...base,requests:22}}, backend_pools:[
|
||||
{id:'docker-generated-a',health_mode:'http',members:[{status:'down'}]}
|
||||
]});
|
||||
assert.equal(first.dockerMetricView.fields.vhTraffic.textContent, '10.0 KiB/s');
|
||||
assert.equal(first.dockerMetricView.fields.status.textContent, '0/1 Up', 'live status overrides the less frequent Docker inventory snapshot');
|
||||
assert.match(first.dockerMetricView.chartValue.textContent, /^4\.0 req\/s/);
|
||||
assert.ok(first.dockerMetricView.line.getAttribute('points').length > 5, 'chart receives sampled points');
|
||||
// Repaint of an unchanged status snapshot must not create a false 0 req/s sample.
|
||||
now += 500;
|
||||
metrics.renderDockerMetrics({vhost_metrics:{'docker-generated-a':increased}});
|
||||
assert.match(first.dockerMetricView.chartValue.textContent, /^4\.0 req\/s/);
|
||||
sections = [];
|
||||
context.window.rustProxyDocker.reset();
|
||||
assert.equal(button.disabled, true, 'no active hosts disables expand-all');
|
||||
assert.equal(metrics.statisticsSection(agent, 'service/a').open, false, 'sign-out resets expansion');
|
||||
console.log('Docker vhost statistics, shared classes, all/individual toggle and live counters: PASS');
|
||||
+4
-3
@@ -64,7 +64,8 @@ for (const locale of ["en_US", "pl_pl"]) {
|
||||
assert.ok(pack[key]?.trim(), `${locale} missing ${key}`);
|
||||
}
|
||||
}
|
||||
assert.match(source("Cargo.toml"), /version = "0\.34\.5"/);
|
||||
assert.equal(JSON.parse(source("src/admin/openapi.json")).info.version, "0.34.5");
|
||||
const currentVersion = source("Cargo.toml").match(/^version = "([^"]+)"/m)?.[1];
|
||||
assert.ok(currentVersion, "Cargo.toml must declare a release version");
|
||||
assert.equal(JSON.parse(source("src/admin/openapi.json")).info.version, currentVersion);
|
||||
assert.match(source("CHANGELOG.md"), /## 0\.34\.5 — Administration UX improvements/);
|
||||
console.log("0.34.5 UI layout, inactive probe controls, localization and version: PASS");
|
||||
console.log("0.34.5 UI layout, inactive probe controls, localization and synchronized API version: PASS");
|
||||
@@ -54,7 +54,7 @@ assert.match(refresh, /class="appearance-settings-grid refresh-settings-grid"/);
|
||||
for (const id of ['refresh-live-enabled', 'refresh-overview-seconds', 'refresh-traffic-seconds', 'refresh-cache-seconds', 'refresh-system-info-seconds']) {
|
||||
assert.equal(refresh.split(`id="${id}"`).length - 1, 1);
|
||||
}
|
||||
assert.match(css, /\.refresh-settings-grid > \.check\s*\{[^}]*grid-column:\s*1 \/ -1/);
|
||||
assert.match(css, /\.refresh-settings-grid\s*>\s*\.check\s*\{[^}]*grid-column:\s*1\s*\/\s*-1/);
|
||||
|
||||
// Remove only presentation of the process group, not the stat source or API.
|
||||
const system = html.slice(html.indexOf('id="settings-page-system-info"'), html.indexOf('id="runtime-acme-mode"'));
|
||||
@@ -74,7 +74,7 @@ for (const style of ['square', 'subtle', 'rounded', 'extra']) {
|
||||
}
|
||||
assert.match(docsCss, /\.reader-sidebar,\s*\.markdown-reader,/);
|
||||
assert.match(docsCss, /\.swagger-ui select,[\s\S]*?border-radius: var\(--rp-docs-radius-control/);
|
||||
assert.match(css, /\.metric-info > summary\s*\{[^}]*var\(--rp-radius-control/);
|
||||
assert.match(css, /\.metric-info\s*>\s*summary\s*\{[^}]*var\(--rp-radius-control/);
|
||||
assert.match(css, /#panel-overview \.cache-mix-legend i\s*\{[^}]*var\(--rp-radius-control/);
|
||||
|
||||
for (const locale of ['en_US', 'pl_pl']) {
|
||||
@@ -83,5 +83,7 @@ for (const locale of ['en_US', 'pl_pl']) {
|
||||
assert.ok(translation.vhTinyauth && !translation.vhTinyauth.includes('IP'));
|
||||
}
|
||||
assert.match(source('CHANGELOG.md'), /## 0\.34\.5 — Administration UX improvements/);
|
||||
assert.match(source('Cargo.toml'), /version = "0\.34\.5"/);
|
||||
const currentVersion = source('Cargo.toml').match(/^version = "([^"]+)"/m)?.[1];
|
||||
assert.ok(currentVersion);
|
||||
assert.equal(JSON.parse(source('src/admin/openapi.json')).info.version, currentVersion);
|
||||
console.log('0.34.5 UX follow-up layout, source IDs, access scopes and corner preferences: PASS');
|
||||
@@ -0,0 +1,29 @@
|
||||
// UI contract for ordinary Vhost MIME request filtering + neutral Docker publish toggle.
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const editor = read('src/admin/web/vhosts/editor.js');
|
||||
const core = read('src/admin/web/vhosts/core.js');
|
||||
const docker = read('src/admin/web/docker.js');
|
||||
const css = read('src/admin/web/style.css');
|
||||
const openapi = JSON.parse(read('src/admin/openapi.json'));
|
||||
for (const language of ['en_US', 'pl_pl']) {
|
||||
const keys = JSON.parse(read(`langs/${language}.json`)).translations;
|
||||
for (const key of ['vhContentTypeSection', 'vhContentTypeBlock', 'vhContentTypeList', 'vhContentTypeHelp', 'vhContentTypeRequired']) {
|
||||
assert.ok(keys[key], `${language}: ${key}`);
|
||||
}
|
||||
}
|
||||
assert.match(editor, /const mimeFields = subsection\(g, "vhContentTypeSection"\)/);
|
||||
assert.match(editor, /mimeList\.disabled = !mimeEnabled\.checked/);
|
||||
assert.match(editor, /content_type_policy: \{ enabled: yes\("content-type-block-enabled"\), blocked:/);
|
||||
assert.match(core, /content_type_policy: \{ enabled: false, blocked: \[\] \}/);
|
||||
assert.doesNotMatch(docker, /content-type-block|vhContentType/);
|
||||
assert.match(css, /\.docker-publish-field\s*\{[^}]*padding: 0;/);
|
||||
assert.match(css, /\.docker-publish:checked::before\s*\{[^}]*var\(--accent\)/);
|
||||
assert.doesNotMatch(css.slice(css.indexOf('/* Compact neutral publish switch;'), css.indexOf('.docker-service-quick-actions {')), /var\(--green\)|var\(--pill-live-line\)|999px/);
|
||||
assert.equal(openapi.info.version, '0.34.6');
|
||||
assert.equal(openapi.components.schemas.Vhost.properties.content_type_policy.properties.blocked.maxItems, 64);
|
||||
console.log('Ordinary Vhost Content-Type editor and neutral Docker switch: PASS');
|
||||
Reference in new issue
Block a user