Repository navigation
sessions: move the ad-hoc session form into a modal - #275
Draft
jseifeddine wants to merge 27 commits into
Draft
jseifeddine wants to merge 27 commits into
jseifeddine wants to merge 27 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
… 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
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
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
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
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 sol1#252: at 1710px the connections, recordings and reports tables fit inside the viewport and nothing scrolls sideways.
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
…rgin Stretching the display to fill a mod-16 shortfall costs sharpness when the picture is already drawn one framebuffer pixel per physical pixel, and nearly every RDP session hits the rounding. So the fill now applies only when the picture is already being resampled (scale x devicePixelRatio != 1), where it costs nothing; a pixel-exact session is never stretched, and the framebuffer is centred in the available area so the small margin is split evenly in the dark background instead of showing as a bare strip down one edge. offsetLeft includes the centring margins and ignores the fill transform, so the pointer maths that divides out fillX/fillY is unchanged. Fixes sol1#251
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. Run in the CI e2e job.
…ders A device moved between NetBox tenants kept its connection entry in both tenant folders: the webhook writes to the device's current tenant folder and has no way to name the old one (its context carries only the old tenant's id, not its slug). PUT .../entries/:entry?move=true deletes same-named entries from every other folder of the scope after the write, each deletion audited, so an upsert keyed on a stable name follows its device when the folder changes. The NetBox webhook examples use it. Part of sol1#253
The flag defaults to off, accepts an explicit true or false, and rejects a non-boolean value instead of treating it as true.
NetBox stores no secrets, so a synced entry was created without a password or key and showed "Login..." until a five-minute timer added the fleet key. Folders now carry default credentials: a per-session-type username, password, private key or domain at <folder>/.defaults, lent at connect time to entries that have no secret of their own, subfolders inheriting per type (nearest folder wins). 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, summaries never carry them, every change audited), and a Default credentials section in the folder dialog. `.defaults` is hidden from entry listings like `.config`. Part of sol1#253
…argument defaults handler Inserting the folder-defaults handlers above ab_list_all left its doc comment stranded over a section banner, which clippy flags as an empty line after a doc comment; the two lines move back onto the function they describe. ab_put_folder_defaults takes eight extractors, one over clippy's default, so it carries the usual allow like the other axum handlers here.
The page loaded folders, entries and credential status once and cached them for the life of the tab, so a change made by a webhook, another admin or another browser was invisible until a reload. GET /api/addressbook/version returns a counter that middleware bumps after every successful change made through the address-book API (entries, folders, folder config, defaults); 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 the counter moves, plus an unconditional refresh every two minutes for changes made directly in Vault or by another instance. Nothing reloads while a dialog is open or the tab is hidden; a hidden tab catches up when shown again. 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.
…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.
…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
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
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
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 sol1#254
jseifeddine
force-pushed
the
ux/sessions-adhoc-modal
branch
from
October 12, 2026 04:21
e44426e to
ed58c1a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #262.
The form opens as a modal from a button on the right of the Sessions heading, drops the page's framed form box inside the dialog, primary button first and Cancel the same height as Connect.
Testing
sessions-adhoc-modal.spec.mjs(opens from the heading button, Cancel closes, buttons share a height, a viewer gets no button).