feat: dashboard KDE Plasma 6 per residui Ollama Cloud con countdown reset
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)
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# stato locale dei reset (privato, generato dal backend)
|
||||||
|
*.state/
|
||||||
|
|
||||||
|
# backup e temporanei
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
# eventuali node_modules o ambienti di test
|
||||||
|
node_modules/
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Ollama Cloud Usage — plasmoide KDE Plasma 6
|
||||||
|
|
||||||
|
Widget per KDE Plasma 6 che mostra il **residuo della sessione (5h)** e il
|
||||||
|
**residuo settimanale (7d)** di [Ollama Cloud](https://ollama.com), il provider
|
||||||
|
`ollama-cloud` configurato nel [pi agent](https://pi.dev).
|
||||||
|
|
||||||
|
Legge la chiave API da `~/.pi/agent/auth.json` (provider `ollama-cloud`, lo
|
||||||
|
stesso usato dall'estensione `pi-ollama-cloud`) e interroga l'endpoint
|
||||||
|
**non documentato** `GET https://ollama.com/api/usage`. L'API restituisce
|
||||||
|
`usage` come frazione 0..1 **già usata**, quindi il residuo è `1 - usage`.
|
||||||
|
|
||||||
|
## Funzionalità
|
||||||
|
|
||||||
|
- **Icona nel pannello** + due mini-barre verticali (5h / 7d) colorate in base
|
||||||
|
al residuo: verde ≥ soglia gialla, giallo tra soglia rossa e gialla, rosso
|
||||||
|
sotto la soglia rossa. Click = apre il popup.
|
||||||
|
- **Popup** con due barre grandi, percentuale residuo/usato, conteggio richieste
|
||||||
|
per modello e costo attività delle ultime 4 settimane.
|
||||||
|
- **Aggiornamento automatico** ogni N secondi (default 300, come l'estensione
|
||||||
|
pi) + pulsante di refresh manuale.
|
||||||
|
- **Configurabile**: intervallo di aggiornamento, soglie di colore, visibilità
|
||||||
|
del costo attività e dei conteggi per modello.
|
||||||
|
|
||||||
|
## File
|
||||||
|
|
||||||
|
```
|
||||||
|
metadata.json metadata del pacchetto (Plasma/Applet)
|
||||||
|
contents/config/main.xml schema della configurazione (kcfg)
|
||||||
|
contents/scripts/usage.sh backend: legge la key, chiama /api/usage, stampa JSON
|
||||||
|
contents/ui/main.qml UI (compact + full representation)
|
||||||
|
contents/ui/LimitRow.qml riga di residuo riutilizzabile
|
||||||
|
contents/ui/configGeneral.qml pagina di configurazione
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installazione
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# dal sorgente
|
||||||
|
kpackagetool6 --type Plasma/Applet --install /home/enne2/Dev/org.enne2.ollamacloud.usage
|
||||||
|
|
||||||
|
# aggiornamento dopo modifiche
|
||||||
|
kpackagetool6 --type Plasma/Applet --upgrade /home/enne2/Dev/org.enne2.ollamacloud.usage
|
||||||
|
```
|
||||||
|
|
||||||
|
Poi "Aggiungi widget" → cerca "Ollama Cloud Usage" e trascinalo nel pannello o
|
||||||
|
sul desktop.
|
||||||
|
|
||||||
|
## Dipendenze
|
||||||
|
|
||||||
|
- `jq` e `curl` (per lo script di backend)
|
||||||
|
- la chiave API di Ollama Cloud in `~/.pi/agent/auth.json` sotto
|
||||||
|
`ollama-cloud` (oppure nella variabile `OLLAMA_API_KEY`)
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- L'endpoint `/api/usage` è **non documentato** e potrebbe cambiare o sparire:
|
||||||
|
lo script gestisce i casi 404/401/429/5xx restituendo un JSON di errore che il
|
||||||
|
plasmoide mostra in modo leggibile.
|
||||||
|
- La chiave API non viene mai stampata né passata come argomento del processo.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
|
||||||
|
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
|
||||||
|
<kcfgfile name=""/>
|
||||||
|
<group name="General">
|
||||||
|
<entry name="refreshIntervalSec" type="Int">
|
||||||
|
<default>300</default>
|
||||||
|
<min>30</min>
|
||||||
|
<max>3600</max>
|
||||||
|
<label>Intervallo di aggiornamento (secondi)</label>
|
||||||
|
<whatsthis>Quanto spesso il plasmoide aggiorna l'utilizzo di Ollama Cloud. Minimo 30s. Default 300s (5 minuti, come l'estensione pi).</whatsthis>
|
||||||
|
</entry>
|
||||||
|
<entry name="warnThreshold" type="Int">
|
||||||
|
<default>40</default>
|
||||||
|
<min>0</min>
|
||||||
|
<max>100</max>
|
||||||
|
<label>Soglia gialla per il residuo (%)</label>
|
||||||
|
<whatsthis>Sotto questa percentuale di residuo la barra diventa gialla.</whatsthis>
|
||||||
|
</entry>
|
||||||
|
<entry name="critThreshold" type="Int">
|
||||||
|
<default>20</default>
|
||||||
|
<min>0</min>
|
||||||
|
<max>100</max>
|
||||||
|
<label>Soglia rossa per il residuo (%)</label>
|
||||||
|
<whatsthis>Sotto questa percentuale di residuo la barra diventa rossa.</whatsthis>
|
||||||
|
</entry>
|
||||||
|
<entry name="showActivity" type="Bool">
|
||||||
|
<default>true</default>
|
||||||
|
<label>Mostra il costo attività (ultime 4 settimane)</label>
|
||||||
|
</entry>
|
||||||
|
<entry name="showModels" type="Bool">
|
||||||
|
<default>true</default>
|
||||||
|
<label>Mostra il conteggio richieste per modello</label>
|
||||||
|
</entry>
|
||||||
|
</group>
|
||||||
|
</kcfg>
|
||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Backend del plasmoide Ollama Cloud Usage.
|
||||||
|
# L'API /api/usage non fornisce reset_at: manteniamo una stima locale e la
|
||||||
|
# ricalibriamo quando usage/conteggi scendono (reset effettivamente osservato).
|
||||||
|
set -u
|
||||||
|
|
||||||
|
AUTH_JSON="${OLLAMA_AUTH_JSON:-$HOME/.pi/agent/auth.json}"
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/ollama-cloud-usage"
|
||||||
|
STATE_FILE="$STATE_DIR/reset-state.json"
|
||||||
|
KEY=""
|
||||||
|
|
||||||
|
if [ -f "$AUTH_JSON" ]; then
|
||||||
|
KEY="$(jq -r '.["ollama-cloud"].key // empty' "$AUTH_JSON" 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
[ -n "$KEY" ] || KEY="${OLLAMA_API_KEY:-}"
|
||||||
|
|
||||||
|
if [ -z "$KEY" ]; then
|
||||||
|
printf '{"ok":false,"error":"no_api_key","detail":"nessuna chiave Ollama Cloud configurata"}\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
RESP="$(curl -sS --max-time 15 https://ollama.com/api/usage -H "Authorization: Bearer $KEY" 2>/dev/null)"
|
||||||
|
CURL_RC=$?
|
||||||
|
if [ "$CURL_RC" -ne 0 ] || [ -z "$RESP" ]; then
|
||||||
|
printf '{"ok":false,"error":"fetch_failed","detail":"curl exit %s"}\n' "$CURL_RC"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! printf '%s' "$RESP" | jq -e '(.limits.session.usage | type == "number") and (.limits.weekly.usage | type == "number")' >/dev/null 2>&1; then
|
||||||
|
printf '{"ok":false,"error":"bad_json","detail":"risposta non valida da /api/usage"}\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
NOW="$(date +%s)"
|
||||||
|
SESSION_USAGE="$(printf '%s' "$RESP" | jq -c '.limits.session.usage')"
|
||||||
|
WEEKLY_USAGE="$(printf '%s' "$RESP" | jq -c '.limits.weekly.usage')"
|
||||||
|
SESSION_REQ="$(printf '%s' "$RESP" | jq -c '[.limits.session.models[]?.request_count] | add // 0')"
|
||||||
|
WEEKLY_REQ="$(printf '%s' "$RESP" | jq -c '[.limits.weekly.models[]?.request_count] | add // 0')"
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
||||||
|
STATE_JSON='{}'
|
||||||
|
[ -f "$STATE_FILE" ] && STATE_JSON="$(<"$STATE_FILE")"
|
||||||
|
|
||||||
|
# $1 nome finestra, $2 durata sec, $3 usage, $4 richieste.
|
||||||
|
# Esporta RESET_AT e CALIBRATED; calibrated=true solo dopo reset osservato.
|
||||||
|
calculate_reset() {
|
||||||
|
local name="$1" duration="$2" current_usage="$3" current_req="$4"
|
||||||
|
local previous_usage previous_req previous_reset previous_calibrated detected periods
|
||||||
|
|
||||||
|
previous_usage="$(printf '%s' "$STATE_JSON" | jq -c --arg n "$name" '.[$n].last_usage // null')"
|
||||||
|
previous_req="$(printf '%s' "$STATE_JSON" | jq -c --arg n "$name" '.[$n].last_requests // null')"
|
||||||
|
previous_reset="$(printf '%s' "$STATE_JSON" | jq -r --arg n "$name" '.[$n].reset_at // 0')"
|
||||||
|
previous_calibrated="$(printf '%s' "$STATE_JSON" | jq -r --arg n "$name" '.[$n].calibrated // false')"
|
||||||
|
detected="$(jq -n --argjson old "$previous_usage" --argjson new "$current_usage" --argjson oldreq "$previous_req" --argjson newreq "$current_req" '($old != null and (($new < ($old - 0.02)) or ($newreq < $oldreq)))')"
|
||||||
|
|
||||||
|
if [ "$previous_reset" -le 0 ]; then
|
||||||
|
RESET_AT=$((NOW + duration))
|
||||||
|
CALIBRATED=false
|
||||||
|
elif [ "$detected" = true ]; then
|
||||||
|
RESET_AT=$((NOW + duration))
|
||||||
|
CALIBRATED=true
|
||||||
|
else
|
||||||
|
RESET_AT="$previous_reset"
|
||||||
|
CALIBRATED="$previous_calibrated"
|
||||||
|
# Una stima scaduta viene avanzata; resta STIMA finché non osserviamo il reset.
|
||||||
|
if [ "$NOW" -ge "$RESET_AT" ]; then
|
||||||
|
periods=$(((NOW - RESET_AT) / duration + 1))
|
||||||
|
RESET_AT=$((RESET_AT + periods * duration))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate_reset session 18000 "$SESSION_USAGE" "$SESSION_REQ"
|
||||||
|
SESSION_RESET="$RESET_AT"
|
||||||
|
SESSION_CALIBRATED="$CALIBRATED"
|
||||||
|
calculate_reset weekly 604800 "$WEEKLY_USAGE" "$WEEKLY_REQ"
|
||||||
|
WEEKLY_RESET="$RESET_AT"
|
||||||
|
WEEKLY_CALIBRATED="$CALIBRATED"
|
||||||
|
|
||||||
|
# Stato 0600: non contiene segreti, ma resta privato al profilo utente.
|
||||||
|
umask 077
|
||||||
|
TMP_STATE="$(mktemp "$STATE_DIR/.reset-state.XXXXXX" 2>/dev/null || true)"
|
||||||
|
if [ -n "$TMP_STATE" ]; then
|
||||||
|
jq -n \
|
||||||
|
--argjson now "$NOW" \
|
||||||
|
--argjson su "$SESSION_USAGE" --argjson sr "$SESSION_REQ" --argjson sat "$SESSION_RESET" --argjson sc "$SESSION_CALIBRATED" \
|
||||||
|
--argjson wu "$WEEKLY_USAGE" --argjson wr "$WEEKLY_REQ" --argjson wat "$WEEKLY_RESET" --argjson wc "$WEEKLY_CALIBRATED" \
|
||||||
|
'{updated_at:$now,session:{last_usage:$su,last_requests:$sr,reset_at:$sat,calibrated:$sc},weekly:{last_usage:$wu,last_requests:$wr,reset_at:$wat,calibrated:$wc}}' >"$TMP_STATE" && mv "$TMP_STATE" "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$RESP" | jq -c \
|
||||||
|
--argjson fetched "$NOW" \
|
||||||
|
--argjson session_at "$SESSION_RESET" --argjson session_cal "$SESSION_CALIBRATED" \
|
||||||
|
--argjson weekly_at "$WEEKLY_RESET" --argjson weekly_cal "$WEEKLY_CALIBRATED" \
|
||||||
|
'{ok:true,session:{remaining:(1-.limits.session.usage),used:.limits.session.usage,models:.limits.session.models},weekly:{remaining:(1-.limits.weekly.usage),used:.limits.weekly.usage,models:.limits.weekly.models},activity:.activity,fetched_at:$fetched,resets:{session:{at:$session_at,estimated:($session_cal|not)},weekly:{at:$weekly_at,estimated:($weekly_cal|not)}}}' \
|
||||||
|
2>/dev/null || printf '{"ok":false,"error":"bad_json","detail":"impossibile elaborare la risposta"}\n'
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/* Card compatta per una quota Ollama Cloud, pensata per il dashboard desktop. */
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string title: ""
|
||||||
|
property string period: ""
|
||||||
|
property real remaining: 0.0
|
||||||
|
property real used: 0.0
|
||||||
|
property color accent: "#00bcd4"
|
||||||
|
property var models: []
|
||||||
|
|
||||||
|
function requestCount() {
|
||||||
|
let count = 0
|
||||||
|
for (let i = 0; i < models.length; ++i) count += Number(models[i].request_count || 0)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
function requestLabel() {
|
||||||
|
const count = requestCount()
|
||||||
|
return count >= 1000 ? (count / 1000).toFixed(1) + "K req" : count + " req"
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumWidth: 0
|
||||||
|
Layout.preferredHeight: 42
|
||||||
|
radius: 6
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.26)
|
||||||
|
border.width: 1
|
||||||
|
border.color: root.accent
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 5
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 25
|
||||||
|
Layout.preferredHeight: 14
|
||||||
|
radius: 8
|
||||||
|
color: root.accent
|
||||||
|
PC3.Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.period
|
||||||
|
color: "#101012"
|
||||||
|
font.bold: true
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PC3.Label {
|
||||||
|
text: root.title
|
||||||
|
font.bold: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumWidth: 0
|
||||||
|
}
|
||||||
|
PC3.Label {
|
||||||
|
text: root.requestLabel()
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||||
|
}
|
||||||
|
PC3.Label {
|
||||||
|
text: Math.round(root.remaining * 100) + "%"
|
||||||
|
color: root.accent
|
||||||
|
font.bold: true
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 6
|
||||||
|
Rectangle {
|
||||||
|
id: track
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 4
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.55)
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: track.width * Math.max(0, Math.min(1, root.remaining))
|
||||||
|
height: parent.height
|
||||||
|
radius: 4
|
||||||
|
color: root.accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* configGeneral.qml — pagina di configurazione del plasmoide Ollama Cloud Usage.
|
||||||
|
* Le property alias "cfg_<key>" si collegano automaticamente alle voci di
|
||||||
|
* contents/config/main.xml.
|
||||||
|
*/
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: page
|
||||||
|
|
||||||
|
property alias cfg_refreshIntervalSec: refreshSpin.value
|
||||||
|
property alias cfg_warnThreshold: warnSpin.value
|
||||||
|
property alias cfg_critThreshold: critSpin.value
|
||||||
|
property alias cfg_showActivity: activityCheck.checked
|
||||||
|
property alias cfg_showModels: modelsCheck.checked
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.largeSpacing
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Intervallo di aggiornamento (secondi)")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
PC3.SpinBox {
|
||||||
|
id: refreshSpin
|
||||||
|
from: 30
|
||||||
|
to: 3600
|
||||||
|
stepSize: 30
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Soglia residuo giallo (%)")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
PC3.SpinBox {
|
||||||
|
id: warnSpin
|
||||||
|
from: 0
|
||||||
|
to: 100
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.Label {
|
||||||
|
text: i18n("Soglia residuo rosso (%)")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
PC3.SpinBox {
|
||||||
|
id: critSpin
|
||||||
|
from: 0
|
||||||
|
to: 100
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
PC3.CheckBox {
|
||||||
|
id: activityCheck
|
||||||
|
text: i18n("Mostra costo attività (ultime 4 settimane)")
|
||||||
|
}
|
||||||
|
PC3.CheckBox {
|
||||||
|
id: modelsCheck
|
||||||
|
text: i18n("Mostra conteggio richieste per modello")
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillHeight: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
/* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"KPackageStructure": "Plasma/Applet",
|
||||||
|
"KPlugin": {
|
||||||
|
"Id": "org.enne2.ollamacloud.usage",
|
||||||
|
"Name": "Ollama Cloud Usage",
|
||||||
|
"Name[it]": "Utilizzo Ollama Cloud",
|
||||||
|
"Description": "Shows the remaining session (5h) and weekly (7d) allowance for Ollama Cloud, reading the API key from ~/.pi/agent/auth.json (the 'ollama-cloud' provider of the pi agent)",
|
||||||
|
"Description[it]": "Mostra il residuo della sessione (5h) e settimanale (7d) di Ollama Cloud, leggendo la chiave API da ~/.pi/agent/auth.json (provider 'ollama-cloud' di pi agent)",
|
||||||
|
"Icon": "view-statistics",
|
||||||
|
"Category": "System Information",
|
||||||
|
"License": "GPL-3.0",
|
||||||
|
"Version": "1.0.0",
|
||||||
|
"Authors": [
|
||||||
|
{ "Name": "enne2" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"X-Plasma-API-Minimum-Version": "6.0"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user