Dashboard Studio 1.14.0: new cards, kiosk announcements, guest mode, templates, sharing and phone app

- Seven new cards: parcels, air quality, water and gas usage, solar forecast, garage and gate, sports, Formula One
- Kiosk: announcements from automations, tab rotation, guest mode, click sound
- Editor: templates, widget presets, undo history, alignment guides, Lovelace import, share link and QR code, theme variants, suggestions, touch and hold actions
- Phones: installable app with shortcuts, offline states, tab swipe, pull to refresh, bottom sheets, haptics, phone/large visibility, landscape layout, editing on the phone, away tab
- Fixed: the weather forecast and to-do list could stay on Loading…

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 21:08:42 +01:00
parent 93bb42dcbb
commit 79d4a0f011
17 changed files with 2256 additions and 58 deletions
+39 -1
View File
@@ -147,7 +147,9 @@ def record_history(old, new):
async def index(request):
# Stamp the version on every asset URL, so an update can never be served from a stale cache.
html = (WEB_DIR / "index.html").read_text()
html = re.sub(r'(static/[\w.-]+\.(?:js|css|png))"', rf'\1?v={VERSION}"', html)
# Locally, files change without a new version number: stamp them with their last change instead.
stamp = VERSION if not MOCK else f"{VERSION}-{int(max(f.stat().st_mtime for f in WEB_DIR.iterdir()))}"
html = re.sub(r'(static/[\w.-]+\.(?:js|css|png))"', rf'\1?v={stamp}"', html)
html = html.replace("__VERSION__", VERSION)
return web.Response(text=html, content_type="text/html", headers={"Cache-Control": "no-store"})
@@ -198,6 +200,40 @@ async def version(request):
return web.json_response({"version": VERSION}, headers={"Cache-Control": "no-store"})
def all_widgets(widgets):
for w in widgets or []:
if isinstance(w, dict):
yield w
yield from all_widgets((w.get("cfg") or {}).get("children"))
async def manifest(request):
"""The web app manifest, so the dashboard can be installed on a phone's home screen.
Widgets marked as a shortcut appear when you long-press the app icon (Android)."""
shortcuts = []
for d in load_store().get("dashboards", []):
for t in d.get("tabs", []):
for w in all_widgets(t.get("widgets")):
if w.get("shortcut") and len(shortcuts) < 4:
shortcuts.append({"name": str(w["shortcut"])[:40], "url": f"./?ds_action={w.get('id')}",
"icons": [{"src": "static/icon-192.png", "sizes": "192x192", "type": "image/png"}]})
body = {
"name": "Dashboard Studio", "short_name": "Dashboards", "id": "./", "start_url": "./", "scope": "./",
"display": "standalone", "background_color": "#0f1526", "theme_color": "#0f1526",
"icons": [{"src": "static/icon-192.png", "sizes": "192x192", "type": "image/png"},
{"src": "static/icon-512.png", "sizes": "512x512", "type": "image/png"},
{"src": "static/app-icon.svg", "sizes": "any", "type": "image/svg+xml"}],
"shortcuts": shortcuts,
}
return web.json_response(body, content_type="application/manifest+json", headers={"Cache-Control": "no-store"})
async def service_worker(request):
"""Network first; the last good copy of the page, its files and the dashboards when the network fails."""
js = (WEB_DIR / "sw.js").read_text().replace("__VERSION__", VERSION)
return web.Response(text=js, content_type="application/javascript", headers={"Cache-Control": "no-store"})
async def get_dashboards(request):
return web.json_response(load_store())
@@ -334,6 +370,8 @@ def make_app():
app = web.Application(middlewares=[ingress_only], client_max_size=8 * 1024 * 1024)
app.router.add_get("/", index)
app.router.add_get("/api/version", version)
app.router.add_get("/manifest.webmanifest", manifest)
app.router.add_get("/sw.js", service_worker)
app.router.add_get("/api/changelog", changelog)
app.router.add_get("/api/whoami", whoami)
app.router.add_get("/api/users", users)