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
-
+
+
+
+
+
+
@@ -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 += ``;
- 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 += '';
- }
- if (motors.length) {
- targetOpts += '';
- }
- if (outputs.length) {
- 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});