Adds a per-entry RDP keyboard layout, wiring guacd's `server-layout`
parameter (previously hardcoded to empty) through the address-book entry,
the connect request, and the guacd handshake, with a dropdown in the
connections editor and a migration mapping from Guacamole's `server-layout`
on import. Empty keeps guacd's default (en-us-qwerty).
Dismissing the "N credentials to set up" banner now persists in
localStorage instead of a page-lifetime flag, so it stays gone across
refreshes. It records exactly which credentials were dismissed and
re-appears only if a genuinely new unset credential shows up — so a
partial save that shrinks the set won't re-nag, but a new requirement
still surfaces.
The collapsible group caret used the small-triangle glyph (U+25BE) like
the old folder chevron did, so it read faint. Switch to the full-size
U+25BC and bump its size, matching the folder-tree chevron.
The credential editor rendered a flat list inside a modal that scrolled as
a whole, so past a few variables the Save/Close buttons and context
scrolled off and it got unusable. Restructure it: the modal header (title,
description, filter box) and the Save/Close footer are pinned and only the
credential list scrolls. Add a filter box (shown once there are more than a
handful of variables) that live-filters rows by name/domain, and group each
domain into a collapsible section. Focused single-connect mode is unchanged
(few rows, no filter box).
Fetch the user's set credential variables on load and cross-reference
against each entry's credential_variables (already on EntryInfo). Entries
that reference variables the user hasn't set get a "credentials needed"
pill, and a dismissible banner reports how many credentials are still to
set up (aggregated across the search index). Both link to the editor, and
refresh after a save. Turns the old reactive "connect fails, then hunt for
the menu" flow into something visible up front.
When a connect fails for missing credential variables, open the My
Credentials editor focused on just the missing variables ("credentials
needed to connect to X") and, on save, re-run that exact connect instead
of dead-ending the user in the full editor with a toast. openMyCredentials
now takes { onlyVars, heading, onSaved }.
Also promote My Credentials to a visible "Credentials" nav link (kept in
the Settings menu too) so people can find it without hitting a failure
first.
Each per-user credential variable can live in the shared backend (fleet-
wide) or the local backend (this instance) - its location is the scope.
Reads merge both backends (local wins); writes partition by a per-key
scope sent from My Credentials, moving a key between backends when its
toggle changes and honouring "leave blank to keep". A "Shared across
sites" toggle appears per credential.
Quick-start preserved: with one Vault the shared and local cells alias the
default, creds_split() is false, the toggle is hidden, and everything is
written to the single store in one call - byte-identical to before. New
config user_credentials_default_scope (default "local") seeds new toggles.
The write partition is a pure function with unit tests covering single-
store, blank-keeps-existing, scope routing, cross-scope moves, and the
default-scope fallback.
list_all_folders now returns (folders, unavailable_scopes); a scope whose
dedicated backend is down is reported instead of silently dropped, and
GET /api/addressbook includes unavailable_scopes. The Connections tree
renders a non-interactive greyed "<scope> connections unavailable" node
so a partial outage is visible while other scopes stay usable; the 15s
/api/me poll refreshes it.
Single-Vault is unaffected: every scope aliases the one backend, so
unavailable_scopes is always empty while it is up (full outage still
returns 503 and the existing banner). Adds a test asserting a lone
[vault] block is sufficient (quick-start invariant).
The folder expand/collapse indicator used the small-triangle glyphs
(U+25BE/U+25B8), which render as faint, hard-to-see ink even at 20px,
while every other collapsible on the page uses the full-size triangles
(U+25BC/U+25B6). Switch the folder chevron to match so it is clearly
visible.
Adds RDP multi-monitor, reusing the protocol-agnostic client work already used
by SPICE (secondary-monitors advertisement, multimon-layout cropping, pop-out
windows, native-mouse mapping).
guacd (patch 010): a "secondary-monitors" arg enables multi-monitor. The
Display Update module now tracks a per-monitor layout (tiled left-to-right,
top-aligned, widths rounded to RDP-valid geometry) and sends the full
DISPLAY_CONTROL_MONITOR_LAYOUT array via SendMonitorLayout rather than a single
monitor. The host extends its desktop across the monitors and streams one
combined framebuffer, so there is no client-side compositing (unlike SPICE).
guacd advertises secondary-monitors on user join and publishes the
multimon-layout so the client can split the framebuffer into per-monitor
windows.
rustguac: RdpParams gains secondary_monitors, wired from the entry's
max_monitors and sent as the secondary-monitors connect arg (resize-method is
already display-update, which the Display Control channel requires). The
address-book entry editor gains a Monitors field for RDP.
Validated against a Windows RDP host: ticking a second monitor extends the
desktop, the second head renders and resizes, and clicks land on both.
Each entry gains a compact icon button beside Connect that launches the
session in a separate browser window (which can be dragged to another
physical display, and its secondary monitors popped out from there). Connect
is unchanged (opens a new tab).
The window is opened synchronously in the click handler so it is not
popup-blocked after the async connect fetch, then navigated once the session
exists; it also works through the credential prompt. The icon is a compact,
vertically-centred sibling that stays on one line with Connect.
client.html: per-monitor tick-boxes (contiguity-enforced) that open each
secondary monitor in its own window, blitting that monitor's region of the
combined framebuffer (rAF) and forwarding mouse (combined coords) + keyboard;
untick/close releases the monitor. Requests floor to a sane size if a popup
reports zero dimensions.
Entry storage: AddressBookEntry/EntryInfo gain max_monitors, ab_connect_entry
passes it through, and the Proxmox editor gets a Monitors field. Lets a saved
Proxmox/SPICE entry offer multiple monitors (previously only the ad-hoc API
path could).
KNOWN ISSUE: enabling multi-monitor (secondary-monitors>0) intermittently drops
the SPICE connection ~2-6s after connect, independent of the request. Under
investigation; single-monitor is unaffected. A3 end-to-end (tick -> activate ->
render) still needs a real-browser verification (Playwright can't size popups).
Split SPICE into two connection types: "spice" (direct libvirt/QEMU) and
"proxmox" (PVE console brokered via the spiceproxy API). Both produce a guacd
SPICE connection.
- Deliver the SPICE ticket/password as a connect arg instead of a post-connect
argv stream, so it is set before guacd authenticates. Fixes an auth race that
produced intermittent "SPICE authentication failed".
- TLS-only SPICE sends an empty plain port so guacd connects via tls-port
rather than plaintext against a TLS endpoint.
- Proxmox node is optional: resolve it from the VM id via /cluster/resources
(as the PVE web UI does).
- Split the PVE API token into a visible Token ID (shown in the User column)
and a masked secret; join them as "id=secret" for the API.
- Surface the PVE response body on non-2xx (safe: only a 2xx spiceproxy
response carries a ticket), turning opaque 500s into actionable messages.
- SSH tunneling for Proxmox: tunnel both the PVE API call and the spiceproxy
connection through the jump-host chain in-branch. Also rewrite tls_port
(not port) for direct-SPICE TLS over a tunnel.
- Store proxmox fields on address book entries; populate Host/User columns;
orange Proxmox badge. Runtime dep: libspice-client-glib-2.0-8.
Add SPICE to the connections entry editor: a SPICE type option, a fields
block (hostname / port / password / color-depth, plus TLS / tls-port /
ignore-cert / CA cert / cert-subject / proxy for connecting through a SPICE
proxy such as Proxmox's), and the show/hide, save, load, and clear wiring
mirroring the VNC type. connections.html is served from disk, so no binary
rebuild is needed for this file.
Add an autohide_side_tabs option (Option<bool>, default off) on the address
book entry, threaded through the same path as fullscreen_on_connect
(AddressBookEntry, EntryInfo, CreateSessionRequest, Session, SessionInfo, the
API connect/quick-connect builders, and import defaults). When set, client.html
slides the left-edge Clipboard and Files tabs off screen when idle and brings
them back when the pointer nears the left edge; defaults preserve the current
always-visible behaviour. Checkbox added to the entry editor.
These three RDP visual flags were hardcoded off in guacd.rs. Expose them
as per-connection options (Option<bool>, default false) threaded through
the same path as enable_desktop_composition: RdpParams, the session
request, Vault entry + response, the API connect/quick-connect builders,
import defaults, and the connections.html entry editor (Video Performance
section). Defaults preserve existing behavior; VDI sessions stay off.
Cherry-picked from pletch/rustguac@da3cfda
Typescript recording is now per-connection opt-in, off by default. Adds a
record_typescript flag on the address-book entry (Vault), threaded through
EntryInfo / CreateSessionRequest, and a "Enable typescript recording for
this session" checkbox in the connection editor's Recording Settings (SSH
entries only). The SSH branch records a typescript only when the entry has
opted in AND [recording].typescript_path is configured globally. Ad-hoc
SSH sessions (no entry) never record.
Docs: document the per-connection opt-in, and add a LUKS-at-rest recipe
(point typescript_path at a subdir of the LUKS-encrypted drive volume
rustguac already mounts) as the recommended way to encrypt typescripts at
rest with no extra infrastructure.
Per-entry boolean fullscreen_on_connect flag. When set, the client enters
browser fullscreen on the first user gesture after CONNECTED and locks
the Escape key (Chromium navigator.keyboard.lock API) so it reaches the
remote session instead of exiting fullscreen. Firefox / Safari fall back
to standard fullscreen with a one-time toast explaining Esc will exit.
A small floating "Fullscreen" toggle in the top-right corner lets any
user enter fullscreen at any time once the session is connected. In
fullscreen mode a thin top bar shows the entry name plus Exit and
Disconnect buttons.
Closes#154.
Closes#132.
VDI containers come in two patterns: ones whose entrypoint reads
VDI_USERNAME / VDI_PASSWORD env vars and provisions an account
matching them (the contrib/vdi-test-image style), and ones with a
baked-in fixed account that ignore those env vars. Pre-change, only
the first worked; users with baked-image containers had to log in
manually inside the session because rustguac's auto-derived RDP
credentials never matched the container's actual account.
- AddressBookEntry gains optional container_username and
container_password fields, persisted to Vault.
- When set, session.rs uses those values for the RDP connect into
the container instead of deriving the username from the
operator's identity and generating a per-connect password.
- VDI_USERNAME / VDI_PASSWORD env vars are still injected with the
resolved values - images that read them get consistent state;
images that ignore them keep using their baked-in account.
- The container name derives from the resolved username, so an
entry with a fixed container_username produces a container
shared by all operators connecting through that entry. Documented.
- EntryInfo exposes container_username back to the editor;
container_password is never serialised to clients (has_container_password
boolean indicates whether one is stored).
- The entry update handler preserves container_password when not
supplied on update (same pattern as password / private_key).
- Both fields go through entry_credential_variables and
resolve_credential_variables, so the actual values can be sourced
from each operator's saved credential variables ($corp_username etc).
- Connections UI gains the two fields with explanatory text linking
out to the VDI docs and credential variables docs.
- docs/vdi.md restructured around the two patterns (Pattern A:
env-var driven, Pattern B: baked-in account) with the
container-sharing note for Pattern B.
- docs/credential-variables.md gains an explicit "where variables
can be used" table covering the new fields.
Subtle side fix: env merge in session.rs used to call
env.entry(K).or_insert(V), which meant a user-supplied
VDI_USERNAME in container_env would silently win over the
auto-derived one - opposite of the documented intent
("Don't let user-provided env override the core VDI vars").
Switched to env.insert() so the resolved values always win.
Adds a find-as-you-type search input to the Connections page entries-header
with global search over every entry the user has access to. Search runs
client-side against an in-memory index built from a new endpoint.
Backend (GET /api/addressbook/search-index):
- Iterative tree walk (BFS over (scope, path) queue) using list_folders +
list_subfolders.
- Subfolder traversal is unconditional because resolve_folder_access permits
a child to grant access independently of a denied parent; ACL is enforced
per folder before its entries are emitted.
- Returns flat {entries: [{scope, folder_path, entry: EntryInfo}]}.
- Operator role required, admin bypass.
Frontend (static/connections.html):
- Search input lives in .folder-actions between folder title/desc and admin
buttons; auto right margin keeps add/edit/delete folder buttons hard-right.
- loadSearchIndex runs once after loadFolders; placeholder shows "Indexing..."
until ready.
- Tokenized substring matcher with simple scoring (name-prefix > name-substring
> host > folder-path); cap at 50 results with "+N more" footer.
- Results render in entries-table styling with a Folder breadcrumb column,
inline Connect, and an "open folder" link. Matched substrings highlighted
with <mark>.
- Connect from search results looks up the entry in searchIndex (not
currentEntries) when searchActive is true.
- "open folder" walks the tree, expands ancestors via loadSubfolders chain,
selects the target, scrolls into view, clears search.
- Keyboard: / focuses the input (skipped in inputs/textareas/modals); Esc
clears the query then blurs.
CSS (static/rustguac.css):
- .connections-search styling, mark highlight, breadcrumb cell,
search-open-folder link, and search-more footer.
### RDP NTLM default
New [rdp] config section with default_auth_pkg. The resolver in
session.rs walks entry value -> config default -> hardcoded "ntlm".
Kerberos/Negotiate are still selectable per-entry or via the config
override, but the default is NTLM because Kerberos needs a KDC
reachable via DNS (often over TCP) and its failure mode is a silent
RDP hang that looks exactly like an unrelated network issue.
Existing entries and Guacamole-imported entries that stored
auth_pkg = None now resolve to NTLM automatically. Admins who do
run Kerberos-integrated hosts can set default_auth_pkg =
"kerberos" or "negotiate" in config.toml to restore the old
behaviour.
UI: the entry modal's NLA dropdown now says "Server default (NTLM)"
instead of "Default (negotiate)" so the behaviour matches the
label. Added an explicit "Negotiate (Kerberos first, NTLM fallback)"
option for completeness.
5 unit tests cover the resolver matrix (entry wins, empty entry
falls through, no entry falls through, empty config default falls
through, server default wins when entry is None).
### Connections tree persistence
Folder expansion state and the selected folder are now persisted
to localStorage, so reopening the page / logging back in no longer
collapses the whole tree or snaps you back to the alphabetical
first folder.
- `rustguac_connections_expanded`: {scope|path: true} map, saved
on every toggleFolder() and on the auto-expand-on-subfolder-
create path.
- `rustguac_connections_selected`: {scope, path}, saved on every
selectedFolder assignment (click, new folder, new subfolder,
delete-to-null, move entry).
On page load, loadFolders() now chains: fetch top-level folders ->
restoreExpandedTree() walks saved keys shallowest-first so deeper
paths can resolve via findFolder() after their ancestors populate
subfolderCache -> try restoring saved selection -> fall back to
the current auto-select-first behaviour only if nothing restored.
Stale keys (deleted folders, ACL-revoked folders) are dropped
opportunistically during the restore walk. Per-browser, not
per-user; try/catch wraps every storage call so private-mode / quota
errors degrade silently to the pre-persistence behaviour.
Prior behaviour: delete_folder only cleared entries and .config of
the named folder; subfolder .config keys were left behind. Pre-v1.6.0
that was fine because subfolders didn't exist. Post-subfolders the
UI would refresh after delete, list_children would still find the
orphaned subfolder markers, and the folder would appear stuck (the
DELETE request returned 204, no error surfaced, nothing looked
broken except the folder refused to go away).
delete_folder now BFS-collects every folder path in the subtree
before wiping entries and .config at each level. Return type changes
from Result<(), _> to Result<(usize, usize), _> so the endpoint can
report (subfolders_deleted, entries_deleted) back to the UI; the
single caller in api.rs is updated.
UI: confirmation prompt now explicitly mentions "AND all its
subfolders and entries" when the selected folder has_children is
true, so admins don't nuke a subtree by accident. After a successful
delete, a transient banner reports the number of entries (and
subfolders when > 0) that got swept up.
FolderConfig gains `inherit_from_parent: bool` (default false via
#[serde(default)] so existing deployments keep their per-folder-only
semantics). When true, if a folder's own allowed_groups doesn't grant
the caller, the access check walks up the slash-separated path and
evaluates each ancestor the same way. Admins still bypass all checks.
Inheritance stops at any folder with the flag off, preserving the
ability to lock down a specific subtree.
New resolve_folder_access helper in vault.rs centralises the walk-up
logic; check_folder_access, ab_list_folders, and ab_list_all in
api.rs all route through it so access semantics live in one place.
Import: new --allowed-groups flag writes the ACL onto the root import
folder only; subfolders are created with inherit_from_parent=true so
the whole imported tree picks up the same rules without per-folder
writes. Matches the pattern admins will want after an initial
Guacamole migration where every connection group should be visible
to the same OIDC group.
Related fix: list_credential_variables in api.rs no longer scans only
top-level folders — it now recurses into subfolders so variable
references buried in an imported tree show up on the My Credentials
page (previously they stayed invisible post-v1.6.0 when subfolders
became first-class).
UI: folder modal gets an "Inherit permissions from parent folder"
checkbox. Defaults: unchecked for new top-level folders (no parent
to inherit from), checked for new subfolders, and reflects the
stored value when editing.
Feature #103: single-entry auto-connect
- New `auto_open_if_singleton: Option<bool>` on AddressBookEntry and
EntryInfo. Admin ticks it per entry in the Connections modal;
importer initialises to None.
- After the /api/addressbook batch resolves on the Connections page,
if the user sees exactly one entry and it has the flag set, the
page fetches /connect and navigates the current tab to the new
session. Same-tab navigation (not a popup) because browsers block
window.open after an async fetch without a user gesture.
- A sessionStorage flag gates the auto-open to once per browser
session — an accidental refresh of Connections doesn't re-spawn
sessions. Logout clears the flag so the next login fires again.
- Admins never meet the singleton condition (they have many
entries) so this is effectively kiosk-only.
Escape hatch: client.html Home button
- Ctrl+Alt+Shift panel header now has a 🏠 Home button next to the
close ×. Takes the tab back to /connections.html — primary route
for a kiosk user whose session auto-opened into this tab.
Bug fix: folder-modal group picker silently added a group on save
- addFmGroupFromInput used to fall through to picking the combo's
highlighted suggestion when the input was empty. fm-save calls
addFmGroupFromInput to flush any typed-but-not-chipped text; with
an empty input that meant every Save on an existing folder tacked
on the first unselected known group (e.g. an operator group) —
visible only on the next edit. Split the "pick suggestion when
empty" behaviour out to an explicit helper that only fires from
Enter-with-empty-input, never from Save.
Per-entry Share gating
- AddressBookEntry gains an `allow_sharing: Option<bool>` (default
None = off). Admin opts in per entry via an Allow session sharing
checkbox in the entry modal.
- EntryInfo exposes the field so the modal prefills on edit.
- CreateSessionRequest carries it through; the ab_connect_entry
quick-connect and legacy quick-connect paths propagate the entry's
value.
- Session gets a share_allowed bool. SessionInfo.share_url is only
populated when share_allowed is true, so the Connections
Active Sessions Share button auto-hides without any new UI logic.
- Default behaviour on create:
* explicit allow_sharing on the request → honoured
* entry-derived session without flag → off (admin opt-in)
* ad-hoc session (no address_book_entry) → on
The ad-hoc default preserves the long-standing API-key
session-creation flow where external callers expect share_url in
the POST /api/sessions response.
Modal checkbox alignment (side-effect fix)
- The generic .modal input rule in rustguac.css was forcing every
input — checkboxes included — to 44px height + block + 100% width,
which misaligned every tickbox/label pair across the entry, folder,
and onboarding modals.
- Added carve-outs for input[type=checkbox] / [type=radio] that keep
native size, inline, with a small right margin.
- Labels that directly contain a checkbox/radio (via :has()) now flex
the control + text on one baseline with a clean gap, and drop the
uppercase letter-spacing used for full-width field labels.
Modified-stage-4: always-visible buttons, no hover-reveal.
- Each active session card now has a Terminate button next to Share.
Confirmation dialog on click, DELETE /api/sessions/{id}, reloads
the grid on success. Hover state uses .btn-danger (primary red).
- Share button still hides when share_url is absent so the upcoming
per-entry allow_sharing toggle gates it automatically.
- Share modal's caution text was a dim .field-hint that was easy to
miss. It now renders as a proper warning banner: 1px border +
bumped left edge in --status-pending (theme-aware yellow), ⚠ icon,
and larger body text for legibility.
Connections
- Clicking an active session card used to POST /connect for entries
with an address_book_entry, which minted a new session every time.
Confirmed in the log: a web-session owner clicked back to their
active card after a shadow join and got two duplicate sessions.
- Now the click just opens s.client_url — which attaches to the
existing session. Dormant VDI containers below still need /connect
(no live session to attach to), so that branch is untouched.
Tokens page
- Operator/viewer roles saw the Sessions nav link on the Tokens page
(every other page correctly hid it for level < 3). Clicking it
redirected straight back out because sessions.html rejects level < 3.
- Added the same hide-on-level-<3 logic as the other pages; also
reveal the Reports link for level >= 3 in the same pass.
Part of the Sessions-page rework (shadow-sessions plan, stage 1).
The Sessions page is becoming an admin/poweruser management view;
user-initiated share lives with the user's own active sessions in
Connections.
Connections
- Each active session card gets a Share button in a new action row
below the thumbnail/meta.
- Share opens a themed modal with the full share URL pre-selected
and a Copy button (async clipboard with execCommand fallback).
- Clicking the card still reconnects — Share handler stops
propagation so the two actions don't collide.
- Overlay click or Close dismisses the modal.
Sessions page
- Share column removed (was columns 10 of 10 — down to 9).
- Dead CSS (.btn-share, .share-url) and JS (expandedShares,
shareFullUrl, rowId, the share/copy click handlers) removed.
- Ad-hoc jump-host styling (.btn-add-hop) stays.
Three pieces of v1.6.0 work that happened together and are easier to
review as one save point.
Rename: Address Book → Connections
- static/addressbook.html renamed to static/connections.html
- Nav links, page titles, empty states, onboarding, and prose updated
across all 8 static pages (connections, admin, docs, index,
recordings, reports, sessions, tokens).
- README, CLAUDE.md, and every file under docs/ updated.
- src/main.rs: connections.html added to the branded-page map and
route list; /addressbook.html returns a 308 permanent redirect so
existing bookmarks keep working.
- Backend API paths, Rust types, and Vault storage paths are
deliberately unchanged — internal only.
Folder allowed_groups picker
- New SQLite table `seen_groups` tracks OIDC groups observed in any
user login; OIDC callback upserts after extracting groups.
- `GET /api/auth/known-groups` (admin-only) returns the union of
group_role_mappings and seen_groups.
- `GET /api/addressbook/folders/{scope}/{folder}/config` adds the
missing endpoint the frontend was already calling — existing
allowed_groups now prefill the edit-folder modal.
- Folder modal swaps the free-text comma-separated input for a chip
picker with a themed combobox dropdown: autocomplete over known
groups, keyboard nav, "+ add custom" row for unlisted groups.
Active session visibility (GitHub #102)
- `GET /api/sessions` scopes to the caller's own sessions by default;
`?all=true` lets admins opt in (used by the Sessions page).
- `GET /api/sessions/{id}` and the thumbnail GET/PUT endpoints are
now owner-or-admin, returning 404 for other callers so session
existence isn't leaked.
- Connections' Active Sessions strip is now always owner-scoped —
admins still manage everyone via the Sessions page.