Skip to content

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

python
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

python
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

python
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

  • columns clamped to 1-4 by the frontend
  • density must be one of compact/normal/loose (validated server-side)
  • density precedence: Section.densityPage.densitynormal
  • icon must 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

json
{
  "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 on GET /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 default

Edit 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:

python
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.

Built for self-hosted IoT runtimes.