/* 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 } } } } } }