Skip to content

Commit d4c1b63

Browse files
authored
Merge branch 'rstrouse:master' into fix/century-pump-light
2 parents 7a5f5f0 + 0ff6537 commit d4c1b63

17 files changed

Lines changed: 846 additions & 117 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Playwright artifacts
2+
playwright-report/
3+
test-results/
4+
blob-report/
5+
.cache/ms-playwright/
16
## Ignore Visual Studio temporary files, build results, and
27
## files generated by popular Visual Studio add-ons.
38

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Below is a minimal example running both the backend `nodejs-poolController` (ser
1818
```yaml
1919
services:
2020
njspc:
21-
image: ghcr.io/sam2kb/njspc
21+
image: ghcr.io/tagyoureit/njspc
2222
container_name: njspc
2323
restart: unless-stopped
2424
environment:
@@ -48,7 +48,7 @@ services:
4848
# user: "0:0"
4949

5050
njspc-dash:
51-
image: ghcr.io/sam2kb/njspc-dash
51+
image: ghcr.io/rstrouse/njspc-dash
5252
container_name: njspc-dash
5353
restart: unless-stopped
5454
depends_on:
@@ -83,7 +83,7 @@ The application loads configuration from `/app/config.json` at startup and rewri
8383
1. Create a host directory and seed the file (optional – if omitted, an empty file will be populated after first change):
8484
```bash
8585
mkdir -p config
86-
docker run --rm ghcr.io/sam2kb/njspc-dash cat /app/config.json > config/config.json
86+
docker run --rm ghcr.io/rstrouse/njspc-dash cat /app/config.json > config/config.json
8787
```
8888
2. Use the bind mount shown in the compose example: `./config/config.json:/app/config.json`.
8989
3. If the mounted file is empty, defaults + environment overrides are applied and the file will be written once you change settings via the UI/API.

pages/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!DOCTYPE html>
1+
<!DOCTYPE html>
22

33
<html xmlns="http://www.w3.org/1999/xhtml">
44

@@ -73,7 +73,7 @@
7373
</head>
7474

7575
<body>
76-
<div class="dashOuter picDashboard" data-panel="dashboard">
76+
<div class="dashOuter picDashboard" data-panel="dashboard" role="main" aria-label="Pool Control Dashboard">
7777
<header class="picHeader">
7878
<div class="picController picControlPanel control-panel" style="display:block;"></div>
7979
</header>

pages/messageManager.html

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!DOCTYPE html>
1+
<!DOCTYPE html>
22

33
<html xmlns="http://www.w3.org/1999/xhtml">
44
<head>
@@ -45,27 +45,28 @@
4545

4646
</head>
4747
<body>
48-
<div class="mmgrOuter picMessageManager" data-panel="messages">
48+
<div class="mmgrOuter picMessageManager" data-panel="messages" role="main" aria-label="Message Manager">
4949
<header>
5050
<div class="picController picControlPanel control-panel" style="display:block;"></div>
5151
</header>
5252
<div class="messageContainer">
5353
<!-- Tab Navigation (Settings-style tabBar widget) -->
5454
<div class="mmgrTabHeader">
55-
<div class="mmgrTabBar"></div>
55+
<div class="mmgrTabBar" id="mmgrTabBar" data-nav-group="message-manager-tabs"></div>
5656
<div class="view-bar-actions">
5757
<span class="view-bar-status" title="Replay load status"></span>
58-
<button type="button" class="view-bar-icon-btn view-bar-clear" title="Clear Messages">
58+
<button type="button" class="view-bar-icon-btn view-bar-clear" title="Clear Messages" data-nav-id="mmgr-clear-messages" aria-label="Clear Messages">
5959
<i class="fas fa-broom"></i>
6060
</button>
61-
<button type="button" class="view-bar-icon-btn view-bar-filter" title="Filter Display">
61+
<button type="button" class="view-bar-icon-btn view-bar-filter" title="Filter Display" data-nav-id="mmgr-filter-display" aria-label="Filter Display">
6262
<i class="fas fa-filter"></i>
6363
</button>
64-
<button type="button" class="upload-btn view-bar-upload" title="Choose Files">
64+
<button type="button" class="upload-btn view-bar-upload" title="Choose Files" data-nav-id="mmgr-choose-files" aria-label="Choose Replay Files">
6565
<i class="fas fa-folder-open"></i> Choose Files
6666
</button>
6767
<input id="universalReplayFileInput" type="file" accept=".json,.zip,.log" multiple style="display:none;" />
6868
</div>
69+
<div class="mmgr-live-region" aria-live="polite" aria-atomic="true"></div>
6970
</div>
7071
<!-- Message List View -->
7172
<div class="view-container active" data-view="messages">
@@ -119,11 +120,6 @@
119120
$mmTabBar.find('div.picTab[data-tabid="tabEntityFlow"] span.picTabText:first')
120121
.html('<i class="fas fa-project-diagram"></i>Entity Flow');
121122

122-
// Move actions into the tab row (right aligned)
123-
var $tabsRow = $mmTabBar.find('div.picTabs:first');
124-
$('<div class="mmgrTabSpacer"></div>').appendTo($tabsRow);
125-
$('.view-bar-actions').appendTo($tabsRow);
126-
127123
// View switching on tab change
128124
$mmTabBar.on('tabchange', function (evt) {
129125
var viewName = 'messages';
@@ -152,6 +148,7 @@
152148
var $replayStatus = $('.view-bar-status');
153149
var $clearBtn = $('.view-bar-clear');
154150
var $filterBtn = $('.view-bar-filter');
151+
var $liveRegion = $('.mmgr-live-region');
155152

156153
$replayBtn.on('click', function(e) {
157154
e.preventDefault();
@@ -161,6 +158,7 @@
161158
// Show status updates emitted from Entity Flow widget
162159
$('div.picEntityFlow').on('replayLoadStatus', function(e) {
163160
$replayStatus.text(e.text || '');
161+
$liveRegion.text(e.text || '');
164162
});
165163

166164
$replayInput.on('change', function(e) {
@@ -183,6 +181,7 @@
183181
var ml = $('div.picMessages:first')[0];
184182
if (ml && ml.clear) ml.clear();
185183
$replayStatus.text('');
184+
$liveRegion.text('Messages cleared');
186185
// Reset upload button text
187186
$replayBtn.html('<i class="fas fa-folder-open"></i> Choose Files');
188187
$replayBtn.attr('title', 'Choose Files');

scripts/config/chemistry.js

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(function ($) {
1+
(function ($) {
22
$.widget('pic.configChemistry', {
33
options: {},
44
_create: function () {
@@ -33,6 +33,11 @@
3333
});
3434
$.getApiService('/config/options/chemControllers', null, 'Loading Options...', function (opts, status, xhr) {
3535
chemOpts = opts;
36+
var standaloneSupported = typeof opts.intellichemStandaloneSupported !== 'undefined'
37+
? makeBool(opts.intellichemStandaloneSupported)
38+
: (($('body').attr('data-controllertype') || '').toLowerCase() === 'nixie');
39+
var standaloneEnabledMessage = 'Standalone mode keeps IntelliChem communication active even when the pool body is off. Use for Intellichem-only systems.';
40+
var standaloneUnsupportedMessage = 'Standalone mode is only available when Nixie directly controls IntelliChem.';
3641
for (var i = 0; i < opts.controllers.length; i++) {
3742
$('<div></div>').appendTo(pnl).pnlChemControllerConfig(opts)[0].dataBind(opts.controllers[i]);
3843
}
@@ -190,6 +195,7 @@
190195
case 'intellichem':
191196
cc.name = 'IntelliChem';
192197
cc.address = 144;
198+
cc.intellichemStandalone = standaloneSupported && !!data.intellichemStandalone;
193199
cc.ph = { setpoint: 7.4, dosingMethod: 0, flowReadingsOnly: true, tolerance: { enabled: true, low: 7.2, high: 7.6 }, phSupply: 1, acidType: 5, tank: { alarmEmptyEnabled: true, alarmEmptyLevel: 20 } };
194200
cc.orp = { setpoint: 750, dosingMethod: 0, flowReadingsOnly: true, tolerance: { enabled: true, low: 650, high: 800 }, phLockout: 7.8, tank: { alarmEmptyEnabled: true, alarmEmptyLevel: 20 } };
195201
break;
@@ -213,7 +219,7 @@
213219
}
214220
$('<div></div>').css({ textAlign: 'center' }).appendTo(divSelection).append('<i class="fas fa-flask" style="font-size:30pt;"></i>');
215221
$('<div></div>').css({ textAlign: 'center' }).appendTo(divSelection).text('Chem Controller');
216-
$('<div></div>').appendTo(divSelection).pickList({
222+
var chemTypePick = $('<div></div>').appendTo(divSelection).pickList({
217223
required: true,
218224
style: { textAlign: 'left' },
219225
bindColumn: 0, displayColumn: 2, labelText: 'Chem Controller Type<br/>', binding: 'type',
@@ -224,9 +230,30 @@
224230
e.stopPropagation();
225231
}).on('mousedown', function (e) {
226232
e.stopImmediatePropagation();
227-
228233
});
229-
234+
var standaloneOpt = $('<div></div>').appendTo(divSelection).checkbox({
235+
labelText: 'Intellichem Standalone',
236+
binding: 'intellichemStandalone'
237+
}).css({ marginTop: '.25rem', textAlign: 'left' })
238+
.on('mousedown', function (e) { e.stopImmediatePropagation(); })
239+
.on('click', function (e) { e.stopPropagation(); })
240+
.hide();
241+
var standaloneInfo = $('<span></span>').appendTo(divSelection)
242+
.css({ marginLeft: '.35rem', cursor: 'help', display: 'none' })
243+
.append('<i class="fas fa-info-circle"></i>');
244+
var updateStandaloneOption = function (selectedType) {
245+
var isIntellichem = typeof selectedType !== 'undefined' && selectedType !== null && selectedType.name === 'intellichem';
246+
if (!isIntellichem || !standaloneSupported) {
247+
standaloneOpt.hide();
248+
standaloneInfo.hide();
249+
if (standaloneOpt[0] && typeof standaloneOpt[0].val === 'function') standaloneOpt[0].val(false);
250+
return;
251+
}
252+
standaloneOpt.show();
253+
standaloneInfo.show();
254+
if (standaloneOpt[0] && typeof standaloneOpt[0].disabled === 'function') standaloneOpt[0].disabled(false);
255+
};
256+
chemTypePick.on('selchanged', function (evt) { updateStandaloneOption(evt.newItem); });
230257
divSelection = $('<div></div>').addClass('picButton').addClass('chemController-type').addClass('chemDoser').addClass('btn').css({ width: '157px', height: '97px', verticalAlign: 'middle' })
231258
.appendTo(line)
232259
.on('mouseover', function (e) { $(e.currentTarget).addClass('button-hover'); })
@@ -1158,6 +1185,44 @@
11581185
columns: [{ binding: 'val', hidden: true, text: 'Address' }, { binding: 'desc', text: 'Address' }],
11591186
items: addresses, inputAttrs: { style: { width: '3rem' } }, labelAttrs: { style: { marginLeft: '.25rem' } }
11601187
});
1188+
$('<div></div>').appendTo(line).checkbox({
1189+
labelText: 'Intellichem Standalone',
1190+
binding: binding + 'intellichemStandalone'
1191+
}).addClass('pnl-intellichem-standalone')
1192+
.hide();
1193+
var standaloneTipMessage = 'Standalone mode keeps IntelliChem communication active even when the pool body is off. Use for Intellichem-only systems.';
1194+
var standaloneInfo = $('<span></span>').appendTo(line)
1195+
.addClass('pnl-intellichem-standalone')
1196+
.css({ marginLeft: '.25rem', cursor: 'help', display: 'none' })
1197+
.attr('tabindex', 0)
1198+
.attr('data-tip-message', standaloneTipMessage);
1199+
$('<i class="fas fa-info-circle"></i>').appendTo(standaloneInfo);
1200+
var showStandaloneTip = function () {
1201+
$.pic.fieldTip.clearTips(line);
1202+
var tipMessage = standaloneInfo.attr('data-tip-message') || standaloneTipMessage;
1203+
var tipContent = $('<div></div>').css({
1204+
maxWidth: '22rem',
1205+
whiteSpace: 'normal',
1206+
lineHeight: '1.25rem'
1207+
}).text(tipMessage);
1208+
$.pic.fieldTip.showTip(line, {
1209+
field: standaloneInfo,
1210+
message: tipContent,
1211+
closeAfter: 5000,
1212+
placement: { my: 'right top', at: 'right bottom+8' }
1213+
});
1214+
setTimeout(function () {
1215+
line.find('div.picFieldTip:last')
1216+
.addClass('intellichem-standalone-tip')
1217+
.removeClass('left right')
1218+
.addClass('bottom');
1219+
}, 0);
1220+
};
1221+
standaloneInfo
1222+
.on('mouseenter focusin click', function (evt) {
1223+
evt.stopPropagation();
1224+
showStandaloneTip();
1225+
});
11611226
$('<hr></hr>').appendTo(pnl);
11621227
$('<div></div>').appendTo(pnl).addClass('pnl-chemcontroller-type');
11631228

@@ -1208,6 +1273,11 @@
12081273
var cols = acc[0].columns();
12091274
cols[0].elText().text(obj.name);
12101275
var type = o.types.find(elem => elem.val === obj.type);
1276+
var standaloneSupported = typeof o.intellichemStandaloneSupported !== 'undefined'
1277+
? makeBool(o.intellichemStandaloneSupported)
1278+
: (($('body').attr('data-controllertype') || '').toLowerCase() === 'nixie');
1279+
var standaloneEnabledMessage = 'Standalone mode keeps IntelliChem communication active even when the pool body is off. Use for Intellichem-only systems.';
1280+
var standaloneUnsupportedMessage = 'Standalone mode is only available when Nixie directly controls IntelliChem.';
12111281
cols[1].elText().text(typeof type !== 'undefined' ? type.desc : 'Controller');
12121282

12131283
var ctype = el.attr('data-controllertype');
@@ -1258,10 +1328,27 @@
12581328
if (typeof obj.orp === 'undefined') obj.orp = {
12591329
mixingTime: 60, maxDosingTime: 20, pump: {}, probe: {}, tank: {}
12601330
};
1331+
if (typeof obj.intellichemStandalone === 'undefined') obj.intellichemStandalone = false;
12611332
self.splitSeconds('mixingTime', obj.ph, obj.ph.mixingTime);
12621333
self.splitSeconds('mixingTime', obj.orp, obj.orp.mixingTime);
12631334
self.splitSeconds('maxDosingTime', obj.ph, obj.ph.maxDosingTime);
12641335
self.splitSeconds('maxDosingTime', obj.orp, obj.orp.maxDosingTime);
1336+
el.find('div.picCheckbox[data-bind="intellichemStandalone"]').each(function () {
1337+
if (type.name === 'intellichem') {
1338+
$(this).show();
1339+
if (typeof this.disabled === 'function') this.disabled(!standaloneSupported);
1340+
if (!standaloneSupported && typeof this.val === 'function') this.val(false);
1341+
}
1342+
else $(this).hide();
1343+
});
1344+
el.find('span.pnl-intellichem-standalone').each(function () {
1345+
if (type.name === 'intellichem') {
1346+
$(this).show();
1347+
var tip = standaloneSupported ? standaloneEnabledMessage : standaloneUnsupportedMessage;
1348+
$(this).attr('data-tip-message', tip);
1349+
}
1350+
else $(this).hide();
1351+
});
12651352
if (type.hasAddress) {
12661353
el.find('*[data-bind="address"]').each(function () {
12671354
$(this).show();

scripts/config/general.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(function ($) {
1+
(function ($) {
22
$.widget('pic.configGeneral', {
33
options: {},
44
_create: function () {
@@ -223,6 +223,26 @@
223223
});
224224
}
225225
if (controller !== 'nixie') $('<div></div>').appendTo(line).checkbox({ labelText: 'Heater Cooldown Delay', binding: 'options.cooldownDelay' });
226+
var firmware = parseFloat($('body').attr('data-firmware') || '0');
227+
if (controller === 'intellicenter' && firmware >= 3.008) {
228+
line = $('<div></div>').appendTo(pnl);
229+
$('<div></div>').appendTo(line).valueSpinner({
230+
canEdit: true, labelText: 'Freeze Cycle Time', binding: 'options.freezeCycleTime', dataType: 'number', fmtType: '#,##0', min: 1, max: 60, step: 1, maxlength: 3,
231+
units: 'min', labelAttrs: { style: { width: '14rem', display: 'inline-block' } }, inputAttrs: { style: { width: '4.5rem' } }
232+
});
233+
line = $('<div></div>').appendTo(pnl);
234+
$('<div></div>').appendTo(line).pickList({
235+
labelText: 'Freeze Override', binding: 'options.freezeOverride', bindColumn: 0,
236+
columns: [{ binding: 'val', hidden: true, text: 'Val' }, { binding: 'desc', text: 'Duration' }],
237+
items: [
238+
{ val: 30, desc: '30 minutes' },
239+
{ val: 90, desc: '90 minutes (1.5 hrs)' },
240+
{ val: 150, desc: '150 minutes (2.5 hrs)' },
241+
{ val: 210, desc: '210 minutes (3.5 hrs)' }
242+
],
243+
inputAttrs: { style: { width: '14rem' } }, labelAttrs: { style: { width: '14rem', display: 'inline-block' } }
244+
});
245+
}
226246
btnPnl = $('<div class="picBtnPanel btn-panel"></div>').appendTo(pnl);
227247
btnSave = $('<div id="btnSaveDelays"></div>').appendTo(btnPnl).actionButton({ text: 'Save Delays', icon: '<i class="fas fa-save"></i>' });
228248
btnSave.on('click', function (e) {

scripts/configPage.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(function ($) {
1+
(function ($) {
22
$.widget('pic.configPage', {
33
options: {
44
cfg: {},
@@ -110,6 +110,8 @@
110110
var self = this, o = self.options, el = self.element;
111111
var tabs = $('<div class="picTabPanel"></div>');
112112
tabs.appendTo(el);
113+
tabs.attr('id', 'configMainTabBar');
114+
tabs.attr('data-nav-group', 'config-main-tabs');
113115
tabs.tabBar();
114116
tabs.find('div.picTabContents').addClass('picConfigTabContents');
115117
tabs.on('tabchange', function (evt) { self._onTabChanged(evt); });
@@ -168,6 +170,8 @@
168170
if (typeof subTabs !== 'undefined') {
169171
var tabs = $('<div class="picTabPanel"></div>');
170172
tabs.appendTo(contents);
173+
tabs.attr('id', `${attrs.id || 'config'}SubTabBar`);
174+
tabs.attr('data-nav-group', `${attrs.id || 'config'}-sub-tabs`);
171175
tabs.tabBar();
172176
tabs.find('div.picTabContents').addClass('picConfigTabContents');
173177
//tabs.on('tabchange', function (evt) { self._onTabChanged(evt); });

0 commit comments

Comments
 (0)