Repository navigation
v2.5: simpler settings, less work per request, OOM protection, ECDSA repository - #64
Conversation
The toolbar polled /api/vm/gpio every 200 ms for as long as the desktop was open, although outside the popover it only renders the power-on glow and the HDD LED. Poll every 300 ms while the popover is open and every 3 s otherwise; opening the popover refreshes immediately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Screen toolbar item wrote /kvmapp/kvm/type (via POST /api/vm/screen type) on mount and on every local mode or codec change whenever the viewer was an administrator. That file is device-wide, so merely opening the page in a browser with a different local player choice silently changed the setting for everyone. Nothing depends on that implicit write: the Go server only writes the file (service/vm/screen.go) and never reads it back; the Direct, WebRTC and MJPEG streams pick their encoder from the stream that is opened and from the shared encoder state (POST /api/stream/state). Its only reader is kvm_system, which shows the type on the OLED and uses it to bucket the quality value. The video settings dialog already sends `type` in the batched POST /api/vm/video request when an administrator applies a different transport or codec (settingsRequest in lib/video-model.ts), which is the explicit path that should own this setting. Remove the effect and the now unused getScreenType helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RustDesk add-on kept its own en/ru label map and picked it with
startsWith("ru"), so every other language got English and the strings
were invisible to translators. Move them to settings.rustdesk.* in en.ts
and ru.ts, use i18next interpolation for the version, and drop the two
labels that were never rendered (installed, codec).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g blocks - One set of semantic colours (fg, fg-muted, line, surface, success, warning, danger, info) in lib/theme-tokens.ts for antd and in @theme for Tailwind utilities; a test keeps them equal. fg-muted is neutral-400, the lowest contrast meant for information. - Static message, Modal.confirm and notification calls render outside the ConfigProvider and used the default light theme; holderRender gives them the app theme. - components/ui/settings.tsx: SettingsPage, SettingsSection, SettingRow, Panel, StatusBadge and IconButton for the settings pages, which each hand-build these today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three dashboard section links used ids that no settings tab has:
'device-general' (the tab is 'device') and 'date-time' (the tab is
'system-date-time') opened an empty page, and 'network' only toggled the
sidebar group instead of opening the network page ('network-general').
Also fall back to the dashboard when a tab id is unknown or the tab is
no longer available (for example an extension that was uninstalled), so
a stale id can never render a blank settings page again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
encrypt() uses a single passphrase-based AES call, but importing the crypto-js root bundled every hash and cipher. crypto-js/aes pulls in only AES and its MD5/EvpKDF/Base64/OpenSSL-format dependencies, so the output format is unchanged; a test checks that the full library still decrypts it and that both imports are byte-identical for a fixed salt. encrypt chunk: 69.5 kB -> 17.7 kB (gzip 24.1 -> 6.0 kB). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frame detect is a device-wide encoder setting, but it had no source of truth: the server only pushed it into the native encoder (which starts with it off after every restart), any signed-in user could change it, and each browser remembered its own copy in localStorage (nano-kvm-frame-detect). The settings dialog could therefore show it as on while the device had it off, and the MJPEG player paused frame detect based on that stale local copy. Server: - Save the choice in /etc/kvm/frame_detect (like the other /etc/kvm settings) and apply it when KvmVision is initialized. - Return it as `frameDetect` from GET /api/vm/screen. - Move POST /api/stream/mjpeg/detect into the admin group; the temporary pause (/detect/stop) stays available to viewers, does nothing when frame detect is off and restores the saved choice afterwards instead of always re-enabling it. Web: - Read frameDetect from GET /api/vm/screen; applying the setting no longer writes localStorage, and the key's getter and setter are gone. - The MJPEG player always asks for the short pause and lets the server decide whether frame detect is on. Add a router test for the admin-only update and its persistence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The date and time settings promise that the 12/24-hour format is shared across the interface, including VPN handshake times, but useDeviceTime() was never called and timePreferencesAtom was read nowhere, so the format only affected the preview in the settings page. Wiring it is small, so do that rather than removing the promise: load the preferences once at the desktop root (re-polled every minute, and only replacing the atom value when it changes), show the handshake time in the device time zone and format as the tooltip of the relative age, and format the system log, boot and diagnostics timestamps with it instead of the browser's locale defaults. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In browsers the package only re-exports the native WebSocket through its w3cwebsocket shim (its constructor returns the native instance), so WsClient and the WebRTC signalling socket behave identically with WebSocket directly: same onopen/onclose/onerror/onmessage handlers, binaryType, reconnect and heartbeat logic. The shim and its es5-ext dependency leave the bundle (websocket chunk 7.5 kB -> 5.1 kB), and websocket/@types/websocket are removed with pnpm remove. @types/websocket was also what pulled Node types into the program, so the one Node-only test under src now references them explicitly. Tests that injected the shim now inject a WebSocket stub instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ManualRegion recomputed the centered input region every 250 ms and always stored a new object in inputRegionAtom, which the desktop root reads, so the whole desktop re-rendered four times a second while a manual region was selected. The region selector overlay did the same with its media size and frame rectangle. Keep the previous object when the geometry is unchanged (shallow comparison in isSameGeometry) and add a test for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop menu imports api/picoclaw.ts for the runtime status poll, which also re-exported the gateway helpers and so pulled the gateway client (lib/picoclaw-gateway.ts) into the desktop page for every user. Move the gateway helpers unchanged into api/picoclaw-gateway.ts, used only by the lazily loaded sidebar; api/picoclaw.ts keeps the REST calls. The gateway singleton has no constructor side effects, so creating it when the sidebar first loads changes nothing at runtime. Shared picoclaw chunk on the desktop page: 8.7 kB -> 2.4 kB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Route the remaining literal labels through i18next with en and ru keys:
the Extensions section and its load/empty states (previously chosen with
startsWith("ru")), Dashboard and Ethernet titles, dashboard ZRAM/SD swap
rows and the code-built fps, Mbit/s and MiB values, the preferred gateway
options, the Wi-Fi "not detected" note, the USB pointer profile default,
keyboard lock indicators (with their own keyboardDisabled key), the HDD
LED tooltip, image mount modes, the touchpad guide diagram label,
autostart table headers, the Wi-Fi setup password placeholder and the
NetBird/Tailscale English error fallbacks. Add the missing audio.resume
key. Virtual keyboard layouts are now listed by their native names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the 22 keys the Russian locale was missing (current password, password length, roles and the whole user management section) and translate values that were still copied from English: PicoClaw control, connection and start texts, Route Allowed IPs, the SSH banner style, the credits button and the USB gadget diagnostic label. Brand names, protocol names and placeholders remain identical on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The screen statically imported the MJPEG, WebRTC (H.264) and Direct players although a session uses exactly one (switching modes reloads the page). Each player is now its own chunk (mjpeg 1.6 kB, h264-direct 3.9 kB, h264-webrtc 8.8 kB). The desktop page fetches the chosen player alongside the encoder state and only then marks the encoder ready, so the player still mounts in the same commit as the mouse/region handlers that attach to its #screen element, and no fallback is shown. If that preload failed, the screen retries the import, renders nothing until it arrives and reports a second failure to the error boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 22 community locales still carried six settings.tailscale keys from the removed manual-install instructions. Remove them, and flag every locale that translates well under 90% of the English keys (all of them except en and ru, at about 42%) as partial. The language selector shows such languages with a translated "partial" note, since half of their interface falls back to English. Danish, Italian and Indonesian are listed by their native names like the other languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Many actions only logged a failure with console.log or had no rejection handler at all, so the user saw nothing (or an unhandled rejection in the console) when a script upload, image mount or delete, Mouse Jiggler, hostname, web title, mDNS, reboot, keyboard shortcut or Tailscale restart/stop/uninstall request failed; the HDMI idle timeout silently reverted, and a failed HDMI state read left the switch spinning. Add lib/request-error.ts, which recognizes the failures the shared HTTP client already handles (aborted requests, 401 session expiry, the forced password change redirect) and builds the text to show: a localized lead plus the server's message as detail. showRequestError() shows it with the antd message API, as the settings pages already do, and takes a translation key so loaders called from mount effects need no `t` dependency. Wake-on-LAN keeps reporting in its own status line, the image mount keeps its existing notification, and the background leader key read now just keeps the current key on failure. The HTTP client now logs only unexpected errors rather than every aborted or 401 request. The ISO upload keeps fetch() for its progress handling but no longer assumes a JSON body: a 413 or other non-JSON error page now shows "Download failed (HTTP 413)" instead of a JSON parse error, and a 401 signals session expiry like the shared client. Add error.requestFailed (en, ru) and tests for the error classification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/i18n-keys.test.mjs loads every locale and fails when a locale has keys that en.ts does not define, when a language without the partial flag misses an English key, when a translation interpolates a value English does not pass, or when languages.ts and the locale files disagree. Plural suffixes are compared per language. CI already runs tests/*.test.mjs; pnpm test:i18n runs the check alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The password page gave no reason when it was forced on a factory
password, returned to the login page without saying the change worked,
and replaced the server's validation message (code -5, e.g. a password
that is too weak) with "Unexpected error". Cancel silently logged out
on a forced change, and otherwise reloaded the whole application.
The HTTP client handled the server's "password change required" answer
by setting the URL hash directly. The route guard's account still said
no change was required, so the page opened in its non-forced form and
Cancel led straight back to a page whose requests bounced to it again.
- Show auth.changePasswordDesc when the change is forced, and a success
message after the change before returning to the login page.
- Show the server's message for code -5.
- The client now raises a password-change-required event; the route
guard marks the account and redirects, so the page knows it is forced.
- A forced Cancel is labelled "Cancel and log out"; otherwise Cancel
goes back in the router history (or to the desktop) without a reload.
- The tips card said the change also resets the root/SSH password for
every administrator. The server does that only for the built-in
(system) admin account (service/auth/password.go), and GET
/api/auth/account does not say which account that is, so reword the
tip to name the built-in admin account (en, ru).
- Drop initialValues={{ remember: true }} from the login and password
forms, which have no such field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hotspot setup page showed the success text after any exception and
after unknown response codes. Only code 0 is a success now; an answer
from the device with another code is a failure. A request that fails
without any response is expected while the device leaves the hotspot to
join the network, so it shows a neutral "connecting, the hotspot may
disconnect" text (en, ru) instead of claiming success or failure.
The "Finished" button that replaced the submit button after success had
no action; remove it and keep the submit button, so a wrong network can
be corrected. The password placeholder was a hard-coded English string
and now uses auth.placeholderPassword. Drop the stray
initialValues={{ remember: true }} from both forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The terminal page reacted only to close code 4401 (session expired). Any other close (the shell exited, the server restarted, the network dropped, the WebSocket never connected) left a silent, dead terminal. Write a visible "Connection closed. Press Enter to reconnect." line (en, ru) into the terminal on any other close that the page did not initiate itself; Enter reloads the page, which reconnects with the same serial parameters because they are kept in sessionStorage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use sentence case for labels, buttons and titles, "OK" instead of "Ok", the single ellipsis character, "Sign in"/"Sign out" for the account actions (key names unchanged), lowercase "fps" in prose and labels and "HID-only" and "leader key" as common nouns. Remove the unused screen.videoDirectTips string, which pointed to the wrong settings page, from all locales. In Russian, use the ellipsis character and Cyrillic "ОК" throughout, describe frame rate in words and fix the leader-key recorder prompt that called a shortcut a "ярлык". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop mounted a Notification that asked GET /api/auth/password whether the password was still the factory one and then offered to change it (dismissable for three days via the nano-kvm-skip-modify-password key). It can never appear: that state is the account's mustChangePassword flag, and ProtectedRoute redirects such an account to /auth/password before the desktop mounts; while the flag is set the server answers every other request with the "password change required" code, which now also sends the route guard to the password page. Remove the component, its mount, the web isPasswordUpdated() helper, the skip key's getter/setter and the expiry helpers only it used. The server endpoint stays for API compatibility. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The toolbar Settings button is shown to everyone, but the modal body (navigation, page loaders, VPN/extension icons, the Radix scroll area) was part of the desktop chunk. Move the modal JSX, tab list and titles unchanged into settings/dialog.tsx and load it lazily the first time the modal opens (by click or by a settings request); it stays mounted afterwards so the close animation is kept. All state, the addon inventory polling and the open/close/lock logic stay in the button component, and the chunk is prefetched when the button is hovered or focused. Desktop chunk: 284 kB -> 251 kB (dialog chunk: 23 kB). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Capture, USB, Image, Download, Terminal, Script and PicoClaw are only rendered for admins, yet their code (including the USB settings page, download progress UI and script runner) was in the desktop chunk for every user. They now come from one lazily loaded chunk (admin-items, 47 kB) that non-admin sessions never request. Order, keys and grouping are unchanged; while the chunk loads each item keeps its 30x30 slot, and Image (which renders nothing until the virtual-disk state is known) keeps an empty one, so the toolbar does not shift. Desktop chunk: 251 kB -> 202 kB; JS loaded by the desktop page for a non-admin (WebRTC player): 1484 kB -> 1416 kB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Translating the NetBird and Tailscale status fallback inside getStatus made the callback depend on t, so switching the language started a new status request (caught by effect-lifecycle.test.mjs). Store the fallback as its key and translate it when rendering instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # web/src/i18n/locales/en.ts # web/src/pages/desktop/menu/power/index.tsx
public/mockServiceWorker.js (12.8 kB) was copied into every production build although only "vite --mode mocked" registers it. Move it to msw/ (also the msw.workerDirectory, so msw keeps it updated on install) and add a small Vite plugin, enabled only in mocked mode, that serves it from the dev server and emits it for "vite build --mode mocked". Checked: "pnpm run mocked" serves /mockServiceWorker.js byte-identical to the file, and a production build no longer contains it. The vite.config.ts import order now follows the repository Prettier configuration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nanokvm-os.svg, nanokvm-os-screen.svg and nanokvm-os-screen-full.svg are not referenced by index.html, the web sources, the server or the scripts; only the favicon nanokvm-os-connection*.svg files are used. The next recorded platform outputs (platform/expected.sha256) will no longer list these files under web/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the lazy-loading changes the largest chunks are the terminal page (xterm, 337 kB), the shared React DOM/antd core chunk (297 kB) and the PicoClaw sidebar (256 kB), all below Vite's default 500 kB limit, so the raised 1024 kB limit is dropped and the build reports no warnings. manualChunks was measured and not added. A react/react-dom/react-router vendor chunk changes nothing (Rolldown already keeps them in app-free chunks). Adding antd/rc-component to a vendor chunk grows the JS+CSS loaded by the login page from 1071 kB to 1424 kB and by the desktop page (non-admin, WebRTC) from 1484 kB to 1629 kB, while saving only 53 kB of re-downloads after a small app-only change (331 kB -> 278 kB). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OS and Software updates, Software page queries, VPN add-on installs and the RustDesk service now queue behind each other, also across the detached update helper. Software page queries wait up to 12 s for another run and then keep their former time limits, so they stay below the browser timeout. RustDesk keeps its own mutex, which also covers its apk state check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VPN list asks for the tool versions every 5 s and each provider page every 60 s. The answer was kept for a minute, after which every installed tool ran with --version again (wg: 10 ms from a Go process on the device; the Go-based tailscale and netbird clients take far longer, a 4 MiB Go program already needs 130 ms, 730 ms with a cold page cache). A version only changes when apk replaces the program, so the output is now kept per executable until its inode, size or modification time changes. A failed run is still retried after a minute, and concurrent requests share one run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VPN list polls the NetBird status every 5 s and the dashboard every 30 s. With NetBird installed but stopped, each poll ran rc-service netbird status (0.45 s on the device for an existing service) and then netbird version, a Go program. As for RustDesk, a service without its /run/openrc/started link is stopped and rc-service now runs only for a started service, which may have crashed. The installed version is kept until the netbird program is replaced. A running daemon is still asked with rc-service and netbird status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VPN list polls the Tailscale status every 5 s and the dashboard every 30 s, and each poll started tailscale status --json. The client is a large Go program; on the device a 4 MiB Go program already takes 130 ms to start, 730 ms with a cold page cache, and the client's memory competes with video on a 104 MiB system. The status now comes from tailscaled's local API on the socket the client itself uses, GET /localapi/v0/status, which is the document the client prints. With nothing listening on the socket the client would fail the same way, so the daemon is reported as not running directly. Any other failure, such as a refused request or an unreadable answer, falls back to the client as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InitGoMemLimit was never called, so a saved /etc/kvm/GOMEMLIMIT only took effect when set from the web page and was lost at the next start. Apply it at start, and without a saved value limit the Go-managed memory to a quarter of MemTotal (26 MiB in the 4K video memory mode, above the measured 22 MiB peak of the whole process anonymous memory). A GOMEMLIMIT environment value replaces the default; turning the saved limit off returns to the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NanoKVM-Server, which also owns video capture, sets oom_score_adj -300 for itself at start, so it works for the OpenRC service, the legacy init script and manual starts alike; S95nanokvm sets the same for kvm_system. -300 is about 47 MiB in the 4K video memory mode: the server outlives other processes in its normal footprint but can still be killed if it runs away. Web terminal shells and the nkos-apply-updates worker, which restarts services after apk, reset the value to 0 so that what they start is not protected or preferred. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NanoKVM-Server lowers its own oom_score_adj to -300, and children inherit it: sshd, avahi, chrony, OpenVPN and the Tailscale, NetBird and RustDesk daemons started from settings would be protected like the server. Those starts now reset the value to 0 before exec, through a shell wrapper for rc-service and init scripts (so what they spawn starts at 0 too) and right after start for openvpn, which runs no scripts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without -k the packages and rootfs steps created a key in OUTPUT/keys and made the image trust it. apk does not tie a key to a repository, so such an image trusted a throwaway key, whose private half stays on the build machine, for every repository; a release built without -k would ship that. Those steps now require -k with the release key, or -d for a local test key, named nanokvm-test-*, with a warning not to publish. The check runs before the first step, not after a long build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The packages and rootfs steps run build.sh again in a user namespace, which passed -k but not -d, so a -d build stopped there with "APK signing key required". -d also took any .rsa in OUTPUT/keys, such as a key left by an earlier build or copied there; it now uses only the nanokvm-test-* key that it creates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Devices trust the keys in /etc/apk/keys, but no package owned the NanoKVM release key there: the rootfs step and manual setup copied it. An update could therefore neither add a new key nor remove an old one. nanokvm-keys installs firmware/alpine/keys in /etc/apk/keys, as alpine-keys does for Alpine; its package() accepts only public keys there. nanokvm-base depends on it, as alpine-base depends on alpine-keys, and nanokvm-release lists it with the tested version. build-alpine-packages.sh builds it first and falls back to firmware/alpine/keys when the payload tree has no keys/. The rootfs step still copies the keys before apk add nanokvm-release, since apk must trust the repository first. /etc/apk is not a protected path, so apk takes over the identical file without a conflict or an .apk-new, also on devices that already have the unowned file; the step now fails if any file in /etc/apk/keys has no owner. With -d the packages step adds the test key to nanokvm-keys, so a test image owns the key it trusts. -k must name a key whose public half is in firmware/alpine/keys under the same name, else devices that update would not trust the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abuild calls abuild-sign without -t, and abuild-sign's default type RSA is RSA with SHA-1 (.SIGN.RSA.<key>). abuild has no setting for the type, so build-alpine-packages.sh and build-alpine-tuned-packages.sh put a small abuild-sign wrapper first in PATH that adds -t RSA256: packages and APKINDEX.tar.gz get .SIGN.RSA256.<key>, RSA with SHA-256. Both scripts now refuse a package or index without such a signature. qualify-alpine-c906-repo.py signs its regenerated APKINDEX the same way. apk-tools 3.0.8 on the device accepts RSA256 package and index signatures from a trusted key, and rejects them from an untrusted key or with a changed signature. apk mkndx already signs Packages.adb with SHA-512 and needs no change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The maintainer's new release key for the apk-tools v3 index (Packages.adb): an ECDSA P-256 public key, SHA-256 of its DER b8e89b667c3ffeb5acb14e45779d74eaf0423205f27d7d904f36af81a50269bf. nanokvm-keys ships it next to dgrace-6aaddbb6.rsa.pub, so devices trust it once they install the next release, and the rootfs step installs it for the bootstrap. nanokvm-keys and both key copies now take any *.pub, not only *.rsa.pub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The packages step now also writes Packages.adb, the apk-tools v3 index, next to APKINDEX.tar.gz with apk mkndx in the builder. It is signed with the ECDSA release key given by the new option -e and with the RSA key of -k, so that devices which trust either key accept it during the transition; the step checks that each key alone verifies it. The v2 APKINDEX stays signed with the RSA key (RSA256), so 2.0 devices can still read it, and the packages stay ordinary abuild packages. Release builds need both -k and -e, checked before the first step like -k: -e NAME.key needs NAME.pub next to it, an ECDSA P-256 or P-384 key that is one of firmware/alpine/keys. With -d the build creates a test key nanokvm-test-ec-*.key as well, puts it in nanokvm-keys and makes the image trust it. The rootfs step installs from the v3 index, and the image's NanoKVM repository line is now https://nkos.pesin.pro/repos/nanokvm/riscv64/Packages.adb. Alpine's lines are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No package manages /etc/apk/repositories, so devices that update keep the v2 line https://nkos.pesin.pro/repos/nanokvm. nanokvm-keys is new to them, so its post-install runs once on the update that brings the release keys, after its files, the ECDSA key among them, are in place. It replaces that one exact line (trailing slash and spaces allowed) with https://nkos.pesin.pro/repos/nanokvm/riscv64/Packages.adb and leaves every other line, Alpine's and tagged ones included, as it is. A failure keeps the old line, which still works. The new apk settings take effect with the next apk run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build-alpine-tuned-packages.sh (EC_SIGN_KEY) and qualify-alpine-c906-repo.py (--ec-sign-key or EC_SIGN_KEY) take the ECDSA release key, NAME.key with NAME.pub next to it, and pass it to apk mkndx as a second --sign-key. Packages.adb then carries an RSA and an ECDSA signature, and both scripts check that each key alone verifies it. Without the EC key they sign with the RSA key only, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build-alpine-personal-image.sh turns the NanoKVM and C906 repository base URLs into their v3 index URL, BASE/riscv64/Packages.adb, for the build and for the image's /etc/apk/repositories; a URL that already ends in .adb is kept. Alpine's lines are unchanged. The personal builder server requires riscv64/Packages.adb in its repositories and keys a build by that index. build-alpine-builder-deploy takes --repo-key more than once, for the RSA and the ECDSA key, and requires the v3 index; the example configuration lists both keys. migrate-alpine-stock.sh also removes a C906 line in the v3 form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
firmware/alpine/README.md gets a Signing keys section: which key signs what, how nanokvm-keys owns the keys and takes over the copies of the bootstrap, the v2 and v3 indexes and how devices move to the v3 index, how to publish a release (build with -k and -e, upload riscv64/ with both indexes last), how to move to the ECDSA key alone while 2.0 devices still update, and how to replace a key: ship it signed by the old key, sign with the new key (both during the transition), then drop the old key. platform/README.md describes -e and the v3 index, and the attended and tuning READMEs the second key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web UI and NanoKVM-Server changed in this branch (chunking, precompressed .gz assets, live status, faster status handlers, apk runner, OOM and certificate changes). Only the web/ and server/ entries are replaced: two consecutive builds produced identical outputs. Kernel, boot, native and firmware outputs are unchanged by this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Reviewed head
CodeQL: the failing security check points at Validation performed here: targeted Go suites for the APK runner, OOM scores, tool cache, VM, network, RustDesk and memory utilities pass. The router suite fails only on the added missing-asset regression reproducer. Eight focused web tests for encryption, shared polling and settings aliases pass. No deployment, reboot, or device changes were made. Release integration note: the prepared v2.5-a1 release branch already contains the signing/index commits plus USB NCM Internet sharing and separate image/application versioning. Preserve those additions when integrating this PR, and regenerate final output checksums after the combined server/web build instead of replacing the release manifest wholesale. |
Editing these files through the Windows file share dropped their executable bit, so ./platform/build.sh failed with Permission denied. They get back the modes they have on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The year-long immutable Cache-Control was set before checking the file, so a 404 for a hashed asset (during a package replacement or an incomplete deployment) carried it and could stick in browsers after the file arrived. The headers are now set only when the asset exists; a missing one, even with a stray .gz, is an ordinary uncached 404. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks. Both regressions are fixed in [P1] Executable modes. The bits were lost because these files were edited through the Windows share ( [P2] Cache policy on missing assets. In CodeQL alert #50 (
|
|
Rechecked head Both review findings are resolved:
The CodeQL explanation correctly distinguishes the reversible client-side wrapper from HTTPS transport protection and server-side bcrypt. All current GitHub checks are green. No remaining blockers from my review. No device deployment or reboot was performed. When integrating into the prepared release, retain USB NCM Internet sharing and separate image/application versions, and rebuild the combined server/web artifacts and checksums. |
Fixes and optimisation for 2.5: a simpler settings UI, far less work per request on the device, protection against running out of memory, and an ECDSA-signed package repository. No version bump.
Settings UI
VPN / <provider>with a back arrow. Oldnetwork-*/vpn-*ids and dashboard links still work.components/ui/settings.tsx,lib/theme-tokens.ts) on every settings page: 58 → 13 distinct raw colour classes, 16 → 3 card variants.Performance (measured on the device)
GET /api/vm/live-status).gzassets served with year-long immutable caching for/assets.wpa_cli/iw;/proc/net/route;wg show dump;ip;/run/openrc/started;Reliability
internal/apkrun): a cross-process lock under /run, low priority for index loads, and a memory check that refuses a run that cannot fit (one index load peaks at ~40 MiB of the ~104 MiB Linux has in 4K mode).oom_score_adj1000); NanoKVM-Server is protected (-300); daemons the server starts reset to 0.APK trust and signing
platform/build.shrequires a chosen key:-k(RSA) and-e(ECDSA) for releases, or-dfor a local test key. It no longer silently creates and trusts a throwaway key.New
nanokvm-keyspackage owns the release public keys (dgrace-6aaddbb6.rsa.pub,nkos-release-ec-b8e89b66.pub);nanokvm-basedepends on it, so keys can be rotated by updates.Signatures:
APKINDEX.tar.gzare signed with RSA-SHA256 instead of SHA-1;Packages.adbindex is dual-signed (RSA + ECDSA);Device migration: on install,
nanokvm-keysmoves the NanoKVM repository line to…/riscv64/Packages.adb. 2.0 devices receive 2.5 through the RSA-signed v2 index and then switch.Verified on apk-tools 3.0.8 in scratch roots:
Publishing 2.5:
platform/build.sh -k …/dgrace-6aaddbb6.rsa -e …/nkos-release-ec-b8e89b66.key.OUTPUT/release/apk/recipes/riscv64/*tohttps://nkos.pesin.pro/repos/nanokvm/riscv64/, packages first and both indexes last.Details are in
firmware/alpine/README.md#signing-keys.Testing
teststub(49 packages), gofmt clean;pnpm build, lint, 168 tests;scripts/test-*.py,firmware/alpine/tests, tools tests.platform/expected.sha256: only theweb/andserver/entries are updated. Two consecutive builds were identical; kernel, boot, native and firmware outputs are unchanged by this branch.Not covered
kvm_system-300 setting inS95nanokvmtakes effect once installed from packages.nanokvm-rustdeskadd-on.main, and CI does not check Prettier.🤖 Generated with Claude Code