Operator screen
Shift Handover
The shift log, the shift in numbers, the stops still without a reason, the standing alarms and the order, and whether the handover is ready.
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>Shift Handover - 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">
<div class="hmi-screen sh-screen">
<header class="hmi-header">
<div class="hmi-identity">
<span class="hmi-plant">LINE 3 · SHIFT HANDOVER</span>
<span class="hmi-area" id="shiftLine"></span>
</div>
<div class="hmi-header-status" id="ready" role="status"></div>
<div class="hmi-header-actions">
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<section class="hmi-body sh-body">
<div class="hmi-panel sh-log-panel">
<div class="hmi-panel-header">
<span>Shift log</span>
<span class="sh-hint">What the next shift needs to know that no tag says</span>
</div>
<div class="hmi-panel-content">
<smart-shift-log id="log" label="Line 3 shift log" author="A. Brandt"></smart-shift-log>
</div>
</div>
<div class="sh-side">
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>The shift in numbers</span>
</div>
<div class="hmi-panel-content sh-tiles">
<smart-status-tile id="tileOutput" label="Bottles" precision-digits="0"></smart-status-tile>
<smart-status-tile id="tileOee" label="OEE" unit="%" precision-digits="1" target="85"></smart-status-tile>
<smart-status-tile id="tileStops" label="Stops without a reason" precision-digits="0"></smart-status-tile>
<smart-status-tile id="tileAlarms" label="Standing alarms" precision-digits="0"></smart-status-tile>
</div>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Before you hand over</span>
<span class="sh-hint">Give every stop its reason</span>
</div>
<div class="hmi-panel-content">
<smart-downtime-log id="downtime" label="Stops without a reason"></smart-downtime-log>
</div>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Standing alarms</span>
<span class="sh-hint">Handed over as they are</span>
</div>
<div class="hmi-panel-content">
<smart-alarm-grid id="alarms" timestamp-format="time"></smart-alarm-grid>
</div>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Orders</span>
</div>
<div class="hmi-panel-content">
<smart-order-queue id="orders" label="Line 3 orders" read-only></smart-order-queue>
</div>
</div>
</div>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.shiftlog.js"></script>
<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.downtimelog.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.alarmgrid.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.orderqueue.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,
toggle = document.querySelector('.hmi-theme-toggle');
try { light = window.top === window && localStorage.getItem('smart-industrial-dark') === 'false'; } catch (e) { }
/* after the screen's own onload, which is where its button starts listening */
if (light && toggle) { setTimeout(function () { if (document.body.getAttribute('theme') === 'industrial-dark') { toggle.click(); } }, 0); }
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* Shift handover.
*
* The last twenty minutes of a shift, on the screen the outgoing operator hands over from. The
* log is the centre of it - what the next shift needs to know that no tag says - and around it
* the things a good handover checks: the shift's numbers, the stops that still have no reason
* (the next shift will not know why), the alarms that are standing, and where the order is.
* The header says when the handover is ready: every stop explained and a handover entry
* written. The station here is a stand-in that stores entries with its own clock and the name
* of whoever is signed in. */
window.onload = function () {
const log = document.getElementById('log'),
downtime = document.getElementById('downtime'),
alarms = document.getElementById('alarms'),
orders = document.getElementById('orders'),
shiftLine = document.getElementById('shiftLine'),
ready = document.getElementById('ready'),
themeToggle = document.getElementById('themeToggle'),
tiles = {
output: document.getElementById('tileOutput'),
oee: document.getElementById('tileOee'),
stops: document.getElementById('tileStops'),
alarms: document.getElementById('tileAlarms')
},
MIN = 60000,
H = 3600000,
NOW = Date.now(),
START = NOW - 7.7 * H,
END = START + 8 * H,
at = (minutes) => new Date(START + minutes * MIN).toISOString(),
time = (ms) => new Date(ms).toLocaleTimeString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit' });
const ENTRIES = [
{ id: 'e1', at: new Date(START - 40 * MIN).toISOString(), author: 'M. Keller', category: 'handover', text: 'Running still 0.5 l. Filler 2 lost a bottle guide screw at 05:20 - temporary fix in place, part ordered (WO-1182). Keep an eye on it.', tag: 'plc.FILLER2' },
{ id: 'e2', at: at(8), author: 'A. Brandt', category: 'note', text: 'Took over from M. Keller. Filler 2 guide checked - holding.' },
{ id: 'e3', at: at(26), author: 'A. Brandt', category: 'production', text: 'Changeover to still 0.5 l, 24-pack finished; first good pack at ' + time(START + 26 * MIN) + '.' },
{ id: 'e4', at: at(86), author: 'A. Brandt', category: 'safety', text: 'Guard door on the palletiser did not latch first time. Latched on the second try; maintenance told.', tag: 'plc.PAL.DOOR' },
{ id: 'e5', at: at(240), author: 'A. Brandt', category: 'quality', text: 'Two cases rejected at the checkweigher for underfill; filler head 7 adjusted by maintenance, OK since.', tag: 'plc.CW.REJECT' },
{ id: 'e6', at: at(330), author: 'J. Novak', category: 'maintenance', text: 'Palletiser door switch replaced. Tested ten times, latches every time.', tag: 'plc.PAL.DOOR' }
];
const REASONS = [
{ code: 'changeover', label: 'Changeover', group: 'Planned' },
{ code: 'cleaning', label: 'Cleaning', group: 'Planned' },
{ code: 'break', label: 'Break', group: 'Planned' },
{ code: 'breakdown', label: 'Breakdown', group: 'Unplanned' },
{ code: 'jam', label: 'Jam', group: 'Unplanned' },
{ code: 'starved', label: 'Starved (no material)', group: 'Unplanned' },
{ code: 'blocked', label: 'Blocked (downstream full)', group: 'Unplanned' },
{ code: 'quality', label: 'Quality hold', group: 'Unplanned' },
{ code: 'other', label: 'Other', group: 'Unplanned' }
];
const STOPS = [
{ id: 's1', machine: 'filler', machineName: 'Filler', from: at(71), to: at(78), seconds: 420, reason: '' },
{ id: 's2', machine: 'labeller', machineName: 'Labeller', from: at(196), to: at(205), seconds: 540, reason: '' },
{ id: 's3', machine: 'packer', machineName: 'Case packer', from: at(262), to: at(268), seconds: 360, reason: 'jam', classifiedBy: 'A. Brandt' }
];
const ALARMS = [
{ id: 'a1', tag: 'TT-3104', priority: 3, message: 'Glue unit temperature low', area: 'Case packer', active: true, acknowledged: true, timestamp: NOW - 95 * MIN },
{ id: 'a2', tag: 'LS-3021', priority: 2, message: 'Cap hopper level low', area: 'Capper', active: true, acknowledged: false, timestamp: NOW - 12 * MIN },
{ id: 'a3', tag: 'PT-3001', priority: 3, message: 'Air pressure low', area: 'Utilities', active: true, acknowledged: true, timestamp: NOW - 5 * H, shelvedUntil: NOW + 2 * H }
];
const ORDERS = [
{ id: 'ORD-24117', product: 'Still water 0.5 l, 24-pack', quantity: 60000, unit: 'bottles', status: 'running', produced: 55720, started: at(26), due: new Date(END + 20 * MIN).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(END + 4 * H).toISOString(), recipe: 'spark-05' }
];
let next = 7;
shiftLine.textContent = 'Shift A ' + time(START) + '–' + time(END) + ' · hands over to shift B in ' + Math.round((END - NOW) / MIN) + ' min';
function show() {
const unexplained = STOPS.filter((s) => !s.reason).length,
handover = ENTRIES.some((e) => e.category === 'handover' && Date.parse(e.at) >= START);
log.shift = { name: 'Shift A', from: new Date(START).toISOString(), to: new Date(END).toISOString() };
log.entries = ENTRIES.map((e) => Object.assign({}, e));
downtime.reasons = REASONS;
downtime.stops = STOPS.map((s) => Object.assign({}, s));
alarms.alarms = ALARMS.map((a) => Object.assign({}, a));
orders.orders = ORDERS.map((o) => Object.assign({}, o));
tiles.output.value = ORDERS[0].produced;
tiles.output.footnote = 'order ' + ORDERS[0].id + ', ' + Math.round(ORDERS[0].produced / ORDERS[0].quantity * 100) + ' %';
tiles.oee.value = 78.4;
tiles.stops.value = unexplained;
tiles.stops.state = unexplained ? 'warning' : 'ok';
tiles.alarms.value = ALARMS.length;
ready.textContent = unexplained || !handover
? 'Not ready: ' + [unexplained ? unexplained + ' stop' + (unexplained === 1 ? '' : 's') + ' without a reason' : '', handover ? '' : 'no handover entry yet'].filter(Boolean).join(', ')
: 'Ready to hand over';
ready.classList.toggle('sh-ready', !unexplained && handover);
}
/* The station stores the entry with its own clock and the signed-in name, and hands the log back. */
log.addEventListener('entryAdd', function (event) {
const entry = event.detail;
log.busy = true;
setTimeout(function () {
log.busy = false;
ENTRIES.push({ id: 'e' + next++, at: new Date().toISOString(), author: 'A. Brandt', category: entry.category, text: entry.text, tag: entry.tag, correcting: entry.correcting });
show();
}, 400);
});
downtime.addEventListener('stopClassify', function (event) {
const detail = event.detail;
downtime.busy = detail.id;
setTimeout(function () {
downtime.busy = '';
Object.assign(STOPS.find((s) => s.id === detail.id), { reason: detail.reason, comment: detail.comment, classifiedBy: 'A. Brandt' });
show();
}, 300);
});
alarms.addEventListener('acknowledge', function (event) {
const alarm = ALARMS.find((a) => a.id === event.detail.id);
if (alarm) {
alarm.acknowledged = true;
show();
}
});
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';
});
/* the rows say their state in their own colour and weight; the panel is narrow */
alarms.showStateColumn = false;
orders.showFinished = false;
tiles.output.formatFunction = (value) => Number(value).toLocaleString('en');
show();
};
styles.css
/* Shift handover. The log is the screen; the column beside it is the checklist a good handover
runs through. */
html,
body {
height: 100%;
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: 13px;
}
.sh-body {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
gap: 12px;
align-items: start;
}
@media (max-width: 1100px) {
.sh-body {
grid-template-columns: 1fr;
}
}
.sh-side {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.sh-hint {
font-size: 11px;
font-weight: 400;
text-transform: none;
letter-spacing: 0;
color: var(--smart-text-secondary-color);
}
.sh-log-panel smart-shift-log {
height: 860px;
}
.sh-tiles {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
/* the header says whether the handover is ready: the warning edge until it is */
#ready {
padding: 4px 10px;
border-left: 3px solid var(--smart-industrial-alarm-warning);
font-size: 13px;
font-weight: 600;
color: var(--smart-text-color);
}
#ready.sh-ready {
border-left-color: var(--smart-industrial-ok, var(--smart-primary));
}
smart-downtime-log {
height: 230px;
}
smart-alarm-grid {
width: 100%;
height: 190px;
}
smart-order-queue {
width: 100%;
}