Commit Graph
239 Commits
Author SHA1 Message Date
bluebluesodaandCommandCodeBot 8d2d31041f docs(ipv6): pin the pre-responder ndppd upgrade path
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>
2026-09-16 13:07:13 +00:00
bluebluesodaandCommandCodeBot 4c93ae4be3 feat(haproxy): replace Traefik with a pinned HAProxy domain proxy
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>
2026-09-16 12:59:13 +00:00
bluebluesodaandCommandCodeBot f6e0727ca4 ci(release): fetch the tag object before reading its notes
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>
2026-09-16 07:41:27 +00:00
bluebluesodaandCommandCodeBot 1c6df157a5 fix(mgr,db): make an unpersisted CPU cap self-correcting; refuse a gapped schema
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>
2026-09-16 07:31:11 +00:00
bluebluesodaandCommandCodeBot c2009ddc69 chore(copy): tighten 1.8 wording, drop the "was X (not Y)" asides
- 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>
2026-09-16 07:12:06 +00:00
bluebluesodaandCommandCodeBot 3c48c27cc3 fix(admin): the CPU limit card no longer claims to be in effect while it is off
- 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>
2026-09-16 07:00:27 +00:00
bluebluesodaandCommandCodeBot 83f0f02d45 feat(panel,admin): fix knowledge preview, panel-native confirms, CPU editor, persistent admin sessions
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>
2026-09-16 06:51:35 +00:00
bluebluesodaandCommandCodeBot 627543da07 feat(panel,audit): keep panel-password access on expired accounts; fill audit gaps
- 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>
2026-09-15 17:41:19 +00:00
bluebluesodaandCommandCodeBot 1078e99cfa ci(release): take release notes from the tag annotation
- 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>
2026-09-15 17:03:14 +00:00
bluebluesodaandCommandCodeBot 76e6b0990c feat(quota): per-container validity period with expiry lockdown
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>
2026-09-15 16:58:37 +00:00
bluebluesodaandCommandCodeBot 8cb5c7cb82 feat(knowledge): admin-authored knowledge base with a user-side reader
- 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>
2026-09-15 16:40:26 +00:00
bluebluesodaandCommandCodeBot 3b8cfdcd93 refactor(settings): move snapshot sharing and CPU limit to config.yaml
- `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>
2026-09-15 16:25:00 +00:00
bluebluesodaandCommandCodeBot 65b54d7970 feat(snapshot): admin toggle for snapshot sharing
- 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>
2026-09-15 16:08:37 +00:00
bluebluesodaandCommandCodeBot 2363b89743 feat(snapshot): share a checkpoint to install from it
- 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>
2026-09-15 15:57:30 +00:00
bluebluesodaandCommandCodeBot 0e4528ad85 feat(cpu): global dynamic CPU limit rule
- 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>
2026-09-15 14:44:54 +00:00
bluebluesodaandCommandCodeBot 3df47aece0 fix(panel): show whole GiB for large switcher memory
- 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>
2026-09-12 08:31:33 +00:00
bluebluesodaandCommandCodeBot 8a73d1bd0f fix(panel): use MiB for the switcher memory unit
- MachineSpecs now renders "<cpu>c <mem>m <disk>g" (e.g. "2c 2048m 15g")
  instead of converting memory to GiB
- update the pinned group/panel tests

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-12 08:19:02 +00:00
bluebluesodaandCommandCodeBot 209383806b feat(bandwidth): configurable monthly reset day
- 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>
2026-09-12 08:09:44 +00:00
bluebluesodaandCommandCodeBot 0c159ae504 fix(admin): attribute impersonated container switches to the operator
- 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>
2026-09-12 07:15:25 +00:00
bluebluesodaandCommandCodeBot 2e2022fd1f fix(panel): space out group switcher spec labels
- 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>
2026-09-12 07:15:22 +00:00
bluebluesodaandCommandCodeBot 44c3727ad3 docs: document multi-container user groups
- 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>
2026-09-11 06:41:06 +00:00
bluebluesodaandCommandCodeBot 770b763d2a fix(cli): keep panel/ssh output when the group password is inherited
- 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>
2026-09-11 06:41:01 +00:00
bluebluesodaandCommandCodeBot 77b3fa927a feat(panel): compact machine labels in the group switcher
- 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>
2026-09-11 06:40:56 +00:00
bluebluesodaandCommandCodeBot 69d771d17d fix(admin): sort users by name by default
- make the initial user list order deterministic
- keep manual column sorting behavior intact

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-09 08:33:25 +00:00
bluebluesodaandCommandCodeBot bb4f9160d1 fix(admin): preserve create-form helper text
- 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>
2026-09-09 08:11:11 +00:00
bluebluesodaandCommandCodeBot 77a98d3824 fix(admin): improve create-user form layout
- 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>
2026-09-09 08:04:02 +00:00
bluebluesodaandCommandCodeBot f5e26a376a fix(panel): show group-specific machine labels
- 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>
2026-09-09 07:54:32 +00:00
bluebluesodaandCommandCodeBot b327000001 fix(panel): clarify machine terminology and switching
- 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>
2026-09-09 07:41:14 +00:00
bluebluesodaandCommandCodeBot e6bf390da0 feat(panel): add grouped multi-container users
- 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>
2026-09-09 07:21:02 +00:00
bluebluesodaandCommandCodeBot 6856e510a4 feat(addon): add container transparent proxy templates
- add sing-box, nftables, and systemd deployment templates
- document external GeoIP rule-set setup

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-08 13:17:14 +00:00
bluebluesodaandCommandCodeBot a64df2ac96 feat(panel): show build version in user footer
- add configurable user footer visibility
- render the injected release or local-build version

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-08 13:15:58 +00:00
bluebluesodaandCommandCodeBot 9b68d7b7b6 fix(panel+admin): checkbox color clash and over-wide IPv6 column
- 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>
2026-09-06 12:52:54 +00:00
bluebluesodaandCommandCodeBot 602544c7bb feat(passwd+admin): real-time rule checks; usernames fold to lowercase
- 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>
2026-09-06 12:18:27 +00:00
bluebluesodaandCommandCodeBot e012352b31 feat(passwd): require >=10 chars with letters and digits on password change
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>
2026-09-06 10:03:31 +00:00
bluebluesodaandCommandCodeBot 7e87cfd373 fix(admin): reset-password modal login-as posts a blank name; panel borders 2px
- 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>
2026-09-06 06:14:03 +00:00
bluebluesodaandCommandCodeBot ba3d23af82 fix(panel): restore gray-blue light bg; bold port block; v4-off notice
- 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>
2026-09-06 05:59:52 +00:00
bluebluesodaandCommandCodeBot c0ce7a422d fix(panel): style port block like IPs; hide it when v4 forwarding is off
- 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>
2026-09-06 05:43:48 +00:00
bluebluesodaandCommandCodeBot 9dc133f99a feat(panel): rework user overview connectivity UI
- 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>
2026-09-06 05:21:53 +00:00
bluebluesodaandCommandCodeBot b46bd0a901 fix(panel): theme color only on admin impersonation; thin borders back to 1px
- 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>
2026-09-06 04:52:37 +00:00
bluebluesodaandCommandCodeBot 01869464cb fix(panel): responsive layout for sticky notes on narrow screens
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>
2026-09-05 15:44:40 +00:00
bluebluesodaandCommandCodeBot b1d40a1107 chore(notes): halve sticky-notes plaintext cap to 256 KiB
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-05 08:27:23 +00:00
bluebluesodaandCommandCodeBot 48d8cafe2f fix(install): skip 50-image when incus.image is a custom alias
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>
2026-09-04 13:33:12 +00:00
bluebluesodaandCommandCodeBot a8bc34a325 feat(admin): drop the plan-preset hint line
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>
2026-09-04 12:30:47 +00:00
bluebluesodaandCommandCodeBot 817b23c922 feat(admin): add S/M/L plan presets to the create-user form
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>
2026-09-04 12:22:07 +00:00
bluebluesoda f2d3eee4d8 fix(install): fall back to IPv4 when go.dev download fails over IPv6
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
2026-09-04 08:53:34 +00:00
bluebluesoda e25e729de7 fix(ipv6): disable npd6 alongside ndppd to avoid competing NAs
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)
2026-09-04 08:47:36 +00:00
bluebluesoda c9b18ffee5 Merge remote-tracking branch 'origin/dev-ndp'
# Conflicts:
#	src/internal/admin/handlers.go
2026-09-04 08:33:56 +00:00
bluebluesoda 69f79187c8 feat(ports): replace net.slot_range with net.user_ports; probe SSH ports at add time
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.
2026-09-04 08:32:13 +00:00
bluebluesoda 3fdb946baa Update 80-debian-dev-image.sh 2026-09-02 23:30:25 +08:00
bluebluesoda fd6448bd27 Update handlers.go 2026-09-02 19:04:21 +08:00