@burdenoff/microfe-bigconsole 2026.730.5 → 2026.730.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardViewPage.js","names":[],"sources":["../../../src/bigconsole/pages/DashboardViewPage.tsx"],"sourcesContent":["/**\n * DashboardViewPage\n *\n * View/edit a single dashboard - fetches data from real backend.\n * Supports drilldown context propagation via URL parameters.\n */\n\nimport { type FC, memo, useEffect, useCallback, useState, useRef } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft } from 'lucide-react';\nimport { Button, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useBigConsoleNavigate } from '../context';\nimport { DashboardCanvas, ExportDashboardDialog, DrilldownBreadcrumb, LiveCanvasToggle } from '../components/dashboard';\nimport { useDashboardStore, useWidgetStore } from '../store';\nimport { useDashboardOperations, useWidgetOperations, usePageOperations } from '../hooks';\nimport { useFilterUrlSync } from '../hooks/useFilterUrlSync';\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardViewPage: FC = memo(function DashboardViewPage() {\n const { dashboardId } = useParams<{ dashboardId: string }>();\n const navigate = useBigConsoleNavigate();\n const [currentPageId, setCurrentPageId] = useState<string | null>(null);\n const [isSaving, setIsSaving] = useState(false);\n const [notFound, setNotFound] = useState(false);\n const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);\n // Live canvas: when on, poll the dashboard + widget STRUCTURE so server-side\n // pipeline builds (AI assistant agent or any other writer) appear without a reload.\n const [isLive, setIsLive] = useState(false);\n\n // Drilldown context from URL\n const { isInDrilldown, getContextFromUrl, getDrilldownMetadata } = useFilterUrlSync();\n\n // Store actions\n const currentDashboard = useDashboardStore((state) => state.currentDashboard);\n const setCurrentDashboard = useDashboardStore((state) => state.setCurrentDashboard);\n const setGlobalFilterValue = useDashboardStore((state) => state.setGlobalFilterValue);\n const clearAllGlobalFilters = useDashboardStore((state) => state.clearAllGlobalFilters);\n const widgets = useWidgetStore((state) => state.widgets);\n const clipboardWidgetId = useWidgetStore((state) => state.clipboardWidgetId);\n const clipboardAction = useWidgetStore((state) => state.clipboardAction);\n const clearClipboard = useWidgetStore((state) => state.clearClipboard);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n\n // Dashboard operations hook\n const { loading, error, fetchDashboard, updateDashboard } = useDashboardOperations();\n\n // Widget operations hook\n const {\n createWidget: createWidgetMutation,\n deleteWidget: deleteWidgetMutation,\n duplicateWidget: duplicateWidgetMutation,\n batchUpdatePositions,\n refetch: refetchWidgets,\n } = useWidgetOperations(currentPageId || undefined, dashboardId);\n\n // Page operations hook\n const {\n createPage: createPageMutation,\n updatePage: updatePageMutation,\n deletePage: deletePageMutation,\n reorderPages: reorderPagesMutation,\n } = usePageOperations();\n\n // Load dashboard data\n useEffect(() => {\n if (!dashboardId) {\n navigate('/dashboards');\n return;\n }\n\n // Clear stale global filters from previous dashboard before loading new one.\n // This prevents drilldown context filters (ctx_*) from the source dashboard\n // from persisting and incorrectly filtering data on the target dashboard.\n clearAllGlobalFilters();\n\n setNotFound(false);\n\n // Fetch dashboard from backend\n fetchDashboard(dashboardId).then((dashboard) => {\n if (dashboard) {\n // Set the first page as current page\n if (dashboard.pages && dashboard.pages.length > 0) {\n setCurrentPageId(dashboard.pages[0].id);\n }\n } else {\n // Dashboard not found\n setNotFound(true);\n }\n });\n\n // Cleanup\n return () => {\n setCurrentDashboard(null);\n clearAllGlobalFilters();\n setNotFound(false);\n };\n }, [dashboardId, navigate, fetchDashboard, setCurrentDashboard, clearAllGlobalFilters]);\n\n // Load drilldown context from URL and apply to global filters\n useEffect(() => {\n if (!isInDrilldown) return;\n\n // Get context params from URL (ctx_* params)\n const contextParams = getContextFromUrl();\n const drilldownMeta = getDrilldownMetadata();\n\n // Apply context to dashboard state (available for widgets to use)\n // Store as special filter values that widgets can reference\n Object.entries(contextParams).forEach(([key, value]) => {\n // Store context params as global filter values with ctx_ prefix for clarity\n setGlobalFilterValue(`ctx_${key}`, value);\n });\n\n // Store drilldown metadata as special values\n if (drilldownMeta.depth > 0) {\n setGlobalFilterValue('__drilldown_depth', drilldownMeta.depth);\n setGlobalFilterValue('__drilldown_parent', drilldownMeta.parent);\n setGlobalFilterValue('__drilldown_path', drilldownMeta.path);\n }\n\n // Log for debugging\n if (Object.keys(contextParams).length > 0) {\n console.debug('[DashboardViewPage] Loaded drilldown context:', {\n context: contextParams,\n metadata: drilldownMeta,\n });\n }\n }, [isInDrilldown, getContextFromUrl, getDrilldownMetadata, setGlobalFilterValue]);\n\n // ---------------------------------------------------------------------------\n // Live canvas: poll dashboard + widget STRUCTURE while live mode is on.\n //\n // The per-widget DATA poll (WidgetWrapper → refreshInterval) already keeps\n // numbers fresh, but nothing refetches the board's STRUCTURE, so a widget the\n // AI assistant's api-calls agent (DataSink → Parser → Widget) — or any other\n // server-side writer — adds would never appear without a manual reload. This\n // interval closes that gap: it silently refetches the dashboard (pages) and\n // the widget list, so new widgets/pages materialise on the canvas live.\n //\n // Efficiency: it runs ONLY while live mode is on AND the tab is visible, and\n // pauses while the board is in edit mode so a poll cannot clobber an in-flight\n // drag/resize. Polling, not subscriptions — the backend GraphQL server is\n // HTTP-only with no graphql-ws/PubSub transport (see LiveBoardBanner).\n //\n // Latency note: the floor on how quickly a change shows up is NOT this poll\n // interval but fe-libs' shared GraphQL read cache (RECENT_QUERY_CACHE_TTL_MS,\n // ~15s) — `network-only` is served from that short-lived client cache, so a\n // newly-created widget surfaces on the next poll AFTER its cache entry\n // expires. No manual reload is ever needed; worst-case freshness is ~15s.\n // ---------------------------------------------------------------------------\n const refetchWidgetsRef = useRef(refetchWidgets);\n refetchWidgetsRef.current = refetchWidgets;\n\n useEffect(() => {\n if (!isLive || !dashboardId) return;\n\n const LIVE_POLL_MS = 3500;\n\n const pollStructure = () => {\n // Skip work when the tab is backgrounded or the board is being edited.\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n if (useDashboardStore.getState().viewMode === 'edit') return;\n void fetchDashboard(dashboardId, { silent: true });\n void refetchWidgetsRef.current();\n };\n\n // Refetch immediately on enable so newly-built widgets show up at once,\n // then settle into the cadence.\n pollStructure();\n const intervalId = window.setInterval(pollStructure, LIVE_POLL_MS);\n\n // Catch up the moment the operator returns to the tab.\n const handleVisibility = () => {\n if (document.visibilityState === 'visible') pollStructure();\n };\n document.addEventListener('visibilitychange', handleVisibility);\n\n return () => {\n window.clearInterval(intervalId);\n document.removeEventListener('visibilitychange', handleVisibility);\n };\n }, [isLive, dashboardId, fetchDashboard]);\n\n // Handle save - batch update widget positions and dashboard metadata\n const handleSave = useCallback(async () => {\n if (!dashboardId) return;\n\n setIsSaving(true);\n try {\n // Get all widgets and their positions\n const widgetList = Array.from(widgets.values());\n if (widgetList.length > 0) {\n const positionUpdates = widgetList.map((widget) => ({\n id: widget.id,\n position: widget.position,\n }));\n\n await batchUpdatePositions(positionUpdates);\n }\n\n // Update dashboard metadata if needed\n if (currentDashboard) {\n await updateDashboard({\n id: dashboardId,\n name: currentDashboard.name,\n description: currentDashboard.description,\n });\n }\n } catch (error) {\n } finally {\n setIsSaving(false);\n }\n }, [dashboardId, widgets, currentDashboard, batchUpdatePositions, updateDashboard]);\n\n // Navigate back to the dashboards list\n const handleBack = useCallback(() => {\n navigate('/dashboards');\n }, [navigate]);\n\n // Handle page change\n const handlePageChange = useCallback((pageId: string) => {\n setCurrentPageId(pageId);\n }, []);\n\n // Handle paste widget from clipboard\n const handlePaste = useCallback(async () => {\n const pageId = currentPageId || currentDashboard?.pages?.[0]?.id;\n if (!clipboardWidgetId || !dashboardId || !pageId) {\n return;\n }\n\n const sourceWidget = widgets.get(clipboardWidgetId);\n if (!sourceWidget) {\n clearClipboard();\n return;\n }\n\n // Calculate new position (place at bottom of existing widgets)\n const widgetsArray = Array.from(widgets.values());\n const maxY = widgetsArray.reduce((max, w) => {\n const widgetBottom = (w.positionY ?? 0) + (w.positionHeight ?? 4);\n return Math.max(max, widgetBottom);\n }, 0);\n\n const newPosition = {\n x: 0,\n y: maxY,\n width: sourceWidget.positionWidth ?? 4,\n height: sourceWidget.positionHeight ?? 4,\n };\n\n const newTitle = clipboardAction === 'copy' ? `${sourceWidget.title} (Copy)` : sourceWidget.title;\n\n let newWidget = null;\n\n // Try duplicateWidget API first\n try {\n newWidget = await duplicateWidgetMutation(clipboardWidgetId, newPosition, newTitle);\n } catch (err) {}\n\n // Fallback: Create new widget with source widget's data\n if (!newWidget) {\n newWidget = await createWidgetMutation({\n pageId,\n dashboardId,\n type: sourceWidget.type,\n title: newTitle,\n description: sourceWidget.description,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: (sourceWidget as unknown as Record<string, string | undefined>).dataSinkId,\n datasetId: (sourceWidget as unknown as Record<string, string | undefined>).datasetId,\n parserId: (sourceWidget as unknown as Record<string, string | undefined>).parserId,\n parserRules: (sourceWidget as unknown as Record<string, string | undefined>).parserRules,\n config: sourceWidget.config || {},\n position: newPosition,\n refreshInterval: sourceWidget.refreshInterval ?? undefined,\n metadata: sourceWidget.metadata,\n });\n }\n\n if (newWidget) {\n selectWidget(newWidget.id);\n\n // If cut action, delete the original widget from backend\n if (clipboardAction === 'cut') {\n await deleteWidgetMutation(clipboardWidgetId);\n }\n }\n\n clearClipboard();\n }, [\n clipboardWidgetId,\n clipboardAction,\n dashboardId,\n currentPageId,\n currentDashboard,\n widgets,\n duplicateWidgetMutation,\n createWidgetMutation,\n deleteWidgetMutation,\n selectWidget,\n clearClipboard,\n ]);\n\n // Handle export\n const handleExport = useCallback(() => {\n setIsExportDialogOpen(true);\n }, []);\n\n const handleExportClose = useCallback(() => {\n setIsExportDialogOpen(false);\n }, []);\n\n // ============================================================================\n // Page Management Callbacks\n // ============================================================================\n\n // Handle adding a new page\n const handlePageAdd = useCallback(async () => {\n if (!currentDashboard || !dashboardId) return;\n\n const newPageCount = (currentDashboard.pages?.length || 0) + 1;\n const newPageName = `Page ${newPageCount}`;\n\n // Call GraphQL mutation to create page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: newPageName,\n });\n\n if (newPage) {\n // Update local state with the created page\n setCurrentDashboard({\n ...currentDashboard,\n pages: [...(currentDashboard.pages || []), newPage],\n });\n\n // Switch to the new page\n setCurrentPageId(newPage.id);\n }\n }, [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]);\n\n // Handle renaming a page\n const handlePageRename = useCallback(\n async (pageId: string, newName: string) => {\n if (!currentDashboard) return;\n\n // Call GraphQL mutation to update page\n const updatedPage = await updatePageMutation({\n id: pageId,\n name: newName,\n });\n\n if (updatedPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: currentDashboard.pages?.map((page) => (page.id === pageId ? { ...page, name: newName } : page)),\n });\n }\n },\n [currentDashboard, setCurrentDashboard, updatePageMutation]\n );\n\n // Handle duplicating a page\n const handlePageDuplicate = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || !dashboardId) return;\n\n const sourcePage = currentDashboard.pages?.find((p) => p.id === pageId);\n if (!sourcePage) return;\n\n // Call GraphQL mutation to create a copy of the page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: `${sourcePage.name} (Copy)`,\n });\n\n if (newPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: [...(currentDashboard.pages || []), newPage],\n });\n\n setCurrentPageId(newPage.id);\n }\n // Note: Widget duplication would require additional backend support\n },\n [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]\n );\n\n // Handle deleting a page\n const handlePageDelete = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || (currentDashboard.pages?.length || 0) <= 1) return;\n\n // Call GraphQL mutation to delete page\n const success = await deletePageMutation(pageId);\n\n if (success) {\n const updatedPages = currentDashboard.pages\n ?.filter((p) => p.id !== pageId)\n .map((p, index) => ({ ...p, order: index }));\n\n setCurrentDashboard({\n ...currentDashboard,\n pages: updatedPages,\n });\n\n // Switch to first page if current page was deleted\n if (currentPageId === pageId) {\n setCurrentPageId(updatedPages?.[0]?.id || null);\n }\n }\n },\n [currentDashboard, setCurrentDashboard, currentPageId, deletePageMutation]\n );\n\n // Handle reordering pages\n const handlePagesReorder = useCallback(\n async (reorderedPages: Array<{ id: string; name?: string; order?: number }>) => {\n if (!currentDashboard || !dashboardId) return;\n\n // Update local state optimistically\n setCurrentDashboard({\n ...currentDashboard,\n pages: reorderedPages.map((page, index) => ({\n ...page,\n order: index,\n })),\n });\n\n // Call GraphQL mutation to persist the reorder\n const pageIds = reorderedPages.map((p) => p.id);\n await reorderPagesMutation(dashboardId, pageIds);\n },\n [currentDashboard, dashboardId, setCurrentDashboard, reorderPagesMutation]\n );\n\n // Error state\n if (error) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full\" style={{ padding: 'var(--space-pagePadding)' }}>\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-6 w-full max-w-md\">\n <h3 className=\"text-lg font-medium text-status-error-text mb-2\">Error loading dashboard</h3>\n <p className=\"text-sm text-text-secondary mb-4 break-words\">{error.message}</p>\n <div className=\"flex flex-wrap gap-3\">\n <button\n type=\"button\"\n onClick={() => fetchDashboard(dashboardId!)}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary bg-bg-sunken hover:bg-bg-sunken/80 rounded-md\"\n >\n Back to Dashboards\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n // Not found state\n if (notFound) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full\" style={{ padding: 'var(--space-pagePadding)' }}>\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Dashboard not found\"\n description={\n <>\n The dashboard you&apos;re looking for doesn&apos;t exist or has been deleted.\n <span className=\"mt-2 block font-mono text-xs text-text-tertiary break-all\">ID: {dashboardId}</span>\n </>\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Back to Dashboards\n </button>\n }\n />\n </div>\n );\n }\n\n // Loading state\n if (loading || !currentDashboard) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"flex flex-col items-center gap-4\">\n <div className=\"animate-spin w-8 h-8 border-4 border-action-primary-bg border-t-transparent rounded-full\" />\n <p className=\"text-sm text-text-secondary\">Loading dashboard...</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full flex flex-col overflow-hidden\">\n {/* Page header with back navigation - visible in both view and edit modes */}\n <div className=\"flex items-center gap-3 px-3 py-2 border-b border-border-default bg-bg-surface flex-shrink-0\">\n <Button\n onClick={handleBack}\n variant=\"ghost\"\n size=\"sm\"\n title=\"Back to Dashboards\"\n aria-label=\"Back to Dashboards\"\n >\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n Back\n </Button>\n <h2 className=\"text-sm font-semibold text-text-primary truncate\">{currentDashboard.name}</h2>\n {/* Live canvas toggle — watch server-side pipeline builds appear without a reload */}\n <div className=\"ml-auto flex-shrink-0\">\n <LiveCanvasToggle isLive={isLive} onToggle={() => setIsLive((prev) => !prev)} />\n </div>\n </div>\n\n {/* Drilldown Breadcrumb - shown when navigating via drilldown */}\n {isInDrilldown && (\n <DrilldownBreadcrumb\n currentDashboardId={currentDashboard.id}\n currentDashboardName={currentDashboard.name}\n basePath=\"/bigconsole/dashboards\"\n />\n )}\n\n {/* Dashboard Canvas with Page Tabs */}\n <div className=\"flex-1 min-w-0 overflow-hidden\">\n <DashboardCanvas\n dashboardId={currentDashboard.id}\n pageId={currentPageId || currentDashboard.pages?.[0]?.id}\n title={currentDashboard.name}\n onPaste={handlePaste}\n onSave={handleSave}\n isSaving={isSaving}\n onExport={handleExport}\n // Page tabs props\n pages={(currentDashboard.pages || []).map((page, index) => ({\n id: page.id,\n name: page.name || `Page ${index + 1}`,\n order: page.order ?? 0,\n }))}\n activePageId={currentPageId || currentDashboard.pages?.[0]?.id}\n onPageChange={handlePageChange}\n onPageAdd={handlePageAdd}\n onPageRename={handlePageRename}\n onPageDuplicate={handlePageDuplicate}\n onPageDelete={handlePageDelete}\n onPagesReorder={handlePagesReorder}\n showPageTabs={true}\n />\n </div>\n\n {/* Export Dashboard Dialog */}\n <ExportDashboardDialog\n isOpen={isExportDialogOpen}\n dashboardId={currentDashboard.id}\n dashboardName={currentDashboard.name}\n onClose={handleExportClose}\n />\n </div>\n );\n});\n\nexport default DashboardViewPage;\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardViewPage.js","names":[],"sources":["../../../src/bigconsole/pages/DashboardViewPage.tsx"],"sourcesContent":["/**\n * DashboardViewPage\n *\n * View/edit a single dashboard - fetches data from real backend.\n * Supports drilldown context propagation via URL parameters.\n */\n\nimport { type FC, memo, useEffect, useCallback, useState, useRef } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft } from 'lucide-react';\nimport { Button, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useBigConsoleNavigate } from '../context';\nimport {\n DashboardCanvas,\n ExportDashboardDialog,\n PublishToStoreDialog,\n DrilldownBreadcrumb,\n LiveCanvasToggle,\n} from '../components/dashboard';\nimport { useDashboardStore, useWidgetStore } from '../store';\nimport { useDashboardOperations, useWidgetOperations, usePageOperations } from '../hooks';\nimport { useFilterUrlSync } from '../hooks/useFilterUrlSync';\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardViewPage: FC = memo(function DashboardViewPage() {\n const { dashboardId } = useParams<{ dashboardId: string }>();\n const navigate = useBigConsoleNavigate();\n const [currentPageId, setCurrentPageId] = useState<string | null>(null);\n const [isSaving, setIsSaving] = useState(false);\n const [notFound, setNotFound] = useState(false);\n const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);\n // Live canvas: when on, poll the dashboard + widget STRUCTURE so server-side\n // pipeline builds (AI assistant agent or any other writer) appear without a reload.\n const [isLive, setIsLive] = useState(false);\n\n // Drilldown context from URL\n const { isInDrilldown, getContextFromUrl, getDrilldownMetadata } = useFilterUrlSync();\n\n // Store actions\n const currentDashboard = useDashboardStore((state) => state.currentDashboard);\n const setCurrentDashboard = useDashboardStore((state) => state.setCurrentDashboard);\n const setGlobalFilterValue = useDashboardStore((state) => state.setGlobalFilterValue);\n const clearAllGlobalFilters = useDashboardStore((state) => state.clearAllGlobalFilters);\n const widgets = useWidgetStore((state) => state.widgets);\n const clipboardWidgetId = useWidgetStore((state) => state.clipboardWidgetId);\n const clipboardAction = useWidgetStore((state) => state.clipboardAction);\n const clearClipboard = useWidgetStore((state) => state.clearClipboard);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n\n // Dashboard operations hook\n const { loading, error, fetchDashboard, updateDashboard } = useDashboardOperations();\n\n // Widget operations hook\n const {\n createWidget: createWidgetMutation,\n deleteWidget: deleteWidgetMutation,\n duplicateWidget: duplicateWidgetMutation,\n batchUpdatePositions,\n refetch: refetchWidgets,\n } = useWidgetOperations(currentPageId || undefined, dashboardId);\n\n // Page operations hook\n const {\n createPage: createPageMutation,\n updatePage: updatePageMutation,\n deletePage: deletePageMutation,\n reorderPages: reorderPagesMutation,\n } = usePageOperations();\n\n // Load dashboard data\n useEffect(() => {\n if (!dashboardId) {\n navigate('/dashboards');\n return;\n }\n\n // Clear stale global filters from previous dashboard before loading new one.\n // This prevents drilldown context filters (ctx_*) from the source dashboard\n // from persisting and incorrectly filtering data on the target dashboard.\n clearAllGlobalFilters();\n\n setNotFound(false);\n\n // Fetch dashboard from backend\n fetchDashboard(dashboardId).then((dashboard) => {\n if (dashboard) {\n // Set the first page as current page\n if (dashboard.pages && dashboard.pages.length > 0) {\n setCurrentPageId(dashboard.pages[0].id);\n }\n } else {\n // Dashboard not found\n setNotFound(true);\n }\n });\n\n // Cleanup\n return () => {\n setCurrentDashboard(null);\n clearAllGlobalFilters();\n setNotFound(false);\n };\n }, [dashboardId, navigate, fetchDashboard, setCurrentDashboard, clearAllGlobalFilters]);\n\n // Load drilldown context from URL and apply to global filters\n useEffect(() => {\n if (!isInDrilldown) return;\n\n // Get context params from URL (ctx_* params)\n const contextParams = getContextFromUrl();\n const drilldownMeta = getDrilldownMetadata();\n\n // Apply context to dashboard state (available for widgets to use)\n // Store as special filter values that widgets can reference\n Object.entries(contextParams).forEach(([key, value]) => {\n // Store context params as global filter values with ctx_ prefix for clarity\n setGlobalFilterValue(`ctx_${key}`, value);\n });\n\n // Store drilldown metadata as special values\n if (drilldownMeta.depth > 0) {\n setGlobalFilterValue('__drilldown_depth', drilldownMeta.depth);\n setGlobalFilterValue('__drilldown_parent', drilldownMeta.parent);\n setGlobalFilterValue('__drilldown_path', drilldownMeta.path);\n }\n\n // Log for debugging\n if (Object.keys(contextParams).length > 0) {\n console.debug('[DashboardViewPage] Loaded drilldown context:', {\n context: contextParams,\n metadata: drilldownMeta,\n });\n }\n }, [isInDrilldown, getContextFromUrl, getDrilldownMetadata, setGlobalFilterValue]);\n\n // ---------------------------------------------------------------------------\n // Live canvas: poll dashboard + widget STRUCTURE while live mode is on.\n //\n // The per-widget DATA poll (WidgetWrapper → refreshInterval) already keeps\n // numbers fresh, but nothing refetches the board's STRUCTURE, so a widget the\n // AI assistant's api-calls agent (DataSink → Parser → Widget) — or any other\n // server-side writer — adds would never appear without a manual reload. This\n // interval closes that gap: it silently refetches the dashboard (pages) and\n // the widget list, so new widgets/pages materialise on the canvas live.\n //\n // Efficiency: it runs ONLY while live mode is on AND the tab is visible, and\n // pauses while the board is in edit mode so a poll cannot clobber an in-flight\n // drag/resize. Polling, not subscriptions — the backend GraphQL server is\n // HTTP-only with no graphql-ws/PubSub transport (see LiveBoardBanner).\n //\n // Latency note: the floor on how quickly a change shows up is NOT this poll\n // interval but fe-libs' shared GraphQL read cache (RECENT_QUERY_CACHE_TTL_MS,\n // ~15s) — `network-only` is served from that short-lived client cache, so a\n // newly-created widget surfaces on the next poll AFTER its cache entry\n // expires. No manual reload is ever needed; worst-case freshness is ~15s.\n // ---------------------------------------------------------------------------\n const refetchWidgetsRef = useRef(refetchWidgets);\n refetchWidgetsRef.current = refetchWidgets;\n\n useEffect(() => {\n if (!isLive || !dashboardId) return;\n\n const LIVE_POLL_MS = 3500;\n\n const pollStructure = () => {\n // Skip work when the tab is backgrounded or the board is being edited.\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n if (useDashboardStore.getState().viewMode === 'edit') return;\n void fetchDashboard(dashboardId, { silent: true });\n void refetchWidgetsRef.current();\n };\n\n // Refetch immediately on enable so newly-built widgets show up at once,\n // then settle into the cadence.\n pollStructure();\n const intervalId = window.setInterval(pollStructure, LIVE_POLL_MS);\n\n // Catch up the moment the operator returns to the tab.\n const handleVisibility = () => {\n if (document.visibilityState === 'visible') pollStructure();\n };\n document.addEventListener('visibilitychange', handleVisibility);\n\n return () => {\n window.clearInterval(intervalId);\n document.removeEventListener('visibilitychange', handleVisibility);\n };\n }, [isLive, dashboardId, fetchDashboard]);\n\n // Handle save - batch update widget positions and dashboard metadata\n const handleSave = useCallback(async () => {\n if (!dashboardId) return;\n\n setIsSaving(true);\n try {\n // Get all widgets and their positions\n const widgetList = Array.from(widgets.values());\n if (widgetList.length > 0) {\n const positionUpdates = widgetList.map((widget) => ({\n id: widget.id,\n position: widget.position,\n }));\n\n await batchUpdatePositions(positionUpdates);\n }\n\n // Update dashboard metadata if needed\n if (currentDashboard) {\n await updateDashboard({\n id: dashboardId,\n name: currentDashboard.name,\n description: currentDashboard.description,\n });\n }\n } catch (error) {\n } finally {\n setIsSaving(false);\n }\n }, [dashboardId, widgets, currentDashboard, batchUpdatePositions, updateDashboard]);\n\n // Navigate back to the dashboards list\n const handleBack = useCallback(() => {\n navigate('/dashboards');\n }, [navigate]);\n\n // Handle page change\n const handlePageChange = useCallback((pageId: string) => {\n setCurrentPageId(pageId);\n }, []);\n\n // Handle paste widget from clipboard\n const handlePaste = useCallback(async () => {\n const pageId = currentPageId || currentDashboard?.pages?.[0]?.id;\n if (!clipboardWidgetId || !dashboardId || !pageId) {\n return;\n }\n\n const sourceWidget = widgets.get(clipboardWidgetId);\n if (!sourceWidget) {\n clearClipboard();\n return;\n }\n\n // Calculate new position (place at bottom of existing widgets)\n const widgetsArray = Array.from(widgets.values());\n const maxY = widgetsArray.reduce((max, w) => {\n const widgetBottom = (w.positionY ?? 0) + (w.positionHeight ?? 4);\n return Math.max(max, widgetBottom);\n }, 0);\n\n const newPosition = {\n x: 0,\n y: maxY,\n width: sourceWidget.positionWidth ?? 4,\n height: sourceWidget.positionHeight ?? 4,\n };\n\n const newTitle = clipboardAction === 'copy' ? `${sourceWidget.title} (Copy)` : sourceWidget.title;\n\n let newWidget = null;\n\n // Try duplicateWidget API first\n try {\n newWidget = await duplicateWidgetMutation(clipboardWidgetId, newPosition, newTitle);\n } catch (err) {}\n\n // Fallback: Create new widget with source widget's data\n if (!newWidget) {\n newWidget = await createWidgetMutation({\n pageId,\n dashboardId,\n type: sourceWidget.type,\n title: newTitle,\n description: sourceWidget.description,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: (sourceWidget as unknown as Record<string, string | undefined>).dataSinkId,\n datasetId: (sourceWidget as unknown as Record<string, string | undefined>).datasetId,\n parserId: (sourceWidget as unknown as Record<string, string | undefined>).parserId,\n parserRules: (sourceWidget as unknown as Record<string, string | undefined>).parserRules,\n config: sourceWidget.config || {},\n position: newPosition,\n refreshInterval: sourceWidget.refreshInterval ?? undefined,\n metadata: sourceWidget.metadata,\n });\n }\n\n if (newWidget) {\n selectWidget(newWidget.id);\n\n // If cut action, delete the original widget from backend\n if (clipboardAction === 'cut') {\n await deleteWidgetMutation(clipboardWidgetId);\n }\n }\n\n clearClipboard();\n }, [\n clipboardWidgetId,\n clipboardAction,\n dashboardId,\n currentPageId,\n currentDashboard,\n widgets,\n duplicateWidgetMutation,\n createWidgetMutation,\n deleteWidgetMutation,\n selectWidget,\n clearClipboard,\n ]);\n\n // Handle export\n const handleExport = useCallback(() => {\n setIsExportDialogOpen(true);\n }, []);\n\n // Publish this dashboard as a reusable store template\n const [isPublishDialogOpen, setIsPublishDialogOpen] = useState(false);\n const handlePublishTemplate = useCallback(() => {\n setIsPublishDialogOpen(true);\n }, []);\n const handlePublishClose = useCallback(() => {\n setIsPublishDialogOpen(false);\n }, []);\n\n const handleExportClose = useCallback(() => {\n setIsExportDialogOpen(false);\n }, []);\n\n // ============================================================================\n // Page Management Callbacks\n // ============================================================================\n\n // Handle adding a new page\n const handlePageAdd = useCallback(async () => {\n if (!currentDashboard || !dashboardId) return;\n\n const newPageCount = (currentDashboard.pages?.length || 0) + 1;\n const newPageName = `Page ${newPageCount}`;\n\n // Call GraphQL mutation to create page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: newPageName,\n });\n\n if (newPage) {\n // Update local state with the created page\n setCurrentDashboard({\n ...currentDashboard,\n pages: [...(currentDashboard.pages || []), newPage],\n });\n\n // Switch to the new page\n setCurrentPageId(newPage.id);\n }\n }, [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]);\n\n // Handle renaming a page\n const handlePageRename = useCallback(\n async (pageId: string, newName: string) => {\n if (!currentDashboard) return;\n\n // Call GraphQL mutation to update page\n const updatedPage = await updatePageMutation({\n id: pageId,\n name: newName,\n });\n\n if (updatedPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: currentDashboard.pages?.map((page) => (page.id === pageId ? { ...page, name: newName } : page)),\n });\n }\n },\n [currentDashboard, setCurrentDashboard, updatePageMutation]\n );\n\n // Handle duplicating a page\n const handlePageDuplicate = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || !dashboardId) return;\n\n const sourcePage = currentDashboard.pages?.find((p) => p.id === pageId);\n if (!sourcePage) return;\n\n // Call GraphQL mutation to create a copy of the page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: `${sourcePage.name} (Copy)`,\n });\n\n if (newPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: [...(currentDashboard.pages || []), newPage],\n });\n\n setCurrentPageId(newPage.id);\n }\n // Note: Widget duplication would require additional backend support\n },\n [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]\n );\n\n // Handle deleting a page\n const handlePageDelete = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || (currentDashboard.pages?.length || 0) <= 1) return;\n\n // Call GraphQL mutation to delete page\n const success = await deletePageMutation(pageId);\n\n if (success) {\n const updatedPages = currentDashboard.pages\n ?.filter((p) => p.id !== pageId)\n .map((p, index) => ({ ...p, order: index }));\n\n setCurrentDashboard({\n ...currentDashboard,\n pages: updatedPages,\n });\n\n // Switch to first page if current page was deleted\n if (currentPageId === pageId) {\n setCurrentPageId(updatedPages?.[0]?.id || null);\n }\n }\n },\n [currentDashboard, setCurrentDashboard, currentPageId, deletePageMutation]\n );\n\n // Handle reordering pages\n const handlePagesReorder = useCallback(\n async (reorderedPages: Array<{ id: string; name?: string; order?: number }>) => {\n if (!currentDashboard || !dashboardId) return;\n\n // Update local state optimistically\n setCurrentDashboard({\n ...currentDashboard,\n pages: reorderedPages.map((page, index) => ({\n ...page,\n order: index,\n })),\n });\n\n // Call GraphQL mutation to persist the reorder\n const pageIds = reorderedPages.map((p) => p.id);\n await reorderPagesMutation(dashboardId, pageIds);\n },\n [currentDashboard, dashboardId, setCurrentDashboard, reorderPagesMutation]\n );\n\n // Error state\n if (error) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full\" style={{ padding: 'var(--space-pagePadding)' }}>\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-6 w-full max-w-md\">\n <h3 className=\"text-lg font-medium text-status-error-text mb-2\">Error loading dashboard</h3>\n <p className=\"text-sm text-text-secondary mb-4 break-words\">{error.message}</p>\n <div className=\"flex flex-wrap gap-3\">\n <button\n type=\"button\"\n onClick={() => fetchDashboard(dashboardId!)}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary bg-bg-sunken hover:bg-bg-sunken/80 rounded-md\"\n >\n Back to Dashboards\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n // Not found state\n if (notFound) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full\" style={{ padding: 'var(--space-pagePadding)' }}>\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Dashboard not found\"\n description={\n <>\n The dashboard you&apos;re looking for doesn&apos;t exist or has been deleted.\n <span className=\"mt-2 block font-mono text-xs text-text-tertiary break-all\">ID: {dashboardId}</span>\n </>\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Back to Dashboards\n </button>\n }\n />\n </div>\n );\n }\n\n // Loading state\n if (loading || !currentDashboard) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"flex flex-col items-center gap-4\">\n <div className=\"animate-spin w-8 h-8 border-4 border-action-primary-bg border-t-transparent rounded-full\" />\n <p className=\"text-sm text-text-secondary\">Loading dashboard...</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full flex flex-col overflow-hidden\">\n {/* Page header with back navigation - visible in both view and edit modes */}\n <div className=\"flex items-center gap-3 px-3 py-2 border-b border-border-default bg-bg-surface flex-shrink-0\">\n <Button\n onClick={handleBack}\n variant=\"ghost\"\n size=\"sm\"\n title=\"Back to Dashboards\"\n aria-label=\"Back to Dashboards\"\n >\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n Back\n </Button>\n <h2 className=\"text-sm font-semibold text-text-primary truncate\">{currentDashboard.name}</h2>\n {/* Live canvas toggle — watch server-side pipeline builds appear without a reload */}\n <div className=\"ml-auto flex-shrink-0\">\n <LiveCanvasToggle isLive={isLive} onToggle={() => setIsLive((prev) => !prev)} />\n </div>\n </div>\n\n {/* Drilldown Breadcrumb - shown when navigating via drilldown */}\n {isInDrilldown && (\n <DrilldownBreadcrumb\n currentDashboardId={currentDashboard.id}\n currentDashboardName={currentDashboard.name}\n basePath=\"/bigconsole/dashboards\"\n />\n )}\n\n {/* Dashboard Canvas with Page Tabs */}\n <div className=\"flex-1 min-w-0 overflow-hidden\">\n <DashboardCanvas\n dashboardId={currentDashboard.id}\n pageId={currentPageId || currentDashboard.pages?.[0]?.id}\n title={currentDashboard.name}\n onPaste={handlePaste}\n onSave={handleSave}\n isSaving={isSaving}\n onExport={handleExport}\n onPublishTemplate={handlePublishTemplate}\n // Page tabs props\n pages={(currentDashboard.pages || []).map((page, index) => ({\n id: page.id,\n name: page.name || `Page ${index + 1}`,\n order: page.order ?? 0,\n }))}\n activePageId={currentPageId || currentDashboard.pages?.[0]?.id}\n onPageChange={handlePageChange}\n onPageAdd={handlePageAdd}\n onPageRename={handlePageRename}\n onPageDuplicate={handlePageDuplicate}\n onPageDelete={handlePageDelete}\n onPagesReorder={handlePagesReorder}\n showPageTabs={true}\n />\n </div>\n\n {/* Export Dashboard Dialog */}\n <ExportDashboardDialog\n isOpen={isExportDialogOpen}\n dashboardId={currentDashboard.id}\n dashboardName={currentDashboard.name}\n onClose={handleExportClose}\n />\n\n {/* Publish as Template */}\n <PublishToStoreDialog\n isOpen={isPublishDialogOpen}\n dashboardId={currentDashboard.id}\n dashboardName={currentDashboard.name}\n dashboardDescription={currentDashboard.description}\n onClose={handlePublishClose}\n />\n </div>\n );\n});\n\nexport default DashboardViewPage;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA2BA,IAAa,IAAwB,EAAK,WAA6B;CACrE,IAAM,EAAE,mBAAgB,GAAoC,EACtD,IAAW,GAAuB,EAClC,CAAC,GAAe,KAAoB,EAAwB,KAAK,EACjE,CAAC,IAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAoB,KAAyB,EAAS,GAAM,EAG7D,CAAC,GAAQ,KAAa,EAAS,GAAM,EAGrC,EAAE,kBAAe,sBAAmB,4BAAyB,IAAkB,EAG/E,IAAmB,GAAmB,MAAU,EAAM,iBAAiB,EACvE,IAAsB,GAAmB,MAAU,EAAM,oBAAoB,EAC7E,IAAuB,GAAmB,MAAU,EAAM,qBAAqB,EAC/E,IAAwB,GAAmB,MAAU,EAAM,sBAAsB,EACjF,IAAU,GAAgB,MAAU,EAAM,QAAQ,EAClD,IAAoB,GAAgB,MAAU,EAAM,kBAAkB,EACtE,IAAkB,GAAgB,MAAU,EAAM,gBAAgB,EAClE,IAAiB,GAAgB,MAAU,EAAM,eAAe,EAChE,IAAe,GAAgB,MAAU,EAAM,aAAa,EAG5D,EAAE,YAAS,UAAO,mBAAgB,uBAAoB,GAAwB,EAG9E,EACJ,cAAc,GACd,cAAc,GACd,iBAAiB,GACjB,yBACA,SAAS,MACP,EAAoB,KAAiB,KAAA,GAAW,EAAY,EAG1D,EACJ,YAAY,GACZ,YAAY,GACZ,YAAY,GACZ,cAAc,MACZ,IAAmB;AAsCvB,CAnCA,QAAgB;AACd,MAAI,CAAC,GAAa;AAChB,KAAS,cAAc;AACvB;;AAwBF,SAlBA,GAAuB,EAEvB,EAAY,GAAM,EAGlB,EAAe,EAAY,CAAC,MAAM,MAAc;AAC9C,GAAI,IAEE,EAAU,SAAS,EAAU,MAAM,SAAS,KAC9C,EAAiB,EAAU,MAAM,GAAG,GAAG,GAIzC,EAAY,GAAK;IAEnB,QAGW;AAGX,GAFA,EAAoB,KAAK,EACzB,GAAuB,EACvB,EAAY,GAAM;;IAEnB;EAAC;EAAa;EAAU;EAAgB;EAAqB;EAAsB,CAAC,EAGvF,QAAgB;AACd,MAAI,CAAC,EAAe;EAGpB,IAAM,IAAgB,GAAmB,EACnC,IAAgB,GAAsB;AAiB5C,EAbA,OAAO,QAAQ,EAAc,CAAC,SAAS,CAAC,GAAK,OAAW;AAEtD,KAAqB,OAAO,KAAO,EAAM;IACzC,EAGE,EAAc,QAAQ,MACxB,EAAqB,qBAAqB,EAAc,MAAM,EAC9D,EAAqB,sBAAsB,EAAc,OAAO,EAChE,EAAqB,oBAAoB,EAAc,KAAK,GAI1D,OAAO,KAAK,EAAc,CAAC,SAAS,KACtC,QAAQ,MAAM,iDAAiD;GAC7D,SAAS;GACT,UAAU;GACX,CAAC;IAEH;EAAC;EAAe;EAAmB;EAAsB;EAAqB,CAAC;CAuBlF,IAAM,IAAoB,GAAO,EAAe;AAGhD,CAFA,EAAkB,UAAU,GAE5B,QAAgB;AACd,MAAI,CAAC,KAAU,CAAC,EAAa;EAE7B,IAEM,UAAsB;AAEtB,UAAO,WAAa,OAAe,SAAS,oBAAoB,aAChE,EAAkB,UAAU,CAAC,aAAa,WACzC,EAAe,GAAa,EAAE,QAAQ,IAAM,CAAC,EAC7C,EAAkB,SAAS;;AAKlC,KAAe;EACf,IAAM,IAAa,OAAO,YAAY,GAAe,KAAa,EAG5D,UAAyB;AAC7B,GAAI,SAAS,oBAAoB,aAAW,GAAe;;AAI7D,SAFA,SAAS,iBAAiB,oBAAoB,EAAiB,QAElD;AAEX,GADA,OAAO,cAAc,EAAW,EAChC,SAAS,oBAAoB,oBAAoB,EAAiB;;IAEnE;EAAC;EAAQ;EAAa;EAAe,CAAC;CAGzC,IAAM,KAAa,EAAY,YAAY;AACpC,SAEL;KAAY,GAAK;AACjB,OAAI;IAEF,IAAM,IAAa,MAAM,KAAK,EAAQ,QAAQ,CAAC;AAW/C,IAVI,EAAW,SAAS,KAMtB,MAAM,EALkB,EAAW,KAAK,OAAY;KAClD,IAAI,EAAO;KACX,UAAU,EAAO;KAClB,EAAE,CAEwC,EAIzC,KACF,MAAM,EAAgB;KACpB,IAAI;KACJ,MAAM,EAAiB;KACvB,aAAa,EAAiB;KAC/B,CAAC;WAEU,WACN;AACR,MAAY,GAAM;;;IAEnB;EAAC;EAAa;EAAS;EAAkB;EAAsB;EAAgB,CAAC,EAG7E,IAAa,QAAkB;AACnC,IAAS,cAAc;IACtB,CAAC,EAAS,CAAC,EAGR,KAAmB,GAAa,MAAmB;AACvD,IAAiB,EAAO;IACvB,EAAE,CAAC,EAGA,KAAc,EAAY,YAAY;EAC1C,IAAM,IAAS,KAAiB,GAAkB,QAAQ,IAAI;AAC9D,MAAI,CAAC,KAAqB,CAAC,KAAe,CAAC,EACzC;EAGF,IAAM,IAAe,EAAQ,IAAI,EAAkB;AACnD,MAAI,CAAC,GAAc;AACjB,MAAgB;AAChB;;EAUF,IAAM,IAAc;GAClB,GAAG;GACH,GARmB,MAAM,KAAK,EAAQ,QAAQ,CAAC,CACvB,QAAQ,GAAK,MAAM;IAC3C,IAAM,KAAgB,EAAE,aAAa,MAAM,EAAE,kBAAkB;AAC/D,WAAO,KAAK,IAAI,GAAK,EAAa;MACjC,EAAE;GAKH,OAAO,EAAa,iBAAiB;GACrC,QAAQ,EAAa,kBAAkB;GACxC,EAEK,IAAW,MAAoB,SAAS,GAAG,EAAa,MAAM,WAAW,EAAa,OAExF,IAAY;AAGhB,MAAI;AACF,OAAY,MAAM,EAAwB,GAAmB,GAAa,EAAS;UACvE;AA+Bd,EA5BA,AACE,MAAY,MAAM,EAAqB;GACrC;GACA;GACA,MAAM,EAAa;GACnB,OAAO;GACP,aAAa,EAAa;GAE1B,YAAa,EAA+D;GAC5E,WAAY,EAA+D;GAC3E,UAAW,EAA+D;GAC1E,aAAc,EAA+D;GAC7E,QAAQ,EAAa,UAAU,EAAE;GACjC,UAAU;GACV,iBAAiB,EAAa,mBAAmB,KAAA;GACjD,UAAU,EAAa;GACxB,CAAC,EAGA,MACF,EAAa,EAAU,GAAG,EAGtB,MAAoB,SACtB,MAAM,EAAqB,EAAkB,GAIjD,GAAgB;IACf;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,EAGI,KAAe,QAAkB;AACrC,IAAsB,GAAK;IAC1B,EAAE,CAAC,EAGA,CAAC,IAAqB,KAA0B,EAAS,GAAM,EAC/D,KAAwB,QAAkB;AAC9C,IAAuB,GAAK;IAC3B,EAAE,CAAC,EACA,KAAqB,QAAkB;AAC3C,IAAuB,GAAM;IAC5B,EAAE,CAAC,EAEA,KAAoB,QAAkB;AAC1C,IAAsB,GAAM;IAC3B,EAAE,CAAC,EAOA,KAAgB,EAAY,YAAY;AAC5C,MAAI,CAAC,KAAoB,CAAC,EAAa;EAOvC,IAAM,IAAU,MAAM,EAAmB;GACvC;GACA,MANkB,SADE,EAAiB,OAAO,UAAU,KAAK;GAQ5D,CAAC;AAEF,EAAI,MAEF,EAAoB;GAClB,GAAG;GACH,OAAO,CAAC,GAAI,EAAiB,SAAS,EAAE,EAAG,EAAQ;GACpD,CAAC,EAGF,EAAiB,EAAQ,GAAG;IAE7B;EAAC;EAAkB;EAAa;EAAqB;EAAmB,CAAC,EAGtE,KAAmB,EACvB,OAAO,GAAgB,MAAoB;AACpC,OAGe,MAAM,EAAmB;GAC3C,IAAI;GACJ,MAAM;GACP,CAAC,IAGA,EAAoB;GAClB,GAAG;GACH,OAAO,EAAiB,OAAO,KAAK,MAAU,EAAK,OAAO,IAAS;IAAE,GAAG;IAAM,MAAM;IAAS,GAAG,EAAM;GACvG,CAAC;IAGN;EAAC;EAAkB;EAAqB;EAAmB,CAC5D,EAGK,KAAsB,EAC1B,OAAO,MAAmB;AACxB,MAAI,CAAC,KAAoB,CAAC,EAAa;EAEvC,IAAM,IAAa,EAAiB,OAAO,MAAM,MAAM,EAAE,OAAO,EAAO;AACvE,MAAI,CAAC,EAAY;EAIjB,IAAM,IAAU,MAAM,EAAmB;GACvC;GACA,MAAM,GAAG,EAAW,KAAK;GAC1B,CAAC;AAEF,EAAI,MACF,EAAoB;GAClB,GAAG;GACH,OAAO,CAAC,GAAI,EAAiB,SAAS,EAAE,EAAG,EAAQ;GACpD,CAAC,EAEF,EAAiB,EAAQ,GAAG;IAIhC;EAAC;EAAkB;EAAa;EAAqB;EAAmB,CACzE,EAGK,KAAmB,EACvB,OAAO,MAAmB;AACpB,SAAC,MAAqB,EAAiB,OAAO,UAAU,MAAM,MAGlD,MAAM,EAAmB,EAAO,EAEnC;GACX,IAAM,IAAe,EAAiB,OAClC,QAAQ,MAAM,EAAE,OAAO,EAAO,CAC/B,KAAK,GAAG,OAAW;IAAE,GAAG;IAAG,OAAO;IAAO,EAAE;AAQ9C,GANA,EAAoB;IAClB,GAAG;IACH,OAAO;IACR,CAAC,EAGE,MAAkB,KACpB,EAAiB,IAAe,IAAI,MAAM,KAAK;;IAIrD;EAAC;EAAkB;EAAqB;EAAe;EAAmB,CAC3E,EAGK,KAAqB,EACzB,OAAO,MAAyE;AAC1E,GAAC,KAAoB,CAAC,MAG1B,EAAoB;GAClB,GAAG;GACH,OAAO,EAAe,KAAK,GAAM,OAAW;IAC1C,GAAG;IACH,OAAO;IACR,EAAE;GACJ,CAAC,EAIF,MAAM,EAAqB,GADX,EAAe,KAAK,MAAM,EAAE,GAAG,CACC;IAElD;EAAC;EAAkB;EAAa;EAAqB;EAAqB,CAC3E;AAqED,QAlEI,IAEA,kBAAC,OAAD;EAAK,WAAU;EAAmD,OAAO,EAAE,SAAS,4BAA4B;YAC9G,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,MAAD;KAAI,WAAU;eAAkD;KAA4B,CAAA;IAC5F,kBAAC,KAAD;KAAG,WAAU;eAAgD,EAAM;KAAY,CAAA;IAC/E,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAe,EAAa;MAC3C,WAAU;gBACX;MAEQ,CAAA,EACT,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,cAAc;MACtC,WAAU;gBACX;MAEQ,CAAA,CACL;;IACF;;EACF,CAAA,GAKN,IAEA,kBAAC,OAAD;EAAK,WAAU;EAAmD,OAAO,EAAE,SAAS,4BAA4B;YAC9G,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aACE,kBAAA,GAAA,EAAA,UAAA,CAAE,uEAEA,kBAAC,QAAD;IAAM,WAAU;cAAhB,CAA4E,QAAK,EAAmB;MACnG,EAAA,CAAA;GAEL,QACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,cAAc;IACtC,WAAU;cACX;IAEQ,CAAA;GAEX,CAAA;EACE,CAAA,GAKN,KAAW,CAAC,IAEZ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAK,WAAU,4FAA6F,CAAA,EAC5G,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAAwB,CAAA,CAC/D;;EACF,CAAA,GAKR,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,SAAS;MACT,SAAQ;MACR,MAAK;MACL,OAAM;MACN,cAAW;gBALb,CAOE,kBAAC,GAAD,EAAW,WAAU,gBAAiB,CAAA,EAAA,OAE/B;;KACT,kBAAC,MAAD;MAAI,WAAU;gBAAoD,EAAiB;MAAU,CAAA;KAE7F,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,IAAD;OAA0B;OAAQ,gBAAgB,GAAW,MAAS,CAAC,EAAK;OAAI,CAAA;MAC5E,CAAA;KACF;;GAGL,KACC,kBAAC,IAAD;IACE,oBAAoB,EAAiB;IACrC,sBAAsB,EAAiB;IACvC,UAAS;IACT,CAAA;GAIJ,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,IAAD;KACE,aAAa,EAAiB;KAC9B,QAAQ,KAAiB,EAAiB,QAAQ,IAAI;KACtD,OAAO,EAAiB;KACxB,SAAS;KACT,QAAQ;KACE;KACV,UAAU;KACV,mBAAmB;KAEnB,QAAQ,EAAiB,SAAS,EAAE,EAAE,KAAK,GAAM,OAAW;MAC1D,IAAI,EAAK;MACT,MAAM,EAAK,QAAQ,QAAQ,IAAQ;MACnC,OAAO,EAAK,SAAS;MACtB,EAAE;KACH,cAAc,KAAiB,EAAiB,QAAQ,IAAI;KAC5D,cAAc;KACd,WAAW;KACX,cAAc;KACd,iBAAiB;KACjB,cAAc;KACd,gBAAgB;KAChB,cAAc;KACd,CAAA;IACE,CAAA;GAGN,kBAAC,IAAD;IACE,QAAQ;IACR,aAAa,EAAiB;IAC9B,eAAe,EAAiB;IAChC,SAAS;IACT,CAAA;GAGF,kBAAC,IAAD;IACE,QAAQ;IACR,aAAa,EAAiB;IAC9B,eAAe,EAAiB;IAChC,sBAAsB,EAAiB;IACvC,SAAS;IACT,CAAA;GACE;;EAER"}