f on the info screen runs fstrim in a running VM with the guest agent
enabled. A container gets an info note (no guest agent; use pct
fstrim on the node). A stopped VM or one without an agent gets a
warning, and nothing runs. Otherwise a confirm comes first, then
agent ping and fstrim. The result reports the trimmed total
(GiB/MiB), the number of paths and any per-path errors.
Review fixes:
- Odd agent output no longer turns a finished trim into "fstrim
failed". Unreadable output gives "no results". Odd entries are
repaired: non-dict entries are dropped, a non-int trimmed becomes 0
and a missing path becomes "?". Well-formed entries are returned
unchanged.
- fstrim gets a 30-minute CLI timeout instead of 330 s, because a big
disk can take longer.
- Notifications are never parsed as markup (GuestApp.notify defaults
to markup=False). Guest names, paths and CLI or agent errors are
untrusted text, so a "[/b]" in them could break the toast.
- f before the info screen has loaded says "still loading" instead of
a wrong "not running".
- "1 path" in the singular.
Not run live: the issue asks for approval before fstrim on VM 108.
Only the harmless agent ping was run there, and it answered.
Closes#19
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The info screen's Resources table shows Autostart (yes/no), Startup
(order, up and down delays) and Tags. o opens a dialog to change them.
Only keys that differ are sent, in one `set <kind> config` call
through the existing set_net. A confirm lists the old and new value
of each key.
Tags are removed with `--delete tags` on set config. The CLI also has
a `delete ct|vm ... tags` command, but it shares the form that
destroys the whole guest, so the app never uses it; nothing in the
code builds a delete ct|vm argv. Inputs are validated before sending:
startup values must be ints >= 0, and tags must match
[A-Za-z0-9_][A-Za-z0-9_+.-]*. They are split on ; , or whitespace and
deduplicated.
Live test on CT 104: onboot 1 -> 0, startup order=5,up=10, tag
tui-test added, then all set back through the dialog. The config
ended identical to the baseline.
Closes#18
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/ opens a filter box under the guest table, and the table filters as
you type. Terms are separated by spaces, all must match, and case is
ignored:
- a plain term matches part of the name, the VMID or a tag
- is:running and is:stopped match the status
- type:lxc/ct and type:vm/qemu match the guest type
Enter keeps the filter and goes back to the table. Escape clears it,
from the box or from the table. The frame shows
"· filter: <text> · N of M". Actions only ever see the visible rows,
and the filter survives reloads and context switches.
Review fixes:
- Typing "[/b]" or "[/]" crashed the app, because the filter text went
into the border subtitle, which Textual parses as markup. The
subtitle is now plain text, so "[red]" is also shown literally.
- Escape on the table now clears a filter that was applied with Enter.
- guest_matches tolerates a None status or type.
Read-only live check on mainframe: "debian" gives 104, "type:vm" gives
108, "community-script is:running" gives 14 of 19, and "is:stopped"
gives 0 of 19.
Closes#16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README gets an "Install" section: install with uv tool from the
Gitea repo or from a local checkout, then upgrade and uninstall. The
"Run" section says to use `cv4pve-tui` when installed and
`uv run cv4pve-tui` from a checkout.
Verified by installing into throwaway tool dirs (UV_TOOL_DIR and
UV_TOOL_BIN_DIR under /tmp): `uv tool install .` gave a cv4pve-tui
binary. Started from /tmp in a pseudo-terminal, it drew the guest
frame and ran until it was stopped. The pyproject metadata (entry
point, readme, requires-python) was already enough.
Closes#17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At startup the app runs `cv4pve-cli --version`. If it differs from
cli.TESTED_VERSION (2.4.0), a warning says which version was found
and which one was tested. The app parses output formats that are not
a stable API: the config list text, prune-backups as a JSON string,
the CT ip-addresses JSON string and the exit codes with --wait. A
newer cv4pve-cli could break them in confusing ways.
If cv4pve-cli cannot be run at all, an error says so once, instead of
every later call failing without explanation. The README notes the
tested version.
Closes#14
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
U opens "Package updates": the pending apt updates of every online
node, with node, package, current and new version, origin and
priority. A node that fails is shown as "<node>: unavailable", and the
other nodes are still listed. A hint says the list is as of the
node's last apt update; this call does not refresh it.
The main frame's bottom border shows "· N package updates" after the
backup count, with a singular for 1. The count is fetched on mount,
after a context switch and whenever the updates screen loads, not on
every guest reload, since it costs one CLI call per node. It uses the
same generation guard as the guest list, so a count from the old
cluster is dropped after a switch.
Running apt update or upgrade stays out of scope; it is a write.
Read-only live check: "mainframe · 23 package updates", and 23 rows
in the screen.
Closes#13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
l on the main screen opens "Tasks", the recent cluster tasks newest
first. l on the info screen shows only that guest's tasks. Columns:
start, duration (or "running"), node, type, VMID ("-" for node
tasks), user and status. A failed task gets a "!" in the leading
column. Enter opens the task's log in a plain-text widget, so the
output is never parsed as markup.
`task list -o json` has no start or end times, so the list comes from
`/cluster/tasks`. The upid is validated (it must start with "UPID:")
before it goes in as a positional argument.
Review fixes:
- A finished task without a start time no longer crashes the app; its
duration shows "-".
- "running" now means there is no endtime. A finished task with an
empty status shows "unknown" and is flagged.
Read-only live check: 25 tasks listed, and the log of the newest
(vzdelsnapshot 104) ends with TASK OK.
Closes#10
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guest table gets two last columns, CPU % (cpu * 100) and Mem %
(mem / maxmem), each with one decimal. They show "-" for a guest that
is not running or lacks the field.
The info screen's Resources table gets a header and an "In use"
column: CPU %, memory MiB with %, swap for containers, and rootfs used
GiB with % for containers. VM disks show "-", because the API reports
0 without guest-agent disk info. The info screen reads status and
usage in one status/current call (new cli.current).
After an action, the acted guest's status and usage both come from
status/current, so a guest that has just started no longer shows the
lagging listing's 0.0 %.
One blind-written test expected a VM's CPU cell to read "4 cores". VMs
have always shown sockets ("4 cores × 1 socket", as the resources
spec and its tests define), so that expectation was corrected.
Closes#12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.gitea/workflows/ci.yaml runs on PRs into main and on pushes to main.
It installs uv, runs `uv sync --locked` with Python 3.12 (the lowest
version requires-python allows), then pytest, `ruff check` and
`ruff format --check`.
ruff is pinned in the dev group (0.17.3). The repo has no ruff config,
so the rules are ruff's defaults for that version; pinning makes CI
and local runs check the same rules. The setup follows the workflow
that already runs green on this Gitea instance (ubuntu-latest, pinned
checkout SHA, uv via the official script). The README gets a CI badge
and the lint commands.
Closes#11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A "Snapshots" section on the info screen lists the snapshots, newest
first, without Proxmox's "current" pseudo entry. Keys:
- p takes a snapshot. The name defaults to tui_YYYYMMDD_HHMM, the
description is optional, and VMs can include RAM.
- R rolls back to the snapshot under the cursor. The confirm names the
snapshot and its date. A running guest is started again; its status
is read right before the rollback, and if it cannot be read the
rollback does not run.
- D deletes the snapshot under the cursor, after a confirm.
`delete ct|vm <node> <vmid> -y` without the snapshot subcommand
destroys the whole guest. delete_snapshot therefore validates the name
first (letter first, then [A-Za-z0-9_-], not "current"), writes
"snapshot" into the argv as a literal, and checks the argv shape
before running it. A property test covers rejected names.
The info screen now scrolls, because four sections do not fit short
terminals. A description starting with "-" is refused, since it is a
positional argument and could be read as an option.
Live test on CT 104: take, roll back (started again) and delete. The
snapshot list, status, config and IPv4/IPv6 match the baseline.
Closes#8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guest table marks with "!" each guest that Proxmox's not-backed-up
list returns. The frame's bottom border shows the count, e.g.
"mainframe · 2 without backup job". This uses Proxmox's own answer, so
it also counts jobs that select guests by pool.
If the coverage call fails, the guests are still listed and a warning
says the coverage is unknown.
Closes#9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new "Resources" section at the top of the info screen shows CPU
cores (with sockets for VMs), memory, swap for containers, and each
disk with its size. The disks are the same ones the edit dialog
offers, so cloud-init and CD drives are left out. A container without
a cores limit shows "unlimited".
The open info screen reloads after any guest job, so values edited
with e show up straight away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enter on a guest now opens its info screen, replacing i. The guest
table shows Enter as "Info" in the footer. A priority binding at app
level would have taken Enter away from buttons and lists in the
dialogs, so the hint comes from the table's own binding instead.
Editing cores, memory and disk moved from the main screen into the
info screen (key e). When the edit dialog closes, you are back on the
info screen. Test helpers that pressed i or e on the main screen now
go through Enter, because the requirement changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The info screen's style now applies everywhere. The guest table sits
in a "Guests" frame with the active context in its bottom border.
Every dialog is a centred box with a title, labelled fields and the
buttons bottom right: confirm (titled by the action), edit resources,
switch context, back up and edit network.
Fixed:
- The network dialog hid most of its fields. In Horizontal rows
Textual gave Select a width of 1 and Input 100 %, which pushed
bridge, firewall, the IP mode selects and the gateways off-screen.
Every field in a row now has an explicit width.
- A guest name or note containing markup, such as "a[/b]", crashed the
guest table and the dialogs, because DataTable cells and border
titles were parsed as markup. They are now plain text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Put backups and network in their own bordered sections titled
"Backups" and "Network". Inside Network, the configured settings and
the addresses in use are labelled "Configured" and "In use". The
"unavailable" note is hidden when it is empty, so it no longer leaves
a blank line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Press n in the info screen to edit one interface: bridge, VLAN tag,
firewall, IPv4 (DHCP, static or none) and IPv6 (SLAAC, DHCP, static or
none). The netN string is parsed, the chosen fields are changed and
the string is written back, so hwaddr, model=MAC, mtu, rate, trunks
and type are kept. For VMs, IP settings go to cloud-init ipconfigN and
only when the VM has a cloud-init drive.
A confirm shows the old and new raw values before anything is sent.
Press u to restore the values from before the last change to that
guest in this session.
Review fixes:
- Undo entries are cleared on a context switch, because VMIDs repeat
across clusters.
- has_cloudinit only looks at disks.
- CIDR needs a decimal prefix: no netmask form and no /0.
- Fields you did not touch are not rewritten.
Live test on CT 104: firewall, VLAN 99, IPv6 DHCP and static IPv4 were
each applied and undone, and net0 ended identical to the baseline.
Going from static back to DHCP left the CT without IPv4 until a
reboot, so the confirm now warns that IP changes on a running
container may need one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proxmox ties backups to the VMID only, so a deleted guest's backups
were listed under the new guest with that VMID. CT 104 showed planka
backups next to debian's, and VM 108 showed vaultwarden backups.
Backups whose notes name another guest are now hidden by default. The
notes come from the job's {{guestname}}, so "debian (manual)" still
counts as debian. A line says how many are hidden, and the a key shows
them, marked "(earlier guest)". Backups without notes are always
shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Press i on a guest to see its backups and network. Backups from every
backup-capable storage are listed newest first, with the backup jobs
that cover the guest. The network part shows the netN settings
(bridge, VLAN, MAC, IPv4/IPv6 setting, firewall) and the addresses in
use.
Press b in the info screen to back the guest up now: snapshot mode and
zstd, like the scheduled job, to a storage you pick (the job's storage
is preselected). It asks for confirmation, marks the guest busy and
uses --wait.
Fixed after review and a read-only live check:
- prune-backups arrives as a JSON string; parsing it as a dict
crashed the app.
- CT addresses come from ip-addresses. inet6 holds only the
link-local address, so the global SLAAC address was missing.
- The info screen uses the guest's live status, and odd API data
(null size, unknown address types) no longer kills the app.
- The test fake now copies guest dicts; a shared constant was being
mutated across tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Write ESC c, the sequence `reset` sends, inside suspend() right before
ssh. The container shell then starts on a clean screen with no
leftover TUI terminal modes. It runs locally, so the container does
not need `reset` installed and `pct enter` is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Press t on a running container to suspend the TUI and run
`ssh -t root@<node> pct enter <vmid>`. cv4pve-cli has no shell or exec
for LXC, so this goes around it and needs key-based root SSH to the
PVE node.
- VMs, stopped containers and busy guests get a warning instead.
- Only exit code 255 (ssh failed) is reported as an error. Other codes
are the last status of the container's shell.
- OSError is caught inside suspend(). Textual's suspend() has no
try/finally, so an exception escaping it would leave the app
suspended.
- The guest table reloads when the shell returns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Press c to list cv4pve-cli contexts and switch between them. The
active context is always shown in the header.
- Switching runs `cv4pve-cli config use`, which changes the global
current context. cv4pve-cli 2.4.0 has no per-call context option.
- `config list` has no JSON output, so its text lines are parsed.
- The guest table is cleared as soon as another context is chosen, so
the old cluster's guests cannot be acted on.
- A generation counter drops guest listings that started before a
switch, and g and c are refused while a switch runs. Without this,
a reload already in flight could refill the table with the old
cluster's guests, and VMIDs collide across clusters.
103 tests pass. The switch was tested live from mainframe to
mainframe-edge and back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
List VMs and LXC containers from the current cv4pve-cli context and
start, shutdown, stop and reboot them, change cores and memory, and
grow disks. The app shells out to `cv4pve-cli -o json` and stores no
credentials.
- Every mutating call uses --wait: without it cv4pve-cli exits 0 even
when the task fails.
- shutdown, stop and reboot need confirmation; disk growth is
increment-only and asks again because it cannot be undone.
- After an action the acted-on guest's status is read from
status/current, because /cluster/resources lags a few seconds.
- Notifies when a cores/memory change is pending until reboot.
Tests mock cv4pve-cli (85 tests). Live-tested edit, shutdown, start
and reboot on CT 104 (mainframe), restored afterwards; disk resize
was not run live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>