User Flows (Step-by-Step)
3.1 Authentication Flow (Login)
Entry: User opens app → default redirect to /auth.

Fig 1 — Login Page: Email address, Send magic code and the Google option
Login page (/auth) — User sees "Sign in to SpaceOS Automation" with an Email address field. User enters the work email → Send magic code. The footer shows the build version (e.g. Version 0.1.0 · ZenSpace SpaceOS Automation).

Fig 2 — Verification code step: sent-to banner above the masked code field
OTP request — App calls requestOTPSevice(email). A "Verification code sent successfully" toast confirms the send and the card switches to the Verification code step, showing the banner "We’ve sent a verification code to <email>. Enter it below to finish signing in." above a masked 6-digit field. Use a different email returns to the email step.

Fig 3 — OTP Email: "Your ZenSpace Verification Code" in the inbox

Fig 4 — Verification code entered, Verify & continue
OTP verification — The code arrives from ZenSpace <admin@zenspace.io> with the subject "Your ZenSpace Verification Code" and expires in 10 minutes. User enters the 6-digit code → Verify & continue. App calls verifyOTPSevice(session_id, otp). On success: checkAuth() runs, then redirect to /organization.
Already logged in — If isAuthenticated and user lands on /auth, they are redirected to /organization.
Auth Contract
Token is stored in localStorage.auth_token. A 401 from any authenticated request clears the token and pushes the user back to /auth.
Testing focus: Invalid email, OTP request failure, wrong/expired OTP (past the 10-minute window), Use a different email, resend behaviour, redirect when already authenticated.
3.2 Organization Selection Flow
Entry: After login, user is on /organization.

Fig 5 — Organization Selection ("Select an organization")
Organization list — Page title "Select an organization" with the sub-line "Choose where you want to work today. Your access and permissions change based on the selected organization." Data from /auth/organizations, rendered as a card grid: initials or logo avatar, name, contact email, an ACTIVE status tag, address (or "No address on file"), timezone, space allowance (e.g. "Unlimited spaces") and a Created date. A Search organizations box and a refresh control sit in the top bar. Card action menu (three-dot) is only visible to super_admin users.
Select organization — Each card carries an Open → action which calls navigate('/physical-spaces?orgId=<org.id>') — Physical Spaces is the landing module after org selection.
Organization context — OrganizationProvider reads orgId from URL, fetches org via getOrganizationByIdAction(orgId), and stores it in Redux (organization). All protected pages use this for API org_id, breadcrumbs, timezone formatting, etc.

Fig 6 — Switch Organization: search panel with recent organizations

Fig 7 — Switch Organization: full list with search and scroll
Switch organization — From any protected page, open the sidebar Team Switcher (organization name and email at the top of the sidebar); the popover offers Switch Organization, which navigates back to /organization.
Testing focus: No orgs (empty state copy), API failure, missing/invalid orgId on a protected route, Switch Organization from different pages.
3.3 Physical Spaces Flow (Primary Module)
Entry: /physical-spaces?orgId=<id> — selected directly from the organization card.

Fig 8 — Physical Spaces: Card View

Fig 9 — Physical Spaces: Table View
Physical Spaces list — Fetches from /physical-spaces?org_id=<org_id>. The top bar holds a "Search physical spaces" box, a refresh control and the table/card view toggle; Create Space sits above the list and Rows-per-page with pagination below. Card view shows Name, description (or "No description"), Status (Active/Inactive), the linked virtual pod as a badge (e.g. Oak Boardroom) and a "Created … ago" timestamp, each with a ⋮ menu. Table view lists Space, Status and Created with a View button and ⋮ menu per row.

Fig 10 — Create physical space modal (Name, Description)

Fig 11 — Physical Spaces: Card View with the newly created space
Create space — Create Space opens the Create physical space modal ("Physical spaces are real-world areas with an active date range."). Required: Name (≤200 chars); optional: Description. Helper copy notes that dates use the organization timezone and that past days in that timezone cannot be selected. Confirm with Create; Close dismisses.

Fig 12 — Physical Spaces: card ⋮ menu (Edit / Delete)

Fig 13 — Edit physical space modal (Name, Description, Active toggle)
Edit space — ⋮ → Edit opens the Edit physical space modal pre-filled with Name and Description, plus an Active toggle ("Inactive spaces may be hidden from scheduling flows."). Confirm with Save changes.

Fig 14 — Delete physical space: Caution confirmation
Delete space — ⋮ → Delete opens the Caution modal ("Delete this physical space? This cannot be undone."). Confirm with Delete or dismiss with Close.
Open space detail — Click row/card or View → /physical-spaces/:id?orgId=... (URL built via appendOrgIdToUrl).
Physical space detail — the header carries Preview display and Edit actions plus four summary tiles: Status (with elapsed duration), Devices (count, or "None linked yet"), Active Pod (pod name and copyable mapping ID, or "No pod linked") and Last Updated. Below them sits the pill nav — Mappings, Devices, Webhook Logs and Attempt History, with Virtual Pod appearing as a fifth tab only while a pod is mapped:

Fig 15 — Physical Space Detail: Mappings Tab — table view with an active mapping

Fig 16 — Physical Space Detail: Mappings Tab — calendar view with month counters

Fig 17 — Physical Space Detail: Mappings Tab — "No mappings yet" empty state
Mappings — Mapping Timeline of virtual-pod links in table and calendar views. The table lists Virtual Pod, Period, Status and Action with per-row Unlink; the Mapping Calendar shows mappings for the month in the organization timezone with total / active / scheduled / expired counters and month navigation. With nothing linked it shows "No mappings yet".

Fig 18 — Physical Space Detail: Virtual Pod Tab (amenities, status timeline, current booking)
Virtual Pod — visible only while a mapping is active. Four tiles show Virtual Pod (name and internal code), Current Status, Available At (with timezone) and Bookings count. A Meeting Space Details panel shows Group, Capacity, Available From, Available Until and Amenities tags (e.g. WiFi, Wheelchair Access, U-shape setup, VC camera). A Status Timeline shows Previous / Current / Next state, and while a booking is running a Current Booking block lists Organizer, Email, Start and End; with nothing in progress it reads "No active booking data available for this virtual pod."

Fig 19 — Physical Space Detail: Devices Tab — table view with connected adapters

Fig 20 — Physical Space Detail: Devices Tab — card view with the magic-link toggle

Fig 21 — Devices card view expanded: full URL, adapter ID, actions and access methods

Fig 22 — Adapter card ⋮ menu (Test / Manage Device / Disable / Delete)

Fig 23 — Adapter Actions menu (ADMIN and GUEST-EQUIVALENT commands)

Fig 24 — Delete adapter: Caution confirmation (Delete Adapter)

Fig 25 — Physical Space Detail: Devices Tab — adapter and display empty states

Fig 26 — Add Device Adapter modal (endpoint URL, API key, Preview Capabilities)

Fig 27 — Create Meeting Room Display modal (physical space pre-filled)

Fig 28 — Verification Code modal after creating a display from the space

Fig 29 — Physical Space Detail: Devices Tab — the new display listed (Not verified, Offline)
Devices — adapters and meeting room displays connected to this space, in table or card view. The adapter table lists Name (with criticality tag CRITICAL / STANDARD / OPTIONAL and device kind, e.g. Door, Sensor, Ac, Fan, Light, Web-Display, Custom), Brand · Model, Endpoint, Status, Health ("Last pinged …") and Magic-link visibility, with search, view toggle, refresh and Add Device; empty it reads "No adapters connected". Card view adds a "Show on guest magic-link page" toggle (when off the device is hidden from the booking’s magic-link page while the adapter itself stays active — use Status → Disabled to fully disable it), Webhooks / Embeds indicators, the adapter version, Test connection (result shown inline, e.g. "Ping succeeded in 15ms (HTTP 200)") and Show more, which reveals Full URL, Adapter ID, Ping Failures, Last Status Change, Created, JSON Schema support, the supported Actions per role and an Access methods matrix (QR Code, NFC Card, Pin Code, Hand Wave, Mobile Tap, Touch Pass, Face Unlock, Mobile Wave, Mobile Shake, Mobile Unlock). The card ⋮ menu offers Test, Manage Device, Disable and Delete; Delete opens a Caution modal ("This will permanently remove the adapter and its configuration from this physical space") confirmed with Delete Adapter. Actions opens a role-split command list — ADMIN (Unlock, Lock, Lock Rule, Status, Audit Log) and GUEST-EQUIVALENT (Unlock, Status). Add Device opens the Add Device Adapter modal, which takes the full Adapter Endpoint URL (including the device_id query string) and the adapter-provided API Key, then Preview Capabilities. Below it a Meeting Room Displays list shows Name, Status (Linked), Connectivity (Online/Offline) and Created At, with Create display; empty it reads "No displays connected". Create display opens a modal with the physical space pre-filled and read-only; on submit a Verification Code modal shows the pairing code and the new display is listed as Not verified / Offline until the code is entered on the device.

Fig 30 — Physical Space Detail: Webhook Logs Tab — delivered events with Export As

Fig 31 — Webhook Logs Filter modal (status, event type, date range)

Fig 32 — Physical Space Detail: Webhook Logs Tab — empty state
Webhook Logs — logs for linked display devices (Event Type, Status, Processing Time, Created At) with search, Filter, Export As and pagination; click View on a row to open /physical-spaces/webhook-logs/:logId. Filter opens a modal with Status, Event Type, Start Date and End Date, plus Reset and Apply. Realtime via useRealtimeWebhooks.

Fig 33 — Physical Space Detail: Attempt History Tab (empty state)

Fig 34 — Attempt History Filter modal (status, credential type, date range)
Attempt History — append-only log of credential generation attempts (Type, Status, Error, Trigger, Duration, Time) with search and Filter; click a row to open /physical-spaces/credential-logs/:requestId. Filter opens a modal with Status (e.g. Generated, Missing Config), Credential Type (e.g. Lock, Wi-Fi), From Date and To Date, plus Reset and Apply. With nothing recorded it shows the "No credential attempts yet" empty state.
Testing focus: create/edit/delete space, name length limit, Active toggle, mapping calendar + table, unlink mapping, virtual-pod state and current-booking block, Add Device Adapter URL/API-key validation and Preview Capabilities, magic-link visibility toggle, Test connection, adapter Actions per role, delete adapter, create display from the space, webhook and attempt-history filters, realtime status, and every empty state.
3.4 IoT Adapters Flow
Entry: Sidebar → IoT Adapters → /IoT-Adapters.

Fig 35 — IoT Adapters: List with the "No IoT Adapters Found" empty state
Adapters list — Lists the organization’s IoT adapter records. Columns: Name, Verified (Verified / Not Verified), Server status, Onboarding status (Verified / Unverified) and Created At, with per-row View and a ⋮ menu. A "Search adapters" box and refresh control sit in the top bar. Create sits above the table; with none created the table shows "No IoT Adapters Found" with Create IoT Adapter and Refresh actions.

Fig 36 — Create IoT Adapter modal
Create adapter — Create opens the Create IoT Adapter modal. Required: the adapter name, used to identify it in the admin dashboard. Confirm with Create adapter; Close dismisses without saving.
Adapter detail — View opens the adapter detail page (adapter details and network configuration). Header badges show verification (NOT VERIFIED / VERIFIED) and connectivity (OFFLINE / ONLINE). Three summary tiles: Verification Code, Last Seen (Never until the adapter first checks in) and Created. The Overview tab holds Basic information (adapter name, Verification, Verif. Code, Org ID) and Timestamps (Created, Updated, Last Seen).
Edit adapter — ⋮ → Edit opens the Edit IoT Adapter modal pre-filled with the current adapter name. Only the name is editable; confirm with Update adapter.
Delete adapter — ⋮ → Delete opens the Caution modal ("This action cannot be undone"). Confirm with Delete or dismiss with Close.
Testing focus: create adapter (name required, max length), edit name, delete confirmation and list refresh, verification-code display, realtime onboarding / server-status updates on the list, empty state with no adapters.
3.5 Meeting Room Displays Flow
Entry: Sidebar → Meeting Room Displays → /meeting-room-displays.

Fig 37 — Meeting Room Displays: List with two displays (physical and virtual space columns)

Fig 38 — Meeting Room Displays: List with the "No Displays Found" empty state
Displays list — Fetches from /displays?org_id=<org_id>. Columns: Name, Physical Space (a link through to the space), Virtual Space (the mapped pod), Verified (Not verified until the code is entered on the device, Linked once paired) and Created At, with per-row View and a ⋮ menu. A "Search displays" box and refresh control sit in the top bar; Create sits above the table, Rows-per-page and pagination below. With none created it shows "No Displays Found" with Create and Refresh actions.

Fig 39 — Create Meeting Room Display modal (Select physical space)

Fig 40 — Create Meeting Room Display: physical space dropdown open

Fig 41 — Create Meeting Room Display: physical space selected

Fig 42 — Verification Code modal (pairing code shown once)
Create display — Create opens the Create Meeting Room Display modal. Opened from this page it offers a Select physical space dropdown listing the organization’s spaces (clearable once chosen); opened from a physical space it is pre-filled and read-only. Confirm with Create display — a "Resource created successfully" toast fires and the Verification Code modal shows the pairing code to enter on the physical device.

Fig 43 — Meeting Room Display Detail: Overview (Basic Information, Android Device Info, Metadata)

Fig 44 — Meeting Room Display Detail: Status (current / previous / next state)

Fig 45 — Meeting Room Display Detail: Integration (meeting space, organization ID, webhook URL)
View display detail (/meeting-room-displays/:id) — the header shows the live space state badge (e.g. Available). Eight tabs: Overview (Basic Information — timezone, local time, Display Name, Verification Code, Verification Status; Android Device Info — platform, model, OS version, app version, last seen, Online/Offline; Metadata — Created At / Updated At), Status (Meeting Space Status: Current / Previous / Next State and Available At), Integration (linked Meeting Space ID and name, Organization ID, Webhook URL or "Not configured"), Media (Display Image with placement badge, Logo, each with a Configure action), Configuration, Devices, Webhook Logs and Notifications.

Fig 46 — Meeting Room Display Detail: Media (display image and logo)

Fig 47 — Configure image modal (placement SIDE / BG / FULL_IMAGE)

Fig 48 — Configure logo modal
Media configuration — Configure image opens a modal with Image placement (SIDE / BG / FULL_IMAGE) and an upload area accepting PNG, JPG or GIF up to 5 MB; Configure logo opens the equivalent logo upload modal. Save image / Save logo commits the change, Close dismisses it.

Fig 49 — Meeting Room Display Detail: Configuration (booking URL and display options)
Configuration — Booking URL (the URL booked on this display; must be a valid URL) plus Display Options toggles: Show Booking URL (booking URL / QR code on the device), Show Meeting Space Name, Show Booking User Name, Show Day Availability Scroller, Show Next Available Time, Show Booked Until End Time and Show Available Until End Time. Changes are committed with Save Configuration.

Fig 50 — Meeting Room Display Detail: Devices → Screenshots (with Request Screenshot)

Fig 51 — Screenshot Details modal (captured display frame and Download)
Devices — shows the paired Android device (model, platform, app version, OS version, Wi-Fi and Online/Offline state) and a Screenshots subsection. The screenshot table lists Thumbnail, Screenshot ID, Captured At, File Size, Created At and per-row view / download actions, with search, refresh, pagination and a Request Screenshot action. Opening a row shows the Screenshot Details modal — the captured display frame plus Captured At, File Size, Created At, full Screenshot ID and Download.

Fig 52 — Meeting Room Display Detail: Webhook Logs (empty state)
Webhook Logs — same shape as the physical-space log list (Event Type, Status, Processing Time, Created At) with search and Filter; when the display has received nothing it shows the "No Webhook Logs Found" empty state.

Fig 53 — Meeting Room Display Detail: Notifications (counters and delivery log)
Notifications — five counters (Total, Sent, Delivered, Failed, Pending) above a table of Event Type (e.g. display.status.updated, display.config.updated, screenshot.request), Status, Device Online, Connected Sockets, Triggered By, Delivered At and Created At, with search and Filter.

Fig 54 — Meeting Room Displays: row ⋮ menu (Edit / Delete)

Fig 55 — Edit Meeting Room Display modal (update the linked physical space)
Edit display — ⋮ → Edit opens the Edit Meeting Room Display modal ("Update the physical space linked to this meeting room display"), a clearable physical-space dropdown confirmed with Update display.

Fig 56 — Delete display: Caution confirmation

Fig 57 — Meeting Room Displays: list after deletion, with the success toast
Delete display — ⋮ → Delete opens the Caution modal ("Are you sure you want to delete this display? This action cannot be undone."). Confirm with Delete; a "Resource deleted successfully" toast fires and the row leaves the list.
Form Invariants
booking_url (display config) must be a valid URL.
Testing focus: Create → realtime verify, edit, delete; save configuration and confirm the display reflects it; Request Screenshot then open and download the result; webhook-log and notification lists including empty states.
3.6 Kiosk Tablets Flow
Entry: Sidebar → Kiosk Tablets → /kiosk-tablets. New in v3.6.

Fig 58 — Kiosk Tablets: summary tiles and the pod-to-tablet list (both pods Unassigned)
Kiosk Tablets list — Page title “Kiosk Tablets” with the sub-line “Map each pod to a Hexnode tablet and keep an eye on it.” The top bar holds a “Search pods or tablets” box and a refresh control; the header shows an “updated … ago” freshness stamp plus View all Kiosk tablet devices and Settings actions. Four summary tiles: Pods (pods in this org), Assigned (“N still without a tablet”), Live (online tablets “of N assigned”) and Needs Attention (“Everything healthy” when zero). The table lists one row per pod (physical space): Pod (space name, the mapped booking pod e.g. “Booking: Oak Boardroom”, and the physical-space ID), Display (the pod’s web-display adapter ID, ada-…), Tablet (the assigned Hexnode tablet, or a + Assign tablet link), Status (Unassigned until a tablet is mapped) and Actions (Assign).

Fig 59 — Assign tablet modal: Web / Native display type and the Hexnode tablet picker
Assign tablet — Assign (or the + Assign tablet link) opens the Assign tablet modal for that pod. Display type toggles between Web and Native. Web: “The tablet opens this pod’s web display in the browser. Its display id is resolved for you and written to the tablet’s Hexnode Asset Tag.” Native: “The tablet runs the native display app, which handles its own room. No asset tag — only native group membership.” The Tablet dropdown searches the tablets enrolled in Hexnode (e.g. “45 tablets enrolled in Hexnode”), each shown as name · model, with “— offline” appended for tablets that have not checked in. Assign stays disabled until a tablet is chosen; Close dismisses.
Kiosk settings — Settings opens the Kiosk settings modal (“Pick the Hexnode group each display type belongs to.”). Fields: Web display group and Native display group (Hexnode groups with device counts, e.g. “Zenspace MRD Web app — 22 devices”), plus Web display policy and Native display policy (Hexnode policies). Helper copy: “Setting a policy per type lets a display-type switch strip the other type’s policy from the tablet.” Confirm with Save settings; Close dismisses. The current values load a moment after the modal opens.

Fig 60 — All Devices: every Hexnode tablet across organizations, with status, assignment and organization filters
All Devices (/all-devices) — View all Kiosk tablet devices opens a cross-organization page (“Every Hexnode tablet across every organization.”) with its own sidebar (Organizations only) and a back arrow. Four tiles: Total (enrolled tablets), Online, Offline (“No recent check-in”) and Needs Attention (“Offline, unmapped or drifted”). Filters: a search box (name, pod, org, model, asset tag), Status (All statuses / Online / Offline / Needs attention), Assignment (All assignments / Assigned / Unassigned) and Organization, with an “N of N” result count and Refresh. Columns: Device (name, model · Android version), HX id (Hexnode device ID), Status (Online / Offline, with a “Needs attention” flag under it where relevant), Asset Tag (the web-display adapter ID, ada-…, for web-display tablets), Mapped pod (pod name and booking), Org, Policy (with its source group, e.g. “via Zenspace MRD Web app” or “via Zenspace Meeting Display App”) and Last check-in.
Testing focus: tile counts vs the table, search by pod and by tablet, Assign with Web vs Native (asset tag written only for Web), Assign disabled with no tablet selected, offline tablets flagged in the picker, Kiosk settings save and policy swap on a display-type switch, All Devices filters and counts, and empty / error states when Hexnode is unreachable.
3.7 API Keys Flow
Entry: Sidebar → API Keys → /api-keys.

Fig 61 — API Keys List: one active key
Keys list — Fetches from /org-api-keys?org_id=<org_id>. Columns: Name, Permissions (R / W / U / D), Status (Active/Inactive), Expiration (Never when no expiry is set), Last Used (Never until first use), Created, Revoked At and Actions. A "Search API keys" box and refresh control sit in the top bar; Create Key sits above the table, Rows-per-page and pagination below. Each row carries a Rotate button and a ⋮ menu.

Fig 62 — Create API key modal (name, Expires At, permissions)
Create key — Create Key opens the Create API key modal ("The plaintext key is shown once after creation"). Fields: Name, Expires At (optional date picker) and Permissions — Read, Write, Update and Delete, all selected by default. Confirm with Create key; Close dismisses.

Fig 63 — Copy new API key modal (plaintext shown once)

Fig 64 — Copy new API key modal after copying (tick shown, confirm enabled)

Fig 65 — API Keys List: two active keys after the new key is created
Copy the plaintext key — On success a "Resource created successfully" toast fires and the Copy new API key modal shows the org_-prefixed key once, with a copy icon. The icon turns to a tick once copied and the I have copied the key button becomes active; the key is never retrievable again after the modal closes.

Fig 66 — API Keys: row ⋮ menu (Revoke)

Fig 67 — Revoke API key: Caution confirmation
Rotate / revoke key — Rotate issues a replacement (the old key is revoked and the new plaintext key is shown through the same modal). ⋮ → Revoke is the only item in the row menu and is available while the key is active; it opens a Caution modal ("Revoke this API key? This action cannot be undone.") confirmed with Revoke or dismissed with Close.
Critical UX Guardrail
Plaintext API keys are visible exactly once — inside OrgApiKeySecretModal after create or rotate. Do not log, store, or surface them anywhere else.
Testing focus: create with and without an expiry, deselecting individual permissions, confirm the copy tick enables I have copied the key, ensure the plaintext is unrecoverable after closing, rotate, revoke, and expired-key display formatting.
3.8 Public Magic Link Flow (/magic/:token)
Entry: Guest opens /magic/:token directly — no login required.
Fig 68 — Magic Link: Loading state ("Verifying your access…")
Bootstrap — Page calls getMagicLinkContext(token) (raw fetch against VITE_BACKEND_URL via joinBackendUrl). Loading state: animated shield with "Verifying your access..."

Fig 69 — Magic Link: Ready state — booking banner with device sections (Door Access, WiFi) and adapter iframes
Ready state — Banner shows booking space name and formatted booking window. Devices grouped into sections (locks, wifi, fan, AC, lights, sensors, cameras, other). Each device card may render an adapter iframe (adapter_embed_html) or an error state if adapter_embed_error is present. Iframes use sandbox="allow-scripts".
Fig 70 — Magic Link: Error state — expired/invalid token with Try again
Error state — Invalid/expired token, network error, or backend rejection → error icon, server message, Try again button.
Empty state — If context.devices is empty: "No devices are currently available for this booking."
Critical Guardrails
This page must remain reachable without an authenticated session.
Do not log token, embed HTML, or any guest credentials to console or analytics.
Do not loosen the iframe sandbox (sandbox="allow-scripts" only).
Testing focus: valid token (booking + devices), expired/invalid token, server error → retry, no devices, timezone formatting, iframe rendering.
3.9 Webhook Logs Flow
There are two webhook log surfaces:
Display Webhook Logs
Source: /displays/:id/webhook-logs and the global /webhooks/logs.
Detail route: /meeting-room-displays/webhook-logs/:logId.
Realtime: useRealtimeWebhooks consumes mrd:webhook.received.
Physical-Space Webhook Logs
Source: /webhooks/physical-space/logs?physical_space_id=<id>.
Detail route: /physical-spaces/webhook-logs/:logId.
Realtime: mrd:physical-space.webhook.processed.

Fig 71 — Webhook Log Detail: Overview (event, latency, signature, retries)
Both detail pages show the full payload and metadata. The Webhook Log detail page presents summary tiles for Event (type and trigger, e.g. space.stateChanged triggered by DAY_START), Latency (processing time and HTTP method), Signature (Valid, with origin IP), and Retries, followed by Overview / Headers / Payload / Response tabs. The Overview tab shows Event Information (event type, event ID with copy, HTTP method, status), Timing & Performance (processing time, created/updated, retry count), and Security (signature verification and request origin with user agent).
Testing focus: open a log from each surface, deep-link to a logId, realtime list updates without page refresh.
3.10 Booking Access Credential Logs Flow
Entry: Physical Space detail → Attempt History section → click a row.
Credential attempt list is rendered by CredentialLogsList inside the Attempt History section.
Clicking a row navigates to /physical-spaces/credential-logs/:requestId.
Detail page shows status, type (LOCK/WIFI), timing metrics, and any error state for the credential generation attempt.
Testing focus: open detail from Attempt History, deep-link a requestId, status/type formatting via formatCredentialStatus / formatCredentialType.
3.11 Logout Flow
Entry: Any authenticated page (organization or protected).

Fig 72 — Log out from the sidebar footer (NavUser popover)
User opens the sidebar footer → NavUser (avatar, display name and email).
The popover repeats the account name and email and offers Log out.
On click: localStorage.removeItem('auth_token'), logoutUserService(), checkAuth(), navigate('/auth').
User lands on login page; protected routes are no longer accessible.
Testing focus: logout from organization page vs from a protected page; after logout, hitting /organization or /physical-spaces redirects to /auth.