UI Widgets
UI Widgets are custom Svelte components that can be added to UI Pages. Widgets can display data, provide controls, show charts, integrate external services, or present any custom visualization.
Overview
The UI Widgets page provides a complete development environment for creating custom dashboard widgets using Svelte 5. Widgets are compiled server-side and delivered to clients as standalone components.
Interface Layout
The page is split into sections:
Left Side - Widget List:
- Dropdown selector of all Svelte widgets
- Filter/search capabilities
- New widget button
- Delete widget button
- Recompile All button
Right Side - Widget Editor:
- Widget configuration form
- Code editor with syntax highlighting
- Live preview pane
- Error display
- Save/test buttons
Creating a Widget
To create a new widget:
- Click New Widget button
- Enter widget title when prompted (e.g., "Weather Dashboard")
- Widget is created with default template
- Code editor loads with starter code
- Customize the widget (see Coding Widgets section)
- Click Save to compile and save
Widget Configuration
Basic Settings
Widget Selector
- Select existing widget to edit
- Shows all Svelte widgets
- Switching widgets preserves unsaved changes
Name
- Internal identifier (lowercase_with_underscores)
- Auto-generated from title
- Cannot contain spaces or special characters
Title
- Display name shown in widget selector
- Appears in UI page configuration
- User-friendly text
Enabled
- Toggle to enable/disable widget
- Disabled widgets:
- Don't compile
- Cannot be added to pages
- Configuration preserved
Admin Only
- When on, the widget is hidden from the end-user UI Pages widget picker
- Used for admin dashboard tiles (Monitor Tag rollups, Active Zones, Climate Issues, etc.) that don't make sense on regular user-facing UIs
- Admin-only widgets are still addable to the admin home dashboard via its Customize Dashboard modal
- An ADMIN badge appears next to the Title field while editing an admin-only widget, and the widget selector dropdown appends
• ADMINafter the name so the scope is visible at a glance
Width
- Percentage of container width (0-100%)
- Default: 30%
- Determines widget size on page
Height
- Percentage of container height (0-100%)
- Default: 45%
- Determines widget vertical size
Code Editor
Full-featured Svelte 5 code editor:
Features
- Syntax Highlighting: JavaScript, HTML, CSS
- Line Numbers: Easy navigation
- Auto-Save: Preserves work automatically (draft saved to browser)
- Tab Support: Proper code indentation
- Find/Replace: Standard editor features
- Full Screen: Expand editor for focused coding
Widget Structure
Widgets follow standard Svelte single-file component format:
<script>
import {onMount} from 'svelte';
import GemApp from '../gem/app';
let data = [];
onMount(async function() {
// Initialize widget
loadData();
});
async function loadData() {
// Fetch data for widget
data = await GemApp.getInstance().query('zone', {subsystem_id: 1});
}
</script>
<style>
.widget-container {
padding: 15px;
height: 100%;
}
.widget-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 10px;
}
</style>
<div class="widget-container">
<div class="widget-title">My Widget</div>
{#each data as item}
<div>{item.label}: {item.power}</div>
{/each}
</div>
Available APIs in Widgets
Widgets have access to:
GemApp Instance:
const gem = GemApp.getInstance();
Common Methods:
// Query data
let zones = await gem.query('zone', {subsystem_id: 1});
let device = await gem.queryOne('device', {id: 5});
// Send commands
await gem.command({zone_id: 10, command: 'on'});
// Execute macros
await gem.macro(macroId);
// Get attributes
let attrs = await gem.getAttributes('zone', zoneId);
let value = await gem.getAttribute('device', deviceId, 'temperature');
// Set attributes
await gem.setAttribute('zone', zoneId, 'brightness', 75, 'int');
// Subscribe to updates
let token = await gem.subscribe('zone', zoneId, (data) => {
console.log('Zone updated:', data);
});
// Unsubscribe
await gem.unsubscribe('zone', zoneId, token);
// Notifications
gem.showMessage({message: 'Action complete', severity: 'success'});
Svelte 5 Features:
<script>
import {onMount} from 'svelte';
let count = $state(0); // Reactive state
let doubled = $derived(count * 2); // Derived value
function increment() {
count++;
}
</script>
<button onclick={increment}>
Count: {count} (Doubled: {doubled})
</button>
Live Preview
The preview pane shows the widget as it will appear:
Preview Features
- Live Rendering: Widget renders in real-time
- Theme Applied: Uses current system theme
- Actual Size: Shows configured width/height percentages
- Interactive: Widget functionality works in preview
- Error Display: Compilation/runtime errors shown below
Preview Controls
Collapse/Expand
- Toggle preview visibility
- Collapse for more editor space
- Expand to see changes
Size Preview
- Preview respects width/height settings
- Change width/height to see size impact
Compiling Widgets
Automatic Compilation
When you click Save:
- Widget configuration saved to database
- Widget code compiled to JavaScript
- Compilation output cached
- Preview updated automatically
- Success or error message shown
Compilation Process
Steps:
- Svelte compiler processes the code
- JavaScript generated
- CSS extracted and scoped
- Component registered with GEM
- Cache updated
- UIs notified of new widget version
Compilation Errors
If compilation fails:
Error Display:
- Red error box appears below preview
- Shows error message
- Shows line number and column
- Indicates problem area
Common Errors:
- Syntax errors (missing brackets, quotes)
- Invalid Svelte syntax
- Import errors
- Type errors
Fix Process:
- Review error message
- Locate problem line
- Fix the issue
- Click Save to recompile
- Error clears when successful
Runtime Errors
Errors that occur when widget runs:
Error Display:
- Shown in preview below widget
- Logged to browser console
- May include stack trace
Common Causes:
- API call failures
- Null reference errors
- Invalid data format
- Subscription errors
Debugging:
- Use
console.log()extensively - Check browser developer console
- Verify API calls return expected data
- Test with sample data first
Recompile All Widgets
The Recompile All button recompiles all enabled widgets:
When to Use:
- After Svelte Upgrade: Ensure compatibility with new Svelte version
- After System Update: Recompile for new GEM APIs
- Bulk Fix: Apply compilation improvements to all widgets
- Cache Clear: Force regeneration of all widget code
Process:
- Click Recompile All
- Confirm action
- System compiles all enabled widgets
- Success/failure message shows results
- Individual errors logged for failed widgets
Common Widget Types
Status Widgets
Display system or device status:
<script>
import {onMount} from 'svelte';
let devices = $state([]);
onMount(async () => {
devices = await GemApp.getInstance().query('device', {enabled: true});
});
</script>
<div class="status-widget">
<h3>Device Status</h3>
{#each devices as device}
<div class:online={device.connected} class:offline={!device.connected}>
{device.label}: {device.connected ? 'Online' : 'Offline'}
</div>
{/each}
</div>
Chart Widgets
Display data visualizations:
<script>
import {onMount} from 'svelte';
import Chart from 'chart.js/auto';
let canvas;
let history = $state([]);
onMount(async () => {
history = await GemApp.getInstance().query('attribute_history', {
name: 'temperature',
_limit: 100
});
new Chart(canvas, {
type: 'line',
data: {
labels: history.map(h => h.timestamp),
datasets: [{
label: 'Temperature',
data: history.map(h => h.value)
}]
}
});
});
</script>
<canvas bind:this={canvas}></canvas>
Control Widgets
Interactive controls:
<script>
let zones = $state([]);
async function toggleZone(zoneId) {
await GemApp.getInstance().command({
zone_id: zoneId,
command: 'toggle'
});
await loadZones(); // Refresh
}
</script>
<div class="controls">
{#each zones as zone}
<button onclick={() => toggleZone(zone.id)}>
{zone.label}: {zone.power}
</button>
{/each}
</div>
Data Widgets
Display API or sensor data:
<script>
import {onMount} from 'svelte';
let weather = $state(null);
onMount(async () => {
// Get weather from attribute or API
let w = await GemApp.getInstance().getAttribute('system', 0, 'weather_data');
weather = JSON.parse(w.weather_data || '{}');
});
</script>
{#if weather}
<div class="weather-widget">
<div class="temp">{weather.temp}°F</div>
<div class="conditions">{weather.conditions}</div>
</div>
{/if}
Camera Widgets
Display camera feeds:
<script>
export let cameraUrl = 'rtsp://camera.local/stream';
// Proxy camera stream through GEM
let streamUrl = `/api/camera/stream?url=${encodeURIComponent(cameraUrl)}`;
</script>
<div class="camera-widget">
<img src={streamUrl} alt="Camera Feed" />
</div>
Advanced Topics
Widget Configuration Schema
Widgets can accept configuration:
// In widget code
export let config = {};
// Access config values
let zoneId = config.zone_id;
let refreshRate = config.refresh_rate || 5000;
Configure when adding widget to page:
{
"zone_id": 10,
"refresh_rate": 3000,
"show_graph": true
}
Real-Time Updates
Subscribe to real-time zone/device updates:
<script>
import {onMount, onDestroy} from 'svelte';
let zone = $state(null);
let token;
onMount(async () => {
const zoneId = 10;
// Initial load
zone = await GemApp.getInstance().queryOne('zone', {id: zoneId});
// Subscribe to updates
token = await GemApp.getInstance().subscribe('zone', zoneId, (updatedZone) => {
zone = updatedZone; // Auto-updates UI
});
});
onDestroy(async () => {
if (token) {
await GemApp.getInstance().unsubscribe('zone', 10, token);
}
});
</script>
<div>
Zone: {zone?.label}
Power: {zone?.power}
</div>
Third-Party Libraries
Import and use external libraries:
<script>
import Chart from 'chart.js/auto';
import dayjs from 'dayjs';
// Other npm packages available in GEM
</script>
Available Libraries (already installed in GEM):
- chart.js
- dayjs
- lodash
- And many more (check package.json)
State Management
Complex widgets can use Svelte stores:
<script>
import {writable} from 'svelte/store';
const zones = writable([]);
async function loadZones() {
let data = await GemApp.getInstance().query('zone', {subsystem_id: 1});
zones.set(data);
}
onMount(loadZones);
</script>
{#each $zones as zone}
<div>{zone.label}</div>
{/each}
Widget Best Practices
-
Error Handling: Always handle API failures gracefully
-
Loading States: Show loading indicators
-
Null Checks: Verify data exists before accessing
-
Cleanup: Unsubscribe in onDestroy
-
Performance: Avoid excessive polling or subscriptions
-
Responsive: Design for multiple screen sizes
-
Theme Compatibility: Use theme CSS variables
-
Documentation: Comment complex logic
-
Testing: Test in preview before deploying
-
Version Control: Export widget code to external files
Troubleshooting
Widget Won't Compile
Check:
- Syntax Errors: Review error message for line number
- Missing Imports: Verify all imports are available
- Svelte Version: Ensure Svelte 5 syntax
- Quotes: Use consistent quote style
- Brackets: Ensure all brackets are closed
Widget Crashes on Load
Check:
- API Calls: Wrap in try/catch
- Null Values: Check for null before accessing properties
- Subscriptions: Ensure valid IDs
- Browser Console: Check for detailed error messages
Widget Shows Blank
Check:
- Data Loading: Verify API calls return data
- Conditional Rendering: Check {#if} conditions
- CSS: Verify styles don't hide content
- Size: Widget has reasonable width/height
- Console: Check for errors
Preview Not Updating
Try:
- Click Save to recompile
- Toggle preview collapse/expand
- Clear browser cache
- Check for compilation errors
Cannot Save Widget
Check:
- Name: Must be valid (lowercase_with_underscores)
- Compilation: Widget must compile successfully
- Permissions: User has permission to update widgets
- Network: Connection to GEM server