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:
@@ -0,0 +1,6 @@
|
||||
# backup locali del QML (non versionati)
|
||||
contents/ui/*.bak-*
|
||||
*.bak-*
|
||||
|
||||
# output locali
|
||||
outputs/
|
||||
@@ -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.
|
||||
@@ -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 |
Executable
+8
@@ -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
|
||||
Executable
+7
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user