# Nanometrics Dashboard — Plan d'implémentation > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Créer le dashboard web Nanometrics en HTML/CSS/JS vanilla, servi par Nginx, avec grille d'agents temps réel via WebSocket, popups détail/config/SMART, et design system Gruvbox seventies. **Architecture:** Single Page Application sans build step. WebSocket vers le serveur Go pour les mises à jour temps réel. Appels REST pour la config et l'historique. Design system chargé depuis `design_system/tokens/tokens.css`. Assets (polices, Font Awesome) versionnés localement. **Tech Stack:** HTML5, CSS3, vanilla JS (ES2022), Font Awesome 6 (local), polices locales (Inter, JetBrains Mono, Share Tech Mono) --- ## Structure des fichiers ``` dashboard/ ├── index.html — point d'entrée unique ├── js/ │ ├── app.js — init, WebSocket, boucle principale │ ├── grid.js — rendu grille + tuiles │ ├── popups.js — popups détail, config agent, SMART, config serveur │ ├── api.js — appels REST (fetch) │ └── charts.js — courbes SVG sparklines ├── css/ │ └── app.css — styles app (import tokens.css) ├── fonts/ — Inter, JetBrains Mono, Share Tech Mono (woff2) └── vendor/ └── fontawesome/ ├── css/all.min.css └── webfonts/ ``` --- ### Task 1 : Structure HTML + design system **Files:** - Create: `dashboard/index.html` - Create: `dashboard/css/app.css` - [ ] **Télécharger Font Awesome 6 en local** ```bash mkdir -p dashboard/vendor/fontawesome cd dashboard/vendor/fontawesome curl -L https://use.fontawesome.com/releases/v6.5.1/fontawesome-free-6.5.1-web.zip -o fa.zip unzip fa.zip cp -r fontawesome-free-6.5.1-web/css . cp -r fontawesome-free-6.5.1-web/webfonts . rm -rf fontawesome-free-6.5.1-web fa.zip ``` - [ ] **Télécharger les polices Google localement** ```bash mkdir -p dashboard/fonts # Inter variable curl -o dashboard/fonts/inter.woff2 \ "https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2" # JetBrains Mono curl -o dashboard/fonts/jetbrains-mono.woff2 \ "https://fonts.gstatic.com/s/jetbrainsmono/v18/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxjOVmNeaAh.woff2" # Share Tech Mono curl -o dashboard/fonts/share-tech-mono.woff2 \ "https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFc7pAsEIc3Xew.woff2" ``` - [ ] **Créer `dashboard/css/app.css`** ```css /* Polices locales */ @font-face { font-family: 'Inter'; src: url('../fonts/inter.woff2') format('woff2'); font-weight: 100 900; font-display: swap; } @font-face { font-family: 'JetBrains Mono'; src: url('../fonts/jetbrains-mono.woff2') format('woff2'); font-weight: 400 700; font-display: swap; } @font-face { font-family: 'Share Tech Mono'; src: url('../fonts/share-tech-mono.woff2') format('woff2'); font-weight: 400; font-display: swap; } /* Tokens design system — copié depuis design_system/tokens/tokens.css */ /* À remplacer par @import '../design_system/tokens/tokens.css' en prod */ :root[data-theme="dark"] { --accent:#fe8019;--accent-soft:#d65d0e;--accent-glow:rgba(254,128,25,.28);--accent-tint:rgba(254,128,25,.1); --bg-0:#1d1813;--bg-1:#2a231d;--bg-2:#32291f;--bg-3:#3c332a;--bg-4:#4a3f33;--bg-5:#57493c; --ink-1:#f2e5c7;--ink-2:#d5c4a1;--ink-3:#a89984;--ink-4:#7c6f64; --ok:#4dbb26;--warn:#fabd2f;--err:#fb4934;--blue:#3db0d1;--purple:#c882c8; --border-1:rgba(255,255,255,.06);--border-2:rgba(255,255,255,.12);--border-3:rgba(255,255,255,.26); --tile-3d:0 1px 0 rgba(255,255,255,.08) inset,0 -1px 0 rgba(0,0,0,.3) inset,0 6px 20px rgba(0,0,0,.5); --tile-press:inset 0 2px 8px rgba(0,0,0,.5),inset 0 1px 3px rgba(0,0,0,.4); --hover-glow:0 0 0 1px var(--accent-soft),0 0 24px var(--accent-glow),0 6px 20px rgba(0,0,0,.5); --font-ui:'Inter',system-ui,sans-serif; --font-mono:'JetBrains Mono',monospace; --font-terminal:'Share Tech Mono',monospace; } :root[data-theme="light"] { --accent:#af3a03;--accent-soft:#d65d0e;--accent-glow:rgba(175,58,3,.18);--accent-tint:rgba(175,58,3,.08); --bg-0:#d5c4a1;--bg-1:#ebdbb2;--bg-2:#d5c4a1;--bg-3:#bdae93;--bg-4:#a89984;--bg-5:#928374; --ink-1:#3c3836;--ink-2:#504945;--ink-3:#665c54;--ink-4:#7c6f64; --ok:#3c911c;--warn:#b57614;--err:#9d0006;--blue:#2d82a3;--purple:#8c468c; --border-1:rgba(0,0,0,.08);--border-2:rgba(0,0,0,.15);--border-3:rgba(0,0,0,.3); --tile-3d:0 1px 0 rgba(255,255,255,.55) inset,0 -1px 0 rgba(0,0,0,.08) inset,0 4px 14px rgba(0,0,0,.13); --tile-press:inset 0 2px 6px rgba(0,0,0,.2); --hover-glow:0 0 0 1px var(--accent-soft),0 0 18px var(--accent-glow),0 4px 14px rgba(0,0,0,.13); --font-ui:'Inter',system-ui,sans-serif; --font-mono:'JetBrains Mono',monospace; --font-terminal:'Share Tech Mono',monospace; } *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} body{background:var(--bg-1);color:var(--ink-1);font-family:var(--font-ui);font-size:13px; height:100vh;display:flex;flex-direction:column;overflow:hidden;transition:background .2s,color .2s} /* TOOLTIP global position:fixed */ #tooltip{position:fixed;z-index:9999;pointer-events:none;background:var(--bg-0);color:var(--ink-1); border:1px solid var(--border-3);border-radius:5px;padding:4px 9px;font-size:11px; font-family:var(--font-ui);white-space:nowrap;opacity:0;transition:opacity .12s; box-shadow:0 4px 12px rgba(0,0,0,.4)} #tooltip.show{opacity:1} /* HEADER */ .header{background:var(--bg-2);border-bottom:1px solid var(--border-2);padding:0 20px; height:48px;display:flex;align-items:center;gap:12px;flex-shrink:0} .logo{display:flex;align-items:center;gap:8px} .logo-led{width:9px;height:9px;border-radius:50%;background:var(--accent); box-shadow:0 0 8px var(--accent-glow);animation:blink 2s infinite} @keyframes blink{0%,100%{opacity:1}50%{opacity:.4}} .logo-name{font-weight:700;font-size:14px;letter-spacing:.05em;font-family:var(--font-terminal)} .logo-ver{font-size:10px;color:var(--ink-4);font-family:var(--font-terminal)} .h-sep{width:1px;height:24px;background:var(--border-2)} .h-spacer{flex:1} .h-stats{display:flex;gap:14px} .h-stat{display:flex;align-items:center;gap:5px} .h-stat .lbl{font-size:9px;color:var(--ink-4);font-family:var(--font-terminal);letter-spacing:.06em} .h-stat .val{font-family:var(--font-mono);font-weight:700;font-size:13px} .c-ok{color:var(--ok)}.c-warn{color:var(--warn)}.c-err{color:var(--err)}.c-n{color:var(--ink-2)} .hbtn{width:34px;height:34px;border-radius:8px;border:1px solid var(--border-2);background:var(--bg-3); color:var(--ink-2);font-size:14px;display:flex;align-items:center;justify-content:center; cursor:pointer;user-select:none;transition:background .12s,color .12s,transform .08s,box-shadow .08s} .hbtn:hover{background:var(--bg-4);color:var(--accent)} .hbtn:active{transform:translateY(1px) scale(.96);box-shadow:var(--tile-press)} .hbtn.active-btn{background:var(--accent);color:var(--bg-0);border-color:var(--accent-soft)} /* GRILLE */ .main{flex:1;padding:14px 16px;overflow-y:auto} .agents-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--tile-min,220px),1fr));gap:10px} .tile{background:var(--bg-3);border-radius:10px;padding:12px 14px;border:1px solid var(--border-1); box-shadow:var(--tile-3d);cursor:pointer;display:flex;flex-direction:column;gap:9px; transition:box-shadow .15s,transform .08s,border-color .15s} .tile:hover{box-shadow:var(--hover-glow);border-color:var(--accent-soft)} .tile:active{transform:translateY(2px) scale(.99);box-shadow:var(--tile-press)} .tile.t-warn{border-color:rgba(250,189,47,.3)} .tile.t-warn:hover{border-color:var(--warn);box-shadow:0 0 0 1px var(--warn),0 0 22px rgba(250,189,47,.22),0 6px 20px rgba(0,0,0,.5)} .tile.t-err{border-color:rgba(251,73,52,.35)} .tile.t-err:hover{border-color:var(--err);box-shadow:0 0 0 1px var(--err),0 0 22px rgba(251,73,52,.25),0 6px 20px rgba(0,0,0,.5)} .tile.t-off{opacity:.5;cursor:default}.tile.t-off:hover,.tile.t-off:active{box-shadow:var(--tile-3d);border-color:var(--border-1);transform:none} .tile-head{display:flex;align-items:center;gap:8px;user-select:none} .t-icon{width:28px;height:28px;border-radius:7px;background:var(--bg-4);display:flex;align-items:center; justify-content:center;color:var(--accent);font-size:13px;flex-shrink:0;overflow:hidden} .t-icon img{width:100%;height:100%;object-fit:cover} .t-names{flex:1;min-width:0} .t-host{font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .t-ip{font-family:var(--font-mono);font-size:10px;color:var(--ink-4)} .t-led{width:8px;height:8px;border-radius:50%;flex-shrink:0} .s-ok{background:var(--ok);box-shadow:0 0 6px var(--ok)} .s-warn{background:var(--warn);box-shadow:0 0 6px var(--warn);animation:blink 1.5s infinite} .s-err{background:var(--err);box-shadow:0 0 8px var(--err);animation:blink 1s infinite} .s-off{background:var(--ink-4)} .tile-gauges{display:flex;flex-direction:column;gap:5px} .g-row{display:flex;align-items:center;gap:7px} .g-ico{width:18px;height:18px;display:flex;align-items:center;justify-content:center; font-size:11px;color:var(--ink-3);flex-shrink:0;cursor:help} .g-bar{flex:1;height:5px;border-radius:3px;background:var(--bg-1);overflow:hidden} .g-fill{height:100%;border-radius:3px;background:var(--ok);transition:width .3s} .g-fill.w{background:var(--warn)}.g-fill.e{background:var(--err)} .g-val{font-family:var(--font-mono);font-size:11px;color:var(--ink-2);width:34px;text-align:right} .tile-foot{font-family:var(--font-terminal);font-size:10px;color:var(--ink-4); display:flex;align-items:center;gap:5px;user-select:none} /* FOOTER */ .footer{background:var(--bg-0);border-top:1px solid var(--border-2);height:26px; display:flex;align-items:center;font-family:var(--font-terminal);font-size:11px; color:var(--ink-4);flex-shrink:0} .f-mode{background:var(--accent);color:var(--bg-0);padding:0 12px;height:100%; display:flex;align-items:center;font-weight:700;letter-spacing:.04em;user-select:none} .f-cell{padding:0 12px;border-right:1px solid var(--border-1);display:flex;align-items:center;gap:5px;height:100%} .f-val{font-family:var(--font-mono);color:var(--ink-2)}.f-val.w{color:var(--warn)} .f-minibar{width:36px;height:4px;border-radius:2px;background:var(--bg-3);overflow:hidden} .f-minifill{height:100%;border-radius:2px;background:var(--ok)}.f-minifill.w{background:var(--warn)} .f-spacer{flex:1}.f-right{padding:0 12px;display:flex;align-items:center;gap:6px;color:var(--ink-3)} .f-time{font-family:var(--font-mono);color:var(--ink-2)} /* OVERLAY + POPUP */ .overlay{position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:100;display:flex; align-items:center;justify-content:center;backdrop-filter:blur(2px)} .popup{background:var(--bg-2);border:1px solid var(--border-3);border-radius:12px; box-shadow:0 24px 64px rgba(0,0,0,.7);display:flex;flex-direction:column;overflow:hidden} .pop-close{width:28px;height:28px;border-radius:6px;background:var(--bg-5);color:var(--ink-3); display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:12px; border:1px solid var(--border-1);user-select:none;transition:background .12s,color .12s,transform .08s} .pop-close:hover{background:var(--err);color:#fff} .pop-close:active{transform:translateY(1px) scale(.93)} .btn{padding:6px 14px;border-radius:8px;border:1px solid var(--border-2);background:var(--bg-4); color:var(--ink-2);font-size:12px;font-family:var(--font-ui);cursor:pointer; display:flex;align-items:center;gap:6px;user-select:none;transition:background .1s,transform .08s} .btn:hover{background:var(--bg-5)}.btn:active{transform:translateY(1px) scale(.97)} .btn.primary{background:var(--accent);color:var(--bg-0);border-color:var(--accent-soft);font-weight:600} .btn.primary:hover{background:var(--accent-soft)} /* Popup détail agent */ #popup-detail{width:560px;max-width:96vw;max-height:92vh;resize:both;overflow:hidden; min-width:400px;min-height:320px} #popup-detail .pop-body{overflow-y:auto;flex:1} .pop-head{background:var(--bg-3);padding:14px 18px;border-bottom:1px solid var(--border-2); display:flex;align-items:center;gap:12px;flex-shrink:0} .agent-icon-wrap{position:relative;width:44px;height:44px;border-radius:10px;flex-shrink:0; cursor:pointer;overflow:hidden;background:var(--bg-4);display:flex; align-items:center;justify-content:center;color:var(--accent);font-size:18px; border:2px solid var(--border-2);transition:border-color .15s} .agent-icon-wrap:hover{border-color:var(--accent)} .agent-icon-overlay{position:absolute;inset:0;background:rgba(0,0,0,.6);display:flex; flex-direction:column;align-items:center;justify-content:center; gap:2px;opacity:0;transition:opacity .15s;font-size:10px;color:#fff} .agent-icon-wrap:hover .agent-icon-overlay{opacity:1} .pop-host{font-weight:700;font-size:15px}.pop-ip{font-family:var(--font-mono);font-size:11px;color:var(--ink-4)} .pop-led{width:10px;height:10px;border-radius:50%;background:var(--ok);box-shadow:0 0 8px var(--ok);flex-shrink:0} .pop-body{padding:16px 18px;display:flex;flex-direction:column;gap:14px} .sec-title{font-size:9px;color:var(--ink-4);font-family:var(--font-terminal);letter-spacing:.08em;margin-bottom:8px} .kpi-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:7px} .kpi{background:var(--bg-3);border-radius:8px;padding:10px 12px;border:1px solid var(--border-1);box-shadow:var(--tile-3d)} .kpi-lbl{font-size:9px;color:var(--ink-4);font-family:var(--font-terminal);letter-spacing:.06em} .kpi-val{font-family:var(--font-mono);font-size:20px;font-weight:700;line-height:1.1;margin-top:2px} .kpi-val .u{font-size:10px;color:var(--ink-3);font-weight:400} .kpi-sub{font-size:10px;color:var(--ink-4);font-family:var(--font-mono);margin-top:2px} .charts-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px} .chart-card{background:var(--bg-3);border-radius:8px;padding:10px 12px;border:1px solid var(--border-1)} .chart-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px} .chart-label{display:flex;align-items:center;gap:6px;font-size:10px;font-family:var(--font-terminal);color:var(--ink-3)} .chart-cur{font-family:var(--font-mono);font-size:16px;font-weight:700} .chart-svg{width:100%;height:52px;display:block} .chart-axis{display:flex;justify-content:space-between;margin-top:2px;font-family:var(--font-terminal);font-size:9px;color:var(--ink-4)} .smart-btn{display:inline-flex;align-items:center;gap:8px;padding:7px 12px;border-radius:8px; border:1px solid var(--border-2);background:var(--bg-3);cursor:pointer; transition:background .12s,border-color .12s,transform .08s;font-family:var(--font-terminal);font-size:11px} .smart-btn:hover{background:var(--bg-4)}.smart-btn:active{transform:translateY(1px)} .smart-btn.ok{border-color:rgba(77,187,38,.3);color:var(--ok)} .smart-dot{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 5px var(--ok)} .meta-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px} .meta{background:var(--bg-3);border-radius:6px;padding:8px 10px;border:1px solid var(--border-1)} .meta-lbl{font-size:9px;color:var(--ink-4);font-family:var(--font-terminal);letter-spacing:.06em} .meta-val{font-family:var(--font-mono);font-size:12px;color:var(--ink-2);margin-top:2px} .proto-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 7px;border-radius:999px; font-size:10px;font-family:var(--font-terminal);font-weight:600} .proto-badge.udp{background:rgba(61,176,209,.15);color:var(--blue);border:1px solid rgba(61,176,209,.3)} .proto-badge.mqtt{background:rgba(200,130,200,.15);color:var(--purple);border:1px solid rgba(200,130,200,.3)} .pop-foot{padding:10px 18px;border-top:1px solid var(--border-2);background:var(--bg-3); display:flex;align-items:center;gap:8px;flex-shrink:0} .pop-uptime{font-family:var(--font-terminal);font-size:11px;color:var(--ink-4);flex:1} .btn-agent-cfg{width:34px;height:34px;border-radius:8px;border:1px solid var(--border-2); background:var(--bg-4);color:var(--ink-3);font-size:15px;display:flex; align-items:center;justify-content:center;cursor:pointer;user-select:none; transition:background .12s,color .12s,transform .08s} .btn-agent-cfg:hover{background:var(--bg-5);color:var(--accent)} /* Métriques tableau 3 colonnes */ .metrics-table{display:flex;flex-direction:column;border:1px solid var(--border-2);border-radius:8px;overflow:hidden} .metrics-header{display:grid;grid-template-columns:1fr 56px 56px;background:var(--bg-4); padding:8px 12px;gap:4px;align-items:center} .mh-label{font-size:9px;color:var(--ink-4);font-family:var(--font-terminal);letter-spacing:.06em} .mh-proto{font-size:9px;font-family:var(--font-terminal);font-weight:700;text-align:center; display:flex;flex-direction:column;align-items:center;gap:2px} .mh-proto.udp{color:var(--blue)}.mh-proto.mqtt{color:var(--purple)} .metric-row{display:grid;grid-template-columns:1fr 56px 56px;padding:8px 12px;gap:4px; align-items:center;background:var(--bg-3);border-top:1px solid var(--border-1);transition:background .1s} .metric-row:hover{background:var(--bg-4)} .metric-cell{display:flex;align-items:center;gap:8px} .metric-ico{font-size:13px;color:var(--ink-3);width:16px;text-align:center} .metric-name{font-size:12px;color:var(--ink-2);font-family:var(--font-terminal)} .metric-chk{display:flex;justify-content:center} .cbox{width:20px;height:20px;border-radius:5px;border:2px solid var(--border-2);background:var(--bg-1); display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:11px; color:transparent;transition:background .12s,border-color .12s,color .12s,transform .08s; user-select:none;flex-shrink:0} .cbox:hover{border-color:var(--border-3);transform:scale(1.08)} .cbox.udp-on{background:rgba(61,176,209,.18);border-color:var(--blue);color:var(--blue)} .cbox.mqtt-on{background:rgba(200,130,200,.18);border-color:var(--purple);color:var(--purple)} /* Config serveur */ .scfg-body{padding:18px;display:flex;flex-direction:column;gap:14px;overflow-y:auto} .scfg-sec-title{font-size:9px;color:var(--ink-4);font-family:var(--font-terminal); letter-spacing:.08em;padding-bottom:6px;border-bottom:1px solid var(--border-1)} .scfg-row{display:flex;align-items:center;gap:10px} .scfg-row>label{font-size:12px;color:var(--ink-3);width:110px;flex-shrink:0;font-family:var(--font-terminal)} .scfg-slider{flex:1;accent-color:var(--accent)} .scfg-val{font-family:var(--font-mono);font-size:12px;color:var(--ink-2);width:48px;text-align:right} .scfg-select{flex:1;background:var(--bg-3);border:1px solid var(--border-2);border-radius:6px; color:var(--ink-1);padding:6px 10px;font-size:12px} .scfg-toggle-row{display:flex;align-items:center;justify-content:space-between; padding:8px 10px;background:var(--bg-3);border-radius:7px;border:1px solid var(--border-1)} .toggle{position:relative;width:34px;height:18px;flex-shrink:0} .toggle input{opacity:0;width:0;height:0} .toggle-slider{position:absolute;inset:0;border-radius:9px;background:var(--bg-4); border:1px solid var(--border-2);cursor:pointer;transition:background .2s} .toggle-slider::before{content:'';position:absolute;width:12px;height:12px;border-radius:50%; background:var(--ink-4);top:2px;left:2px;transition:transform .2s,background .2s} .toggle input:checked+.toggle-slider{background:rgba(254,128,25,.3);border-color:var(--accent)} .toggle input:checked+.toggle-slider::before{transform:translateX(16px);background:var(--accent)} /* SMART */ .smart-verdict{display:flex;align-items:center;gap:14px;background:rgba(77,187,38,.1); border:1px solid rgba(77,187,38,.3);border-radius:10px;padding:14px 18px} .si-val{font-family:var(--font-mono);font-size:18px;font-weight:700} .si-val .u{font-size:11px;color:var(--ink-3);font-weight:400} .si-desc{font-size:11px;color:var(--ink-3);margin-top:4px;line-height:1.4} .attr-row{display:flex;align-items:center;gap:10px;padding:6px 10px; border-radius:6px;background:var(--bg-3);border:1px solid var(--border-1)} .attr-ok{color:var(--ok)} ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg-1)} ::-webkit-scrollbar-thumb{background:var(--bg-4);border-radius:3px} ``` - [ ] **Créer `dashboard/index.html`** ```html