This document describes the web dashboard framework, the @expose_web_page decorator system, and the shared UI components.
The web extension serves the single-page dashboard and manages page registration from all extensions. Each extension can contribute pages, static assets, and API endpoints to the dashboard without modifying the web extension itself.
WebModule (module) — page registry, iterates get_protocols(WebPageContributor)
WebService (extension) — routes, SPA shell, /web/config/pages endpoint
@expose_web_page decorator — declarative page registration on any extension
Any extension method can declare itself as a web page provider:
@expose_web_page(
page_id="map_viewer",
title="Map Viewer",
icon="fa-map",
description="Interactive map viewer.",
required_roles=[], # empty = public
section="main", # "main", "admin", "tools"
priority=10, # lower = higher in nav
)
async def provide_map_viewer(self, request: Request):
return HTMLResponse(...)The decorator attaches a _web_page_config dict to the method. The extension's get_web_pages() implementation (from extensions/tools/web_collect.collect_web_pages) emits a WebPageSpec per decorated method, satisfying WebPageContributor via structural subtyping. WebModule iterates get_protocols(WebPageContributor) during startup.
1. WebModule.lifespan() iterates get_protocols(WebPageContributor)
2. Each contributor yields WebPageSpec(page_id, title, handler, icon, priority, ...)
3. Each page is registered with dedup guard (page_id uniqueness)
4. GET /web/config/pages returns filtered page list based on user roles
5. SPA shell renders navigation from the page list
The /web/config/pages endpoint filters pages by the authenticated user's roles:
- Pages with
required_roles=[]are visible to everyone - Pages with
required_roles=["sysadmin"]are only in the response for sysadmin users - The SPA shell renders navigation items only for pages present in the response
Extensions can also register static file directories. @expose_static populates get_static_assets(), satisfying StaticAssetProvider:
@expose_static(prefix="/maps/static")
def provide_static(self):
return os.path.join(os.path.dirname(__file__), "static")A cascaded dropdown component shared across 5+ extensions for selecting Catalog → Collection → Asset:
<ds-context-selector
id="ctx"
show-assets="true"
on-change="handleContextChange">
</ds-context-selector>The component:
- Fetches catalogs from
/catalogs/on mount - Fetches collections from
/catalogs/{id}/collectionson catalog select - Fetches assets from
/features/catalogs/{id}/collections/{id}/itemson collection select - Emits change events with
{catalog_id, collection_id, asset_id}
Used by: Data Explorer, STAC Browser, Map Viewer, Admin Panel, Features Inspector.
A Web Component for switching the UI language:
<ds-language-selector></ds-language-selector>Reads available languages from window.__DS_LANGUAGES__ and persists selection in localStorage. All multilingual content (titles, descriptions) is stored as JSONB {"en": "...", "it": "..."} and the selected language key is passed as ?lang= parameter.
| Page ID | Extension | Section | Roles | Description |
|---|---|---|---|---|
home |
web | main | — | Landing page |
dashboard |
web | main | — | System dashboard with stats |
docs_viewer |
web | main | — | API documentation browser |
demo_manager |
web | admin | sysadmin | Demo data provisioning |
map_viewer |
maps | main | — | Interactive tile map |
stac_browser |
stac | main | — | STAC catalog browser |
data_explorer |
features | main | — | Feature data explorer |
configs_editor |
configs | admin | sysadmin | Configuration management |
admin_panel |
admin | admin | sysadmin | User/key management |
logs_viewer |
logs | admin | sysadmin | Access log viewer |
| Path | Purpose |
|---|---|
src/dynastore/modules/web/web_module.py |
WebModule — central page registry |
src/dynastore/extensions/web/decorators.py |
@expose_web_page, @expose_static decorators |
src/dynastore/extensions/web/web.py |
WebService — SPA shell, routes, demo endpoints |
src/dynastore/extensions/web/static/ |
Shared JS/CSS (context-selector.js, etc.) |