import { emit, on } from "../lib/bus.js"; import { navigateToView, navigateToItem } from "../lib/router.js"; import { api } from "../lib/api.js"; class OpsShell extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this._currentView = "chat"; this._currentParams = {}; this._takeoverCount = 0; this.shadowRoot.innerHTML = `

Bot Ops Console

SSE: connecting…
`; } connectedCallback() { this._unsub = on("sse:status", (s) => { const el = this.shadowRoot.getElementById("sseStatus"); el.textContent = s.ok ? "SSE: connected" : "SSE: disconnected (retrying…)"; }); // Listen for view switch requests from other components this._unsubSwitch = on("ui:switchView", ({ view }) => { if (view) this.setView(view, {}, { updateUrl: true }); }); // Listen for router changes (popstate, initial load) this._unsubRouter = on("router:change", ({ view, params }) => { this.setView(view, params, { updateUrl: false }); }); // Navigation - intercept clicks on nav links const navBtns = this.shadowRoot.querySelectorAll(".nav-btn"); for (const btn of navBtns) { btn.onclick = (e) => { e.preventDefault(); const view = btn.dataset.view; this.setView(view, {}, { updateUrl: true }); }; } // Notification bell click const bell = this.shadowRoot.getElementById("notificationBell"); bell.onclick = () => { this.setView("takeovers", {}, { updateUrl: true }); }; // Listen for new takeovers via SSE - update badge immediately this._unsubTakeover = on("takeover:created", () => { this._takeoverCount++; this.updateTakeoverBadge(this._takeoverCount); }); // Start polling for takeovers this.pollTakeovers(); this._pollInterval = setInterval(() => this.pollTakeovers(), 30000); } disconnectedCallback() { this._unsub?.(); this._unsubSwitch?.(); this._unsubRouter?.(); this._unsubTakeover?.(); if (this._pollInterval) clearInterval(this._pollInterval); } async pollTakeovers() { try { const data = await api.takeovers({ limit: 1 }); const count = data.pending_count || (data.items?.length || 0); this._takeoverCount = count; this.updateTakeoverBadge(count); } catch (e) { // Silently fail - don't break the UI console.debug("Error polling takeovers:", e); } } updateTakeoverBadge(count) { const badge = this.shadowRoot.getElementById("takeoverBadge"); const bell = this.shadowRoot.getElementById("notificationBell"); if (count > 0) { badge.textContent = count > 99 ? "99+" : count; badge.style.display = "inline"; bell.classList.add("has-pending"); bell.title = `${count} takeover(s) pendiente(s)`; } else { badge.style.display = "none"; bell.classList.remove("has-pending"); bell.title = "No hay takeovers pendientes"; } } setView(viewName, params = {}, { updateUrl = true } = {}) { this._currentView = viewName; this._currentParams = params; // Update nav buttons const navBtns = this.shadowRoot.querySelectorAll(".nav-btn"); for (const btn of navBtns) { btn.classList.toggle("active", btn.dataset.view === viewName); } // Update views const views = this.shadowRoot.querySelectorAll(".view"); for (const view of views) { const isActive = view.id === `view${viewName.charAt(0).toUpperCase() + viewName.slice(1)}`; view.classList.toggle("active", isActive); } // Update URL if requested if (updateUrl) { if (params.id) { navigateToItem(viewName, params.id); } else { navigateToView(viewName); } } // Emit event for components that need to know about route params emit("router:viewChanged", { view: viewName, params }); } } customElements.define("ops-shell", OpsShell);