mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-21 14:12:34 -05:00
The dashboard flip-flopped between "absent" and "present_still" after
navigating away and back. The cause was not sensing. On a dropped socket the
client fell back to a built-in simulation and rendered synthetic values that
were indistinguishable from live ones, so the UI was showing made-up readings
and saying nothing about it.
That is the most damaging failure this UI can have. A sensing system that
invents data silently is worse than one that shows nothing, because every
downstream judgement -- including which nodes to move and whether a change
helped -- is then made against fiction.
Changes:
- Client-side simulation is default-OFF and must be opted into explicitly.
- An unreachable server retries forever instead of substituting data.
- A banner states plainly whenever what is displayed is not live. The
labels say what they mean: "NO DATA - SERVER UNREACHABLE" for an
unreachable server, and "INVENTED DATA - NOT MEASURED" for simulation,
replacing "OFFLINE - CLIENT SIMULATION", which read like a status rather
than a warning.
Also fixes a reconnect race in the same path: two in-flight connects could each
install handlers, so a stale socket kept overwriting fresher state. That race is
what made the flip-flop intermittent and hard to attribute.
Co-Authored-By: claude-flow <ruv@ruv.net>
132 lines
4.1 KiB
JavaScript
132 lines
4.1 KiB
JavaScript
// Global data-source banner.
|
|
//
|
|
// The per-tab banner in SensingTab was correct and still invisible where it
|
|
// mattered: a reader on Dashboard or Live Demo never saw it, so a screen full
|
|
// of client-invented sensing frames was indistinguishable from measurement.
|
|
// This bar is fixed to the viewport and present on every tab, so the provenance
|
|
// of what is on screen is never more than a glance away.
|
|
//
|
|
// It shows nothing at all while data is live. An always-on badge becomes
|
|
// wallpaper; appearing only when the reading is not trustworthy keeps it
|
|
// meaningful.
|
|
|
|
import { sensingService } from '../services/sensing.service.js';
|
|
|
|
const STYLE_ID = 'ruview-data-source-banner-style';
|
|
const BAR_ID = 'ruview-data-source-banner';
|
|
|
|
const CONFIG = {
|
|
// 'live' is intentionally absent: no banner is shown for trustworthy data.
|
|
'server-simulated': {
|
|
text: 'SYNTHETIC DATA — the server is generating this, no hardware is being read',
|
|
bg: '#7c3a00',
|
|
fg: '#ffd9a0',
|
|
},
|
|
reconnecting: {
|
|
text: 'RECONNECTING — the display is frozen at the last live reading',
|
|
bg: '#33302a',
|
|
fg: '#e8dcc0',
|
|
},
|
|
unreachable: {
|
|
text: 'SERVER UNREACHABLE — nothing on this screen is current',
|
|
bg: '#5c1010',
|
|
fg: '#ffc9c9',
|
|
},
|
|
simulated: {
|
|
text: 'SIMULATED — every value on this screen is invented by your browser, not measured',
|
|
bg: '#6b0f6b',
|
|
fg: '#ffc9ff',
|
|
},
|
|
};
|
|
|
|
const CSS = `
|
|
#${BAR_ID} {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 100000;
|
|
display: none;
|
|
padding: 7px 14px;
|
|
font: 600 13px/1.35 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
letter-spacing: 0.04em;
|
|
text-align: center;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.45);
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
|
}
|
|
#${BAR_ID}.is-visible { display: block; }
|
|
/* Simulated data is the dangerous case — it looks real. Make the bar move so
|
|
it cannot be mistaken for a static decoration. */
|
|
#${BAR_ID}.is-simulated { animation: ruview-dsb-pulse 1.6s ease-in-out infinite; }
|
|
@keyframes ruview-dsb-pulse { 50% { filter: brightness(1.45); } }
|
|
@media (prefers-reduced-motion: reduce) {
|
|
#${BAR_ID}.is-simulated { animation: none; }
|
|
}
|
|
body.ruview-has-data-source-banner { padding-top: 30px; }
|
|
`;
|
|
|
|
export class DataSourceBanner {
|
|
constructor() {
|
|
this._bar = null;
|
|
this._unsubscribe = null;
|
|
}
|
|
|
|
init() {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
if (!document.getElementById(STYLE_ID)) {
|
|
const style = document.createElement('style');
|
|
style.id = STYLE_ID;
|
|
style.textContent = CSS;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
this._bar = document.getElementById(BAR_ID);
|
|
if (!this._bar) {
|
|
this._bar = document.createElement('div');
|
|
this._bar.id = BAR_ID;
|
|
this._bar.setAttribute('role', 'status');
|
|
// Announce changes, but don't interrupt: the reader is mid-task and the
|
|
// message describes context, not an error to act on right now.
|
|
this._bar.setAttribute('aria-live', 'polite');
|
|
document.body.appendChild(this._bar);
|
|
}
|
|
|
|
// dataSource changes are published on the state-listener channel.
|
|
this._unsubscribe = sensingService.onStateChange(() => this.render());
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
if (!this._bar) return;
|
|
const cfg = CONFIG[sensingService.dataSource];
|
|
|
|
if (!cfg) {
|
|
this._bar.classList.remove('is-visible', 'is-simulated');
|
|
document.body.classList.remove('ruview-has-data-source-banner');
|
|
return;
|
|
}
|
|
|
|
this._bar.textContent = cfg.text;
|
|
this._bar.style.background = cfg.bg;
|
|
this._bar.style.color = cfg.fg;
|
|
this._bar.classList.add('is-visible');
|
|
this._bar.classList.toggle('is-simulated', sensingService.dataSource === 'simulated');
|
|
document.body.classList.add('ruview-has-data-source-banner');
|
|
}
|
|
|
|
dispose() {
|
|
if (this._unsubscribe) {
|
|
this._unsubscribe();
|
|
this._unsubscribe = null;
|
|
}
|
|
if (this._bar && this._bar.parentNode) {
|
|
this._bar.parentNode.removeChild(this._bar);
|
|
}
|
|
this._bar = null;
|
|
document.body.classList.remove('ruview-has-data-source-banner');
|
|
}
|
|
}
|
|
|
|
export const dataSourceBanner = new DataSourceBanner();
|