Use per-codec capture locks and encoder backpressure, simplify frame ownership, and follow HDMI input dimensions. Refresh prebuilt video libraries while preserving server-only builds and deployment instructions.
* feat(auth): add fine-grained feature permissions for multi-user access
Introduce a grantable permission catalog on top of the admin/user roles:
administrators keep implicit full access, while regular users receive an
explicit permission set covering console view/control, power, media
mount/manage, terminal, device, network, update, mcp, tailscale and ai
feature groups. Media access splits along the trust boundary: mounting
library media and reading it is media.mount, while changing library
contents (uploads, deletes, file exchange with the target) is media.manage.
Server:
- store permissions on the account record; nil keeps the pre-permission
user behavior (console view/control and power) so upgrades are seamless
- add RequirePermission middleware and map every route to its group;
user management stays admin-only
- require console.view in the grant set a regular user ends up with,
including when a role change alone would leave an account without it,
since nothing in the app renders without that grant
- let a user who may mount media read the library and follow an image
download, while starting, cancelling or deleting stays media.manage
- scope console.control to input: keyboard, mouse, paste, and the input
region that maps pointer motion onto the target screen. Video encoding,
quality, resolution and capture recovery need only console.view, since
every account holds it and a watcher still has to pick a quality its
link can carry
- websocket console drops keyboard/mouse reports for view-only sessions
- permission changes keep the login valid: TokenVersion is only bumped
for role/enabled changes, a grant that changes a long-lived
connection's cached state drops that channel with a dedicated 4403
close code so the client reconnects with fresh permissions, and the
frontend is notified over the console websocket to refresh in place;
a rejected request also triggers a silent account refresh
- document the catalog, the media split, the input-only scope of
console.control and the grants that reach the whole device (terminal,
device, update, mcp, ai) in server/README.md and README_ZH.md
- warn in the account editor that mcp hands out a device-wide API key
which keeps working after the grant is revoked, the same way the ai
grant is flagged for reaching the device outside the other grants
Web:
- account endpoint returns effective permissions; menus, settings tabs,
storage page actions and editors render per permission instead of the
isAdmin flag, and the terminal and storage routes are guarded by the
matching grant
- a session without console.control stops capturing the pointer and the
keyboard, hides the input-region editor and its stale-region cleanup,
and marks the console as view only, mirroring the server that drops
those reports; the client also gates a report already in flight
- user management gains a grouped permission editor with the console-view
grant pinned, and warning hints on the mcp and ai grants; rows use
aligned icon actions with tooltips
- every page refreshes the account when its grants change: the console
socket handles the auth event itself, pages without a socket re-read on
route change and when they become visible, and the ai sidebar closes
once the grant is gone
- the terminal, the WebRTC viewer and the console all honor the 4403
permission-change close instead of stranding the page or the login
Localization:
- ship every string this feature adds, including the permission editor,
the view-only badge and the mcp/ai warnings, in all 24 supported
locales
Verified with go vet, go test for authn/middleware/service auth, tsc
--noEmit, eslint, prettier, and a key-by-key comparison of every locale
against en.ts (694 keys, none missing).
* fix(auth): keep the emulated USB devices under the device grant
The virtual disk and the RNDIS adapter toggle the emulated USB gadget in
Settings > Device: the disk hands the target machine the raw data
partition, the adapter hands it a network interface. Neither is a media
or a network operation, so both follow `device` instead of media.mount
and network.
- route /vm/device/virtual through `device` only, so no handler has to
narrow a union to the device a request touches
- render both switches wherever the device settings are visible, and
read the medium state, stop a transfer, or pick the disk warning only
with the matching media grant
- the storage page offers to enable the virtual disk only to accounts
that may toggle it; the others are told which account has to do it,
with the new string added in every locale
- document that attaching a medium needs the virtual disk, and that
`device` can hand out the raw data partition and a network interface
Verified with go vet, go test for authn/middleware/service auth, tsc
--noEmit, eslint, prettier, and a key-by-key comparison of every locale
against en.ts (695 keys, none missing).
* fix(auth): repair the permission editor and the refresh path
The permission editor and the code around it shipped with several defects:
- the editor kept its field values after the modal was destroyed (antd forms
preserve values by default), so opening it for a second account showed the
previous account's grants and saving wrote them onto that account. The
selection now lives in component state and is seeded when the editor opens,
so a cancelled edit is dropped as well
- a save closed the dialog even when the server refused the change, and the
reason was replaced by a generic message. updateUser reports success now and
the server's own refusal (an omitted console.view, an invalid role, ...) is
shown next to the failure
- a refused virtual-disk toggle in Settings > Device only reached the console.
A device holder without media.manage cannot cancel the transfer that holds
/data, so that refusal is expected and now says why the switch did not move
- losing the grant behind an open settings tab left an empty panel, and a
vanished update tab could keep the modal locked and unclosable
- a role change bumped the token version and logged the account out mid
session, while a permission change did not. Only disabling an account
invalidates the login now: a role change resolves to its implicit grants and
takes the same notify-and-reconnect path as a grant edit, so a promoted or
demoted account keeps working and simply reconnects with new capabilities
- the console offered a mouse menu whose items only shape input, and mounted
the on-screen keyboard, for accounts that cannot send input at all
- the account rows stopped wrapping on narrow screens and the icon-only
actions had no accessible name
- the editor groups grants by what they reach, with media.mount next to
media.manage instead of under the console grants, and gained a title, a
Save/Cancel pair and a localized Media section
Server side, authn.Has replaces three copies of the same lookup, the
permission-aware channel map documents that console.view is pinned, and the
HDMI reset keeps its console.view grant with the reasoning written down.
The documentation follows: a permission change no longer claims to interrupt
the video stream, HDMI reset is listed with the view operations, and only
disabling an account invalidates a login.
* feat(auth): give the service grants their own permission group
mcp, tailscale and ai reach the device on their own — an API key outside the
web session, a VPN mesh, an agent driving the local loopback interface —
instead of through the settings the remaining grants cover, so they no longer
share the Administration section with terminal, device, network and update.
The editor renders them as a Services section between Media and
Administration, and the label ships in all 24 locales.
* refactor(auth): model grants as a catalog with implied grants
Replace the media-aware flat grant list with a catalog the server serves to the
account editor: each grant carries a group, a risk level and the grants it
implies, so labels, sections and risk marks cannot drift from what the server
enforces.
Server:
- resolve implied grants before answering a request, which removes the
route-level unions: console.control implies console.view, and both
medium.attach and library.write imply library.read
- split media access three ways along the trust boundary: medium.attach mounts,
ejects and writes into the shared partition the target machine reads,
library.write changes what the library keeps, and library.read lists and
downloads. The file endpoints serve an image and the partition through one
path, so they admit either grant and check the medium type in the request
- stop pinning console.view: with control implying view, an account may hold no
console access at all, and the web app lands on a page that offers the
sections its grants open
- materialize the grant set when the account file is read, so a record written
before fine-grained permissions keeps exactly what the user role granted, and
nil no longer means different behavior later; the split media grants are
translated on load
- keep grants this build does not know instead of refusing the file, so a
downgrade cannot lock the device out
- serve the stored set to the account list and the effective closure to the
account endpoint
Web:
- render the editor from the catalog: grouped sections, a per-grant tooltip with
a risk marker, implied grants shown as included, and the grants that amount to
full device control summarized in one line
- show an administrator's stored set read-only with what a demotion restores,
and refuse to edit while the catalog cannot be loaded
- guard the terminal, storage and console routes and the in-page actions that
were gated by the isAdmin flag by the matching grant instead
- add a landing page for an account without console access, and let the account
list say how many grants each account holds
- ship the new strings in all 24 locales
Docs: describe the catalog, the media split, the implied grants and the
unpinned console in server/README.md and server/README_ZH.md.
* fix(web): align the settings icons with the rest of the menu
The settings icon carried a 3px top padding to compensate for the baseline gap
an inline svg leaves inside an antd Badge, which pushed it below the other menu
icons. A flex wrapper removes the gap instead, so the icon centers the way every
MenuItem does.
- center the settings modal tab icons in their 16px box instead of leaving them
on the box baseline
- draw the account and the Tailscale tabs at 16px: both used an 18px icon inside
a 16px box, and the Tailscale icon now takes a size prop instead of a class
* fix(web): draw CJK punctuation with a CJK face
antd components (tooltips, dialogs) and the Tailwind-styled page text used
different font stacks, so the ideographic comma and full stop could resolve to
different faces: a Latin font in antd's stack draws them above the baseline,
which made Chinese and Japanese tooltips look misaligned.
- pin U+3001 and U+3002 to a real CJK face through a @font-face with
unicode-range, falling back to the rest of the stack when none of the local
fonts is installed
- give the app one stack, shared by html/body and antd's font-family token
* feat(web): list administrators above regular users
An account that can change everyone else belongs at the top of the list, and the
row reads differently at a glance: administrators get a lighter card than the
regular accounts they administer.
The sort is stable, so accounts keep the order the account file lists them in
within each group.
* fix(auth): keep a granted capability reachable from the page that configures it
Two defects in the same catalog. The grants whose controls live in the
console menu could be handed out without console.view, and nothing outside
the console renders them, so an account could hold a capability no page
offered. And the catalog carried a third risk level the editor never used.
- imply console.view from power, device, network, update, mcp, tailscale
and ai, the way console.control already did, so every grant has a page
that can use it; terminal and the media grants keep their own pages
- collapse risk to two levels: warn now marks the three grants that amount
to full device control (terminal, mcp, ai), while device, update and the
grants that change settings everyone shares are plain info
- default a user-role account created without naming a grant set to what
the user role granted before grants existed, so a script that only knows
about roles keeps working; an explicit empty set still means empty
- document the implied console, the two levels and the create default
* fix(web): repair the account permission editor and its confirmations
The editor treated an explicitly stored grant that another selection
already implies as editable, so console.view looked free to uncheck on an
account where device settings brought it back; it also attributed the
grant to that other selection, which describes a default permission
backwards. Saving closed the dialog, and an administrator's reference set
was described by a banner above thirteen boxes it contradicted.
- lock any grant a selected grant already brings in, and only add the
"included by" note to a grant the account does not store itself
- keep the dialog open after a save and let the footer button say Saved
for a moment; editing again drops the confirmation
- drop the administrator banner and render the whole catalog as its
read-only set, so a freshly created administrator no longer trips the
empty-set warning
- render the catalog's two risk levels: warn draws the shield and the
full-control summary, info stays a plain help mark
- ship the new Saved label, rename the summary key and retire the demote
hint in all 24 locales, and mirror the catalog in the mocked handlers
* fix(web): honor the grants for the storage mount mode and the mouse menu
The full storage page still offered the mount-mode selector to an account
that cannot attach, where the console menu already hid it, and the mouse
menu vanished for a view-only account even though its cursor style is
local to the screen and needs no grant.
- hide the mount-mode selector on the storage page unless medium.attach,
the way the console menu does
- keep the mouse menu for a view-only account with the cursor style as its
only entry, instead of returning nothing
* refactor(web): use antd for the chat composer and drop the scroll-area primitive
The chat input was the last visible raw form control, and the settings
dialog and shortcut lists scrolled through a Radix primitive whose styling
does not match the scrollbars the rest of the app draws.
- send the chat message through antd's Input.TextArea; the composer's
shape is carried by important utilities rather than a second styling
system, and Enter-to-send plus the IME handling stay as they were
- replace the Radix scroll area with the native scroll container the other
pages use, so the settings dialog and the shortcut lists scroll alike
- remove the component that wrapped it and the @radix-ui/react-scroll-area
dependency
* fix(auth): close the gaps between grants and what they reach
- require library.write for a partition upload of an .iso/.img and for
deleting a partition file or folder that holds one: every image on /data
is a library entry, so medium.attach alone could add or remove them
- mark update as warn: an offline package is checked only against the
checksum its uploader supplies, so the grant amounts to root
- regenerate the device-wide MCP API key when an account loses the mcp
grant (revoked, demoted, disabled or deleted), so a copied key stops
working with the grant
- re-read the account after a session registers, so a grant edit landing
between authentication and registration can no longer leave a
long-lived connection on the old grants
- return the account before and after from Store.Update under one lock,
so concurrent edits diff against the state they actually changed
- keep stored grants this build does not know when a request replaces
the set, instead of refusing or dropping them
- push the auth refresh event off the admin request path
- cover the catalog closure, legacy translation, unknown-grant carry-over,
warn levels and the library guard with tests
* fix(web): follow the grant fixes in the account and tailscale pages
- show the memory and swap settings in the Tailscale menu only with
device, which their endpoints require
- let only the latest account refresh set the state, and coalesce the
focus and visibilitychange events that fire together on tab return
- keep the storage page console socket across auth pushes by depending
on the grant instead of the hasPermission identity
- send only catalog grants from the permission editor; the server keeps
stored ones it does not know
- describe the MCP key rotation in all 24 locales, add the singular
grant count, and mirror the update risk level in the mocks
* fix(auth): check the on-disk name and report a failed key rotation
- check a partition upload against the file it actually replaces: exFAT
folds case with its own table, so a name that is not an image in the
request can still overwrite one on disk
- report a failed MCP key rotation to the administrator instead of
answering OK while the old key keeps working
- re-read the account after registration only for the long-lived
channels a grant edit can drop, so short requests do not read the
account file twice
* feat(auth): let the administrator choose whether to rotate the MCP key
Regenerating the device-wide MCP API key disconnects every MCP client, so
rotating it on every revocation took a decision away from the person who
has to live with it. A change that takes the mcp grant away now warns
first and leaves the rotation to the administrator.
- server: rotate only when the request asks for it (rotateMcpKey in the
update body, a query parameter on delete) and report mcpKeyRotated in
the response; a kept key is logged as a warning
- web: before saving a grant set, changing the role, disabling or deleting
an account that would lose the key, show a warning with a "Regenerate
the MCP API key" checkbox, checked by default; unchecking it explains
that a copied key stays valid, and a rotation is confirmed afterwards
- describe the choice in the mcp grant help and the server READMEs, and
ship the new strings in all 24 locales
---------
Co-authored-by: Guoguo <i@qwq.trade>
* feat(storage): browse a medium and exchange files with the target machine
The image menu and the download page are replaced by one storage page: it
lists the media the emulated USB disk can carry, mounts and ejects them, and
browses the files inside a medium over that same gadget, so the two machines
exchange files without going through the terminal.
Mediated media:
* Add the media API (list, state, mount, eject, delete, download, files) and a
file browser on top of it, and keep the pre-2.6 image endpoints working as a
compatibility layer over the same code.
* Add diskfs: an image is mounted through a loop device so the kernel's exFAT
and FAT drivers read it, and the data partition is browsed in place, because
the emulated disk already hands that very directory to the target machine.
* Keep the one-writer policy the read-only mounts introduced: an image is
served read-only in every mode, and the whole data partition is remounted
read-only here while the target machine is its writer, through the same
SetDataWritable the virtual disk switch uses. The file manager refuses those
writes itself as well, so a request does not depend on a mount state.
* Name the mounted medium and the USB device after the image file, cut to the
SCSI inquiry product field and the USB product string, and restore the
defaults when the medium is released.
* Close the HID devices around a gadget re-enumeration and reopen them after,
since mounting and a forced eject drop them.
* Report transfers through one job slot with progress, queueing behind a
running transfer, and a free space preflight before an upload starts. Image
uploads keep the volume descriptor and partition table check, so an unusable
image is rejected while it streams.
The shared folder view:
* Refresh it on a timer while the target machine owns the partition. The
target's block level writes land in this device's page cache, which the
mounted exFAT shares, so the file data here is already current - but the
dentry and inode caches are not, and a file the target grew keeps the size
it was first read with. Dropping those caches and pushing a storage event
makes the web UI follow the target machine within a second of its own
writeback. The partition's write counter cannot drive this: it only moves
once this side writes its dirty pages back, which is tens of seconds later
or never.
* Send the refresh notifications from their own goroutine, so a client that
stopped reading cannot hold up the refresh loop.
The web UI:
* Add the storage page: the medium list with mount and eject, the file browser
with search and sorting, uploads, downloads and deletes, and transfer
progress.
* Gate writing on the target machine owning the disk - image uploads and file
uploads alike - instead of letting the server answer with an error the page
has to surface, and drop the medium rows' download and delete buttons there
too.
* Give the shared folder, .iso and .img their own glyphs, and pick the file
icon from the extension, so a listing can be read at a glance.
* Name the partition the way the storage page does in the desktop menu as
well, instead of showing the API's untranslated string.
* Read the mounted medium in Settings - Device from the media state, now that
the storage API it used is gone.
* Tell an ISO 9660 image apart from a filesystem that cannot be read at all:
the kernel of this device carries no iso9660 support (CONFIG_ISO9660_FS), so
the contents of an ISO cannot be listed while the image itself is served to
the target machine like any other medium.
Localization:
* Translate the new strings for every locale, keep the Settings - Device
strings the read-only mounts added, and drop the keys the retired image menu
and download page left behind.
Chores:
* Remove the desktop image menu, the download page and the API they used.
* fix(storage): harden media file transfers
Serialize medium deletion with gadget attachment and restore /data write access after failed mount transitions.
Use available filesystem space for upload limits, refine virtual-device and shared-file handling, and update media path and installation test coverage.
Pin an image while it is streamed out, so a writable mount cannot start between the check and the last byte; sweep stale upload temporaries from the whole data partition at startup, which the file manager itself cannot delete; and preflight URL downloads against the response size, which only the upload path did.
Show dot files in the file browser again: only the two transfer temporary prefixes stay hidden, which is what the dot filter was for.
Delete folders recursively after the web UI confirms, refusing when an attached or mounted medium lives inside the directory; accept every .iso and .img upload by name instead of sniffing volume descriptors; and show image and file transfers in one progress display at the top of the storage page.
* fix(storage): make cache drops work and close delete and gadget races
- drop_caches only accepts 1..4 on 5.10: write 4 once to silence the log,
then 2/3. Writing 6/7 failed with EINVAL, so the shared folder never
saw what the target machine wrote.
- Resolve a delete path to its on-disk spelling (exFAT is case-insensitive)
before checking it against the attached and loop-mounted media, refuse
deleting a loop-mounted image or a folder holding an upload target, and
fail closed when the disk state cannot be read.
- GetMediaState no longer fails when the virtual disk is off and lun.0 does
not exist; browsing works with the virtual disk off.
- An explicit enable for the virtual disk compares against the disk being
configured, so it leaves a served image alone; the settings toggle keeps
the #937 semantics.
- Restore /data write access after a failed gadget script or network switch.
- eject clears the LUN file before resetting ro/cdrom, so a refused eject
does not change the type of the medium still being served.
- Mount loop images with MS_NOSYMFOLLOW and open files with O_NOFOLLOW.
- Accept UDF images as optical media.
- Only refuse deleting transfer temporaries while a transfer runs; sweep
stale ones in the background, by exact name, and not on a read-only /data;
hide them from listings.
- Cap URL downloads without Content-Length at the free space and verify the
received length.
- Encode Content-Disposition filename* strictly per RFC 5987.
- Restore image_test.go and add tests for the above.
* fix(web): keep the file browser and transfers consistent on the storage page
- Tag a listing with the medium and path it belongs to, so entries of the
previous directory are never shown or acted on under a new path.
- Poll the desktop storage menu only while it is open.
- Show a dedicated message for a checksum mismatch.
- Bring back the USB / SCP / TF card hints behind a help button.
- Pick up a transfer already running when the page opens; make uploads
cancellable; track concurrent uploads separately; let a failed job be
dismissed; show a success state and guard the progress display by task.
- Handle request errors that were left unhandled.
- Dedicated confirmation for deleting a folder recursively.
- Treat UDF images as optical, send credentials on the image download
pre-check, translate the remaining Hungarian string.
---------
Co-authored-by: Guoguo <i@qwq.trade>
`react-simple-keyboard` ships only a UMD/CJS bundle whose `module.exports`
is `{ KeyboardReact, default: KeyboardReact }` with `__esModule: true`.
Vite 8 applies Node-compatible CJS interop for ESM importers, so the
default import resolved to the whole `module.exports` object instead of
the component. Rendering it made React throw "Minified React error #130"
(`got: object`) as soon as the on-screen keyboard was opened.
Import the `KeyboardReact` named export, which is the same component,
and keep `KeyboardButtonTheme` as a type-only import.
Fixes#936
Only one side may write the mounted image or the /data partition at a time,
and uploaded images are validated before they are stored.
Image mounts:
* Serve every mounted image read-only, in CD-ROM and mass storage mode alike:
detach the LUN first and set the read-only flag before the image is
attached, so the target machine can neither write to nor format it. The
flags were previously only written for CD-ROM mounts, so a mass storage
mount inherited ro=0 and the image stayed writable from the host.
* Report the virtual disk as on only while the LUN really carries the /data
partition. With an image mounted the switch used to stay active but
unchangeable, and enabling the disk is now a confirmed action instead of a
rejected one: it cancels the mounted image, and the confirm also warns that
the image download is interrupted because /data becomes read-only here.
* Ask before mounting: the image list warns that the virtual disk is released
for the image, or offers to enable it first when it is off, instead of
dropping the /data share silently. The new strings are translated for every
locale.
* Name the mounted medium and the USB device after the image file, cut to the
16 bytes of the SCSI inquiry product field and the 126 bytes of the USB
product string, and restore the previous names on unmount.
* Reset the LUN flags on every gadget restart. They survived a restart, so
the shared partition came back to the host read-only, or as an optical
drive, after an image had been mounted.
The /data handover:
* Mount /data read-only at boot whenever the virtual disk serves the raw
partition as a writable disk, so the handover holds from the first boot on.
* Add SetDataWritable(), which remounts /data and verifies the new state.
Enabling the virtual disk makes /data read-only before the host can write
it, and refuses to expose the disk when the remount fails instead of taking
the risk. Unmounting an image, which hands the raw partition back to the
host, does the same, while mounting an image switches /data back to
writable because a served image is read-only for the host.
* Cancel a running image transfer before the handover and wait for it to
stop, and report the state in the image list, where mounting still works
while deleting and uploading do not. The delete action is disabled with a
hint, and the downloader offers to turn the virtual disk off.
Image uploads:
* Validate the volume descriptors while streaming, so an unusable image is
rejected in seconds instead of after the whole transfer: accept ISO 9660 and
UDF volume descriptors (CD001, BEA01) as well as raw disk images (GPT, APFS,
HFS+, MBR), and reject anything else. Require the image to fit in /data with
a 128 MiB reserve before any of it is received, log the underlying errors on
the rejection paths, and keep the temporary file, the progress ticker and the
close handling in defers.
Add unit tests for the upload header check, the free space preflight and the
image name truncation.
Make input-region calibration work across MJPEG and H.264 video modes, including robust four-sided black-border detection and initial-selection edge snapping. Persist multiple reusable manual regions, keep manual and original-resolution choices mutually exclusive, migrate legacy settings, and localize the new controls for every supported language.
Add device-level input-region APIs for reading, validating, and atomically persisting absolute mouse calibration settings, including automatic, manual, and disabled modes.
Introduce a shared screen viewport that keeps the rendered media geometry and configured input region aligned across MJPEG, direct H.264, and WebRTC streams. Add automatic black-border detection and a manual selection overlay with dragging, resizing, magnified preview, keyboard locking, and cancellation support.
Provide original-resolution presets so manual calibration can be recalculated for common aspect ratios, expose the controls from the desktop mouse menu, close menus before entering selection mode, and preserve active file transfers when the download menu closes.
Add localized control-region labels and messages for all supported locales, and update direct-frame rendering so media dimensions are reported after the canvas has been drawn.
- Add touch gestures for relative mouse mode, including tap, drag, long press, double-tap drag, and two-finger scrolling.
- Improve absolute mouse touch handling and prevent black letterbox areas from generating remote mouse events.
- Fit H.264 and MJPEG screens with contain scaling and disable browser touch handling on the screen elements.
- Move golang.org/x/sys to the direct dependency list.
Upgrade Vite to 8.2.0 and @vitejs/plugin-react to 6.0.5.
Use Vite's native tsconfig path resolver and remove vite-tsconfig-paths.
Verified with pnpm build and pnpm lint (existing warnings only).
Layer the frontend toolchain on top of the existing release builder
image so a single container covers Go, C support-layer and web
development, with the builder image remaining the single source of
truth for the cross toolchain, Go and MaixCDK.
- Mount the workspace at /home/build/NanoKVM, where the Makefile and
support/sg2002/build expect it, and resync MaixCDK components via
update_lib on create so C builds never use stale sources
- Install Node from official dist tarballs (integrity-checked, pinned
to the major used by CI), with NODE_DIST_MIRROR / NPM_REGISTRY /
BASE_IMAGE build args for restrictive or mirrored corporate networks
- Export the MaixCDK virtualenv via remoteEnv so non-interactive
processes (tasks, extensions, exec) get it too; make
updateRemoteUserUID explicit since the published builder image bakes
the CI runner's uid
- Run pnpm non-interactively during post-create (no TTY) and give the
skeleton-less home a standard ~/.profile -> ~/.bashrc chain
- Make the web toolchain convention explicit via package.json engines
(Node >= 22, pnpm >= 11, matching CI) and document it in web/README
- Document the dev container in the README, point server/README at the
container flow that works on any host OS, and ignore .pnpm-store/
+ Add persistent custom update server configuration, authenticated URL support, manifest validation, and the settings UI.
+ Keep application updates rebootless by coordinating video shutdown, migration-time service startup, and online/offline service restarts.
+ Localize the custom update server workflow across all supported languages.
- Add decode-driven flow control and bounded GOP-aware queues to prevent stale H.264 frames from accumulating in TCP and WebSocket buffers.
- Isolate slow clients with dedicated writer goroutines, write deadlines, and safer connection lifecycle handling.
- Move the Direct H.264 socket into the worker, simplify the low-latency decode path, and improve reconnect behavior.
Cancel PicoClaw start/readiness waits when MCP takes control, clear held manual input state after failed release reports, and keep paste duration below the control-mode wait budget.
Also add MCP copy failure translations and avoid reconnect effect churn when the locale changes.
Add the MCP settings view and frontend API bindings for configuring remote-control access.
Expose AI-control ownership state in PicoClaw, lock conflicting keyboard input paths, and preserve chat state when control is released.
- Replace the Discussion community link with Discord
- Align terminal and script ordering in the menu and appearance settings
- Remove the divider before the leader key menu item
Normalize stored MAC addresses, avoid shell execution for ether-wake, handle missing WOL history gracefully, and improve the WOL menu display for named MAC entries.
Use client snapshots for stream fanout, reduce frame queue latency, expose backend ICE server configuration to the WebRTC client, and clean up disconnected WebRTC clients more aggressively.
- Wait for gateway session release before reconnecting to a new or selected session
- Refresh runtime status before deciding whether a selected history session can reconnect
- Render structured thought messages from gateway payload metadata
- Prevent PicoClaw history rows from overflowing horizontally
- Add localized labels for the verifying install stage
- Bump PostCSS to 8.5.10 and refresh the pnpm lockfile
- Regenerate the MSW service worker for msw 2.12.10
- Add pnpm build approvals for frontend tooling
- Add missing localized strings for recent UI features, including keyboard shortcuts, clipboard paste, Wi-Fi AP auth, terminal options, device settings, Tailscale, updates, and PicoClaw.
- Align non-English locale resources with the English translation keys.
- Add DNS configuration APIs for manual and DHCP modes, including persistence and udhcpc hook support.
- Add a Network settings tab with DNS controls, move Wi-Fi and HTTPS settings into it, and update translations.
- Update locales
commit 3dea4b1cfcb866d50c9c093b797c0f4f48fec23f
Author: wenjie <meetwenjie@gmail.com>
Date: Fri Apr 10 17:11:07 2026 +0800
fix(picoclaw): clean temp media and surface MCP screenshots in chat
- remove shared load_image staging and pass source paths through directly
- delete /tmp/picoclaw_media when a gateway session closes
- push MCP screenshot observations to downstream websocket clients
- hide null tool feedback messages and auto-scroll when screenshots load
commit f58ffe1e27f83f64e2b0e8c2f8dcac46af715ace
Author: wenjie <meetwenjie@gmail.com>
Date: Fri Apr 10 15:55:59 2026 +0800
Harden PicoClaw local API auth and session locking
Split PicoClaw routes by caller trust level and require the internal loopback token for local-only endpoints. Update the NanoKVM bridge script to send the internal token for loopback requests. Relax session lock acquisition for screenshot and action calls so the active session can perform local operations without permanently taking over the lock, and stop forcing the runtime dm_scope default.
commit 462a0670bbc5138c893fbd1155e72465b82c2065
Author: wenjie <meetwenjie@gmail.com>
Date: Fri Apr 10 13:52:39 2026 +0800
fix(picoclaw): improve KVM reliability and sidebar behavior
- follow HTTPS loopback redirects in the NanoKVM bridge script
- enable MJPEG frame caching only during active PicoClaw gateway sessions
- restore legacy screen zoom behavior across MJPEG and H264 renderers
- keep the PicoClaw sidebar available on mobile without splitter layout conflicts
- hide empty "null"/"undefined" chat messages and keep MJPEG failures on a black screen
commit bfda85c6368825c7ddf47d347ba5ec2588135885
Author: wenjie <meetwenjie@gmail.com>
Date: Fri Apr 10 11:37:15 2026 +0800
fix(picoclaw): verify runtime downloads and remove unused config API
- verify the downloaded runtime archive against the published SHA-512 checksum
- remove the unused /api/picoclaw/config endpoints and related frontend state
- rename runtime_control.go to runtime_constants.go for clearer intent
commit 10b34aaea59e10d70c3cdecb01915444561ceaf7
Author: wenjie <meetwenjie@gmail.com>
Date: Thu Apr 9 16:32:30 2026 +0800
fix: stabilize picoclaw runtime defaults and secure local MCP access
- persist required NanoKVM startup defaults before launching picoclaw and after saving model config
- force-enable the pico channel when loading config so runtime status can recover to ready
- derive the local MCP URL from the configured HTTP port and keep loopback-only HTTP access behind an internal token
- move loopback HTTP redirect logic into middleware to simplify main server startup
- improve runtime/sidebar state handling and add the load-image endpoint for active picoclaw sessions
commit d20540195bf45ea290f727b473ee8cd2a1bbb68d
Author: wenjie <meetwenjie@gmail.com>
Date: Fri Mar 27 17:55:29 2026 +0800
feat(picoclaw): add picoclaw integration