feat: initial Z.AI Tray Checker implementation #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/initial-implementation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Z.AI Tray Checker — Initial Implementation
A KDE Plasma system tray tool for monitoring Z.AI GLM Coding Plan usage.
What's included
api_client.py)peak_hours.py)config.py)~/.config/zai-tray-checker/settings.jsoncredentials.py)keyring(Secret Service API)main.py)QSystemTrayIcon+ popup panel with usage bars, peak banner, color-coded states.desktopautostart entry, systemd user unitFeatures
API endpoints used
Reverse-engineered from the zai-usage-tracker VS Code extension:
GET /api/monitor/usage/quota/limitGET /api/monitor/usage/model-usageAuth: bare token in
Authorizationheader.Tests
35 unit tests, all passing:
test_api_client.py(13 tests) — quota parsing, usage stats, auth errors, header format, fetch_alltest_peak_hours.py(14 tests) — peak detection, boundary conditions, midnight wrap, time-until-changetest_config.py(5 tests) — load/save, defaults, invalid JSON, API key never persistedHow to run
Roadmap (future PRs)
Notes
.desktopfile is inassets/— copy to~/.config/autostart/for autostartpython3-pyside6 python3-keyring python3-keyring-kwalletQThreadto avoid blocking the UIplease don't make up future release tags in the readme. In particular for things we haven't talked about.
One question though: The forgejo runner does have a python install. As long as you use
python3 -m venv .venv
. .venv/bin/activate
pip install
it will work. It is Debian's PEP-668 "externally-managed" Python, so a plain global pip install will error out but above should work. You could potentially run therefore the tests in the cicd here directly
🤖 Hermes automated review: minor comments
Reviewed base
ffc3e01→ head35648b1(19 files, +1669/−1). Ran the test suite locally: 35/35 passed in 0.14s. This is a solid initial implementation with notably good security hygiene for a credentials-handling app:Settings.save()explicitlypopsapi_keybefore serializing (config.py~line 43).eval/exec, nopickle, no shell injection, noos.system. Network calls useurllib.requestwith a timeout and structured error handling (api_client.py_request).peak_hours.pycorrectly handles timezone conversion, midnight-wrap ranges, and naive-vs-aware datetimes; well-tested.FetchWorker(QThread)keeps the Qt event loop responsive during network fetches.A few minor observations (non-blocking):
Minor
main.py_fetch_data(~line 470): each refresh creates a newFetchWorkerassigned toself._worker, overwriting any previous reference. If the user clicks "Refresh Now" while a fetch is in flight, the old worker keeps running and itsfinished_signal(still connected) will emit a staleUsageDataafter the newer one, causing the UI to briefly show outdated values. Consider guarding withif self._worker and self._worker.isRunning(): return(or disconnect the old signal andquit()/wait()the previous worker before starting a new one).FetchWorkerdefined inside the method —main.py~line 475: theQThreadsubclass and thefrom PySide6.QtCore import QThread, Signal as QSignalimport live inside_fetch_data, so the class is redefined on every fetch. Works fine, but hoisting the class to module scope (and the import to the top) is the conventional pattern and makes it easier to extend (e.g. error signal, cancellation).load_api_keyswallows all exceptions as "no key" —credentials.py~line 35:except Exception: return Nonemeans a genuinely broken keyring (missing backend, DBus error, etc.) is indistinguishable from "no key stored", and the app will silently prompt for re-entry. Acceptable for resilience in a tray app, but a debug-level log of the swallowed exception would help users diagnose why they're repeatedly asked for the key.fetch_usage_statsweekly window uses UTC end-of-day —api_client.py~line 140:end = now.replace(hour=23, minute=59, second=59, ...)is computed in UTC, thenstart = end - timedelta(days=days). Since Z.ai's quota window is UTC+8, the "7-day" boundary is off by up to 8 hours. Almost certainly immaterial for a weekly prompt/token tally, but worth noting if the numbers ever need to align with Z.ai's own dashboard.Settings.api_keyfield —config.py~line 26: the field exists on the persisted dataclass with a comment "Only used transiently; key stored in keyring." Sincesave()pops it, it's safe, but having a transient field on a@dataclassthat is alsoload()ed (viacls(**data)) is slightly surprising. If someone ever writes the key into settings JSON by another code path,load()would happily hydrate it. A property or a separate transient holder would be more self-documenting, but this is cosmetic.main.py(GUI) is understandably untested; the pure-logic modules (api_client,peak_hours,config) have good unit coverage. No blocking issues found.Automated daily review. I never merge PRs. Note: this is a PR conversation comment with file:line references, not a formal Forgejo review approval — the MCP integration cannot create inline review comments or approval states.
Thanks for the feedback! All addressed in
5ab1db5:README roadmap removed — you're right, I shouldn't make up version tags for things we haven't discussed. Stripped the entire Roadmap section.
CI workflow added —
.forgejo/workflows/ci.ymlrunspyteston every push/PR using the venv pattern you suggested:Uses
runs-on: docker(Debian runner). The tests are pure-logic (no display needed), so they should run cleanly.Also addressed the automated review's non-blocking items while I was at it:
_fetch_dataagainst stale concurrent fetches (if self._worker.isRunning(): return)FetchWorkerto module scope (was redefined on every call)logger.debug()toload_api_key/delete_api_keyexception pathsapi_keyfield fromSettingsdataclass entirely — it was never needed there, and nowload()also filters unknown keys for forward-compatibility36 tests, all passing.
🤖 Hermes automated review: no blocking issues found
Re-reviewed the fix commit
5ab1db5(baseffc3e01, delta from previous head35648b1). CI/CD: CI / test ✅ (13s) passed for head5ab1db5— local build/test skipped per CI policy.All items from the previous review and from @bjoern's feedback are properly addressed:
.forgejo/workflows/ci.yml) — uses the venv pattern Björn suggested; tests run cleanly on the Debian runnerif self._worker and self._worker.isRunning(): returnprevents race-condition UI updatesFetchWorkerhoisted to module scope — no longer redefined on every fetchlogger.debug()added toload_api_key/delete_api_keycatch pathsapi_keyfield removed fromSettings—load()now filters unknown keys for forward-compatibility; the dataclass no longer has a misleading transient fieldThe security posture remains solid: no hardcoded secrets, API key stays in the keyring, network calls use
urllibwith timeout and structured error handling.Minor (non-blocking)
.gitattributes:1—root = trueis not a valid git attribute — this is an EditorConfig directive. Git emits" is not a valid attribute name"on every operation. Theroot = trueline belongs in.editorconfig, not.gitattributes. Purely cosmetic (thetext=auto eol=lfand indent rules still work), but removing it silences the warning. (Note: this line was present in the previous commit too —5ab1db5did not introduce it.)No blocking issues found. This is not a formal Forgejo approval — the MCP integration cannot create review approval states.
Automated daily review. I never merge PRs.