Repository navigation
Connections UX: scrollable sidebar, flush folder rows, selection feedback, filter and keyboard navigation - #255
jseifeddine wants to merge 49 commits into
Conversation
The Content-Security-Policy had no img-src, so default-src 'self' governed images and refused the data: URIs the Guacamole client draws every tile from on browsers without WebCodecs ImageDecoder. Safari and Firefox rendered SSH and RDP sessions black with only rect fills and the cursor cell showing; Chrome decodes straight from the stream and never fetches a URL, which hid it. The same directive blocks the remote mouse cursor (cursor: url(data:...)) and blob: image URLs. Move the policy into a documented constant, add img-src 'self' data: blob:, and add a unit test that fails if img-src loses either source. Document the proxy-side header replacement for releases up to v1.10.5. Fixes sol1#249
Windows RDP without GFX rounds the requested width down to a multiple of 16, so a 1710-wide window gets a 1696-wide desktop. The uniform fit in scaleDisplay() then keeps the scale at 1 (the height matches) and the missing 14px shows as a strip of bare page down the right edge. After the uniform fit, stretch the display element by the remaining fraction on each axis when it is at most 2.5%, and divide the same factors out of pointer coordinates before sendMouseState. A larger gap is a real aspect mismatch and keeps the letterbox. Multi-monitor pop-outs reset the fill. Fixes sol1#251
The 18px table with 20px cell padding and a separate cell each for edit, clone and delete overran the main column by ~300px on a 1710px MacBook display, cutting "delete" off the right edge. Put the three admin actions in one cell, and add a max-width: 1800px breakpoint on the connections page: 20px page padding, 240-280px sidebar, 16px cells with tighter padding, 14px headers, a 110px Connect button and a smaller folder title. The icon-only "open in new window" button keeps its compact size. Nothing is hidden; at 1710px the table fits with about 250px to spare. Fixes sol1#252
Three things kept the Connections page a stale picture of NetBox and Vault: a device moved between tenants kept its entry in both folders, a synced entry waited on a five-minute timer for its credentials, and the page never refreshed until it was reloaded. PUT .../entries/:entry?move=true deletes same-named entries from every other folder of the scope after the write, each audited, so an upsert keyed on a stable name follows its device when the folder changes. Folders get default credentials: a per-session-type username, password or private key at <folder>/.defaults, lent at connect time to entries with no secret of their own, subfolders inheriting per type. Listings report such entries as has_credentials with inherited_credentials, so they show Connect from the moment a webhook creates them. Admin API (GET/PUT/DELETE .../defaults, secrets write-only, audited) and a section in the folder dialog. GET /api/addressbook/version is a counter bumped by middleware after every successful change through the API; it costs no Vault read. The page polls it every three seconds and reloads folders, entries, the search index and the credential status only when it moves, plus an unconditional refresh every two minutes for changes made elsewhere. Nothing reloads while a dialog is open or the tab is hidden. Fixes sol1#253
…ol1#254) With 20+ customer folders the sidebar shared the page scroll with the entries table, so picking a folder far down the list changed content off screen, and every leaf row was pushed ~90px in by a hidden chevron placeholder while names were ellipsised. - .sidebar is position: sticky with its own thin scrollbar and max-height tied to the viewport; .main keeps scrolling with the page. - The FOLDERS header is wrapped in .sidebar-pin together with a new empty #sidebar-tools slot, both pinned at the top of the sidebar. - buildFolderNode() renders a .tree-chevron only for folders with children; leaves get no placeholder (removed the .tree-chevron.leaf rule), so rows start right after the 3px accent bar. - Name, entry-count pill (.folder-count, now just the number with a data-count attribute and "N entries" title) and a smaller scope glyph share one line; description sits beneath at --fz-xs; rows are ~40px. - Nested .tree-children indent 14px behind the existing dashed guide. All new CSS is in one block at the end of the page's <style>.
With a long folder list, clicking a folder appeared to do nothing: the entries table was off screen, the only loading state was a text swap, and nothing drew the eye to the changed title. loadEntries() also called renderFolders() just to update one count badge, rebuilding the whole sidebar on every click. - Jump to the entries header when the shown folder changes (smooth, 'auto' under prefers-reduced-motion); never on initial load, live refresh, forceRefresh after a mutation or search clear. - .changed on #entries-header replays a 350ms title slide-in and an accent underline flash on the action bar; cleared on animationend. - Skeleton table (4 shimmering rows, same geometry as the real table) while entries are fetched from the server; the cached path renders immediately with no skeleton. - updateFolderCount() swaps the number in the existing .folder-count badge in place; renderFolders() only as a fallback when the badge does not exist yet. - renderFolders() is wrapped (not edited) to preserve the sidebar scroll position across rebuilds and to reveal the selected row within the sidebar (nearest, no page scroll) when the selection changes or after loadFolders() rebuilds the tree.
The 1800px breakpoint set a tighter page gutter on the connections page only, so switching to Sessions or any other tab snapped the content sideways. Move it to rustguac.css so every page moves together. Part of sol1#254
# Conflicts: # static/connections.html
…es (sol1#254) The sidebar lists one folder per customer and gets long. Add a live filter box (name, path, description; loaded subfolders included, with ancestors of a match kept visible) mounted in a new #sidebar-tools div, persisted in sessionStorage so live refreshes keep it. Make the tree keyboard-navigable (arrows, Home/End, Enter/Space, Left/Right to collapse/expand) with role=tree/treeitem, aria-selected/expanded and a roving tabindex, and add an 'f' shortcut that mirrors the '/' one. Implemented by post-processing the DOM renderFolders() builds and by wrapping that function; its body is unchanged.
# Conflicts: # static/connections.html
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 sol1#254
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 sol1#254
…sublines 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 sol1#254
… input after the transition With fullscreen-on-connect, the first mousedown after CONNECTED is the browser's fullscreen gesture, but Guacamole.Mouse forwarded it to guacd as well, and the transition that followed swallowed its release. guacd was left with the button held and the terminal selected text on every pointer move; the same with a key press as the gesture. Stop the gesture event before the display's handlers see it, and on entering fullscreen release everything explicitly: keyboard.reset() and a mouse state with no buttons at the last pointer position, so a release lost for any other reason during the transition cannot stick either. Fixes sol1#256
…t request The session was created at the window's size, re-sent after connect, then replaced by the fullscreen size once the gesture landed, with the display scaled to fit in between and refit after the server's rounding: four or five visible size changes for one connection. The connect call now carries the screen's size and the server creates a fullscreen-on-connect session at it; the client keeps that target while still windowed (scaled to fit until the gesture lands), so the post-connect sends and the fullscreen resize ask for what the server already has and guacd coalesces them to nothing. Fixes sol1#257
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 sol1#254
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 sol1#254
The 18px tables with 20px cell padding ran past the right edge on a 1710px MacBook display, the recording link cut off. Same treatment as the connections table: 16px cells with tighter padding, 14px headers, actions kept on one line. Nothing is hidden. Part of sol1#254
…lscreen Sizing every fullscreen-on-connect session to the screen made an SSH terminal narrower and soft until the gesture landed: it was scaled to fit the window's height. A terminal reflows instantly and must stay crisp, so only RDP and VNC, where a server-side resize is slow and visible, start at the screen's size; everything else keeps the window's size and resizes on fullscreen. The screen-size target also outlived fullscreen: after Esc the client kept asking for it and the user was stuck with the scaled view. It now applies only until fullscreen has been entered once. The SSH creation log carries width and height, as the RDP one does. Verified: unit test on the server rule, five client-side cases, and six end-to-end cases against a live instance reading the sizes guacd was given. Part of sol1#257
…ol1#257) A fullscreen-on-connect session needs a user gesture before the browser will go fullscreen, so until now the first thing the user saw was the session scaled down to the window, with a gap down the right where the taller screen-sized framebuffer did not fit, and only the next click brought the real picture. Now a gate covers the page from before the tunnel opens: the session connects and paints behind it at the size it will have in fullscreen, and the gesture that dismisses the gate is the one that enters fullscreen. No extra round trip, no scaled frame first. The gate shows the entry name and the connection state, and offers to stay windowed (button or Escape); a rejected fullscreen request, or one the browser ignores, falls back to the window after a moment and sizes the session to it. Gesture events are taken on window in the capture phase so neither the click nor the key reaches the remote. The gate is hidden on disconnect or error so the usual overlays show.
…l1#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.
…der display names (sol1#254) 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. Connections: the folder header (name, description, search, actions and the new filters) 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. Folders with a display name show it as the title with the folder key as a sub-header, in the header and the sidebar; the folder modal gets a Display name field. Sessions: the ad-hoc session form moves into a modal opened from a button on the right of the Sessions heading. Nav: the key glyph before Credentials is sized to the text.
A folder can carry a display_name beside its description. It is stored in the Vault .config (omitted when empty, so existing configs are unchanged), returned in folder listings and GET .../config, and accepted by POST /api/addressbook/folders and PUT .../folders/:scope/:folder. The NetBox tenant sync sets it to the tenant name, so the connections page can show 'Dani & Fiori's Gourmet Pizzeria' over the folder key instead of three spellings of the tenant in one line.
The ad-hoc form drops the page's framed form box inside the modal, and the folder modal asks for the display name before the description.
…tion (sol1#257) A fullscreen-on-connect RDP/VNC session was created at the screen's size so no resize was needed once fullscreen landed. But while the window is shorter than the screen, that taller framebuffer is scaled to fit and letterboxes with a gap down the side, which is what showed on the first paint. The session is now always created and kept at the window's size, so the first paint fills the viewport. The one resize into fullscreen is hidden behind the cover instead. The connect cover is no longer a gate that asks for a click: it is a plain splash with a spinner, the entry name and the connection state. It lifts on its own once connected, revealing the window-filled session. The next click or key is armed as the fullscreen gesture, exactly as before; that press is taken in the capture phase so it never reaches the remote, and a cover goes back up over the fullscreen resize. No button, no 'click to proceed' step.
…; compact filter buttons (sol1#254) The '/' search box leaves the folder header for a bigger, centred box in the brand row, inline with the logo, with a search icon and a '/' hint that clears once it is in use. Focusing it (or pressing /) engages a search mode: the folder's own header is hidden as irrelevant, a results summary and the Type/Folder facets take its place, and with no text yet every connection the user can see is listed. Escape or opening a folder leaves the mode. The results table is rebuilt: fixed columns, one line per cell with ellipsis, folder shown by display name over its key, and Connect, new window and open-folder gathered into one right-aligned actions cell instead of sprawling across the row. The facet filter buttons are compacted to the same height and padding as the other controls.
…sol1#257) The previous approach painted the session windowed as soon as it connected, then entered fullscreen on a click and resized — a visible windowed frame, a delay, and a second repaint. Now the session goes fullscreen first and is painted once at the final size. The Connect click opens the client window within the gesture, so that window carries the user activation; the client requests fullscreen on load and it is usually granted with no further click. A bitmap (RDP/VNC) session is created at the screen size, so the picture painted behind the cover is already the fullscreen one with no resize. The cover stays over the page the whole time — fullscreen request, connect, paint — and lifts only once the window is fullscreen and the session connected. If the browser refuses the load-time request, the cover says so and the next click or key enters fullscreen (taken in the capture phase, never sent to the remote). Also tighten the connections folder header: the title and description share one baseline with the key tucked beneath, the action buttons sit as a group on the right, a single divider separates them from the filter row, and the search-engaged summary and filters form a compact band.
…ed control heights (sol1#254) The folder view no longer changes when you search. The results open in a dim, blurred overlay over it: a centred panel with the summary, the Type and Folder facets, and the results table, while the folder you were in stays exactly as it was underneath. Focusing the search (or pressing /) opens it and lists every connection; typing narrows it; Escape, the backdrop or opening a folder closes it and the folder view is untouched. The results get their own filter instance so the folder view keeps its own Type filter. Connect, new-window and open-folder in a result row work the same as in the folder view (the handler is bound to both panes). Also: the main content pane gets a left gutter so the tenant name, buttons and table are not jammed against the edge, and the list search input is set to the same height as the facet buttons on the recordings and reports pages.
) The ad-hoc modal's Cancel used the small button height while Connect used the full one, so they did not line up; Cancel now uses the shared modal cancel style and both are the same height, with the primary button first to match the other modals. Section headings center their heading and action button on one line instead of sitting on the text baseline.
The search is a floating overlay now, so it no longer has to live on the connections page. static/search.js is a drop-in module: it injects the search box into the brand row, the results overlay into the body, loads the address-book index, and wires the / shortcut and the facets, on any page that includes it. Added to sessions, recordings, reports, admin, tokens, docs and index. On the connections page its own in-page search still runs (results interact with the folder tree there), so the module stands aside when it sees that page's search box. Connect from a result opens the client in a window opened within the click (so fullscreen activation carries); an entry needing credentials sends the user to the connections page to set them. Opening a folder from another page's search navigates to connections with #open=scope/path, which selects that folder on arrival.
sol1#254) On pages other than connections the brand row had no stacking context, so the search box was dimmed behind its own overlay; the injected styles now raise the brand row above it. The magnifier glyph matches the connections page. The index is cached in sessionStorage and only re-fetched when the address book's version changes (checked on load and polled), so moving between pages is instant with no re-index flash; the connections page still loads a fresh copy and bumps the version on edits.
…nect (sol1#254) The main content pane gets a more generous, symmetric horizontal gutter and the folder header a little more room at the top, so the tenant name, buttons and table are clearly inset. The facet dropdown reserves space for its scrollbar so it no longer sits over the count numbers. The Connect button keeps a fixed size and spins in place instead of growing to 'Connecting...' and shoving the row around; it also keeps its own label (Connect or Login...) rather than resetting to Connect.
static/ui.js provides promise-based rgConfirm/rgAlert that show a themed modal matching the app, injected self-contained so it works on any page including the standalone client. Every native alert()/confirm() is replaced: delete user and revoke token (admin), revoke token (tokens), delete recording (recordings), popup-blocked (client), and terminate session, delete entry, delete folder and the SSH host-key probe (connections). Destructive actions get a red confirm button, the host-key trust prompt an accent one. The global search's Connect buttons show the same in-place spinner.
…dex carries folder display name (sol1#254) The folder title was flush against the header's left edge; it now gets the same horizontal padding as the table cells, so it is inset and sits over the table content below. The address-book search index now includes each entry's folder display name, so search results can show it the way the folder view does.
…sol1#254) The connection search and its results overlay were implemented twice — inline on the connections page and again in search.js. They are now one component. The styles live once in rustguac.css, and search.js is the single implementation, used on every page including connections, which dropped ~16k of duplicated markup, CSS and JS. The connections page keeps only the folder-specific hooks it needs: window.rgSelectFolder selects a folder in the sidebar in place (so opening a folder from a result there doesn't navigate), and the loaded index is shared as window.rgSearchIndex for the credentials banner. search.js uses window.rgSelectFolder when present and otherwise navigates to connections with #open=scope/path. Also folded in: results show the folder display name over its key (the search index now carries it), the overlay is wider (one step under full width), results have the open-in-new-window button, Escape closes the overlay from anywhere rather than only when the box is focused, and a click on the dim backdrop closes it.
Defensive escaping so the type value can never break out of the class attribute, matching how the folder view builds the badge.
From an independent review of the new modules: - rgConfirm/rgAlert resolve any already-open dialog before replacing it, so a second call can't leave the first promise hanging. - Enter in a dialog acts on the focused button (Cancel cancels), and Tab is trapped between the two buttons. - The search overlay's Escape yields to an open facet menu, closing just the menu first, then the overlay on the next press. - search.js stands down cleanly on a page with no brand row instead of throwing.
…ro-click fullscreen (sol1#254, sol1#257) - The folder action buttons (add entry, subfolder, edit, delete) move down to the filter row beside Type, leaving the top row for the tenant name, description and key alone. - The search overlay now closes predictably: Escape from anywhere while it is open, and a mousedown anywhere outside the search box and the results panel. Clicking inside the panel (results, facets) keeps it open; a facet menu's own Escape closes just the menu first. Replaces the previous blur/keep-timer logic that could leave it stuck open after a click into the results. - Fullscreen-on-connect requests fullscreen the moment the client page is interactive (gated by a new ?fs=1 flag the launchers add), before the async session fetch, so it uses the user activation carried from the Connect click rather than losing it to the round trip. When the browser grants it there is no click at all; the armed gesture remains as the fallback.
) The results table's base styling — cell padding, header style, row dividers, the folder-scope icon size and colour — came only from the connections page's inline .entries-table rules, so the overlay looked right on connections but unstyled on every other page. Those rules now live under #search-results in rustguac.css, scoped and specific enough to override any page's own table styles, so the results look identical everywhere.
A browser only lets the tab that holds the Connect gesture go fullscreen; opening a new tab drops that gesture, so the ?fs=1 request in the new tab always fell back to a manual click. Fullscreen-on-connect now fullscreens the CURRENT tab on the Connect click — the one moment a valid gesture exists — and loads the session in a full-viewport iframe (the embedded client fills it). No new tab, no second click. - search.js: rgLaunchFullscreen (shared, used by connections and global search); overlay + spinner + Exit button; tears down on the Exit button, on fullscreen exit, or on a session-end message from the embedded client. - client.html: ?embedded=1 skips all in-client fullscreen logic (the parent tab owns it) and posts ready/end to the parent so the launcher can hide the spinner and tear down. - The session is created at screen size so the embedded client paints at the final fullscreen size with no resize. - The new-window (new tab) button keeps the old path; a cred-prompt entry keeps the old path too.
|
Only commenting on the client/RDP display parts here (#257). The rest is UI work I haven't reviewed. #257 (fullscreen-on-connect). Avoiding the resize is worth having: with H.264 passthrough, a resolution change rebuilds the browser's hardware decoder, which stalls video for several seconds. One thing to be aware of with the screen-size start, though: Creating at
|
Zero-click fullscreen loads the session in a same-origin iframe, but every response carried X-Frame-Options: DENY, so the browser blocked the frame and the session showed a black screen with a spinner. The only way through was to reopen from Active Sessions and reconnect. Fixes, all together: - main.rs: X-Frame-Options SAMEORIGIN (was DENY) and CSP frame-ancestors 'self', so a same-origin page can host the client in a frame; cross-origin framing stays blocked. - api.rs: reuse a Pending session the same user already opened for an entry instead of creating a duplicate on a repeat Connect (find_reusable_session). - api.rs: create a fullscreen RDP session at the screen size rounded down to a multiple of 16 (round_down_mod16), matching guacd's display-channel rounding so there is no post-connect resize and the H.264 decoder does not rebuild/stall (sol1#257). - client.html: fit a bitmap session sharply — stretch to fill the mod-16 gap only when the picture is already resampled; on a pixel-exact session keep it crisp and centre it so the small margin is split evenly in the dark background rather than a bare strip on one edge (sol1#251). - recordings/reports/sessions: drop the redundant page title; the nav already names and marks the active page.
Cap reuse to recently-created Pending sessions so a repeat Connect within a few seconds reattaches to the one just made, while an older Pending — whose guacd may have died — is left to the pending-timeout reaper and a fresh session is created instead. Avoids ever handing back a stale, dead stream.
|
Thanks, both look right to me. Creating at the mod-16 size means guacd's own check makes the post-connect send a no-op, and the session is never off-grid even during connect, which is better than what I suggested. The pixel-exact centering for #251 is exactly what I had in mind. One note for whenever this meets #238 (native resolution): there the requested size is multiplied by |
Sessions now matches Recordings: one borderless row with the search box, the facet filters, and the page action on the right — not a bordered section head above a separate filter row. Adds a Sessions search (user, host, owner, ID, type) and a User facet alongside Owner, so the filter set is complete. Reports moves from its own .filter-bar to the same shared .list-tools/.list-search, with Export CSV as the right-aligned action. Placeholders standardised to 'Search <noun> (fields…)' everywhere.
|
Thanks — both points are addressed, and both now live in #255 (the single inclusive PR; #250 is folded into it). #251 — the margin. Took both of your alternatives, together. The framebuffer is centred, so any residual mod-16 margin is split evenly and shows the dark background rather than a bare strip on one edge. The fill stretch now fires only when the picture is already being resampled ( #257 — the post-connect resize. Kept the post-connect size send, as you asked. On top of it, a fullscreen RDP session is now created at the screen size rounded down to a multiple of 16 ( Also relevant: the zero-click path changed since you reviewed it. A new tab drops the fullscreen gesture, so Connect now fullscreens the current tab and loads the session in a same-origin iframe instead. That needed One heads-up for merge order: #255 overlaps #238 in the display-sizing path — |
…ewport The ⧉ (open in a new window) button still showed the "click to enter fullscreen" cover: the launcher omitted ?fs=1, but the client re-read the entry's fullscreen_on_connect from the session metadata and armed the gate anyway, and a popup window carries no click activation so the request was refused. ⧉ now opens the client with fs=0 (explicitly windowed), the client never arms fullscreen for it, and both launchers create the session at the popup's own inner size so nothing resizes on load. The search overlay's ⧉ opens the same sized popup as the connections page instead of a tab. Zero-click fullscreen (same-tab iframe) still showed one visible snap: the launcher tab's fullscreen transition lands after the session connects, so the frame first fitted the picture to the pre-fullscreen viewport (gaps at the sides) and then snapped on the resize event. The embedded client now shows the connect cover from load, the launcher reports whether fullscreen was entered or refused once the frame is connected, and the frame lifts the cover — and only then runs the post-connect fit-to-window sends — once the viewport has reached the screen size (immediately if refused; capped at 1.5s after the signal and 3s after connect). The user sees only the final picture.
The remaining snap on zero-click fullscreen was a size mismatch, not timing: the launcher created the session at screen.width x screen.height, but a fullscreen viewport on a notched Mac is shorter than the screen (the strip beside the camera is never used), so the first paint was a fit of a taller picture (gaps at the sides) that the post-connect fit then resized away. The final viewport size cannot be predicted, so measure it: the launcher enters fullscreen, waits until the viewport has held one size for 150ms (and, having entered, until it has actually left its windowed size, capped at 1.5s), then sends the connect with that width/height. Refused or unavailable fullscreen connects at the tab's size immediately; a 2.5s cap never leaves the user on a spinner. The embedded client now simply trusts the launcher's 'viewport' message to lift the cover and start the fit sends, instead of comparing against the screen size (which never matched on a notched Mac and left the cover up for the 1.5s fallback).
…always SSH was pixel-perfect after the previous commit but RDP still flashed once: initial_session_size prefers screen_width/height for a fullscreen bitmap session, and the launcher still sent the physical screen (1710x1112) next to the measured viewport (1710x1074). The session was created at 1696x1104, the client's first size send became 1696x1072, and the remote lost 32 rows after the first paint. The launcher now sends only the measured viewport (no screen_*), and the server rounds every RDP initial size down to the 16px grid, fullscreen or windowed, since guacd rounds the client's first post-connect size send to that grid regardless — so creation and first send always agree and no path resizes on connect. VNC keeps exact sizes. Tests updated.
"Open in a new window" still resized once on RDP: the launcher read the popup's innerWidth/innerHeight straight after window.open, which reports the opener's size until the new window is laid out, and the search-overlay path also sent screen_width/height, which the server prefers for a fullscreen bitmap entry. Either way the session was created at a size the popup never had, and the first fit-to-window send resized it. The popup now opens on a small splash page, static/launch.html (spinner, entry name, site theme), which measures its own viewport once loaded and stable for 150ms and posts it to the opener. rgOpenSessionWindow in search.js wraps that (popup + a size promise, with defaults if the popup is blocked and a 2.5s cap); both the connections page and the search overlay create the session at that size and then navigate the popup to the client. No screen_* is sent for a popup. The client keeps its connect cover up for a windowed (fs=0) session until connected, so the splash hands over to the final picture with nothing in between.
|
@jseifeddine I appreciate the effort. This is way too much to review all at once and test. Can you please put some time into breaking this up into chunks that can be seperately tested? I need automated tests, playwright tests and user tests on all these. Great to have you contributing, I look forward to work with you on this. |
Fixes #249, fixes #251, fixes #252, fixes #253, fixes #254, fixes #256, fixes #257.
This is one branch for the whole piece of work: the Safari/Firefox rendering fix, the RDP display fixes, and the Connections/Sessions UX overhaul. It supersedes #250 (now closed), whose four commits form the base of this branch.
Rendering and RDP display
Safari and Firefox drew sessions black (#249).
img-src 'self' data: blob:is added to the Content-Security-Policy thatsecurity_headersputs on every response, the policy moved into a documented constant, and a unit test fails ifimg-srcever losesdata:orblob:again. The Guacamole client draws eachimgstream by loading adata:URI into anImageon browsers without WebCodecsImageDecoder; with noimg-srcdirectivedefault-src 'self'refused every load,Display.draw()skipped it silently, and the frame still flushed, so nothing showed in rustguac or guacd logs. The same directive also unblocks the remote cursor (cursor: url(data:...)) and theblob:image path. Verified in Safari 27: before, 109 image loads / 109img-srcviolations / black framebuffer; after, 119/119 loads, 0 violations, identical to Chrome.RDP strip of bare page on one edge (#251). The display channel rounds every size down to a multiple of 16 (patch 007, which prevents the H.264 green band), so a 1710-wide window gets a 1696-wide desktop and the uniform fit in
scaleDisplay()left the rest as bare page. The framebuffer is now centred so any residual margin is split evenly and shows the dark background rather than a strip on one edge, and it is stretched to fill that margin only when the picture is already being resampled (effective scale × devicePixelRatio ≠ 1), where the stretch costs no sharpness. On a pixel-exact or HiDPI session it is never stretched, so text stays crisp. Pointer coordinates are divided by the fill factors beforesendMouseState;offsetLeftincludes the centring margins and ignores the transform, so that stays correct. Larger gaps are real aspect mismatches and keep the letterbox.Connections table overran the page at 1710px (#252). Edit, clone and delete share one cell, and a
max-width: 1800pxbreakpoint tightens page padding, sidebar, cell size and the Connect button. The row that ran off the right edge now fits with room to spare, nothing hidden.Stale entries after a NetBox tenant change, folder defaults, live page (#253).
PUT .../entries/:entry?move=truemakes the target folder the entry's only home in the scope (same-named entries elsewhere are deleted and audited), so a device that changes tenant follows instead of doubling. Folders can lend default credentials per session type to entries with none of their own, inherited by subfolders.GET /api/addressbook/versionis a counter bumped after every successful change; the page polls it every 3 s and reloads only when it moves.Fullscreen-on-connect
Stuck mouse button after fullscreen-on-connect (#256). The first press after CONNECTED is the browser's fullscreen gesture; it was also forwarded to guacd, and the transition swallowed its release, so the terminal dragged a selection on every move. The gesture now stops before the display's handlers (capture phase), and entering fullscreen releases every held key and button explicitly.
Zero-click fullscreen, no resize, no black screen (#257). Connecting a fullscreen-on-connect entry now goes straight to the fullscreen session with no second click and no intermediate repaint — the first picture the user sees is the final one:
X-Frame-Optionsis nowSAMEORIGIN(wasDENY) with CSPframe-ancestors 'self', so a same-origin page can host the client while cross-origin framing stays blocked. Without this the iframe was blocked and the session showed as a black screen — the symptom this fixes.screen.height, and the transition is asynchronous), so the launcher enters fullscreen first, waits until the viewport has held one size for 150 ms, and only then sends the connect with that width and height. If fullscreen is refused the overlay fills the tab as it is and the connect goes out at once; a cap never leaves the user on a spinner.Esc, or the session ending) tears the iframe down and returns to the list.Open in a new window (⧉) is explicitly windowed. The button next to Connect never applies fullscreen-on-connect, whatever the entry says (the client is opened with
fs=0), so it no longer shows a "click to enter fullscreen" cover that a popup — which carries no click activation — could never satisfy. Its session is sized the same way as the framed one: the popup opens on a small splash page (static/launch.html: spinner, entry name, site theme) that measures its own viewport once laid out and stable, posts it to the opener, and the session is created at exactly that size before the popup is navigated to the client. Reading the popup's size from the opener right afterwindow.openreports the opener's size until the new window is laid out, which was the one remaining resize on this path. The client keeps its connect cover up for a windowed session until connected, so the splash hands over to the final picture with nothing in between. The search overlay's ⧉ opens the same sized popup as the connections page.Reuse instead of a duplicate session. A repeat Connect within 15 s reattaches to the Pending session just created for that entry rather than spawning another; after that window a fresh one is made, so a stale session is never reused.
Sessions, Reports, Recordings
One list toolbar across the pages. Sessions, Recordings and Reports share one borderless
.list-toolsrow: a.list-searchbox on the left, the facet filters, and the page's action pinned right (New ad-hoc session on Sessions, Export CSV on Reports). Placeholders readSearch <noun> (fields…). Sessions gained a search (user, host, owner, ID, type) and a User facet alongside Owner, so its filter set is complete.Facet filters on every list (#254).
static/filters.jsgives sessions, recordings, reports and connections a row of dropdown filters next to the search: Type, Folder, User, Owner and Status as each page has them — compact multi-selects of the present values, counts that narrow as other facets apply, All/None, a find box, a Clear link, and selections kept per page in sessionStorage.One type badge everywhere. All four tables use the shared
.type-badge(sessions printed plain text, recordings a coloured word).Redundant page titles removed. The nav marks the active page, so the duplicate page-level heading (Recordings, Reports, the Sessions label) is gone; only genuine sub-section headings remain.
Connections page (#254)
Sidebar and density. The Connections page is an app shell: the header stays and the sidebar and entries area are two independent scroll panes that always fit the viewport. The sidebar is a bordered panel with a pinned header band (Folders label, a compact new-folder button, the filter box) over the list — hairline dividers, ~46px rows, the chevron column only for folders with children, name and count on one line (count a dim-at-zero pill), the scope glyph small at the right edge.
Selection feedback. Selecting a folder scrolls the entries header to the top (smooth, or instant under
prefers-reduced-motion) and keeps the selected row visible; the title slides in and the action bar's underline flashes accent. While entries load the table shows a same-height skeleton. Count badges update in place; the sidebar is rebuilt only when the folder set changes, preserving scroll position; live refreshes trigger none of the motion.Folder filter and keyboard navigation. A filter box narrows the tree as you type (name, path, description; ancestors of matches stay visible; "N of M" while filtering; Esc clears; text survives live refreshes). The tree carries ARIA tree roles with a roving tabindex: Arrow keys move/expand/collapse, Home/End jump, Enter/Space select,
ffocuses the box.Header that does not flash. Each page is served in one variant per role level, differing only in a
data-role-levelattribute on<html>, so a logged-in request paints its navigation correctly on first load (ETag per variant); the stylesheet gates links on that attribute. Every page carries the same eight links in the same order, and the logo is content-addressed and cached for a year.Sticky folder header. The folder header pins to the top of the entries pane: the display name and description share a baseline with the folder key beneath, the action buttons group on the right, a single divider separates them from the filter row.
Folder display names. A folder config can carry a
display_namebeside its description (stored in Vault, accepted by create/update, returned in listings, set by the NetBox tenant sync), shown as the title with the folder key as a sub-header, in the header and sidebar and the folder modal.Tenant sublines. "host (Tenant)" display names read as the host over a quieter tenant line in the entries, search, recordings and reports tables.
Shared components
Global search, one component, on every page. The search box and its results overlay live once — styles in
rustguac.css, behaviour instatic/search.js— and run on every page including connections (which dropped its duplicated inline copy). Focusing the box (or/) dims the page and floats the results; results show the folder display name over its key with Connect, open-in-new-window and open-folder actions; a fullscreen-on-connect result Connects straight into the iframe fullscreen; Escape and a backdrop click close the overlay; the index is cached per session and re-fetched only when the address book version changes.Themed dialogs.
static/ui.jsprovides promise-basedrgConfirm/rgAlertreplacing every nativealert()/confirm()with a modal matching the app (destructive in red, host-key trust in accent); self-contained so it works on the standalone client page too.Shared page gutter. The 1800px narrow-desktop padding lives in
rustguac.cssfor every page, so switching tabs no longer snaps the content sideways.How it was built and checked
cargo testpasses (381 tests, 1 ignored),cargo fmt --checkclean. The CSP fix was verified in Safari 27 against patched and unpatched builds. The RDP display, fullscreen-on-connect, session reuse, mod-16 sizing and the Sessions search/User filter were verified end to end against a live deployment with a real xrdp target: Connect goes straight to a painted fullscreen desktop with no second click, two rapid connects return the same session and a third after the cap a fresh one, and the Sessions list, search and User facet filter live data. The measured-viewport sizing was verified in a real browser on a notched MacBook (1710×1112 screen, 1710×1074 fullscreen viewport) against SSH and RDP targets: the session log shows the session created at the viewport size (RDP on the mod-16 grid,1696×1072), and the first picture is the final one with no resize or repaint. The UX slices were rendered in headless Chrome at 1710×1112 from the realrenderFolders()output; the keyboard/filter logic has a 37-assertion DOM test and the facet filters 19 checks across the four pages.