Repository navigation
plugins: add ZimaOS 0.1.16 - #205
leandrofelix-dev wants to merge 2 commits into
Conversation
🦊 mika's reviewSummary
Estimated review effort: 3/5 · risk: high 📁 Walkthrough details
|
| Severity | Category | Fix effort | File | Issue |
|---|---|---|---|---|
| 🟠 major | correctness | medium | plugins/zimaos/bin/zima |
Possible Bug — If an earlier authenticated request refreshes a rotating token and a later request returns 401 with a transient refresh failure, authed_get clears the in-memory tokens and this branch skips saving them. The old refresh… |
| 🟡 minor | correctness | medium | plugins/zimaos/bin/zima |
Possible Bug — The image cache key contains only the model path, not the server host. Switching between two servers with the same model reuses the first server's image, and a server without an image also inherits the old one; scope the… |
| 🟡 minor | correctness | low | plugins/zimaos/bin/zima |
Bug — Include s_code in the authentication check. A failed refresh during the storage request currently leaves authed true even though the session has been cleared. |
| 🟡 minor | correctness | medium | plugins/zimaos/content/Panel.qml |
Possible Bug — The storage card displays only the volume name and never uses storageLine, storageAvail, or storageFraction. After login, users cannot see the advertised free space even when the storage API returns it; render the… |
| 🟡 minor | correctness | medium | plugins/zimaos/content/Panel.qml |
Possible Bug — The service calculates downText and upText, but the panel never displays them. Consequently the advertised link rates are unavailable after login; add the calculated rates to the network section. |
| 🟡 minor | correctness | low | plugins/zimaos/service/Main.qml |
Bug — After useHost() sets activeHost, changes to the address in plugin settings are ignored for the rest of the service lifetime. Clear the override when savedHost changes so the configured address takes effect. |
⚡ Quick fixes by effort
Low effort (apply from the diff):
plugins/zimaos/service/Main.qml— Apply address changes from plugin settingsplugins/zimaos/bin/zima— Persist cleared expired session tokensplugins/zimaos/bin/zima— Detect storage request authentication failuresplugins/zimaos/bin/zima— Handle app-grid authentication failuresplugins/zimaos/service/Main.qml:18 — Bug — AfteruseHost()setsactiveHost, changes to the address in plugin settings are ignored for the rest of the service lifetime. Clear the override whensavedHostchanges so the configured address takes effect.plugins/zimaos/bin/zima:647 — Bug — Includes_codein the authentication check. A failed refresh during the storage request currently leavesauthedtrue even though the session has been cleared.
Medium effort (a function-level change):
plugins/zimaos/bin/zima:714 — Possible Bug — If an earlier authenticated request refreshes a rotating token and a later request returns 401 with a transient refresh failure,authed_getclears the in-memory tokens and this branch skips saving them. The old refresh…plugins/zimaos/content/Panel.qml:321 — Possible Bug — The storage card displays only the volume name and never usesstorageLine,storageAvail, orstorageFraction. After login, users cannot see the advertised free space even when the storage API returns it; render the…plugins/zimaos/content/Panel.qml:264 — Possible Bug — The service calculatesdownTextandupText, but the panel never displays them. Consequently the advertised link rates are unavailable after login; add the calculated rates to the network section.plugins/zimaos/bin/zima:664 — Possible Bug — The image cache key contains only the model path, not the server host. Switching between two servers with the same model reuses the first server's image, and a server without an image also inherits the old one; scope the…
🤖 Prompt for AI agents (click, copy, paste into Claude/Codex)
Fix the following issues found by mika-agent on Ryoku-dev/ryostore#205 (branch plugin/zimaos):
- plugins/zimaos/service/Main.qml:18-23 — Bug (minor severity, low effort): After `useHost()` sets `activeHost`, changes to the address in plugin settings are ignored for the rest of the service lifetime. Clear the override when `savedHost` changes so the configured address takes effect.
suggested fix:
readonly property string savedHost: {
var raw = settings && settings.host !== undefined && settings.host !== null
? ("" + settings.host).trim() : "";
return raw;
}
onSavedHostChanged: svc.activeHost = ""
readonly property string host: svc.activeHost.length > 0 ? svc.activeHost : svc.savedHost
- plugins/zimaos/bin/zima:97-101 — Security (major severity, medium effort): A bare address defaults to HTTP, so the login password and subsequent access and refresh tokens travel unencrypted. A network observer on the LAN can capture them; require HTTPS by default or obtain explicit informed con…
- plugins/zimaos/bin/zima:714-718 — Possible Bug (major severity, medium effort): If an earlier authenticated request refreshes a rotating token and a later request returns 401 with a transient refresh failure, `authed_get` clears the in-memory tokens and this branch skips saving them. The old refresh…
- plugins/zimaos/content/Panel.qml:321-335 — Possible Bug (minor severity, medium effort): The storage card displays only the volume name and never uses `storageLine`, `storageAvail`, or `storageFraction`. After login, users cannot see the advertised free space even when the storage API returns it; render the…
- plugins/zimaos/content/Panel.qml:264-279 — Possible Bug (minor severity, medium effort): The service calculates `downText` and `upText`, but the panel never displays them. Consequently the advertised link rates are unavailable after login; add the calculated rates to the network section.
- plugins/zimaos/bin/zima:664-670 — Possible Bug (minor severity, medium effort): The image cache key contains only the model path, not the server host. Switching between two servers with the same model reuses the first server's image, and a server without an image also inherits the old one; scope the…
- plugins/zimaos/bin/zima:647-653 — Bug (minor severity, low effort): Include `s_code` in the authentication check. A failed refresh during the storage request currently leaves `authed` true even though the session has been cleared.
suggested fix:
if u_code == 401 or n_code == 401 or c_code == 401 or s_code == 401:
result["authError"] = "session expired"
if session.get("access_token"):
save_session(path, session)
sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
return
result["authed"] = True
For each: verify it is real in the current code, apply the smallest correct fix, and run the repo's tests. Skip anything already resolved and say why.
🧪 No relevant tests detected in the diff.
🚦 Merge recommendation: changes_required
🦊 Generated by Mika — the Ryoku org's self-hosted AI code reviewer. Machine findings: verify before merging.
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 0 critical and 4 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
| def request_bytes(host, method, path, token=None, body=None, timeout=TIMEOUT): | ||
| url = "http://" + host + path | ||
| data = None | ||
| headers = {"Accept": "*/*"} | ||
| if body is not None: | ||
| data = json.dumps(body).encode("utf-8") | ||
| headers["Content-Type"] = "application/json" | ||
| if token: | ||
| headers["Authorization"] = token | ||
| req = urllib.request.Request(url, data=data, headers=headers, method=method) | ||
| try: | ||
| with urllib.request.urlopen(req, timeout=timeout) as res: | ||
| return res.status, res.read() |
There was a problem hiding this comment.
🟠 major · Security · fix: low effort
Disable redirects for API requests. urllib forwards the Authorization header across redirects, so a server or network attacker can redirect an authenticated request to another host and steal the access token.
All requests use plaintext HTTP, including the login password and authorization tokens. Anyone able to observe or modify traffic between the client and server can steal credentials or sessions; support verified HTTPS and require it for authenticated requests.
urlopen follows redirects, including cross-host redirects, with the Authorization header attached. A compromised server or network attacker can redirect an authenticated GET to an attacker-controlled host and receive the access token; reject redirects for API requests or validate their destination and strip credentials.
Suggested change
| def request_bytes(host, method, path, token=None, body=None, timeout=TIMEOUT): | |
| url = "http://" + host + path | |
| data = None | |
| headers = {"Accept": "*/*"} | |
| if body is not None: | |
| data = json.dumps(body).encode("utf-8") | |
| headers["Content-Type"] = "application/json" | |
| if token: | |
| headers["Authorization"] = token | |
| req = urllib.request.Request(url, data=data, headers=headers, method=method) | |
| try: | |
| with urllib.request.urlopen(req, timeout=timeout) as res: | |
| return res.status, res.read() | |
| def request_bytes(host, method, path, token=None, body=None, timeout=TIMEOUT): | |
| class NoRedirect(urllib.request.HTTPRedirectHandler): | |
| def redirect_request(self, req, fp, code, msg, headers, newurl): | |
| return None | |
| url = "http://" + host + path | |
| data = None | |
| headers = {"Accept": "*/*"} | |
| if body is not None: | |
| data = json.dumps(body).encode("utf-8") | |
| headers["Content-Type"] = "application/json" | |
| if token: | |
| headers["Authorization"] = token | |
| req = urllib.request.Request(url, data=data, headers=headers, method=method) | |
| try: | |
| with urllib.request.build_opener(NoRedirect()).open(req, timeout=timeout) as res: | |
| return res.status, res.read() |
| def save_session(path, data): | ||
| tmp = path + ".tmp" | ||
| with open(tmp, "w", encoding="utf-8") as handle: | ||
| json.dump(data, handle, ensure_ascii=False) | ||
| handle.write("\n") | ||
| os.chmod(tmp, 0o600) |
There was a problem hiding this comment.
🟠 major · Security · fix: low effort
The temporary session file is created using the process umask and only restricted after the tokens are written. If the existing state directory is traversable and the umask permits it, another local user can read the tokens during that window; create the file with mode 0600 before writing.
CWE: CWE-732
Reachability: A local user can traverse an existing plugin state directory while a login or status refresh writes session.json.tmp under a permissive umask.
| session = load_session(path) | ||
| result = { | ||
| "ok": True, | ||
| "authed": False, | ||
| "authError": "", | ||
| "username": session.get("username") or "", | ||
| "dashboardUrl": "http://" + host + "/", | ||
| "filesUrl": "http://" + host + "/modules/icewhale_files/", | ||
| "peerDropUrl": "http://" + host + "/modules/icewhale_peerdrop/", | ||
| "backupUrl": "", | ||
| } | ||
| result.update(device) | ||
| result["deviceImage"] = ensure_device_image(host, payload, path) | ||
| if not session.get("access_token") and not session.get("refresh_token"): | ||
| sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n") | ||
| return | ||
| u_code, u_payload = authed_get(host, "/v1/sys/utilization", session) |
There was a problem hiding this comment.
🟠 major · Security · fix: medium effort
The session is not associated with its server, so changing the configured host causes the next status request to send the previous server's access token to the new host. Bind sessions to the normalized host and discard or isolate them when it changes.
CWE: CWE-522
Reachability: A signed-in user changes the host setting to another server; refresh() invokes status, which loads the shared session and sends its token to that server.
| Card { | ||
| id: diskCard | ||
| visible: root.service && root.service.authed | ||
| width: parent.width | ||
| implicitHeight: 104 * root.s | ||
| color: diskMa.containsMouse ? root.fillHover : root.fillIdle | ||
| border.color: diskMa.containsMouse ? root.seal : root.sep | ||
|
|
||
| Text { | ||
| anchors.left: parent.left | ||
| anchors.top: parent.top | ||
| anchors.right: folderMark.left | ||
| anchors.leftMargin: 16 * root.s | ||
| anchors.topMargin: 14 * root.s | ||
| anchors.rightMargin: 8 * root.s | ||
| text: i18n.tr("Open") + " " + (root.service && root.service.storageTitle.length > 0 | ||
| ? root.service.storageTitle | ||
| : i18n.tr("storage")) |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
The panel never displays downText, upText, or storageLine, despite fetching those values and advertising link rates and free storage. Add these values to the authenticated card so users can see the promised measurements.
| "source": "assets/zima-mark.png", | ||
| "destination": "assets/zima-mark.png", | ||
| "sha256": "e67752d7533cee5453c398ca5aaf19ee25e17c4b36f5896904d771623997cd70", | ||
| "mode": "0644", | ||
| "size": 6014, | ||
| "install": false | ||
| }, | ||
| { | ||
| "source": "bin/__pycache__/zimacpython-314.pyc", | ||
| "destination": "bin/__pycache__/zimacpython-314.pyc", | ||
| "sha256": "f9fe0e66d22e1ae6899bcea7437ab16a35d82a94f54f59f2fa02801e82d2d027", | ||
| "mode": "0644", | ||
| "size": 32652, | ||
| "install": true | ||
| }, |
There was a problem hiding this comment.
🟠 major · Data Integrity · fix: low effort
The product manifest requires bin/__pycache__/zimacpython-314.pyc, but that file is absent from the PR and ignored by .gitignore. Catalogue validation or installation cannot verify and fetch the listed file; remove the cache entry and regenerate the manifest and registry checksum.
assets/zima-mark.png is marked install: false, although Widget.qml loads it for the bar glyph. Install the asset so catalogue installations do not render a blank widget.
| function refresh() { | ||
| if (!svc.hostReady || svc.binPath === "" || svc.stateFile === "") | ||
| return; | ||
| if (proc.running && proc.opKind === "status") | ||
| return; | ||
| for (var i = 0; i < svc._queue.length; i++) { | ||
| if (svc._queue[i].kind === "status") | ||
| return; |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
useHost() triggers onHostChanged, which can start a status request before login() queues the login. If that status request is still running when login succeeds, refresh() skips the post-login status, leaving the sign-in form visible until the next timer tick.
| svc.deviceName = parsed.deviceName ? ("" + parsed.deviceName) : svc.deviceName; | ||
| svc.osVersion = parsed.osVersion ? ("" + parsed.osVersion) : ""; | ||
| svc.deviceModel = parsed.deviceModel ? ("" + parsed.deviceModel) : ""; |
There was a problem hiding this comment.
💡 Suggestion (bug)
Clear stale device names
Suggested change
| svc.deviceName = parsed.deviceName ? ("" + parsed.deviceName) : svc.deviceName; | |
| svc.osVersion = parsed.osVersion ? ("" + parsed.osVersion) : ""; | |
| svc.deviceModel = parsed.deviceModel ? ("" + parsed.deviceModel) : ""; | |
| svc.deviceName = parsed.deviceName ? ("" + parsed.deviceName) : ""; | |
| svc.osVersion = parsed.osVersion ? ("" + parsed.osVersion) : ""; | |
| svc.deviceModel = parsed.deviceModel ? ("" + parsed.deviceModel) : ""; |
To apply: click the sparkletip, or ask @mika-agent ask <question>.
| svc.filesUrl = parsed.filesUrl ? ("" + parsed.filesUrl) : ""; | ||
| svc.backupUrl = parsed.backupUrl ? ("" + parsed.backupUrl) : ""; | ||
| svc.peerDropUrl = parsed.peerDropUrl ? ("" + parsed.peerDropUrl) : ""; |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
Clear deviceName when the response omits it. Retaining the previous name can identify a newly selected host as the old device.
Suggested change
| svc.filesUrl = parsed.filesUrl ? ("" + parsed.filesUrl) : ""; | |
| svc.backupUrl = parsed.backupUrl ? ("" + parsed.backupUrl) : ""; | |
| svc.peerDropUrl = parsed.peerDropUrl ? ("" + parsed.peerDropUrl) : ""; | |
| svc.deviceName = parsed.deviceName ? ("" + parsed.deviceName) : ""; | |
| svc.osVersion = parsed.osVersion ? ("" + parsed.osVersion) : ""; | |
| svc.deviceModel = parsed.deviceModel ? ("" + parsed.deviceModel) : ""; |
| if (disk) { | ||
| svc.storageName = disk.name ? ("" + disk.name) : ""; | ||
| svc.storageTotal = svc.num(disk.total); | ||
| svc.storageUsed = svc.num(disk.used); | ||
| svc.storageAvail = svc.num(disk.avail); | ||
| } |
There was a problem hiding this comment.
💡 Suggestion (bug)
Clear stale storage when unavailable
Suggested change
| if (disk) { | |
| svc.storageName = disk.name ? ("" + disk.name) : ""; | |
| svc.storageTotal = svc.num(disk.total); | |
| svc.storageUsed = svc.num(disk.used); | |
| svc.storageAvail = svc.num(disk.avail); | |
| } | |
| if (disk) { | |
| svc.storageName = disk.name ? ("" + disk.name) : ""; | |
| svc.storageTotal = svc.num(disk.total); | |
| svc.storageUsed = svc.num(disk.used); | |
| svc.storageAvail = svc.num(disk.avail); | |
| } else { | |
| svc.storageName = ""; | |
| svc.storageTotal = -1; | |
| svc.storageUsed = -1; | |
| svc.storageAvail = -1; | |
| } |
To apply: click the sparkletip, or ask @mika-agent ask <question>.
| function applyResult(kind, text) { | ||
| var parsed = null; | ||
| try { | ||
| parsed = JSON.parse((text || "").trim()); | ||
| } catch (e) { | ||
| parsed = { ok: false, error: "bad response" }; |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
Clear storage fields when a successful authenticated response contains no storage. Otherwise, a removed pool or a host without storage continues to show the previous pool's capacity.
Suggested change
| function applyResult(kind, text) { | |
| var parsed = null; | |
| try { | |
| parsed = JSON.parse((text || "").trim()); | |
| } catch (e) { | |
| parsed = { ok: false, error: "bad response" }; | |
| if (disk) { | |
| svc.storageName = disk.name ? ("" + disk.name) : ""; | |
| svc.storageTotal = svc.num(disk.total); | |
| svc.storageUsed = svc.num(disk.used); | |
| svc.storageAvail = svc.num(disk.avail); | |
| } else { | |
| svc.storageName = ""; | |
| svc.storageTotal = -1; | |
| svc.storageUsed = -1; | |
| svc.storageAvail = -1; | |
| } |
066d03a to
9ee299f
Compare
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 0 critical and 2 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
| g_code, g_payload = authed_get(host, "/v2/app_management/web/appgrid", session) | ||
| if g_code == 200: | ||
| result["backupUrl"] = find_backup(g_payload, host) |
There was a problem hiding this comment.
🟠 major · Possible Bug · fix: low effort
A token refreshed while fetching the app grid is never saved because the session is persisted before that request. If the server rotates refresh tokens and the access token expires at this point, the next poll uses the old tokens and can force the user to sign in again; persist the session after the final authenticated request.
| "source": "LICENSE", | ||
| "destination": "LICENSE", | ||
| "sha256": "97483adbcfff65229fdf21ea76178690f1bfe3ccd5fbde97e344e04d625dc4bd", | ||
| "mode": "0644", | ||
| "size": 1062, | ||
| "install": false |
There was a problem hiding this comment.
🟡 minor · Data Integrity · fix: low effort
The MIT license requires its copyright and permission notice to accompany copies or substantial portions of the software, but the product manifest excludes LICENSE from installation. Catalogue installations therefore distribute the plugin without that notice; install the license alongside the plugin.
| var n = Number(value); | ||
| return isFinite(n) ? n : -1; | ||
| } | ||
|
|
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
Number(null) returns zero, so missing metrics reported as null appear as real zero values. Treat null and undefined as unavailable before converting.
Suggested change
| var n = Number(value); | |
| return isFinite(n) ? n : -1; | |
| } | |
| function num(value) { | |
| if (value === null || value === undefined) | |
| return -1; | |
| var n = Number(value); | |
| return isFinite(n) ? n : -1; | |
| } |
| onHostChanged: { | ||
| svc._prevStamp = -1; | ||
| svc.downBps = 0; | ||
| svc.upBps = 0; | ||
| if (!svc.hostReady) { | ||
| svc.online = false; | ||
| svc.authed = false; | ||
| svc.deviceName = ""; | ||
| svc.deviceImage = ""; | ||
| svc.errorText = ""; | ||
| return; | ||
| } | ||
| svc.refresh(); |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
Changing the host while a status process is running drops the new refresh because refresh() returns for the old status operation. Its result is then applied without checking its host, so the panel can show the old server and links until the next poll, or longer if the new server is unreachable; discard stale results and queue a refresh for the new host.
9ee299f to
a92cad4
Compare
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 0 critical and 1 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
| def normalize_host(raw): | ||
| text = (raw or "").strip() | ||
| # A typed LAN host has no scheme and stays http. https:// is opt-in and | ||
| # keeps certificate verification. Anything else is rejected. |
There was a problem hiding this comment.
🟠 major · Security · fix: medium effort
Bare addresses default to HTTP, and the login request sends the password over that connection; subsequent requests also expose access and refresh tokens. A network observer on the LAN can capture these credentials. Require HTTPS for authentication, or make plaintext access an explicit, prominently warned opt-in.
CWE: CWE-319
Reachability: A user enters a bare IP address or hostname in the login panel and signs in while an attacker can observe the network.
| if u_code == 401 or n_code == 401 or c_code == 401: | ||
| result["authError"] = "session expired" | ||
| if session.get("access_token"): | ||
| save_session(path, session) |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: low effort
If token refresh fails, authed_get clears both tokens, but this branch only saves the session when an access token remains. The expired tokens therefore survive on disk and are retried on every poll, repeatedly attempting refresh instead of persisting the expired session. Save the cleared session when authentication expires.
Suggested change
| if u_code == 401 or n_code == 401 or c_code == 401: | |
| result["authError"] = "session expired" | |
| if session.get("access_token"): | |
| save_session(path, session) | |
| save_session(path, session) |
| readonly property string lang: { | ||
| var n = Qt.locale().name; | ||
| if (i18n.catalogs[n]) | ||
| return n; | ||
| var base = n.split("_")[0]; | ||
| return i18n.catalogs[base] ? base : "en"; | ||
| } | ||
|
|
||
| readonly property var catalogs: ({ |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
This private catalog only translates Portuguese and falls back to English for every other locale, bypassing the shell's I18n catalog. Users of other supported shell languages will see the new panel in English. Route these strings through the shared catalog and add the required source and English keys.
| property string activeHost: "" | ||
| readonly property string savedHost: { | ||
| var raw = settings && settings.host !== undefined && settings.host !== null | ||
| ? ("" + settings.host).trim() : ""; | ||
| return raw; | ||
| } | ||
| readonly property string host: svc.activeHost.length > 0 ? svc.activeHost : svc.savedHost |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
After a login from the panel, activeHost remains set, so later changes to the host setting are ignored by the host binding. The widget continues polling the old server until the service is restarted or another login changes activeHost. Keep the active host synchronized with settings changes.
The catalogue screen requires an https project URL, and plugin share does not copy one into the registry entry.
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 0 critical and 2 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
| if host.startswith("https://"): | ||
| return "https", host[len("https://") :] | ||
| if host.startswith("http://"): | ||
| return "http", host[len("http://") :] | ||
| return "http", host |
There was a problem hiding this comment.
🟠 major · Security · fix: medium effort
A bare address defaults to HTTP, so the login password and subsequent access and refresh tokens travel unencrypted. A network observer on the LAN can capture them; require HTTPS by default or obtain explicit informed consent before using HTTP.
CWE: CWE-319
Reachability: A user enters the suggested bare LAN IP and signs in; login and authenticated requests then use HTTP.
| if not isinstance(raw, str): | ||
| return "" | ||
| parsed = urllib.parse.urlparse(raw) | ||
| if parsed.scheme or parsed.netloc or parsed.path != "/v2/zimaos/device/image": | ||
| return "" | ||
| query = urllib.parse.parse_qs(parsed.query, keep_blank_values=False) | ||
| model = (query.get("model") or [""])[0] |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
Include s_code in the authentication check. A failed refresh during the storage request currently leaves authed true even though the session has been cleared.
Suggested change
| if not isinstance(raw, str): | |
| return "" | |
| parsed = urllib.parse.urlparse(raw) | |
| if parsed.scheme or parsed.netloc or parsed.path != "/v2/zimaos/device/image": | |
| return "" | |
| query = urllib.parse.parse_qs(parsed.query, keep_blank_values=False) | |
| model = (query.get("model") or [""])[0] | |
| if u_code == 401 or n_code == 401 or c_code == 401 or s_code == 401: | |
| result["authError"] = "session expired" | |
| if session.get("access_token"): | |
| save_session(path, session) | |
| sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n") | |
| return | |
| result["authed"] = True |
| if not rel: | ||
| return dest if os.path.isfile(dest) else "" | ||
| try: | ||
| if os.path.isfile(dest) and os.path.getsize(dest) > 32: | ||
| with open(stamp, "r", encoding="utf-8") as handle: | ||
| if handle.read().strip() == rel: | ||
| return dest |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
The image cache key contains only the model path, not the server host. Switching between two servers with the same model reuses the first server's image, and a server without an image also inherits the old one; scope the cache to the host and model.
Handle authentication failure on the final app-grid request as well. Otherwise, a failed token refresh there reports an authenticated session and leaves expired tokens on disk.
| Item { | ||
| width: parent.width | ||
| implicitHeight: Math.max(ipText.implicitHeight, dashBtn.height) | ||
|
|
||
| Text { | ||
| id: ipText | ||
| anchors.left: parent.left | ||
| anchors.right: dashBtn.left | ||
| anchors.rightMargin: 12 * root.s | ||
| anchors.verticalCenter: parent.verticalCenter | ||
| text: root.service && root.service.ipAddress.length > 0 ? root.service.ipAddress : "" | ||
| color: root.sumi | ||
| font.family: Theme.mono | ||
| font.pixelSize: 12 * root.s | ||
| elide: Text.ElideRight | ||
| } |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
The service calculates downText and upText, but the panel never displays them. Consequently the advertised link rates are unavailable after login; add the calculated rates to the network section.
What is this?
ZimaOS, a bar widget: QS Bar card for a ZimaOS server: device name, link rate, storage, and a shortcut to the dashboard.
Checklist
registry.json(lastUpdatedinYYYY-MM-DD;official: falsefor community items).ryoku plugin add).tests/validate-catalogue.shpasses from the repo root.Opened with
ryoku plugin share; the product manifest and the registry entry were generated from the installed plugin.