- admin CPU card: the "what saving does depends on the checkbox" footnote is
gone; the state line says it instead, and the state/description text is
shorter
- user panel: the reinstall share field is just "或从分享的检查点安装" and its
note "将从该检查点恢复,不安装系统。"; the share warning no longer parenthesises
the contents ("拿到分享码的人可以读取该检查点内的全部数据,包括容器里的凭据。"),
the second confirmation is "确认删除 / 更早的 N 个检查点会被删除,无法恢复。",
the revoke prompt drops the repeated question, and the restore/reset prompts
lose their "…,确认?"/"(重置后无法恢复)" tails
- the share toast only mentions removed checkpoints when there were some
(previously "share code created (0 older checkpoints removed)")
- expiry: "账户已到期,机器已关机并锁定。" and "该账户已到期,仅可延期。";the
"new deadline = max(now, current) + duration" footnote and "(0 = permanent)"
parentheticals are gone
- knowledge editor: label is a plain "Markdown: headings, lists, …" legend and
the empty preview is a dash instead of an instruction
- docs: the 1.8 sections (expiry, snapshot sharing, refquota, sessions, CPU
rule, knowledge base) are cut roughly in half
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- with the rule disabled the parameter inputs are hidden entirely (they were
shown with values that had no effect), and a state line says which of the two
states the rule is in; ticking the box reveals the parameters again
- the parameter fields lost `required`: they are prefilled from the server, the
server validates every range, and a hidden-but-required input makes the
browser refuse to submit with a "not focusable" error
- the save message now matches what happened: "saved and applied now" only when
the rule is enabled, otherwise "saved — it is disabled, so no container is
capped" (nothing is applied, and any capped container was just restored)
- the button is a plain "Save"; the effect depends on the checkbox above it
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
User panel:
- the reinstall modal drops the system highlight as soon as a share code is
typed, so "nothing selected" is an explicit state that says the container is
rebuilt from a checkpoint rather than from an image; clearing the code
restores the default image, and clicking a tile drops a stale code instead of
letting it silently override the visible selection
- the time-machine actions (share/restore/delete/revoke, plus the sticky-notes
reset) confirm in an in-page dialog styled like the panel instead of
window.confirm(); publishing still asks twice when it would delete the older
checkpoints
Admin panel:
- the knowledge-base preview renders again: it posts a FormData body
(multipart/form-data) and ParseForm alone ignores multipart bodies, so the
server rendered the empty string and the preview box stayed blank
- the dynamic CPU limit rule moves back to the panel: an editable card on the
overview saves into the DB (the single source of truth) and applies on the
spot, restoring capped containers when it is switched off. The cpu_limit.*
config keys and their `vps config set` entries are gone, and `vps install`
no longer touches the rule, so an upgrade cannot reset it
- admin sessions are persisted (new admin_sessions table, hashed tokens) so a
panel restart no longer logs the operator out; a password change from the
panel or the CLI still invalidates every other session
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- an expired account may still change its own panel password, and the admin
may still reset it — only the container stays locked. Both routes are
deliberately exempt from the read-only gate and both stay audited
- snapshot sharing is now OFF by default (new installs and upgrades alike):
`snapshots.share` defaults to false and the panel re-derives its DB mirror
from the config file at startup, so even a binary-only upgrade picks the
default up. No admin-panel UI for it; enable with `vps config set
snapshots.share true`
- publishing a checkpoint now asks twice when it would delete older
checkpoints (first the exposure, then the irreversible deletion)
- audit completeness: distinct domain.add/domain.update/domain.delete,
notes.update, and subject suffixes for snapshot.share.<checkpoint>,
reinstall.share.<owner> and quota.expire.<user>, with matching labels in
the audit viewer (knowledge.* labels added too)
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Port of dev-quota-expiry onto 1.8. The branch's other three commits are
already in main, so only this feature is new here; its migration is
renumbered to v16 (1.8 already uses v14/v15).
- users.expires_at (RFC3339 UTC, '' = permanent): the admin sets days at
creation and extends from the users table (72h/30d/90d/180d/custom/forever;
extend = max(now, current) + duration)
- the 60s loop force-stops expired containers (stop force:true) and disables
boot.autostart; the account is locked to read-only — the user panel refuses
every mutation (including the new snapshot-share/unshare routes) and the
admin panel allows only extend + delete
- the user panel shows a live countdown and a locked banner; the admin users
table dims expired rows and tags the name "(-Nd)"
- CLI: vps add --days N, vps quota --days N | --clear-expiry
- audit: quota.expiry (admin change) and quota.expire.<user> (auto stop)
- the container and its checkpoints are never deleted, so a checkpoint shared
before expiry stays installable by others (cloning a stopped container's
snapshot works — verified end to end)
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- new `knowledge` table (schema v15) and an admin page (/knowledge) to create,
edit, preview and delete Markdown articles; the Markdown source is capped at
256 KiB and both title and content are required
- `internal/markdown`: a small, dependency-free Markdown renderer for a basic
subset (headings, fenced code, inline code, bold/italic, links, lists,
blockquotes, rules). It escapes HTML first and only allows http/https/mailto/
#/ links, so an article can never inject markup
- the admin editor previews through POST /knowledge-preview, which returns the
rendered HTML as JSON
- the user panel's machine card gains a "知识库 / Knowledge base" button that
opens a large overlay (nearly fullscreen on phones): every article is
embedded rendered, the list opens an article with a back button, and a copy
button is added to each code block
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- `snapshots.share` and `cpu_limit.*` are now config.yaml fields managed with
`vps config set` (validated, shown by `vps config list/help`) instead of being
panel/DB-only settings; the admin panel now shows the CPU rule read-only and
the snapshot-sharing card is gone
- both apply immediately: `vps config set` writes the config file, mirrors the
value into the DB and applies it right away — no panel restart — the same
pattern as net.v4_forward. `vps install` refreshes the mirror from the file
- the CPU rule is read from the mirror (falling back to the config file) so the
enforcer and the read-only card pick up a change live; the share toggle too
- drop the now-unused admin routes/handlers, the DB-only rule setting and the
ConfigSet CPU form
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- add a snapshot_share_enabled setting with an admin-overview toggle;
absent means ENABLED, so an upgraded host gets the feature on by default
- turning it off hides the user-side share/import entry points and makes
the manager refuse new shares and installs, while existing containers and
checkpoints are left untouched
- the stored codes are deliberately kept in the database (not deleted), so
re-enabling restores them for as long as their checkpoint still exists;
a code whose checkpoint has vanished is reported as invalid instead of
building a half-container, and deleting a checkpoint still removes its code
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- a user can publish one checkpoint behind a random v4-UUID code; another
user enters that code in the reinstall dialog and gets a container cloned
copy-on-write from the snapshot (no image is built, so no extra disk until
it diverges), re-provisioned as their own: own IP/ports/disk quota and
network, a fresh hostname/root password/machine-id, the source's
authorized_keys cleared, and no init script run
- publishing deletes the older checkpoints (a clone pins its origin, so they
could never be restored to again); the panel warns with the exact count.
One share per container; deleting the checkpoint/container or reinstalling
revokes it, and a code for the container itself is handled as a restore
- clone via POST /1.0/instances with source.type=copy, then delete the old
container and rename the clone into place; new lx.CloneFromSnapshot /
lx.RenameInstance and a shared instanceSpec() so a clone is configured
exactly like a fresh container
- new snapshot_shares table (schema v14) and /snapshot-share /
/snapshot-unshare panel routes plus the time-machine and reinstall UI
- also fix a refused restore (a checkpoint pinned by a clone) so the
container is always left running, and recognise the "subsequent internal
snapshot(s)" refusal message alongside the existing one
- also give new containers a HARD disk limit: the pool carries
volume.zfs.use_refquota so a volume's usage counts the blocks it
references (including inherited image/snapshot blocks); existing
containers are untouched
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- admin panel gains a global dynamic CPU limit card (default off, prefilled
10/60/0.5/2/30): a container that keeps using over y% of its own quota for
x consecutive minutes is capped to z cores for n hours m minutes, then its
normal quota is restored automatically
- reuse lx.SetCPU (live instance-config PATCH) and the per-minute
resource_samples.cpu_pct_x10 (already relative to the container's quota);
the new EnforceCPULimits pass runs in sampleResourceLoop, and the rule plus
the active-limit state live in DB settings so a vps.service restart keeps
every countdown and still restores on expiry
- user panel shows the capped core count and a frontend countdown while a
limit is active; containers whose own quota is <= z are skipped
- cover the rule validation, streak detection, active-state cleanup and the
user-panel badge in unit tests
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- MachineSpecs keeps raw MiB up to 9600, then switches to a rounded whole
GiB (e.g. 10000 -> "2c 10g")
- cover the boundary and rounding in the group tests
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- new panel.bandwidth_reset_day (1-28, default 1); the monthly bandwidth
period key now starts on that day (mgr.BandwidthPeriod), so counters
roll over on the configured day instead of the 1st
- user panel quota label shows "流量配额(至次月 N 号)"
- docs + BandwidthPeriod/label tests
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- handleSwitchUser logged the raw session username, so a container switch
made under an operator ("log in as user") session was recorded as the
user's own action; use auditActor to record "000+<user>"
- localize the session.switch.<target> and color.update audit actions,
which rendered as raw action strings on the audit page
- add a regression test for the impersonated switch actor
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- render each switcher option as "<label> - <cpu>c <mem>g <disk>g"
(e.g. "A - 2c 8g 60g") instead of the cramped "A-2c8g60g"
- update MachineSpecs and the pinned tests
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- vps add of a sibling inherited the group hash and blanked Result.Password,
which also suppressed the ssh and panel lines in printAdded
- print an explicit inherited-password note and always emit ssh/panel
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- label each switcher option "<label>-<cpu>c<mem>g<disk>g" (e.g. 2-4c8g40g)
and drop the shared group-name prefix
- order members base-first then by numeric suffix (A, 1, 2, ... 10)
- tag the sticky-notes card title with the current machine on multi-container
groups; single-container users keep a bare title
- add mgr.MachineSpecs and cover labels, order and the tag with tests
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- keep the full username validation hint visible
- reserve a stable IPv6 pool usage row
- prevent helper text from shifting form controls
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- keep username hints from shifting resource fields
- give IPv6 pool selection room to breathe
- align the create action and tighten responsive layout
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- show A for the primary machine
- preserve numeric child suffixes including leading zeroes
- cover group label parsing with tests
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- call user-facing containers machines in Chinese text
- keep impersonation state when switching machines
- align and narrow admin create-user controls
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- 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>
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>
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.
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>
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]
- 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>
- 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.
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>
- 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