- group sibling users by the username-number convention
- synchronize group credentials, colors, and session rotation
- add group switching and destructive-action warnings
- keep group updates transactional and validate names server-side
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- user/admin password rule checklists no longer reuse the global .ok
button class (whose background turned whole rows green); ticks now use
a dedicated .done state that only recolors the item text and mark
- admin user table in IPv6-pool mode shows 有/无 (Yes/No) instead of the
full address, keeping the column narrow like the other two modes
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- pw.Validate now requires >=10 chars with an uppercase, a lowercase and a
digit; messages and templates updated to match
- Change-password forms (user panel + admin) show a live checklist that
ticks each rule off as the user types
- mgr.Add and `vps add` lowercase the username before validating and
checking duplicates, so uppercase input is accepted and stored lowercase
- Create-user form folds the username to lowercase live and reports rule
and duplicate problems before submit
- drop the wordy password-rule hints added in the last passwd commit
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Shared pw.Validate rule replaces the per-panel 14-char checks; user and
admin change-password handlers, templates and i18n messages all updated.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- The modal "log in as this user" form carries class=inline, and the CSS
rule form.inline{display:inline} overrides the hidden attribute, so the
button was visible in every modal. It posted the stale/empty name input
and failed with "user not found". Add form.inline[hidden]{display:none}
- Reset-panel-pass now attaches the username to the flash (kind
user_created), so the reset modal's login-as button is shown only for
that user and logs in correctly instead of submitting a blank name
- User panel: component borders (cards, buttons, inputs, quick-stat
tiles, modal, spinner; login card and input) go from 1px back to 2px
- Tests: login-as form must render hidden + CSS guard present; reset flash
must carry the username
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Restore the light-mode page background to the gray-blue #e6ebf1 (with
white cards) that predates the all-white change, in the user overview,
login page and the themed override, so light mode keeps its contrast
- Port-forwarding card: bold the three-digit block number and render the
00/99 tails at the same size, un-bolded and faded (opacity only)
- When IPv4 forwarding is off the card now says "IPv4 转发已关闭 / IPv4
forwarding is off" instead of leaving the slot empty
- Update tests for the new markup, v4-off notice and restored background
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Render the port block in the same yellow code style as IP addresses,
dropping the bold; de-emphasize the trailing 00/99 with a smaller,
faded span
- Only advertise the port block while v4 forwarding is on — with it off
(IPv6-only host) there is nothing to forward to, so the card hides it
- Update tests: v4-off renders no port-block line even when a start_port
exists; zh/en assert the de-emphasized tails
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Drop the "available ports" cell from the overview table; the IPV4 row
now carries just the public IP across the row
- Rename the SSH row label to "SSH 端口" in zh (SSH port unchanged)
- Rename the Domains card to "端口转发 / Port forwarding": it now shows
the user's port block (Available: <b>103</b>00-103<b>99</b>, block
number bolded) followed by a "域名转发 / Domain forwarding" section
with the existing domain table and add form unchanged
- pageData: replace the unused PortsShort with PortsPrefix (whole-hundred
block number); update tests for the new layout and zh labels
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Per-user accent color now tints the user panel only for operator
impersonation sessions (handleOverview clears ThemeColor otherwise);
users logging in with their own password see the default theme
- Restore 1px borders on user-panel cards, buttons, inputs, quick stats,
modals and spinner, and on the login card/input (was 3px)
- Update docs and tests; add HTTP-level test covering both session kinds
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Stack the notes list above the editor below 640px instead of squeezing
the fixed 28% sidebar alongside the textarea; let the toolbar buttons
wrap so save/lock/delete no longer overflow on mobile.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Reinstall/upgrade with incus.image set to anything other than the
default vpsmgr/debian-sshd now skips the Debian 13 sshd image build —
a custom image operator does not launch the stock one, so pulling
Debian 13 and rebuilding it is wasted work (even when the default
image is missing).
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The explanation line above the S/M/L preset cards is unnecessary
chrome; remove it and its CSS/JS/labels.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Three fixed preset cards sit above the create form; each stores a
customizable name plus cpu/mem/disk/bandwidth specs in localStorage
(pure front-end, no backend change). "Edit" opens a modal, "Apply"
overwrites the current form values.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Some hosts resolve go.dev/dl through an IPv6 path that serves the site and
manifest fine but returns 404 for the tarball CDN (dl.google.com). curl
prefers IPv6 by default, so ensure_go's download loop failed on every
candidate and aborted the local build.
- add curl_retry: fetch with the default address family, fall back to
'curl -4' on failure; use it for both the manifest and the tarball
- the IPv4 fallback is a pure retry — hosts with working IPv6 are
unaffected, and pure-IPv4 hosts were never broken
- drop an unused 'latest' variable in the download loop
The in-tree NDP responder relies on being the only NDP proxy on the host.
main.go already disabled ndppd.service at install; npd6 (an alternative
distro NDP proxy daemon) was left running, so a host that happened to have
npd6 installed would answer Neighbor Solicitations with a competing
link-local-source NA and break the global-address fix.
- main.go: also `systemctl disable --now npd6.service` at install
- uninstall.sh: also stop/disable npd6 in the IPv6 cleanup path
- docs: note both daemons are disabled (ipv6-ndp-responder.md, ipv6.md)
net.slot_range required operators to reason about IPv4 last-octet slots to
control the host's reserved port span. Replace it with net.user_ports, a
direct comma-separated set of user-port ranges (default 10000-29999) that a
new container's whole-hundred block is drawn from, auto-aligned to whole
hundreds and merged. Only-new-users effect; existing containers keep their
ports.
- cfg: ParseUserPorts/CanonicalUserPorts/UserPortRanges/UserPortCount/
UserPortBlockStarts; legacy slot_range is migrated once on Load and the
old key stays in the file, ignored, no longer editable.
- db: migration v13 adds a UNIQUE index on users.start_port (port blocks are
no longer derived from idx, so uniqueness must be enforced directly);
UsedStartPorts() feeds the allocator.
- mgr: port blocks are allocated independently of the IPv4 idx via
allocUserPortBlock; admin capacity readout follows UserPortCount.
- installer: 00-ip-ask.sh asks for port ranges (default = no confirm, a
changed value echoes capacity and asks Y/n) and converts legacy slot_range;
00-check.sh scans only the effective user-port ranges.
- ssh: allocSSHPort now probes the host (bind test) before picking a port,
reassigning on conflict; the install no longer hard-fails on an occupied
30000-31999 port.
A plain ./install.sh downloads the prebuilt RELEASE binary, which always
comes from main regardless of the checked-out branch. If this is a real
git checkout on a non-main branch, prompt for confirmation before
continuing. Tarball / one-click pipe installs have no .git directory and
skip the check. The branch is read directly from .git/HEAD so git(1) is
not required; a detached HEAD is treated as non-main.
A plain ./install.sh downloads the prebuilt RELEASE binary, which always
comes from main regardless of the checked-out branch. If this is a real
git checkout on a non-main branch, prompt for confirmation before
continuing. Tarball / one-click pipe installs have no .git directory and
skip the check. The branch is read directly from .git/HEAD so git(1) is
not required; a detached HEAD is treated as non-main.
On the dev-ndp branch the prefix-mode boot unit vps-ipv6.service runs a
long-lived `vps ipv6-proxy` process that maps /usr/local/bin/vps, so
stopping only vps.service left the binary held and 40-panel.sh failed to
overwrite it with ETXTBSY ("Text file busy"). Stop every vpsmgr unit that
keeps the binary mapped (vps.service and vps-ipv6.service) before copying,
in both the local-build and prebuilt paths. Verified live: cp fails while
the panel runs, succeeds once it is stopped.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
On the dev-ndp branch the prefix-mode boot unit vps-ipv6.service runs a
long-lived `vps ipv6-proxy` process that maps /usr/local/bin/vps, so
stopping only vps.service left the binary held and 40-panel.sh failed to
overwrite it with ETXTBSY ("Text file busy"). Stop every vpsmgr unit that
keeps the binary mapped (vps.service and vps-ipv6.service) before copying,
in both the local-build and prebuilt paths. Verified live: cp fails while
the panel runs, succeeds once it is stopped.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
Bring in the two v6-unrelated features committed on main: the snapshot
size column in the time-machine modal, and tab-completion for `vps config
set`.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
Typing a config key by hand was tedious, so `vps config set <TAB>` now
completes just the keys `vps config set` accepts. The completion script
(dropped into /usr/share/bash-completion/completions by 40-panel.sh) pulls
candidates live from a hidden `vps config completions` subcommand, which
returns EditableKeys() from the registry — so offered keys never drift from
what set actually enforces. Also completes top-level commands, config
subcommands and (for del/power/quota/passwd) container names.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
Incus's snapshot API already returns each snapshot's size, so the overview
now renders a Size column (via a newly exported HumanBytes) and the time
machine modal is bumped from 420px to the wide layout. The per-row value is
greeted only when snapshots exist; the header ships in the template.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
Clarify that security.port_isolation + ipv4/ipv6_filtering gate east-west
traffic on the bridge while the responder handles north-south NA replies on
the upstream NIC, and document the gateway-neighbour pinning that plans
around the isolation filter's deliberate side effect on fe80::1 NDP.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
Add docs/ipv6-ndp-responder.md: why prefix mode replaced the distro ndppd
daemon with a project-owned responder (some providers drop link-local-source
NAs), the data-plane correctness guarantees, the A/B design hardening, what
changed on dev-ndp, and an 8-point checklist for wide-scale testing. Link it
from docs/README.md.
Some upstream IPv6 providers reject NDP replies whose source is a
link-local address (what ndppd and Linux proxy_ndp emit) and require the
advertised global address. Replace the distro ndppd prefix-mode data
plane with an in-tree raw AF_PACKET responder whose NA source is the
target's global address, re-reading a panel-writable rules file while it
runs so vps add/del never restarts it.
- add internal/ndp: minimal NDP responder (NS->NA, DAD handling, correct
ICMPv6 checksum/hop-limit/flags); only answers for target blocks inside
the operator's routed prefix (confines the root responder's trust surface)
- main: ipv6-proxy unit (prefix mode) + ipv6-proxy command + ethernetMAC
pre-check at install so a broken ext_if never hot-loops under Restart=always
- ipv6.go: write ndppd.conf atomically (temp+rename), drop all ndppd daemon
management and its sudoers grants
- mgr/routed_ipv6.go: configure guest v6 before publishing the NDP rule;
pin fe80::1 gateway neighbor, UseGateway=false, local /112 route +
vpsmgr-ipv6.service helper in the guest
- 50-image.sh/80-debian-dev-image.sh: bake UseGateway=false into [IPv6AcceptRA]
- VPSMGR_STORAGE now accepts btrfs alongside zfs and dir. On a btrfs root the
pool is created as a native subvolume at /var/lib/incus/storage-pools/vpsmgr
(Incus only allows that source path and requires it NOT to pre-exist); on any
other host it becomes a sparse loop-file pool sized exactly like ZFS.
- install.sh asks for an explicit beta confirmation up front when the btrfs
backend is selected (default yes — on a btrfs host there is no ZFS fallback).
Non-interactive installs proceed; non-btrfs/default-zfs installs are
unchanged.
- btrfs swap: on a btrfs root the swapfile is created with
`btrfs filesystem mkswapfile` (btrfs-progs >= 6.1), falling back to
truncate + chattr +C + dd for older tools — the generic fallocate+mkswap path
fails on btrfs (swap must be NODATACOW and preallocated). Non-btrfs hosts
keep the existing fallocate path.
- 00-check.sh installs btrfs-progs and skips the ZFS DKMS/kernel-header
toolchain for btrfs and dir alike; 10-incus.sh verifies the final driver.
- Panel Go code is untouched: container management is storage-driver-
transparent, and lx.EnsureZFSRemoveSnapshots is a no-op off ZFS.
- Docs (architecture, configuration, READMEs, AGENTS) updated for the new
backend, including snapshot-restore semantics (btrfs keeps later snapshots).
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
30-traefik.sh enabled traefik whenever the adopted net.traefik was true, even
on an IPv6-only box (net.v4_forward false). The domain proxy is only served
while BOTH v4 ingress and net.traefik are on, so traefik must stay installed
but DISABLED (no start, no boot autostart) when v4 forwarding is off, regardless
of the kept net.traefik value; re-enabling v4 later restores it. The start
branch is now gated on VPSMGR_V4_FORWARD first, matching the panel's own
ApplyTraefikState (V4Forward && Traefik).
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
- Usernames may now contain hyphens (lowercase letters/digits/hyphens), but a
new name must both start and end with a lowercase letter, max 31 unchanged.
This drops digit-terminated names like "a1"/"vps12345678" at creation.
- The strict rule is enforced only when CREATING a user (ValidateName in
Add/userAdd and the admin add-user form's pattern). An existing-user
operation (del / quota / passwd) now uses ValidateExistingName, which accepts
every historically-valid shape, so a legacy digit-ending username stays
operable rather than being locked out by the new creation rule.
- Tests cover the strict rule, its new rejects, and the legacy-compatible
existing-name check.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
- architecture.md: new bullet under Users and resources explaining that
restoring to an older checkpoint discards the later ones, done as a single
Incus call via zfs.remove_snapshots on the ZFS root volume (recursive
rollback), applied at add/reinstall and refreshed on every `vps install`
(incl. --update), with the delete-and-retry fallback and the reinstall
checkpoint-count warning.
- configuration.md: snapshots.limit row now notes that restoring to an older
checkpoint auto-deletes the newer ones.
- README / README.en: list the time-machine (checkpoint) feature in the user
panel feature lists.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
- A restore to an older checkpoint now relies on zfs.remove_snapshots set on
the container root volume, so a single Incus restore auto-discards the later
snapshots instead of failing with "subsequent snapshot(s)" and leaving a
partially-executed rollback.
- The volume option is set at Add/Reinstall (on the fresh root volume) and by
a new ApplyZFSRemoveSnapshotsToAll pass run during `vps install`, so a host
updated via install.sh --update gets the behaviour immediately without
reinstalling containers. ZFS-only: non-ZFS (dir test) pools are skipped, as
they have no snapshots.
- SnapshotRestore keeps a fallback that deletes the newer snapshots by
creation time and retries once, for a volume that somehow lacks the option.
- User panel adopts the time-machine metaphor: the button is now
"时光机"/Time machine, individual snapshots are "检查点"/checkpoints, the
reinstall modal warns with the exact checkpoint count, and the admin audit
labels + success toasts use the same terms. Adds unit tests for the
newer-snapshot selection and timestamp parsing.
- ask_range now prints one line right after the slot range is finalized
(env / adoption / non-interactive / interactive alike) with the computed
container capacity and the usable user-port span it reserves, so the
operator sees the effect immediately.
- An existing config WITHOUT a slot_range key (an upgrade from a
pre-slot-range version) no longer prompts: it keeps the default 2-201
(which the panel applies anyway) and returns, so a one-click upgrade of
an already-deployed panel is never interrupted by the new question.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
Add net.slot_range, a shrinkable container slot range (read as the IPv4
last octet a new container may take; idx = octet-1) that bounds how many
containers can be created and, consequently, how much of the user-port
block 10000-29999 a host reserves. Default 2-201 = 200 slots; must always
be a sub-range of 2-201 (only-shrink rule). Affects ONLY new containers —
existing ones keep their slot/ports.
- cfg: add net.slot_range with strict ParseSlotRange validation
(exactly one '-', both ends integers in 2..201, lo<=hi), SlotCount/
SlotIdxBounds helpers, and VPSMGR_SLOT_RANGE env handling in FillAuto.
Register net.slot_range as operator / next-add in the config registry.
- db/mgr: NextFreeIdx now picks from a caller-supplied [idxMin,idxMax]
window instead of the fixed 1..cfg.MaxUsers; mgr.Add feeds it from the
configured range. Existing users outside a narrowed range keep their
slots.
- admin panel: capacity readout follows SlotCount() instead of a fixed
200 (honestly shows over-capacity when pre-existing users fall outside
a shrunken range).
- installer: 00-checklist asks the slot range right after the subnet
octet (env/adoption/non-interactive branches, validated). The
port-occupancy check now scans only the range's user-port span rather
than the whole 10000-29999.
- 80/443 handling: if either is already bound by a non-vpsmgr process,
the install no longer fails — it proceeds with Traefik installed but
DISABLED (net.traefik false, no autostart). 00-check writes a marker
that install.sh turns into VPSMGR_TRAEFIK=0 for 30-traefik and the
panel. A belt-and-suspenders guard skips the force-off when our own
traefik.service is already active (update/adoption). SSH 30000-31999
and the user-port span remain hard failures.
- traefik: bump pinned version to 3.7.12 and rewrite 30-traefik's
enable/disable decision to respect the effective net.traefik (forced
env > adopted config > default), so a disabled traefik is never
briefly started.
- docs: README(s), config example, architecture and configuration docs
updated; tests added for slot-range parsing and the FillAuto env vars.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
- prompt now shows [<RAM> GiB/y/N] with the auto size computed from
host memory; answering y creates a swap file of RAM × 1:1
- custom sizes in GiB are still accepted, N/Enter aborts as before
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
- new installs pull incus-base (Zabbly lts-7.0) which ships the same
incusd daemon without the QEMU/SPICE/Ceph VM stack (~240 MiB less
download, 500 MiB+ less disk)
- leave hosts that already carry the full `incus` package untouched
- uninstall --purge now also removes the full-incus VM dependencies,
guarded by an installed reverse-dependency check (never blanket autoremove)
- update the dev docs install snippet to incus-base
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
- add runtime net.traefik switch with live DB state\n- stop and disable Traefik when IPv4 forwarding is off\n- block new domains while Traefik is disabled