Operator screen
Mobile Supervisor
The plant at a phone's width: what needs someone, the alarms, the sites worst first, the line's order and the last handover.
index.htm as it is written with the smart-industrial package; industrial-demos.css is the screens' shared layout
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Mobile Supervisor - Smart.Industrial</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="industrial-demos.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial-dark">
<!-- A phone's worth of screen: on a phone it is the whole screen; on a desktop it is shown
at a phone's width, so what it leaves out is visible. -->
<div class="ms-phone">
<header class="ms-header">
<div>
<div class="ms-plant">Millbrook plant</div>
<div class="ms-sub"><span id="who">A. Brandt</span> · supervisor · <span id="clock"></span></div>
</div>
<button class="ms-theme" id="themeToggle" type="button">Light</button>
</header>
<main class="ms-body">
<section class="ms-section" aria-labelledby="nowHeading">
<h2 class="ms-heading" id="nowHeading">Now</h2>
<div class="ms-tiles">
<smart-status-tile id="tileAlarms" label="Unacknowledged" precision-digits="0"></smart-status-tile>
<smart-status-tile id="tileOee" label="Line 3 OEE" unit="%" precision-digits="1"></smart-status-tile>
<smart-status-tile id="tileSites" label="Sites to look at" precision-digits="0"></smart-status-tile>
<smart-status-tile id="tileStops" label="Stops, no reason" precision-digits="0"></smart-status-tile>
</div>
</section>
<section class="ms-section" aria-labelledby="alarmsHeading">
<h2 class="ms-heading" id="alarmsHeading">Alarms</h2>
<smart-alarm-banner id="banner" max-visible="3" empty-message="No active alarms"></smart-alarm-banner>
</section>
<section class="ms-section" aria-labelledby="sitesHeading">
<h2 class="ms-heading" id="sitesHeading">Sites</h2>
<smart-site-map id="sites" label="Network" view="list" aspect-ratio="4 / 3"></smart-site-map>
</section>
<section class="ms-section" aria-labelledby="lineHeading">
<h2 class="ms-heading" id="lineHeading">Line 3</h2>
<smart-order-queue id="orders" label="Line 3 orders" read-only></smart-order-queue>
</section>
<section class="ms-section" aria-labelledby="handoverHeading">
<h2 class="ms-heading" id="handoverHeading">Last handover</h2>
<smart-shift-log id="handover" label="Handover" read-only filter="handover"></smart-shift-log>
</section>
</main>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.statustile.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.alarmbanner.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.sitemap.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.orderqueue.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.shiftlog.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
<script>
/* The site's light/dark switch is the reader's choice: a screen opened from the light site
in a new tab opens light too. Framed, the site's page sets the theme itself. */
addEventListener('load', function () {
var light = false;
try { light = window.top === window && localStorage.getItem('smart-industrial-dark') === 'false'; } catch (e) { }
/* after the screen's own onload, which is where its switch starts listening */
if (light) {
setTimeout(function () {
var shell = document.querySelector('smart-hmi-shell'),
toggle = document.querySelector('#themeToggle');
if (shell && typeof shell.setTheme === 'function') { shell.setTheme('industrial'); }
else if (toggle && document.body.getAttribute('theme') === 'industrial-dark') { toggle.click(); }
}, 0);
}
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* Mobile supervisor.
*
* The plant in a supervisor's pocket, read on the walk between buildings: what needs someone
* now, the alarms, the sites worst first, the line's order, and the last handover. The same
* components as the control-room screens, at a phone's width - the site map as its list, the
* order queue and the shift log read-only. Nothing here commands the plant: a phone on a
* mobile network is a place to look, and acting stays with the screens next to the machines.
* Acknowledging an alarm is the one thing it does, because a supervisor who has seen an alarm
* should be able to say so. */
window.onload = function () {
const banner = document.getElementById('banner'),
sites = document.getElementById('sites'),
orders = document.getElementById('orders'),
handover = document.getElementById('handover'),
clock = document.getElementById('clock'),
themeToggle = document.getElementById('themeToggle'),
tiles = {
alarms: document.getElementById('tileAlarms'),
oee: document.getElementById('tileOee'),
sites: document.getElementById('tileSites'),
stops: document.getElementById('tileStops')
},
MIN = 60000,
H = 3600000,
NOW = Date.now();
const ALARMS = [
{ id: 'a1', tag: 'PS North P2', message: 'Overcurrent trip', priority: 1, acknowledged: false, timestamp: NOW - 18 * MIN },
{ id: 'a2', tag: 'PS Harbour', message: 'Outstation not answering', priority: 2, acknowledged: false, timestamp: NOW - 14 * MIN },
{ id: 'a3', tag: 'LS-3021', message: 'Cap hopper level low', priority: 2, acknowledged: true, timestamp: NOW - 12 * MIN },
{ id: 'a4', tag: 'Hill reservoir LT-01', message: 'Level below the refill band', priority: 3, acknowledged: true, timestamp: NOW - 50 * MIN }
];
const SITES = [
{ id: 'ps-north', name: 'PS North', x: 34, y: 17, state: 'alarm', alarms: 2, reading: '0.8 bar' },
{ id: 'ps-harbour', name: 'PS Harbour', x: 86, y: 76, state: 'offline', reading: 'no data' },
{ id: 'res-hill', name: 'Hill reservoir', x: 63, y: 12, state: 'warning', alarms: 1, reading: 'level 38 %' },
{ id: 'ps-south', name: 'PS South', x: 36, y: 84, state: 'maintenance', reading: 'valve work' },
{ id: 'wtp', name: 'Riverside treatment works', x: 18, y: 62, state: 'normal', reading: '412 m³/h' },
{ id: 'ps-mill', name: 'PS Mill Lane', x: 51, y: 42, state: 'normal', reading: '3.9 bar' },
{ id: 'ps-east', name: 'PS East', x: 82, y: 33, state: 'normal', reading: '4.1 bar' },
{ id: 'line-3', name: 'Line 3 (bottling)', x: 55, y: 60, state: 'normal', reading: 'running, OEE 76 %' }
];
function show() {
const unacknowledged = ALARMS.filter((a) => !a.acknowledged).length;
banner.alarms = ALARMS.map((a) => Object.assign({}, a));
tiles.alarms.value = unacknowledged;
tiles.alarms.state = unacknowledged ? 'critical' : 'ok';
tiles.oee.value = 76.4;
tiles.sites.value = SITES.filter((s) => s.state !== 'normal').length;
tiles.sites.state = 'warning';
tiles.stops.value = 2;
tiles.stops.state = 'warning';
}
banner.addEventListener('acknowledge', function (event) {
const alarm = ALARMS.find((a) => a.id === event.detail.alarm.id);
if (alarm) {
alarm.acknowledged = true;
show();
}
});
sites.sites = SITES;
sites.addEventListener('siteOpen', function (event) {
const site = SITES.find((s) => s.id === event.detail.id);
/* on a phone, a site opens as its own page; here it says so */
sites.querySelector('.smart-site-map-detail').insertAdjacentHTML('beforeend', '<p class="ms-note">Opens the ' + site.name + ' page.</p>');
});
orders.orders = [
{ id: 'ORD-24117', product: 'Still water 0.5 l, 24-pack', quantity: 60000, unit: 'bottles', status: 'running', produced: 43142, started: new Date(NOW - 4.8 * H).toISOString(), due: new Date(NOW + 2.8 * H).toISOString(), recipe: 'still-05' },
{ id: 'ORD-24118', product: 'Sparkling water 0.5 l, 24-pack', quantity: 24000, unit: 'bottles', status: 'queued', produced: 0, due: new Date(NOW + 6 * H).toISOString() }
];
handover.shift = { name: 'Shift A', from: new Date(NOW - 5 * H).toISOString(), to: new Date(NOW + 3 * H).toISOString() };
handover.entries = [
{ id: 'h1', at: new Date(NOW - 5.6 * H).toISOString(), author: 'M. Keller', category: 'handover', text: 'Running still 0.5 l. Filler 2 lost a bottle guide screw - temporary fix, part ordered (WO-1182). PS North P2 tripped twice overnight; P1 is carrying the zone.' },
{ id: 'h2', at: new Date(NOW - 2 * H).toISOString(), author: 'A. Brandt', category: 'note', text: 'Guide holding.' }
];
themeToggle.addEventListener('click', function () {
const theme = document.body.getAttribute('theme') === 'industrial-dark' ? 'industrial' : 'industrial-dark';
document.body.setAttribute('theme', theme);
themeToggle.textContent = theme === 'industrial-dark' ? 'Light' : 'Dark';
});
const time = () => { clock.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit' }); };
/* a boolean attribute is true whenever it is present, so these are switched off here */
banner.showTimestamps = false;
orders.showFinished = false;
time();
setInterval(time, 15000);
show();
};
styles.css
/* Mobile supervisor. One column at a phone's width; on a wider screen the same column, framed,
so what it leaves out is part of what it shows. */
html,
body {
margin: 0;
}
body.viewport {
background: var(--smart-industrial-canvas, var(--smart-body-background-color));
color: var(--smart-text-color);
font-family: var(--smart-font-family);
font-size: 14px;
}
.ms-phone {
box-sizing: border-box;
width: 100%;
max-width: 420px;
margin: 0 auto;
background: var(--smart-industrial-canvas, var(--smart-body-background-color));
}
@media (min-width: 600px) {
.ms-phone {
margin: 16px auto;
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 18px;
overflow: hidden;
}
}
.ms-header {
position: sticky;
top: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
background: var(--smart-industrial-panel-header-background, var(--smart-surface));
}
.ms-plant {
font-size: 16px;
font-weight: 700;
letter-spacing: 0.02em;
}
.ms-sub {
font-size: 12px;
color: var(--smart-text-secondary-color);
}
/* a thumb's worth of target */
.ms-theme {
min-height: 40px;
padding: 0 14px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
cursor: pointer;
}
.ms-body {
display: flex;
flex-direction: column;
gap: 18px;
padding: 14px 16px 28px;
}
.ms-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.ms-heading {
margin: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--smart-text-secondary-color);
}
.ms-tiles {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.ms-tiles smart-status-tile {
min-width: 0;
}
smart-alarm-banner,
smart-site-map,
smart-order-queue {
width: 100%;
}
/* as tall as what it holds: a phone scrolls the page, not a box in it */
smart-shift-log {
height: auto;
}
/* the log is read here: the filters, the search and the export are for the desk */
smart-shift-log .smart-shift-log-bar {
display: none;
}
.ms-note {
margin: 6px 0 0;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
Components on this screen
Alarm BannerHMI ShellOrder QueueShift LogSite MapStatus Tile