een-api-toolkit 0.3.20 → 0.3.28
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.
- package/.claude/agents/docs-accuracy-reviewer.md +146 -0
- package/.claude/agents/een-auth-agent.md +168 -0
- package/.claude/agents/een-devices-agent.md +331 -0
- package/.claude/agents/een-events-agent.md +375 -0
- package/.claude/agents/een-media-agent.md +315 -0
- package/.claude/agents/een-setup-agent.md +126 -0
- package/.claude/agents/een-users-agent.md +239 -0
- package/.claude/agents/test-runner.md +144 -0
- package/CHANGELOG.md +10 -45
- package/README.md +23 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +169 -1700
- package/docs/ai-reference/AI-AUTH.md +288 -0
- package/docs/ai-reference/AI-DEVICES.md +569 -0
- package/docs/ai-reference/AI-EVENTS.md +1745 -0
- package/docs/ai-reference/AI-MEDIA.md +974 -0
- package/docs/ai-reference/AI-SETUP.md +267 -0
- package/docs/ai-reference/AI-USERS.md +255 -0
- package/examples/vue-event-subscriptions/package-lock.json +8 -1
- package/examples/vue-event-subscriptions/package.json +1 -0
- package/examples/vue-event-subscriptions/src/App.vue +1 -41
- package/examples/vue-event-subscriptions/src/composables/useHlsPlayer.ts +272 -0
- package/examples/vue-event-subscriptions/src/main.ts +3 -3
- package/examples/vue-event-subscriptions/src/stores/connection.ts +101 -0
- package/examples/vue-event-subscriptions/src/stores/mediaSession.ts +79 -0
- package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +349 -88
- package/examples/vue-event-subscriptions/src/views/Logout.vue +6 -0
- package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +0 -13
- package/examples/vue-events/package-lock.json +8 -1
- package/examples/vue-events/package.json +1 -0
- package/examples/vue-events/src/components/EventsModal.vue +269 -47
- package/examples/vue-events/src/composables/useHlsPlayer.ts +272 -0
- package/examples/vue-events/src/stores/mediaSession.ts +79 -0
- package/package.json +10 -2
- package/scripts/setup-agents.ts +116 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
# Cameras & Bridges API - EEN API Toolkit
|
|
2
|
+
|
|
3
|
+
> **Version:** 0.3.28
|
|
4
|
+
>
|
|
5
|
+
> Complete reference for camera and bridge management.
|
|
6
|
+
> Load this document when working with devices.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Camera Types
|
|
11
|
+
|
|
12
|
+
### Camera
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
type CameraStatus =
|
|
16
|
+
| 'online' | 'offline' | 'deviceOffline' | 'bridgeOffline'
|
|
17
|
+
| 'invalidCredentials' | 'error' | 'streaming' | 'registered'
|
|
18
|
+
| 'attaching' | 'initializing'
|
|
19
|
+
|
|
20
|
+
interface Camera {
|
|
21
|
+
id: string
|
|
22
|
+
name: string
|
|
23
|
+
accountId: string
|
|
24
|
+
bridgeId?: string | null
|
|
25
|
+
locationId?: string | null
|
|
26
|
+
status?: CameraStatus
|
|
27
|
+
timezone?: string
|
|
28
|
+
guid?: string
|
|
29
|
+
ipAddress?: string
|
|
30
|
+
macAddress?: string
|
|
31
|
+
tags?: string[]
|
|
32
|
+
notes?: string
|
|
33
|
+
deviceInfo?: CameraDeviceInfo
|
|
34
|
+
shareDetails?: CameraShareDetails
|
|
35
|
+
devicePosition?: CameraDevicePosition
|
|
36
|
+
createdAt?: string
|
|
37
|
+
updatedAt?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CameraDeviceInfo {
|
|
41
|
+
make?: string // Manufacturer (e.g., "Axis")
|
|
42
|
+
model?: string // Model name
|
|
43
|
+
firmwareVersion?: string
|
|
44
|
+
directToCloud?: boolean // No bridge required
|
|
45
|
+
serialNumber?: string
|
|
46
|
+
resolution?: string
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Parameters
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
interface ListCamerasParams {
|
|
54
|
+
pageSize?: number // Results per page
|
|
55
|
+
pageToken?: string // Pagination token
|
|
56
|
+
include?: string[] // Additional fields
|
|
57
|
+
sort?: string[] // Sort order
|
|
58
|
+
status__in?: CameraStatus[] // Filter by status
|
|
59
|
+
status__ne?: CameraStatus // Exclude by status
|
|
60
|
+
tags__contains?: string[] // All tags must match
|
|
61
|
+
tags__any?: string[] // Any tag matches
|
|
62
|
+
name__contains?: string // Partial name match
|
|
63
|
+
q?: string // Full-text search
|
|
64
|
+
bridgeId__in?: string[] // Filter by bridge
|
|
65
|
+
locationId__in?: string[] // Filter by location
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Bridge Types
|
|
72
|
+
|
|
73
|
+
### Bridge
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
type BridgeStatus =
|
|
77
|
+
| 'online' | 'offline' | 'error' | 'idle'
|
|
78
|
+
| 'registered' | 'attaching' | 'initializing'
|
|
79
|
+
|
|
80
|
+
interface Bridge {
|
|
81
|
+
id: string
|
|
82
|
+
name: string
|
|
83
|
+
accountId: string
|
|
84
|
+
locationId?: string | null
|
|
85
|
+
guid?: string
|
|
86
|
+
timezone?: string
|
|
87
|
+
status?: BridgeStatus | { connectionStatus?: BridgeStatus }
|
|
88
|
+
tags?: string[]
|
|
89
|
+
deviceInfo?: BridgeDeviceInfo
|
|
90
|
+
networkInfo?: BridgeNetworkInfo
|
|
91
|
+
cameraCount?: number
|
|
92
|
+
createdAt?: string
|
|
93
|
+
updatedAt?: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface BridgeNetworkInfo {
|
|
97
|
+
localIpAddress?: string
|
|
98
|
+
publicIpAddress?: string
|
|
99
|
+
macAddress?: string
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Camera Functions
|
|
106
|
+
|
|
107
|
+
### getCameras(params?)
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { getCameras } from 'een-api-toolkit'
|
|
111
|
+
|
|
112
|
+
// Basic usage
|
|
113
|
+
const { data, error } = await getCameras()
|
|
114
|
+
|
|
115
|
+
// Filter by status
|
|
116
|
+
const { data } = await getCameras({
|
|
117
|
+
status__in: ['online', 'streaming']
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Full-text search
|
|
121
|
+
const { data } = await getCameras({
|
|
122
|
+
q: 'front door',
|
|
123
|
+
include: ['deviceInfo', 'status']
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Filter by tags
|
|
127
|
+
const { data } = await getCameras({
|
|
128
|
+
tags__any: ['floor1', 'floor2']
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### getCamera(cameraId, params?)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { getCamera } from 'een-api-toolkit'
|
|
136
|
+
|
|
137
|
+
const { data, error } = await getCamera('camera-id-123')
|
|
138
|
+
|
|
139
|
+
// With additional fields
|
|
140
|
+
const { data: detailed } = await getCamera('camera-id-123', {
|
|
141
|
+
include: ['deviceInfo', 'status', 'shareDetails', 'tags']
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Bridge Functions
|
|
148
|
+
|
|
149
|
+
### getBridges(params?)
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { getBridges } from 'een-api-toolkit'
|
|
153
|
+
|
|
154
|
+
// Basic usage
|
|
155
|
+
const { data, error } = await getBridges()
|
|
156
|
+
|
|
157
|
+
// Filter by status
|
|
158
|
+
const { data } = await getBridges({
|
|
159
|
+
status__in: ['online'],
|
|
160
|
+
include: ['deviceInfo', 'networkInfo']
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### getBridge(bridgeId, params?)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { getBridge } from 'een-api-toolkit'
|
|
168
|
+
|
|
169
|
+
const { data, error } = await getBridge('bridge-id-123', {
|
|
170
|
+
include: ['deviceInfo', 'networkInfo', 'status']
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Filter Patterns
|
|
177
|
+
|
|
178
|
+
| Filter | Example | Description |
|
|
179
|
+
|--------|---------|-------------|
|
|
180
|
+
| `status__in` | `['online', 'streaming']` | Include specific statuses |
|
|
181
|
+
| `status__ne` | `'offline'` | Exclude a status |
|
|
182
|
+
| `tags__contains` | `['outdoor']` | All tags must match |
|
|
183
|
+
| `tags__any` | `['floor1', 'floor2']` | Any tag matches |
|
|
184
|
+
| `name__contains` | `'lobby'` | Partial name match |
|
|
185
|
+
| `q` | `'front door'` | Full-text search |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Vue Components
|
|
190
|
+
|
|
191
|
+
### Cameras.vue
|
|
192
|
+
|
|
193
|
+
```vue
|
|
194
|
+
<script setup lang="ts">
|
|
195
|
+
import { ref, computed, watch, onMounted } from 'vue'
|
|
196
|
+
import { getCameras, type Camera, type CameraStatus, type EenError, type ListCamerasParams } from 'een-api-toolkit'
|
|
197
|
+
|
|
198
|
+
// Status filter
|
|
199
|
+
const statusFilter = ref<CameraStatus | ''>('')
|
|
200
|
+
|
|
201
|
+
// Reactive state
|
|
202
|
+
const cameras = ref<Camera[]>([])
|
|
203
|
+
const loading = ref(false)
|
|
204
|
+
const error = ref<EenError | null>(null)
|
|
205
|
+
const nextPageToken = ref<string | undefined>(undefined)
|
|
206
|
+
const totalSize = ref<number | undefined>(undefined)
|
|
207
|
+
|
|
208
|
+
const hasNextPage = computed(() => !!nextPageToken.value)
|
|
209
|
+
|
|
210
|
+
const params = ref<ListCamerasParams>({
|
|
211
|
+
pageSize: 20,
|
|
212
|
+
include: ['deviceInfo', 'status']
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
async function fetchCameras(fetchParams?: ListCamerasParams, append = false) {
|
|
216
|
+
loading.value = true
|
|
217
|
+
error.value = null
|
|
218
|
+
|
|
219
|
+
const mergedParams = { ...params.value, ...fetchParams }
|
|
220
|
+
const result = await getCameras(mergedParams)
|
|
221
|
+
|
|
222
|
+
if (result.error) {
|
|
223
|
+
error.value = result.error
|
|
224
|
+
if (!append) {
|
|
225
|
+
cameras.value = []
|
|
226
|
+
totalSize.value = undefined
|
|
227
|
+
}
|
|
228
|
+
nextPageToken.value = undefined
|
|
229
|
+
} else {
|
|
230
|
+
if (append) {
|
|
231
|
+
cameras.value = [...cameras.value, ...result.data.results]
|
|
232
|
+
} else {
|
|
233
|
+
cameras.value = result.data.results
|
|
234
|
+
}
|
|
235
|
+
nextPageToken.value = result.data.nextPageToken
|
|
236
|
+
totalSize.value = result.data.totalSize
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
loading.value = false
|
|
240
|
+
return result
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function refresh() {
|
|
244
|
+
return fetchCameras()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function fetchNextPage() {
|
|
248
|
+
if (!nextPageToken.value) return
|
|
249
|
+
return fetchCameras({ ...params.value, pageToken: nextPageToken.value }, true)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function setParams(newParams: ListCamerasParams) {
|
|
253
|
+
params.value = newParams
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Watch for status filter changes
|
|
257
|
+
watch(statusFilter, async (newStatus) => {
|
|
258
|
+
if (newStatus) {
|
|
259
|
+
setParams({
|
|
260
|
+
pageSize: 20,
|
|
261
|
+
include: ['deviceInfo', 'status'],
|
|
262
|
+
status__in: [newStatus]
|
|
263
|
+
})
|
|
264
|
+
} else {
|
|
265
|
+
setParams({
|
|
266
|
+
pageSize: 20,
|
|
267
|
+
include: ['deviceInfo', 'status']
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
await fetchCameras()
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Helper to extract status string from the union type
|
|
274
|
+
function getStatusString(status?: CameraStatus | { connectionStatus?: CameraStatus }): CameraStatus | undefined {
|
|
275
|
+
if (!status) return undefined
|
|
276
|
+
if (typeof status === 'string') return status
|
|
277
|
+
return status.connectionStatus
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Get status badge class
|
|
281
|
+
function getStatusClass(status?: CameraStatus | { connectionStatus?: CameraStatus }): string {
|
|
282
|
+
const statusStr = getStatusString(status)
|
|
283
|
+
switch (statusStr) {
|
|
284
|
+
case 'online':
|
|
285
|
+
case 'streaming':
|
|
286
|
+
return 'status-online'
|
|
287
|
+
case 'offline':
|
|
288
|
+
case 'deviceOffline':
|
|
289
|
+
case 'bridgeOffline':
|
|
290
|
+
return 'status-offline'
|
|
291
|
+
case 'error':
|
|
292
|
+
case 'invalidCredentials':
|
|
293
|
+
return 'status-error'
|
|
294
|
+
default:
|
|
295
|
+
return 'status-unknown'
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
onMounted(() => {
|
|
300
|
+
fetchCameras()
|
|
301
|
+
})
|
|
302
|
+
</script>
|
|
303
|
+
|
|
304
|
+
<template>
|
|
305
|
+
<div class="cameras">
|
|
306
|
+
<div class="header">
|
|
307
|
+
<h2>Cameras</h2>
|
|
308
|
+
<div class="controls">
|
|
309
|
+
<select v-model="statusFilter" class="status-filter">
|
|
310
|
+
<option value="">All Statuses</option>
|
|
311
|
+
<option value="online">Online</option>
|
|
312
|
+
<option value="streaming">Streaming</option>
|
|
313
|
+
<option value="offline">Offline</option>
|
|
314
|
+
<option value="deviceOffline">Device Offline</option>
|
|
315
|
+
<option value="bridgeOffline">Bridge Offline</option>
|
|
316
|
+
<option value="error">Error</option>
|
|
317
|
+
</select>
|
|
318
|
+
<button @click="refresh" :disabled="loading">
|
|
319
|
+
{{ loading ? 'Loading...' : 'Refresh' }}
|
|
320
|
+
</button>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div v-if="totalSize !== undefined" class="total-count">
|
|
325
|
+
Total: {{ totalSize }} camera{{ totalSize !== 1 ? 's' : '' }}
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div v-if="loading && cameras.length === 0" class="loading">
|
|
329
|
+
Loading cameras...
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div v-else-if="error" class="error">
|
|
333
|
+
Error: {{ error.message }}
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div v-else>
|
|
337
|
+
<div v-if="cameras.length > 0" class="camera-grid">
|
|
338
|
+
<router-link
|
|
339
|
+
v-for="camera in cameras"
|
|
340
|
+
:key="camera.id"
|
|
341
|
+
:to="`/cameras/${camera.id}`"
|
|
342
|
+
class="camera-card"
|
|
343
|
+
>
|
|
344
|
+
<div class="camera-header">
|
|
345
|
+
<h3>{{ camera.name }}</h3>
|
|
346
|
+
<span :class="['status-badge', getStatusClass(camera.status)]">
|
|
347
|
+
{{ getStatusString(camera.status) || 'Unknown' }}
|
|
348
|
+
</span>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="camera-details">
|
|
351
|
+
<p v-if="camera.deviceInfo?.make || camera.deviceInfo?.model">
|
|
352
|
+
<strong>Device:</strong>
|
|
353
|
+
{{ camera.deviceInfo?.make || '' }} {{ camera.deviceInfo?.model || '' }}
|
|
354
|
+
</p>
|
|
355
|
+
<p v-if="camera.locationId">
|
|
356
|
+
<strong>Location:</strong> {{ camera.locationId }}
|
|
357
|
+
</p>
|
|
358
|
+
<p v-if="camera.tags && camera.tags.length > 0">
|
|
359
|
+
<strong>Tags:</strong> {{ camera.tags.join(', ') }}
|
|
360
|
+
</p>
|
|
361
|
+
</div>
|
|
362
|
+
</router-link>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<p v-else class="no-cameras">
|
|
366
|
+
No cameras found{{ statusFilter ? ' with selected filter' : '' }}.
|
|
367
|
+
</p>
|
|
368
|
+
|
|
369
|
+
<div v-if="hasNextPage" class="pagination">
|
|
370
|
+
<button @click="fetchNextPage" :disabled="loading">
|
|
371
|
+
{{ loading ? 'Loading...' : 'Load More' }}
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</template>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Bridges.vue
|
|
380
|
+
|
|
381
|
+
```vue
|
|
382
|
+
<script setup lang="ts">
|
|
383
|
+
import { ref, computed, watch, onMounted } from 'vue'
|
|
384
|
+
import { getBridges, type Bridge, type BridgeStatus, type EenError, type ListBridgesParams } from 'een-api-toolkit'
|
|
385
|
+
|
|
386
|
+
// Status filter
|
|
387
|
+
const statusFilter = ref<BridgeStatus | ''>('')
|
|
388
|
+
|
|
389
|
+
// Reactive state
|
|
390
|
+
const bridges = ref<Bridge[]>([])
|
|
391
|
+
const loading = ref(false)
|
|
392
|
+
const error = ref<EenError | null>(null)
|
|
393
|
+
const nextPageToken = ref<string | undefined>(undefined)
|
|
394
|
+
const totalSize = ref<number | undefined>(undefined)
|
|
395
|
+
|
|
396
|
+
const hasNextPage = computed(() => !!nextPageToken.value)
|
|
397
|
+
|
|
398
|
+
const params = ref<ListBridgesParams>({
|
|
399
|
+
pageSize: 20,
|
|
400
|
+
include: ['deviceInfo', 'status', 'networkInfo']
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
async function fetchBridges(fetchParams?: ListBridgesParams, append = false) {
|
|
404
|
+
loading.value = true
|
|
405
|
+
error.value = null
|
|
406
|
+
|
|
407
|
+
const mergedParams = { ...params.value, ...fetchParams }
|
|
408
|
+
const result = await getBridges(mergedParams)
|
|
409
|
+
|
|
410
|
+
if (result.error) {
|
|
411
|
+
error.value = result.error
|
|
412
|
+
if (!append) {
|
|
413
|
+
bridges.value = []
|
|
414
|
+
totalSize.value = undefined
|
|
415
|
+
}
|
|
416
|
+
nextPageToken.value = undefined
|
|
417
|
+
} else {
|
|
418
|
+
if (append) {
|
|
419
|
+
bridges.value = [...bridges.value, ...result.data.results]
|
|
420
|
+
} else {
|
|
421
|
+
bridges.value = result.data.results
|
|
422
|
+
}
|
|
423
|
+
nextPageToken.value = result.data.nextPageToken
|
|
424
|
+
totalSize.value = result.data.totalSize
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
loading.value = false
|
|
428
|
+
return result
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function refresh() {
|
|
432
|
+
return fetchBridges()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function fetchNextPage() {
|
|
436
|
+
if (!nextPageToken.value) return
|
|
437
|
+
return fetchBridges({ ...params.value, pageToken: nextPageToken.value }, true)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function setParams(newParams: ListBridgesParams) {
|
|
441
|
+
params.value = newParams
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Watch for status filter changes
|
|
445
|
+
watch(statusFilter, async (newStatus) => {
|
|
446
|
+
if (newStatus) {
|
|
447
|
+
setParams({
|
|
448
|
+
pageSize: 20,
|
|
449
|
+
include: ['deviceInfo', 'status', 'networkInfo'],
|
|
450
|
+
status__in: [newStatus]
|
|
451
|
+
})
|
|
452
|
+
} else {
|
|
453
|
+
setParams({
|
|
454
|
+
pageSize: 20,
|
|
455
|
+
include: ['deviceInfo', 'status', 'networkInfo']
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
await fetchBridges()
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// Helper to extract status string from the union type
|
|
462
|
+
function getStatusString(status?: BridgeStatus | { connectionStatus?: BridgeStatus }): BridgeStatus | undefined {
|
|
463
|
+
if (!status) return undefined
|
|
464
|
+
if (typeof status === 'string') return status
|
|
465
|
+
return status.connectionStatus
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Get status badge class
|
|
469
|
+
function getStatusClass(status?: BridgeStatus | { connectionStatus?: BridgeStatus }): string {
|
|
470
|
+
const statusStr = getStatusString(status)
|
|
471
|
+
switch (statusStr) {
|
|
472
|
+
case 'online':
|
|
473
|
+
return 'status-online'
|
|
474
|
+
case 'offline':
|
|
475
|
+
return 'status-offline'
|
|
476
|
+
case 'error':
|
|
477
|
+
return 'status-error'
|
|
478
|
+
default:
|
|
479
|
+
return 'status-unknown'
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
onMounted(() => {
|
|
484
|
+
fetchBridges()
|
|
485
|
+
})
|
|
486
|
+
</script>
|
|
487
|
+
|
|
488
|
+
<template>
|
|
489
|
+
<div class="bridges">
|
|
490
|
+
<div class="header">
|
|
491
|
+
<h2>Bridges</h2>
|
|
492
|
+
<div class="controls">
|
|
493
|
+
<select v-model="statusFilter" class="status-filter">
|
|
494
|
+
<option value="">All Statuses</option>
|
|
495
|
+
<option value="online">Online</option>
|
|
496
|
+
<option value="offline">Offline</option>
|
|
497
|
+
<option value="error">Error</option>
|
|
498
|
+
<option value="idle">Idle</option>
|
|
499
|
+
</select>
|
|
500
|
+
<button @click="refresh" :disabled="loading">
|
|
501
|
+
{{ loading ? 'Loading...' : 'Refresh' }}
|
|
502
|
+
</button>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div v-if="totalSize !== undefined" class="total-count">
|
|
507
|
+
Total: {{ totalSize }} bridge{{ totalSize !== 1 ? 's' : '' }}
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div v-if="loading && bridges.length === 0" class="loading">
|
|
511
|
+
Loading bridges...
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<div v-else-if="error" class="error">
|
|
515
|
+
Error: {{ error.message }}
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
<div v-else>
|
|
519
|
+
<div v-if="bridges.length > 0" class="bridge-grid">
|
|
520
|
+
<router-link
|
|
521
|
+
v-for="bridge in bridges"
|
|
522
|
+
:key="bridge.id"
|
|
523
|
+
:to="`/bridges/${bridge.id}`"
|
|
524
|
+
class="bridge-card"
|
|
525
|
+
>
|
|
526
|
+
<div class="bridge-header">
|
|
527
|
+
<h3>{{ bridge.name }}</h3>
|
|
528
|
+
<span :class="['status-badge', getStatusClass(bridge.status)]">
|
|
529
|
+
{{ getStatusString(bridge.status) || 'Unknown' }}
|
|
530
|
+
</span>
|
|
531
|
+
</div>
|
|
532
|
+
<div class="bridge-details">
|
|
533
|
+
<p v-if="bridge.deviceInfo?.make || bridge.deviceInfo?.model">
|
|
534
|
+
<strong>Device:</strong>
|
|
535
|
+
{{ bridge.deviceInfo?.make || '' }} {{ bridge.deviceInfo?.model || '' }}
|
|
536
|
+
</p>
|
|
537
|
+
<p v-if="bridge.networkInfo?.localIpAddress">
|
|
538
|
+
<strong>IP:</strong> {{ bridge.networkInfo.localIpAddress }}
|
|
539
|
+
</p>
|
|
540
|
+
<p v-if="bridge.locationId">
|
|
541
|
+
<strong>Location:</strong> {{ bridge.locationId }}
|
|
542
|
+
</p>
|
|
543
|
+
<p v-if="bridge.tags && bridge.tags.length > 0">
|
|
544
|
+
<strong>Tags:</strong> {{ bridge.tags.join(', ') }}
|
|
545
|
+
</p>
|
|
546
|
+
</div>
|
|
547
|
+
</router-link>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<p v-else class="no-bridges">
|
|
551
|
+
No bridges found{{ statusFilter ? ' with selected filter' : '' }}.
|
|
552
|
+
</p>
|
|
553
|
+
|
|
554
|
+
<div v-if="hasNextPage" class="pagination">
|
|
555
|
+
<button @click="fetchNextPage" :disabled="loading">
|
|
556
|
+
{{ loading ? 'Loading...' : 'Load More' }}
|
|
557
|
+
</button>
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
</template>
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Reference Examples
|
|
567
|
+
|
|
568
|
+
- `examples/vue-cameras/`
|
|
569
|
+
- `examples/vue-bridges/`
|