From 3eee01f0f22986c4d43d667826833f8451efab9a Mon Sep 17 00:00:00 2001 From: Matteo Benedetto Date: Thu, 20 Aug 2026 18:48:51 +0200 Subject: [PATCH] feat: dashboard KDE Plasma 6 per utilizzo OpenAI Codex Widget desktop 224x224 che mostra: - residuo settimanale con barra e percentuale - countdown esatto al reset (reset_at dall'API) - piano (Plus/Free), crediti, stato limiti - backend bash che chiama https://chatgpt.com/backend-api/wham/usage - legge il token OAuth da ~/.pi/agent/auth.json (provider openai-codex) --- .gitignore | 4 + README.md | 50 +++++ contents/config/main.xml | 28 +++ contents/scripts/usage.sh | 45 ++++ contents/ui/configGeneral.qml | 57 +++++ contents/ui/main.qml | 355 ++++++++++++++++++++++++++++++ dist/org.enne2.codex.usage.tar.gz | Bin 0 -> 4835 bytes metadata.json | 16 ++ 8 files changed, 555 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 contents/config/main.xml create mode 100755 contents/scripts/usage.sh create mode 100644 contents/ui/configGeneral.qml create mode 100644 contents/ui/main.qml create mode 100644 dist/org.enne2.codex.usage.tar.gz create mode 100644 metadata.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c9ef85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.bak +*.tmp +node_modules/ +/tmp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..29ffa9e --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# OpenAI Codex Usage — plasmoide KDE Plasma 6 + +Widget desktop 224×224 per KDE Plasma 6 che mostra il **residuo settimanale** +di [OpenAI Codex](https://openai.com/codex), autenticato via OAuth +(profilo ChatGPT). + +Legge il token OAuth da `~/.pi/agent/auth.json` (provider `openai-codex` +configurato nel [pi agent](https://pi.dev)) e interroga l'endpoint +(non documentato) `GET https://chatgpt.com/backend-api/wham/usage`. + +## Funzionalità + +- **Piano**: Plus / Free / Team visibile nell'header. +- **Residuo settimanale** con barra di avanzamento e percentuale. +- **Countdown esatto** al reset (data/ora + giorni/ore/minuti mancanti), + direttamente dal campo `reset_at` della risposta API. +- **Crediti**: saldo crediti extra (se disponibili). +- **Stato limiti**: indicatore visivo se `limit_reached` o `spend_control.reached`. +- **Aggiornamento automatico** configurabile + pulsante di refresh manuale. + +## File + +``` +metadata.json metadata del pacchetto (Plasma/Applet) +contents/config/main.xml schema della configurazione (kcfg) +contents/scripts/usage.sh backend: token OAuth, chiama wham/usage, stampa JSON +contents/ui/main.qml UI (dashboard 224×224) +contents/ui/configGeneral.qml pagina di configurazione +``` + +## Installazione + +```bash +# da sorgente +kpackagetool6 --type Plasma/Applet --install /home/enne2/Dev/org.enne2.codex.usage + +# da archivio +kpackagetool6 --type Plasma/Applet --install dist/org.enne2.codex.usage.tar.gz +``` + +## Dipendenze + +- `jq` e `curl` (per lo script di backend) +- il token OAuth di OpenAI Codex in `~/.pi/agent/auth.json` sotto `openai-codex` (impostato da pi con `/login`) + +## Note + +- L'endpoint `/backend-api/wham/usage` è **non documentato** e potrebbe cambiare. +- Se il token OAuth scade, il plasmoide mostrerà "token scaduto". Riesegui `/login` + in pi (provider `openai-codex`) per rigenerarlo. \ No newline at end of file diff --git a/contents/config/main.xml b/contents/config/main.xml new file mode 100644 index 0000000..2f5964f --- /dev/null +++ b/contents/config/main.xml @@ -0,0 +1,28 @@ + + + + + + 300 + 30 + 3600 + + Quanto spesso il plasmoide aggiorna i dati Codex. Minimo 30s. Default 300s (5 minuti). + + + 60 + 0 + 100 + + + + 30 + 0 + 100 + + + + \ No newline at end of file diff --git a/contents/scripts/usage.sh b/contents/scripts/usage.sh new file mode 100755 index 0000000..8c23eb8 --- /dev/null +++ b/contents/scripts/usage.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Backend del plasmoide OpenAI Codex Usage. +# Chiama l'endpoint (non documentato) GET https://chatgpt.com/backend-api/wham/usage +# con OAuth Bearer. La risposta contiene reset_at esatto → nessuna stima locale. +set -u + +AUTH_JSON="${CODEX_AUTH_JSON:-$HOME/.pi/agent/auth.json}" +TOKEN="" +if [ -f "$AUTH_JSON" ]; then + TOKEN="$(jq -r '.["openai-codex"].access // empty' "$AUTH_JSON" 2>/dev/null)" +fi +[ -n "$TOKEN" ] || TOKEN="${CODEX_ACCESS_TOKEN:-}" +[ -n "$TOKEN" ] || TOKEN="${OPENAI_CODEX_ACCESS_TOKEN:-}" + +if [ -z "$TOKEN" ]; then + printf '{"ok":false,"error":"no_token","detail":"nessun token Codex in %s"}\n' "$AUTH_JSON" + exit 0 +fi + +RESP="$(curl -sS --max-time 15 "https://chatgpt.com/backend-api/wham/usage" -H "Authorization: Bearer $TOKEN" 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 + +# Se il token è scaduto, l'API restituisce HTML di login invece di JSON. +if ! printf '%s' "$RESP" | jq -e '.rate_limit.primary_window.used_percent | type == "number"' >/dev/null 2>&1; then + printf '{"ok":false,"error":"token_expired","detail":"token Codex scaduto, riesegui /login in pi"}\n' + exit 0 +fi + +printf '%s' "$RESP" | jq -c '{ + ok: true, + plan_type: .plan_type, + remaining: ((100 - .rate_limit.primary_window.used_percent) / 100), + used_percent: .rate_limit.primary_window.used_percent, + reset_at: .rate_limit.primary_window.reset_at, + limit_window: .rate_limit.primary_window.limit_window_seconds, + credits: { balance: .credits.balance, has_credits: .credits.has_credits }, + spend_reached: .spend_control.reached, + limit_reached: .rate_limit.limit_reached +}' 2>/dev/null || printf '{"ok":false,"error":"bad_json","detail":"risposta non valida da wham/usage"}\n' + +exit 0 \ No newline at end of file diff --git a/contents/ui/configGeneral.qml b/contents/ui/configGeneral.qml new file mode 100644 index 0000000..c84319b --- /dev/null +++ b/contents/ui/configGeneral.qml @@ -0,0 +1,57 @@ +/* + * configGeneral.qml — pagina di configurazione del plasmoide OpenAI Codex Usage. + */ +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 + + 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 + } + + Item { Layout.fillHeight: true } + } +} \ No newline at end of file diff --git a/contents/ui/main.qml b/contents/ui/main.qml new file mode 100644 index 0000000..81ef6b8 --- /dev/null +++ b/contents/ui/main.qml @@ -0,0 +1,355 @@ +/* OpenAI Codex 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 data: null // { ok, plan_type, remaining, used_percent, reset_at, limit_window, credits, spend_reached, limit_reached } + 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 + + Plasmoid.title: i18n("OpenAI Codex") + Plasmoid.icon: "code-context" + Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground + toolTipMainText: i18n("Codex — utilizzo settimanale") + toolTipSubText: root.data ? i18n("Residuo: %1% · %2", + Math.round((root.data.remaining || 0) * 100), + root.data.plan_type || "—") : (root.errorMsg || "") + + preferredRepresentation: fullRepresentation + switchWidth: 150 + switchHeight: 150 + + Plasma5Support.DataSource { + id: executable + engine: "executable" + connectedSources: [] + + onNewData: function(sourceName, result) { + root.loading = false + executable.disconnectSource(sourceName) + if (result["exit code"] !== 0) { + root.errorMsg = i18n("comando fallito (exit %1)", result["exit code"]) + return + } + try { + const parsed = JSON.parse((result["stdout"] || "").trim()) + if (parsed && parsed.ok === false) { + root.errorMsg = parsed.error + (parsed.detail ? " — " + parsed.detail : "") + root.data = null + } else if (parsed && parsed.ok && parsed.remaining !== undefined) { + root.data = parsed + root.errorMsg = "" + root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss") + } else { + root.errorMsg = i18n("risposta inattesa") + root.data = null + } + } catch (e) { + root.errorMsg = i18n("JSON non valido: %1", e.message) + root.data = 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 remainingPct() { return root.data ? Math.round((root.data.remaining || 0) * 100) : 0 } + function usedPct() { return root.data ? Math.round(root.data.used_percent || 0) : 0 } + function accentColor() { + const pct = remainingPct() + if (pct <= root.critThreshold) return Kirigami.Theme.negativeTextColor + if (pct <= root.warnThreshold) return Kirigami.Theme.neutralTextColor + return "#7cb342" + } + + function resetMoment() { + const tick = root.clockTick + if (!root.data || !root.data.reset_at) return "—" + const d = new Date(root.data.reset_at * 1000) + return d.toLocaleDateString(Qt.locale(), "dd/MM") + " " + d.toLocaleTimeString(Qt.locale(), "HH:mm") + } + function resetRemaining() { + const tick = root.clockTick + if (!root.data || !root.data.reset_at) return "—" + let mins = Math.max(0, Math.floor((root.data.reset_at * 1000 - Date.now()) / 60000)) + if (mins === 0) return i18n("ora") + const days = Math.floor(mins / 1440) + mins -= days * 1440 + const hours = Math.floor(mins / 60) + mins -= hours * 60 + return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins) + } + + function windowLabel() { + if (!root.data || !root.data.limit_window) return "7D" + const secs = root.data.limit_window + if (secs >= 604800) return "7D" + if (secs >= 3600) return Math.round(secs / 3600) + "H" + return Math.round(secs / 60) + "M" + } + + 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 + } + + compactRepresentation: Component { + Item { + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + Kirigami.Icon { + anchors.fill: parent + source: Plasmoid.icon + color: root.data ? root.accentColor() : 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: 9 + spacing: 5 + + // header + RowLayout { + Layout.fillWidth: true + Kirigami.Icon { + source: Plasmoid.icon + color: "#7cb342" + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + } + ColumnLayout { + spacing: 0 + PC3.Label { + text: i18n("CODEX") + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1 + } + PC3.Label { + text: root.data ? (root.data.plan_type || "—").toUpperCase() : "—" + color: "#7cb342" + 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.49, 0.76, 0.26, 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) + } + + // barra principale + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 72 + radius: 6 + color: Qt.rgba(0, 0, 0, 0.26) + border.width: 1 + border.color: root.accentColor() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 7 + spacing: 3 + + RowLayout { + Layout.fillWidth: true + spacing: 5 + Rectangle { + Layout.preferredWidth: 24 + Layout.preferredHeight: 15 + radius: 8 + color: root.accentColor() + PC3.Label { + anchors.centerIn: parent + text: root.windowLabel() + color: "#101012" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 + } + } + PC3.Label { + text: i18n("Settimanale") + font.bold: true + Layout.fillWidth: true + } + PC3.Label { + text: root.remainingPct() + "%" + color: root.accentColor() + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize + 3 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 8 + Rectangle { + anchors.fill: parent + radius: 4 + color: Qt.rgba(0, 0, 0, 0.55) + } + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: parent.width * Math.max(0, Math.min(1, root.data ? root.data.remaining : 0)) + height: parent.height + radius: 4 + color: root.accentColor() + } + } + + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: i18n("%1% usato", root.usedPct()) + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.Label { + visible: Boolean(root.data && root.data.limit_reached) + text: i18n("✕ LIMITE") + color: Kirigami.Theme.negativeTextColor + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + } + } + + // reset + crediti + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 54 + radius: 5 + color: Qt.rgba(0, 0, 0, 0.28) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 5 + spacing: 2 + + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: i18n("RESET") + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: root.resetMoment() + color: root.accentColor() + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 + } + } + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: root.resetRemaining() + color: "#7cb342" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: root.data && root.data.credits ? i18n("Cred. %1", root.data.credits.balance || "0") : "" + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + RowLayout { + Layout.fillWidth: true + PC3.Label { + visible: Boolean(root.data && root.data.spend_reached) + text: i18n("Spend control raggiunto") + color: Kirigami.Theme.negativeTextColor + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + } + } + + RowLayout { + 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 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: root.data && root.data.plan_type ? root.data.plan_type.toUpperCase() : "" + color: Kirigami.Theme.disabledTextColor + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + } + } + } +} \ No newline at end of file diff --git a/dist/org.enne2.codex.usage.tar.gz b/dist/org.enne2.codex.usage.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..bbc102b1459e9b560c4a54492724419c121f3f6e GIT binary patch literal 4835 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 JrHKG|0043lcgp|( literal 0 HcmV?d00001 diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..34db869 --- /dev/null +++ b/metadata.json @@ -0,0 +1,16 @@ +{ + "KPackageStructure": "Plasma/Applet", + "KPlugin": { + "Id": "org.enne2.codex.usage", + "Name": "OpenAI Codex Usage", + "Name[it]": "Utilizzo OpenAI Codex", + "Description": "Shows the weekly usage allowance for OpenAI Codex (ChatGPT OAuth profile), with exact reset countdown, plan type, and credits", + "Description[it]": "Mostra il residuo settimanale di OpenAI Codex (profilo ChatGPT OAuth), con countdown esatto del reset, tipo piano e crediti", + "Icon": "code-context", + "Category": "System Information", + "License": "GPL-3.0", + "Version": "1.0.0", + "Authors": [ { "Name": "enne2" } ] + }, + "X-Plasma-API-Minimum-Version": "6.0" +} \ No newline at end of file