Skip to main content

UIs

UIs (User Interfaces) represent distinct dashboard instances that can be accessed via web browsers, tablets, or mobile devices. Each UI can have unique zones, controls, themes, and pages tailored to specific users or locations.

Overview

GEM supports multiple UIs, allowing you to create:

  • Room-specific control panels (kitchen, living room, bedroom)
  • Role-specific dashboards (homeowner, guest, installer)
  • Device-specific interfaces (wall-mounted tablets, mobile phones)
  • Location-specific controls (main house, guest house, pool area)
Licensing

The auto-generated site UI (a UI linked to a site space) and rendering of any already-built UI are always free. The Custom UI & Widgets module is required to author bespoke UI pages, widgets, themes, custom zone layouts, and the widget gallery. Trials and development builds unlock it, and existing UIs keep working if a license later changes. See License.

Viewing UIs

The main grid displays all configured UIs with the following columns:

  • ID - Unique UI identifier
  • Name - Internal UI name
  • Label - Display name
  • Group - UI grouping category
  • Site Space - Linked Site Space; when set, the UI is space-linked and its zones come from zone.site_space_id
  • Show Child Spaces - When Site Space is set, include zones from descendant spaces too (read-only when no space is linked)
  • Sort Index - Display order
  • Enabled - Whether the UI is active

System UI (ID 0) represents the default UI.

Grid Actions

  • Add - Create a new UI
  • Edit - Modify an existing UI
  • Delete - Remove a UI
  • Sort - Reorder UIs by dragging rows
  • Reload - Refresh the grid data

Quick Actions (Toolbox)

For each UI row:

  • Logout Icon - Force logout all sessions on this UI (prompts for confirmation)
  • Notification Icon - Test web push notifications for this UI
  • Copy Icon - Duplicate the UI under a new name. This is a deep copy: the UI row plus its attributes, zone assignments, controls, macros, page links, and zone-group links all come along. Pages themselves are shared, not copied — both UIs reference the same pages
  • View Icon - Open the UI in a new browser tab

Header Actions

  • Reload All - Force reload of all active UIs (prompts for confirmation, refreshes every connected client)

Toolbar Actions

The grid toolbar includes the standard DataGrid actions plus:

  • Format Labels (label icon) - Normalize the label column via formatLabel (treats any non-alphanumeric as a word break and capitalizes the first letter of each word). Short all-caps words of 2 letters or less are kept as acronyms (kitchen_TVKitchen TV, master_BRMaster BR), but longer all-caps words are Title Cased to tame shouting (KITCHENKitchen, MAIN_LOFTMain Loft). Mixed-case words preserve their inner casing. Scope follows the current view: with rows multi-selected only those rows are formatted, otherwise every row matching the current filters (across all pages). Shows a confirmation with the count of labels that would change, skips ones already in canonical form, and the change is undoable. Available on any DataGrid backed by a model with a label column.
  • Undo (undo icon, also Ctrl+Z) - Reverts the most recent grid edit. Covers single-cell edits, bulk fan-outs across a multi-cell selection, drag-reorder sort_index changes, and Format Labels runs. Stack is bounded at 20 steps and lasts only as long as the grid is mounted.

Creating a UI

To create a new UI:

  1. Click Add in the grid toolbar
  2. The UI editor opens with two tabs: UIs and Attributes
  3. Configure the UI properties (see sections below)
  4. Click Save

UI Editor

UIs Tab

Configure the basic UI properties:

Basic Information:

  • Name - Internal identifier (lowercase_with_underscores)
  • Label - Display name shown in UI selector
  • Group - Optional grouping category
  • Enabled - Toggle UI active state
  • Sort Index - Display order in UI selectors

Examples:

Name: kitchen_panel
Label: Kitchen Panel
Group: main_floor
Enabled: Yes

Settings Tabs — Shared Defaults vs Per-UI Overrides

The Display, Pages & Navigation, Intercom & Voice, Screen & Gallery, Light Scenes, and Weather tabs edit UI settings (attributes). Settings are layered: values stored on ui 0 are shared defaults for every UI, and a row on a specific UI overrides the shared default for that UI only.

Shared defaults are edited in one place — the Shared Settings button above the UIs list opens the shared-defaults editor (only the settings tabs; no name/label/space fields). Saving there updates every UI that doesn't override the changed values.

Individual UI editors only store differences. The form shows the merged result, and saving writes just the fields that differ from the shared defaults:

  • Change a field → save stores a per-UI override; the field gains a blue override badge.
  • Set a field back to the shared value (or clear it) → save removes the override so the UI follows the shared default again.
  • Fields matching the shared defaults are never written — opening and saving a UI (or creating a new one) does not copy shared defaults into per-UI rows.
  • Click the on an override badge to revert that field to the shared value immediately.

Toasts list exactly which overrides a save created and which fields reverted to the shared defaults.

A third layer sits above both. A handful of cosmetic settings — wallpaper, theme, homepage, weather location — can also be overridden per user, and that override wins over the per-UI value for whoever is signed in. See Personal Settings; a site with no per-user overrides resolves exactly as described above.

Override Cleanup — sites that accumulated per-UI copies of shared values under the old save behavior will see an Override Cleanup section in the shared-defaults editor listing redundant overrides (per-UI rows identical to the shared default). Removing them changes nothing immediately, but lets those UIs follow future changes to the shared value. Only settings managed by this form are considered — custom UI attributes are never touched.

Light Scenes Tab

Controls how the Lighting control page (/controls/lighting) presents scenes:

  • Disable scene editing — hide scene create/edit controls; users can still recall existing scenes.
  • Disable light scenes — hide scenes entirely. The lights page always opens on the Groups tab and the Default Lights View setting is ignored.
  • Default Lights View — which tab the lights page opens on when scenes are enabled: Scenes (default) or Groups. Choose Groups to land on per-zone controls first even when scenes exist; when scenes are disabled this is ignored.
  • Auto-Scene Labels — display names for the built-in auto-scenes: Soft, Bright, and Off.

Weather Tab

  • Weather Locations — named locations available to weather widgets and pages, stored as latitude,longitude. Drag the handle to reorder rows — the order is how cities appear on the weather page. Star a location to make it the default shown first (clicking the active star clears it; with no star, the first location is used). Each row has a Map button that opens a map picker (search by address or zip, or click to drop a pin); add a new location by typing a name and clicking Pick on Map. Coordinates can also be edited inline. Weather data comes from the free Open-Meteo forecast API — no API key or service URL is needed.
  • Radar (per location row) — opens the radar-view picker: a live radar.weather.gov preview centered on the location. Adjust the zoom slider (and coordinates if needed) to frame exactly the area that matters, or open the site in a new tab, position the map precisely, and paste the address-bar URL back in. Saving stores a radar.weather.gov map URL for that location; the weather page then embeds the live radar map zoomed to the saved view instead of a wide station image.
  • Weather Radar Views — the raw location → radar value map. Values are radar.weather.gov map URLs (set by the Radar button) or legacy NWS station codes like FWS, which still display via the station's loop image.

Attributes Tab

UI-specific attributes control behavior and appearance:

Common UI Attributes:

Theme:

  • theme - Theme name (set via UI Themes page)
  • Controls all visual styling

Gallery:

  • gallery_photo_path - Path to gallery images folder
  • Used for screensaver/photo frame mode
  • Example: /mnt/photos/family

Screensaver:

  • screen_timeout - Idle time before screensaver (seconds)
  • screen_timeout_action - What to do on timeout:
    • not_set - No action
    • gallery - Show photo gallery/screensaver
    • logout - Log out user
    • goto_page - Navigate to specific page

Navigation:

  • default_page - Page to show on login
  • home_button_action - What home button does
  • hide_back_button - Hide the in-app back button. By default every layout shows one whenever there is in-app history to return to. In the classic top bar (Prodigy, Carbon, and any other top layout) the back button shares its slot with the climate shortcut: the pill shows the indoor or outdoor temperature while there is nowhere to go back to, and swaps to a back arrow once the user drills into a page. In rail/tab-bar layouts (e.g. the Ember theme) it appears next to the menu button in the page header. Either way it walks back through visited pages and hands the slot back (or hides itself) when the history is empty. History is per-session, skips transient pages (login, PIN prompts, loading), and clears when roaming to another room's UI. In portrait, pane switches inside split (bifold) pages also enter history — drilling from one pane to the other (e.g. picking a scene on the Lights page) can be undone with back before it walks to the previous page; a pane's history is dropped when you leave its page. Set it from the Hide Back Button switch in the UI editor's Display Settings, or on the shared defaults (UI 0) to hide it on every UI
  • nav_layout - Navigation chrome layout for this UI: top (classic top bar) or rail (left icon rail with page header, landscape only). Overrides the theme's nav_layout; leave empty to inherit from the theme (see the theme contract)
  • control_order - User-saved order for the controls grid and nav rail / tab bar shortcuts, written by the press-and-hold reorder gesture on the controls page (see UI Controls). Delete it to restore the integrator-defined sort-index ordering
  • lock_control_order - Prevent users from reordering controls by press-and-hold on this UI (Lock Control Order switch in the UI editor). Set it on the shared defaults (UI 0) to lock every UI; a per-UI value overrides. Enforced server-side, not just hidden in the client

Layout:

  • zones_per_row - Number of zones per row in grid layouts
  • compact_mode - Reduced spacing for smaller screens
  • show_labels - Show/hide zone labels

Sorting:

  • ui_group_sort - How zone groups are sorted in control pages. Set to alphabetical to sort groups by name instead of the default sort index order. The aggregate All group is not a peer of the others — it is a view over all of them — so it stays pinned first under either setting.

Notifications:

  • web_push_enabled - Enable browser push notifications
  • notification_sound - Alert sound file

Camera Streaming:

  • rtsp_release_ms - Grace window in milliseconds before the client RTSP socket pool tears down an idle camera connection. Default 5000. Higher values keep streams warm across UI transitions (e.g. opening/closing a zone modal) at the cost of holding sockets open after navigation; lower values release server-side ffmpeg decoders sooner but may re-handshake on quick re-opens.

Lutron Keypad Widget Binding:

  • lutron_control_station_id - Station id consumed by the Lutron Keypad widget whenever no keypad is picked in the widget's own settings (or its Use UI attribute option is on, which forces this attribute even when one is picked). Lets one widget definition follow whichever UI it's rendered under instead of hard-coding a station per UI page. Populated automatically by the lutron_qsx device's sync_areas command for any UI whose site_space_id matches a Lutron area that has control stations. When edited manually, the attribute editor shows a station picker (with area name) sourced live from the lutron_qsx driver instead of a free-text id field.
  • lutron_control_station_2_id, lutron_control_station_3_id, ... - Additional stations from the same area, written when an area has more than one keypad. Stations are sorted by name (then station id) so re-syncs land the same physical keypad in the same slot.
  • The Lights control page (/controls/lights) also reads these attributes: when any are present (and the lutron_qsx device still exists), a Keypad segment joins Scenes/Groups in the page's left-pane switcher and becomes the default view, rendering every station in slot order.
Live keypad state without a full sync

On every connect, the lutron_qsx driver walks the processor's areas and subscribes to every LED (/led/{href}/status) and every keypad device (/device/{id}/button/status/event — one subscribe per device covers all of its buttons via LEAP's MultipleButtonStatusEvent). LED and button state goes live as soon as the device connects, without running sync_areas or syncZones first, and new keypads are picked up automatically after a project republish. The led_subscriptions (LED hrefs) and button_subscriptions (keypad device ids) attributes are persisted so the next reconnect's fast path can restore subscribes without re-discovering, and remain available as optional override lists for installs that need explicit subscribes.

AI Chat (Ask GEM):

  • hide_ai_chat - Hide the Ask GEM button from this UI's top nav. Default off (button shown).
  • The button only appears when an Anthropic API key is configured server-side (ai.anthropic_api_key in gem.json or the anthropic_api_key system attribute). Tapping it opens an in-page chat that lets users control the home in plain English (lights, climate, shades, AV, scenes), with optional spoken responses on devices that support the Web Speech API.
What Ask GEM is allowed to reach

Voice requests carry the signed-in user's authority, not the assistant's. Every command it sends is checked twice before it reaches a device:

  • Elevated devices and macros are refused for a non-elevated user, exactly as the same request would be from a button or the REST API. A garage or gate backed by a device flagged Elevated cannot be opened by voice from an account that couldn't open it by tapping.
  • The panel's own scope is enforced — zones and the devices they reach. A phrase that resolves to a zone this UI does not include, or to a device outside the UI's reach (including an AV source dedicated to another room's AV zone), is refused rather than dispatched — even when the same user could reach that target from another UI. A spoken command is contextual to the panel it was spoken at, so a guest tablet can't be talked into another part of the building. UIs with no zone scope resolved at all (no UI Zones and no Site Space) are unaffected.
  • Changing the site mode needs an elevated role. "We're home" runs the mode's whole transition action set — on a typical configuration that is disarm plus entry-door unlock — so it is held to the same requirement as the set_site_mode function itself, refused with an explanation rather than silently claiming the house changed. Asking what the mode is, or which modes exist, stays open to every role.

See Elevated devices and macros.

Reading outside content ends the turn's ability to actuate

Some things Ask GEM can read carry text nobody at the site wrote — a calendar invite title, a track or app name off a streaming service, a hostname that joined the network, a device's own error string, or a camera scene described in words (a sign held up to a lens is text arriving through a sensor). Once any of those has been read while answering a request, the assistant refuses to operate anything physical for the rest of that request and says so.

The refusal covers doors and gates, raw device commands, running a macro, whole-house sweeps, site mode, the water shutoff, and the deferred forms of all of them (a scheduled unlock is still an unlock). Comfort controls — lights, shades, volume, climate, colour — are deliberately not covered, and neither is "lock up", which only ever locks and arms.

The cost is a genuine two-part request ("what's on my calendar, and set the house to away"), which is answered and then declined with a prompt to ask directly. Asking again on its own works normally, because each request starts clean. Refusals are recorded in the server log.

Room-name resolution

When a user asks for something like "turn off the kitchen lights" or "dim the master bedroom to 30%", Ask GEM resolves the room phrase by matching it against the name, label, and group of every UI first. If a UI matches and is space-linked, its Site Space (plus descendants when include_child_spaces is on) defines the scope; if it's a manual UI, the scope is its UI Zone list. If no UI matches, the assistant falls back to a substring match on zone label/name. Naming a UI after the room it controls (kitchen, master_bedroom, downstairs) gives the assistant a much sharper sense of "where" — without it, ambiguous phrases like "downstairs" depend on every relevant zone happening to contain that word.

Delayed commands and cancellation

Ask GEM understands deferred actions ("turn off the patio lights in 30 minutes", "in an hour set the kitchen lights to 20%", "lock the front door in 5 minutes") and queues them in-memory, capped at 8 hours per timer and 20 zones per fan-out. Users can also cancel pending timers ("cancel that timer", "never mind", "cancel the patio timer", "cancel all my timers") or list what's pending ("what timers do I have set?"). Timers are sandboxed per user — one user's pending timers can't be seen or cancelled by another. Pending timers do not survive a server restart; for recurring/persistent rules use Macro Schedules instead.

"When it gets above 78, turn the fan on"

Ask GEM also arms one-shot condition rules — the "when X happens, do Y" phrasing that no timer covers ("turn the fan on when it gets above 78", "when the garage opens turn on the driveway lights", "turn the heater off once the bedroom hits 70", "when the basement humidity goes over 60 turn on the dehumidifier"). It watches one zone attribute (state by default, or temperature / humidity / level / occupied for sensors), and the first time the condition holds it dispatches the action — a single zone, or a lights/shades/fans/power fan-out capped at 20 zones — then disarms itself.

Three behaviours are worth knowing:

  • If the condition is already true, it acts immediately and says so ("It's already 80 in there, so I turned the fan on") rather than silently waiting for a change that already happened.
  • It fires once. An unfired rule also expires on its own after 8 hours by default (24 hours maximum), so a condition that never occurs doesn't linger.
  • Cancellation is shared with timers. "Cancel that", "what am I waiting on?" and "cancel all my timers" list and clear armed rules alongside pending timers, and the listing distinguishes the two.

Rules live in memory and are sandboxed per user, with the same restart caveat as timers. A standing rule the homeowner wants permanently ("every time the garage opens…") is a real Attribute Trigger and integrator configuration — the assistant says so instead of arming a one-shot.

"Undo that" / "put it back"

Ask GEM can put a light, shade, fan, or outlet back to the value it held before its most recent change ("undo that", "put it back", "never mind, put the lights back", "restore the living room lights"). It reads attribute history for level and state, takes the value recorded immediately before the last change, and dispatches the matching command — so the user never has to remember what the old setting was. The default lookback is 15 minutes (max 4 hours for phrasings like "put it back to how it was this morning"); anything older is reported as nothing to undo. A bare "undo that" with no room named undoes only the single most-recently-changed zone, not the house; naming a room fans out to at most 6 zones.

This is hard-scoped to lights, shades, fans, and power. Doors, locks, security, climate, and AV are refused by design so an ambiguous "undo" can never unlock a door, disarm an alarm, or swing a thermostat — the user is asked to state the action instead. Requires attribute history to be enabled for the zones involved.

"Turn everything off"

Ask GEM handles the cross-category shut-down in one sweep ("turn everything off", "shut down the theater", "we're leaving, turn it all off", "kill everything upstairs", "all off"). Naming a room scopes it the same way room resolution works above; naming nothing means the whole house.

By default it covers lights, fans, and AV — AV zones have their source cleared, which single-subsystem commands can't do at all. Outlets are opt-in ("everything", "all the outlets") because a plug may be feeding a fridge or sump pump, and shades are only closed when the user asks for that as part of it. Zones already off are skipped, so the spoken reply reports what actually changed. Capped at 30 zones; a casual whole-house request is previewed for confirmation first.

Climate, security, doors, locks, gates, cameras, irrigation, and pool equipment are never touched, whatever phrasing is used — the user is routed to the specific request instead. As with the other sweeps, a curated goodnight / leaving / all_off macro on the UI is preferred over the generic version, since it's what you programmed and may cover more.

"Lock up" / "secure the house"

Ask GEM can lock every accessible door and arm the alarm in one breath ("lock up", "secure the house", "I'm leaving — lock up", "going to bed, lock up"). Defaults to stay mode for "lock up" / "going to bed" phrasings; use "I'm leaving" / "arm the alarm" for away mode, or "just lock the doors" to skip arming. The sweep is hard-scoped to door/lock/gate and security zones the user can already see on this UI, capped at 30 of each, and skips zones already locked or armed (so re-asking is a no-op). Garage zones are included by default — say "just the interior locks" to exclude them. Unlocking and disarming are intentionally not voice-controllable — for safety, the assistant will direct the user to the touchscreen instead. If you have a curated goodnight / lock_up / leave_home macro on the UI, the assistant prefers running that over the generic sweep so any associated lights/scenes fire too.

"Does the cleaner still have a code?"

Ask GEM answers questions about the access configuration — who currently holds a valid door credential, whose is about to lapse, and whose already has ("who has access to the house?", "who can get in?", "is the guest code still active?", "when does the cleaner's access expire?", "does anyone have a temporary code?", "whose code expires this week?", "can the neighbor still get in?"). Naming a person makes the answer a direct yes/no about them; naming a door ("who can open the gate?") scopes it to that reader's rules. This is distinct from "who came in?" / "did the cleaner come today?", which read the access log — entries that already happened.

The reply reports names, status, doors, and expiry windows only — it never contains a PIN, card number, or any other credential value; the underlying query doesn't even read those columns. It is also strictly read-only: asked to add, extend, or revoke someone's access, the assistant directs the user to the admin app (Users, Access Control) instead of attempting the change.

"Open the garage" / "close the gate"

Ask GEM operates garage doors, gates, and lifts directly ("open the garage", "close the garage door", "shut the garage", "open the driveway gate", "hit the garage button", "stop the garage door", "close both garage doors"). Commands go through the normal zone command path, so relay state mapping and any per-zone <verb>_action override apply; openers that expose only a single momentary button are retried with toggle when they reject a discrete open/close.

What can be opened is decided by the zone's subsystem, never by its label. Zones on garages, gates and lifts are openings; zones on doors and locks are latches, and on access-control drivers open means unlock — so a deadbolt called "Garage Entry Door" is not openable by voice no matter how it is named. If a zone genuinely is an overhead door or gate but has to live on doors, set its door_family attribute to garage, gate or lift; the refusal names that attribute so the fix is self-serve. See Subsystems.

Opening is never guessed. If more than one opening could match and the user named none, the assistant asks which one instead of picking — so "open the garage" in a three-bay garage becomes a question, not a gamble. A house with a single garage needs no qualifier. Closing, stopping, and locking travel in the safe direction and may sweep a scope (capped at 10 zones); zones already open/closed/locked are skipped so the spoken reply reports what actually moved.

Unlocking is not available at all — there is no unlock verb, and open/toggle are refused on deadbolt and latch zones. As with "lock up" above, the user is directed to the touchscreen or keypad. For a deferred version, users can say "close the garage in 10 minutes" (a delayed command) or "open the gate for 5 minutes" (auto-reverses).

"Shut the water off"

Ask GEM operates the whole-home shutoff valve and reads its position ("shut off the water", "kill the water", "close the water valve", "turn the water back on", "is the water shut off?", "is the water valve open?"). Scoped to water and plumbing valve zones the user can already see on this UI — irrigation and sprinkler valves are excluded by design, since those are the sprinkler request instead.

Shutting off travels in the safe direction and may sweep every accessible valve (capped at 5); a valve that rejects close is retried once with off, since some drivers expose only on/off. Re-opening does not. It needs an unambiguous target — the user names the valve, or the house has exactly one — and it is refused outright while any leak sensor is still wet, with the assistant naming the sensor and asking before it will try again. Both directions are idempotent, so "shut the water off" against a closed valve answers "The water was already shut off" rather than re-sending.

Asking about the water reports valve position, whether water is actually flowing, and any active leak signal in one sentence. Consumption questions ("how much water have we used today?", "what's the water pressure?") are answered from the meter instead, and a general "any alerts?" still sweeps every safety sensor. When no valve is configured the assistant says so plainly rather than claiming it shut anything off. For a timed version, "shut the water off for an hour" becomes a delayed command.

See UI Themes and Gallery for theme and gallery configuration.

Accessing UIs

Direct URL Access

Access a specific UI by URL parameter:

https://gem.local/?ui_id=1
https://gem.local/?ui_id=5

Or by UI name:

https://gem.local/?ui=kitchen_panel
https://gem.local/?ui=master_bedroom

UI Selector

When multiple UIs exist, a UI selector appears allowing users to switch between UIs. The selector is a single tile grid that drills down into Site Spaces, UI groups, or a flat All view, with a breadcrumb back button at the top of any sub-view.

Root tiles mix three sources, sorted together by sort_index — entries without one sort last, ties break alphabetically (set ui_sort: alphabetical in app settings to sort by label only). Every drilled sub-view sorts by the same rules:

  • Top-level site spaces — each space with a linked UI lands you in it on tap, and spaces with child spaces show a suffix so you can drill in. Spaces with both a UI and descendants act as both — tap navigates, hold-to-set-default still works.
  • UI groups — every distinct group value from non-space-linked UIs becomes a drill-down tile (suffixed with ). Group order honors settings.ui_groups if set, otherwise falls back to the smallest member sort_index.
  • Ungrouped manual UIs — UIs with no site_space_id and no group show as direct tiles at root.
  • All — a flat list of every UI, always pinned at the top of root when any UIs exist.

A segmented control, centered in the picker header, offers the same jumps without drilling: All first, then one segment per top-level space with descendants and per UI group. Segments follow exactly the tile grid's order — the same comparator sorts both, so the bar and the grid never disagree. Tapping a segment swaps the grid to that scope; the bar shows only at root, hiding while a search is active or a sub-view is open.

Drill-down pushes onto a stack so the back button (‹ Home / ‹ Parent Name) pops one level at a time. Searching collapses the view to a flat filtered list across every UI regardless of current scope; clearing the search restores the drilled scope.

Hold any UI tile for 2 seconds to set it as the default UI for that device (same behavior as before — stored against gem_client_id + user agent).

The selector toolbar also exposes per-device actions that don't require admin access:

  • My Settings — opens the user's own Personal Settings page (wallpaper, theme, homepage, weather location). Only shown when a real account is signed in; a shared panel on a permanent session has no personal layer, so the entry is hidden
  • Clean Screen — temporarily disables touch input so the screen can be wiped down
  • Request Notifications — prompts the browser for web push permission
  • Show / Hide Media Nav — toggles the media navigation bar
  • Zoom — opens a modal that scales the entire UI between 50% and 150% via document.documentElement.style.zoom. The chosen value persists in localStorage (gem_ui_zoom) on that device only and is re-applied each time a UI loads. Useful on smaller iPhones where standard controls get cut off; reset to 100% to clear the override.
  • Mobile App — opens the QR code modal described in Mobile App: App Store and APK downloads plus an External and an Internal setup code, so a device can be pointed at the cloud URL or the local network URL
  • Change GEM Url — on iPad / Android / Capacitor shells and the native mobile app, lets the user point the app at a different server. In the native app this clears the stored server URL and returns to the first-launch setup screen
  • Device Settings — legacy Android shell only; opens the host app's native settings screen

Default UI

UI with ID 0 is the system default. Users without a specific UI assignment see this UI.

UI Components

UIs are composed of:

  1. UI Controls - Navigation buttons/pages (see UI Controls)
  2. UI Zones - Zones assigned to this UI (see UI Zones)
  3. UI Macros - Macros accessible from this UI (see UI Macros)
  4. UI Pages - Custom pages with widgets (see UI Pages)
  5. Theme - Visual styling (see UI Themes)

Space-Linked vs Manual UIs

Every UI is either space-linked or manually composed. The distinction determines where its zones come from.

Default to space-linked

In a well-organized install, most UIs can be built simply by assigning zones to Site Spaces — the space tree drives everything automatically. Reach for manual ui_zone composition only when the UI doesn't map to a physical space (building-wide dashboards, security panels, power-user consoles).

Space-Linked UIs (site_space_id is set)

The UI is bound to a Site Space via site_space_id. Zones are pulled live from zone.site_space_idno ui_zone rows are needed. Moving a zone into or out of the space makes it appear or disappear in the UI automatically.

  • include_child_spaces: false — show only zones in that exact space (default for rooms, kitchens, etc.)
  • include_child_spaces: true — include descendants as well, with each sub-space becoming a zone group (default for containers: building, floor, wing). The control pages also prepend an All group that aggregates zones across every child space.
Overlays

Space-linked UIs can still take explicit UI Zone and UI Control rows — they're layered as overlays on top of the inherited set. Use them to add a zone or control that isn't in the space (e.g. a shared front-gate camera on every room panel, or a room's AV zones when they aren't space-tagged), or to suppress an inherited control by adding a ui_control row with the same component and enabled = false.

Overlay zones are honored everywhere the UI's zone set is resolved — the individual-control views, the AV zone list and media bar (including the default AV zone), the floor-plan map, and the command scope gate — so an overlay row behaves exactly like a zone the space contributed.

Create a space-linked UI from the Site Spaces page with Create UI, or set site_space_id directly when creating a UI.

The side menu for a space-linked UI is auto-generated from the space's zones:

  • One control button per subsystem with zones in scope (lights, shades, climate, security, …).
  • AV is split: an Audio button appears only if at least one in-scope AV zone has av_type = audio, and a Video button only if at least one has av_type = video. Spaces with no AV zones get neither.
  • A Spaces button is appended whenever the space has child spaces, providing drill-down navigation to each child space's UI. Children without their own UI but with descendants that do are shown as drill-down tiles (label suffixed with ) — tapping descends one level deeper, and a breadcrumb with a back button lets you pop back out.

Space-linking is also what gives a UI floor plan maps: whenever the linked space (or an ancestor of it) has a plan image, every subsystem page offers a Map view with a pin per zone, derived from the space geometry. Manual UIs have no physical layout to draw, so the Map affordance never appears on them. See Site Spaces → End-user floor plan maps.

Manual UIs (site_space_id is null)

Zones come from explicit UI Zone rows and controls from UI Control rows. Nothing happens automatically. Use for:

  • Building-wide dashboards ("All Lights", "Security Panel", "HVAC Overview")
  • Power-user management consoles that mix zones from anywhere
  • Guest or role-restricted UIs with hand-picked zones

Common Workflows

Creating a Room Panel

For a kitchen wall tablet:

  1. Click Add
  2. Configure:
    Name: kitchen_panel
    Label: Kitchen Panel
    Group: main_floor
    Enabled: Yes
  3. Save and note the UI ID
  4. Navigate to UI > UI Zones and assign kitchen zones
  5. Navigate to UI > UI Controls and add relevant controls
  6. Navigate to UI > UI Themes and assign a theme
  7. Access via: https://gem.local/?ui=kitchen_panel

Creating a Mobile UI

For homeowner mobile app:

  1. Click Add
  2. Configure:
    Name: mobile_homeowner
    Label: Mobile - Homeowner
    Group: mobile
    Enabled: Yes
  3. Save
  4. Assign all necessary zones (all subsystems)
  5. Add all controls
  6. Set compact theme optimized for mobile
  7. In Attributes, set:
    compact_mode: true
    zones_per_row: 2

Creating a Guest UI

For limited guest access:

  1. Click Add
  2. Configure:
    Name: guest_panel
    Label: Guest Panel
    Enabled: Yes
  3. Save
  4. Assign only guest-accessible zones:
    • Guest bedroom lights
    • Guest bathroom lights
    • Common area lights
    • Temperature control
  5. Add limited controls (no admin, no advanced features)
  6. Create guest user account
  7. Restrict guest role to only this UI

Duplicating a UI

To create a similar UI:

  1. Edit existing UI
  2. Note all settings
  3. Create new UI
  4. Manually configure with similar settings
  5. Or export/import UI configuration (advanced)

Testing UIs

Open UI

Click the View icon to open the UI in a new browser tab:

  • Tests UI functionality
  • Verifies zone assignments
  • Checks theme application
  • Validates controls

Test Notifications

Click the Notification icon to test web push:

  • Sends test notification to connected clients
  • Verifies notification configuration
  • Tests notification delivery

Force Logout

Click the Logout icon to disconnect all clients:

  • Useful for testing login behavior
  • Forces UI reload
  • Clears client cache
  • All users connected to this UI will be logged out

UI Groups

Group UIs by category for organization:

Common Groups:

  • main_floor - Main floor panels
  • second_floor - Second floor panels
  • mobile - Mobile apps
  • guest - Guest interfaces
  • service - Service/installer interfaces

Groups appear in UI selectors and help organize large installations. Removing a group prompts for confirmation — UIs assigned to that group will become ungrouped.

Multiple UIs vs. Single UI

When to Use Multiple UIs

Good Reasons:

  • Different physical locations (room panels)
  • Different user types (owner vs. guest)
  • Different devices (tablet vs. phone)
  • Different themes per location
  • Site-specific configurations (multi-tenant)

Example:

UI 1: Kitchen Panel (kitchen zones only, bright theme)
UI 2: Bedroom Panel (bedroom zones only, dark theme)
UI 3: Mobile App (all zones, mobile-optimized theme)
UI 4: Guest Panel (limited zones, simplified interface)

When to Use Single UI

Good Reasons:

  • Simple installations
  • Consistent experience across devices
  • Centralized management
  • Minimal configuration

Use Roles Instead:

  • Different permissions for different users
  • Role-based access control handles restrictions
  • Single UI, multiple user roles

Security and Access

Role-Based UI Access

Control which users can access which UIs using Roles:

  1. Navigate to Security > Roles
  2. Edit or create a role
  3. In User Interfaces permissions:
    • Allow: Specific UI IDs
    • Deny: Restricted UI IDs
  4. Assign role to users

Example:

Guest Role:
Allow UI: guest_panel
Deny UI: all others

Result: Guests can only access guest_panel

Site-Based UI Access

For multi-site installations:

  1. Create UIs per site
  2. Assign UIs to appropriate sites (via attributes)
  3. Restrict users to specific sites

Advanced Topics

UI Reloading

Reload All Button:

  • Broadcasts reload command to all connected clients
  • Forces UI refresh
  • Useful after:
    • Theme changes
    • Major configuration updates
    • Zone additions
    • Control modifications

Individual UI Logout:

  • Disconnects clients from specific UI
  • Triggers automatic reload on next access
  • Less disruptive than Reload All

Web Push Notifications

Enable browser push notifications:

  1. Edit UI
  2. In Attributes, set web_push_enabled: true
  3. Clients will be prompted to allow notifications
  4. Test with notification icon in grid

Requirements:

  • HTTPS connection (SSL enabled)
  • Browser support (Chrome, Firefox, Safari 16+)
  • User permission granted

Dynamic UI Loading

UIs can load different configurations based on:

  • Query parameters
  • User roles
  • Time of day
  • System mode (home, away, vacation)

Configure via UI attributes and custom logic.

Embedded UIs

Embed GEM UIs in other applications:

<iframe src="https://gem.local/?ui=kitchen_panel&embed=true" />

Embed parameter removes navigation chrome for cleaner integration.

Same-origin framing only

GEM sends X-Frame-Options: SAMEORIGIN, so a UI can only be framed by a page served from the same origin as the controller. A dashboard hosted on another hostname gets a blank frame, and the browser console reports the refusal.

Serve the embedding page from the controller (or from behind the same reverse-proxy hostname), or link out to the UI in a new tab instead. The real-time socket applies the matching rule to its own connections — see Real-time connection origins if a framed UI loads but never updates.

Troubleshooting

UI Not Showing Zones

Check:

  1. Zones assigned to UI in UI > UI Zones
  2. Zone groups configured correctly
  3. Subsystem enabled
  4. Zones enabled
  5. Device online

UI Shows Wrong Theme

Check:

  1. Theme attribute set for UI
  2. A personal theme override on the signed-in account — a personal theme wins over the UI's, and takes effect once the session comes up, so the screen re-themes shortly after login
  3. Theme files exist in /themes/{theme_name}/
  4. Browser cache cleared
  5. CSS file is valid

Cannot Access UI

Check:

  1. UI enabled
  2. User has role permission for UI
  3. Correct URL or UI parameter
  4. Network connectivity
  5. GEM server running

UI Not Reloading

Try:

  1. Force browser refresh (Ctrl+F5)
  2. Use Logout icon to disconnect
  3. Clear browser cache
  4. Use Reload All button

Intercom Has No Microphone

An intercom call connects and the panel plays incoming audio, but nothing is heard at the far end, and the console logs sip: no media devices — insecure origin, microphone unavailable.

Browsers only expose a microphone in a secure context — HTTPS, localhost, or the mobile app. A panel opened over plain HTTP by hostname or IP (http://192.168.1.50/?ui=kitchen_panel) has no microphone available at all, regardless of any permission prompt.

Fix: enable SSL on the controller and open the panel over HTTPS. See SSL / TLS — if the certificate comes from GEM's own CA, install the CA on the panel first so the browser trusts it. The rest of the intercom (ringing, incoming audio, hanging up) works on an insecure origin; only outbound audio is affected.

Offline Behavior

The GEM web UI is cached on each device by a service worker, so panels and phones keep a working interface when the network or the controller is briefly unreachable:

  • On first load (and again after every GEM update) the full UI shell — the application bundle, stylesheets, fonts, and icons — is downloaded into the browser's cache automatically. No page browsing is required to "warm" it.
  • A device that reboots while the controller is offline still loads the UI from its cache, on any page, and reconnects automatically once the controller is reachable again.
  • Live control requires a connection to the controller. Offline, the UI opens and shows its disconnected state; commands and live values resume when the connection returns.
  • Theme styling and images are cached as they are used, so a panel shows its normal theme offline after it has displayed it online at least once.
  • The cache is replaced as a unit when a new GEM version is installed; devices must be online briefly after an update to pick up the new interface.

If a device instead shows "offline and not cached", its cache predates this behavior (or was cleared) — load the UI once while online to restore it.

Reconnect and Resync

Live updates are not replayed: a change broadcast while a panel's connection was down is gone, so reconnecting alone would leave the panel showing pre-outage state (a stale source in the media nav, a wrong volume) until the next change happened to arrive. Three mechanisms close that gap:

  • State resync on reconnect — once the connection is re-established, the panel re-reads the current values it displays live (source, volume, mute and related room AV state) and applies them. A source change made from another panel while this one was offline shows up moments after the connection returns.
  • Source page reload on reconnect — a source view that was open (or was opened) while the connection was down loads nothing: its one-time reads all fail at once, leaving the page empty rather than merely stale — no Roku app grid, no Spotify or Kaleidescape browse list, no DirecTV widget, the generic "Airplay" name instead of the source's own, and for an Amazon Music, Pandora or SiriusXM source not even a panel, since which one to draw is decided by a lookup that failed. Those reads are re-run automatically when the connection returns, so the page fills itself in instead of having to be navigated away from and back.
  • Dead-connection detection — a connection can die without the panel noticing (a Wi-Fi roam, a NAT timeout): the socket still looks connected but nothing arrives. Panels probe the server every 15 seconds; after two consecutive unanswered probes (about 30 seconds) the panel drops and re-establishes the connection itself, which triggers the same resync. This matters most for wall panels, whose screens never sleep and so never hit the resync that runs when a display wakes.

A network blip never signs a panel out — a panel logs out only when the server explicitly reports its session is no longer authenticated.