@camstack/addon-admin-ui 0.1.2 → 0.1.3

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 (127) hide show
  1. package/dist/assets/index-DjELGD4R.css +1 -0
  2. package/dist/assets/index-w55PwKyu.js +598 -0
  3. package/{index.html → dist/index.html} +3 -1
  4. package/dist/server/addon.d.ts +11 -0
  5. package/dist/server/addon.js +50 -0
  6. package/dist/server/addon.js.map +1 -0
  7. package/package.json +4 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -355
  10. package/src/components/addons/AddonUploadZone.tsx +0 -69
  11. package/src/components/addons/CapabilityBadge.tsx +0 -55
  12. package/src/components/addons/CapabilityMap.tsx +0 -133
  13. package/src/components/addons/UpdatesList.tsx +0 -108
  14. package/src/components/agents/AgentCard.tsx +0 -281
  15. package/src/components/agents/AgentLogs.tsx +0 -231
  16. package/src/components/agents/ProcessList.tsx +0 -127
  17. package/src/components/agents/ProcessTree.tsx +0 -369
  18. package/src/components/agents/TaskList.tsx +0 -68
  19. package/src/components/cameras/CameraCard.tsx +0 -60
  20. package/src/components/cameras/LiveEventsPanel.tsx +0 -91
  21. package/src/components/cameras/ProviderSection.tsx +0 -50
  22. package/src/components/cameras/StreamArea.tsx +0 -107
  23. package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
  24. package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
  25. package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
  26. package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
  27. package/src/components/dashboard/BlockPicker.tsx +0 -54
  28. package/src/components/dashboard/BlockWrapper.tsx +0 -97
  29. package/src/components/dashboard/DashboardGrid.tsx +0 -160
  30. package/src/components/dashboard/block-registry.ts +0 -15
  31. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
  32. package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
  33. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
  34. package/src/components/dashboard/blocks/index.ts +0 -32
  35. package/src/components/device/DeviceHeader.tsx +0 -116
  36. package/src/components/device/FloatingPanel.tsx +0 -132
  37. package/src/components/device/FloatingPanelManager.tsx +0 -167
  38. package/src/components/device/PanelContent.tsx +0 -196
  39. package/src/components/device/QuickConfigWizard.tsx +0 -507
  40. package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
  41. package/src/components/device/tabs/EventsTab.tsx +0 -19
  42. package/src/components/device/tabs/LogsTab.tsx +0 -22
  43. package/src/components/device/tabs/OverviewTab.tsx +0 -104
  44. package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
  45. package/src/components/device/tabs/RecordingTab.tsx +0 -47
  46. package/src/components/device/tabs/ReplTab.tsx +0 -153
  47. package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
  48. package/src/components/device/tabs/ZonesTab.tsx +0 -98
  49. package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
  50. package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
  51. package/src/components/device/zone-editor/ZoneList.tsx +0 -150
  52. package/src/components/form-builder/FormBuilder.tsx +0 -135
  53. package/src/components/form-builder/FormField.tsx +0 -732
  54. package/src/components/form-builder/ModelSelector.tsx +0 -239
  55. package/src/components/integrations/AddDeviceDialog.tsx +0 -205
  56. package/src/components/integrations/CompactDeviceCard.tsx +0 -35
  57. package/src/components/integrations/DeviceCard.tsx +0 -29
  58. package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
  59. package/src/components/integrations/DeviceGrid.tsx +0 -79
  60. package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
  61. package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
  62. package/src/components/integrations/IntegrationCard.tsx +0 -40
  63. package/src/components/integrations/IntegrationWizard.tsx +0 -172
  64. package/src/components/integrations/ProviderConfigForm.tsx +0 -89
  65. package/src/components/integrations/ProviderPicker.tsx +0 -91
  66. package/src/components/integrations/SnapshotPopover.tsx +0 -68
  67. package/src/components/metrics/AgentLoad.tsx +0 -105
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -73
  69. package/src/components/metrics/PipelineStatus.tsx +0 -74
  70. package/src/components/metrics/ProcessResources.tsx +0 -123
  71. package/src/components/pipeline/PhaseSettings.tsx +0 -131
  72. package/src/components/shared/CapabilityBadges.tsx +0 -30
  73. package/src/components/shared/ProviderIcon.tsx +0 -42
  74. package/src/components/shared/StatusBadge.tsx +0 -23
  75. package/src/components/shared/WebRtcPlayer.tsx +0 -211
  76. package/src/components/timeline/EventMarker.tsx +0 -32
  77. package/src/components/timeline/TimelineBar.tsx +0 -131
  78. package/src/components/ui/ConfirmDialog.tsx +0 -115
  79. package/src/components/ui/ToastContainer.tsx +0 -92
  80. package/src/contexts/auth-context.tsx +0 -91
  81. package/src/hooks/useBackendClient.ts +0 -6
  82. package/src/hooks/useTheme.ts +0 -1
  83. package/src/i18n/en.json +0 -164
  84. package/src/i18n/index.ts +0 -29
  85. package/src/i18n/it.json +0 -164
  86. package/src/index.css +0 -63
  87. package/src/layouts/AddonPageLoader.tsx +0 -120
  88. package/src/layouts/AppLayout.tsx +0 -254
  89. package/src/layouts/ProtectedRoute.tsx +0 -25
  90. package/src/lib/addon-page-context.ts +0 -29
  91. package/src/lib/backend.ts +0 -16
  92. package/src/main.tsx +0 -21
  93. package/src/pages/AccessDenied.tsx +0 -22
  94. package/src/pages/Cameras.tsx +0 -127
  95. package/src/pages/Dashboard.tsx +0 -6
  96. package/src/pages/DeviceDetail.tsx +0 -175
  97. package/src/pages/IntegrationDetail.tsx +0 -222
  98. package/src/pages/Integrations.tsx +0 -333
  99. package/src/pages/Login.tsx +0 -106
  100. package/src/pages/Metrics.tsx +0 -18
  101. package/src/pages/PipelineConfig.tsx +0 -282
  102. package/src/pages/Showroom.tsx +0 -351
  103. package/src/pages/Timeline.tsx +0 -269
  104. package/src/pages/system/Addons.tsx +0 -396
  105. package/src/pages/system/Agents.tsx +0 -362
  106. package/src/pages/system/Logs.tsx +0 -131
  107. package/src/pages/system/Models.tsx +0 -102
  108. package/src/pages/system/Processes.tsx +0 -129
  109. package/src/pages/system/Repl.tsx +0 -148
  110. package/src/pages/system/Settings.tsx +0 -168
  111. package/src/pages/system/Users.tsx +0 -174
  112. package/src/server/addon.ts +0 -54
  113. package/src/types/config-ui.ts +0 -28
  114. package/src/types/dashboard.ts +0 -39
  115. package/tsconfig.json +0 -29
  116. package/tsconfig.server.json +0 -16
  117. package/tsup.config.ts +0 -20
  118. package/vite.config.ts +0 -68
  119. /package/{public → dist}/brand/logo-dark.svg +0 -0
  120. /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
  121. /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
  122. /package/{public → dist}/brand/logo-light.svg +0 -0
  123. /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
  124. /package/{public → dist}/brand/logo-wide-light.svg +0 -0
  125. /package/{public → dist}/favicon.svg +0 -0
  126. /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
  127. /package/{public → dist}/vendor/react.mjs +0 -0
@@ -1,129 +0,0 @@
1
- import { useState } from 'react'
2
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
- import { RotateCcw } from 'lucide-react'
4
- import { useBackendClient } from '../../hooks/useBackendClient'
5
- import { StatusBadge } from '../../components/shared/StatusBadge'
6
-
7
- export function ProcessesPage() {
8
- const client = useBackendClient()
9
- const queryClient = useQueryClient()
10
- const [confirmingId, setConfirmingId] = useState<string | null>(null)
11
-
12
- const { data: processes, isLoading, isError } = useQuery({
13
- queryKey: ['processes'],
14
- queryFn: () => client.listProcesses(),
15
- refetchInterval: 5000,
16
- })
17
-
18
- const restartMutation = useMutation({
19
- mutationFn: (id: string) => client.trpc.processes.restartProcess.mutate({ id }),
20
- onSuccess: () => {
21
- queryClient.invalidateQueries({ queryKey: ['processes'] })
22
- setConfirmingId(null)
23
- },
24
- })
25
-
26
- const processList = (processes ?? []) as unknown as Array<Record<string, unknown>>
27
-
28
- function formatUptime(seconds: number): string {
29
- if (seconds < 60) return `${seconds}s`
30
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`
31
- const h = Math.floor(seconds / 3600)
32
- const m = Math.floor((seconds % 3600) / 60)
33
- return `${h}h ${m}m`
34
- }
35
-
36
- return (
37
- <div className="p-6 space-y-4">
38
- <div className="flex items-center justify-between">
39
- <h1 className="text-lg font-semibold text-foreground">Processes</h1>
40
- </div>
41
-
42
- {isLoading && (
43
- <div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
44
- )}
45
-
46
- {isError && (
47
- <div className="text-xs text-danger">Failed to load</div>
48
- )}
49
-
50
- {!isLoading && !isError && processList.length === 0 && (
51
- <div className="text-xs text-foreground-subtle">No data</div>
52
- )}
53
-
54
- {processList.length > 0 && (
55
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
56
- <table className="w-full text-xs">
57
- <thead>
58
- <tr>
59
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Name</th>
60
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Status</th>
61
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">CPU %</th>
62
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Memory MB</th>
63
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Uptime</th>
64
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Restarts</th>
65
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Actions</th>
66
- </tr>
67
- </thead>
68
- <tbody>
69
- {processList.map((proc) => {
70
- const id = String(proc.id ?? proc.name ?? '')
71
- const name = String(proc.name ?? proc.id ?? '')
72
- const status = String(proc.status ?? proc.state ?? 'stopped')
73
- const cpu = Number(proc.cpu ?? proc.cpuPercent ?? 0).toFixed(1)
74
- const memory = Number(proc.memory ?? proc.memoryMb ?? proc.memoryBytes != null
75
- ? Number(proc.memoryBytes ?? 0) / 1024 / 1024
76
- : proc.memoryMb ?? 0).toFixed(1)
77
- const uptime = formatUptime(Number(proc.uptime ?? proc.uptimeSeconds ?? 0))
78
- const restarts = Number(proc.restarts ?? proc.restartCount ?? 0)
79
- const isConfirming = confirmingId === id
80
-
81
- return (
82
- <tr key={id} className="hover:bg-primary/5">
83
- <td className="px-3 py-2 text-foreground border-b border-border font-mono">
84
- {name}
85
- </td>
86
- <td className="px-3 py-2 text-foreground border-b border-border">
87
- <StatusBadge status={status} />
88
- </td>
89
- <td className="px-3 py-2 text-foreground border-b border-border">{cpu}%</td>
90
- <td className="px-3 py-2 text-foreground border-b border-border">{memory} MB</td>
91
- <td className="px-3 py-2 text-foreground border-b border-border">{uptime}</td>
92
- <td className="px-3 py-2 text-foreground border-b border-border">{restarts}</td>
93
- <td className="px-3 py-2 text-foreground border-b border-border">
94
- {isConfirming ? (
95
- <div className="flex items-center gap-1.5">
96
- <button
97
- onClick={() => restartMutation.mutate(id)}
98
- disabled={restartMutation.isPending}
99
- className="rounded px-2 py-0.5 text-[10px] bg-danger/10 text-danger hover:bg-danger/20"
100
- >
101
- Confirm
102
- </button>
103
- <button
104
- onClick={() => setConfirmingId(null)}
105
- className="rounded px-2 py-0.5 text-[10px] bg-surface text-foreground-subtle hover:text-foreground border border-border"
106
- >
107
- Cancel
108
- </button>
109
- </div>
110
- ) : (
111
- <button
112
- onClick={() => setConfirmingId(id)}
113
- className="inline-flex items-center gap-1 rounded px-2 py-0.5 text-[10px] bg-primary/10 text-primary hover:bg-primary/20"
114
- >
115
- <RotateCcw className="h-3 w-3" />
116
- Restart
117
- </button>
118
- )}
119
- </td>
120
- </tr>
121
- )
122
- })}
123
- </tbody>
124
- </table>
125
- </div>
126
- )}
127
- </div>
128
- )
129
- }
@@ -1,148 +0,0 @@
1
- import { useState, useRef } from 'react'
2
- import { useMutation } from '@tanstack/react-query'
3
- import { Play, ChevronUp, Trash2 } from 'lucide-react'
4
- import { useBackendClient } from '../../hooks/useBackendClient'
5
-
6
- interface HistoryEntry {
7
- id: number
8
- code: string
9
- result: string
10
- error: boolean
11
- ts: string
12
- }
13
-
14
- let historyCounter = 0
15
-
16
- export function ReplPage() {
17
- const client = useBackendClient()
18
- const [code, setCode] = useState('')
19
- const [history, setHistory] = useState<HistoryEntry[]>([])
20
- const textareaRef = useRef<HTMLTextAreaElement>(null)
21
-
22
- const evalMutation = useMutation({
23
- mutationFn: (src: string) => client.replEval(src),
24
- onSuccess: (data, src) => {
25
- const result = typeof data === 'string' ? data : JSON.stringify(data, null, 2)
26
- setHistory((prev) => [
27
- ...prev,
28
- {
29
- id: ++historyCounter,
30
- code: src,
31
- result,
32
- error: false,
33
- ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
34
- },
35
- ])
36
- setCode('')
37
- },
38
- onError: (err: unknown, src) => {
39
- const result = err instanceof Error ? err.message : String(err)
40
- setHistory((prev) => [
41
- ...prev,
42
- {
43
- id: ++historyCounter,
44
- code: src,
45
- result,
46
- error: true,
47
- ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
48
- },
49
- ])
50
- },
51
- })
52
-
53
- function handleExecute() {
54
- const trimmed = code.trim()
55
- if (!trimmed) return
56
- evalMutation.mutate(trimmed)
57
- }
58
-
59
- function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
60
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
61
- e.preventDefault()
62
- handleExecute()
63
- }
64
- }
65
-
66
- function reuseCode(src: string) {
67
- setCode(src)
68
- textareaRef.current?.focus()
69
- }
70
-
71
- return (
72
- <div className="p-6 space-y-4">
73
- <div className="flex items-center justify-between">
74
- <h1 className="text-lg font-semibold text-foreground">REPL</h1>
75
- {history.length > 0 && (
76
- <button
77
- onClick={() => setHistory([])}
78
- className="inline-flex items-center gap-1 text-[10px] text-foreground-subtle hover:text-foreground"
79
- >
80
- <Trash2 className="h-3 w-3" />
81
- Clear history
82
- </button>
83
- )}
84
- </div>
85
-
86
- {/* Input */}
87
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
88
- <div className="flex items-center justify-between border-b border-border px-3 py-1.5">
89
- <span className="text-[10px] text-foreground-subtle font-mono">JavaScript — Ctrl+Enter to run</span>
90
- <button
91
- onClick={handleExecute}
92
- disabled={evalMutation.isPending || !code.trim()}
93
- className="inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-[10px] font-medium bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
94
- >
95
- <Play className="h-3 w-3" />
96
- {evalMutation.isPending ? 'Running...' : 'Execute'}
97
- </button>
98
- </div>
99
- <textarea
100
- ref={textareaRef}
101
- value={code}
102
- onChange={(e) => setCode(e.target.value)}
103
- onKeyDown={handleKeyDown}
104
- rows={6}
105
- placeholder="// Enter JavaScript code here..."
106
- className="w-full resize-y bg-background p-3 font-mono text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none"
107
- spellCheck={false}
108
- />
109
- </div>
110
-
111
- {/* History */}
112
- {history.length === 0 && (
113
- <div className="text-xs text-foreground-subtle">No evaluations yet</div>
114
- )}
115
-
116
- {history.length > 0 && (
117
- <div className="space-y-3">
118
- {[...history].reverse().map((entry) => (
119
- <div key={entry.id} className="rounded-lg border border-border bg-surface overflow-hidden">
120
- {/* Code input */}
121
- <div className="flex items-start justify-between gap-2 border-b border-border bg-background px-3 py-2">
122
- <pre className="font-mono text-xs text-foreground whitespace-pre-wrap break-all flex-1">{entry.code}</pre>
123
- <div className="flex items-center gap-1.5 flex-shrink-0">
124
- <span className="text-[10px] text-foreground-subtle">{entry.ts}</span>
125
- <button
126
- onClick={() => reuseCode(entry.code)}
127
- title="Reuse"
128
- className="text-foreground-subtle hover:text-foreground"
129
- >
130
- <ChevronUp className="h-3.5 w-3.5" />
131
- </button>
132
- </div>
133
- </div>
134
- {/* Result */}
135
- <pre
136
- className={`px-3 py-2 font-mono text-xs whitespace-pre-wrap break-all ${
137
- entry.error ? 'text-danger' : 'text-success'
138
- }`}
139
- >
140
- {entry.result}
141
- </pre>
142
- </div>
143
- ))}
144
- </div>
145
- )}
146
- </div>
147
- )
148
- }
@@ -1,168 +0,0 @@
1
- import { useState } from 'react'
2
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
- import { useBackendClient } from '../../hooks/useBackendClient'
4
- import { FormBuilder } from '../../components/form-builder/FormBuilder'
5
- import type { ConfigUISchema } from '../../types/config-ui'
6
-
7
- // ---------------------------------------------------------------------------
8
- // SectionPanel — fetches schema + values from backend
9
- // ---------------------------------------------------------------------------
10
-
11
- interface SectionPanelProps {
12
- section: string
13
- }
14
-
15
- function SectionPanel({ section }: SectionPanelProps) {
16
- const client = useBackendClient()
17
- const queryClient = useQueryClient()
18
- const [localValues, setLocalValues] = useState<Record<string, unknown> | null>(null)
19
- const [saveSuccess, setSaveSuccess] = useState(false)
20
- const [saveError, setSaveError] = useState<string | null>(null)
21
-
22
- // Fetch schema from backend
23
- const schemaQuery = useQuery({
24
- queryKey: ['settings-schema', section],
25
- queryFn: () => client.getSettingsSchema(section as Parameters<typeof client.getSettingsSchema>[0]),
26
- staleTime: Infinity, // schemas don't change at runtime
27
- })
28
-
29
- // Fetch values
30
- const valuesQuery = useQuery({
31
- queryKey: ['settings', section],
32
- queryFn: () => client.getSettings(section as Parameters<typeof client.getSettings>[0]),
33
- })
34
-
35
- const schema = (schemaQuery.data as { schema?: ConfigUISchema } | undefined)?.schema
36
- const isReadOnly = (schemaQuery.data as { readOnly?: boolean } | undefined)?.readOnly ?? false
37
-
38
- const remoteValues = ((valuesQuery.data?.data ?? {}) as Record<string, unknown>)
39
- const values = localValues ?? remoteValues
40
-
41
- const isDirty = localValues !== null
42
-
43
- const updateMutation = useMutation({
44
- mutationFn: (updated: Record<string, unknown>) =>
45
- client.updateSettings(section as Parameters<typeof client.updateSettings>[0], updated),
46
- onSuccess: () => {
47
- setSaveSuccess(true)
48
- setSaveError(null)
49
- setLocalValues(null)
50
- void queryClient.invalidateQueries({ queryKey: ['settings', section] })
51
- setTimeout(() => setSaveSuccess(false), 2000)
52
- },
53
- onError: (err: unknown) => {
54
- setSaveError(err instanceof Error ? err.message : 'Failed to save')
55
- },
56
- })
57
-
58
- if (schemaQuery.isLoading || valuesQuery.isLoading) {
59
- return <div className="text-xs text-foreground-subtle animate-pulse py-4">Loading...</div>
60
- }
61
-
62
- if (schemaQuery.isError || valuesQuery.isError) {
63
- return <div className="text-xs text-danger py-4">Failed to load settings</div>
64
- }
65
-
66
- if (!schema || schema.sections.length === 0) {
67
- return <div className="text-xs text-foreground-subtle py-4">No configurable settings for this section.</div>
68
- }
69
-
70
- return (
71
- <div className="space-y-4">
72
- <FormBuilder
73
- schema={schema}
74
- values={values}
75
- onChange={(updated) => setLocalValues(updated)}
76
- disabled={isReadOnly}
77
- />
78
-
79
- {!isReadOnly && (
80
- <div className="flex items-center justify-end gap-3">
81
- {saveSuccess && (
82
- <span className="text-xs text-success">Saved successfully</span>
83
- )}
84
- {saveError && (
85
- <span className="text-xs text-danger">{saveError}</span>
86
- )}
87
- {isDirty && (
88
- <span className="text-[10px] text-orange-400">Unsaved changes</span>
89
- )}
90
- <button
91
- type="button"
92
- onClick={() => updateMutation.mutate(values)}
93
- disabled={updateMutation.isPending || !isDirty}
94
- className="rounded-md bg-primary px-4 py-1.5 text-xs font-medium text-white hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
95
- >
96
- {updateMutation.isPending ? 'Saving...' : 'Save'}
97
- </button>
98
- </div>
99
- )}
100
- </div>
101
- )
102
- }
103
-
104
- // ---------------------------------------------------------------------------
105
- // Page — fetches tabs from backend
106
- // ---------------------------------------------------------------------------
107
-
108
- interface TabEntry {
109
- readonly id: string
110
- readonly label: string
111
- }
112
-
113
- export function SettingsPage() {
114
- const client = useBackendClient()
115
-
116
- // Fetch all schemas + tabs metadata in one call
117
- const allQuery = useQuery({
118
- queryKey: ['settings-schema-all'],
119
- queryFn: () => client.getSettingsSchema(),
120
- staleTime: Infinity,
121
- })
122
-
123
- const tabs: readonly TabEntry[] =
124
- (allQuery.data as { tabs?: readonly TabEntry[] } | undefined)?.tabs ?? []
125
-
126
- const [activeTab, setActiveTab] = useState<string | null>(null)
127
- const currentTab = activeTab ?? (tabs.length > 0 ? tabs[0].id : null)
128
-
129
- if (allQuery.isLoading) {
130
- return (
131
- <div className="p-6">
132
- <div className="text-xs text-foreground-subtle animate-pulse">Loading settings...</div>
133
- </div>
134
- )
135
- }
136
-
137
- return (
138
- <div className="p-6 space-y-4">
139
- <div className="flex items-center justify-between">
140
- <h1 className="text-lg font-semibold text-foreground">Settings</h1>
141
- <span className="text-[10px] rounded-full bg-primary/10 text-primary px-2 py-0.5 font-medium">
142
- system
143
- </span>
144
- </div>
145
-
146
- {/* Tabs — scrollable on small screens */}
147
- <div className="flex gap-1 border-b border-border overflow-x-auto scrollbar-hide">
148
- {tabs.map((tab) => (
149
- <button
150
- key={tab.id}
151
- onClick={() => setActiveTab(tab.id)}
152
- className={[
153
- 'px-3 py-1.5 text-xs font-medium rounded-t transition-colors whitespace-nowrap',
154
- currentTab === tab.id
155
- ? 'text-primary border-b-2 border-primary -mb-px bg-surface'
156
- : 'text-foreground-subtle hover:text-foreground',
157
- ].join(' ')}
158
- >
159
- {tab.label}
160
- </button>
161
- ))}
162
- </div>
163
-
164
- {/* Active panel */}
165
- {currentTab && <SectionPanel key={currentTab} section={currentTab} />}
166
- </div>
167
- )
168
- }
@@ -1,174 +0,0 @@
1
- import { useState } from 'react'
2
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
- import { Plus, X } from 'lucide-react'
4
- import { useBackendClient } from '../../hooks/useBackendClient'
5
-
6
- type Role = 'super_admin' | 'admin' | 'viewer'
7
-
8
- const ROLE_STYLES: Record<Role, string> = {
9
- super_admin: 'bg-danger/10 text-danger',
10
- admin: 'bg-primary/10 text-primary',
11
- viewer: 'bg-foreground-subtle/10 text-foreground-subtle',
12
- }
13
-
14
- export function UsersPage() {
15
- const client = useBackendClient()
16
- const queryClient = useQueryClient()
17
- const [showForm, setShowForm] = useState(false)
18
- const [username, setUsername] = useState('')
19
- const [password, setPassword] = useState('')
20
- const [role, setRole] = useState<Role>('viewer')
21
- const [formError, setFormError] = useState<string | null>(null)
22
-
23
- const { data: users, isLoading, isError } = useQuery({
24
- queryKey: ['users'],
25
- queryFn: () => client.listUsers(),
26
- })
27
-
28
- const createMutation = useMutation({
29
- mutationFn: () => client.createUser(username, password, role),
30
- onSuccess: () => {
31
- queryClient.invalidateQueries({ queryKey: ['users'] })
32
- setShowForm(false)
33
- setUsername('')
34
- setPassword('')
35
- setRole('viewer')
36
- setFormError(null)
37
- },
38
- onError: (err: unknown) => {
39
- setFormError(err instanceof Error ? err.message : 'Failed to create user')
40
- },
41
- })
42
-
43
- const userList = (users ?? []) as unknown as Array<Record<string, unknown>>
44
-
45
- function formatDate(ts: unknown): string {
46
- if (!ts) return '—'
47
- const d = new Date(typeof ts === 'number' ? ts : String(ts))
48
- return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
49
- }
50
-
51
- return (
52
- <div className="p-6 space-y-4">
53
- <div className="flex items-center justify-between">
54
- <h1 className="text-lg font-semibold text-foreground">Users</h1>
55
- <button
56
- onClick={() => setShowForm(true)}
57
- className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground shadow-sm hover:bg-primary/90"
58
- >
59
- <Plus className="h-3.5 w-3.5" />
60
- Create User
61
- </button>
62
- </div>
63
-
64
- {/* Create user form */}
65
- {showForm && (
66
- <div className="rounded-lg border border-border bg-surface p-4 space-y-3">
67
- <div className="flex items-center justify-between">
68
- <span className="text-xs font-medium text-foreground">New User</span>
69
- <button onClick={() => setShowForm(false)} className="text-foreground-subtle hover:text-foreground">
70
- <X className="h-4 w-4" />
71
- </button>
72
- </div>
73
- <div className="grid grid-cols-3 gap-3">
74
- <div className="space-y-1">
75
- <label className="text-[10px] text-foreground-subtle uppercase tracking-wide">Username</label>
76
- <input
77
- type="text"
78
- value={username}
79
- onChange={(e) => setUsername(e.target.value)}
80
- placeholder="john"
81
- className="w-full rounded border border-border bg-background px-2 py-1.5 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary"
82
- />
83
- </div>
84
- <div className="space-y-1">
85
- <label className="text-[10px] text-foreground-subtle uppercase tracking-wide">Password</label>
86
- <input
87
- type="password"
88
- value={password}
89
- onChange={(e) => setPassword(e.target.value)}
90
- placeholder="••••••••"
91
- className="w-full rounded border border-border bg-background px-2 py-1.5 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary"
92
- />
93
- </div>
94
- <div className="space-y-1">
95
- <label className="text-[10px] text-foreground-subtle uppercase tracking-wide">Role</label>
96
- <select
97
- value={role}
98
- onChange={(e) => setRole(e.target.value as Role)}
99
- className="w-full rounded border border-border bg-background px-2 py-1.5 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
100
- >
101
- <option value="viewer">Viewer</option>
102
- <option value="admin">Admin</option>
103
- <option value="super_admin">Super Admin</option>
104
- </select>
105
- </div>
106
- </div>
107
- {formError && <p className="text-[10px] text-danger">{formError}</p>}
108
- <div className="flex justify-end gap-2">
109
- <button
110
- onClick={() => setShowForm(false)}
111
- className="rounded px-3 py-1.5 text-xs text-foreground-subtle border border-border hover:text-foreground"
112
- >
113
- Cancel
114
- </button>
115
- <button
116
- onClick={() => createMutation.mutate()}
117
- disabled={createMutation.isPending || !username || !password}
118
- className="rounded bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
119
- >
120
- {createMutation.isPending ? 'Creating...' : 'Create'}
121
- </button>
122
- </div>
123
- </div>
124
- )}
125
-
126
- {isLoading && (
127
- <div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
128
- )}
129
-
130
- {isError && (
131
- <div className="text-xs text-danger">Failed to load</div>
132
- )}
133
-
134
- {!isLoading && !isError && userList.length === 0 && (
135
- <div className="text-xs text-foreground-subtle">No data</div>
136
- )}
137
-
138
- {userList.length > 0 && (
139
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
140
- <table className="w-full text-xs">
141
- <thead>
142
- <tr>
143
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Username</th>
144
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Role</th>
145
- <th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Created</th>
146
- </tr>
147
- </thead>
148
- <tbody>
149
- {userList.map((user) => {
150
- const userRole = String(user.role ?? 'viewer') as Role
151
- const roleStyle = ROLE_STYLES[userRole] ?? ROLE_STYLES['viewer']
152
- return (
153
- <tr key={String(user.id ?? user.username)} className="hover:bg-primary/5">
154
- <td className="px-3 py-2 text-foreground border-b border-border">
155
- {String(user.username ?? user.name ?? '')}
156
- </td>
157
- <td className="px-3 py-2 text-foreground border-b border-border">
158
- <span className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium ${roleStyle}`}>
159
- {userRole.replace('_', ' ')}
160
- </span>
161
- </td>
162
- <td className="px-3 py-2 text-foreground border-b border-border text-foreground-subtle">
163
- {formatDate(user.createdAt ?? user.created_at ?? user.createdAt)}
164
- </td>
165
- </tr>
166
- )
167
- })}
168
- </tbody>
169
- </table>
170
- </div>
171
- )}
172
- </div>
173
- )
174
- }
@@ -1,54 +0,0 @@
1
- import path from 'node:path'
2
- import { fileURLToPath } from 'node:url'
3
- import type {
4
- ICamstackAddon,
5
- AddonManifest,
6
- AddonContext,
7
- IAdminUI,
8
- CapabilityProviderMap,
9
- } from '@camstack/types'
10
-
11
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
-
13
- /**
14
- * AdminUIAddon — standalone addon that serves the Vite-built frontend.
15
- * The dist/ directory containing the built React app sits alongside this
16
- * compiled file in the package output.
17
- */
18
-
19
- function resolveAdminUiDistDir(): string {
20
- // This addon file lives at <package-root>/dist/server/addon.js.
21
- // Vite assets (index.html, assets/) are at <package-root>/dist/.
22
- return path.resolve(__dirname, '..')
23
- }
24
-
25
- export class AdminUIAddon implements ICamstackAddon {
26
- readonly id = 'admin-ui'
27
-
28
- readonly manifest: AddonManifest = {
29
- id: 'admin-ui',
30
- name: 'CamStack Admin UI',
31
- version: '0.1.0',
32
- packageName: '@camstack/addon-admin-ui',
33
- description: 'Web-based administration interface for CamStack',
34
- capabilities: [{ name: 'admin-ui', mode: 'singleton' }],
35
- }
36
-
37
- async initialize(_ctx: AddonContext): Promise<void> {}
38
- async shutdown(): Promise<void> {}
39
-
40
- getCapabilityProvider<K extends keyof CapabilityProviderMap>(
41
- name: K,
42
- ): CapabilityProviderMap[K] | null {
43
- if (name === 'admin-ui') {
44
- const provider: IAdminUI = {
45
- getStaticDir: () => resolveAdminUiDistDir(),
46
- getVersion: () => this.manifest.version,
47
- }
48
- return provider as CapabilityProviderMap[K]
49
- }
50
- return null
51
- }
52
- }
53
-
54
- export default AdminUIAddon