// Design tokens + mock data for BControl v0.5 web // Visual DNA preserved from the desktop GUI reference. const BC = { brand: '#2290CE', brandDark: '#1A7AB0', brandDeep: '#0F5A87', brandTint: '#EAF4FB', bg: '#F3F3F3', bgAlt: '#FAFAFA', surface: '#FFFFFF', surfaceAlt: '#F9FAFB', border: '#E5E7EB', borderStrong: '#D1D5DB', divider: '#EEF0F3', ink: '#1F2328', inkMuted: '#4B5563', inkSubtle: '#6B7280', inkFaint: '#9CA3AF', fire: { fg: '#B71C1C', bg: '#FEE4E4', bar: '#DC2626', dot: '#DC2626' }, alarm: { fg: '#E65100', bg: '#FFEFD6', bar: '#EA580C', dot: '#EA580C' }, trouble: { fg: '#A16207', bg: '#FEF7C8', bar: '#CA8A04', dot: '#CA8A04' }, supervisory: { fg: '#01579B', bg: '#DFF1FD', bar: '#0284C7', dot: '#0284C7' }, state: { fg: '#6A1B9A', bg: '#F1E4F7', bar: '#9333EA', dot: '#9333EA' }, ok: { fg: '#166534', bg: '#DCFCE7', bar: '#16A34A', dot: '#22C55E' }, pending: { fg: '#92400E', bg: '#FEF3C7', bar: '#D97706', dot: '#F59E0B' }, font: '"Segoe UI", "Segoe UI Variable", -apple-system, BlinkMacSystemFont, system-ui, sans-serif', mono: '"Cascadia Mono", "Cascadia Code", "SF Mono", Consolas, ui-monospace, monospace', radius: 6, radiusLg: 8, shadow: '0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.03)', shadowMd: '0 4px 14px rgba(15,23,42,.08), 0 1px 2px rgba(15,23,42,.05)', shadowLg: '0 12px 32px rgba(15,23,42,.12), 0 2px 6px rgba(15,23,42,.06)', }; const APP_TITLE = 'Monitoreo Multimarca'; const APP_VERSION = 'v0.5'; // Inject keyframes + scrollbar + focus styles const bcStyleEl = document.createElement('style'); bcStyleEl.textContent = ` html,body{margin:0;padding:0;height:100%;background:${BC.bg};font-family:${BC.font};color:${BC.ink};} *{box-sizing:border-box;} @keyframes bc-pulse-fire { 0%,100%{background:${BC.fire.bar};box-shadow:0 0 0 0 rgba(220,38,38,.55),inset 0 0 0 0 rgba(255,255,255,0);} 50%{background:#ff5b5b;box-shadow:3px 0 8px 1px rgba(220,38,38,.55),inset 0 0 0 0 rgba(255,255,255,.2);} } @keyframes bc-pulse-ring{0%,100%{box-shadow:0 0 0 0 rgba(220,38,38,.55);}50%{box-shadow:0 0 0 8px rgba(220,38,38,0);}} @keyframes bc-live-dot{0%,100%{box-shadow:0 0 0 0 rgba(134,239,172,.55);}50%{box-shadow:0 0 0 6px rgba(134,239,172,0);}} @keyframes bc-fade-in{0%{opacity:0;transform:translateY(-4px);}100%{opacity:1;transform:translateY(0);}} @keyframes bc-spin{to{transform:rotate(360deg);}} @keyframes bc-badge-pop{0%{transform:scale(.6);}60%{transform:scale(1.15);}100%{transform:scale(1);}} @keyframes bc-slide-up{0%{opacity:0;transform:translateY(8px);}100%{opacity:1;transform:translateY(0);}} .bc-row-enter{animation:bc-fade-in .45s ease-out var(--bc-row-delay,0ms) both;} .bc-slide-up{animation:bc-slide-up .25s ease-out both;} .bc-scroll::-webkit-scrollbar{width:10px;height:10px;} .bc-scroll::-webkit-scrollbar-thumb{background:#d7d9de;border-radius:10px;border:2px solid transparent;background-clip:padding-box;} .bc-scroll::-webkit-scrollbar-thumb:hover{background:#b6b9c0;background-clip:padding-box;border:2px solid transparent;} .bc-scroll::-webkit-scrollbar-track{background:transparent;} .bc-btn{cursor:pointer;user-select:none;transition:background .12s,border-color .12s,transform .08s,color .12s;font-family:inherit;} .bc-btn:active{transform:translateY(.5px);} .bc-btn:disabled{cursor:not-allowed;opacity:.45;} .bc-iconbtn:hover:not(:disabled){background:rgba(255,255,255,.14)!important;} .bc-iconbtn-dark:hover:not(:disabled){background:${BC.bgAlt}!important;} .bc-cmd-btn:hover:not(:disabled){border-color:${BC.brand}!important;background:${BC.brandTint}!important;color:${BC.brand}!important;} .bc-danger-btn:hover:not(:disabled){background:${BC.fire.bg}!important;border-color:${BC.fire.bar}!important;color:${BC.fire.fg}!important;} .bc-nav-item:hover{background:${BC.bgAlt};} .bc-input{outline:none;box-sizing:border-box;font-family:inherit;} .bc-input:focus{border-color:${BC.brand}!important;box-shadow:0 0 0 3px rgba(34,144,206,.12);} .bc-panel-row:hover{background:${BC.bgAlt};} .bc-panel-row.active{background:${BC.brandTint};} .bc-tab:hover{color:${BC.brand};} .bc-tab.active{color:${BC.brand};border-bottom-color:${BC.brand};} .bc-tbl th{text-align:left;font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:${BC.inkSubtle};padding:8px 10px;border-bottom:1px solid ${BC.border};font-weight:600;background:${BC.surfaceAlt};} .bc-tbl td{padding:9px 10px;border-bottom:1px solid ${BC.divider};font-size:13px;color:${BC.ink};vertical-align:middle;} .bc-tbl tr:last-child td{border-bottom:none;} .bc-tbl tr:hover td{background:${BC.bgAlt};} /* Header responsive collapses */ @media (max-width: 1180px){ .bc-userchip-name{display:none;} } /* Mobile utility classes (hidden on desktop) */ .bc-hamburger{display:none;} .bc-sidebar-overlay{display:none;} /* Mobile layout — 767px breakpoint */ @media (max-width: 767px){ .bc-hamburger{display:flex;align-items:center;justify-content:center;width:44px;height:44px;border:none;background:transparent;color:#fff;flex-shrink:0;padding:0;} .bc-tab-label{display:none;} .bc-livepill-text{display:none;} .bc-monitor-wrap{position:relative;} .bc-sidebar{position:fixed!important;top:56px;left:0;bottom:28px;width:280px!important;flex:none!important;transform:translateX(-100%);transition:transform .22s cubic-bezier(.4,0,.2,1);z-index:100;box-shadow:4px 0 16px rgba(15,23,42,.18);} .bc-sidebar.is-open{transform:translateX(0);} .bc-sidebar-overlay.is-open{display:block;position:fixed;inset:0;background:rgba(15,23,42,.4);z-index:99;} .bc-monitor-main{width:100%!important;min-width:0!important;} .bc-events-header-row,.bc-events-data-row{min-width:0;} .bc-evt-tipo{width:64px!important;flex:0 0 64px!important;} .bc-evt-ts{width:76px!important;flex:0 0 76px!important;font-size:10px!important;} .bc-monitor-main{overflow:hidden;min-height:0;} .bc-events-outer{flex:0 1 auto!important;max-height:calc(100svh - 340px);overflow:hidden;} .bc-cmd-btn,.bc-danger-btn{min-height:44px!important;padding:10px 14px!important;} .bc-statusbar-extra{display:none;} .bc-panel-header-inner{flex-wrap:wrap;gap:8px!important;align-items:flex-start!important;} .bc-panel-meta{display:none!important;} .bc-panel-search{width:100%!important;box-sizing:border-box;border-radius:8px!important;} .bc-panel-search input{width:100%!important;flex:1;} .bc-header-logo{display:none!important;} .bc-livepill{display:none!important;} .bc-admin-table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;} .bc-admin-table-wrap table{min-width:560px;} } `; document.head.appendChild(bcStyleEl); // ───────────── helpers ───────────── function classifyCategory(cls) { const u = (cls || '').toUpperCase(); if (u.includes('FIRE') || u.includes('ALARM')) return 'alarm'; if (u.includes('TROUBLE')) return 'trouble'; if (u.includes('SUPERVISORY')) return 'supervisory'; if (u.includes('STATE')) return 'state'; return null; } function classifyColor(cls) { const cat = classifyCategory(cls); if (cat === 'alarm') return BC.fire; if (cat === 'trouble') return BC.trouble; if (cat === 'supervisory') return BC.supervisory; if (cat === 'state') return BC.state; return { fg: BC.inkMuted, bg: BC.surface, bar: BC.borderStrong, dot: BC.inkFaint }; } function classifyLabel(cls) { const u = (cls || '').toUpperCase(); if (u.includes('FIRE') || u.includes('ALARM')) return 'ALARMA'; if (u.includes('TROUBLE')) return 'FALLA'; if (u.includes('SUPERVISORY')) return 'SUPERVISION'; if (u.includes('STATE')) return 'ESTADO'; return u; } function formatTs(raw, opts = {}) { if (!raw) return '—'; try { const d = new Date(raw); if (isNaN(d)) return '—'; if (opts.full) return d.toLocaleString('es-CL', { hour12: false }); const isToday = d.toDateString() === new Date().toDateString(); if (!isToday) { return d.toLocaleDateString('es-CL', { day: '2-digit', month: '2-digit' }) + ' ' + d.toLocaleTimeString('es-CL', { hour: '2-digit', minute: '2-digit' }); } return d.toLocaleTimeString('es-CL', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch (_) { return '—'; } } function relTime(raw) { if (!raw) return '—'; const d = new Date(raw); if (isNaN(d)) return '—'; const s = Math.floor((Date.now() - d.getTime()) / 1000); if (s < 5) return 'ahora mismo'; if (s < 60) return 'hace ' + s + 's'; if (s < 3600) return 'hace ' + Math.floor(s / 60) + 'm'; if (s < 86400) return 'hace ' + Math.floor(s / 3600) + 'h'; return 'hace ' + Math.floor(s / 86400) + 'd'; } // ───────────── Mock data (REST shapes match backend) ───────────── const USERS = { admin: { id: 1, username: 'admin', role: 'admin', company: 'Carlos Incendio' }, operator: { id: 2, username: 'operator', role: 'operator', company: 'Carlos Incendio' }, viewer: { id: 3, username: 'viewer', role: 'viewer', company: 'Carlos Incendio' }, }; const MOCK_CLIENTS = [ { client_id: 'PNL-001', description: 'Edificio Central - P1', ip: '192.168.10.21', port: 4300, status: 'connected', alive_status: 'connected', last_seen: Date.now() - 2*1000, connected_at: Date.now() - 3600*1000*12, company: 'Carlos Incendio' }, { client_id: 'PNL-002', description: 'Bodega Norte', ip: '192.168.10.22', port: 4300, status: 'connected', alive_status: 'connected', last_seen: Date.now() - 4*1000, connected_at: Date.now() - 3600*1000*48, company: 'Carlos Incendio' }, { client_id: 'PNL-003', description: 'Torre Costanera - Piso 12', ip: '192.168.10.23', port: 4300, status: 'connected', alive_status: 'pending', last_seen: Date.now() - 38*1000, connected_at: Date.now() - 3600*1000*6, company: 'Carlos Incendio' }, { client_id: 'PNL-004', description: 'Planta Industrial Maipú', ip: '192.168.10.24', port: 4300, status: 'disconnected', alive_status: 'disconnected', last_seen: Date.now() - 600*1000, connected_at: Date.now() - 3600*1000*24, company: 'Carlos Incendio' }, ]; const EVENT_TEMPLATES = [ { cls: 'FIRE', desc: 'ZONA 02 · DETECTOR ÓPTICO · SALA SERVIDORES' }, { cls: 'FIRE', desc: 'ZONA 05 · PULSADOR MANUAL ACTIVADO' }, { cls: 'ALARM', desc: 'ZONA 11 · ALARMA GENERAL' }, { cls: 'TROUBLE', desc: 'BATERÍA BAJA - PANEL PRINCIPAL' }, { cls: 'TROUBLE', desc: 'COMUNICACIÓN LOOP 3 - FALLA' }, { cls: 'SUPERVISORY', desc: 'BOMBA DE INCENDIO - FLUJO DETECTADO' }, { cls: 'SUPERVISORY', desc: 'VÁLVULA CERRADA - RED HÚMEDA' }, { cls: 'STATE', desc: 'SISTEMA ARMADO' }, { cls: 'STATE', desc: 'RESET REMOTO EJECUTADO' }, { cls: 'STATE', desc: 'MODO MANTENIMIENTO DESACTIVADO' }, ]; function seedEvents(clientId, panel, count) { const out = []; for (let i = 0; i < count; i++) { const t = EVENT_TEMPLATES[(i * 3 + clientId.length) % EVENT_TEMPLATES.length]; out.push({ id: `${clientId}-${i}-${Math.random().toString(36).slice(2,8)}`, client_id: clientId, description: panel, message: t.desc, cls: t.cls, timestamp: new Date(Date.now() - i * 60000 - Math.random() * 30000).toISOString(), direction: 'incoming', }); } return out; } // global mutable store that simulates DB const MOCK_STATE = { events: [ ...seedEvents('PNL-001', 'Edificio Central - P1', 14), ...seedEvents('PNL-002', 'Bodega Norte', 9), ...seedEvents('PNL-003', 'Torre Costanera - Piso 12', 6), ...seedEvents('PNL-004', 'Planta Industrial Maipú', 4), ].sort((a,b) => (b.timestamp||'').localeCompare(a.timestamp||'')), allowed_clients: MOCK_CLIENTS.map(c => ({ client_id: c.client_id, description: c.description, created_at: new Date(Date.now() - 30*86400000).toISOString(), })), ignored_patterns: [ { id: 1, pattern_type: 'regex', pattern: '^KEEPALIVE.*', description: 'Suprimir keepalives', active: true }, { id: 2, pattern_type: 'contains', pattern: 'HEARTBEAT', description: 'Ruido de protocolo', active: true }, { id: 3, pattern_type: 'regex', pattern: '^POLL\\s+\\d+$', description: 'Poll interno', active: false }, ], audit: [ { id: 101, action: 'LOGIN_SUCCESS', user: 'admin', client_id: null, message: null, success: true, at: new Date(Date.now() - 600*1000).toISOString() }, { id: 102, action: 'COMMAND_SENT', user: 'operator', client_id: 'PNL-001', message: 'Silenciar', success: true, at: new Date(Date.now() - 540*1000).toISOString() }, { id: 103, action: 'DENIED_RESET', user: 'operator', client_id: 'PNL-002', message: '~N', success: false, at: new Date(Date.now() - 420*1000).toISOString() }, { id: 104, action: 'ACK', user: 'operator', client_id: 'PNL-001', message: '~L', success: true, at: new Date(Date.now() - 380*1000).toISOString() }, { id: 105, action: 'COMMAND_SENT', user: 'admin', client_id: 'PNL-003', message: 'Reset', success: true, at: new Date(Date.now() - 200*1000).toISOString() }, ], commands: { // simulates /clients/{id}/commands response 'PNL-001': [ { id: 1, name: 'Silenciar', description: 'Silenciar sirenas', admin_only: false }, { id: 2, name: 'Reconocer (ACK)', description: 'Reconocer evento', admin_only: false }, { id: 3, name: 'Reset', description: 'Reset del panel', admin_only: true }, { id: 4, name: 'Test Lámparas', description: 'Prueba de indicadores', admin_only: false }, ], 'PNL-002': [ { id: 1, name: 'Silenciar', description: 'Silenciar sirenas', admin_only: false }, { id: 2, name: 'Reconocer (ACK)', description: 'Reconocer evento', admin_only: false }, { id: 3, name: 'Reset', description: 'Reset del panel', admin_only: true }, ], 'PNL-003': [ { id: 1, name: 'Silenciar', description: 'Silenciar sirenas', admin_only: false }, { id: 2, name: 'Reconocer (ACK)', description: 'Reconocer evento', admin_only: false }, { id: 3, name: 'Reset', description: 'Reset del panel', admin_only: true }, { id: 5, name: 'Evacuación', description: 'Evacuación general', admin_only: true }, ], 'PNL-004': [], }, }; Object.assign(window, { BC, APP_TITLE, APP_VERSION, USERS, MOCK_CLIENTS, MOCK_STATE, classifyCategory, classifyColor, formatTs, relTime });