Repository navigation
connect: zero-click fullscreen via a same-tab iframe, sized to the measured viewport - #281
Closed
jseifeddine wants to merge 9 commits into
Closed
jseifeddine wants to merge 9 commits into
jseifeddine wants to merge 9 commits into
Conversation
jseifeddine
force-pushed
the
feat/zero-click-fullscreen
branch
2 times, most recently
from
October 13, 2026 07:05
8686684 to
c6558d6
Compare
…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 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 sol1#254 tests: app shell with two independent scroll panes and a clipped body (sol1#254)
…o the served page 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 sol1#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 sol1#254 tests: one navigation on every page, gated by data-role-level
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 tests: the ad-hoc session form opens as a modal (cherry picked from commit 872cc61)
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 sol1#254 sessions, recordings: show the session type with the shared badge (sol1#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 sol1#254 tests: shared facet filters and the list toolbar on every list page
folders: optional display name in the folder config (sol1#254) 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. 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, accepted by POST /api/addressbook/folders and PUT .../folders/:scope/:folder, and carried in the search index as folder_display_name so results can show it the way the folder view does. The NetBox tenant sync sets it to the tenant name. On the connections page a folder with a display name shows it as the title with the folder key as a sub-header, in the header and the sidebar, so 'Dani & Fiori's Gourmet Pizzeria' reads over the key instead of three spellings of the tenant in one line. The folder modal gets a Display name field, asked for before the description. Part of sol1#254 tests: folder display name is optional in storage and shown over the key A legacy .config without display_name loads with an empty one and is stored unchanged; a set name round-trips. The browser spec checks the header, the sidebar and the folder modal field.
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. From an independent review of the module: rgConfirm/rgAlert resolve any already-open dialog before replacing it, so a second call can't leave the first promise hanging; Enter acts on the focused button (Cancel cancels), and Tab is trapped between the two buttons. Part of sol1#254 tests: themed confirm/alert dialogs replace the native ones (cherry picked from commit 58b1e2e)
…page The '/' search box leaves the connections folder header for a bigger, centred box in the brand row, inline with the logo. Results open in a dim, blurred overlay over an untouched folder view: a centred panel with the summary, the Type and Folder facets and the results table (fixed columns, one line per cell, folder shown by display name over its key, Connect, new window and open-folder gathered into one actions cell). Focusing the box or pressing / opens it and lists every connection the user can see; typing narrows it; Escape from anywhere, a mousedown outside the box and panel, or opening a folder closes it (an open facet menu takes the first Escape). static/search.js is the single implementation, used on every page including connections, which dropped ~16k of duplicated markup, CSS and JS; the styles live once in rustguac.css so the results look identical everywhere. The index is cached in sessionStorage and only re-fetched when the address book's version changes, so moving between pages is instant; the connections page keeps the folder-specific hooks it needs (rgSelectFolder selects a folder in place, rgSearchIndex feeds the credentials banner) and opening a folder from another page navigates to connections with #open=scope/path. The session type is escaped before it becomes a badge class, and the module stands down cleanly on a page with no brand row. Connections also gets a more generous content gutter, the folder action buttons move down to the filter row beside Type, the folder title is inset to align with the table, and the Connect button keeps its size and spins in place instead of growing to 'Connecting...'. Part of sol1#254 tests: the global search overlay on every page search: keep page modals above the brand-row search box The global search lifts the brand row to z-index 300 so the box is not dimmed by its own results overlay (200), but the page modals sat at 100, so the search input showed through the ad-hoc session dialog and could still be focused. Modal overlays now sit at 1000, above the search chrome and below the themed dialogs (4000) and the embedded session (5000). A spec pins it on the ad-hoc and folder modals. search: never on the login page, nor for anyone the server does not recognise The global search was included on index.html, so the login page carried the search box, and because the index was cached in sessionStorage it kept answering with the whole address book after logout, or to the next login in the same tab. - index.html no longer loads search.js at all. - search.js only renders for an operator or above: the role the server bakes on <html data-role-level> decides on first paint, and an API-key user (served the anonymous variant) is confirmed through /api/me first. Anonymous or viewer: nothing injected, cache swept. - The cached index is keyed to the identity /api/me returns; a cache left by a different login is dropped rather than shown. - A 401/403 from the index or version endpoints removes the box, the overlay and the cache; the logout link clears the cache before the login page shows. e2e: login page has no box and never loads search.js; anonymous and viewer callers get nothing and a stale cache is wiped; a different login's cache is dropped; the same login keeps its cache across pages; logout and expiry forget it.
…esh Pending one Sizing. The entry connect request now carries the screen's size as well as the window's (screen_width/screen_height, sent by every connect site in connections.html and the client's relaunch). initial_session_size() picks the size the session will be shown at: an RDP/VNC entry that opens fullscreen on connect is created at the screen's size, everything else at the window's, and every RDP size is rounded down to the 16px grid that guacd's display channel (patches/007) rounds the first post-connect size send to. Creation and that send therefore agree, guacd coalesces them, and there is no resize, no H.264 decoder rebuild and no flash on connect. The client's post-connect size send stays as it is. Reuse. A repeat Connect on the same entry within 15 s no longer opens a duplicate: a Pending session (guacd connected, owner never attached) this user created for the entry is handed back instead. The match is made on the fully built CreateSessionRequest and includes its credentials: CreateSessionRequest::credential_fingerprint() is a SHA-256 over the target and every credential field (username, password, private key, domain, auth package, KDC, jump hosts and their credentials, container and Proxmox credentials), stored on the Session and compared, never logged or served. A retry with a different password, key or domain, such as a mistyped prompt, gets a new session rather than the old Pending one with the old credentials. Reuse is also scoped to the same user, the same entry key, Pending status with a live guacd stream, and sessions younger than SessionManager::REUSABLE_PENDING_SECS. sol1#238 (native/HiDPI resolution, display_scale) has not merged: both sizes here are CSS pixels, matching today's post-connect send. When it lands the mod-16 rounding must be applied to the device-pixel size, after the devicePixelRatio multiply; the rounding site carries that note. Tests: fullscreen_sizing_applies_to_bitmap_sessions_only and round_down_mod16_floors_at_sixteen (api); credential_fingerprint_tracks_target_and_credentials, fresh_pending_session_reused_for_same_entry_and_credentials, retry_with_different_credentials_gets_a_new_session and reuse_is_scoped_to_user_entry_status_and_age (session). Part 1/2 of sol1#257. (cherry picked from commit d11d18f)
…asured viewport A browser only lets the tab that holds the Connect gesture go fullscreen; opening a new tab drops that gesture, so a fullscreen-on-connect entry always fell back to a manual click. Connect now fullscreens the CURRENT tab on the click — the one moment a valid gesture exists — and loads the session in a full-viewport same-origin iframe (rgLaunchFullscreen in search.js, shared by the connections page and the global search): overlay, spinner and an Exit button, torn down on Exit, on fullscreen exit or on a session-end message from the embedded client. The client in ?embedded=1 mode skips all in-client fullscreen logic (the parent tab owns it) and posts ready/end to the parent. X-Frame-Options becomes SAMEORIGIN (was DENY) with CSP frame-ancestors 'self', so a same-origin page can host the client in a frame while cross-origin framing stays blocked. The final viewport size cannot be predicted (a fullscreen viewport on a notched Mac is shorter than the screen), so it is measured: the launcher enters fullscreen, waits until the viewport has held one size for 150ms, then sends the connect with that width and height; a refused request connects at the tab's size at once and a 2.5s cap never leaves the user on a spinner. The embedded client shows its connect cover from load and lifts it — then runs the post-connect fit sends — only on the launcher's 'viewport' message, so the user sees only the final picture. The ⧉ (open in a new window) button opens the client explicitly windowed (fs=0) on a small splash page, static/launch.html, which measures its own viewport once stable and posts it to the opener (rgOpenSessionWindow); the session is created at that size, so nothing resizes on load. No screen_* is sent for a popup. When the browser refuses the load-time request, the next click or key enters fullscreen, taken in the capture phase and never sent to the remote, and everything is released once fullscreen lands. Fixes sol1#257 tests: same-origin framing only, and the zero-click fullscreen launcher The CSP test pins frame-ancestors to exactly 'self'. The browser spec drives Connect on a fullscreen-on-connect entry: the launcher overlay opens in the same tab, the connect request carries the measured viewport and no screen size, the embedded client loads in the frame, and Exit tears it down.
jseifeddine
force-pushed
the
feat/zero-click-fullscreen
branch
from
October 16, 2026 11:29
c6558d6 to
ea456b6
Compare
Member
|
Merged into main as d1d2cdf thanks Jad, works nicely on our canary. Three changes our side:
|
davekempe
added a commit
that referenced
this pull request
Oct 17, 2026
… entry #281 relaxed framing to same-origin on every page. Keep DENY and frame-ancestors 'none' everywhere, and send SAMEORIGIN with frame-ancestors 'self' only on the client page of a session opened from a saved entry with fullscreen on connect turned on, the one case the Connections page frames it. An ad-hoc session cannot opt itself in, and an unknown or ended session is not frameable. Tested end to end through the header middleware. docs: a framing and clickjacking section in security.md with the approaches weighed, the corrected CSP row, and a note in reverse-proxies.md to give rustguac its own hostname.
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.
Builds on #279 (
feat/global-connection-search) and #280 (feat/initial-session-size); its own change is the last commit, the compare on the fork: jseifeddine/rustguac@bd9b41e...feat/zero-click-fullscreen. The branch carries #273–#280 until they merge intomain, so the Files tab shows them too. The one-line clippy allow that was a commit of its own here is already in #280.Closes #257 (part 2/2).
Connect fullscreens the current tab on the click and loads the session in a same-origin iframe sized from the measured viewport; the embedded client lifts its cover on the launcher's viewport message;
X-Frame-Options: SAMEORIGIN+frame-ancestors 'self'(cross-origin framing still blocked); ⧉ opens a windowed client onlaunch.html, which measures the popup before the session is created.Review note: the header change from DENY to SAMEORIGIN is deliberate and scoped by frame-ancestors.
Testing
test_csp_allows_same_origin_framing_only.zero-click-fullscreen.spec.mjs(same-tab overlay, connect carries the viewport and no screen size, embedded client in the frame, Exit tears down; non-fullscreen entries unaffected).