From 4017617c376526807dd81c68c1a476e00d052cd7 Mon Sep 17 00:00:00 2001 From: Richard Sauer Date: Wed, 8 Apr 2026 16:03:26 +1000 Subject: [PATCH] User-friendly automation editor with dropdowns and visual UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- templates/editor.html | 193 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 176 insertions(+), 17 deletions(-) diff --git a/templates/editor.html b/templates/editor.html index 316015c..9dcd960 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -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)} @@ -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 = ` +
+

Edit Automation: ${c.label}

+

+ When this automation is activated, all these actions happen at once. + Add what should turn on, set speeds, or adjust temperatures. +

+
${rulesHtml}
+ +
+ + +
+
`; + 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 = ''; + if (temps.length) { + targetOpts += ''; + temps.forEach(c => targetOpts += ``); + targetOpts += ''; + } + if (motors.length) { + targetOpts += ''; + motors.forEach(c => targetOpts += ``); + targetOpts += ''; + } + if (outputs.length) { + targetOpts += ''; + outputs.forEach(c => targetOpts += ``); + targetOpts += ''; + } + + // 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 = ``; + } else if (isMotor) { + actionOpts = ` + + + `; + } else { + actionOpts = ` + + `; + } + + const showValue = rule.action === 'set'; + const valueUnit = isTemp ? '°C' : '%'; + const valueMax = isTemp ? 200 : 100; + const valueStep = isTemp ? 1 : 5; + + return ` +
+ + + + + ${valueUnit} + + +
`; +} + +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(); }