Typing in vi left the whole session underlined. The stream vi sends contains
xterm's modifyOtherKeys, which carries a private ">" prefix:
CSI > 4 ; m and CSI > 4 ; 2 m
A CSI sequence has four private markers — ? < = > — and the parser only knew
"?". The others were dropped on the floor, so their parameters leaked into the
final byte and ">4;m" was read as SGR 4 (underline on) with nothing left to
turn it off. All four markers are recognised now, and a private sequence is
never treated as SGR. The secondary device-attributes query (CSI > c) also gets
its own reply, so a program asking for it does not sit out its timeout.
Found by capturing the real byte stream from vi over a PTY and replaying it
through the emulator: the flags now come out clean, while a plain CSI 4 m still
turns underline on as it should.
Reported alongside it: after leaving insert mode with Escape the shell seemed
to stop accepting keys until the window was clicked. That does not reproduce
against a bare browser here — the same sequence types ":wq" straight through —
so the likely culprit is an input method leaving its composing flag set, which
would swallow every later key. The guard now trusts our own compositionstart/end
tracking instead of the browser's flag, and a committed composition still
reaches the shell.
The key bar carries only what a browser will not hand over: Ctrl+C, Ctrl+Z,
Ctrl+D, Escape, Tab and the arrows. It is a fallback, not the way to use the
terminal — typing works for everything else, and the bar deliberately does not
take focus so the keyboard keeps it. The arrow buttons ask the emulator for the
sequence, so they follow the mode a full-screen program requested just like the
real keys do.
Paste was broken on Windows and Linux. Ctrl+V fell through to the control-key
mapping and sent 0x16 with preventDefault, so the browser never got to fire a
paste event, and Shift+Insert did the same with the Insert sequence. Both, and
Ctrl+Shift+Insert, are now left to the platform; Cmd+V on macOS already worked.
The control keys were wrong in a way only running it showed: a control byte
cannot be written into an HTML attribute, so data-raw="\u0003" arrived as
six literal characters and the buttons typed them instead of sending them.
They carry a name now and the byte is looked up in JavaScript.
Verified against a real container: the bar's bytes (003, 032, 004, 033, ESC[A-D),
arrows inside less, Tab completion, Ctrl+V and Shift+Insert pasting, Enter
running a two-line paste, and Ctrl+C copying when something is selected.
Two things the key testing turned up.
Alt was wrong on macOS. Option+a arrives as the composed character "å", so
sending e.key put that in the shell instead of Meta-a. Both Alt and Ctrl now
take the letter from e.code (the physical key), falling back to e.key when a
browser does not report it; non-letters keep the plain ASCII mapping, which is
what makes Ctrl+[ an Escape.
The "bye" message was dead: the page never sends it, so the handler arm could
never run.
A browser reaching this panel is often a long way away, so the session is now
built to survive the link rather than be torn down by it.
Liveness first, because the old numbers were wrong: pings every 20s with a 60s
deadline. The previous setting was a single 15-minute timer, and since the
browser answers pings automatically it never fired for an idle user — the one
thing it did do was take fifteen minutes to notice a peer that had vanished.
- the shell outlives the socket. When the client goes away the PTY keeps running
for 120s with its output landing in a 256 KiB ring (with a notice if anything
is dropped). A client that comes back with its token resumes the same shell, so
a tunnel, a cell handover or a sleeping laptop no longer costs a running build.
The token travels in the first frame rather than the URL, is bound to the user,
and lives in sessionStorage — reloading resumes, closing the window lets go.
- the page has its own heartbeat: a browser cannot see protocol-level pings, so
the server also sends a small JSON one and the page reconnects with exponential
backoff and jitter after 45s of silence, up to eight attempts.
- a handshake that neither completes nor fails would otherwise stall the retry
loop for good, so an attempt that has not opened in 15s is abandoned.
- at the per-user cap the client is offered its slots back instead of being
refused, which is the only way to reclaim a window the user can no longer see.
Where there is room, the count is just a notice.
Three bugs the browser testing turned up, all of them real:
- Pty writes had no deadline, so a wedged Incus connection blocked the handler
forever — resume hung after any pause long enough to break that socket.
- a chunk whose write failed was neither delivered nor buffered.
- a session stayed resumable between Close and the end of its drain.
An operator can now turn the whole thing off with `vps config set panel.web_ssh
false`: the button is not rendered and the page, the script and the connection
all refuse. It defaults on, including for a config written before the setting
existed.
The terminal was a modal, which is the wrong container for it: a floating dialog
cannot be dragged to a larger size, moved to another screen or maximised, and
the terminal is exactly the thing that wants more room.
- the button moves up next to Start/Stop/Restart and is renamed Web SSH, which
is what people look for. It is styled apart from every other control on the
page on purpose: black key, green type, monospace, with a ">_" prompt in
front, so it reads as a remote shell at a glance.
- clicking it opens /webssh in its own browser window. That window is resizable
like any other, carries a Fullscreen button, and remembers the size it was
last dragged to so the next open comes back the same way. A blocked popup says
so instead of doing nothing.
- the overview page no longer loads the terminal engine at all; it only exists
in the shell window now.
Verified against a real container in the popup: prompt, typing, and a window
resize reaching the PTY (stty size moved from 26 99 to 16 86 when the terminal
area changed), which is the thing a modal could never do.
Adds a Web SSH-style terminal to the user panel, opened from the overview page.
It is deliberately not SSH. The container runs on the same host, so Incus' own
exec API with a PTY is the transport: no sshd, no keys to distribute, no network
path to arrange, and none of the credential handling a real SSH hop needs.
- lx gains an interactive session beside the fire-and-collect Exec: stdin stays
open, output streams as it is produced, and the window can be resized while the
shell runs. Exec and Attach now share the request-and-dial path.
- the panel serves /terminal over a WebSocket. That handshake is a GET, so the
panel's POST CSRF check never sees it, and SameSite does not cover it the way
it covers a form post — csrf.SameOrigin is what stands in for both. The handler
also takes the container from the session and never from the request, refuses
expired accounts, caps concurrent shells per user, and closes idle ones.
- the browser side is a small VT emulator, hand-written because the panel ships
as a single binary with no build step and a CSP that allows no third-party
scripts. It covers the usual CSI/SGR set, the alternate screen, scrollback,
UTF-8 including wide characters, and an IME via a hidden textarea so composed
text (Chinese, Japanese) reaches the shell. Terminal output is escaped, never
treated as markup.
Two protocol details were worked out against the running daemon rather than
assumed, and both are pinned by tests: an interactive exec returns only fds "0"
and "control" (stdout is folded back into "0"), and the resize message carries
its size as strings inside "args" — top-level width/height are silently ignored.
Checked against a real container: prompt, line wrapping, ANSI colour, CJK
output, less, a full-screen program (top), scrollback and window resize, with
the session ending and reporting its exit status when the shell exits.
Also fixes test isolation, which surfaced while running the suite on this host:
the panel and admin test servers used the default Incus socket, so tests that
drive /power, /reinstall and friends acted on whatever real instance shared a
fixture user's name — on this machine, restarting the real "alice" on every run.
Both now point the client at a socket that cannot exist. The suite also got
faster (panels 12s -> 7s) from no longer waiting on real timeouts.
User-panel knowledge base:
- a blank is now an exam-style underline instead of a box: no background, no
padding, and a height pinned to one line, so it no longer stretches the code
line it sits in. The placeholder is a clear yellow and the value keeps the
code's own colour;
- the width still follows the content, so a long pasted key extends the
underline while the rest of the line stays readable;
- the copy button is always present now, not just once every blank is filled: a
block may legitimately have optional or annotated blanks, and copying then
yields the text with those parts left empty;
- it starts breathing as soon as any blank in that block has content, and the
glow is actually visible (accent background plus a scale pulse) rather than
the button simply appearing;
- "copy all" moved up next to Close on the same row, and says what it does.
The blank styling also had to be scoped to .kb-article: the panel's generic
input[type=text] rule is more specific than a bare class, so it was winning and
painting the blanks as form fields. Caught by looking at the rendered page.
Admin panel: the batch form carried a paragraph of explanation per field.
Dropped the parentheticals and the "either way is fine" notes, leaving one
short hint on the username box.
Creating users one at a time does not scale when they are all carved out of the
same prepared machine, so the admin panel now takes a list:
- one username per line, a shared snapshot UUID, the operator keys to activate
on the new machines, and the resource quotas for the whole batch;
- the whole list is checked up front — syntax, duplicates, names already taken —
and a single problem creates nothing, so a rejected batch never leaves a
half-built account behind;
- the run happens in the background and the page polls for progress. It has to:
each user is a serial clone that takes tens of seconds, while the panel's
WriteTimeout is 60s, and a truncated response would also lose the one-time
passwords, which exist only in memory;
- only one batch runs at a time (creations are serialized by the manager
anyway) and the list is capped, so a pasted wall of text cannot churn the host.
Cloning is a new branch of Manager.Add rather than a parallel code path, so the
identity allocation (IPv4 slot, SSH port, user-port block, IPv6 block), the DB
row, the firewall and the rollback all stay in one tested place. Two clone-only
fixups are mandatory, not optional:
- the clone carries the source's /root/.ssh/authorized_keys, which would let the
owner of the source checkpoint log straight into every new machine, so it is
cleared even when no operator key was ticked;
- it also carries the source's machine-id, and a duplicate DUID makes dnsmasq
drop DHCPv6 leases.
The ticked operator keys reuse the existing admin_keys / admin_key_grants pair:
the batch just writes the grants the user would otherwise tick themselves, and a
failure there is reported next to that user instead of failing the container,
which is already usable at that point.
Checked end to end against a real Incus on the test host: a clone of another
user's snapshot came up with its own machine-id, hostname, IP and SSH port, with
the source's key gone and the operator key written as Vpsmgr-Operator. The host
was left exactly as found (container, nftables files and ruleset all restored).
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
A clone carries the source checkpoint's whole state, so its init script was
deliberately skipped — re-running it would duplicate whatever the checkpoint's
owner had already done. But someone importing another machine's image often
does want their own bootstrap to run on top of it, so make it a choice:
- the reinstall form gains a "运行初始化脚本" switch that follows the share-code
box: typing a UUID reveals it, clicking back to a plain image clears the code
and hides it again;
- the state is never remembered — it ships unticked, is forced off every time
the modal opens (which also defeats browser form-state restoration), and an
unticked box never reaches the request body;
- rolling back the user's own checkpoint ignores the switch: the state came
back from the checkpoint, not from an image;
- best-effort like a plain reinstall: a delivery failure warns and never fails
the rebuild, since the container is already usable at that point.
ReinstallFromShare takes the flag as a parameter rather than reading it from
anywhere global, so the caller stays the only thing that decodes the form.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
An operator can now write a prompt inside a fenced code block, where the
reader fills in the machine-specific part:
REGINNAME=*#*#填写地域信息#*#*
- the hand-written renderer splits the fence body on the directive, escapes
the literal runs as before, and emits an inline text input whose placeholder
and node id both come from the label. The id is a hash of the label (plus an
occurrence counter for repeats), so it is stable across edits and reordering
with no server-side storage, and no value is ever emitted — the answer lives
only in the reader's browser;
- a block containing blanks keeps its copy button hidden until every blank in
that block is filled, then pulses to draw the eye. The copied text is rebuilt
by walking the DOM, because innerText cannot see an <input>'s value;
- a new "copy all" button copies the whole article as Markdown with the same
substitution. It is fence-aware: a directive in prose renders no input, so
substituting it would shift every answer after it;
- the admin editor gains a one-line mention of the syntax, and its preview
styles the blanks so the author sees what the reader will get.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The installed unit started HAProxy with the static entry config alone:
-Ws -f /etc/haproxy/haproxy.cfg
That file declares the frontends, the reject backends and the timeouts; the
per-domain backends and the routing maps live in the generation the panel
publishes, and the maps' paths point at it. So HAProxy came up healthy, bound
80/443 and answered 404 on :80 and closed every :443 connection — no managed
domain was reachable. `haproxy -c` on the entry config alone passes, which is
why the install-time check did not catch it.
- the unit now passes both files, the generation through "current" so a reload
picks up whatever the panel published last;
- 30-haproxy.sh validates exactly that command line (entry + generation) and
requires domains.cfg AND both maps before it considers the bootstrap
generation in place, so the pre-start check can no longer pass a setup that
serves nothing;
- tests: TestUnitLoadsBothConfigFiles parses the unit's own ExecStart and fails
if it does not pass two config files, and TestUnitCommandLineRoutesTraffic
runs that command line against a real HAProxy and asserts a registered host
reaches its backend. Both fail against the old unit.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Two problems with `panel.bandwidth_reset_day`, both around its interaction
with the quota period:
- The user panel hardcoded "至次月 N 号", which is wrong whenever the reset day
is still ahead of today: the period in force started on that day of the
PREVIOUS month, so it ends on that day of THIS one (reset day 22 on the 16th
ends on the 22nd of this month). mgr.BandwidthNextReset derives the real
moment, the page flags whether it lands in this calendar month, and the label
follows. A test also pins it against BandwidthPeriod, so the label and the
rollover can never disagree about the boundary.
- Changing the day DISCARDS every user's accumulated bandwidth totals. That is
not an explicit reset: the counters are keyed by a period derived from the
day, so moving the day moves the key and the next sampler pass sees a new
period. It stays that way (it is the period model), but it is no longer
silent: the key is classified ApplyDestructive, so `vps config set` shows a
confirmation naming the loss, repeats it on the --no-apply path (where the
loss lands at the next panel restart instead), and prints it after applying.
docs/configuration.md explains the mechanism and points at the admin panel's
per-user "重置流量统计" button for a deliberate reset.
TestApplyBandwidthPeriodChangeDiscardsTotals pins the discard so the warning
cannot quietly become wrong, and TestBandwidthResetDayIsDestructive keeps the
classification from regressing to a plain restart.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
A host can sit on a pre-1.6 release (distro ndppd, old oneshot vps-ipv6.service)
for a long time before upgrading, so document and lock down how install.sh
converges it:
- the old ndppd/npd6 daemons are disabled and stopped, the unit is replaced by
the in-tree responder, and the legacy per-address kernel proxy_ndp entries
are swept on ipv6-reapply;
- /etc/vpsmgr/ndppd.conf is read AS-IS, so the nested `iface <bridge>` block
and its indentation written by the old panel must keep parsing. Adds
TestLoadRulesAcceptsPreResponderFile to a regression guard, since every
existing container would lose inbound IPv6 if that format ever diverged;
- the old sudoers grants disappear with the rewritten whitelist, and the
distro ndppd package is left installed but inert.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The panel now fronts user domains with HAProxy 3.4 LTS (official performance
packages) instead of Traefik: about 100 MB of RSS and 200 MB of disk saved for
a feature set that never used more than Host routing on :80 and SNI passthrough
on :443.
- hpx: publish the proxy configuration as immutable GENERATIONS. Every domain
mutation renders the whole set from the DB, validates it with the pinned
`haproxy -c`, and only then repoints the `current` symlink, so a rejected
candidate leaves the running proxy untouched and a stale domain can never
survive into a later generation. Routing is carried by two map files
(Host -> backend, SNI -> backend): per-domain frontend rules cannot work from
a second file because HAProxy rejects duplicate section names since 3.3 and
attaches bare directives to the section opened last.
- mgr: republish and reload (SIGUSR2, i.e. `systemctl reload`) after every
domain change, so a change takes effect without dropping established HTTP or
TLS connections. Rollback semantics (DB first, then publish) are unchanged.
- cfg,db: `net.traefik` is renamed to `net.haproxy` with identical values. A
pre-rename config.yaml key and the legacy VPSMGR_TRAEFIK env var are still
read (new name wins), and schema migration v18 moves the settings mirror from
`traefik` to `haproxy`, keeping a newer value when both rows exist.
- 30-haproxy.sh: install the pinned branch from the vendor repository and
migrate a Traefik host in one run — install and validate HAProxy first, then
remove Traefik's binary, unit, service account and dynamic rules (only its
config file is kept, inert). No fallback keeps both proxies alive.
- upgrade-haproxy.sh: follow the pinned branch to its newest patch, with a
configuration check and a graceful reload; `install.sh --update` runs it.
- tests: generation publish/rollback, map rendering, config-key and settings
migrations, plus real-binary tests (skipped unless
VPSMGR_TEST_HAPROXY_BIN is set) that run the generated configuration through
`haproxy -c` and start it in a network namespace.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The release body is the tag's own annotation, but the runner's checkout only
had the commit — no tag object — so `git cat-file -t` failed and every release
silently fell back to "fix some bugs" (v1.8.0 included). The publish job now
fetches tags, and the notes step fetches the tag ref explicitly before reading
it, so the annotation always makes it into the release body.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Addresses two review findings:
- the dynamic CPU limit applied a cap and only then saved the active state. A
failed save left the cap in place with no record, so it could never expire
and switching the rule off would not lift it — the container would stay
pinned to the reduced quota. The caps applied by a pass are now held back
until the state is written; if the write fails they are lifted again (each
back to the user's own quota) and the failure is reported. The quota writer
is behind a one-method interface so the failure path is covered by a test
that fails the state write with a trigger and asserts cap-then-rollback, plus
a test that a failed rollback is reported rather than swallowed
- migrations only compared the highest recorded version, so rows missing from
schema_migrations silently skipped a step and surfaced later as obscure "no
such column" failures. The recorded versions must now form the unbroken run
2..cur (v1 is the baseline, created unconditionally and never recorded) or
the database refuses to open
The third finding (a failed cross-container share install deleting the old
container before the new one is up) is left as is: the user confirms a
reinstall that discards the data, and the retry costs nothing — Delete treats
an already-missing container as success (verified against the live Incus
socket), so the user simply installs again.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- 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>
- the publish job reads the tag's own message and uses it as the release
body: `git tag -a vX.Y.Z -m "..."` writes the notes shown on the release
page
- a lightweight tag carries no message, so the body falls back to
"fix some bugs"; GitHub still appends its auto-generated compare link
below whatever body is passed
- a signed tag's PGP block is stripped from the notes
- document the annotated-tag flow in docs/development.md
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>
- add a "User groups" section: name-derived grouping, name validation, the
shared panel password and accent color, and the container switcher
- describe the switcher labels and the per-container sticky-notes tag
- note the accent color is group-wide in the color section
- list multi-container users in both READMEs
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>