@camstack/addon-admin-ui 0.1.1

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.
Files changed (122) hide show
  1. package/index.html +22 -0
  2. package/package.json +69 -0
  3. package/public/brand/logo-dark.svg +16 -0
  4. package/public/brand/logo-horizontal-dark.svg +21 -0
  5. package/public/brand/logo-horizontal-light.svg +21 -0
  6. package/public/brand/logo-light.svg +16 -0
  7. package/public/brand/logo-wide-dark.svg +24 -0
  8. package/public/brand/logo-wide-light.svg +24 -0
  9. package/public/favicon.svg +8 -0
  10. package/public/vendor/react-jsx-runtime.mjs +24 -0
  11. package/public/vendor/react.mjs +16 -0
  12. package/src/App.tsx +71 -0
  13. package/src/components/addons/AddonCard.tsx +339 -0
  14. package/src/components/addons/AddonUploadZone.tsx +307 -0
  15. package/src/components/addons/CapabilityBadge.tsx +55 -0
  16. package/src/components/addons/CapabilityMap.tsx +133 -0
  17. package/src/components/addons/UpdatesList.tsx +119 -0
  18. package/src/components/agents/AgentCard.tsx +281 -0
  19. package/src/components/agents/AgentLogs.tsx +231 -0
  20. package/src/components/agents/ProcessList.tsx +127 -0
  21. package/src/components/agents/ProcessTree.tsx +369 -0
  22. package/src/components/agents/TaskList.tsx +68 -0
  23. package/src/components/cameras/CameraCard.tsx +60 -0
  24. package/src/components/cameras/LiveEventsPanel.tsx +91 -0
  25. package/src/components/cameras/ProviderSection.tsx +50 -0
  26. package/src/components/cameras/StreamArea.tsx +107 -0
  27. package/src/components/cameras/tabs/AddonsTab.tsx +113 -0
  28. package/src/components/cameras/tabs/CameraEventsTab.tsx +129 -0
  29. package/src/components/cameras/tabs/PipelineTab.tsx +118 -0
  30. package/src/components/cameras/tabs/StreamsTab.tsx +114 -0
  31. package/src/components/dashboard/BlockPicker.tsx +54 -0
  32. package/src/components/dashboard/BlockWrapper.tsx +97 -0
  33. package/src/components/dashboard/DashboardGrid.tsx +160 -0
  34. package/src/components/dashboard/block-registry.ts +15 -0
  35. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +39 -0
  36. package/src/components/dashboard/blocks/StorageBlock.tsx +66 -0
  37. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +67 -0
  38. package/src/components/dashboard/blocks/index.ts +32 -0
  39. package/src/components/device/DeviceHeader.tsx +116 -0
  40. package/src/components/device/FloatingPanel.tsx +132 -0
  41. package/src/components/device/FloatingPanelManager.tsx +167 -0
  42. package/src/components/device/PanelContent.tsx +196 -0
  43. package/src/components/device/QuickConfigWizard.tsx +507 -0
  44. package/src/components/device/tabs/DetectionConfigTab.tsx +96 -0
  45. package/src/components/device/tabs/EventsTab.tsx +19 -0
  46. package/src/components/device/tabs/LogsTab.tsx +22 -0
  47. package/src/components/device/tabs/OverviewTab.tsx +104 -0
  48. package/src/components/device/tabs/ProviderSettingsTab.tsx +34 -0
  49. package/src/components/device/tabs/RecordingTab.tsx +47 -0
  50. package/src/components/device/tabs/ReplTab.tsx +153 -0
  51. package/src/components/device/tabs/TrackTrailTab.tsx +49 -0
  52. package/src/components/device/tabs/ZonesTab.tsx +98 -0
  53. package/src/components/device/zone-editor/ZoneCanvas.tsx +354 -0
  54. package/src/components/device/zone-editor/ZoneForm.tsx +128 -0
  55. package/src/components/device/zone-editor/ZoneList.tsx +150 -0
  56. package/src/components/form-builder/FormBuilder.tsx +135 -0
  57. package/src/components/form-builder/FormField.tsx +732 -0
  58. package/src/components/form-builder/ModelSelector.tsx +239 -0
  59. package/src/components/integrations/AddDeviceDialog.tsx +205 -0
  60. package/src/components/integrations/CompactDeviceCard.tsx +35 -0
  61. package/src/components/integrations/DeviceCard.tsx +29 -0
  62. package/src/components/integrations/DeviceDiscoveryStep.tsx +105 -0
  63. package/src/components/integrations/DeviceGrid.tsx +79 -0
  64. package/src/components/integrations/DeviceGroupHeader.tsx +17 -0
  65. package/src/components/integrations/DiscoveredDeviceCard.tsx +26 -0
  66. package/src/components/integrations/IntegrationCard.tsx +40 -0
  67. package/src/components/integrations/IntegrationWizard.tsx +171 -0
  68. package/src/components/integrations/ProviderConfigForm.tsx +89 -0
  69. package/src/components/integrations/ProviderPicker.tsx +91 -0
  70. package/src/components/integrations/SnapshotPopover.tsx +68 -0
  71. package/src/components/metrics/AgentLoad.tsx +113 -0
  72. package/src/components/metrics/IntegrationUsage.tsx +90 -0
  73. package/src/components/metrics/PipelineStatus.tsx +105 -0
  74. package/src/components/metrics/ProcessResources.tsx +139 -0
  75. package/src/components/pipeline/PhaseSettings.tsx +131 -0
  76. package/src/components/shared/CapabilityBadges.tsx +30 -0
  77. package/src/components/shared/ProviderIcon.tsx +42 -0
  78. package/src/components/shared/StatusBadge.tsx +23 -0
  79. package/src/components/shared/WebRtcPlayer.tsx +211 -0
  80. package/src/components/timeline/EventMarker.tsx +32 -0
  81. package/src/components/timeline/TimelineBar.tsx +131 -0
  82. package/src/components/ui/ConfirmDialog.tsx +115 -0
  83. package/src/components/ui/ToastContainer.tsx +92 -0
  84. package/src/contexts/auth-context.tsx +91 -0
  85. package/src/hooks/useBackendClient.ts +6 -0
  86. package/src/hooks/useTheme.ts +1 -0
  87. package/src/i18n/en.json +164 -0
  88. package/src/i18n/index.ts +29 -0
  89. package/src/i18n/it.json +164 -0
  90. package/src/index.css +63 -0
  91. package/src/layouts/AddonPageLoader.tsx +120 -0
  92. package/src/layouts/AppLayout.tsx +238 -0
  93. package/src/layouts/ProtectedRoute.tsx +25 -0
  94. package/src/lib/addon-page-context.ts +29 -0
  95. package/src/lib/backend.ts +16 -0
  96. package/src/main.tsx +21 -0
  97. package/src/pages/AccessDenied.tsx +22 -0
  98. package/src/pages/Cameras.tsx +127 -0
  99. package/src/pages/Dashboard.tsx +6 -0
  100. package/src/pages/DeviceDetail.tsx +175 -0
  101. package/src/pages/IntegrationDetail.tsx +224 -0
  102. package/src/pages/Integrations.tsx +330 -0
  103. package/src/pages/Login.tsx +106 -0
  104. package/src/pages/Metrics.tsx +18 -0
  105. package/src/pages/PipelineConfig.tsx +282 -0
  106. package/src/pages/Showroom.tsx +351 -0
  107. package/src/pages/Timeline.tsx +269 -0
  108. package/src/pages/system/Addons.tsx +525 -0
  109. package/src/pages/system/Agents.tsx +362 -0
  110. package/src/pages/system/Logs.tsx +131 -0
  111. package/src/pages/system/Models.tsx +102 -0
  112. package/src/pages/system/Processes.tsx +129 -0
  113. package/src/pages/system/Repl.tsx +148 -0
  114. package/src/pages/system/Settings.tsx +168 -0
  115. package/src/pages/system/Users.tsx +174 -0
  116. package/src/server/addon.ts +54 -0
  117. package/src/types/config-ui.ts +210 -0
  118. package/src/types/dashboard.ts +39 -0
  119. package/tsconfig.json +29 -0
  120. package/tsconfig.server.json +16 -0
  121. package/tsup.config.ts +20 -0
  122. package/vite.config.ts +68 -0
@@ -0,0 +1,97 @@
1
+ import { useState } from 'react'
2
+ import { Settings, X, GripVertical } from 'lucide-react'
3
+ import type { DashboardBlock } from '../../types/dashboard'
4
+
5
+ interface BlockWrapperProps {
6
+ block: DashboardBlock
7
+ config: Record<string, unknown>
8
+ size: { w: number; h: number }
9
+ onRemove: () => void
10
+ onConfigChange: (config: Record<string, unknown>) => void
11
+ }
12
+
13
+ export function BlockWrapper({ block, config, size, onRemove, onConfigChange }: BlockWrapperProps) {
14
+ const [showConfig, setShowConfig] = useState(false)
15
+ const Component = block.component
16
+ const Icon = block.icon
17
+
18
+ return (
19
+ <div className="flex flex-col h-full rounded-lg border border-border bg-surface shadow-sm shadow-black/5 overflow-hidden hover:shadow-md hover:shadow-black/8 transition-shadow">
20
+ {/* Title bar */}
21
+ <div className="drag-handle flex items-center h-8 px-2 border-b border-border bg-surface/80 shrink-0 cursor-move group">
22
+ <GripVertical className="h-3 w-3 text-foreground-subtle/30 group-hover:text-foreground-subtle transition-colors mr-1" />
23
+ <Icon className="h-3 w-3 text-primary/70 mr-1.5 shrink-0" />
24
+ <span className="text-[11px] font-semibold text-foreground/80 truncate flex-1">
25
+ {block.name}
26
+ </span>
27
+ <div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
28
+ {block.configSchema && block.configSchema.length > 0 && (
29
+ <button
30
+ onClick={(e) => { e.stopPropagation(); setShowConfig(!showConfig) }}
31
+ className="p-1 rounded-md hover:bg-surface-hover text-foreground-subtle hover:text-foreground transition-colors"
32
+ >
33
+ <Settings className="h-3 w-3" />
34
+ </button>
35
+ )}
36
+ <button
37
+ onClick={(e) => { e.stopPropagation(); onRemove() }}
38
+ className="p-1 rounded-md hover:bg-danger/10 text-foreground-subtle hover:text-danger transition-colors"
39
+ >
40
+ <X className="h-3 w-3" />
41
+ </button>
42
+ </div>
43
+ </div>
44
+
45
+ {/* Config panel */}
46
+ {showConfig && block.configSchema && (
47
+ <div className="p-2.5 border-b border-border bg-background/50 space-y-2">
48
+ {block.configSchema.map((field) => (
49
+ <label key={field.key} className="flex items-center gap-2 text-[11px]">
50
+ <span className="text-foreground-subtle w-20 shrink-0">{field.label}</span>
51
+ {field.type === 'number' && (
52
+ <input
53
+ type="number"
54
+ value={(config[field.key] as number) ?? field.defaultValue ?? ''}
55
+ onChange={(e) => onConfigChange({ ...config, [field.key]: Number(e.target.value) })}
56
+ className="flex-1 rounded-md border border-border bg-surface px-2 py-1 text-foreground text-[11px] focus:border-primary outline-none"
57
+ />
58
+ )}
59
+ {field.type === 'text' && (
60
+ <input
61
+ type="text"
62
+ value={(config[field.key] as string) ?? field.defaultValue ?? ''}
63
+ onChange={(e) => onConfigChange({ ...config, [field.key]: e.target.value })}
64
+ className="flex-1 rounded-md border border-border bg-surface px-2 py-1 text-foreground text-[11px] focus:border-primary outline-none"
65
+ />
66
+ )}
67
+ {field.type === 'boolean' && (
68
+ <input
69
+ type="checkbox"
70
+ checked={(config[field.key] as boolean) ?? field.defaultValue ?? false}
71
+ onChange={(e) => onConfigChange({ ...config, [field.key]: e.target.checked })}
72
+ className="accent-primary"
73
+ />
74
+ )}
75
+ {field.type === 'select' && (
76
+ <select
77
+ value={(config[field.key] as string) ?? field.defaultValue ?? ''}
78
+ onChange={(e) => onConfigChange({ ...config, [field.key]: e.target.value })}
79
+ className="flex-1 rounded-md border border-border bg-surface px-2 py-1 text-foreground text-[11px] focus:border-primary outline-none"
80
+ >
81
+ {field.options?.map((opt) => (
82
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
83
+ ))}
84
+ </select>
85
+ )}
86
+ </label>
87
+ ))}
88
+ </div>
89
+ )}
90
+
91
+ {/* Block content */}
92
+ <div className="flex-1 overflow-auto p-3">
93
+ <Component config={config} size={size} />
94
+ </div>
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,160 @@
1
+ import { useCallback, useState } from 'react'
2
+ import { ResponsiveGridLayout } from 'react-grid-layout'
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ const GridLayout = ResponsiveGridLayout as any
6
+ import { Plus, LayoutDashboard } from 'lucide-react'
7
+ import { BlockWrapper } from './BlockWrapper'
8
+ import { BlockPicker } from './BlockPicker'
9
+ import { getBlock } from './block-registry'
10
+ import type { DashboardBlock, DashboardLayoutItem, DashboardState } from '../../types/dashboard'
11
+ import 'react-grid-layout/css/styles.css'
12
+
13
+ const STORAGE_KEY = 'camstack-dashboard-state'
14
+ const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 0 }
15
+ const COLS = { lg: 12, md: 8, sm: 4, xs: 2 }
16
+
17
+ function loadState(): DashboardState {
18
+ try {
19
+ const raw = localStorage.getItem(STORAGE_KEY)
20
+ if (raw) return JSON.parse(raw)
21
+ } catch { /* ignore */ }
22
+ return { items: [] }
23
+ }
24
+
25
+ function saveState(state: DashboardState): void {
26
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
27
+ }
28
+
29
+ let instanceCounter = 0
30
+
31
+ export function DashboardGrid() {
32
+ const [state, setState] = useState<DashboardState>(loadState)
33
+ const [pickerOpen, setPickerOpen] = useState(false)
34
+
35
+ const updateState = useCallback((next: DashboardState) => {
36
+ setState(next)
37
+ saveState(next)
38
+ }, [])
39
+
40
+ const handleAddBlock = useCallback((block: DashboardBlock) => {
41
+ instanceCounter++
42
+ const instanceId = `${block.id}-${instanceCounter}`
43
+ const defaultConfig: Record<string, unknown> = {}
44
+ block.configSchema?.forEach((f) => {
45
+ if (f.defaultValue !== undefined) defaultConfig[f.key] = f.defaultValue
46
+ })
47
+
48
+ const newItem: DashboardLayoutItem = {
49
+ i: instanceId,
50
+ blockId: block.id,
51
+ x: 0,
52
+ y: Infinity, // place at bottom
53
+ w: block.defaultSize.w,
54
+ h: block.defaultSize.h,
55
+ config: defaultConfig,
56
+ }
57
+
58
+ updateState({ items: [...state.items, newItem] })
59
+ }, [state, updateState])
60
+
61
+ const handleRemoveBlock = useCallback((instanceId: string) => {
62
+ updateState({ items: state.items.filter((item) => item.i !== instanceId) })
63
+ }, [state, updateState])
64
+
65
+ const handleConfigChange = useCallback((instanceId: string, config: Record<string, unknown>) => {
66
+ updateState({
67
+ items: state.items.map((item) =>
68
+ item.i === instanceId ? { ...item, config } : item,
69
+ ),
70
+ })
71
+ }, [state, updateState])
72
+
73
+ const handleLayoutChange = useCallback((layout: any) => {
74
+ updateState({
75
+ items: state.items.map((item) => {
76
+ const layoutItem = layout.find((l: any) => l.i === item.i)
77
+ if (!layoutItem) return item
78
+ return { ...item, x: layoutItem.x, y: layoutItem.y, w: layoutItem.w, h: layoutItem.h }
79
+ }),
80
+ })
81
+ }, [state, updateState])
82
+
83
+ const gridLayout = state.items.map((item) => {
84
+ const block = getBlock(item.blockId)
85
+ return {
86
+ i: item.i,
87
+ x: item.x,
88
+ y: item.y,
89
+ w: item.w,
90
+ h: item.h,
91
+ minW: block?.minSize?.w ?? 2,
92
+ minH: block?.minSize?.h ?? 2,
93
+ }
94
+ })
95
+
96
+ return (
97
+ <div className="p-4">
98
+ <div className="flex items-center justify-between mb-4">
99
+ <h1 className="text-lg font-semibold text-foreground">Dashboard</h1>
100
+ <button
101
+ onClick={() => setPickerOpen(true)}
102
+ className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground"
103
+ >
104
+ <Plus className="h-3.5 w-3.5" />
105
+ Add Block
106
+ </button>
107
+ </div>
108
+
109
+ {state.items.length === 0 ? (
110
+ <div className="flex flex-col items-center justify-center py-24 text-foreground-subtle">
111
+ <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-surface border border-border mb-4">
112
+ <LayoutDashboard className="h-8 w-8 text-foreground-subtle/40" />
113
+ </div>
114
+ <p className="text-sm font-medium text-foreground/70">Your dashboard is empty</p>
115
+ <p className="text-xs text-foreground-subtle mt-1 mb-4">Add blocks to monitor your cameras and system</p>
116
+ <button
117
+ onClick={() => setPickerOpen(true)}
118
+ className="flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-xs font-semibold text-primary-foreground shadow-md shadow-primary/20 hover:shadow-lg transition-all"
119
+ >
120
+ <Plus className="h-3.5 w-3.5" />
121
+ Add your first block
122
+ </button>
123
+ </div>
124
+ ) : (
125
+ <GridLayout
126
+ layouts={{ lg: gridLayout }}
127
+ breakpoints={BREAKPOINTS}
128
+ cols={COLS}
129
+ rowHeight={60}
130
+ draggableHandle=".drag-handle"
131
+ onLayoutChange={handleLayoutChange}
132
+ isResizable
133
+ isDraggable
134
+ >
135
+ {state.items.map((item) => {
136
+ const block = getBlock(item.blockId)
137
+ if (!block) return <div key={item.i} />
138
+ return (
139
+ <div key={item.i}>
140
+ <BlockWrapper
141
+ block={block}
142
+ config={item.config}
143
+ size={{ w: item.w, h: item.h }}
144
+ onRemove={() => handleRemoveBlock(item.i)}
145
+ onConfigChange={(config) => handleConfigChange(item.i, config)}
146
+ />
147
+ </div>
148
+ )
149
+ })}
150
+ </GridLayout>
151
+ )}
152
+
153
+ <BlockPicker
154
+ open={pickerOpen}
155
+ onClose={() => setPickerOpen(false)}
156
+ onSelect={handleAddBlock}
157
+ />
158
+ </div>
159
+ )
160
+ }
@@ -0,0 +1,15 @@
1
+ import type { DashboardBlock } from '../../types/dashboard'
2
+
3
+ const registry = new Map<string, DashboardBlock>()
4
+
5
+ export function registerBlock(block: DashboardBlock): void {
6
+ registry.set(block.id, block)
7
+ }
8
+
9
+ export function getBlock(id: string): DashboardBlock | undefined {
10
+ return registry.get(id)
11
+ }
12
+
13
+ export function getAllBlocks(): DashboardBlock[] {
14
+ return Array.from(registry.values())
15
+ }
@@ -0,0 +1,39 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { useBackendClient } from '../../../hooks/useBackendClient'
3
+ import type { BlockProps } from '../../../types/dashboard'
4
+
5
+ const STAGE_ORDER = [
6
+ 'class-filter', 'tracker', 'sub-detection', 'recognition',
7
+ 'zone-analysis', 'event-generation', 'object-snapshot',
8
+ ]
9
+
10
+ export function PipelineStagesBlock({ config, size }: BlockProps) {
11
+ const client = useBackendClient()
12
+ const { data, isLoading, error } = useQuery({
13
+ queryKey: ['pipelines'],
14
+ queryFn: () => client.listPipelines(),
15
+ refetchInterval: 5_000,
16
+ })
17
+
18
+ if (isLoading) return <div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
19
+ if (error) return <div className="text-xs text-danger">Failed to load pipelines</div>
20
+
21
+ const pipelines = (data ?? []) as Array<Record<string, unknown>>
22
+
23
+ return (
24
+ <div className="space-y-2 text-xs">
25
+ <div className="flex justify-between text-foreground-subtle">
26
+ <span>Active pipelines</span>
27
+ <span className="font-medium text-foreground">{pipelines.length}</span>
28
+ </div>
29
+ <div className="space-y-0.5">
30
+ {STAGE_ORDER.map((stage) => (
31
+ <div key={stage} className="flex items-center gap-1.5">
32
+ <div className="h-1.5 w-1.5 rounded-full bg-success shrink-0" />
33
+ <span className="text-foreground-subtle">{stage}</span>
34
+ </div>
35
+ ))}
36
+ </div>
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,66 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { useBackendClient } from '../../../hooks/useBackendClient'
3
+ import type { BlockProps } from '../../../types/dashboard'
4
+
5
+ interface SystemInfo {
6
+ version?: string
7
+ uptime?: number
8
+ nodeVersion?: string
9
+ platform?: string
10
+ [key: string]: unknown
11
+ }
12
+
13
+ function formatUptime(seconds: number): string {
14
+ if (seconds < 60) return `${Math.floor(seconds)}s`
15
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`
16
+ const h = Math.floor(seconds / 3600)
17
+ const m = Math.floor((seconds % 3600) / 60)
18
+ return `${h}h ${m}m`
19
+ }
20
+
21
+ export function StorageBlock({ config: _config, size: _size }: BlockProps) {
22
+ const client = useBackendClient()
23
+
24
+ const { data: systemInfo, isLoading, error } = useQuery<SystemInfo>({
25
+ queryKey: ['system-info'],
26
+ queryFn: async () => {
27
+ const result = await client.getSystemInfo()
28
+ return result as SystemInfo
29
+ },
30
+ refetchInterval: 30_000,
31
+ })
32
+
33
+ const { data: addons } = useQuery({
34
+ queryKey: ['storage-addons'],
35
+ queryFn: () => client.listAddons(),
36
+ refetchInterval: 60_000,
37
+ })
38
+
39
+ if (isLoading) return <div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
40
+ if (error) return <div className="text-xs text-danger">Failed to load system info</div>
41
+
42
+ const addonList = (addons ?? []) as Array<{ manifest: { id: string } }>
43
+
44
+ return (
45
+ <div className="space-y-1.5 text-xs">
46
+ <div className="flex justify-between">
47
+ <span className="text-foreground-subtle">Platform</span>
48
+ <span className="text-foreground font-medium">{systemInfo?.platform ?? 'N/A'}</span>
49
+ </div>
50
+ <div className="flex justify-between">
51
+ <span className="text-foreground-subtle">Uptime</span>
52
+ <span className="text-foreground font-medium">
53
+ {systemInfo?.uptime != null ? formatUptime(systemInfo.uptime) : 'N/A'}
54
+ </span>
55
+ </div>
56
+ <div className="flex justify-between">
57
+ <span className="text-foreground-subtle">Node</span>
58
+ <span className="text-foreground font-medium font-mono">{systemInfo?.nodeVersion ?? 'N/A'}</span>
59
+ </div>
60
+ <div className="flex justify-between">
61
+ <span className="text-foreground-subtle">Addons</span>
62
+ <span className="text-foreground font-medium">{addonList.length} installed</span>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
@@ -0,0 +1,67 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { CheckCircle, AlertTriangle, Loader2 } from 'lucide-react'
3
+ import { useBackendClient } from '../../../hooks/useBackendClient'
4
+ import type { BlockProps } from '../../../types/dashboard'
5
+
6
+ export function SystemStatusBlock({ config, size }: BlockProps) {
7
+ const client = useBackendClient()
8
+ const { data, isLoading, error } = useQuery({
9
+ queryKey: ['system-info'],
10
+ queryFn: () => client.getSystemInfo(),
11
+ refetchInterval: 10_000,
12
+ })
13
+
14
+ if (isLoading) {
15
+ return (
16
+ <div className="flex items-center gap-2 text-xs text-foreground-subtle">
17
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
18
+ <span>Connecting to server...</span>
19
+ </div>
20
+ )
21
+ }
22
+
23
+ if (error) {
24
+ return (
25
+ <div className="flex items-center gap-2 text-xs text-danger">
26
+ <AlertTriangle className="h-3.5 w-3.5" />
27
+ <span>Unable to reach server</span>
28
+ </div>
29
+ )
30
+ }
31
+
32
+ const info = data as Record<string, unknown> | undefined
33
+
34
+ return (
35
+ <div className="space-y-2 text-xs">
36
+ <div className="flex items-center gap-1.5 mb-2">
37
+ <CheckCircle className="h-3.5 w-3.5 text-success" />
38
+ <span className="text-success font-medium">Online</span>
39
+ </div>
40
+ <Row label="Version" value={String(info?.version ?? 'N/A')} />
41
+ <Row label="Uptime" value={formatUptime(info?.uptime as number)} />
42
+ <Row label="Memory" value={`${formatMB(info?.memoryUsage as number)} MB`} />
43
+ <Row label="Platform" value={String(info?.platform ?? 'N/A')} />
44
+ </div>
45
+ )
46
+ }
47
+
48
+ function Row({ label, value }: { label: string; value: string }) {
49
+ return (
50
+ <div className="flex justify-between items-center">
51
+ <span className="text-foreground-subtle">{label}</span>
52
+ <span className="text-foreground font-medium tabular-nums">{value}</span>
53
+ </div>
54
+ )
55
+ }
56
+
57
+ function formatUptime(seconds?: number): string {
58
+ if (!seconds) return 'N/A'
59
+ const h = Math.floor(seconds / 3600)
60
+ const m = Math.floor((seconds % 3600) / 60)
61
+ return h > 0 ? `${h}h ${m}m` : `${m}m`
62
+ }
63
+
64
+ function formatMB(bytes?: number): string {
65
+ if (!bytes) return 'N/A'
66
+ return (bytes / 1024 / 1024).toFixed(0)
67
+ }
@@ -0,0 +1,32 @@
1
+ import { Monitor, Workflow, HardDrive } from 'lucide-react'
2
+ import { registerBlock } from '../block-registry'
3
+ import { SystemStatusBlock } from './SystemStatusBlock'
4
+ import { PipelineStagesBlock } from './PipelineStagesBlock'
5
+ import { StorageBlock } from './StorageBlock'
6
+
7
+ registerBlock({
8
+ id: 'system-status',
9
+ name: 'System Status',
10
+ icon: Monitor,
11
+ defaultSize: { w: 4, h: 2 },
12
+ minSize: { w: 2, h: 2 },
13
+ component: SystemStatusBlock,
14
+ })
15
+
16
+ registerBlock({
17
+ id: 'pipeline-stages',
18
+ name: 'Pipeline Stages',
19
+ icon: Workflow,
20
+ defaultSize: { w: 4, h: 3 },
21
+ minSize: { w: 3, h: 2 },
22
+ component: PipelineStagesBlock,
23
+ })
24
+
25
+ registerBlock({
26
+ id: 'storage',
27
+ name: 'Storage',
28
+ icon: HardDrive,
29
+ defaultSize: { w: 4, h: 2 },
30
+ minSize: { w: 2, h: 2 },
31
+ component: StorageBlock,
32
+ })
@@ -0,0 +1,116 @@
1
+ import { useState } from 'react'
2
+ import { Link } from 'react-router-dom'
3
+ import { ChevronRight, Video, ScrollText, Terminal, Bell, Zap } from 'lucide-react'
4
+ import { StatusBadge } from '../shared/StatusBadge'
5
+ import { ProviderIcon, getProviderLabel } from '../shared/ProviderIcon'
6
+ import { CapabilityBadges } from '../shared/CapabilityBadges'
7
+ import { QuickConfigWizard } from './QuickConfigWizard'
8
+ import type { PanelId } from './FloatingPanelManager'
9
+
10
+ interface DeviceHeaderProps {
11
+ deviceId: string
12
+ name: string
13
+ status: string
14
+ provider: string
15
+ capabilities: string[]
16
+ activePanel: PanelId | null
17
+ onTogglePanel: (panel: PanelId) => void
18
+ }
19
+
20
+ const PANEL_BUTTONS: Array<{
21
+ id: PanelId
22
+ icon: React.ComponentType<{ className?: string }>
23
+ label: string
24
+ }> = [
25
+ { id: 'stream', icon: Video, label: 'Stream' },
26
+ { id: 'logs', icon: ScrollText, label: 'Logs' },
27
+ { id: 'repl', icon: Terminal, label: 'REPL' },
28
+ { id: 'events', icon: Bell, label: 'Events' },
29
+ ]
30
+
31
+ export function DeviceHeader({
32
+ deviceId,
33
+ name,
34
+ status,
35
+ provider,
36
+ capabilities,
37
+ activePanel,
38
+ onTogglePanel,
39
+ }: DeviceHeaderProps) {
40
+ const [wizardOpen, setWizardOpen] = useState(false)
41
+
42
+ return (
43
+ <>
44
+ <div className="border-b border-border bg-surface px-6 py-4">
45
+ {/* Breadcrumb */}
46
+ <nav className="flex items-center gap-1 text-[11px] text-foreground-subtle mb-3">
47
+ <Link to="/integrations" className="hover:text-foreground transition-colors">
48
+ Integrations
49
+ </Link>
50
+ <ChevronRight className="h-3 w-3" />
51
+ <span className="text-foreground-subtle">{getProviderLabel(provider)}</span>
52
+ <ChevronRight className="h-3 w-3" />
53
+ <span className="text-foreground">{name}</span>
54
+ </nav>
55
+
56
+ {/* Header row */}
57
+ <div className="flex items-center justify-between gap-4">
58
+ <div className="flex items-center gap-3 min-w-0">
59
+ <ProviderIcon type={provider} size="lg" />
60
+ <div className="min-w-0">
61
+ <div className="flex items-center gap-2">
62
+ <h1 className="text-base font-semibold text-foreground truncate">{name}</h1>
63
+ <StatusBadge status={status} />
64
+ </div>
65
+ <div className="mt-1">
66
+ <CapabilityBadges capabilities={capabilities} />
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ {/* Action buttons */}
72
+ <div className="flex items-center gap-1 flex-shrink-0">
73
+ {/* Quick Config wizard button */}
74
+ <button
75
+ onClick={() => setWizardOpen(true)}
76
+ title="Quick Config"
77
+ className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[11px] font-medium border border-border text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
78
+ >
79
+ <Zap className="h-3.5 w-3.5" />
80
+ Quick Config
81
+ </button>
82
+
83
+ {/* Divider */}
84
+ <div className="h-5 w-px bg-border mx-1" />
85
+
86
+ {/* Floating panel toggle buttons */}
87
+ {PANEL_BUTTONS.map(({ id, icon: Icon, label }) => {
88
+ const isActive = activePanel === id
89
+ return (
90
+ <button
91
+ key={id}
92
+ onClick={() => onTogglePanel(id)}
93
+ title={label}
94
+ className={`inline-flex items-center justify-center rounded-lg p-2 transition-colors ${
95
+ isActive
96
+ ? 'bg-primary/15 text-primary'
97
+ : 'text-foreground-subtle hover:text-foreground hover:bg-surface-hover'
98
+ }`}
99
+ >
100
+ <Icon className="h-4 w-4" />
101
+ </button>
102
+ )
103
+ })}
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ {/* Quick Config Wizard (modal) */}
109
+ <QuickConfigWizard
110
+ open={wizardOpen}
111
+ onClose={() => setWizardOpen(false)}
112
+ deviceId={deviceId}
113
+ />
114
+ </>
115
+ )
116
+ }