Dashboard Studio 1.7.0: scroll and swipe cards, more themes, per-device dashboards
- Container cards: horizontal scroll (snap, arrows, keeps position) and swipe (dots, optional autoplay that survives redraws and respects reduced motion), with add/edit/reorder of the cards inside - Nine new themes, contrast-checked, including High contrast; bright accents get dark text - Choose which dashboard opens per device (optionally in kiosk mode) and per Home Assistant user, via /api/whoami and the ingress user headers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,40 @@ async def index(request):
|
||||
return web.Response(text=html, content_type="text/html", headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
USERS_FILE = DATA_DIR / "users.json"
|
||||
|
||||
|
||||
def who(request):
|
||||
"""The Home Assistant user, from the headers the Supervisor's ingress proxy adds."""
|
||||
if MOCK:
|
||||
return {"id": os.environ.get("MOCK_USER_ID", "mock-user-alex"), "name": "alex", "display": "Alex"}
|
||||
h = request.headers
|
||||
uid = h.get("X-Remote-User-Id")
|
||||
return {"id": uid, "name": h.get("X-Remote-User-Name", ""), "display": h.get("X-Remote-User-Display-Name", "")} if uid else None
|
||||
|
||||
|
||||
def load_users():
|
||||
try:
|
||||
return json.loads(USERS_FILE.read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
async def whoami(request):
|
||||
me = who(request)
|
||||
if me: # remember who has used the app, so dashboards can be assigned to them
|
||||
users = load_users()
|
||||
if users.get(me["id"], {}).get("display") != me["display"] or me["id"] not in users:
|
||||
users[me["id"]] = {"name": me["name"], "display": me["display"]}
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
USERS_FILE.write_text(json.dumps(users, indent=2))
|
||||
return web.json_response(me or {}, headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
async def users(request):
|
||||
return web.json_response([{"id": k, **v} for k, v in load_users().items()], headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
async def version(request):
|
||||
return web.json_response({"version": VERSION}, headers={"Cache-Control": "no-store"})
|
||||
|
||||
@@ -214,6 +248,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("/api/whoami", whoami)
|
||||
app.router.add_get("/api/users", users)
|
||||
app.router.add_get("/api/dashboards", get_dashboards)
|
||||
app.router.add_put("/api/dashboards", put_dashboards)
|
||||
app.router.add_get("/api/camera/{entity_id}", camera)
|
||||
|
||||
@@ -74,7 +74,8 @@ class MockHA:
|
||||
entity("script.good_morning", "off", friendly_name="Good Morning"),
|
||||
entity("weather.home", "partlycloudy", friendly_name="Home", temperature=13, humidity=71, wind_speed=14),
|
||||
entity("camera.front_yard", "idle", friendly_name="Front Yard"),
|
||||
entity("person.alex", "home", friendly_name="Alex"),
|
||||
entity("person.alex", "home", friendly_name="Alex", user_id="mock-user-alex"),
|
||||
entity("person.sam", "not_home", friendly_name="Sam", user_id="mock-user-sam"),
|
||||
entity("input_boolean.guest_mode", "off", friendly_name="Guest Mode"),
|
||||
entity("calendar.family", "off", friendly_name="Family"),
|
||||
entity("calendar.work", "on", friendly_name="Work"),
|
||||
|
||||
Reference in New Issue
Block a user