User-friendly automation editor with dropdowns and visual UI

- Replace text prompt with proper popup form
- Dropdown to select control (grouped: Temperatures, Motors, Outputs)
- Action dropdown: Turn ON / Turn OFF / Set speed to / Set temp to
- Number input with units (°C or %) only shows when action is "set"
- Add/remove rule buttons
- Save/Cancel buttons
- Non-technical customer can use without typing any code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Richard Sauer 2026-04-08 16:03:26 +10:00
parent c6e3b0c0eb
commit 4017617c37

View File

@ -168,6 +168,22 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
.popup .sp-btn:active{background:var(--border)}
.popup .sp-val{font-size:32px;min-width:80px;text-align:center}
.popup .close-btn{width:160px;height:44px;border:none;border-radius:8px;background:var(--blue);color:var(--text);font-size:16px;font-weight:600;cursor:pointer}
/* Automation editor */
.auto-editor{width:700px;max-width:95vw;max-height:80vh;overflow-y:auto}
.auto-editor h3{font-size:20px;margin-bottom:12px}
.auto-rule{display:flex;gap:8px;align-items:center;padding:10px;background:var(--bg);border-radius:8px;margin-bottom:8px}
.auto-rule select,.auto-rule input{padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-size:14px}
.auto-rule select{min-width:120px}
.auto-rule input[type=number]{width:80px}
.auto-rule .rule-del{width:32px;height:32px;border:none;border-radius:6px;background:rgba(255,23,68,0.2);color:var(--red);font-size:16px;cursor:pointer;flex-shrink:0}
.auto-rule .rule-del:hover{background:var(--red);color:#fff}
.auto-add-rule{width:100%;padding:12px;border:2px dashed var(--dim);border-radius:8px;background:none;color:var(--text2);font-size:14px;cursor:pointer;margin:8px 0}
.auto-add-rule:hover{border-color:var(--blue);color:var(--blue)}
.auto-btns{display:flex;gap:12px;justify-content:center;margin-top:16px}
.auto-btns button{padding:10px 24px;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer}
.auto-btns .save-btn{background:var(--green);color:#000}
.auto-btns .cancel-btn{background:var(--card);color:var(--text);border:1px solid var(--border)}
</style>
</head>
<body>
@ -499,27 +515,170 @@ function createCardEl(c) {
return el;
}
let autoEditCard = null;
function editAutomation(c) {
const rulesStr = (c.rules || []).map(r => `${r.type}:${r.target}:${r.action}:${r.value||''}`).join('\n');
const help = `Edit automation rules (one per line):
Format: type:target:action:value
autoEditCard = c;
// Gather all available controls from the layout
const allControls = [];
layout.pages.forEach(p => p.cards.forEach(card => {
if (card.id !== c.id) allControls.push(card);
}));
Types: temp, motor, output
Actions: set, on, off
Examples:
temp:Heat Input:set:130
motor:Hot Fan:set:65
motor:Conveyor:on
output:Brush:on
output:Mill:off
const popup = document.getElementById('popup');
popup.style.borderColor = 'var(--blue)';
Current rules:`;
const input = prompt(help, rulesStr);
if (input === null) return;
c.rules = input.split('\n').filter(l => l.trim()).map(l => {
const [type, target, action, value] = l.split(':');
return {type: type||'output', target: target||'', label: target||'', action: action||'on', value: value ? parseInt(value) : undefined};
let rulesHtml = '';
(c.rules || []).forEach((r, i) => {
rulesHtml += buildRuleRow(i, r, allControls);
});
popup.innerHTML = `
<div class="auto-editor">
<h3>Edit Automation: ${c.label}</h3>
<p style="color:var(--text2);font-size:13px;margin-bottom:16px">
When this automation is activated, all these actions happen at once.
Add what should turn on, set speeds, or adjust temperatures.
</p>
<div id="auto-rules">${rulesHtml}</div>
<button class="auto-add-rule" onclick="addAutoRule()">+ Add another action</button>
<div class="auto-btns">
<button class="cancel-btn" onclick="closePopup()">Cancel</button>
<button class="save-btn" onclick="saveAutoRules()">Save</button>
</div>
</div>`;
document.getElementById('popup-overlay').classList.add('active');
}
function buildRuleRow(idx, rule, controls) {
if (!controls) {
controls = [];
layout.pages.forEach(p => p.cards.forEach(c => { if(autoEditCard && c.id !== autoEditCard.id) controls.push(c); }));
}
// Group controls by type
const temps = controls.filter(c => c.type === 'temp');
const motors = controls.filter(c => c.type === 'motor');
const outputs = controls.filter(c => c.type === 'output' || c.type === 'burner');
// What to control dropdown
let targetOpts = '<option value="">-- Select --</option>';
if (temps.length) {
targetOpts += '<optgroup label="Temperatures">';
temps.forEach(c => targetOpts += `<option value="${c.id}" data-type="temp" ${rule.target===c.id?'selected':''}>${c.label}</option>`);
targetOpts += '</optgroup>';
}
if (motors.length) {
targetOpts += '<optgroup label="Motors / Speeds">';
motors.forEach(c => targetOpts += `<option value="${c.id}" data-type="motor" ${rule.target===c.id?'selected':''}>${c.label}</option>`);
targetOpts += '</optgroup>';
}
if (outputs.length) {
targetOpts += '<optgroup label="Outputs / Switches">';
outputs.forEach(c => targetOpts += `<option value="${c.id}" data-type="output" ${rule.target===c.id?'selected':''}>${c.label}</option>`);
targetOpts += '</optgroup>';
}
// Action depends on type
const isTemp = rule.type === 'temp';
const isMotor = rule.type === 'motor';
const isOutput = rule.type === 'output' || rule.type === 'burner';
let actionOpts = '';
if (isTemp) {
actionOpts = `<option value="set" ${rule.action==='set'?'selected':''}>Set to</option>`;
} else if (isMotor) {
actionOpts = `
<option value="on" ${rule.action==='on'?'selected':''}>Turn ON</option>
<option value="off" ${rule.action==='off'?'selected':''}>Turn OFF</option>
<option value="set" ${rule.action==='set'?'selected':''}>Set speed to</option>`;
} else {
actionOpts = `
<option value="on" ${rule.action==='on'?'selected':''}>Turn ON</option>
<option value="off" ${rule.action==='off'?'selected':''}>Turn OFF</option>`;
}
const showValue = rule.action === 'set';
const valueUnit = isTemp ? '&deg;C' : '%';
const valueMax = isTemp ? 200 : 100;
const valueStep = isTemp ? 1 : 5;
return `
<div class="auto-rule" data-idx="${idx}">
<select onchange="autoRuleTargetChanged(this, ${idx})">${targetOpts}</select>
<select class="rule-action" data-idx="${idx}" onchange="autoRuleActionChanged(${idx})">${actionOpts}</select>
<span class="rule-value-wrap" style="display:${showValue?'flex':'none'};align-items:center;gap:6px">
<input type="number" class="rule-value" data-idx="${idx}" value="${rule.value||0}" min="0" max="${valueMax}" step="${valueStep}">
<span style="color:var(--text2);font-size:13px">${valueUnit}</span>
</span>
<button class="rule-del" onclick="removeAutoRule(${idx})">&#x2715;</button>
</div>`;
}
function autoRuleTargetChanged(sel, idx) {
const opt = sel.options[sel.selectedIndex];
const type = opt.dataset.type || 'output';
// Store temporarily so we can rebuild the action dropdown
const rules = collectAutoRules();
if (rules[idx]) {
rules[idx].target = sel.value;
rules[idx].type = type;
rules[idx].label = opt.textContent;
// Reset action based on type
if (type === 'temp') rules[idx].action = 'set';
else rules[idx].action = 'on';
}
autoEditCard.rules = rules;
editAutomation(autoEditCard); // re-render the whole popup
}
function autoRuleActionChanged(idx) {
const actionSel = document.querySelector(`.rule-action[data-idx="${idx}"]`);
const valueWrap = document.querySelectorAll('.rule-value-wrap')[idx];
if (actionSel && valueWrap) {
valueWrap.style.display = actionSel.value === 'set' ? 'flex' : 'none';
}
}
function addAutoRule() {
if (!autoEditCard) return;
const rules = collectAutoRules();
rules.push({type:'output', target:'', label:'', action:'on', value:0});
autoEditCard.rules = rules;
editAutomation(autoEditCard);
}
function removeAutoRule(idx) {
if (!autoEditCard) return;
const rules = collectAutoRules();
rules.splice(idx, 1);
autoEditCard.rules = rules;
editAutomation(autoEditCard);
}
function collectAutoRules() {
const rows = document.querySelectorAll('.auto-rule');
const rules = [];
rows.forEach(row => {
const targetSel = row.querySelector('select');
const actionSel = row.querySelector('.rule-action');
const valueInput = row.querySelector('.rule-value');
const opt = targetSel.options[targetSel.selectedIndex];
rules.push({
type: opt?.dataset?.type || 'output',
target: targetSel.value,
label: opt?.textContent || '',
action: actionSel?.value || 'on',
value: actionSel?.value === 'set' ? parseInt(valueInput?.value || 0) : undefined
});
});
return rules;
}
function saveAutoRules() {
if (!autoEditCard) return;
autoEditCard.rules = collectAutoRules().filter(r => r.target); // remove empty rows
closePopup();
renderCards();
}