The page where a user changes their own password and the one where an
administrator sets a user's password shared an old card: a horizontal
form, a blue Cancel and a green Change. The administrator's had Django's
"Password-based authentication: Enabled/Disabled" with a paragraph about
other backends, and its password fields stayed while Disabled was chosen.
Both pages now have the user and group forms' layout, each its own
template. The administrator chooses how the user signs in, With a
password or Without a password (such as LDAP), and the password fields
and "must change it" show only with a password; Cancel goes back to the
user. A user's own page says that their other sessions end, or, when
they must change it, that they choose one before going on, without a
Cancel. A refused password is said at its field only, without the
general "Wrong Data Provided". The section styles of these forms move
into webvirtcloud.css.
The group form put every user as a checkbox in one unsorted list, the
members among them, with no way to find one: past a few dozen users a
wall of names. Its name field ran the width of the page.
It now has the user form's sections (Group, Permissions, Members), each
with what it is for beside its fields. Members come first, then everyone
else, each by username with their full name; a search box filters them
by username, name or e-mail (table_filter.js). A form's search, marked
data-forget, starts empty: the list pages remember theirs per page title,
which here is the same for every group. Hidden users stay in the form,
so saving while filtered keeps them. The button says Create group or
Save changes.
The create and edit pages of a user had one form for both, ordered for
editing: permissions first, the name and password folded below them and
the quota folded and closed at the end; a new user's name came last.
Both pages now go Account (name, password, may sign in), Role, Quota,
then Groups and permissions, each section with what it is for beside its
fields. Template editor and Superuser become one Role choice with User:
choosing Superuser hides the quota and permissions, which a superuser
does not need (no quota check applies, every permission is theirs); it
keeps the is_staff it had. The quota is always open: each limit takes a
number from 0, or No limit, which is kept as -1 as before; memory is
entered in GiB and kept in MiB, shown exactly so saving a user again
changes nothing. The button says Create user or Save changes.
Each row of the Users page asked has_perm whether its user may clone:
two queries a user (the user's permissions and the groups'). The list's
query now annotates it with one Exists over the clone permission, given
to the user or to a group of the user; a blocked user is shown without
it, as has_perm did. The page's query count no longer grows with the
users.
The Users, Groups and Removed VMs pages were bare tables: rows of
coloured icon buttons without words, five columns of small checks for
a user's role and permissions, dates broken over three lines and a
lone "+" for a new entry. The owner chose a directory layout from three
rendered options; it follows the host pages' cards:
- Each list is a card with its count and search in the header and one
list-group item per entry, which stacks on a phone without scrolling.
- A user shows their role as a badge (Superuser, Template editor) or
"User", Blocked as a red badge, their groups, a granted clone
permission and live API tokens, and the last login on one line;
role and status are searchable. Edit is the visible action, the menu
holds View profile, Block or Unblock and Delete; Block and Delete
are disabled on one's own account, which the server refuses anyway.
- A group shows its member and permission counts; its name opens it.
The page's title said "Users".
- A removed VM shows its UUID, the host it left, its owners and when
the record was made and expires.
- Logs keep their table in a card, the server search in its header,
the count of matching rows, Done and Failed as badges, and a message
column wide enough to read on a phone.
The user form was one long column of horizontal fields. The owner
rejected three boxed layouts and chose one led by what the user may
do: the Active switch, Template editor and Superuser as two cards,
groups as chips, the user's own permissions as switches each saying
what it allows, then the name and password and the quota folded into
details whose summaries tell their values (-1 as "any"); they open on
a new user and on an error inside them. A refused group or permission
is said beside its field. The group form follows it: the name, what
its members may do, its members as chips. Cancel goes back to the
list instead of the browser history.
Three upstream rules in webvirtcloud.css set every page's layout
against Bootstrap 5.3:
- .container { max-width: 900px } held each page to 900px at any
screen width, below Bootstrap's own widths. Pages now take
Bootstrap's container widths up to its lg one, 960px, and stop
there on wider screens: its xl and xxl widths (1140px, 1320px) leave
the panel's forms and lists too wide;
- .page-header (margins of 5px 5px 20px 20px) put headings 20px right
of the content under them; its 14 uses are mb-3, flush with the
content (one carried Bootstrap 4's mr-auto, now me-auto);
- p { margin: 10px } indented every paragraph 10px from the content
beside it; paragraphs take Bootstrap's own bottom margin.
webvirtcloud.css keeps only what Bootstrap has no utility for: the
page width cap, the XML editor's place and height, the Tom Select
colour fix, the sortable table headers' arrows, the grouped VM list's
collapsed hosts and the charts' height.
webvirtcloud.css still carried the upstream panel's rules for what
Bootstrap 5.3 has as utilities and components:
- the page's bottom space (body padding) is pb-5 on base.html's body,
and the console page no longer has to undo it;
- the VM page's action tabs (.action-button, .action-block with a
fixed 65px width) are text-center with a d-block icon; its tab panes
(#power ... #undefine, one rule each) mt-3;
- the bordered sub-tab panes (.tab-pane-bordered, a fixed #eee border
that dark themes could not change) border border-top-0 p-3;
- the search box and header buttons (.search, .btn-header) m-1 and
mt-1; the SSH key select (.keyselect) a form-select;
- the boot order list (.multipleselect, fixed size, #c0c0c0 border) a
list-group of form-check-input items; its unused .multipleselect-on
goes;
- table cells centred by a global rule are align-middle on each table.
Two rules against Bootstrap's behaviour go: menus opened on hover on
wide screens (Bootstrap opens them on a click, and keyboard and touch
users had no hover), and "a { outline: 0 }", which hid the keyboard
focus ring.
The paragraph and page-header margins and the 900px container stay
for a separate change: they set every page's layout.
A tombstone kept the VM's name and UUID but not its compute, so the page
could not say where the VM had been. It now stores the compute's name (as
text: the compute may be the one deleted) and the list shows it; records
made before this have no host. Deleting a record says so, and the admin
tables use scope="col" instead of the invalid span="col".
Closes#217
The list pages' search (filter-table.js) read what was typed as a
regular expression: a "(" or "[" stopped it with an error. It also
needed jQuery, while the instance list had its own script for the same
search. Both are now static/js/table_filter.js: plain text in any case,
the instance list's state list and host groups where a page has them,
remembered for a day as before. A group's header row is marked as one
(data-group-header) instead of being any row without a state.
The search box filtered only the 100 rows on screen, in the browser: an
older row seemed not to exist. The page now searches on the server, in
every row's user, host, VM, VM UUID, message and IP, as plain text, and
can show the failed rows only; its pages keep the search.
Failed rows are marked red, dates carry the year and the time zone, and
a VM's rows link to the VM while it is in the panel. Excel downloads the
rows the search chooses, all pages of them, with the VM report's
workbook (text stays text).
- An administrator could block, delete or demote (unset Active or
Superuser on) their own account, and with it the last one, leaving the
panel without anyone to undo it. These are now refused.
- The user form showed "No password set." for users who have one: its
password field never read the user's password. Its "Set password"
link went to ../password/, a page this panel does not have. The form
now says whether a password is set and links the page that sets it.
- "Staff status" said "can log into this admin site"; here it lets a
user change and delete the template VMs given to them, and is now
called Template editor.
- A quota of 0 counted as no limit, like -1, so a user's quota could not
be taken away. Now -1 has no limit and 0 allows none; the help says
so, and the labels give the units: memory in MiB, disk in GiB.
- A user's page for a user that does not exist answered with a server
error instead of not found.
- The user list said "None" for a user who never logged in; it says
"never", and dates fit their column.
- The password form's labels no longer break mid-word.
Which users can reach the API with a token was seen only by opening each
user. The user list now has an API tokens column: the number of the
user's tokens that still work (expired ones are left out; they are
deleted only at the user's next token), linked to the user's token tab.
One query counts them for all users.
The Logs page shows who added, changed or deleted a compute, from web
or API, and every login, failed login, lockout and logout, with the
client address and a Result column (Done, Failed, Unknown for older
rows). A failed change is recorded with its reason and the error still
reaches the user; a refused one is recorded too.
- logs.audit.write saves each row in its own savepoint and never
raises, so a log write cannot break or undo the action it records.
The audit() context manager writes one row when the change ends.
- Logs gains success and ip; user, host and instance are wider and
date is indexed. The page orders by date then id.
- A compute edit names the changed fields; an edit that changes
nothing writes no row, and an empty password field is no change.
- A compute's VMs leaving the panel get a system row each: owners
kept on the Removed VMs page, moved to another compute, restored,
left without owners, or the removed record expired.
- Job rows carry the result. A job's log entry that cannot be
computed is logged and skipped instead of rolling back the finish
(test_service.py changes this on purpose).
- Logins are recorded by signal receivers; CustomLoginView is removed
and the plain LoginView is used. A wrong OTP code is a failed login,
counted by django-axes toward the lockout.
Panel records and host-side objects follow in the next layers.
Column offsets without a breakpoint applied below it, where the column
is already full width, so the change password, compute, user, VM
create and VM settings forms ran past the right edge on a phone. Each
offset now starts at its column's breakpoint. The interface and network
detail lists were rows directly inside rows and sit in a column now;
the compute form loses a Bootstrap 3 thumbnail padding that broke its
gutters, and the add-rule dialog's hint is no longer cut off. The page
test checks both grid rules on every page.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Twenty tables had no .table-responsive wrapper, so on a phone the user
list, compute list, VM lists, network and network filter pages scrolled
sideways as a whole. Each table now scrolls inside its box, as the
other ten already did. The page test walks the network page too, and
checks that every table on every page has the wrapper.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An accessibility scan (axe-core, WCAG 2 A/AA) of every page found 87
elements without a name. Icon-only buttons and links (the navbar's
administration menu on every page, the add/create buttons, the flavor
delete and back buttons) get a title, which is also their tooltip; the
labels of the language, architecture, chipset and filter selects point
at them; the memory bar is named. The shared delete confirmation page
had an empty <title> and heading, as no view passes "title": it says
"Delete <object>". The power-off form's aria-label was misspelled.
Rendered through every URL on libvirt's test driver, the pages had
duplicate IDs and markup browsers repaired silently:
- Each flavor modal's "Advanced" button toggled #flav-add-collapse, the
first modal's section; every modal now has its own. The flavor and
template copies of the custom tab's control IDs, and the template
tab's unused images input, are gone.
- The VM page's action icons and the XML editors were styled by
repeated IDs; they use classes.
- Modal forms opened inside modal-body and closed after modal-footer;
they wrap both. Stray end tags and duplicate attributes are removed,
paragraphs no longer hold modals or rows, table heads have a row, and
the create page closes its row and column.
- html lang is the active language instead of "en".
A test walks every URL as a superuser and fails on a repeated ID. The
console page still has no doctype: its full-height layout relies on
quirks mode and needs its own check.
A VM that disappears from its compute keeps its owners in a tombstone
until INSTANCE_OWNERSHIP_RETENTION_DAYS pass, but nothing showed them.
Superusers now see them on the "Removed VMs" admin page (name, UUID,
owners, removed, expires) and can remove one, so a VM deleted on
purpose no longer hands its owners to a later VM with the same UUID.
django-bootstrap-icons serves icons from static/icon_cache and fetches a
missing one from the CDN while the page renders; a server without
internet access shows an error text instead of the icon.
- copy (Clone on the instance lists), envelope-open ("Lost OTP?" page)
and save (VM settings, network page) were not cached; they are now,
fetched with django-bootstrap-icons itself.
- The user and group lists asked for 'exclamation-triangle ' with a
trailing space, an icon that does not exist; the warning icon never
showed.
A test checks that every {% bs_icon %} in the templates has its cache
file; it fails on the previous files.
- user_block, user_unblock and admin_email_otp changed state on GET, so an
<img> tag could lock a user out or trigger OTP emails through an
admin's session. They now require POST; the buttons are CSRF forms.
- The console token cookie is set with httponly, SameSite=Lax and, on
HTTPS, secure. Only novncd reads it; the page gets the token directly.