commit 2a0020ca918d95fa5b0c85e9d71315adc850fdf9 Author: enne2 Date: Sun Oct 25 16:12:52 2026 +0100 Strata dashboard plasmoid v1.2: barre a soglie e velocita decode/prefill - card GPU con barre progressive: VRAM (informativa), utilizzo 60/85 %, temperatura 75/85 C, potenza su budget 350 W (ambra 70 %, rosso 90 %) - velocita del motore: DEC (generazione) e PRE (prefill) in tok/s, live durante la generazione, ultima richiesta da fermo (badge LIVE/ULTIMA); semantica inversa: verde sopra la nominale, ambra fra 50 % e nominale - card servizio + controllo unificata con switch load/unload nativo e stato che si chiude sulla fase osservata - MetricBar condiviso con levelColor()/speedColor() e soglie in testa al QML - README: requisiti, campi del control plane, tabella soglie, limiti del box - avatar pixel-art 512x512 in assets/repository-avatar.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4cc226 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# backup locali del QML (non versionati) +contents/ui/*.bak-* +*.bak-* + +# output locali +outputs/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..35c73b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 enne2 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e238fb --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# KDE Strata Dashboard Plasmoid + +Plasmoide per **KDE Plasma 6** che mostra lo stato del servizio di inferenza **Strata** +(Qwen3.8-Flash-Next IQ2_XS servito su NVIDIA L40S) leggendo il control plane HTTP del +servizio. Non parla mai direttamente con il motore: lo `8082` di Strata ascolta solo su +`127.0.0.1` della macchina che lo ospita, quindi tutte le letture passano dal control plane. + +## Cosa mostra + +- **fase** del servizio con badge colorato: caricato / generazione / caricamento / + scaricato / fermo / non raggiungibile; +- **VRAM** usata e totale (barra indaco, informativa: il modello occupa quasi tutta la GPU); +- **utilizzo, temperatura e potenza** della GPU come **barre progressive** che si riempiono + in proporzione al valore e cambiano colore alle soglie; +- **velocità del motore**: `DEC` (generazione, tok/s) e `PRE` (prefill, tok/s), live durante + la generazione e riferite all'ultima richiesta quando il motore è fermo (badge + `LIVE` / `ULTIMA`); +- **modello**: alias, tipo di KV, contesto, concorrenza, disponibilità vision; +- **switch carica/scarica** nativo (`POST /load`, `POST /unload?wait=0`) con stato + `PENDING` che si chiude sulla fase osservata, non su un timer. + +## Requisiti + +- KDE Plasma 6; +- `curl` e `bash`; +- chiave del control plane in `~/.config/strata-control/control.key` (0600); +- control plane raggiungibile su `STRATACTL_API` (default `http://172.30.150.224:18090`). + +## Installazione + +```bash +kpackagetool6 -t Plasma/Applet -i . +``` + +Per aggiornare un'installazione esistente: + +```bash +kpackagetool6 -t Plasma/Applet -u . +``` + +Aggiungi poi **Strata** al desktop o a un pannello dalle impostazioni dei widget Plasma. + +## Backend + +- `contents/scripts/status.sh`: `GET $STRATACTL_API/status` con header `X-API-Key`, stampa il + JSON su stdout (il widget lo legge con `P5Support.DataSource`, intervallo 4 s). +- `contents/scripts/action.sh load|unload`: `POST` in background con `setsid` ed exit + immediato, così il polling del widget non resta bloccato durante le operazioni lunghe. + +Campi usati dal widget: + +| Campo di `/status` | Uso | +| --- | --- | +| `phase`, `service.*` | badge di fase, tooltip | +| `model.live.tok_s`, `model.live.prefill_tok_s_mean` | barre DEC/PRE live | +| `model.last_timings.predicted_per_second`, `.prompt_per_second` | DEC/PRE dell'ultima richiesta | +| `model.alias`, `kv`, `ctx`, `concurrency`, `vision` | card servizio | +| `gpu.mem_used_mib/mem_total_mib/mem_pct`, `util_pct`, `temp_c`, `power_w`, `name` | card GPU | + +Il control plane deve inoltrare `model.live` e `model.last_timings`: la versione del +2026-10-25 legge `live` da `/metrics` (cache 3 s) e passa `last_timings` da `/v1/status`. +Senza quei campi le barre DEC/PRE restano grigie. + +## Soglie e scale + +Tutte in testa a `contents/ui/main.qml`, facili da ritoccare: + +| Grandezza | Fondo scala | Ambra | Rosso | +| --- | --- | --- | --- | +| Utilizzo GPU | 100 % | 60 % | 85 % | +| Temperatura GPU | 100 °C | 75 °C | 85 °C | +| Potenza GPU | 350 W (TDP L40S) | 70 % del budget | 90 % del budget | +| Decode (generazione) | 120 tok/s | < 40 tok/s (nominale) | < 20 tok/s | +| Prefill | 5000 tok/s | < 3000 tok/s (nominale) | < 1500 tok/s | + +Per le due velocità la semantica è **inversa** rispetto ai carichi: alto è bene, quindi +verde sopra la nominale, ambra fra il 50 % e la nominale, rosso sotto. Grigio = valore non +disponibile (motore scarico, sensore assente). + +## Note + +- Avatar del repository in `assets/repository-avatar.png`, immagine pixel-art generata con + Antigravity (modello immagine Gemini). +- Il widget occupa un box desktop da 324x308 pixel logici: card e spaziature sono tarate su + quella altezza. Se il containment lo rimpicciolisce il footer `SYNC` viene tagliato. + +## Licenza + +MIT. Vedi [LICENSE](LICENSE). diff --git a/assets/repository-avatar.png b/assets/repository-avatar.png new file mode 100644 index 0000000..2096ee6 Binary files /dev/null and b/assets/repository-avatar.png differ diff --git a/contents/scripts/action.sh b/contents/scripts/action.sh new file mode 100755 index 0000000..4b08cd0 --- /dev/null +++ b/contents/scripts/action.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# action.sh [uniq] — POST in background, esce subito. +API="${STRATACTL_API:-http://172.30.150.224:18090}" +KEYF="$HOME/.config/strata-control/control.key" +[ $# -ge 1 ] || exit 1 +[ -r "$KEYF" ] || exit 1 +setsid curl -s -m 320 -X POST -H "X-API-Key: $(cat "$KEYF")" "$API/$1" >/dev/null 2>&1 < /dev/null & +exit 0 diff --git a/contents/scripts/status.sh b/contents/scripts/status.sh new file mode 100755 index 0000000..22091cc --- /dev/null +++ b/contents/scripts/status.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Stato del control plane Strata (JSON su stdout). +API="${STRATACTL_API:-http://172.30.150.224:18090}" +KEYF="$HOME/.config/strata-control/control.key" +if [ ! -r "$KEYF" ]; then printf '{"error":"chiave mancante"}'; exit 0; fi +out=$(curl -s -m 6 -H "X-API-Key: $(cat "$KEYF")" "$API/status" 2>/dev/null) +if [ -z "$out" ]; then printf '{"error":"control plane non raggiungibile"}'; else printf '%s' "$out"; fi diff --git a/contents/ui/main.qml b/contents/ui/main.qml new file mode 100644 index 0000000..d209a1f --- /dev/null +++ b/contents/ui/main.qml @@ -0,0 +1,581 @@ +/* Strata (qwen3.8-flash-next) — stato e controllo del servizio di inferenza su seonse-engine-02. + * + * Adattato da llama-gsq3: ora parla con il control plane di Strata, che espone + * load/unload nativi (/load, /unload?wait=0). Strata serve UNA richiesta alla volta: + * la concorrenza e' solo informativa, non c'e' piu' il selettore di slot. + * Stile invariato rispetto agli altri plasmoidi enne2. + */ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.plasma.plasmoid +import org.kde.plasma.core as PlasmaCore +import org.kde.plasma.components 3.0 as PC3 +import org.kde.plasma.plasma5support as Plasma5Support +import org.kde.kirigami as Kirigami + +PlasmoidItem { + id: root + + Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground + preferredRepresentation: fullRepresentation + switchWidth: 150 + switchHeight: 150 + + readonly property color themeGreen: "#4ade80" + readonly property color themeIndigo: "#818cf8" + readonly property color themeViolet: "#a855f7" + + // ---------- palette dei livelli ---------- + readonly property color okColor: "#4ade80" // sotto la soglia di attenzione + readonly property color warnColor: "#fbbf24" // attenzione + readonly property color critColor: "#f87171" // critico + readonly property color naColor: "#5b6b7d" // valore non disponibile + + // ---------- soglie GPU (ritocca qui) ---------- + // Le barre PWR si riempiono sul budget: ambra oltre il 70 %, rosso oltre il 90 %. + // NVIDIA L40S: TDP 350 W, 48 GB; la VRAM resta informativa (colore accento). + readonly property real gpuPowerMax: 350 + readonly property real utilWarn: 60 + readonly property real utilCrit: 85 + readonly property real tempWarn: 75 + readonly property real tempCrit: 85 + readonly property real powerWarnRatio: 0.70 + readonly property real powerCritRatio: 0.90 + + // ---------- velocità di generazione e prefill (ritocca qui) ---------- + // Nominali misurate sul L40S con MTP (2026-10-23): decode ~40 tok/s (picchi ~69), + // prefill ~2800-3800 tok/s. Fondo scala = massimo ragionevole. + readonly property real decodeNominal: 40 + readonly property real decodeMax: 120 + readonly property real prefillNominal: 3000 + readonly property real prefillMax: 5000 + + readonly property string scripts: Qt.resolvedUrl("../scripts/").toString().replace(/^file:\/\//, "") + readonly property string pollSource: "/bin/bash " + root.scripts + "status.sh" + + property var st: ({}) + property string phase: "unknown" + property string errorMsg: "" + property string lastUpdate: "" + property bool pending: false + property string pendingTarget: "" + property int pendingTries: 0 + property int counter: 0 + property string actSource: "" + + readonly property var modelInfo: root.st.model || ({}) + readonly property var gpuInfo: root.st.gpu || ({}) + readonly property var liveInfo: root.modelInfo.live || ({}) + readonly property var lastTimings: root.modelInfo.last_timings || ({}) + readonly property bool speedsLive: typeof root.liveInfo.tok_s === "number" + // durante la generazione valgono i valori live, da idle quelli dell'ultima richiesta + readonly property real decodeTokS: (typeof root.liveInfo.tok_s === "number") + ? root.liveInfo.tok_s : root.metric(root.lastTimings.predicted_per_second) + readonly property real prefillTokS: (typeof root.liveInfo.prefill_tok_s_mean === "number") + ? root.liveInfo.prefill_tok_s_mean : root.metric(root.lastTimings.prompt_per_second) + readonly property var concInfo: root.modelInfo.concurrency || ({}) + readonly property bool loaded: root.modelInfo.loaded === true + readonly property bool running: root.pending ? (root.pendingTarget === "load") + : (root.phase === "loaded" || root.phase === "generating" || root.phase === "loading") + + Plasmoid.busy: root.pending + toolTipMainText: "Strata — " + root.phaseWord() + toolTipSubText: { + const m = root.modelInfo, g = root.gpuInfo + if (root.errorMsg.length > 0) return root.errorMsg + return "fase: " + root.phase + + "\nmodello: " + (m.alias || "—") + (m.kv ? " (" + m.kv + ")" : "") + + "\ncontesto: " + root.ctxLabel() + " per richiesta" + + "\nconcorrenza: " + (root.concInfo.serving || 1) + " · vision: " + (m.vision ? "attiva" : "non disponibile") + + "\nVRAM: " + (g.mem_used_mib / 1024).toFixed(1) + " / " + (g.mem_total_mib / 1024).toFixed(1) + " GiB" + + "\nutil " + (g.util_pct || 0).toFixed(0) + "% · " + (g.temp_c || 0).toFixed(0) + " °C · " + (g.power_w || 0).toFixed(0) + " W" + + "\ndecode " + root.fmt(root.decodeTokS, 1, " tok/s") + " · prefill " + root.fmt(root.prefillTokS, 0, " tok/s") + + (root.speedsLive ? " (live)" : " (ultima richiesta)") + } + + function phaseAccent() { + switch (root.phase) { + case "loaded": return root.themeGreen + case "generating": return root.themeIndigo + case "loading": return root.themeViolet + case "unreachable": return Kirigami.Theme.negativeTextColor + default: return Kirigami.Theme.disabledTextColor + } + } + function phaseWord() { + if (root.pending) return root.pendingTarget === "load" ? "CARICAMENTO" : "SCARICAMENTO" + switch (root.phase) { + case "loaded": return "IN MEMORIA" + case "generating": return "GENERAZIONE" + case "loading": return "CARICAMENTO" + case "unloaded": return "SCARICATO" + case "stopped": return "SERVIZIO FERMO" + case "unreachable": return "NON RAGGIUNGIBILE" + default: return "?" + } + } + function phaseBadge() { + if (root.pending) return "ATTENDI" + switch (root.phase) { + case "loaded": return "IDLE" + case "generating": return "GEN" + case "loading": return "LOAD" + case "unloaded": return "OFF" + case "stopped": return "STOP" + default: return "ERR" + } + } + function stateLine() { + const m = root.modelInfo + if (root.phase === "generating" && m.tok_s) return "in generazione · " + Number(m.tok_s).toFixed(1) + " t/s" + switch (root.phase) { + case "loaded": return "caricato, in attesa" + case "loading": return "caricamento in corso…" + case "unloaded": return "scaricato · VRAM libera" + case "stopped": return "container fermo" + default: return root.errorMsg.length > 0 ? root.errorMsg : "stato sconosciuto" + } + } + function ctxLabel() { + const c = root.modelInfo.ctx + if (!c) return "—" + return c >= 1024 ? Math.round(c / 1024) + "k" : String(c) + } + function gib(v) { return (typeof v === "number") ? (v / 1024).toFixed(1) : "—" } + function num(v, d) { return (typeof v === "number") ? v : (d || 0) } + // valore numerico o NaN (per le barre: NaN -> grigio e barra vuota) + function metric(v) { return (typeof v === "number") ? v : NaN } + function fmt(v, d, suffix) { return (typeof v === "number") ? v.toFixed(d === undefined ? 0 : d) + (suffix || "") : "n/d" } + + // Colore semaforico di una barra: NaN -> grigio, poi verde/ambra/rosso. + function levelColor(value, warnAt, critAt) { + if (isNaN(value)) return root.naColor + if (value >= critAt) return root.critColor + if (value >= warnAt) return root.warnColor + return root.okColor + } + // Frazione 0..1 di riempimento. + function fillRatio(value, scaleMax) { + if (isNaN(value) || !(scaleMax > 0)) return 0 + return Math.max(0, Math.min(1, value / scaleMax)) + } + // Per le velocità la semantica è inversa a quella dei carichi: alto = bene. + function speedColor(value, nominal) { + if (isNaN(value) || value <= 0) return root.naColor + if (value >= nominal) return root.okColor + if (value >= nominal * 0.5) return root.warnColor + return root.critColor + } + + // ---------- polling ---------- + Plasma5Support.DataSource { + id: poll + engine: "executable" + interval: 4000 + connectedSources: [root.pollSource] + onNewData: function (source, data) { + const out = ((data && data["stdout"]) || "").trim() + if (out.length < 2) { root.errorMsg = "nessuna risposta dal control plane"; root.phase = "unreachable"; return } + let o + try { o = JSON.parse(out) } catch (e) { root.errorMsg = "risposta non valida"; return } + if (o.error) { root.errorMsg = o.error; root.phase = "unreachable"; return } + root.errorMsg = "" + root.st = o + root.phase = o.phase || "unknown" + root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss") + root.checkPending() + } + } + + // ---------- azioni ---------- + Plasma5Support.DataSource { + id: act + engine: "executable" + connectedSources: [] + onNewData: function (source, data) { if (source === root.actSource) root.clearAction() } + } + function clearAction() { + if (root.actSource.length > 0) { try { act.disconnectSource(root.actSource) } catch (e) {} } + root.actSource = "" + } + function runAction(cmd, target) { + root.counter++ + root.pending = true + root.pendingTarget = target + root.pendingTries = 0 + root.actSource = "/bin/bash " + root.scripts + "action.sh " + cmd + " " + root.counter + act.connectSource(root.actSource) + pendingTimer.restart() + } + // criterio autorevole: la fase osservata, non il flag locale + function checkPending() { + if (!root.pending) return + const done = (root.pendingTarget === "load" && + (root.phase === "loaded" || root.phase === "generating")) + || (root.pendingTarget === "unload" && + (root.phase === "unloaded" || root.phase === "stopped")) + if (done) { root.pending = false; pendingTimer.stop() } + } + Timer { + id: pendingTimer + interval: 1000 + repeat: true + onTriggered: { + root.pendingTries++ + if (root.pendingTries > 240) { // 240 s: il load da freddo puo' durare ~160 s + root.pending = false + root.errorMsg = "operazione non confermata (timeout)" + stop() + } + } + } + + // Barra progressiva con colore a soglie: si riempie su value/scaleMax, + // colora verde -> ambra -> rosso in base a warnAt/critAt. + component MetricBar: RowLayout { + id: bar + property string label + property real value: NaN + property real scaleMax: 100 + property real warnAt: 60 + property real critAt: 85 + property string display + // opzionale: metriche dove il valore alto è positivo (velocità) + property bool higherIsBetter: false + property real nominal: 1 + + readonly property real ratio: root.fillRatio(bar.value, bar.scaleMax) + readonly property color tint: bar.higherIsBetter + ? root.speedColor(bar.value, bar.nominal) + : root.levelColor(bar.value, bar.warnAt, bar.critAt) + + Layout.fillWidth: true + spacing: 5 + + PC3.Label { + text: bar.label + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 4 + Layout.preferredWidth: 28 + Layout.alignment: Qt.AlignVCenter + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 5 + Layout.alignment: Qt.AlignVCenter + + Rectangle { + anchors.fill: parent + radius: 2.5 + color: Qt.rgba(1, 1, 1, 0.12) + + Rectangle { + width: parent.width * bar.ratio + height: parent.height + radius: 2.5 + color: bar.tint + Behavior on width { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } } + Behavior on color { ColorAnimation { duration: 300 } } + } + } + } + + PC3.Label { + text: bar.display + color: isNaN(bar.value) ? root.naColor : bar.tint + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + horizontalAlignment: Text.AlignRight + Layout.preferredWidth: 52 + Layout.alignment: Qt.AlignVCenter + } + } + + compactRepresentation: Component { + Item { + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + Kirigami.Icon { + anchors.fill: parent + source: "utilities-system-monitor" + color: root.phaseAccent() + } + MouseArea { anchors.fill: parent; onClicked: Plasmoid.expanded = !Plasmoid.expanded } + } + } + + fullRepresentation: Component { + Item { + implicitWidth: 256 + implicitHeight: 248 + Layout.preferredWidth: 256 + Layout.preferredHeight: 248 + + ColumnLayout { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: Math.min(parent.width - 16, 300) + spacing: 2 + + // header + RowLayout { + Layout.fillWidth: true + spacing: 6 + Kirigami.Icon { + source: "utilities-system-monitor" + color: root.phaseAccent() + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + } + RowLayout { + spacing: 6 + Layout.fillWidth: true + PC3.Label { + text: "STRATA" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1 + } + PC3.Label { + text: root.phaseWord() + color: root.phaseAccent() + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + } + Item { Layout.fillWidth: true } + Rectangle { + Layout.preferredWidth: 8 + Layout.preferredHeight: 8 + radius: 4 + color: root.phaseAccent() + } + PC3.Button { + icon.name: "view-refresh" + display: QQC2.AbstractButton.IconOnly + enabled: !root.pending + onClicked: { try { poll.disconnectSource(root.pollSource); poll.connectSource(root.pollSource) } catch (e) {} } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Qt.rgba(root.phaseAccent().r, root.phaseAccent().g, root.phaseAccent().b, 0.35) + } + + PC3.Label { + visible: root.errorMsg.length > 0 + Layout.fillWidth: true + wrapMode: Text.Wrap + maximumLineCount: 2 + elide: Text.ElideRight + color: Kirigami.Theme.neutralTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + text: root.errorMsg + } + + // card GPU + Rectangle { + Layout.fillWidth: true + implicitHeight: gpuBody.implicitHeight + 10 + radius: 5 + color: Qt.rgba(0, 0, 0, 0.28) + border.width: 1 + border.color: Qt.rgba(root.themeIndigo.r, root.themeIndigo.g, root.themeIndigo.b, 0.65) + + ColumnLayout { + id: gpuBody + anchors.fill: parent + anchors.margins: 4 + spacing: 3 + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: "GPU" + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + PC3.Label { + text: String(root.gpuInfo.name || "").replace("NVIDIA ", "") + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + PC3.Label { + text: root.speedsLive ? "LIVE" : "ULTIMA" + color: root.speedsLive ? root.themeGreen : Kirigami.Theme.disabledTextColor + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 4 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: root.gib(root.gpuInfo.mem_used_mib) + " / " + root.gib(root.gpuInfo.mem_total_mib) + " GiB" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 + } + } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 6 + radius: 3 + color: Qt.rgba(1, 1, 1, 0.12) + Rectangle { + width: parent.width * Math.min(1, Math.max(0, root.num(root.gpuInfo.mem_pct, 0) / 100)) + height: parent.height + radius: 3 + color: root.themeIndigo + Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } + } + } + MetricBar { + label: "UTIL" + value: root.metric(root.gpuInfo.util_pct) + scaleMax: 100 + warnAt: root.utilWarn + critAt: root.utilCrit + display: root.fmt(root.gpuInfo.util_pct, 0, "%") + } + MetricBar { + label: "TEMP" + value: root.metric(root.gpuInfo.temp_c) + scaleMax: 100 + warnAt: root.tempWarn + critAt: root.tempCrit + display: root.fmt(root.gpuInfo.temp_c, 0, " °C") + } + MetricBar { + label: "PWR" + value: root.metric(root.gpuInfo.power_w) + scaleMax: root.gpuPowerMax + warnAt: root.gpuPowerMax * root.powerWarnRatio + critAt: root.gpuPowerMax * root.powerCritRatio + display: root.fmt(root.gpuInfo.power_w, 0, " W") + } + + // velocità del motore: decode (generazione) e prefill, tok/s + MetricBar { + label: "DEC" + value: root.decodeTokS + scaleMax: root.decodeMax + nominal: root.decodeNominal + higherIsBetter: true + display: root.fmt(root.decodeTokS, 1, " t/s") + } + MetricBar { + label: "PRE" + value: root.prefillTokS + scaleMax: root.prefillMax + nominal: root.prefillNominal + higherIsBetter: true + display: root.fmt(root.prefillTokS, 0, " t/s") + } + } + } + + // card servizio + controllo (load/unload nativo) + Rectangle { + Layout.fillWidth: true + implicitHeight: svcBody.implicitHeight + 10 + radius: 5 + color: Qt.rgba(0, 0, 0, 0.28) + border.width: 1 + border.color: Qt.rgba(root.phaseAccent().r, root.phaseAccent().g, root.phaseAccent().b, 0.65) + + ColumnLayout { + id: svcBody + anchors.fill: parent + anchors.margins: 4 + spacing: 2 + + RowLayout { + Layout.fillWidth: true + spacing: 4 + PC3.Label { + text: "SERVIZIO" + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + PC3.Label { + text: root.phaseBadge() + color: root.phaseAccent() + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.BusyIndicator { + visible: root.pending + running: root.pending + Layout.preferredWidth: Kirigami.Units.iconSizes.small + Layout.preferredHeight: Kirigami.Units.iconSizes.small + } + PC3.Switch { + checked: root.running + enabled: !root.pending && root.phase !== "unreachable" && root.phase !== "stopped" + onToggled: { + root.runAction(checked ? "load" : "unload", checked ? "load" : "unload") + checked = Qt.binding(function () { return root.running }) + } + QQC2.ToolTip.text: root.running ? "Scarica il modello (libera la VRAM)" : "Carica il modello in VRAM" + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: 500 + } + } + + PC3.Label { + Layout.fillWidth: true + text: root.modelInfo.alias || "—" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1 + elide: Text.ElideRight + } + + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: (root.modelInfo.kv ? root.modelInfo.kv + " KV" : "—") + " · ctx " + root.ctxLabel() + + " · " + (root.concInfo.serving || 1) + "x" + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: root.modelInfo.vision ? "vision ✓" : "vision —" + color: root.modelInfo.vision ? root.themeGreen : Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + + PC3.Label { + Layout.fillWidth: true + text: root.stateLine() + color: root.pending ? root.themeViolet : Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 4 + elide: Text.ElideRight + } + } + } + + Item { Layout.fillHeight: true } + + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: root.lastUpdate ? "SYNC " + root.lastUpdate : "SYNC…" + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: root.pending ? "PENDING" : root.phaseBadge() + color: root.phaseAccent() + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + } + } + } +} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..2834910 --- /dev/null +++ b/metadata.json @@ -0,0 +1,18 @@ +{ + "KPackageStructure": "Plasma/Applet", + "KPlugin": { + "Id": "org.enne2.llamagsq3", + "Name": "Strata (qwen3.8-flash-next)", + "Description": "Stato e controllo del servizio Strata: fase, VRAM, utilizzo/temperatura/potenza GPU a barre, velocita di generazione e prefill, load/unload nativi.", + "Icon": "utilities-system-monitor", + "Version": "1.2", + "License": "MIT", + "Authors": [ + { + "Name": "enne2" + } + ], + "Category": "System Information" + }, + "X-Plasma-API-Minimum-Version": "6.0" +}