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
This commit is contained in:
enne2
2026-10-25 16:12:52 +01:00
commit 2a0020ca91
8 changed files with 730 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
# backup locali del QML (non versionati)
contents/ui/*.bak-*
*.bak-*
# output locali
outputs/
+21
View File
@@ -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.
+89
View File
@@ -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).
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# action.sh <load|unload> [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
+7
View File
@@ -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
+581
View File
@@ -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
}
}
}
}
}
}
+18
View File
@@ -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"
}