reports: show entry names as host over a tenant subline
Session history and the most-used table showed "host (Tenant)" names
wrapped across three lines; they now read as the host over a quieter
tenant line, as on Connections and Recordings.
Part of #254
sessions, recordings: show the session type with the shared badge (#254)
The sessions page printed the type as plain text and the recordings page
as a coloured word; connections and reports already used .type-badge
from rustguac.css. All four now use the same badge. The Proxmox badge
colour moves into the shared stylesheet with the others.
Shared facet filters (static/filters.js) on the sessions, recordings,
reports and connections pages: a dropdown per facet (Type, Folder, User,
Owner, Status as each page has them), multi-select with counts that narrow
as other facets apply, All/None, a find box for long lists, a Clear filters
link, and selections kept per page in sessionStorage. They sit beside the
existing text search and combine with it; the buttons are compacted to the
height and padding of the other controls and the dropdown reserves room for
its scrollbar so it no longer sits over the counts.
The list pages share one toolbar: a borderless row with the search box, the
facets and the page action on the right (New ad-hoc session, Export CSV),
placeholders standardised to 'Search <noun> (fields…)'. Sessions gains a
search (user, host, owner, ID, type) and a User facet; Reports moves from
its own .filter-bar to the shared one. The redundant page titles go — the
nav already names and marks the active page — and section headings centre
their heading and action button on one line.
Connections: the folder header is sticky at the top of the entries pane, so
it stays under the nav and the active-sessions strip while a long table
scrolls. All four pages show the session type with the shared .type-badge
(the Proxmox colour moves into rustguac.css), reports and recordings show
"host (Tenant)" names as the host over a quieter tenant line, and the key
glyph before Credentials in the nav is sized to the text.
Part of #254
tests: shared facet filters and the list toolbar on every list page
The ad-hoc session form sat in a collapsible framed box above the session
list. It now opens as a modal from a button on the right of the Sessions
heading, dropping the page's framed form box inside the dialog, with the
primary button first and Cancel at the same height as Connect, matching the
other modals. Section headings centre their heading and action button on
one line.
Part of #254
tests: the ad-hoc session form opens as a modal
(cherry picked from commit 872cc61fce)
pages: bake the caller's role into the served page, version the logo
The header flashed on every navigation: gated links (Sessions, Reports,
Tokens, Admin) were hidden in the markup and shown after /api/me
answered, the logo was revalidated on each page load, and the nav
differed between pages, so switching tabs reflowed the header.
Serve each page in five variants, one per role level, that differ only
in a data-role-level attribute on <html>; optional auth now runs on the
page routes so a logged-in request gets its variant with the navigation
already right on first paint. The ETag is per variant. The logo joins
scripts and stylesheets in content-addressed, immutable caching.
Part of #254
pages: one navigation for every page, gated by role in CSS
Every page carries the same eight links in the same order, including
the Credentials entry (which opens the dialog on the connections page
and deep-links to it from elsewhere), with data-min-role on the gated
ones instead of inline display:none. rustguac.css shows them from the
data-role-level the server writes on <html>; the pages set that
attribute themselves from /api/me for API-key users, whose page GET
carries no credential. Admin-only controls follow the same gate, so
nothing pops in after the first paint. Recordings shows "host (Tenant)"
entry names as the host over a quieter tenant line.
Part of #254
tests: one navigation on every page, gated by data-role-level
The sticky sidebar was sized from the viewport rather than from where
it starts under the header, so at the top of the page its bottom hung
off screen and the page had to be scrolled before the list could be
scrolled to its end. The page is now an app shell: the header stays,
and the sidebar and the entries area are two independent scroll panes
that always fit the viewport. Selecting a folder scrolls the entries
pane, not the page.
The sidebar is a bordered panel: one pinned header band on a surface
tone with the Folders label, a compact new-folder button and the filter
box, then the list with hairline dividers, 46px rows and one 12px edge
for every band. "host (Tenant)" names read as the host over a quieter
tenant line in the entries and search tables, and a role-gate
reconciliation covers API-key users.
Part of #254
connections: scroll only the entries pane on selection, clip the body
jumpToEntriesHeader() used scrollIntoView(), which scrolls every
scrollable ancestor, and a body with overflow hidden can still be
scrolled by script: selecting a folder pushed the brand row and the nav
off the top of the page. Scroll the entries pane to its top instead, and
make the body overflow: clip, which no script can scroll. The body's
border box now spans the full height so the page padding stays inside
the viewport.
Part of #254
tests: app shell with two independent scroll panes and a clipped body (#254)
The 1710px spec from the fork passed against main as well as against the
change it was meant to pin: its fixture names are short enough to fit
either way under Linux's monospace font, and the overrun on the reporter's
Mac came from wider glyphs and longer real names. With realistic data
(dotted FQDNs, service accounts, long emails and folder paths, which have
no natural break point) main overran by 370px, and the tighter padding on
its own still overran Connections and Recordings.
Table cells on Connections, Recordings and Reports now wrap long text
inside the cell (overflow-wrap: anywhere), with the Connect and action
buttons kept on one line. The spec gains a long-data test per page: all
three fail on main, two fail with the padding change alone, all pass now.
The e2e README lists only the specs that exist.
A browser test harness under tests/e2e: serve.mjs serves the real static
tree over HTTP and lib/api.mjs answers /api/* from a small fixture address
book, recording every request the page makes, so the shipped pages run
against data the test controls without Vault, guacd or a session. CI gains
an e2e job that installs Chromium and runs the suite.
The first spec pins #252: at 1710px the connections, recordings and reports
tables fit inside the viewport and nothing scrolls sideways.
(cherry picked from commit 4ae28efec0)
The comments on the #251 fit said Windows RDP without GFX rounds the
width down. It is our own guacd: patches/007-rdp-disp-mod16 rounds both
dimensions to a multiple of 16, with or without GFX, to stop H.264's
padding showing as a green band along the edge. Say so where the margin
is handled, and that the rounding must not be relaxed to make the margin
go away, since that brings the band back.
tests/client-fit.mjs lifts applyFill() out of client.html and runs it on a
stand-in element: a pixel-exact picture is never stretched, a resampled one
fills a gap of at most 2.5% per axis, a larger gap keeps the letterbox, and
nothing is ever shrunk through the fill.
(cherry picked from commit e7d43ecf28)
Upstream: the CI wiring for the e2e job is not carried (that job is not
in this tree); the test runs in the existing CI job instead.
The entrypoint created the docker-admin key whenever no file existed at
/opt/rustguac/data/rustguac.db. With db_path set anywhere else, for
example on a Kubernetes persistent volume, that file never exists, so
every container start tried to create the admin again and failed with
"UNIQUE constraint failed: admins.name".
add-admin gains --if-none: create the admin only when no admin account
exists, otherwise say so and exit successfully. rustguac opens the
database db_path names, so the entrypoint no longer needs to know where
it is. Any existing admin counts, so a docker-admin an operator removed
after creating their own does not come back. The save-the-key banner is
printed only when a key was actually created.
Reported by @hit-rich.
A busy main thread does not delay pointer movement, it discards it: moves
arriving while a handler runs are merged into one, and what is dispatched
afterwards carries only the last position. A drag feels that immediately --
the path between two points is gone, so a dragged scrollbar thumb is dropped
rather than merely lagged -- while a hover or a video degrades gracefully
under the same load.
getCoalescedEvents() returns what was merged. Replaying it in order restores
the path, though not the timing: nothing running on a blocked thread can do
that, and the positions still arrive together once the thread is free. What
changes is that the remote end sees where the pointer went rather than only
where it ended up.
Deliberately additive. The mousemove listener is untouched and still delivers
every position it did before, so touch, the compatibility mouse events it
produces and the counter that swallows them all behave as they did; the
dispatched position arrives twice and the second is dropped by the
unchanged-coordinates check already in move(). Migrating this file to pointer
events outright is the tidier shape and a far larger change -- every listener
would have to move together, or synthetic events from touch would arrive with
nothing left counting them down.
Only while a button is held, because the path matters for a drag and the
endpoint is all that matters for a hover, and replaying every merged move of
an idle pointer would multiply a session's upstream mouse traffic for movement
nobody can perceive. Capped and evenly sampled, because a pointer reporting at
1000Hz behind a thread blocked for 100ms offers a hundred positions for one
drag, and answering a rendering problem with a traffic one is no answer. An
ordinary pointer never reaches the cap.
The pop-out monitor windows get the same replay. They bypass Guacamole.Mouse
entirely -- it does not track the X axis correctly in a popup -- and map the
pointer themselves, so a drag on a second head would otherwise lose its path
exactly as the first one did. The sampler moves to a static so there is one
definition of the rule rather than two that can drift, and that path needs one
thing the main display does not: it sends unconditionally, where move() drops
an unchanged position, so it keeps what it last sent and drops a repeat --
keyed on the buttons as well as the position, since a press and a release
happen at one place and are not repeats of each other. Feature-detected on the
event rather than on PointerEvent, because a pop-out window is a realm of its
own and the constructor this page can see is not the one that made the event
it is handed.
That pointer handling lives in Guacamole.MonitorPointer rather than inline in
client.html, because these windows cannot be opened without a live
multi-monitor session: while it sat in the page the only thing a test could do
was match the source text and hope, and the two things most worth checking
there are a coordinate mapping and a suppression rule -- exactly the kind that
look right in a diff and are off by a term. It takes the canvas, a function
returning this monitor's rectangle in the combined framebuffer (a call rather
than a captured value, since the layout changes under a running window) and a
function to send a state.
Writing those tests found a real defect in the wheel, which sends a momentary
press and release: it built one state object, sent it, mutated it and sent it
again, which works only while every consumer copies synchronously. It sends
two states now.
Tested rather than tried, because this path only runs once the browser has
already thrown something away: the case it exists for is the one that cannot
be reliably produced, and a broken version feels like the behaviour it
replaced. Twenty tests cover the mapping, the button mask, the repeat
suppression, the wheel's reset of it and the replay, plus a harness driving
real pointer input through the debugger with the main thread deliberately
blocked -- because the unit tests feed events shaped the way the code expects,
and that assumption is the one worth checking against a browser.
(cherry picked from commit d8ce743fe0801bd8c776281ee5541abf8040a339)
A client with no hardware H.264 decoder lost H.264 altogether, and lost it in
the worst available way. Chrome reports a configuration carrying
hardwareAcceleration 'prefer-hardware' as unsupported outright rather than
falling back, so configure() failed asynchronously, the decoder closed, every
frame was then held waiting for a keyframe that cured nothing -- and since
guacd suppresses ordinary image operations for a layer carrying H.264, what
the user saw was a permanently black screen rather than a degraded picture.
Measured on Chrome 153 against an Intel UHD 770 with no VA-API decode
available: isConfigSupported() answers supported for avc1.4d4020 with no hint
and with 'prefer-software', and not supported with 'prefer-hardware'. The
same box then decoded the stream perfectly once the hint was dropped.
So ask for hardware once and, if that is refused, build again without asking.
Dropping the hint unconditionally is the smaller change and the wrong one: it
hands the choice to the browser on every client, including the ones this
whole path exists for, where a software decode costs far more than it saves.
It says so when it happens. A session quietly decoding H.264 in software is
worth knowing about -- it is the difference between the cost this feature was
built to avoid and the cost it was built around -- so the fallback reports
decoder_software_fallback rather than only appearing as a slow session.
Tested, because the hint reads like a preference and restoring it would look
harmless. The test drives the shape Chrome actually presents -- configure()
accepted, the refusal arriving later through the error callback -- and checks
that hardware is asked for first, asked for only once, that no frame is
stranded by the refusal, and that the fallback is audible.
Reachable on a VM, a machine whose driver is blocklisted, or a browser with
acceleration switched off, none of which are exotic.
(cherry picked from commit 59fc416e80daab20ac07bdac5dbebb6ff7eddf75)
Upstream: ported by hand, as the fork's decoder has diverged. The
fallback reports through console.warn only, since the fork's diagnostic()
channel is not carried here. tests/h264-hardware-fallback.mjs passes
unchanged (node tests/h264-hardware-fallback.mjs).
They call assert_cmd's cargo_bin, which needs CARGO_BIN_EXE_rustguac.
Cargo exports that only to integration tests under tests/, never to a
#[cfg(test)] module inside src/. As unit tests they passed locally purely
because a stale target/debug/rustguac was lying around for assert_cmd's
fallback to find; on a clean checkout, and so in CI, all five failed with
"`CARGO_BIN_EXE_rustguac` is unset". They have been broken since they were
written and nothing noticed, because the suite had never run in CI.
Caught by the cargo test step added in 83a9110, on its first run.
Database assertions now go through rusqlite directly rather than the
crate's own db helpers, since rustguac is a binary crate with no lib target
for an integration test to import. That is the better boundary regardless:
it checks the state the CLI actually leaves behind. rusqlite joins
dev-dependencies, which adds nothing to the tree as it is already a runtime
dependency.