diff --git a/templates/editor.html b/templates/editor.html index 5da5fef..e414b50 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -52,8 +52,6 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var( .hmi-card.type-motor,.hmi-card.type-output,.hmi-card.type-burner{background:var(--red)} .hmi-card.on.type-motor,.hmi-card.on.type-output{background:var(--green)} .hmi-card.on.type-burner{background:var(--amber)} -.hmi-card.type-automation{background:var(--red);border:none} -.hmi-card.on.type-automation{background:var(--green)} .hmi-card .card-label{font-size:18px;color:var(--text2);text-align:center} .hmi-card .card-value{font-size:28px;font-weight:700} @@ -74,17 +72,10 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var( .sortable-ghost{opacity:0.4;border:2px dashed var(--blue) !important} /* Add card palette */ -/* Add card placeholder */ -.add-placeholder{width:calc(50% - 4px);height:calc(33.3% - 6px);border:2px dashed var(--dim);border-radius:10px;display:none;align-items:center;justify-content:center;cursor:pointer;transition:all 0.15s;position:relative} -.edit-mode .add-placeholder{display:flex} -.add-placeholder:hover{border-color:var(--blue);background:rgba(45,127,249,0.05)} -.add-placeholder .plus{font-size:36px;color:var(--dim)} -.add-placeholder:hover .plus{color:var(--blue)} -/* Add menu dropdown */ -.add-menu{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--card);border:1px solid var(--border);border-radius:10px;padding:6px;z-index:20;min-width:160px;box-shadow:0 8px 24px rgba(0,0,0,0.5)} -.add-menu.open{display:flex;flex-direction:column;gap:2px} -.add-menu button{padding:10px 16px;border:none;border-radius:6px;background:none;color:var(--text);font-size:14px;text-align:left;cursor:pointer} -.add-menu button:hover{background:var(--border)} +.add-palette{display:none;padding:8px;gap:8px;border-top:1px solid var(--border);flex-shrink:0} +.edit-mode .add-palette{display:flex} +.add-palette button{flex:1;padding:10px;border:2px dashed var(--dim);border-radius:8px;background:none;color:var(--text2);font-size:13px;cursor:pointer} +.add-palette button:hover{border-color:var(--blue);color:var(--blue)} /* Comment badge on cards */ .hmi-card .card-comment-badge{position:absolute;bottom:4px;right:4px;width:20px;height:20px;border-radius:50%;background:var(--amber);color:#000;font-size:10px;font-weight:700;display:none;align-items:center;justify-content:center} @@ -158,9 +149,9 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var( .inline-edit{background:transparent;border:none;border-bottom:2px solid var(--blue);color:var(--text);font-size:inherit;font-family:inherit;text-align:center;outline:none;width:100%} /* Popup */ -#popup-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.85);display:none;align-items:center;justify-content:center;z-index:500} +#popup-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);display:none;align-items:center;justify-content:center;z-index:50} #popup-overlay.active{display:flex} -.popup{z-index:501;background:#0d1220;border:none;border-radius:16px;padding:24px;width:500px;display:flex;flex-direction:column;align-items:center;gap:16px;box-shadow:0 16px 48px rgba(0,0,0,0.8)} +.popup{background:var(--card);border:3px solid var(--blue);border-radius:16px;padding:24px;width:500px;display:flex;flex-direction:column;align-items:center;gap:16px} .popup .title{font-size:24px;color:var(--text2)} .popup .live{font-size:32px;font-weight:700} .popup .sp-row{display:flex;align-items:center;gap:20px} @@ -168,32 +159,6 @@ 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;background:#0d1220;border-radius:16px;padding:24px} -.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)} - -/* Card properties editor */ -.props-editor{width:420px;max-width:95vw} -.props-editor h3{font-size:20px;margin-bottom:16px} -.prop-row{display:flex;align-items:center;gap:12px;margin-bottom:12px} -.prop-row label{width:100px;font-size:13px;color:var(--text2);flex-shrink:0;text-align:right} -.prop-row input,.prop-row select{flex:1;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-size:14px} -.prop-row input[type=color]{width:50px;height:38px;padding:2px;cursor:pointer;flex:0} -.prop-row input[type=range]{flex:1} -.prop-preview{display:inline-block;width:24px;height:24px;border-radius:4px;border:1px solid var(--border);vertical-align:middle;margin-left:8px} @@ -205,8 +170,8 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var( HMI Design Tool
- {{ user }} - Logout + + | @@ -228,7 +193,12 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
- +
+ + + + +
@@ -314,7 +284,7 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var( let layout = null; let currentPage = 0; let mode = 'edit'; // 'edit' or 'preview' -let currentUser = '{{ user }}'; +let currentUser = localStorage.getItem('bfa-user') || ''; let simState = {}; // id -> {on:bool, value:number} let commentTarget = null; let sortableInstance = null; @@ -333,7 +303,17 @@ async function init() { renderSchematic(); } -function initUsers() { /* handled by login page */ } +function initUsers() { + const sel = document.getElementById('user-select'); + sel.innerHTML = ''; + (layout.users || []).forEach(u => { + const o = document.createElement('option'); + o.value = u; o.textContent = u; + if (u === currentUser) o.selected = true; + sel.appendChild(o); + }); + sel.onchange = () => { currentUser = sel.value; localStorage.setItem('bfa-user', currentUser); }; +} function initSimState() { layout.pages.forEach(p => p.cards.forEach(c => { @@ -344,7 +324,15 @@ function initSimState() { })); } -function addUser() { window.location.href = 'logout'; } +async function addUser() { + const name = prompt('Enter name:'); + if (!name) return; + await fetch('api/users', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name})}); + layout.users.push(name); + currentUser = name; + localStorage.setItem('bfa-user', name); + initUsers(); +} // ══════════════════════════════════════════════════════ // MODE @@ -356,7 +344,7 @@ function setMode(m) { document.getElementById('card-grid').classList.toggle('edit-mode', m === 'edit'); document.getElementById('preview-tag').style.display = m === 'preview' ? 'inline' : 'none'; document.querySelectorAll('.edit-overlay').forEach(e => e.style.display = m === 'edit' ? 'flex' : 'none'); - // add-palette removed — inline + buttons handle this now + document.getElementById('add-palette').style.display = m === 'edit' ? 'flex' : 'none'; if (m === 'edit') initSortable(); else if (sortableInstance) { sortableInstance.destroy(); sortableInstance = null; } } @@ -428,35 +416,7 @@ function renderCards() { const el = createCardEl(c); grid.appendChild(el); }); - // In edit mode, fill remaining slots with + placeholders (up to 6 slots per page) - if (mode === 'edit') { - const halfCards = page.cards.filter(c => c.width !== 'full').length; - const fullCards = page.cards.filter(c => c.width === 'full').length; - const slotsUsed = halfCards + fullCards * 2; - const emptySlots = Math.max(1, 6 - slotsUsed); // always show at least 1 - for (let i = 0; i < emptySlots; i++) { - const ph = document.createElement('div'); - ph.className = 'add-placeholder'; - ph.innerHTML = `+ -
- - - - - -
`; - ph.addEventListener('click', (e) => { - if (e.target.tagName === 'BUTTON') return; - // Toggle the menu - const menu = ph.querySelector('.add-menu'); - document.querySelectorAll('.add-menu.open').forEach(m => { if(m!==menu) m.classList.remove('open'); }); - menu.classList.toggle('open'); - e.stopPropagation(); - }); - grid.appendChild(ph); - } - initSortable(); - } + if (mode === 'edit') initSortable(); } function createCardEl(c) { @@ -471,8 +431,6 @@ function createCardEl(c) { // Edit buttons el.innerHTML += `
- -
`; @@ -481,219 +439,32 @@ function createCardEl(c) { const commentCount = (layout.comments || []).filter(x => x.target === c.id).length; el.innerHTML += `
${commentCount}
`; - const cfs = c.fontSize || 18; if (c.type === 'temp') { const color = c.color || '#ff8844'; - el.innerHTML += `
${c.label}
`; + el.innerHTML += `
${c.label}
`; el.innerHTML += `
${Math.round(s.value)} °C
`; el.innerHTML += `
`; el.innerHTML += `
SP: ${c.sp_default || 70} °C
`; el.onclick = () => { if (mode === 'preview') openTempPopup(c); }; } else if (c.type === 'motor') { el.innerHTML += `
- ${c.label} + ${c.label} ${c.sp_default || 50}%
`; el.innerHTML += `
${s.on ? 'ON' : 'OFF'}
`; el.onclick = () => { if (mode === 'preview') toggleSim(c.id); }; } else if (c.type === 'burner') { - el.innerHTML += `
${c.label}
`; + el.innerHTML += `
${c.label}
`; el.innerHTML += `
${s.on ? 'ON' : 'OFF'}
`; el.onclick = () => { if (mode === 'preview') toggleSim(c.id); }; - } else if (c.type === 'automation') { - // Automation card — combines outputs, speeds, temp setpoints - el.innerHTML += `
${c.label}
`; - const rules = c.rules || []; - if (rules.length === 0) { - el.innerHTML += `
No rules configured
Click to edit in edit mode
`; - } else { - let rulesHtml = '
'; - rules.forEach(r => { - const icon = r.type === 'temp' ? '🌡' : r.type === 'motor' ? '⚙' : r.type === 'output' ? '⚡' : '●'; - rulesHtml += `
${icon} ${r.label || r.target}: ${r.action || ''} ${r.value !== undefined ? r.value : ''}
`; - }); - rulesHtml += '
'; - el.innerHTML += rulesHtml; - } - el.onclick = () => { - if (mode === 'edit') { event.stopPropagation(); editAutomation(c); } - else if (mode === 'preview') toggleSim(c.id); - }; } else { // output - el.innerHTML += `
${c.label}
`; + el.innerHTML += `
${c.label}
`; el.innerHTML += `
${s.on ? 'ON' : 'OFF'}
`; el.onclick = () => { if (mode === 'preview') toggleSim(c.id); }; } return el; } -let autoEditCard = null; - -function editAutomation(c) { - 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); - })); - - const popup = document.getElementById('popup'); - popup.style.borderColor = 'var(--border)'; - - 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(); -} - // ══════════════════════════════════════════════════════ // DRAG & DROP // ══════════════════════════════════════════════════════ @@ -716,17 +487,14 @@ function initSortable() { // CARD ACTIONS // ══════════════════════════════════════════════════════ function addCard(type) { - // Close any open add menus - document.querySelectorAll('.add-menu.open').forEach(m => m.classList.remove('open')); const id = type + '_' + Date.now(); const defaults = { temp: {label: 'New Temp', color: '#ff8844', sp_default: 70}, motor: {label: 'New Motor', sp_default: 50}, output: {label: 'New Output'}, - burner: {label: 'Burner'}, - automation: {label: 'New Automation', width: 'full', rules: []} + burner: {label: 'Burner'} }; - const card = {id, type, width: defaults[type]?.width || 'half', ...defaults[type]}; + const card = {id, type, width: 'half', ...defaults[type]}; layout.pages[currentPage].cards.push(card); simState[id] = {on: false, value: card.sp_default || 0}; renderCards(); @@ -740,97 +508,12 @@ function deleteCard(id) { renderCards(); } -function editCardProps(id) { - const card = findCard(id); - if (!card) return; - - const p = document.getElementById('popup'); - p.style.borderColor = 'transparent'; - - let typeOpts = ['temp','motor','output','burner','automation'].map(t => - `` - ).join(''); - - let extraFields = ''; - if (card.type === 'temp') { - extraFields = ` -
°C
`; - } else if (card.type === 'motor') { - extraFields = ` -
%
`; - } - - p.innerHTML = ` -
-

Edit Card

-
- - -
-
- - -
-
- - -
-
- - - - -
-
- - - ${card.fontSize||18}px -
- ${extraFields} -
- - -
-
`; - - // Live preview for color picker - document.getElementById('prop-color').addEventListener('input', e => { - document.getElementById('prop-color-preview').style.background = e.target.value; - }); - document.getElementById('prop-fontsize').addEventListener('input', e => { - document.getElementById('prop-fontsize-val').textContent = e.target.value + 'px'; - }); - - document.getElementById('popup-overlay').classList.add('active'); -} - -function saveCardProps(id) { - const card = findCard(id); - if (!card) return; - - card.label = document.getElementById('prop-name').value || card.label; - card.type = document.getElementById('prop-type').value; - card.width = document.getElementById('prop-width').value; - const color = document.getElementById('prop-color').value; - card.color = color || null; - card.fontSize = parseInt(document.getElementById('prop-fontsize').value) || 18; - - const spEl = document.getElementById('prop-sp'); - if (spEl) card.sp_default = parseInt(spEl.value) || 0; - - closePopup(); - renderCards(); -} - -function toggleCardSize(id) { +function renameCard(id) { const page = layout.pages[currentPage]; const card = page.cards.find(c => c.id === id); if (!card) return; - card.width = card.width === 'full' ? 'half' : 'full'; - renderCards(); + const name = prompt('New name:', card.label); + if (name) { card.label = name; renderCards(); } } // ══════════════════════════════════════════════════════ @@ -839,34 +522,6 @@ function toggleCardSize(id) { function toggleSim(id) { if (!simState[id]) simState[id] = {on: false, value: 0}; simState[id].on = !simState[id].on; - - // Burner turns on Hot Fan automatically - const card = findCard(id); - if (card && card.type === 'burner') { - const fanCard = findCardByLabel('Hot Fan'); - if (fanCard) { - if (!simState[fanCard.id]) simState[fanCard.id] = {on: false, value: fanCard.sp_default || 50}; - simState[fanCard.id].on = simState[id].on; - } - } - - // If this is an automation card, activate/deactivate all its rules - if (card && card.type === 'automation' && card.rules) { - card.rules.forEach(r => { - if (!r.target) return; - if (!simState[r.target]) simState[r.target] = {on: false, value: 0}; - if (simState[id].on) { - // Activating: apply all rules - if (r.action === 'on') simState[r.target].on = true; - else if (r.action === 'off') simState[r.target].on = false; - else if (r.action === 'set') { simState[r.target].on = true; simState[r.target].value = r.value || 0; } - } else { - // Deactivating: turn everything off - if (r.action !== 'off') simState[r.target].on = false; - } - }); - } - renderCards(); updateSchematicAnimations(); } @@ -903,10 +558,6 @@ function findCard(id) { for (const p of layout.pages) { const c = p.cards.find(x => x.id === id); if (c) return c; } return null; } -function findCardByLabel(label) { - for (const p of layout.pages) { const c = p.cards.find(x => x.label === label); if (c) return c; } - return null; -} // ══════════════════════════════════════════════════════ // COMMENTS @@ -1021,10 +672,7 @@ function hideCtx() { document.getElementById('eq-context').style.display = 'none'; ctxEquip = null; } -document.addEventListener('click', e => { - if (!e.target.closest('#eq-context')) hideCtx(); - if (!e.target.closest('.add-placeholder')) document.querySelectorAll('.add-menu.open').forEach(m => m.classList.remove('open')); -}); +document.addEventListener('click', e => { if (!e.target.closest('#eq-context')) hideCtx(); }); function ctxAction(action) { if (!ctxEquip) return; @@ -1201,25 +849,8 @@ function renderSchematic() { } const cc = col||'var(--green)'; if (eq.anim === 'burner') { - // Flame SVG with flicker animation - const fg = mkSvgEl('g', {'class':'anim-overlay', id:'sim-'+eq.id}); - fg.setAttribute('transform', `translate(${eq.x},${eq.y})`); - // Outer flame (orange) - fg.appendChild(mkSvgEl('path', { - d:`M 0 ${-r*0.3} Q ${-r*0.5} ${-r*0.8} ${-r*0.2} ${-r*1.2} Q 0 ${-r*1.5} ${r*0.2} ${-r*1.2} Q ${r*0.5} ${-r*0.8} 0 ${-r*0.3} Z`, - fill:'var(--amber)', opacity:'0.8', 'class':'burner-glow' - })); - // Inner flame (red-orange) - fg.appendChild(mkSvgEl('path', { - d:`M 0 ${-r*0.2} Q ${-r*0.25} ${-r*0.5} ${-r*0.1} ${-r*0.85} Q 0 ${-r*1.05} ${r*0.1} ${-r*0.85} Q ${r*0.25} ${-r*0.5} 0 ${-r*0.2} Z`, - fill:'var(--red)', opacity:'0.9', 'class':'burner-glow', style:'animation-delay:0.2s' - })); - // Core (bright) - fg.appendChild(mkSvgEl('ellipse', { - cx:0, cy: -r*0.4, rx: r*0.12, ry: r*0.25, - fill:'#ffdd44', opacity:'0.9', 'class':'burner-glow', style:'animation-delay:0.4s' - })); - g.appendChild(fg); + g.appendChild(mkSvgEl('circle', {'class':'anim-overlay burner-glow', id:'sim-'+eq.id, + cx:eq.x, cy:eq.y, r:r+4, fill:'none', stroke:col||'var(--amber)', 'stroke-width':'4'})); } else if (eq.anim === 'fan') { // Fan: 4 curved blades spinning inside the circle const ag = mkSvgEl('g', {'class':'anim-overlay', id:'sim-'+eq.id});