From c2fcd5c509a97de12d6d317a74129d80fb794e26 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:38:06 +0200 Subject: [PATCH] Integrate dashboard natively into Deck UI --- DASHBOARD.md | 8 +++--- dashboard-ui.js | 20 ++++++------- dashboard.css | 74 +++++++++++++++++++++++++++++++++++++++++-------- index.html | 2 +- 4 files changed, 78 insertions(+), 26 deletions(-) diff --git a/DASHBOARD.md b/DASHBOARD.md index 637fabc..1266a4a 100644 --- a/DASHBOARD.md +++ b/DASHBOARD.md @@ -5,13 +5,13 @@ Hardware-Seite entfällt. Die Übersicht für Endpunkt und GPU-Modus bleibt als separater Bereich erhalten; die alten Router-, Bild- und Modus-Umschalter werden im Dashboard nicht eingeblendet. -Die Karten und Ansichten ab „Aktives Profil“ übernehmen die Struktur des alten -Athena-Dashboards: CPU, RAM, beide GPUs mit VRAM/Temperatur/Takt/Leistung, +Das Dashboard ist Teil des normalen Deck-DOM und nutzt dessen Navigation, Typografie, +Karten und responsive Layoutregeln. Es übernimmt die Messwerte und Funktionen +ab „Aktives Profil“ aus dem alten Athena-Dashboard: CPU, RAM, beide GPUs mit VRAM/Temperatur/Takt/Leistung, Inferenzraten, Prompt-Cache, Anfragen, Slots, Kontextfenster, MTP, Tokenzähler, GPU- und Slot-Verlauf, Nutzung nach Profil, Modellwechsel, Profile, Dienste, Host/Netzwerk, GPU-Prozesse, llama.cpp-Parameter, -GGUF-Dateien und verschlüsselte portable Backups. Die Farbwerte sind an Deck -angepasst. Nicht verfügbare Laufzeitwerte erscheinen als „–“; sie werden nicht +GGUF-Dateien und verschlüsselte portable Backups. Die Anzeige ist im Deck-Design neu aufgebaut. Nicht verfügbare Laufzeitwerte erscheinen als „–“; sie werden nicht aus historischen Werten geschätzt. Profil und Modell beziehen sich auf Decks eigenen Worker, nicht auf den alten Router. diff --git a/dashboard-ui.js b/dashboard-ui.js index 5d5c8d5..945af9c 100644 --- a/dashboard-ui.js +++ b/dashboard-ui.js @@ -4,14 +4,14 @@ const esc=v=>String(v??'–').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>' const pct=n=>n==null?'–':`${Number(n).toFixed(1)}%`; const gib=b=>b==null?'–':`${(Number(b)/1073741824).toFixed(1)} GiB`; const dur=s=>{if(s==null)return'–';let d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60);return d?`${d}d ${h}h`:`${h}h ${m}m`}; -const $=id=>root.getElementById(id); +const $=id=>root.querySelector(`#${id}`); const imagePhaseLabel=p=>({'running':'Bildauftrag läuft','loading':'Bildmodell lädt','complete':'bereit','failed':'Fehler'})[p]||p||'inaktiv'; function cleanup(){for(const timer of timers)clearInterval(timer);timers=[];controller?.abort();controller=null;host=null;root=null} function gpuCard(g){let total=g.memory_total_mib||0,used=g.memory_used_mib||0,p=total&&g.memory_used_mib!=null?used/total*100:0,load=Math.max(0,Math.min(100,g.gpu_percent||0)),memoryText=total&&g.memory_used_mib!=null?`${(used/1024).toFixed(1)} / ${(total/1024).toFixed(1)} GiB`:'–';return `
GPU ${esc(g.index)}
${esc(g.name)}
${esc(g.pstate)}
${pct(g.gpu_percent)}GPU-Kern
${memoryText}VRAM
${esc(g.temperature_c)} °CTemperatur
${esc(g.power_w)} / ${esc(g.power_limit_w)} WLeistung
${esc(g.memory_controller_percent)} %Speichercontroller
${esc(g.graphics_clock_mhz)} MHzGPU-Takt
${esc(g.memory_clock_mhz)} MHzSpeichertakt
${esc(g.fan_percent)} %Lüfter
GPU-Auslastung${pct(load)}
VRAM${pct(p)}
`} function mount(){ controller=new AbortController(); -root=host.attachShadow({mode:'open'}); -root.innerHTML=`
Athena Deck · Live Telemetry

Dashboard

verbinde …
Aktives Profil
–
Deck wird abgefragt
+root=host; +root.innerHTML=`
ATHENA / TELEMETRIE

Dashboard

System, Inferenz und Verlauf auf einen Blick.

verbinde …
Aktives Profil
–
Deck wird abgefragt
Modell
–
–
CPU
–
–
System-RAM
–
–
@@ -45,8 +45,8 @@ root.innerHTML=`
Notfall-Backups · herunterladen
Verschlüsselte portable Sicherungen
Unersetzliche Daten ohne erneut ladbare Modellgewichte · maximal fünf Generationen
ErstelltGrößeSHA256
Backups werden geladen …
Zum Wiederherstellen wird der separat verwahrte Age-Schlüssel benötigt.
-
`; -root.querySelector('link').addEventListener('load',()=>{if(host?.isConnected)refreshHistory()},{once:true}); +`; + const deNum=n=>n==null?'–':Number(n).toLocaleString('de-DE',{maximumFractionDigits:1}); @@ -130,7 +130,7 @@ function drawHistory(points){ const canvas=$('gpuHistoryChart'),rect=canvas.getBoundingClientRect(),ratio=window.devicePixelRatio||1; canvas.width=Math.max(1,Math.floor(rect.width*ratio));canvas.height=Math.max(1,Math.floor(rect.height*ratio)); const x=canvas.getContext('2d');x.scale(ratio,ratio);const w=rect.width,h=rect.height,pad={l:42,r:44,t:16,b:28}; - x.clearRect(0,0,w,h);x.strokeStyle='#213044';x.fillStyle='#8fa1b5';x.font='11px system-ui';x.lineWidth=1; + x.clearRect(0,0,w,h);x.strokeStyle='#303a37';x.fillStyle='#97a79e';x.font='11px system-ui';x.lineWidth=1; for(let i=0;i<=4;i++){let y=pad.t+(h-pad.t-pad.b)*i/4;x.beginPath();x.moveTo(pad.l,y);x.lineTo(w-pad.r,y);x.stroke();x.fillText(`${100-i*25}%`,4,y+4);x.fillText(`${100-i*25}°`,w-pad.r+7,y+4)} if(!points.length){x.fillText('Noch keine historischen Messwerte',pad.l+10,h/2);return} const min=Math.min(...points.map(p=>p.ts)),max=Math.max(...points.map(p=>p.ts)); @@ -143,8 +143,8 @@ function drawHistory(points){ :span<=45*86400 ?date.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) :date.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'2-digit'}); - x.strokeStyle='#172538';x.beginPath();x.moveTo(xx,pad.t);x.lineTo(xx,h-pad.b);x.stroke(); - x.fillStyle='#8fa1b5';x.textAlign=i===0?'left':i===ticks-1?'right':'center';x.fillText(label,xx,h-7); + x.strokeStyle='#2b3532';x.beginPath();x.moveTo(xx,pad.t);x.lineTo(xx,h-pad.b);x.stroke(); + x.fillStyle='#97a79e';x.textAlign=i===0?'left':i===ticks-1?'right':'center';x.fillText(label,xx,h-7); } x.textAlign='start'; const ids=[...new Set(points.map(p=>p.gpu_index))].sort(); @@ -157,7 +157,7 @@ function drawSlotHistory(points){ const canvas=$('slotHistoryChart'),rect=canvas.getBoundingClientRect(),ratio=window.devicePixelRatio||1; canvas.width=Math.max(1,Math.floor(rect.width*ratio));canvas.height=Math.max(1,Math.floor(rect.height*ratio)); const x=canvas.getContext('2d');x.scale(ratio,ratio);const w=rect.width,h=rect.height,pad={l:42,r:18,t:16,b:28}; - x.clearRect(0,0,w,h);x.strokeStyle='#213044';x.fillStyle='#8fa1b5';x.font='11px system-ui';x.lineWidth=1; + x.clearRect(0,0,w,h);x.strokeStyle='#303a37';x.fillStyle='#97a79e';x.font='11px system-ui';x.lineWidth=1; for(let i=0;i<=4;i++){let y=pad.t+(h-pad.t-pad.b)*i/4;x.beginPath();x.moveTo(pad.l,y);x.lineTo(w-pad.r,y);x.stroke();x.fillText(`${100-i*25}%`,4,y+4)} if(!points.length){x.fillText('Noch keine historischen Slot-Messwerte',pad.l+10,h/2);$('slotLegend').innerHTML='';return} const min=Math.min(...points.map(p=>p.ts)),max=Math.max(...points.map(p=>p.ts)); @@ -166,7 +166,7 @@ function drawSlotHistory(points){ for(let i=0;ip.slot_id))].sort((a,b)=>a-b); $('slotLegend').innerHTML=ids.map((id,idx)=>{let color=historyColors[idx%historyColors.length],key=String(id);return ``}).join(''); diff --git a/dashboard.css b/dashboard.css index f436bf6..2304bfa 100644 --- a/dashboard.css +++ b/dashboard.css @@ -1,11 +1,63 @@ - -:host{color-scheme:dark;--bg:#101416;--panel:#181f21;--panel2:#202a25;--line:#303a37;--text:#e9edea;--muted:#97a79e;--cyan:#c5edaf;--green:#bdeba4;--amber:#ffc65c;--red:#ff8c8c} -*{box-sizing:border-box}.dashboard-root{margin:0;background:radial-gradient(circle at 15% -10%,#183049 0,transparent 35%),var(--bg);font:14px/1.45 Inter,ui-sans-serif,system-ui;color:var(--text)}main{max-width:1450px;margin:auto;padding:0}.top{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin-bottom:20px}.eyebrow{color:var(--cyan);font-weight:700;letter-spacing:.14em;text-transform:uppercase;font-size:11px}h1{margin:3px 0 0;font-size:30px}.live{display:flex;align-items:center;gap:8px;color:var(--muted)}.dot{width:9px;height:9px;border-radius:50%;background:var(--green);box-shadow:0 0 14px var(--green)}.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:14px}.card{background:linear-gradient(145deg,rgba(17,27,40,.96),rgba(10,16,24,.96));border:1px solid var(--line);border-radius:14px;padding:17px;min-width:0}.span3{grid-column:span 3}.span4{grid-column:span 4}.span6{grid-column:span 6}.span12{grid-column:span 12}.label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.08em}.value{font-size:26px;font-weight:750;margin-top:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sub{color:var(--muted);margin-top:4px}.bar-label{display:flex;justify-content:space-between;color:var(--muted);font-size:11px;margin-top:13px}.bar{height:9px;background:#070b11;border-radius:99px;overflow:hidden;margin-top:5px}.fill{height:100%;width:0;background:linear-gradient(90deg,var(--cyan),var(--green));transition:width .5s}.fill.gpu-load{background:linear-gradient(90deg,#8b7cff,var(--cyan))}.gpu-title{display:flex;justify-content:space-between;align-items:center;gap:12px}.badge{border:1px solid var(--line);background:#07101a;color:var(--cyan);padding:4px 8px;border-radius:99px;font-size:11px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:16px}.metric b{display:block;font-size:18px}.metric span{color:var(--muted);font-size:11px}.status{color:var(--green)}.status.bad{color:var(--red)}table{border-collapse:collapse;width:100%;margin-top:10px}th,td{text-align:left;padding:8px;border-bottom:1px solid var(--line)}th{color:var(--muted);font-weight:500}.error{color:var(--red);white-space:pre-wrap}.footer{color:var(--muted);font-size:12px;text-align:right;margin-top:14px}@media(max-width:900px){.span3,.span4,.span6{grid-column:span 12}.metrics{grid-template-columns:repeat(2,1fr)}.top{align-items:flex-start;flex-direction:column}} -.amber{color:var(--amber)}.section-title{grid-column:span 12;margin:10px 2px -3px;color:var(--cyan);font-size:12px;font-weight:750;letter-spacing:.13em;text-transform:uppercase}.slot-list{display:grid;gap:12px;margin-top:13px}.slot{border:1px solid var(--line);border-radius:11px;padding:13px;background:#141a1c}.slot-head,.row{display:flex;justify-content:space-between;align-items:center;gap:12px}.slot-head b{font-size:16px}.profiles{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin-top:13px}.profile-item{border:1px solid var(--line);border-radius:10px;padding:10px}.profile-item.active{border-color:var(--cyan);box-shadow:inset 0 0 0 1px #c5edaf33}.profile-item b{display:block}.profile-item span{color:var(--muted);font-size:11px}.scroll{max-height:330px;overflow:auto}.scroll th{position:sticky;top:0;background:var(--panel)}.event{padding:8px 0;border-bottom:1px solid var(--line)}.event:last-child{border:0}.event time{color:var(--muted);font-size:11px;margin-right:8px}@media(max-width:900px){.profiles{grid-template-columns:1fr}} -.span4 .metrics{grid-template-columns:repeat(2,1fr)} -.history-controls{display:flex;flex-wrap:wrap;gap:7px;margin:10px 0 14px}.history-controls button{border:1px solid var(--line);background:#141a1c;color:var(--muted);padding:6px 10px;border-radius:8px;cursor:pointer}.history-controls button.active{color:var(--cyan);border-color:var(--cyan)}.chart-legend{display:flex;flex-wrap:wrap;gap:8px;margin:2px 0 10px}.chart-legend button{border:1px solid var(--series);background:#141a1c;color:var(--text);padding:6px 10px;border-radius:8px;cursor:pointer}.chart-legend button::before{content:'';display:inline-block;width:10px;height:3px;background:var(--series);margin:0 7px 3px 0}.chart-legend button.off{opacity:.4;text-decoration:line-through}.chart{width:100%;height:250px;display:block}.token-total{font-size:24px;font-weight:750;margin-top:5px}.history-note{color:var(--muted);font-size:11px;margin-top:8px} -.usage-list{display:grid;gap:13px;margin-top:8px}.usage-head{display:flex;justify-content:space-between;gap:14px;align-items:baseline}.usage-head b{font-size:16px}.usage-head span{color:var(--muted)}.usage-meta{display:flex;justify-content:space-between;gap:12px;color:var(--muted);font-size:11px;margin-top:5px} -.mode-row{display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap}.mode-buttons,.mode-buttons span{display:flex;gap:9px;flex-wrap:wrap}.mode-buttons button,.mode-buttons a{border:1px solid var(--line);background:#141a1c;color:var(--text);padding:9px 14px;border-radius:9px;cursor:pointer;text-decoration:none;font:inherit}.mode-buttons button.active{border-color:var(--cyan);color:var(--cyan);box-shadow:inset 0 0 0 1px #c5edaf33}.mode-buttons button:disabled{opacity:.45;cursor:wait}.mode-buttons span[hidden]{display:none}.mode-buttons a.stable{border-color:#66e3a466;color:var(--green)}.mode-buttons a.experimental{border-color:#ffc65c66;color:var(--amber)}.mode-error{color:var(--red)} -.image-controls{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;margin-top:12px}.image-controls .mode-buttons{margin-top:8px}@media(max-width:900px){.image-controls{grid-template-columns:1fr}} -.download{display:inline-block;border:1px solid #66e3a466;color:var(--green);padding:6px 10px;border-radius:8px;text-decoration:none}.hash{font:11px ui-monospace,SFMono-Regular,monospace;color:var(--muted);word-break:break-all} - +/* Native Deck dashboard: one DOM, one typography and surface system. */ +#deck-dashboard-host{--db-line:#303a37;--db-muted:#97a79e;--db-accent:#c5edaf;--db-danger:#e9aaa0;min-width:0;color:#e9edea} +#deck-dashboard-host *{box-sizing:border-box} +#deck-dashboard-host .top{display:flex;align-items:end;justify-content:space-between;gap:22px;margin-bottom:28px} +#deck-dashboard-host .top h1{margin:12px 0 6px} +#deck-dashboard-host .top p{margin:0} +#deck-dashboard-host .live{display:flex;align-items:center;gap:9px;color:var(--db-muted);font-size:12px;white-space:nowrap} +#deck-dashboard-host .dot{width:8px;height:8px;border-radius:50%;background:#bdeba4;box-shadow:0 0 0 4px #bdeba41c} +#deck-dashboard-host .grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:18px;margin:0} +#deck-dashboard-host .card{background:#181f21;border:1px solid var(--db-line);border-radius:13px;padding:24px;min-width:0} +#deck-dashboard-host .span3{grid-column:span 3}#deck-dashboard-host .span4{grid-column:span 4}#deck-dashboard-host .span6{grid-column:span 6}#deck-dashboard-host .span12{grid-column:span 12} +#deck-dashboard-host .label{color:#95a69b;font-size:11px;letter-spacing:1px;text-transform:uppercase} +#deck-dashboard-host .value{font-size:27px;font-weight:550;letter-spacing:-.7px;margin:14px 0 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#e4f1de} +#deck-dashboard-host .sub{font-size:12px;line-height:1.55;color:var(--db-muted);margin-top:5px} +#deck-dashboard-host .section-title{grid-column:1/-1;color:#c5edaf;font-size:11px;letter-spacing:2px;text-transform:uppercase;margin:20px 0 -4px;padding-bottom:12px;border-bottom:1px solid var(--db-line)} +#deck-dashboard-host .bar-label{display:flex;justify-content:space-between;gap:10px;color:var(--db-muted);font-size:11px;margin-top:15px} +#deck-dashboard-host .bar{height:7px;background:#101718;border-radius:20px;overflow:hidden;margin-top:7px} +#deck-dashboard-host .fill{height:100%;width:0;background:#bdeba4;transition:width .45s} +#deck-dashboard-host .fill.gpu-load{background:#7fc8b6} +#deck-dashboard-host .gpu-title,#deck-dashboard-host .row,#deck-dashboard-host .slot-head{display:flex;align-items:center;justify-content:space-between;gap:16px} +#deck-dashboard-host .gpu-title .value{font-size:19px;white-space:normal;line-height:1.35} +#deck-dashboard-host .badge{border:1px solid #3a493f;border-radius:20px;padding:5px 9px;color:#c5edaf;font-size:11px;white-space:nowrap} +#deck-dashboard-host .metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:20px} +#deck-dashboard-host .metric{min-width:0} +#deck-dashboard-host .metric b{display:block;font-size:17px;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +#deck-dashboard-host .metric span{display:block;color:var(--db-muted);font-size:11px;line-height:1.45;margin-top:4px} +#deck-dashboard-host .span4 .metrics{grid-template-columns:repeat(2,minmax(0,1fr))} +#deck-dashboard-host .status{color:#bdeba4}#deck-dashboard-host .status.bad,#deck-dashboard-host .error{color:var(--db-danger)} +#deck-dashboard-host table{border-collapse:collapse;width:100%;margin-top:15px;font-size:12px} +#deck-dashboard-host th,#deck-dashboard-host td{text-align:left;padding:11px 8px;border-bottom:1px solid var(--db-line);vertical-align:top} +#deck-dashboard-host th{font-weight:500;color:var(--db-muted)} +#deck-dashboard-host .scroll{max-height:330px;overflow:auto} +#deck-dashboard-host .scroll th{position:sticky;top:0;background:#181f21} +#deck-dashboard-host .slot-list,#deck-dashboard-host .usage-list{display:grid;gap:12px;margin-top:17px} +#deck-dashboard-host .slot,#deck-dashboard-host .profile-item{background:#151b1c;border:1px solid var(--db-line);border-radius:10px;padding:16px} +#deck-dashboard-host .slot-head b,#deck-dashboard-host .usage-head b{font-size:14px;font-weight:550} +#deck-dashboard-host .profiles{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:15px} +#deck-dashboard-host .profile-item.active{border-color:#9acb88} +#deck-dashboard-host .profile-item b{display:block;font-weight:550} +#deck-dashboard-host .profile-item span{color:var(--db-muted);font-size:11px} +#deck-dashboard-host .event{padding:10px 0;border-bottom:1px solid var(--db-line)} +#deck-dashboard-host .event:last-child{border:0} +#deck-dashboard-host .event time{color:var(--db-muted);font-size:11px;margin-right:8px} +#deck-dashboard-host .history-controls{display:flex;flex-wrap:wrap;gap:6px;margin:0} +#deck-dashboard-host .history-controls button,#deck-dashboard-host .chart-legend button{border:1px solid #3a493f;background:#151b1c;color:#c5d2c9;border-radius:7px;padding:7px 11px;font:inherit;font-size:11px;cursor:pointer} +#deck-dashboard-host .history-controls button:hover,#deck-dashboard-host .chart-legend button:hover{background:#253229} +#deck-dashboard-host .history-controls button.active{background:#28362b;color:#d3efc5;border-color:#668659} +#deck-dashboard-host .chart-legend{display:flex;flex-wrap:wrap;gap:7px;margin:17px 0 6px} +#deck-dashboard-host .chart-legend button::before{content:'';display:inline-block;width:10px;height:3px;background:var(--series);margin:0 7px 3px 0} +#deck-dashboard-host .chart-legend button.off{opacity:.45} +#deck-dashboard-host .chart{display:block;width:100%;height:250px} +#deck-dashboard-host .token-total{font-size:25px;font-weight:550;color:#e4f1de;margin-top:12px} +#deck-dashboard-host .history-note{font-size:11px;color:var(--db-muted);margin-top:9px} +#deck-dashboard-host .usage-head,#deck-dashboard-host .usage-meta{display:flex;align-items:baseline;justify-content:space-between;gap:14px} +#deck-dashboard-host .usage-head span,#deck-dashboard-host .usage-meta{color:var(--db-muted)} +#deck-dashboard-host .usage-meta{font-size:11px;margin-top:7px} +#deck-dashboard-host .download{display:inline-block;border:1px solid #668659;color:#c5edaf;border-radius:7px;padding:6px 10px;text-decoration:none} +#deck-dashboard-host .hash{font:11px ui-monospace,SFMono-Regular,monospace;color:var(--db-muted);word-break:break-all} +#deck-dashboard-host .footer{font-size:11px;color:var(--db-muted);margin-top:22px;text-align:right} +#deck-dashboard-host .error{white-space:pre-wrap} +@media(max-width:1180px){#deck-dashboard-host .span3{grid-column:span 6}#deck-dashboard-host .span4{grid-column:span 6}#deck-dashboard-host .row{align-items:flex-start;flex-direction:column}} +@media(max-width:800px){#deck-dashboard-host .span3,#deck-dashboard-host .span4,#deck-dashboard-host .span6{grid-column:span 12}#deck-dashboard-host .top{align-items:flex-start;flex-direction:column}#deck-dashboard-host .profiles{grid-template-columns:1fr}} +@media(max-width:500px){#deck-dashboard-host .card{padding:18px}#deck-dashboard-host .metrics{grid-template-columns:repeat(2,minmax(0,1fr))}#deck-dashboard-host .gpu-title{align-items:flex-start;flex-direction:column}#deck-dashboard-host .usage-meta{flex-direction:column;gap:3px}} diff --git a/index.html b/index.html index f82f53b..9efeca0 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router
+Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router