Commit Graph

399 Commits

Author SHA1 Message Date
5a1fb72660 i18n: add bulk_migrate and access realm keys (ru + en)
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
instances: bulk_migrate_title/desc, target_node, select_node,
           online_migration, status_migrating, nothing_to_migrate,
           migrate_same_server
access: user_attr, username_claim translated to Russian
2026-08-13 16:24:52 +05:00
008e29b70f i18n: add missing nodes power and common action keys (ru + en)
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
nodes: reboot, shutdown, shell, confirm_reboot, confirm_shutdown,
       reboot_started, shutdown_started
common: comment, add, clone, iso, notes, reinstall, done, copied, message
2026-08-13 16:21:30 +05:00
80d7a98256 i18n: fix critical untranslated strings across the UI
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
- DestroyTab: wrap all hardcoded English strings with t()
- instances: add status_reinstalling, status_changing_password keys
- common: add shutdown key (ru + en)
- fw: translate groups, ipsets, aliases, security_group and related actions
- nodeadm: translate upgrade button label
- en.json: fix Russian placeholder_notes in English locale
2026-08-13 16:17:07 +05:00
d88647de77 i18n(roles): add missing permission label translations to Russian
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 18s
node_power, app_view/install/manage, script_view/execute/manage
2026-08-13 15:57:46 +05:00
82a3ae72be i18n(roles): translate remote_migrate permission labels to Russian 2026-08-13 15:56:00 +05:00
a7fda196b5 docs: release notes and version bump for v1.15.0
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
2026-08-13 15:41:27 +05:00
8337539e9e i18n(users): translate the terminated-sessions toast
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 2m12s
Deploy to Production / Trigger production update (push) Failing after 11s
Both toasts printed the endpoint's own message field verbatim, so a Russian UI
answered "Terminated 0 sessions". That string is assembled in Python and can
never be translated where it is built, so the endpoints now also return the raw
count and the phrase is composed on the client.

message is kept: it goes to the audit log and may have other consumers, and
adding a field breaks no one.

Russian needs three forms, and zero does not land in the obvious one — 0 is the
"many" category ("сессий", not "сессии") — hence four keys against English's
two.

Fixes both call sites: the per-user button in the table row and Terminate all
sessions on the Sessions tab, which shared the same untranslated string.
2026-08-12 15:00:07 +05:00
9e4120bd8d feat(instances): disable Create Instance when the quota is used up
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
The wizard already refuses to open once a limit has no headroom left, but the
button leading to it still looked available, so the only way to find out was to
click it. Reuse exhaustedMetrics() from the wizard's quota module, so the button
and the wizard cannot disagree about what counts as exhausted, and skip the
check for admins for the same reason the wizard does — their own quota says
nothing about an instance they create for someone else.

The button is rendered as a Link, and disabled does nothing to an <a>: it would
still navigate. So the exhausted case renders a plain button with no Link, and a
tooltip naming the limits that are full. The trigger wraps it in a span because
a disabled button receives no pointer events of its own.

The wizard keeps its own block, which is what still covers navigating straight
to /instances/create.
2026-08-12 14:44:42 +05:00
91fa1b9971 fix(nginx): raise limit_conn so HTTP/2 stream count stops throttling the app
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
Rebuilding the frontend surfaced the limiter sitting behind the request-rate
one fixed in 935c804: with limit_req no longer rejecting the bundle, the
cold load started failing on `limiting connections by zone "addr"` instead,
and the page went blank again.

nginx counts each concurrent HTTP/2 stream against limit_conn, and a browser
multiplexes the entire page over a single connection, so `limit_conn addr 10`
capped the whole page at ten in-flight requests. That is far below the 89
files the build emits, and it would equally have throttled the parallel API
queries the dashboard issues after login — the login screen just happened to
be where it showed.

Raise the server-level cap to 100 and give the static-asset location 200.
Verified against the live deployment: 89/89 assets return 200, the login
page renders, and nginx has logged no limiting events since the reload.
2026-08-11 21:24:17 +05:00
925b5c8664 chore: mark the shell entry points executable
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
deploy.sh, bootstrap.sh, update_host.sh, setup_ssh.sh and pve all carry a
#!/bin/bash shebang and are invoked directly, but git recorded every one of
them as 100644. A fresh clone therefore could not run ./deploy.sh, and any
host where the bit had been fixed by hand showed a permanently dirty
working tree — the mode was the only difference.

Record the bit so a clone is runnable and `git status` stays clean.
2026-08-11 21:14:16 +05:00
c981a14cf9 chore(nginx): stop tracking the generated site config
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 11s
nginx/conf.d/pvemanager.conf is not source: setup_nginx_config() writes it
on every deploy by copying one of the .template files and substituting the
domain. Tracking it meant the repository carried one deployment's hostname,
and every deploy showed the file as modified.

Untrack it and ignore it. The templates it is generated from stay tracked,
so nothing is lost — a missing copy is restored by re-running deploy.sh, or
by hand:

    cp nginx/conf.d/pvemanager.conf.template nginx/conf.d/pvemanager.conf
    sed -i "s/DOMAIN_NAME/<domain>/g" nginx/conf.d/pvemanager.conf

Note for existing checkouts: this commit deletes the path, so a deployed
host that pulls it will lose its live config unless the file is regenerated
afterwards.
2026-08-11 21:10:09 +05:00
0662dda6ae fix(compose): actually drop the published ports in production
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 11s
compose.prod.yml has always intended to take the app and frontend off the
host interface and let nginx front them — "remove port exposure (nginx will
handle it)". It did not work: Compose concatenates list-valued fields when
merging -f files, so `ports: []` contributed nothing and 8000:8000 and
3001:80 survived from compose.yml.

The effect was that an SSL deployment still answered on both ports over
plain HTTP, bypassing TLS, HSTS, the security headers and the rate limits —
confirmed against the live host from off-box, where the API returned its
health payload unencrypted on :8000 and the panel loaded on :3001.

Use the !reset tag so the inherited value is dropped rather than extended.
Verified in place: the merged config now publishes only 80 and 443, both
former ports refuse connections from outside, and the panel still serves
over HTTPS.
2026-08-11 21:05:42 +05:00
935c80427f fix(nginx): stop rate-limiting the frontend bundle into a blank page
Loading the panel over the domain produced a white page while the same
panel served directly from the frontend container rendered fine. The built
SPA requests 86 files on a cold load — one entry module plus 85 preloaded
chunks — and the server-level

    limit_req zone=general burst=20 nodelay;   # general = 10r/s

applies to those static assets too. Measured against the live site, 60 of
the 86 came back 503 and nginx logged "limiting requests ... by zone
general" for each. The entry module was among the casualties, so React
never booted and the document stayed empty.

Give static assets their own zone and restate the limit in a location that
matches them. The limit has to be restated rather than omitted: nginx
inherits limit_req from the enclosing level only when a location declares
none, and there is no `limit_req off`.

Both templates get the block, since the non-SSL config serves the same
bundle. Verified on the live deployment: 86/86 assets now return 200 and
the login page renders with no console errors.
2026-08-11 21:05:30 +05:00
74b7a9fe73 fix(nginx): raise the container file descriptor limit to match worker_connections
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
nginx.conf configures 2048 worker_connections, but Docker's default
RLIMIT_NOFILE of 1024 capped what the workers could actually open — and
since a proxied connection needs a descriptor both downstream and upstream,
the real ceiling was about a quarter of the configured one. nginx warned
about exactly this on every config test.

Raise nofile to 8192 for the nginx container. Verified in place: the limit
goes from 1024 to 8192 and `nginx -t` is now free of warnings.
2026-08-11 20:47:49 +05:00
5f20b87824 fix(nginx): drop two directives that only produced reload warnings
`listen 443 ssl http2` has been deprecated since nginx 1.25.1 in favour of
a separate `http2 on;` directive, and Let's Encrypt no longer publishes an
OCSP responder URL in its certificates, so `ssl_stapling` had nothing to
staple. Both warned on every reload — noise that now matters more, since
nginx reloads itself every 6h to pick up renewed certificates.

Reloads are clean afterwards and HTTP/2 is still negotiated.
2026-08-11 20:38:03 +05:00
c6a32255df fix(ssl): stop reporting a working certificate as a failed deployment
The SSL deployment aborted even when Let's Encrypt had issued a perfectly
valid certificate. obtain_ssl_certificate() confirmed success with

    [ -d "nginx/certbot/conf/live/${domain}" ]

but certbot runs as root inside its container and creates live/, archive/
and accounts/ with mode 0700. Whenever deploy.sh runs as a non-root user
that test fails with EACCES no matter what is on disk, so every SSL deploy
fell back to the HTTP-only config and printed "Failed to obtain SSL
certificate" right after certbot reported the certificate was fine.

Check the certificate from inside a container instead, and treat the files
on disk as the source of truth rather than certbot's exit code, so a
certificate that is present and not yet due for renewal counts as success.
Pass --keep-until-expiring to make that case explicit.

Also drop the recursive chmod on nginx/certbot: it could not touch the
root-owned files it was aimed at (producing the wall of "Operation not
permitted" noise) and would have exposed the private keys if it had.

Verify nginx actually came up after switching to the SSL config and roll
back to HTTP if it did not — a crash-looping nginx takes the whole panel
offline, which is worse than serving without HTTPS.

Reload nginx every 6h so renewed certificates are picked up: nginx reads
the certificate once at startup, so the renewal loop would otherwise keep
serving the old one until the container happened to restart.

Ignore nginx/certbot/ — it holds Let's Encrypt private keys and was
untracked but not ignored.
2026-08-11 20:28:58 +05:00
bddbf15652 feat(rbac): split setting:view into panel settings and a permission-free profile
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m22s
Deploy to Production / Trigger production update (push) Failing after 11s
setting:view used to gate the whole /settings page, which mixed panel-wide
configuration with a user's own account. Revoking it to keep a standard user
out of the panel settings also took away their profile, password, 2FA and SSH
keys, so the permission could not be used for what it names.

The account half now lives at /profile: its endpoints are self-scoped and only
require a session, so the page carries no permission at all and is reached from
the user menu (and Ctrl+K) rather than a sidebar group. /settings keeps the
panel-wide tabs behind setting:view, with saving gated on setting:update,
security on setting:manage and the SMTP/Telegram channels on is_admin, matching
what each endpoint checks.

The shipped "user" role no longer gets setting:view. Migration 42 drops it from
roles holding it without setting:update/manage, since those grants only existed
to reach the profile page; it runs once (marker in panel_settings) so a later
deliberate grant is not reverted on the next start.
2026-08-11 20:07:34 +05:00
991daddd63 fix(rbac): guard routes by permission and hide admin-only tabs
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
Hiding a sidebar entry was the only frontend gate: /users, /logs and
/settings opened for anyone who typed the URL, and their pages rendered
every tab regardless of the caller (the backend then answered the tab's
requests with 403s).

Route elements are now wrapped in RequirePermission, gated on the same
permission the page API requires, so a hidden menu item can no longer be
reached by URL. Denied users land on the first page they can open, or on
an explanatory screen when nothing is reachable, so the redirect cannot
loop. Permission checking moves into lib/permissions.ts and canSeeNavItem
delegates to it, keeping menu and guard in sync.

Settings hides the panel tab without setting:update, the security tab
without setting:manage, the SMTP/Telegram cards from non-admins, and the
update controls behind setting:manage; profile, SSH keys and personal
notification preferences stay available to everyone. Users hides the
roles tab without role:view and the security tab without setting:manage.
2026-08-11 18:44:20 +05:00
c40611bedb feat(instances): tell users which quota is in the way before the wizard
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
Quotas were only enforced by check_quota() on the backend, which answers 429
from the deploy endpoint — so a user walked all five wizard steps and learned
about the limit on the very last click. QuotaHint coloured the offending number
red but blocked nothing.

Split the two situations the UI never distinguished:

  * No headroom at all (used >= limit): no configuration would ever fit, so the
    wizard does not open. It is replaced by a message naming exactly which
    limits are used up, with their numbers.
  * Headroom left but the chosen configuration does not fit (used + requested >
    limit): the wizard opens, and from the config step onwards it names the
    metric and disables Next/Deploy until the resources are reduced.

The overrun check deliberately stays quiet until the config step. Gating every
step meant a default size that did not fit the remaining quota stranded the user
on the server picker, with no way to reach the fields and shrink it.

Neither block applies to admins: their own quota says nothing about an instance
they are creating on behalf of someone else, and blocking on it would lock an
admin with a quota out of the wizard entirely. The figures are still shown.

Also label the quota card as a projection. It renders used + the instance being
created, so a user with 3 of 4 instances saw "4 / 4" and reasonably read it as
already exhausted — which is what prompted this.

The arithmetic lives in features/instances/quota.ts as pure functions rather
than inline in JSX, so the two thresholds can be reasoned about on their own.
2026-08-11 18:21:51 +05:00
c4fda1b0bb feat(migrations): backfill the owner of instances created before the fix
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m20s
Deploy to Production / Trigger production update (push) Failing after 10s
Instances deployed while save_vm_instance() dropped the owner are still sitting
in the database with owner_id = NULL, so their users cannot see them. Recover
the creator from the two places it was recorded:

  1. deploy_tasks — user_id is a real foreign key stored when the deploy was
     queued. server_id is absent for LXC deploys, so a NULL there is treated as
     matching any server instead of discarding the row.
  2. audit_logs — vm_create / ct_create / lxc_create entries, which only carry a
     username and are matched by name.

Ownership decides who may see, stop and delete an instance, so a wrong guess is
worse than no owner at all: a row is updated only when every applicable piece of
evidence names the same existing user. VMIDs get reused, so conflicting evidence
is expected and those instances deliberately stay ownerless for an operator to
assign with assign_instances_owner.py. Existing owners, soft-deleted rows,
failed create events and logs from deleted users are all left alone, which also
makes the migration idempotent.

Note this recovers who created an instance, not who it was meant for: a VM an
admin created on behalf of a user is attributed to the admin, because the
intended owner was never persisted before the fix.

The attribution rule is a pure function (pick_backfill_owner) so it can be
tested directly. The migration's own statements are portable, so the tests run
it against SQLite with the two information_schema helpers stubbed out.
2026-08-11 17:28:59 +05:00
1152410860 fix(instances): assign the owner on create and inject the owner's SSH keys
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 2m30s
Deploy to Production / Trigger production update (push) Failing after 10s
Newly created VMs/LXCs ended up with owner_id = NULL, and a VM created by an
admin on behalf of a user received the admin's SSH key instead of the user's.

Ownership: save_vm_instance() only set owner_id when inserting a new row. The
row practically always existed by then — sync_vm_cache runs every 10s and
registers VMs it sees on Proxmox with owner_id = NULL while the clone is still
running, whereas the deploy saves the owner minutes later at 95% progress. So
every deploy landed in the update branch and silently dropped the owner. Set
owner_id there too, but only when it is passed explicitly: callers such as
save-before-delete do not know the owner and must not blank it out.

SSH keys: the profile key was taken from whoever pressed "Create" rather than
from the instance owner, and the LXC path injected no profile key at all. Add
build_deploy_ssh_keys(), which merges pasted keys, keys picked from the library
(same authorization rules as before) and the owner's profile key, deduplicated.
Both the template and LXC deploy endpoints now use it, so the key material is
resolved in one place instead of inside the deploy worker.

ISO VMs are left alone on purpose: they are blank VMs without cloud-init, so
there is nowhere to inject keys.

Covered by backend/tests/test_instance_ownership.py, including the sync race.
2026-08-11 17:19:19 +05:00
markmorado
e2d776bb6c fix(bootstrap): register the host at the correct /proxmox/api/servers path
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
The proxmox router is mounted under the /proxmox prefix in main.py, so the
real create-server endpoint is /proxmox/api/servers, not /api/servers as
the registration step assumed. The bare /api root only has /api/auth/*.

Verified end to end against a real Proxmox host: LXC provisioning, Docker
install inside it, deploy.sh --standalone build+run, admin login, local
root@pam token creation, and registration all completed successfully,
with the panel reporting the new node online.
2026-07-25 01:01:38 +05:00
markmorado
35220a1458 fix(bootstrap): tolerate apt update failures from the PVE enterprise repo
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
On an unsubscribed Proxmox host, apt's enterprise repo answers 401 and
apt-get update exits non-zero even though every other source succeeded,
aborting the whole bootstrap under set -e before git/curl were ever
installed. The subsequent apt-get install still fails loudly if a package
genuinely can't be found, so tolerating this specific failure is safe.

Found by running bootstrap.sh against a real Proxmox host.
2026-07-25 00:47:03 +05:00
markmorado
e81abf33df fix(nodes): stop showing wrong node uptime for multi-node clusters
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Server card used Math.max over all cluster nodes' uptime, so
rebooting one node still showed another node's higher uptime,
making it look like the reboot had no effect.
2026-07-25 00:44:56 +05:00
markmorado
f4f917c73e feat(install): add one-command bootstrap for regular hosts and PVE hosts
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Adds bootstrap.sh implementing Stage 3 of the roadmap: it detects whether
it runs on a plain Debian/Ubuntu host or directly on a Proxmox VE host.
On a PVE host it provisions a Debian 12 LXC, installs the panel inside it
via the existing deploy.sh, then creates a root@pam API token locally and
registers the host as the panel's first node — no Proxmox password is
ever transmitted.

Also removes the insecure pvemanager_secure_password fallback for
POSTGRES_PASSWORD in compose.yml, since every deploy path already
generates a real .env before the stack starts.
2026-07-25 00:44:08 +05:00
markmorado
b7c3a6ed35 feat(instances): ISO eject/mount with boot-order switch and hybrid reboot
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
- Images: show real ISO files already on the node storage (incl. those
  present before the node joined the panel) via existing node/isos endpoint
- ISO dialog: rename "detach" to "Eject", show current image inside the
  select (drop overflowing side text), add "boot from disk" checkbox
- Backend: on eject, put disk first in boot order; on mount, optional
  "boot from this ISO" puts CD-ROM first
- Reboot to apply the new boot order via hybrid restart: graceful ACPI
  shutdown with forceStop fallback, then start (fixes guest-ping timeout
  on freshly installed OS without QEMU Guest Agent)
- Update iso_next_steps hint to reflect the automated flow
2026-07-24 23:00:32 +05:00
markmorado
aa100b49c1 docs: release notes and version bump for v1.14.0
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
2026-07-24 07:00:46 +05:00
markmorado
29a5d4381d fix(instances): keep workspace-scoped filters in sync with active workspace
The per-column "Node" filter memoized its option list against TanStack's
Column object, which keeps the same identity across data updates, so the
checklist stayed frozen at whatever nodes were visible when the popover
first mounted instead of reflecting the current (workspace-scoped) rows.

Switching the active workspace also left the server/node/column filters
holding values from the previous workspace, which could filter the newly
scoped VM list down to nothing.
2026-07-24 07:00:41 +05:00
markmorado
dfe91964d9 docs: release notes and version bump for v1.13.0
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
2026-07-24 06:58:21 +05:00
markmorado
c526be3bb2 feat(appstore): add storage selection to golden template dialog
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Replace free-text storage inputs with dropdowns populated from the
server/node's actual storages (filtered by vztmpl/rootdir content),
matching the pattern used elsewhere in the app.
2026-07-24 06:52:30 +05:00
markmorado
3d63f4fb51 feat(scripts): add name search to scripts catalog
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
2026-07-24 06:42:43 +05:00
markmorado
d0b4c70c72 docs: release notes and version bump for v1.12.0
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
2026-07-24 06:39:24 +05:00
markmorado
9b493da7ad feat(cluster): import existing Proxmox cluster into the panel
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Add detect/import flow for nodes already clustered on the Proxmox side
(outside the panel). Matches nodelist entries from cluster/config/join
against known panel servers by IP/hostname and links their panel
records to the cluster_name group, without running any pvecm operation.
2026-07-24 06:35:34 +05:00
markmorado
b804a9bec5 docs: release notes and version bump for v1.11.0
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
2026-07-24 06:30:51 +05:00
markmorado
99d8ce750d feat(images): support uploading local ISO/vztmpl files to node storage
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Adds a local file upload path alongside the existing URL download flow:
a new /images/upload endpoint streams the file to a temp path and hands
it to a background worker that proxies it into Proxmox storage via the
upload API (requests-toolbelt handles the large multipart stream). Also
adds /images/resolve-filename to recover the real filename (Content-
Disposition) for URL-based downloads with opaque paths.

Raise nginx client_max_body_size and timeouts on both the frontend proxy
and the standalone nginx configs so multi-GB ISOs don't get cut off.
2026-07-24 06:17:46 +05:00
markmorado
8b4c8c3a30 fix(realtime): sync instance/workspace list state without manual refresh
- Invalidate workspace list cache after assigning servers/users so
  server/user counts on workspace cards update immediately.
- Invalidate node/VM caches after workspace server/user assignment so
  the Nodes page reflects visibility changes right away.
- Soft-delete the cached VM/LXC row and broadcast vm_deleted the moment
  a VM/container is destroyed, instead of waiting for the 10s
  background sync_vm_cache tick.
- Invalidate the VM list on the frontend when a deploy task completes,
  since the backend already persists the new instance to cache
  synchronously before marking the task done.
2026-07-24 05:39:23 +05:00
markmorado
e10f9a2a03 refactor(servers): unify connection status badges
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Deploy to Production / Trigger production update (push) Has been cancelled
Extract ServerStatusBadge so every server list renders the auth-error
state identically instead of repeating the online/offline ternary, and
expose last_error_kind from the cluster topology and user-assignment
endpoints, which build their payloads by hand and so did not inherit it
from the server schema.
2026-07-24 05:04:45 +05:00
markmorado
ebc692a324 feat(instances): create blank VMs for ISO installs and download ISOs
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Deploy to Production / Trigger production update (push) Has been cancelled
Add a third instance kind to the create wizard: a blank KVM VM with an
empty disk, the installation ISO mounted on ide2 and put first in the
boot order, so the guest boots straight into the installer over noVNC.
Picking a Windows guest presets OVMF, a SATA disk and an e1000 NIC,
which the installer sees without virtio drivers loaded.

Also teach the image downloader content=iso, so ISOs can be fetched
onto a node from the panel: by URL from the images page or from a saved
mirror (mirrors now accept kind=iso), and straight from the ISO step of
the wizard.

The bundled catalog stays qcow2-only on purpose — install ISOs have no
stable "latest" URLs, so hardcoded links would rot on every point release.
2026-07-24 05:00:51 +05:00
markmorado
6a4da4f80b docs: revise the one-command install stage
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Deploy to Production / Trigger production update (push) Has been cancelled
deploy.sh already covers most of the installer (OS detection, Docker setup,
random secret generation, nginx, TLS, image build, pve CLI), so the stage is
about publishing a thin bootstrap rather than writing one from scratch.

Records the decisions taken: the bootstrap detects its environment and
creates an LXC when run on a PVE host, the repository is public, and images
keep being built locally. On a PVE host the node can register itself without
a password via pvesh, reusing the token provisioning from stage 1.
2026-07-24 04:43:07 +05:00
markmorado
075b8cdaaa feat(servers): distinguish rejected credentials from unreachable nodes
An offline server used to report a single "Failed to connect" state, but
"the node does not answer" and "the node answered 401" are fixed in
completely different ways — the first is power or network, the second is a
stale token, which is what happens when a node joins a cluster and its
user.cfg and priv/token.cfg are replaced by the cluster ones.

- add ProxmoxClient.check_connection() returning (ok, error_kind, error);
  token auth surfaces 401 as ResourceException rather than
  AuthenticationError, since proxmoxer validates the token on first use
- store the reason in proxmox_servers.last_error_kind (migration 40) and
  expose it through the server response
- surface an auth_error state in the UI with a hint explaining the usual
  cause

Storage listings now also report per-node availability: the cluster config
declares a storage for the whole cluster but says nothing about whether it
actually came up on a given node, so a missing volume group or pool stayed
invisible. Availability comes from a single /cluster/resources call.
2026-07-24 04:42:59 +05:00
markmorado
db7411b1bf docs: add development roadmap
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Deploy to Production / Trigger production update (push) Has been cancelled
Records the six agreed directions with their status, the files each one
touches, the steps involved and the decisions already made, so the work can
be picked up without prior context.
2026-07-24 04:03:37 +05:00
markmorado
65c61e7886 feat(servers): auto-provision API tokens and add node metric graphs
Servers can now be connected with a login and password: the panel
authenticates once, creates its own API token on the node (privsep=0) and
uses it from then on. The token can be revoked from the Proxmox UI, cannot
be used for SSH or web login, and appears in the PVE audit log under its
own name. The password stays stored (encrypted) because ProxmoxClient needs
it for SSH fallbacks such as pvecm delnode.

- add proxmox/token_provisioning.py: token creation with free-name
  selection and revoke-on-failure so a failed registration does not leave
  an orphaned token whose secret was never persisted
- POST /api/servers accepts auto_create_token
- add POST /api/servers/{id}/provision-token to migrate servers that are
  still on password auth
- store api_token_value as EncryptedString; it was kept in plaintext while
  the password next to it was already encrypted
- drop the unused ProxmoxServerBase schema, a stale copy of the create
  schema whose validator rejected the new auth mode

Node detail pages gain a Graphs tab (CPU, IO delay, memory, load average,
network, swap, root filesystem) built on the existing node rrddata
endpoint. The chart card is extracted from the instance graphs tab so both
views share it, and X axis ticks now switch to dates on multi-day ranges.
2026-07-24 04:03:30 +05:00
markmorado
b66424de12 chore: ignore .claude directory 2026-07-24 04:02:54 +05:00
markmorado
07af758f9a fix(nodes): show a loading state for the instances list
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Deploy to Production / Trigger production update (push) Has been cancelled
The overview tab rendered "no data" while the VM list was still loading, so a
server with instances looked empty until the request came back. Gate the empty
state on isLoading and show skeleton rows instead.

Also add the missing nodes.instances_on_server key to both locales — the raw
key was rendered in place of the heading.
2026-07-24 03:18:22 +05:00
markmorado
ff1396142b fix(api): scope server listings to workspace, stop serialising Proxmox calls
/api/cluster/topology and /api/resources/all read every ProxmoxServer row
regardless of the caller access or the X-Active-Workspace header, so the
Cluster page listed all servers and dashboard aggregates counted the whole
panel instead of the active workspace. Extract the access + workspace filter
out of /api/servers into get_visible_server_ids() and apply it in all three.

Live metrics were also gathered server by server with the cache lock held
across the network calls, so a single unreachable cluster stalled the request
(21-31s in the logs) and concurrent requests queued behind it. Fetch in
parallel, skip servers already known offline, drop the redundant
is_connected() probe, and hold the lock only around the cache dict.

/api/resources/all now orders by server id: without it Postgres returned heap
order, which shifts whenever update_status rewrites a row, so the server list
reshuffled between requests.
2026-07-24 03:18:14 +05:00
markmorado
35a8a11bb5 docs: add commit message language and no-co-author rules 2026-07-23 23:23:45 +05:00
markmorado
4cdbed54e4 style(theme): add blue tint to dark theme surfaces
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Shift dark-theme background, card, popover, sidebar and border/input
tokens from neutral hue 286 (near-zero chroma) to a blue-graphite
hue 258 with higher chroma, so the panel reads as midnight-blue
instead of flat black.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 23:21:05 +05:00
markmorado
fca14828dc docs(wiki): expand RBAC guide and add full Russian WIKI.ru.md
Rewrite the RBAC/Security section with a detailed model: permission
format, engine evaluation order (admin bypass, wildcard, manage-implies),
requires dependencies, full permission catalogue, per-page permission
map, built-in role breakdown, and the ownership-based user isolation
(vm:manage, not vm:view, grants the global view). Fix stale references
(version 1.10.2, Networks needs server:manage, SSH keys user:manage).

Add WIKI.ru.md as a complete Russian translation mirroring WIKI.md,
including the expanded RBAC section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 22:58:18 +05:00
markmorado
e67f6b7249 fix(update): recover from stalled panel self-update instead of hanging at 80%
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
The panel update status lived only in-memory and was cleared solely by the
host watchdog restarting the container. If the rebuild failed mid-way (e.g. the
build OOMs the host when no swap is configured), the container never restarted
and the UI stayed at "restarting - 80%" forever.

- update_service: get_update_status() now self-heals — after UPDATE_TIMEOUT_SECONDS
  (default 900s) of "is_updating" it transitions to a terminal "failed" state with
  a clear error pointing at the host update service / logs.
- use-settings: poll update status while the server reports is_updating (not only
  right after the click in the same tab), so reloading a stuck page keeps refreshing.
- SettingsPage: banner now renders the "failed" state with the error message.
- update_host.sh: build images sequentially (app, then frontend) to lower peak
  memory and avoid the OOM that killed the build mid-run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 22:57:41 +05:00
markmorado
ceec83d2fb docs: release notes and version bump for v1.10.2
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 22:22:10 +05:00