mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
BrightSign: report the LAN address, real disk, memory and load
Every one of these fields existed in the schema, the API and the dashboard,
and every one was NULL or misleading on a BrightSign. The XT245 had 6000
consecutive telemetry rows with local_ip NULL while sitting at a perfectly
reachable 192.168.1.46, and reported "1026 MB" of storage for a 119 GB NVMe.
The host half (autorun.brs) does collect an address, but nothing the host
sends was arriving at all — proven by the storage figure, which was the
browser's cache quota rather than any disk. So the page has to read this
itself, which is also the half that can be delivered: st-bridge.js is served
per page load, while autorun.brs needs a release bump to reach a player.
It is Node's standard library, not a @brightsign module. The widget is created
with nodejs_enabled, so os and fs are simply there — this is what BrightSign's
own dev-cookbook does in html5-app-template (both the .ts and .js variants).
Looking for a platform module is the trap, and it cost most of a day:
@brightsign/networkconfiguration EXISTS but exposes only callback,
getNeighborInformation and enableLeds — no config reader. hostconfiguration
has getConfig()/applyConfig() but returns host settings (forwardingEnabled,
hostName, loginPassword, nameServers) with no address in them. Both enumerated
on the live player, because the JavaScript API doc pages 404 and BrightSign's
own roNetworkConfiguration page links to one of the dead URLs.
getCurrentConfig() is BrightScript-only.
local_ip os.networkInterfaces(), skipping internal and 169.254
ram_total/free os.totalmem() / os.freemem()
cpu_usage 1-min load average / core count, as a clamped percentage
uptime_seconds os.uptime() — the MACHINE, overriding the page's own
performance.now(), so a widget rebuilt by the watchdog no
longer hides weeks of real uptime
storage_* fs.statfsSync over the mounts under /storage, largest wins
(ours boots from NVMe with a dead card slot; others from SD)
Dashboard: the RAM and CPU cards were gated on "is this Android?", which was
right when Android was the only family that could measure them. They now
render for any player that reports the value, so a BrightSign gets them and
Android is untouched — including keeping its "--" cards when no reading has
arrived, since an empty card is a known state and a missing one reads as
"cannot". The BrightSign storage card loses its "player storage" caveat,
because the number is now the disk it always claimed to be.
Verified on the real XT245 (FW 9.1.93.2): 116.8 GB free of 116.8 GB, 2.68 GB
of 3.57 GB RAM, 3% CPU, uptime tracking the machine, local_ip 192.168.1.46 —
matching the address found independently by MAC-vendor scan, and a disk figure
matching the kernel's own block count.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
This commit is contained in:
parent
f58c537d15
commit
def30e6d39
|
|
@ -47,6 +47,14 @@
|
|||
*/
|
||||
var VideoModeConfigClass = tryRequire('@brightsign/videomodeconfiguration');
|
||||
var CecClass = tryRequire('@brightsign/cec');
|
||||
/*
|
||||
* Node's standard library, present because the widget is created with nodejs_enabled. Used for
|
||||
* the LAN address (see refreshTelemetry) exactly as BrightSign's own dev-cookbook templates do.
|
||||
* tryRequire, not a bare require: in a plain browser there is no require at all, and this file
|
||||
* must load there too.
|
||||
*/
|
||||
var osModule = tryRequire('os');
|
||||
var fsModule = tryRequire('fs');
|
||||
|
||||
var port = null;
|
||||
if (MessagePortClass) {
|
||||
|
|
@ -881,6 +889,125 @@
|
|||
} catch (e) { /* older OS without the call */ }
|
||||
}
|
||||
|
||||
/*
|
||||
* The address this player holds on the LAN — the one an integrator needs to reach its DWS on
|
||||
* site, and the field the dashboard has always had a slot for and never been able to fill.
|
||||
*
|
||||
* This is Node's own `os.networkInterfaces()`, which is what BrightSign's dev-cookbook does in
|
||||
* both html5-app-template/src/info.ts and src-js/info.js. The widget is created with
|
||||
* nodejs_enabled, so the standard library is simply there — there is no @brightsign module for
|
||||
* this, and looking for one is a dead end that cost a whole afternoon:
|
||||
*
|
||||
* @brightsign/networkconfiguration EXISTS but exposes only callback,
|
||||
* getNeighborInformation and enableLeds — no config reader at all.
|
||||
* @brightsign/hostconfiguration has getConfig()/applyConfig(), but it returns HOST settings
|
||||
* (forwardingEnabled, hostName, loginPassword, nameServers…) with no address in them.
|
||||
*
|
||||
* Both verified by enumerating the live objects on our XT245 (FW 9.1.93.2), not from docs —
|
||||
* the docs pages for the JavaScript API 404, and their own roNetworkConfiguration page links
|
||||
* to one of the dead URLs. getCurrentConfig() is BrightScript-only.
|
||||
*
|
||||
* `internal` is Node's own loopback flag, which beats string-matching 127.*; the 169.254
|
||||
* link-local a player assigns itself when DHCP never answered is still filtered by hand,
|
||||
* because sending an operator to an unreachable address is worse than showing nothing.
|
||||
*
|
||||
* family is compared loosely: it is the string "IPv4" on the Node in this firmware (and in
|
||||
* the cookbook), but became the number 4 in Node 18, and this file outlives firmwares.
|
||||
*/
|
||||
if (osModule && typeof osModule.networkInterfaces === 'function') {
|
||||
try {
|
||||
var ifaces = osModule.networkInterfaces() || {};
|
||||
var names = Object.keys(ifaces);
|
||||
for (var ni = 0; ni < names.length && !telemetry.local_ip; ni++) {
|
||||
var addrs = ifaces[names[ni]] || [];
|
||||
for (var ai = 0; ai < addrs.length; ai++) {
|
||||
var a = addrs[ai];
|
||||
if (!a || a.internal) continue;
|
||||
if (a.family !== 'IPv4' && a.family !== 4) continue;
|
||||
var ip = String(a.address || '');
|
||||
if (!ip || ip.indexOf('169.254.') === 0) continue;
|
||||
telemetry.local_ip = ip;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) { /* no networking yet, or a firmware without it — stay silent */ }
|
||||
}
|
||||
|
||||
/*
|
||||
* Memory, load and REAL uptime — all from the same Node standard library the address above
|
||||
* came from, and all previously NULL on every BrightSign in the fleet.
|
||||
*
|
||||
* uptime deliberately OVERRIDES the page's own figure. index.html sends
|
||||
* performance.now()/1000, which is how long this PAGE has been up; a widget rebuilt by the
|
||||
* watchdog resets it while the player has been running for weeks. os.uptime() is the machine,
|
||||
* which is what an operator reading "uptime" means and what makes a reboot loop visible.
|
||||
*
|
||||
* cpu_usage is the 1-minute load average normalised by core count and expressed as a
|
||||
* percentage, so it is comparable with what the other players report rather than being a raw
|
||||
* load figure that means nothing next to them. Clamped, because load can exceed core count.
|
||||
*/
|
||||
if (osModule) {
|
||||
try {
|
||||
if (typeof osModule.totalmem === 'function' && typeof osModule.freemem === 'function') {
|
||||
var totalB = osModule.totalmem();
|
||||
var freeB = osModule.freemem();
|
||||
if (isFinite(totalB) && totalB > 0) telemetry.ram_total_mb = Math.round(totalB / 1048576);
|
||||
if (isFinite(freeB) && freeB >= 0) telemetry.ram_free_mb = Math.round(freeB / 1048576);
|
||||
}
|
||||
if (typeof osModule.uptime === 'function') {
|
||||
var up = osModule.uptime();
|
||||
if (isFinite(up) && up > 0) telemetry.uptime_seconds = Math.round(up);
|
||||
}
|
||||
if (typeof osModule.loadavg === 'function' && typeof osModule.cpus === 'function') {
|
||||
var la = osModule.loadavg();
|
||||
var cores = (osModule.cpus() || []).length || 1;
|
||||
if (la && isFinite(la[0])) {
|
||||
var pct = Math.round((la[0] / cores) * 100);
|
||||
telemetry.cpu_usage = pct < 0 ? 0 : (pct > 100 ? 100 : pct);
|
||||
}
|
||||
}
|
||||
} catch (e) { /* a firmware without part of the stdlib — report what did work */ }
|
||||
}
|
||||
|
||||
/*
|
||||
* REAL disk, from statfs rather than the browser's storage quota.
|
||||
*
|
||||
* The quota is what this file used to report and it is not the disk: our XT245 answered
|
||||
* "1026 MB total" for a 119 GB NVMe, because navigator.storage.estimate() describes the
|
||||
* widget's cache budget. An operator reading that has been told something false about the
|
||||
* machine, which is worse than an empty field.
|
||||
*
|
||||
* The volume is DISCOVERED, not assumed. BrightSign mounts storage under /storage (SD, SSD,
|
||||
* USB), and which one a given player boots from varies — ours runs from an NVMe while the
|
||||
* card slot is dead. So statfs every mount and keep the largest, which is the content volume
|
||||
* on every shape of player. Falls back to the widget's own working directory.
|
||||
*/
|
||||
if (fsModule && typeof fsModule.statfsSync === 'function') {
|
||||
try {
|
||||
var candidates = [];
|
||||
try {
|
||||
var mounts = fsModule.readdirSync('/storage') || [];
|
||||
for (var mi = 0; mi < mounts.length; mi++) candidates.push('/storage/' + mounts[mi]);
|
||||
} catch (e) { /* no /storage on this firmware */ }
|
||||
candidates.push('/');
|
||||
var bestTotal = 0, bestFree = 0;
|
||||
for (var ci = 0; ci < candidates.length; ci++) {
|
||||
try {
|
||||
var st = fsModule.statfsSync(candidates[ci]);
|
||||
if (!st || !isFinite(st.blocks) || !isFinite(st.bsize)) continue;
|
||||
var tot = st.blocks * st.bsize;
|
||||
// bavail is space usable by an unprivileged writer; bfree includes the reserve.
|
||||
var fre = (isFinite(st.bavail) ? st.bavail : st.bfree) * st.bsize;
|
||||
if (tot > bestTotal) { bestTotal = tot; bestFree = fre; }
|
||||
} catch (e) { /* not a mount point */ }
|
||||
}
|
||||
if (bestTotal > 0) {
|
||||
telemetry.storage_total_mb = Math.round(bestTotal / 1048576);
|
||||
telemetry.storage_free_mb = Math.round(bestFree / 1048576);
|
||||
}
|
||||
} catch (e) { /* leave the quota estimate below to fill in */ }
|
||||
}
|
||||
|
||||
/*
|
||||
* REAL device storage, when the host could see a volume.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -544,10 +544,15 @@ async function loadDevice(deviceId, activeTab = null) {
|
|||
</div>` : ''}
|
||||
${latestTelemetry.storage_total_mb ? `
|
||||
<div class="info-card">
|
||||
<!-- Labelled "player storage", not "storage": on this family the number is the
|
||||
widget's cache quota, not the device filesystem. Same column as Android's real
|
||||
disk figures, so the label is what stops it being read as "the disk is 1 GB". -->
|
||||
<div class="info-card-label">${t('device.info.player_storage')}</div>
|
||||
<!-- This used to be labelled "player storage" because the number WAS the widget's
|
||||
cache quota rather than the disk — a real XT245 with a 119 GB NVMe reported
|
||||
"1026 MB", and the label was the only thing stopping that being read as the disk
|
||||
size. The bridge now reads the actual filesystem (statfs over the mounts under
|
||||
/storage, largest wins), so it means the same thing as Android's figure and is
|
||||
labelled the same. ⚠️ The bridge is served per page load, so a player that has not
|
||||
re-fetched it yet still reports the quota — see the CDN caching note in
|
||||
docs/player-parity.md before trusting a suspiciously round ~1 GB here. -->
|
||||
<div class="info-card-label">${t('device.info.storage')}</div>
|
||||
<div class="info-card-value small" id="telStorage">${latestTelemetry.storage_free_mb != null ? t('device.info.size_free', { size: formatBytes(latestTelemetry.storage_free_mb) }) : '--'}</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-fill ${((latestTelemetry.storage_total_mb - latestTelemetry.storage_free_mb) / latestTelemetry.storage_total_mb) < 0.8 ? 'success' : 'warning'}"
|
||||
|
|
@ -605,11 +610,18 @@ async function loadDevice(deviceId, activeTab = null) {
|
|||
<div class="info-card-label">${t('device.clock.label')}</div>
|
||||
<div class="info-card-value small">${renderDeviceClock(device)}</div>
|
||||
</div>
|
||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||
<!-- Shown for Android as before, and now for ANY player that actually reports the value.
|
||||
These were platform-gated when Android was the only family that could measure them;
|
||||
a BrightSign widget runs with nodejs_enabled and the bridge reads os.totalmem/freemem
|
||||
and the load average, so the numbers exist and were being thrown away by a gate that
|
||||
asked what the device IS instead of what it SENT. Keeping the Android arm means a
|
||||
panel that reports nothing still shows "--" there rather than losing its cards. -->
|
||||
${(device.android_version && !device.android_version.startsWith('Web/')) || latestTelemetry.ram_free_mb != null ? `
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">${t('device.info.ram')}</div>
|
||||
<div class="info-card-value small" id="telRam">${latestTelemetry.ram_free_mb ? t('device.info.size_free', { size: formatBytes(latestTelemetry.ram_free_mb) }) : '--'}</div>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
${(device.android_version && !device.android_version.startsWith('Web/')) || latestTelemetry.cpu_usage != null ? `
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">${t('device.info.cpu_usage')}</div>
|
||||
<div class="info-card-value small" id="telCpu">${latestTelemetry.cpu_usage != null ? latestTelemetry.cpu_usage.toFixed(1) + '%' : '--'}</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const path = require('node:path');
|
|||
const SRC = fs.readFileSync(path.join(__dirname, '..', '..', 'brightsign', 'st-bridge.js'), 'utf8');
|
||||
|
||||
/** Load the bridge into a fake window. `mods` present => pretend we are on a BrightSign. */
|
||||
function load({ search = '', mods = null, ua = 'Mozilla/5.0 Chrome/150', seed = {}, storageEstimate = null, temperature = null } = {}) {
|
||||
function load({ search = '', mods = null, ua = 'Mozilla/5.0 Chrome/150', seed = {}, storageEstimate = null, temperature = null, os = null, fs = null } = {}) {
|
||||
const posted = [];
|
||||
const registryStore = new Map(Object.entries(seed));
|
||||
const cec = { sent: [] };
|
||||
|
|
@ -66,6 +66,10 @@ function load({ search = '', mods = null, ua = 'Mozilla/5.0 Chrome/150', seed =
|
|||
|
||||
if (mods) {
|
||||
sandbox.require = (name) => {
|
||||
// Node's standard library, present because the widget runs with nodejs_enabled.
|
||||
// Not an @brightsign module, so it is answered before the platform ones.
|
||||
if (name === 'os') { if (!os) throw new Error("Cannot find module 'os'"); return os; }
|
||||
if (name === 'fs') { if (!fs) throw new Error("Cannot find module 'fs'"); return fs; }
|
||||
if (name === '@brightsign/messageport') {
|
||||
return function () {
|
||||
return {
|
||||
|
|
@ -424,3 +428,132 @@ test('off-platform it resolves false immediately rather than hanging the render'
|
|||
await ready;
|
||||
assert.equal(await api.setOrientation('portrait'), false);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The LAN address.
|
||||
//
|
||||
// The dashboard has had a "Local IP" field since 1.9.29 and it was NULL for every BrightSign ever
|
||||
// paired — 6000 consecutive telemetry rows on our XT245 while it sat at a perfectly reachable
|
||||
// 192.168.1.46. The host half (autorun.brs) does collect it, but nothing the host sends was
|
||||
// arriving, so the field could only ever be filled from the page.
|
||||
//
|
||||
// There is no @brightsign module for this, and looking for one is the trap: on FW 9.1.93.2
|
||||
// @brightsign/networkconfiguration exists but exposes only callback/getNeighborInformation/
|
||||
// enableLeds, and @brightsign/hostconfiguration returns host settings with no address in them.
|
||||
// Both enumerated on the live player. BrightSign's own dev-cookbook (html5-app-template, both the
|
||||
// .ts and .js variants) uses Node's os.networkInterfaces(), which is available because the widget
|
||||
// is created with nodejs_enabled.
|
||||
|
||||
test('the LAN address comes from os.networkInterfaces(), the way the vendor does it', () => {
|
||||
const { api } = load({
|
||||
mods: [],
|
||||
os: {
|
||||
networkInterfaces: () => ({
|
||||
lo: [{ address: '127.0.0.1', family: 'IPv4', internal: true }],
|
||||
eth0: [{ address: '192.168.1.46', family: 'IPv4', internal: false }],
|
||||
}),
|
||||
},
|
||||
});
|
||||
api.refreshTelemetry();
|
||||
assert.equal(api.telemetrySnapshot().local_ip, '192.168.1.46');
|
||||
});
|
||||
|
||||
test('loopback and a DHCP-less link-local are never reported', () => {
|
||||
// 169.254.x is what a player assigns itself when DHCP never answered. Sending an operator to an
|
||||
// address that cannot be reached is worse than showing nothing.
|
||||
for (const bad of ['127.0.0.1', '169.254.10.4']) {
|
||||
const { api } = load({
|
||||
mods: [],
|
||||
os: { networkInterfaces: () => ({ eth0: [{ address: bad, family: 'IPv4', internal: bad.startsWith('127.') }] }) },
|
||||
});
|
||||
api.refreshTelemetry();
|
||||
assert.equal(api.telemetrySnapshot().local_ip, undefined, `${bad} must not be reported`);
|
||||
}
|
||||
});
|
||||
|
||||
test('family is accepted as the string OR the number', () => {
|
||||
// "IPv4" on the Node in this firmware and in the cookbook; the number 4 since Node 18. This file
|
||||
// outlives firmwares, so it must not care which it is handed.
|
||||
const { api } = load({
|
||||
mods: [],
|
||||
os: { networkInterfaces: () => ({ eth0: [{ address: '10.0.0.7', family: 4, internal: false }] }) },
|
||||
});
|
||||
api.refreshTelemetry();
|
||||
assert.equal(api.telemetrySnapshot().local_ip, '10.0.0.7');
|
||||
});
|
||||
|
||||
test('a browser has no os module and simply reports no address', () => {
|
||||
const { api } = load({ mods: [] });
|
||||
assert.doesNotThrow(() => api.refreshTelemetry());
|
||||
assert.equal(api.telemetrySnapshot().local_ip, undefined);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Memory, load, uptime and REAL disk — all from Node's stdlib, all previously NULL on BrightSign.
|
||||
//
|
||||
// The storage numbers are the ones that were actively misleading rather than merely absent: the
|
||||
// page reported navigator.storage.estimate(), so our XT245 answered "1026 MB total" for a 119 GB
|
||||
// NVMe. That is the browser's cache budget, not the machine, and an operator reading it has been
|
||||
// told something false. Verified on the player: 119616 MB, which matches the kernel's block count.
|
||||
|
||||
const OS_STUB = {
|
||||
networkInterfaces: () => ({ eth0: [{ address: '192.168.1.46', family: 'IPv4', internal: false }] }),
|
||||
totalmem: () => 3656 * 1048576,
|
||||
freemem: () => 2773 * 1048576,
|
||||
uptime: () => 149,
|
||||
loadavg: () => [0.2, 0.3, 0.3],
|
||||
cpus: () => [{}, {}, {}, {}],
|
||||
};
|
||||
|
||||
test('memory and load are reported from os, not left empty', () => {
|
||||
const { api } = load({ mods: [], os: OS_STUB });
|
||||
api.refreshTelemetry();
|
||||
const t = api.telemetrySnapshot();
|
||||
assert.equal(t.ram_total_mb, 3656);
|
||||
assert.equal(t.ram_free_mb, 2773);
|
||||
assert.equal(t.cpu_usage, 5, '0.2 load over 4 cores = 5%');
|
||||
});
|
||||
|
||||
test('uptime is the MACHINE, which is what makes a reboot loop visible', () => {
|
||||
// The page sends performance.now()/1000 — how long the PAGE has been up. A widget rebuilt by the
|
||||
// watchdog resets that while the player has been running for weeks.
|
||||
const { api } = load({ mods: [], os: OS_STUB });
|
||||
api.refreshTelemetry();
|
||||
assert.equal(api.telemetrySnapshot().uptime_seconds, 149);
|
||||
});
|
||||
|
||||
test('THE MISLEADING ONE: storage is the disk, not the browser cache quota', () => {
|
||||
const fsStub = {
|
||||
readdirSync: (p) => (p === '/storage' ? ['sd', 'ssd'] : []),
|
||||
statfsSync: (p) => {
|
||||
if (p === '/storage/ssd') return { blocks: 31258710, bsize: 4096, bavail: 31245000, bfree: 31245000 };
|
||||
if (p === '/storage/sd') return { blocks: 1000, bsize: 4096, bavail: 500, bfree: 500 };
|
||||
throw new Error('not a mount');
|
||||
},
|
||||
};
|
||||
const { api } = load({ mods: [], os: OS_STUB, fs: fsStub, storageEstimate: { quota: 1026 * 1048576, usage: 2 * 1048576 } });
|
||||
api.refreshTelemetry();
|
||||
const t = api.telemetrySnapshot();
|
||||
assert.equal(t.storage_total_mb, 122104, 'the 119 GB volume, not the 1026 MB quota');
|
||||
assert.ok(t.storage_total_mb > 100000, 'a browser quota would be ~1000');
|
||||
});
|
||||
|
||||
test('the LARGEST mount wins, because which volume a player boots from varies', () => {
|
||||
// Ours runs from an NVMe with a dead card slot; others boot from SD. Picking the first mount
|
||||
// would report a 4 MB card as the content volume on exactly those players.
|
||||
const fsStub = {
|
||||
readdirSync: () => ['sd', 'ssd'],
|
||||
statfsSync: (p) => (p === '/storage/sd'
|
||||
? { blocks: 1024, bsize: 4096, bavail: 1000, bfree: 1000 }
|
||||
: { blocks: 262144, bsize: 4096, bavail: 200000, bfree: 200000 }),
|
||||
};
|
||||
const { api } = load({ mods: [], os: OS_STUB, fs: fsStub });
|
||||
api.refreshTelemetry();
|
||||
assert.equal(api.telemetrySnapshot().storage_total_mb, 1024, 'the 1 GiB ssd, not the 4 MiB sd');
|
||||
});
|
||||
|
||||
test('a firmware without statfs degrades instead of throwing', () => {
|
||||
const { api } = load({ mods: [], os: OS_STUB, fs: { readdirSync: () => [] } });
|
||||
assert.doesNotThrow(() => api.refreshTelemetry());
|
||||
assert.equal(api.telemetrySnapshot().local_ip, '192.168.1.46', 'and the rest still reports');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,13 +34,13 @@ const template = (() => {
|
|||
return SRC.slice(i + START.length, j);
|
||||
})();
|
||||
|
||||
function render(device) {
|
||||
function render(device, telemetry) {
|
||||
const caps = Array.isArray(device.capabilities) ? device.capabilities : null;
|
||||
const sandbox = {
|
||||
device,
|
||||
caps,
|
||||
can: (cap) => (caps ? caps.includes(cap) : true),
|
||||
latestTelemetry: {},
|
||||
latestTelemetry: telemetry || {},
|
||||
diagWidget: null,
|
||||
// Stubs. Each returns something recognisable so a control cannot be "found" by accident.
|
||||
t: (key) => key,
|
||||
|
|
@ -84,6 +84,13 @@ const BRIGHTSIGN = {
|
|||
|
||||
const has = (html, id) => html.includes(`id="${id}"`);
|
||||
|
||||
// Same harness, but with a telemetry payload — the cards above are driven by it.
|
||||
function renderWith(device, telemetry) {
|
||||
const saved = renderWith._tel;
|
||||
renderWith._tel = telemetry;
|
||||
try { return render(device, telemetry); } finally { renderWith._tel = saved; }
|
||||
}
|
||||
|
||||
test('a browser tab is no longer offered controls over a machine it cannot touch', () => {
|
||||
const html = render(WEB);
|
||||
assert.equal(has(html, 'rebootBtn'), false, 'a tab cannot reboot the PC it is running on');
|
||||
|
|
@ -188,3 +195,46 @@ test('every gated control still renders balanced markup', () => {
|
|||
assert.equal(bopen, bclose, 'unbalanced <button>');
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Info cards follow the DATA, not the platform.
|
||||
//
|
||||
// RAM and CPU were gated on "is this an Android panel?", which was right when Android was the only
|
||||
// family that could measure them. A BrightSign widget runs with nodejs_enabled, so the bridge now
|
||||
// reads os.totalmem/freemem and the load average — the numbers arrive and the old gate threw them
|
||||
// away. Storage on that family was worse than absent: it reported the browser's cache quota, so a
|
||||
// 119 GB player displayed "1026 MB".
|
||||
|
||||
const BS_WITH_DATA = {
|
||||
platform: 'brightsign', hardware_model: 'XT245', hardware_os_version: '9.1.93.2',
|
||||
android_version: 'Web/Safari/537.36', local_ip: '192.168.1.46',
|
||||
capabilities: ['playback.video', 'audio.volume', 'remote.input'],
|
||||
};
|
||||
const REAL_TELEMETRY = {
|
||||
storage_free_mb: 119563, storage_total_mb: 119616,
|
||||
ram_free_mb: 2773, ram_total_mb: 3656, cpu_usage: 5, uptime_seconds: 149,
|
||||
};
|
||||
|
||||
test('a BrightSign that reports memory and load gets cards for them', () => {
|
||||
const html = renderWith(BS_WITH_DATA, REAL_TELEMETRY);
|
||||
assert.ok(has(html, 'telRam'), 'RAM card missing on a player that reports RAM');
|
||||
assert.ok(has(html, 'telCpu'), 'CPU card missing on a player that reports load');
|
||||
assert.ok(has(html, 'telStorage'), 'and the disk it now measures for real');
|
||||
});
|
||||
|
||||
test('Android keeps its cards whether or not a reading has arrived yet', () => {
|
||||
// The old gate was platform-based, so an Android panel with no telemetry still showed "--".
|
||||
// Switching to data-presence must not take that away — an empty card is a known state, a missing
|
||||
// one reads as "this panel cannot do that".
|
||||
for (const tel of [REAL_TELEMETRY, {}]) {
|
||||
const html = renderWith({ client_type: 'apk', android_version: '13', capabilities: ['playback.video'] }, tel);
|
||||
assert.ok(has(html, 'telRam'), 'Android must keep its RAM card');
|
||||
assert.ok(has(html, 'telCpu'), 'Android must keep its CPU card');
|
||||
}
|
||||
});
|
||||
|
||||
test('a browser tab gains nothing — it measures none of this', () => {
|
||||
const html = renderWith({ android_version: 'Web/Chrome', capabilities: ['playback.video'] }, {});
|
||||
assert.equal(has(html, 'telRam'), false);
|
||||
assert.equal(has(html, 'telCpu'), false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue