.notes-cover and .notes-main set display:flex, which overrode the HTML
hidden attribute (the UA [hidden]{display:none} rule loses to any author
display value). As a result the enable, locked and editor states rendered
simultaneously. Add explicit [hidden] overrides so the three states stay
mutually exclusive.
- SSH keys: drop the long "clean key / comment fallback" explanation from
both the user and admin panels (the behavior is unchanged).
- Notes: before enabling or unlocking, the card shows only the essentials
(a single enable button / the password prompt) with nothing else visible.
- Titles are now edited inline in the list tabs; the separate title field is
gone, giving the body more room.
- Delete moved off the tabs into the editor toolbar with a two-step
confirm (armed for 3s), disabled when only one note remains; a manual
save button sits next to it.
- Save status is explicit: red "storage full" on overflow (client and
server), red "network problem" on fetch failures, plus the existing
unsaved/saving/saved timestamp states. The enable flow caches the key
even if the first save fails.
Move the init-script editor out of its standalone card into a modal opened
from the Machine card (JSON save keeps the modal open; the form POST path is
unchanged). In its place, add end-to-end-encrypted sticky notes: the browser
derives an AES-256-GCM key from a user-chosen password (PBKDF2, 200k iters)
via WebCrypto, and the server only stores an opaque envelope in a new
sticky_notes table (schema v9). Notes have enable/locked/unlocked states,
a 7-day local key cache, add/delete/drag-reorder, and auto-save 3s after
edits stop. Plaintext is capped at 512 KiB client-side; the server rejects
oversized bodies (notes_full) - both surface as "storage full".
Add an operator-side SSH key store (admin_keys table, schema v8) managed
from a floating panel opened via a new header tab. Keys are stored cleanly
(comment stripped, name falls back to the comment) and reconciled full-set
on save, mirroring the user panel; they are not injected into containers yet.
Adds SaveAdminKeys/ListAdminKeys in mgr and a JSON /keys endpoint with
tests for the db, mgr and admin handler layers.
Replace the client-side "quick add SSH key" (one key, stuffed into the
init script) with a real key manager: a floating panel in the Machine
card lists every stored key with an enable checkbox, editable name,
clean read-only key and delete, plus an add row that auto-fills the name
from the key comment.
- DB v7: ssh_keys(user_id, name, key, active) table, cascades on user
delete; keys stored clean (type + base64, trailing comment stripped)
- mgr: ParsePublicKey validation, SaveSSHKeys full-set reconcile
(update/add/delete + duplicate rejection), ApplySSHKeys injects
append-only into ~/.ssh/authorized_keys with per-key dedup, preserving
any keys the user added by hand
- /ssh-keys JSON endpoint saves the set and applies active keys to the
running container immediately (best-effort; warns when unreachable);
active keys are also injected on reinstall
- deactivation is additive-only: unchecked keys stop being re-applied
but are never removed from the container
- i18n warn string; tests for db CRUD/cascade, key parsing/reconcile,
and the panel handler
Add scripts/80-debian-dev-image.sh, modeled on 50-image.sh: pulls
images:debian/13, installs the universal sshd/tooling set plus the dev
toolchain (python3 + pip + venv, git/sqlite3/ripgrep/jq/gh/sshpass, all
--no-install-recommends), Go 1.26.7 (arch-aware, sha256 from the dl JSON
API), nvm v0.40.7 + Node 24, cleans caches, and publishes
vpsmgr/debian-dev-sshd (base deleted).
- Symlink go/gofmt/node/npm/npx into /usr/local/bin: /etc/profile.d is
only read by login shells, so non-interactive ssh commands would miss
them; nvm's .bashrc hook already covered node
- Register the alias in imageLabels so the reinstall dialog shows
"Debian 13 dev"; extend collectManagedImages tests
- Optional: not part of install.sh, keeping small hosts lean
- Document the new image in docs/architecture.md, development.md and the
AGENTS.md image-build invariant
vps-ipv6.service runs `vps ipv6-reapply`, which failed hard when a
single container (e.g. a user-broken networkd) rejected its config.
That aborted `systemctl enable --now vps-ipv6` during install and left
`vps install` halfway, with the panel service stopped.
Align ipv6-reapply with the install path: treat RoutedIPv6Error as a
warning and move on, so one bad guest cannot take down the boot unit or
the panel. Host-level errors still abort.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Parser now takes only `type base64` (greedy over the base64 alphabet,
stopping at the first space); any trailing comment such as
"Generated By Termius" is dropped instead of failing the match.
- Preview shows the cleaned key (no comment) so WYSIWYG with what gets
inserted into the init script.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Widen the CPU average window from 5 to 15 minutes across the admin
and user panels (List, Show, BatchUsers, PanelResources) and relabel
the columns/cards accordingly.
- Replace the hardcoded white background of the create-user IPv6
dropdown with a themed class so it matches the dark color scheme.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Strip the trailing comment from pasted SSH public keys on insert
(e.g. "Generated By Termius") instead of rejecting the whole key.
- Save the init script via native form submit: the previous fetch
followed the handler's 302 redirect and mistook the returned HTML
page for an error banner, dumping it into the toast.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The left axis caption was hardcoded Chinese; render it via the same
lang switch as the rest of the template.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Add scripts/70-opensuse-image.sh, modeled on 60-rhel-image.sh: pulls
images:opensuse/16.0, installs sshd + universal tooling via zypper,
cleans caches, and publishes vpsmgr/opensuse-sshd (base deleted).
- Register the alias in imageLabels so the reinstall dialog shows
"openSUSE Leap 16"; extend collectManagedImages tests
- Leap 16 ships systemd-networkd, so panel IPv6 config takes the
existing Debian networkd branch — no runtime code changes needed
- Optional: not part of install.sh, keeping small hosts lean
- Document the new image in README (zh/en) and docs/architecture.md
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Three canvas charts (CPU line, memory area, bandwidth stacked bars)
on the overview page, after the init-script card. Per-minute data
comes from the existing resource_samples table via a new /stats JSON
endpoint — pure DB read, no Incus call.
- mgr: ChartHistory returns per-minute points; bandwidth is the delta
of the cumulative rx/tx counters between samples, safe against
container restarts (boot-time guard) and counter resets
- panel: /stats handler (auth required) serves the last 24h for the
current user; ChartPoint JSON {m, c, y, r, t}
- overview.html: three chart cards with shared pan (drag), zoom
(wheel at cursor), double-click reset, adaptive time ticks that
never crowd narrow screens, stable Y axes computed once from the
full window, dark mode, per-minute hover tooltips
- tests: TestStatsEndpoint covers auth, JSON shape, and the bandwidth
delta computation
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The swap-reapply command is internal bookkeeping users should never
see: containers get the configured incus.swap_ratio automatically, so
remove it from the CLI surface.
- cmdInstall now calls ApplySwapToAll alongside the other upgrade
self-heal steps, so containers created before swap support get their
allowance after upgrading and running `vps install`
- vps config set incus.swap_ratio keeps applying immediately to all
containers (unchanged)
- drop the swap-reapply dispatch, cmdSwapReapply and help entry
- docs updated accordingly
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Incus 7 on cgroup v2 writes memory.swap.max=0 for every container
unless limits.memory.swap carries an explicit byte amount (true/false
both end up as 0), so containers previously got no host swap at all
and `free` inside showed 0. Add incus.swap_ratio (default 0.5) so each
container gets limits.memory.swap = limits.memory x ratio, set at
create/reinstall and on quota changes.
- config: incus.swap_ratio field + registry entry (non-negative float,
0 disables swap; ApplyImmediate)
- lx: Client carries swapRatio; SetMem keeps swap proportional,
SetSwap refreshes only the swap key
- mgr: ApplySwapToAll refreshes every container from the DB memory
quota (idempotent, no restart)
- cli: `vps config set incus.swap_ratio N` re-applies to all
containers automatically; `vps swap-reapply` is the manual/repair
path for containers created before swap support
- docs: configuration/architecture/README updated
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Ubuntu images ship lxd as a snap even when it is never used or
initialized; its daemon/socket, /var/lib/lxd (or /var/snap/lxd) and
lxdbr0 collide with Incus. Before installing Incus, detect snap lxd and
remove it — but only when clearly unused: abort instead of deleting when
the daemon is running, containers exist, or an initialized lxd.db is
present (checked under both deb and snap data dirs). Idempotent; skipped
when the snap is already gone.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
50-image.sh checked for the base image (vpsmgr-debian-13) BEFORE the
prebuilt sshd image, but the base is deleted after the first build as a
build intermediate. Every reinstall/upgrade therefore re-pulled
images:debian/13 over the network, then discovered vpsmgr/debian-sshd
already existed and exited. Check the sshd image first and exit before
touching the base image when it is present.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
HardenIsolation unconditionally applied the NIC isolation options
(security.port_isolation / ipv4_filtering / ipv6_filtering) to eth0.
Incus only accepts those options on bridged NICs — pool-mode eth0 is a
routed NIC carrying the public /128, so `vps install` failed with
"Invalid device option security.ipv4_filtering". Prefix mode (single
bridged eth0) was unaffected, which is why the bug only surfaced in
pool mode.
- EnsureEth0Options generalized to EnsureDeviceOptions(name, devName)
(wrapper kept for the EnsureBlockRoutes caller)
- HardenIsolation now iterates all NICs and skips non-bridged ones;
pool-mode eth1 (bridged) is hardened like a prefix-mode eth0
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
display_ip is purely cosmetic (panel URL, SSH hints) and never used in
rules, so accept IPv6 addresses, domain names, or any string without
spaces instead of requiring a parseable IP.
- registry: validate only that the value contains no spaces
- tests: domain accepted, space-containing value rejected
- docs/examples: drop the "public IPv4" wording
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The external Globalping verification could previously stretch to ~10
minutes (ICMP and TCP tests each ran up to 3 attempts with a 90s wait
per measurement). Give both tests a shared 300s budget, checked before
each attempt and inside the wait loop, so the installer never blocks
longer than ~5 minutes on a slow/unresponsive Globalping.
- gp_wait waits for the remaining budget instead of a fixed 90s
- gp_run_test stops starting attempts once the budget is used up
- summary reports the elapsed time, or that the budget was hit
- verdict stays "unverified" on budget exhaustion (grep contract
with 00-ip-ask.sh unchanged)
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The installer and IPv6 probe found the external interface only through
`ip route show default` (the IPv4 main table). On IPv6-only hosts and
policy-routed boxes (v4 default in a non-main table, e.g. WARP/wireguard
tunnels) that returns nothing, so check-ipv6-support.sh died at
"no default route / ext interface found" before probing anything and
the installer silently fell back to a wrong iface.
Fall back to the IPv6 default route, then the first non-virtual UP
interface (excluding lo/incusbr/virbr/docker/veth/warp/wg), in all five
places that detect the WAN iface: check-ipv6-support.sh, 00-check.sh,
00-ip-ask.sh, uninstall.sh and DetectExtIF.
Verified on a real IPv6-only host: probe now reports ext iface eth0 and
reaches Phase 2 instead of dying.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The Machine-card snapshots button was the same neutral white as
Restart; give it a light-violet fill (dark-mode aware) so the two are
distinguishable at a glance.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- snapshot management now lives behind a button in the Machine card,
opening a modal instead of a standalone card
- drop the size column from the snapshot table
- the modal reopens after an operation via #snapshots hash so the user
sees the updated list
- remove now-unused FormatBytes helper
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- user panel: create/list/restore/delete disk-only snapshots per container,
with confirm dialogs and a reinstall warning that snapshots are wiped
- snapshots.limit config key (default 1) enforces a per-container cap;
admin panel is untouched
- restore keeps the live container config (disk_only) so quotas/NIC limits
stay in sync with the DB, serialized via opMu against reinstall/delete
- audit log: snapshot.create / snapshot.delete / snapshot.restore
- fix: snapshot names carry a random suffix to avoid same-second collisions
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- admin: record user.create, user.delete, quota.update, passwd.reset,
blocked.update and admin.passwd (actor "000" for admin's own changes)
- panel: record passwd.change when a user changes their own password
- audit page: bilingual labels for the new action types
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- admin: add reset-traffic-only action in the quota modal that zeroes a
user's monthly counters and immediately lifts any active throttle
- mgr: tighten username validation to lowercase letters/digits only
(no hyphens) and halve the max length to 31
- panel: match the admin panel's neutral dark palette for consistent
dark-mode backgrounds across both UIs
- fall back to one live instances-list call only for users lacking a sample\n- steady state keeps the dashboard free of Incus calls\n- apply the same fallback to CLI list/show
- click Name/5m CPU/Mem/Procs/Disk headers to cycle asc/desc/default\n- single-column client-side sorting with indicator arrows\n- show the SSH port bare on the second line under Ports/SSH
- continue host installation when one container rejects guest IPv6 setup\n- preserve fatal handling for host and database errors\n- identify the affected container in reapply failures
- collect container metrics in one background Incus sample\n- serve dashboard usage from seven-day SQLite history\n- replace per-container disk exec and live page sampling
- install.sh accepts --update (alongside --local-build): it forces a
fresh download of the latest prebuilt release over an existing binary.
A plain re-run still skips an installed binary (idempotent adopt).
- Conservative by design: on download/checksum failure the installed
binary is kept and a warning is printed — never a surprise local
rebuild over an unknown checkout.
- install_prebuilt now stops vps.service before replacing the binary
(ETXTBSY on a live panel), and only after the download has verified,
so a failed download never takes the panel down.
- docs: AGENTS.md invariant, architecture.md install lifecycle,
README.zh-CN usage line.
- Admin domain panel gains a blocked-domains textarea (one domain per
line); entries and all their subdomains are refused by mgr.AddDomain —
the single add path — using label-boundary matching (example.co.uk
blocks a.example.co.uk but not forexample.co.uk). Existing domains are
untouched; only the add path is guarded.
- Save is per-line validated: invalid lines are skipped and reported by
their 1-based line numbers in a flash banner, the rest are saved.
- Migration v5 seeds a default list (mozilla/github/aws/microsoft/apple/
google/etc.) only when the settings key was never written, so admin
edits or a full clear are never re-seeded.
- normalizeDomain is rewritten around explicit rules shared by the add
path and the blocked list: lowercase, strip all trailing dots, reject
any character outside [a-z0-9.-] (never silently strip — pasting a URL
is an error), must end with a letter (excludes dotted-quad IPs), must
contain a dot (no single-word names), each label non-empty/<=63 and
starting+ending with a letter or digit (consecutive hyphens mid-label
like xn--p1ai stay legal), total <= 253. No TLD validation.
- Tests: db round-trip + v5 seed (fresh/reopen/admin-edit/preexisting-key),
matching semantics, parse/save partial validation, validation rules.
- docs: architecture.md blocked-domain section.
- New installs use a dir storage pool when VPSMGR_STORAGE=dir (default
zfs). A failed ZFS pool still aborts — the switch is explicit, never
an automatic fallback.
- 00-check.sh skips zfsutils-linux, the DKMS module build and the zfs
boot units entirely in dir mode (no kernel module needed).
- 10-incus.sh creates/adopts a dir pool without requiring zpool; zfs
mode still refuses any non-ZFS pool, fresh or adopted.
- uninstall.sh is unchanged: incus storage delete handles both drivers
and the zpool destroy is already guarded by 'zpool list'.
- docs: architecture.md and AGENTS.md document the switch.
poolContainerScript only rendered Debian systemd-networkd files
(/etc/systemd/network/eth0.network binding the public /128). RHEL-family
images use NetworkManager, which ignores networkd config, so a pool-mode
Alma/Rocky container never bound its routed-NIC /128 and had no public
IPv6 — while Debian worked. Verified on a whitelist-provider host: the
nmcli path (eth0 = manual IPv6, /128, fe80::1 gateway, IPv4 disabled;
eth1 = DHCPv4) brings up the /128, the default route and v4/v6/DNS
connectivity, and the connection survives reboots.
- routed_ipv6.go: poolContainerScript branches on the guest stack —
RHEL/NetworkManager gets a static nmcli connection matched by DEVICE
(name-grep fallback), Debian keeps the networkd files. Idempotent and
self-healing on reinstall/ipv6-reapply.
- ipv6_test.go: TestPoolContainerScript asserts both branches render the
/128, fe80::1 gateway and DHCPv4 markers.
Trim the Globalping attribution in the installer to a short credit.
In the admin IPv6 pool page, show a prominent bilingual alert telling
operators not to add the host's own currently-used public IPv6 address
to the pool (it would break host IPv6 connectivity).
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Make the IPv6 pool option the default choice in the installer prompt
(press enter for pool) and show a small thank-you attribution to
Globalping for powering the external reachability probe.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The VERSION endpoint can temporarily name a release before its archive is
available through every download edge, causing a hard 404 during local
builds. Read the official stable JSON manifest instead, select the archive
for the current architecture, retry downloads, and fall back through the
previous stable release when the newest archive is unavailable.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Remove secondary-disk discovery and whole-disk ZFS creation from the
installer. New installations now always create a sparse ZFS loop-file
pool from free space on /, while existing vpsmgr pools are still adopted.
The installer no longer scans, selects, formats, or modifies devices such
as /dev/vdb, including virtual ISO/CD devices.
Update storage documentation and keep the existing reclaim, pool sizing,
and ZFS quota behavior.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The script accidentally contained two ask_ipv6 definitions. Bash used the
second legacy definition, so the new detector/globalping flow was silently
overridden and installations jumped straight to manual prefix input.
Keep only the detector-based implementation.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Switch the fixture to the 2001:db8:: documentation block so the
workspace carries no real-format provider prefix.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>