Pages & Sections
Backend-declared pages are extra tabs rendered automatically by the app. The backend describes structure (which entities/properties, grouping, grid columns, density) and semantic intent — never renderer details like pixels or icon-library names.
Purpose
Let the backend organize the UI into named pages without writing frontend code. Each page becomes a bottom tab.
Signature
Page(
id: str, # unique id, becomes /page/<id>
title: str, # tab label
icon: str | None, # SEMANTIC icon token (e.g. "chart", "gauge")
columns: int = 2, # default grid columns (1-4)
density: str = "normal", # "compact" | "normal" | "loose"
entities: list | None, # simple mode: one section per entity
sections: list | None, # advanced mode: explicit sections
)
Section(
title: str,
entities: list, # entity IDs
properties: list | None, # None = all properties
columns: int | None, # override page columns
density: str | None, # override page density
)Semantic tokens (renderer-agnostic)
- density → spacing:
compact(tight),normal,loose(airy). The frontend maps these to px; the backend never sends pixels. - icon → semantic intent:
grid,list,settings,chart,gauge,temperature,water,flame,pulse,stats,document. The frontend maps these to its icon set (Ionicons today, could be anything later).
Minimal example
from odevice import App, Device, Page
app = App("Demo")
app.add(Device(id="pump-01", name="Pump", properties={...}))
app.add_page(Page(id="reports", title="Reports", entities=["pump-01"]))Complete example
app.add_page(Page(
id="reports",
title="Reports",
icon="chart",
columns=2,
density="loose",
sections=[
Section(title="Pump Performance", entities=["pump-01"],
properties=["pressure", "temperature"]),
Section(title="Water Tank", entities=["tank-01"], columns=1, density="compact"),
],
))Constraints
columnsclamped to 1-4 by the frontenddensitymust be one ofcompact/normal/loose(validated server-side)- density precedence:
Section.density→Page.density→normal iconmust be a known semantic token (falls back to a document icon)
Common mistakes
- Sending a pixel value or an icon-library name (e.g. "bar-chart-outline") — use the semantic token instead
- Referencing an entity ID not added via
app.add()→ section item silently skipped - Forgetting
app.add_page()→ page never appears in the manifest
Generated schema
{
"id": "reports",
"title": "Reports",
"icon": "chart",
"columns": 2,
"density": "loose",
"sections": [
{ "title": "Pump Performance", "items": [{"entity": "pump-01", "property": "pressure"}] }
]
}User layout customization (per-user overlay)
Users can rearrange pages without changing Python/backend source. Every section and widget item carries a stable id ({pageId}-s{index} for sections, {sectionId}-{entity}-{property} for widgets, -2/-3 suffixes for duplicates), so overlays survive restarts and content changes.
Developer default → User overlay → Resolved page
Developer Page (Python) User overlay (per-user) Resolved Page (renderer)
↓ ↓ ↓
sections/items with ids sections:{title/order/userCreated} reordered sections
↑ widgets:{section/order/size/hidden} moved widgets
└────────────────────────────────────────────────────────── semantic sizes applied- The backend never mutates the developer schema; the overlay is stored separately under
UserConfig.pages.<pageId>and applied onGET /manifest. - The frontend mirrors the same resolver (
utils/pageLayout.ts) so drag updates apply instantly without refetching. - If the backend adds a widget later it appears safely in its dev position; if it removes one, stale overlay entries are silently dropped.
API
PUT /config/page { page_id, config: { sections, widgets } } atomic replace
DELETE /config/page/{id} reset to developer defaultEdit mode
⋯ → Edit Dashboard (or Edit Page) on any page opens the editor: drag widgets (same or across sections), reorder sections, resize semantically (compact/standard/wide/hero — snapped, never pixels), create/rename/delete user sections, hide widgets, and Reset to restore the developer default. Layout persists after each drop/resize via PUT /config/page.
Customization policy seam (future)
A widget can opt out of editing via its presentation metadata:
Section(..., presentation={"customizable": {"move": False, "resize": False, "hide": False}})The frontend hides the corresponding handles/menu actions. Default remains fully permissive; the Python API is not yet public.