Add deprioritized device group
Build and Deploy Gateway / build-and-push (push) Successful in 19s
Build and Deploy Gateway / deploy (push) Successful in 14s

This commit is contained in:
2026-08-10 09:25:29 +03:00
parent ec68ba6a7b
commit f233660dc3
13 changed files with 262 additions and 60 deletions
+47 -3
View File
@@ -79,6 +79,7 @@ test('malformed persisted devices and remote observations cannot enter canonical
id: deviceId(mac),
alias: '',
pinned: false,
deprioritized: false,
hostname: null,
manufacturer: null,
source: 'neighbor',
@@ -183,6 +184,7 @@ test('device inventory discovers, merges, persists metadata and expires anonymou
assert.equal(snapshot.devices[0].status, 'recent');
snapshot = service.update(snapshot.devices[0].id, { alias: 'Телевизор', pinned: true }, snapshot.revision);
const namedId = snapshot.devices[0].id;
const restarted = createDeviceInventoryService({ store, observe: async () => observation, vendor, now: () => current });
assert.deepEqual(restarted.snapshot().devices[0], snapshot.devices[0]);
assert.throws(
@@ -190,15 +192,57 @@ test('device inventory discovers, merges, persists metadata and expires anonymou
(error) => error.code === 'STATE_CONFLICT',
);
observation = { observedAt: current.toISOString(), observations: [], error: 'source unavailable' };
current = new Date('2026-07-01T10:06:00.000Z');
observation = {
observedAt: current.toISOString(),
observations: parseNeighborSnapshot([
{ dst: '192.168.50.18', dev: 'br0', lladdr: '00:11:22:33:44:55', state: ['REACHABLE'] },
], current.toISOString()),
error: null,
};
snapshot = await restarted.refresh();
assert.equal(snapshot.devices.length, 1);
assert.equal(snapshot.devices[0].id, namedId);
assert.equal(snapshot.devices[0].ip, '192.168.50.18');
assert.equal(snapshot.devices[0].alias, 'Телевизор');
assert.equal(snapshot.devices[0].pinned, true);
observation = {
observedAt: current.toISOString(),
observations: parseNeighborSnapshot([
{ dst: '192.168.50.18', dev: 'br0', lladdr: '00:11:22:33:44:66', state: ['REACHABLE'] },
], current.toISOString()),
error: null,
};
snapshot = await restarted.refresh();
const replacement = snapshot.devices.find(({ mac: deviceMac }) => deviceMac === '00:11:22:33:44:66');
assert.notEqual(replacement.id, namedId);
assert.equal(replacement.alias, '');
assert.equal(replacement.pinned, false);
snapshot = restarted.update(namedId, { deprioritized: true }, snapshot.revision);
const deprioritized = snapshot.devices.find(({ id }) => id === namedId);
assert.equal(deprioritized.pinned, false);
assert.equal(deprioritized.deprioritized, true);
assert.throws(
() => restarted.update(namedId, { pinned: true, deprioritized: true }, snapshot.revision),
(error) => error.code === 'REQUEST_INVALID',
);
const persisted = createDeviceInventoryService({ store, observe: async () => observation, vendor, now: () => current });
assert.equal(persisted.snapshot().devices.find(({ id }) => id === namedId).deprioritized, true);
observation = { observedAt: current.toISOString(), observations: [], error: 'source unavailable' };
snapshot = await persisted.refresh();
assert.equal(snapshot.devices.length, 2);
assert.equal(snapshot.source.error, 'source unavailable');
snapshot = restarted.update(snapshot.devices[0].id, { alias: '', pinned: false }, snapshot.revision);
snapshot = persisted.update(namedId, { alias: '' }, snapshot.revision);
current = new Date('2026-08-02T10:00:00.000Z');
observation = { observedAt: current.toISOString(), observations: [], error: null };
snapshot = await restarted.refresh();
snapshot = await persisted.refresh();
assert.deepEqual(snapshot.devices.map(({ id }) => id), [namedId]);
snapshot = persisted.update(namedId, { deprioritized: false }, snapshot.revision);
snapshot = await persisted.refresh();
assert.equal(snapshot.devices.length, 0);
const failed = readNeighborSnapshot(() => ({ status: 1, stderr: 'not available' }), () => current);
+2 -2
View File
@@ -75,7 +75,7 @@ test('device route forwards list and refresh query paths as raw JSON responses',
});
test('device route forwards metadata patch and policy arguments without coercion', async () => {
const patch = { expectedRevision: 7, alias: 'Desk', pinned: false, extra: 0 };
const patch = { expectedRevision: 7, alias: 'Desk', pinned: false, deprioritized: true, extra: 0 };
const metadata = createHarness({ body: patch });
const metadataResponse = response();
assert.equal(await metadata.route.handle({
@@ -85,7 +85,7 @@ test('device route forwards metadata patch and policy arguments without coercion
assert.deepEqual(metadata.calls, [[
'update',
deviceId,
{ alias: 'Desk', pinned: false, extra: 0 },
{ alias: 'Desk', pinned: false, deprioritized: true, extra: 0 },
7,
]]);
assert.deepEqual(metadataResponse.payload, { revision: 3 });
+23 -4
View File
@@ -116,12 +116,22 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
assert.match(panel, /const \[pinCollapses, setPinCollapses\][\s\S]*animationDone[\s\S]*result: saved \? 'saved' : 'failed'/);
assert.match(panel, /const expanded = device\.pinned && !collapsing[\s\S]*const showDetails = device\.pinned \|\| collapsing[\s\S]*pinned=\{showDetails\}[\s\S]*onCollapseEnd=/);
assert.match(panel, /const compact = deprioritized && !collapsing[\s\S]*const expanded = device\.pinned && !collapsing[\s\S]*const showDetails = !compact && \(device\.pinned \|\| collapsing\)[\s\S]*pinned=\{showDetails\}[\s\S]*onCollapseEnd=/);
assert.match(panel, /Закреплённые[\s\S]*Убраны вниз[\s\S]*Остальные/);
assert.match(panel, /client-device-group-heading[\s\S]*role=\{groupStart \? 'heading' : undefined\}[\s\S]*aria-level=\{groupStart \? 3 : undefined\}/);
assert.match(panel, /toggleDeprioritized[\s\S]*deprioritized: !deprioritized[\s\S]*deprioritized: true/);
assert.match(panel, /client-device-deprioritize[\s\S]*aria-pressed=\{deprioritized\}[\s\S]*Вернуть \$\{title\} в основной список[\s\S]*Убрать \$\{title\} вниз/);
assert.match(panel, /\{!compact && <TrafficChart/);
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
assert.match(panel, /maxLength=\{64\}[\s\S]*autoFocus/);
assert.match(styles, /\.client-devices \{\s*width: min\(580px, 100vw\)/);
assert.match(styles, /\.client-device \{[\s\S]*--client-device-chart-height: 34px;[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*grid-template-rows: 34px var\(--client-device-chart-height\);[\s\S]*padding: 10px 8px/);
assert.match(styles, /\.client-device\.is-pinned \{[\s\S]*--client-device-chart-height: 72px/);
assert.match(styles, /\.client-device\.is-deprioritized \{[\s\S]*grid-template-rows: 34px;[\s\S]*padding-block: 6px/);
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-main \{[\s\S]*grid-column: 1 \/ 4/);
assert.match(styles, /\.client-device-deprioritize-wrap \{[\s\S]*grid-column: 4;[\s\S]*grid-row: 2/);
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-deprioritize-wrap \{[\s\S]*grid-row: 1/);
assert.match(styles, /\.client-device-group-heading \{[\s\S]*text-transform: uppercase/);
assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-expand 520ms/);
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-collapse 520ms/);
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-axis \{[\s\S]*top: 0;[\s\S]*height: 58px/);
@@ -156,7 +166,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(styles, /\.client-device-name-separator \{[\s\S]*color: var\(--client-muted\)/);
assert.match(styles, /\.client-device-ip\.is-copied \{[\s\S]*client-device-ip-copy 800ms/);
assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding: 11px 0 0/);
assert.match(styles, /\.client-device-main > h3\.is-address-only \{[\s\S]*flex: 0 1 auto/);
assert.match(styles, /\.client-device-main > h4\.is-address-only \{[\s\S]*flex: 0 1 auto/);
assert.match(styles, /\.client-device-last-seen \{[^}]*height: 10px[\s\S]*align-items: center/);
assert.match(styles, /\.client-device-traffic-value\.has-delta > \.is-total[\s\S]*translateY\(-0\.18em\)/);
assert.match(styles, /\.client-device-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/);
@@ -249,9 +259,10 @@ test('device traffic formatting and sorting preserve uint64 precision and canoni
{ id: 'd', uploadBytes: '15', downloadBytes: '5', proxyUploadBytes: '100' },
{ id: 'c', uploadBytes: '10', downloadBytes: '10', proxyDownloadBytes: '100' },
{ id: 'e', pinned: true, uploadBytes: '0', downloadBytes: '0' },
{ id: 'f', deprioritized: true, uploadBytes: '999999', downloadBytes: '0' },
];
assert.deepEqual(sortDevicesByTraffic(devices, 'desc').map(({ id }) => id), ['e', 'b', 'a', 'c', 'd']);
assert.deepEqual(sortDevicesByTraffic(devices, 'asc').map(({ id }) => id), ['e', 'c', 'd', 'a', 'b']);
assert.deepEqual(sortDevicesByTraffic(devices, 'desc').map(({ id }) => id), ['e', 'b', 'a', 'c', 'd', 'f']);
assert.deepEqual(sortDevicesByTraffic(devices, 'asc').map(({ id }) => id), ['e', 'c', 'd', 'a', 'b', 'f']);
assert.deepEqual(
stabilizeDevicesByTraffic([
{ id: 'a', uploadBytes: '999' },
@@ -266,4 +277,12 @@ test('device traffic formatting and sorting preserve uint64 precision and canoni
], 'desc', ['a', 'b']).ids,
['b', 'a'],
);
assert.deepEqual(
stabilizeDevicesByTraffic([
{ id: 'a', deprioritized: true, uploadBytes: '999' },
{ id: 'b', uploadBytes: '1' },
{ id: 'c', pinned: true, uploadBytes: '0' },
], 'desc', ['a', 'b', 'c']).ids,
['c', 'b', 'a'],
);
});
@@ -50,6 +50,7 @@ test('all unknown inventory results pass one identity-preserving runtime parser'
lastSeenAt: null,
status: 'online',
pinned: true,
deprioritized: false,
downloadBytes: '12',
uploadBytes: '30',
proxyDownloadBytes: '0',
@@ -99,6 +100,8 @@ test('all unknown inventory results pass one identity-preserving runtime parser'
{ ...valid, devices: [{ ...valid.devices[0], uploadBytes: '-1' }] },
{ ...valid, devices: [{ ...valid.devices[0], proxyUploadBytes: 1 }] },
{ ...valid, devices: [{ ...valid.devices[0], status: 'connected' }] },
{ ...valid, devices: [{ ...valid.devices[0], deprioritized: 'yes' }] },
{ ...valid, devices: [{ ...valid.devices[0], pinned: true, deprioritized: true }] },
{ ...valid, devices: [{ ...valid.devices[0], desiredPolicy: 'automatic' }] },
{ ...valid, devices: [{ ...valid.devices[0], policyStatus: 'queued' }] },
{ ...valid, devices: [{ ...valid.devices[0], confidence: 'unknown' }] },
+14 -14
View File
@@ -36,26 +36,26 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 804,
cascadeEdges: 821,
customProperties: 31,
declarations: 2914,
declarations: 2941,
important: 0,
keyframes: 51,
media: 10,
rules: 878,
variableReferences: 336,
rules: 888,
variableReferences: 338,
},
hashes: {
cascadeEdges: '18e5bdda435293145dbb52319f1e842cc8b7cc3e8beb60ec381c26d0410cfe44',
cascadeEdges: '1ffc2a2f986970988643129ff3e32f19c31bb8b1e018a3a474a976e23adc9776',
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
declarations: '01152af9f22aa7a7654356f81b8a6110488f9d70754b2517d1f6bafeff4b14fd',
declarations: '77314876411a21c88472dc277a59ca24192ca7d38de1ce8f367e7bd1007c5323',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
keyframes: '0bed7ec3cd3a86ee091cf9b07c081ab2364ac5a17e55e95bc9c4a6430419b019',
ruleDeclarationSequences: '7ffd551563a8d801bcd5e5f9886b9551940dd9561240f0f8a771a85d44fc19db',
selectors: '09962ebb3bc769f74ae785a44f8e032c0a83985ce14ea652135732b5af69042a',
variableReferences: '5988cb6c9b658d380c5b51c04621c0579e1cc5e8abcd764d78a9d42b802252c9',
witnesses: '68d7c48f20f7a8ae2c0a6ddabe743dfb84fd02fba303e3cc9d3b902efd0f8758',
ruleDeclarationSequences: 'ce3831db5966264c17fa3af3e10fc6a41398cc619d061fa155ac43b47f0947ea',
selectors: '828cf6e95b03c890e3a969be20eb05be1f70872d6076e8f9a6a38cf6b415d012',
variableReferences: '9b6214da6e8e02b01deb6d5a98fe4c695a1fdaff4f65c936b805d7d5841db04d',
witnesses: 'c34721ec2cb99104eae77841ae88ddf77cde278d15c21675a020f3cfdd13a28a',
},
};
@@ -110,7 +110,7 @@ test('tokens, shared primitives, and feature styles have one explicit owner', ()
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
const witnesses = readStyleWitnesses(root);
assert.equal(witnesses.length, 709);
assert.equal(witnesses.length, 716);
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
assert.deepEqual(ledger.counts, acceptedLedger.counts);
@@ -293,8 +293,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
assert.deepEqual(assets, ['index-BJnEzGgg.css']);
assert.deepEqual(assets, ['index-DQr1ElW4.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 109140);
assert.equal(sha256(built), '9e1f7811080177f366b693a77e92c615aff1a74537b34d597100c9707a8aea95');
assert.equal(built.byteLength, 110647);
assert.equal(sha256(built), '91bf07a74af3312f9b048fdebd642e811fafc3bb2ec49b55592a35f085c25f41');
});