refactor: standalone dashboard folders with index.html, zip-based import/export (.tsd/.tss/.tsp)

- Each dashboard is now a self-contained folder: manifest.json + index.html
- Auto-scan dashboards/ and data/dashboards/ on startup
- Export: .tsd (dashboards), .tss (scenes), .tsp (game plugins) - all zip format
- Dashboard index.html is complete standalone page (WS + data binding)
- Aspect ratio constraints handled in each dashboard's own JS
- Removed template-based dashboard rendering in favor of static serve
- Import via file upload endpoints, export via direct file download
This commit is contained in:
2026-07-25 15:35:08 +08:00
parent d61bb61a83
commit 63e3c8d607
21 changed files with 752 additions and 380 deletions
+61 -28
View File
@@ -103,22 +103,23 @@ async def api_get_dashboard(theme_id: str):
@router.get("/dashboards/{theme_id}/template")
async def api_get_dashboard_template(theme_id: str):
tmpl = dashboard_manager.get_template(theme_id)
return {"html": tmpl}
tmpl = dashboard_manager.get_index_html(theme_id)
return {"html": tmpl or ""}
@router.post("/dashboards")
async def api_create_dashboard(data: dict[str, Any]):
cfg = data.get("config", {})
theme = DashboardTheme(
name=data.get("name", "Untitled"),
category=data.get("category", "basic"),
description=data.get("description", ""),
author=data.get("author", ""),
config=data.get("config", {}),
icon=cfg.get("icon", "📊"),
aspect_ratio=cfg.get("aspect_ratio", "auto"),
render_mode=cfg.get("render_mode", "contain"),
)
dashboard_manager.save(theme)
if data.get("template_html"):
dashboard_manager._save_template_file(theme.id, data["template_html"])
dashboard_manager.save(theme, data.get("index_html", ""))
return theme.to_dict()
@@ -127,13 +128,17 @@ async def api_update_dashboard(theme_id: str, data: dict[str, Any]):
existing = dashboard_manager.get(theme_id)
if not existing:
raise HTTPException(404, "Dashboard not found")
theme = DashboardTheme.from_dict(existing)
for k in ["name", "category", "description", "author", "config"]:
if k in data:
setattr(theme, k, data[k])
dashboard_manager.save(theme)
if "template_html" in data:
dashboard_manager._save_template_file(theme.id, data["template_html"])
theme = DashboardTheme(
id=theme_id,
name=data.get("name", existing.get("name", "")),
category=data.get("category", existing.get("category", "basic")),
description=data.get("description", existing.get("description", "")),
author=data.get("author", existing.get("author", "")),
icon=data.get("icon", existing.get("icon", "📊")),
aspect_ratio=data.get("aspect_ratio", existing.get("aspect_ratio", "auto")),
render_mode=data.get("render_mode", existing.get("render_mode", "contain")),
)
dashboard_manager.save(theme, data.get("index_html", ""))
return theme.to_dict()
@@ -145,15 +150,25 @@ async def api_delete_dashboard(theme_id: str):
@router.get("/dashboards/{theme_id}/export")
async def api_export_dashboard(theme_id: str):
result = dashboard_manager.export_theme(theme_id)
if not result:
from fastapi.responses import Response
zip_bytes = dashboard_manager.export_theme_zip(theme_id)
if not zip_bytes:
raise HTTPException(404, "Dashboard not found")
return result
theme = dashboard_manager.get(theme_id)
filename = f"{theme['id']}.tsd" if theme else "dashboard.tsd"
return Response(
content=zip_bytes,
media_type="application/zip",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@router.post("/dashboards/import")
async def api_import_dashboard(data: dict[str, Any]):
ok = dashboard_manager.import_theme(data)
async def api_import_dashboard(file: UploadFile = File(...)):
if not file.filename or not file.filename.endswith('.tsd'):
raise HTTPException(400, "Only .tsd files are accepted")
zip_data = await file.read()
ok = dashboard_manager.import_theme_zip(zip_data)
return {"ok": ok}
@@ -224,15 +239,25 @@ async def api_delete_scene(scene_id: str):
@router.get("/scenes/{scene_id}/export")
async def api_export_scene(scene_id: str):
result = scene_manager.export_scene(scene_id)
if not result:
from fastapi.responses import Response
zip_bytes = scene_manager.export_scene_zip(scene_id)
if not zip_bytes:
raise HTTPException(404, "Scene not found")
return result
scene = scene_manager.get(scene_id)
filename = f"{scene['id']}.tss" if scene else "scene.tss"
return Response(
content=zip_bytes,
media_type="application/zip",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@router.post("/scenes/import")
async def api_import_scene(data: dict[str, Any]):
ok = scene_manager.import_scene(data)
async def api_import_scene(file: UploadFile = File(...)):
if not file.filename or not file.filename.endswith('.tss'):
raise HTTPException(400, "Only .tss files are accepted")
zip_data = await file.read()
ok = scene_manager.import_scene_zip(zip_data)
return {"ok": ok}
@@ -254,8 +279,11 @@ async def api_get_game(plugin_id: str):
@router.post("/games/install")
async def api_install_game_plugin(data: dict[str, Any]):
ok = game_plugin_manager.install_plugin(data)
async def api_install_game_plugin(file: UploadFile = File(...)):
if not file.filename or not file.filename.endswith('.tsp'):
raise HTTPException(400, "Only .tsp files are accepted")
zip_data = await file.read()
ok = game_plugin_manager.install_plugin_zip(zip_data)
return {"ok": ok}
@@ -267,10 +295,15 @@ async def api_remove_game_plugin(plugin_id: str):
@router.get("/games/{plugin_id}/export")
async def api_export_game_plugin(plugin_id: str):
result = game_plugin_manager.export_plugin(plugin_id)
if not result:
from fastapi.responses import Response
zip_bytes = game_plugin_manager.export_plugin_zip(plugin_id)
if not zip_bytes:
raise HTTPException(404, "Game plugin not found")
return result
return Response(
content=zip_bytes,
media_type="application/zip",
headers={"Content-Disposition": f'attachment; filename="{plugin_id}.tsp"'},
)
@router.post("/games/reload")