Widget desktop 224x224 che mostra: - residuo sessione (5h) e settimanale (7d) con barre colorate - conteggio richieste, costo 4 settimane, modello top - timer di reset stimati (calibrati al primo reset osservato) - backend bash che chiama https://ollama.com/api/usage - legge la chiave API da ~/.pi/agent/auth.json (provider ollama-cloud)
341 lines
14 KiB
QML
341 lines
14 KiB
QML
/* Ollama Cloud Usage — dashboard desktop per KDE Plasma 6. */
|
|
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
|
|
|
|
property var usage: null
|
|
property string lastUpdate: ""
|
|
property string errorMsg: ""
|
|
property bool loading: false
|
|
property int clockTick: 0
|
|
|
|
property int refreshSecs: Plasmoid.configuration.refreshIntervalSec
|
|
property int warnThreshold: Plasmoid.configuration.warnThreshold
|
|
property int critThreshold: Plasmoid.configuration.critThreshold
|
|
property bool showActivity: Plasmoid.configuration.showActivity
|
|
property bool showModels: Plasmoid.configuration.showModels
|
|
|
|
Plasmoid.title: i18n("Ollama Cloud")
|
|
Plasmoid.icon: "view-statistics"
|
|
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
|
|
toolTipMainText: i18n("Ollama Cloud — dashboard quota")
|
|
toolTipSubText: root.toolTipSub()
|
|
|
|
// Il widget è desktop-first; la vista compatta è solo fallback per pannelli.
|
|
preferredRepresentation: fullRepresentation
|
|
switchWidth: 150
|
|
switchHeight: 150
|
|
|
|
Plasma5Support.DataSource {
|
|
id: executable
|
|
engine: "executable"
|
|
connectedSources: []
|
|
|
|
onNewData: function(sourceName, data) {
|
|
root.loading = false
|
|
executable.disconnectSource(sourceName)
|
|
if (data["exit code"] !== 0) {
|
|
root.errorMsg = i18n("comando fallito (exit %1)", data["exit code"])
|
|
return
|
|
}
|
|
try {
|
|
const result = JSON.parse((data["stdout"] || "").trim())
|
|
if (result && result.ok === false) {
|
|
root.errorMsg = result.error + (result.detail ? " — " + result.detail : "")
|
|
root.usage = null
|
|
} else if (result && result.session && result.weekly) {
|
|
root.usage = result
|
|
root.errorMsg = ""
|
|
root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss")
|
|
} else {
|
|
root.errorMsg = i18n("risposta inattesa")
|
|
root.usage = null
|
|
}
|
|
} catch (e) {
|
|
root.errorMsg = i18n("JSON non valido: %1", e.message)
|
|
root.usage = null
|
|
}
|
|
}
|
|
}
|
|
|
|
function refresh() {
|
|
if (root.loading) return
|
|
root.loading = true
|
|
const path = Qt.resolvedUrl("../scripts/usage.sh").toString().replace(/^file:\/\//, "")
|
|
executable.connectSource("/bin/bash " + shellQuote(path))
|
|
}
|
|
function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\"'\"'") + "'" }
|
|
function pctRemaining(limit) {
|
|
return limit && typeof limit.remaining === "number" ? Math.round(limit.remaining * 100) : 0
|
|
}
|
|
function totalRequests(limit) {
|
|
if (!limit || !limit.models) return 0
|
|
let count = 0
|
|
for (let i = 0; i < limit.models.length; ++i) count += Number(limit.models[i].request_count || 0)
|
|
return count
|
|
}
|
|
function topModel(limit) {
|
|
if (!limit || !limit.models || limit.models.length === 0) return "—"
|
|
let top = limit.models[0]
|
|
for (let i = 1; i < limit.models.length; ++i) {
|
|
if (limit.models[i].request_count > top.request_count) top = limit.models[i]
|
|
}
|
|
return top.name
|
|
}
|
|
function shortModel(limit) {
|
|
const name = topModel(limit)
|
|
if (name.indexOf("deepseek-v4-flash") === 0) return "DeepSeek V4"
|
|
if (name.indexOf("glm-") === 0) return name.replace(/:.*/, "").toUpperCase()
|
|
return name.replace(/:.*/, "")
|
|
}
|
|
// Verde/ciano quando c'è margine; giallo/rosso vicino all'esaurimento.
|
|
function accentFor(pct, normalAccent) {
|
|
if (pct <= root.critThreshold) return Kirigami.Theme.negativeTextColor
|
|
if (pct <= root.warnThreshold) return Kirigami.Theme.neutralTextColor
|
|
return normalAccent
|
|
}
|
|
function resetMoment(reset) {
|
|
const tick = root.clockTick // dipendenza: aggiorna la visualizzazione ogni 30 secondi
|
|
if (!reset || !reset.at) return "—"
|
|
const date = new Date(reset.at * 1000)
|
|
return date.toLocaleDateString(Qt.locale(), "dd/MM") + " " + date.toLocaleTimeString(Qt.locale(), "HH:mm")
|
|
}
|
|
function resetRemaining(reset) {
|
|
const tick = root.clockTick
|
|
if (!reset || !reset.at) return "—"
|
|
let minutes = Math.max(0, Math.floor((reset.at * 1000 - Date.now()) / 60000))
|
|
if (minutes === 0) return i18n("ora")
|
|
const days = Math.floor(minutes / 1440)
|
|
minutes -= days * 1440
|
|
const hours = Math.floor(minutes / 60)
|
|
minutes -= hours * 60
|
|
return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, minutes)
|
|
}
|
|
function resetStatus() {
|
|
if (!root.usage || !root.usage.resets) return i18n("STIMA RESET")
|
|
return (root.usage.resets.session.estimated || root.usage.resets.weekly.estimated)
|
|
? i18n("RESET · STIMA") : i18n("RESET · RILEVATO")
|
|
}
|
|
function toolTipSub() {
|
|
if (root.usage) return i18n("Sessione: %1% · Settimana: %2%", pctRemaining(root.usage.session), pctRemaining(root.usage.weekly))
|
|
return root.errorMsg || i18n("Aggiornamento in corso…")
|
|
}
|
|
|
|
Component.onCompleted: root.refresh()
|
|
Timer {
|
|
interval: Math.max(30, root.refreshSecs) * 1000
|
|
repeat: true
|
|
running: true
|
|
onTriggered: root.refresh()
|
|
}
|
|
Timer {
|
|
interval: 30000
|
|
repeat: true
|
|
running: true
|
|
onTriggered: root.clockTick += 1
|
|
}
|
|
|
|
// Fallback minimale se fosse aggiunto accidentalmente a un pannello.
|
|
compactRepresentation: Component {
|
|
Item {
|
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
|
Kirigami.Icon {
|
|
anchors.fill: parent
|
|
source: Plasmoid.icon
|
|
color: root.usage ? root.accentFor(Math.min(root.pctRemaining(root.usage.session), root.pctRemaining(root.usage.weekly)), "#00bcd4") : Kirigami.Theme.disabledTextColor
|
|
}
|
|
MouseArea { anchors.fill: parent; onClicked: Plasmoid.expanded = !Plasmoid.expanded }
|
|
}
|
|
}
|
|
|
|
fullRepresentation: Component {
|
|
Item {
|
|
implicitWidth: 224
|
|
implicitHeight: 224
|
|
Layout.preferredWidth: 224
|
|
Layout.preferredHeight: 224
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 8
|
|
spacing: 4
|
|
|
|
// Header con stato della connessione.
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Kirigami.Icon {
|
|
source: Plasmoid.icon
|
|
color: "#00bcd4"
|
|
Layout.preferredWidth: 20
|
|
Layout.preferredHeight: 20
|
|
}
|
|
ColumnLayout {
|
|
spacing: 0
|
|
PC3.Label {
|
|
text: i18n("OLLAMA CLOUD")
|
|
font.bold: true
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1
|
|
}
|
|
PC3.Label {
|
|
text: root.errorMsg ? i18n("OFFLINE") : i18n("QUOTA DASHBOARD")
|
|
color: root.errorMsg ? Kirigami.Theme.negativeTextColor : "#00bcd4"
|
|
font.bold: true
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
|
|
}
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
Rectangle {
|
|
Layout.preferredWidth: 9
|
|
Layout.preferredHeight: 9
|
|
radius: 5
|
|
color: root.errorMsg ? Kirigami.Theme.negativeTextColor : "#7cb342"
|
|
}
|
|
PC3.Button {
|
|
icon.name: "view-refresh"
|
|
display: QQC2.AbstractButton.IconOnly
|
|
enabled: !root.loading
|
|
onClicked: root.refresh()
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 1
|
|
color: Qt.rgba(0, 188 / 255, 212 / 255, 0.45)
|
|
}
|
|
|
|
PC3.Label {
|
|
visible: root.errorMsg.length > 0
|
|
Layout.fillWidth: true
|
|
wrapMode: Text.Wrap
|
|
color: Kirigami.Theme.negativeTextColor
|
|
text: i18n("Errore: %1", root.errorMsg)
|
|
}
|
|
|
|
LimitRow {
|
|
Layout.fillWidth: true
|
|
title: i18n("Sessione")
|
|
period: "5H"
|
|
remaining: root.usage ? root.usage.session.remaining : 0
|
|
used: root.usage ? root.usage.session.used : 0
|
|
accent: root.accentFor(root.pctRemaining(root.usage ? root.usage.session : null), "#00bcd4")
|
|
models: (root.showModels && root.usage) ? root.usage.session.models : []
|
|
}
|
|
|
|
LimitRow {
|
|
Layout.fillWidth: true
|
|
title: i18n("Settimanale")
|
|
period: "7D"
|
|
remaining: root.usage ? root.usage.weekly.remaining : 0
|
|
used: root.usage ? root.usage.weekly.used : 0
|
|
accent: root.accentFor(root.pctRemaining(root.usage ? root.usage.weekly : null), "#7cb342")
|
|
models: (root.showModels && root.usage) ? root.usage.weekly.models : []
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 28
|
|
radius: 5
|
|
color: Qt.rgba(0, 0, 0, 0.28)
|
|
|
|
GridLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 5
|
|
columns: 2
|
|
columnSpacing: 8
|
|
rowSpacing: 0
|
|
PC3.Label {
|
|
text: i18n("COSTO 4W")
|
|
color: Kirigami.Theme.disabledTextColor
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
|
}
|
|
PC3.Label {
|
|
text: i18n("MODELLO TOP")
|
|
color: Kirigami.Theme.disabledTextColor
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
|
}
|
|
PC3.Label {
|
|
text: root.usage && root.usage.activity ? "$" + (root.usage.activity.cost || "0.00") : "—"
|
|
font.bold: true
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
|
|
}
|
|
PC3.Label {
|
|
text: root.usage ? root.shortModel(root.usage.weekly) : "—"
|
|
font.bold: true
|
|
elide: Text.ElideRight
|
|
Layout.maximumWidth: 94
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 40
|
|
radius: 5
|
|
color: Qt.rgba(0, 0, 0, 0.28)
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
spacing: 0
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
PC3.Label {
|
|
text: root.resetStatus()
|
|
color: "#00bcd4"
|
|
font.bold: true
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
PC3.Label {
|
|
text: root.lastUpdate ? i18n("SYNC %1", root.lastUpdate) : i18n("SYNC…")
|
|
color: Kirigami.Theme.disabledTextColor
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
|
}
|
|
}
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
PC3.Label {
|
|
text: i18n("5H %1", root.resetMoment(root.usage ? root.usage.resets.session : null))
|
|
color: Kirigami.Theme.disabledTextColor
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
PC3.Label {
|
|
text: root.resetRemaining(root.usage ? root.usage.resets.session : null)
|
|
color: "#00bcd4"
|
|
font.bold: true
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
|
}
|
|
}
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
PC3.Label {
|
|
text: i18n("7D %1", root.resetMoment(root.usage ? root.usage.resets.weekly : null))
|
|
color: Kirigami.Theme.disabledTextColor
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
PC3.Label {
|
|
text: root.resetRemaining(root.usage ? root.usage.resets.weekly : null)
|
|
color: "#7cb342"
|
|
font.bold: true
|
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |