"""DashboardExportable: the bundling showcase. A dashboard's agent cards and app cards are pulled into the closure as sessions + apps (each session pulls its custom mode); the layout's entity-keyed dicts are rewritten local->bundle on export and bundle->fresh-local on import via the RemapTable. Mirrors the in-app duplicate_dashboard remap. Browser cards keep their url/tabs but get fresh ids; after writing the dashboard we re-point each imported session at it.""" from __future__ import annotations from datetime import datetime, timezone from uuid import uuid4 from backend.apps.swarm.exportable import DepRef, ExportContext, RemapTable from backend.apps.swarm.models import EntityType, Requirement class DashboardExportable: type = EntityType.dashboard def __init__(self, did: str, name: str, data: dict): self.local_id = did self.name = name self.p_data = data @classmethod def load(cls, local_id: str) -> "DashboardExportable | None": data = p_read(local_id) if data is None: return None return cls(local_id, data.get("name") or "Dashboard", data) def serialize(self, ctx: ExportContext) -> dict: layout = dict(self.p_data.get("layout") or {}) cards = {} for sid, card in (layout.get("cards") or {}).items(): bid = ctx.bundle_id_for(EntityType.session, sid) if bid: cards[bid] = {**card, "session_id": bid} view_cards = {} for key, card in (layout.get("view_cards") or {}).items(): # Keys are output_id for the primary card, `output_id#N` for extra instances of the same app; resolve the bundle id off the bare output id and rebuild the suffix. oid = str(card.get("output_id") or key).split("#")[0] bid = ctx.bundle_id_for(EntityType.app, oid) if bid: inst = int(card.get("instance") or 1) # parent_session_id tethers the app card to the agent that built it; it's a session id, so it remaps like spawned_by on browser cards. parent = card.get("parent_session_id") view_cards[bid if inst <= 1 else f"{bid}#{inst}"] = { **card, "output_id": bid, "parent_session_id": ctx.bundle_id_for(EntityType.session, parent) if parent else None, } browser_cards = {} for bkey, card in (layout.get("browser_cards") or {}).items(): c = dict(card) spawn = c.get("spawned_by") c["spawned_by"] = ctx.bundle_id_for(EntityType.session, spawn) if spawn else None browser_cards[bkey] = c expanded = [b for b in (ctx.bundle_id_for(EntityType.session, s) for s in (layout.get("expanded_session_ids") or [])) if b] return {"name": self.p_data.get("name") or "Dashboard", "layout": { **layout, "cards": cards, "view_cards": view_cards, "browser_cards": browser_cards, "expanded_session_ids": expanded, }} def files(self) -> dict[str, bytes]: return {} def dependencies(self) -> list[DepRef]: layout = self.p_data.get("layout") or {} deps = [DepRef(EntityType.session, sid, "has_agent") for sid in (layout.get("cards") or {})] p_view_oids = {str(card.get("output_id") or key).split("#")[0] for key, card in (layout.get("view_cards") or {}).items()} deps += [DepRef(EntityType.app, oid, "has_app") for oid in sorted(p_view_oids)] return deps def requirements(self) -> list[Requirement]: return [] @classmethod def import_(cls, payload: dict, files: dict[str, bytes], remap: RemapTable) -> str: new_did = uuid4().hex layout = dict(payload.get("layout") or {}) cards = {} for bid, card in (layout.get("cards") or {}).items(): nsid = remap.local(bid) if nsid: cards[nsid] = {**card, "session_id": nsid} view_cards = {} for key, card in (layout.get("view_cards") or {}).items(): bid = str(card.get("output_id") or key).split("#")[0] noid = remap.local(bid) if noid: inst = int(card.get("instance") or 1) parent = card.get("parent_session_id") view_cards[noid if inst <= 1 else f"{noid}#{inst}"] = { **card, "output_id": noid, "parent_session_id": remap.local(parent) if parent else None, } browser_cards = {} for p_bkey, card in (layout.get("browser_cards") or {}).items(): nbid = "browser-" + uuid4().hex[:10] c = dict(card) c["browser_id"] = nbid # Re-stamp the home dashboard, else the card keeps the source's id and the anti-bleed render guard (DashboardCardLayer keepAliveHidden) hides it on the imported dashboard. c["dashboard_id"] = new_did spawn = c.get("spawned_by") c["spawned_by"] = remap.local(spawn) if spawn else None browser_cards[nbid] = c expanded = [e for e in (remap.local(b) for b in (layout.get("expanded_session_ids") or [])) if e] now = datetime.now(timezone.utc).isoformat() doc = { "id": new_did, "name": payload.get("name") or "Imported Dashboard", "auto_named": False, "created_at": now, "updated_at": now, "layout": { **layout, "cards": cards, "view_cards": view_cards, "browser_cards": browser_cards, "expanded_session_ids": expanded, }, } p_write(new_did, doc) p_retag_sessions(cards.keys(), new_did) return new_did @classmethod def rollback(cls, local_id: str) -> None: import os d = p_dash_dir() if d: p = os.path.join(d, f"{local_id}.json") if os.path.exists(p): os.remove(p) def p_dash_dir() -> str | None: try: from backend.config.paths import DASHBOARDS_DIR return DASHBOARDS_DIR except Exception: return None def p_read(did: str) -> dict | None: import os from backend.config.json_store import read_json_or_none d = p_dash_dir() return read_json_or_none(os.path.join(d, f"{did}.json")) if d else None def p_write(did: str, doc: dict) -> None: import os from backend.config.json_store import atomic_write_json d = p_dash_dir() if d: atomic_write_json(os.path.join(d, f"{did}.json"), doc) def p_retag_sessions(session_ids, dashboard_id: str) -> None: # Best-effort: a hiccup here must not orphan the just-written dashboard. from backend.apps.agents.manager.session.session_store import load_session_data, save_session for sid in session_ids: try: d = load_session_data(sid) if d is not None: d["dashboard_id"] = dashboard_id save_session(sid, d) except Exception: pass