een-api-toolkit 0.3.30 → 0.3.38
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 +15 -3
- package/.claude/agents/een-auth-agent.md +131 -0
- package/.claude/agents/een-devices-agent.md +10 -7
- package/.claude/agents/een-events-agent.md +98 -0
- package/.claude/agents/een-grouping-agent.md +394 -0
- package/.claude/agents/een-media-agent.md +25 -5
- package/CHANGELOG.md +77 -6
- package/README.md +5 -3
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +561 -0
- package/dist/index.js +388 -218
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +13 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -1
- package/docs/ai-reference/AI-GROUPING.md +411 -0
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-alerts-metrics/README.md +2 -0
- package/examples/vue-alerts-metrics/alert-metrics-screenshot.png +0 -0
- package/examples/vue-alerts-metrics/e2e/auth.spec.ts +1 -1
- package/examples/vue-alerts-metrics/package-lock.json +17 -14
- package/examples/vue-alerts-metrics/package.json +1 -1
- package/examples/vue-bridges/package-lock.json +21 -15
- package/examples/vue-bridges/package.json +1 -1
- package/examples/vue-cameras/package-lock.json +21 -15
- package/examples/vue-cameras/package.json +1 -1
- package/examples/vue-event-subscriptions/README.md +2 -0
- package/examples/vue-event-subscriptions/event-subscriptions-screenshot.png +0 -0
- package/examples/vue-event-subscriptions/package-lock.json +17 -14
- package/examples/vue-event-subscriptions/package.json +1 -1
- package/examples/vue-events/events-screenshot.png +0 -0
- package/examples/vue-events/package-lock.json +17 -14
- package/examples/vue-events/package.json +1 -1
- package/examples/vue-feeds/package-lock.json +21 -15
- package/examples/vue-feeds/package.json +1 -1
- package/examples/vue-layouts/.env.example +12 -0
- package/examples/vue-layouts/README.md +320 -0
- package/examples/vue-layouts/e2e/app.spec.ts +76 -0
- package/examples/vue-layouts/e2e/auth.spec.ts +264 -0
- package/examples/vue-layouts/index.html +13 -0
- package/examples/vue-layouts/layouts-screenshot.png +0 -0
- package/examples/vue-layouts/package-lock.json +1722 -0
- package/examples/vue-layouts/package.json +28 -0
- package/examples/vue-layouts/playwright.config.ts +47 -0
- package/examples/vue-layouts/src/App.vue +124 -0
- package/examples/vue-layouts/src/components/LayoutModal.vue +456 -0
- package/examples/vue-layouts/src/main.ts +25 -0
- package/examples/vue-layouts/src/router/index.ts +62 -0
- package/examples/vue-layouts/src/views/Callback.vue +76 -0
- package/examples/vue-layouts/src/views/Home.vue +188 -0
- package/examples/vue-layouts/src/views/Layouts.vue +355 -0
- package/examples/vue-layouts/src/views/Login.vue +33 -0
- package/examples/vue-layouts/src/views/Logout.vue +59 -0
- package/examples/vue-layouts/src/vite-env.d.ts +12 -0
- package/examples/vue-layouts/tsconfig.json +21 -0
- package/examples/vue-layouts/tsconfig.node.json +10 -0
- package/examples/vue-layouts/vite.config.ts +12 -0
- package/examples/vue-media/e2e/auth.spec.ts +35 -1
- package/examples/vue-media/media-screenshot.png +0 -0
- package/examples/vue-media/package-lock.json +19 -14
- package/examples/vue-media/package.json +1 -1
- package/examples/vue-users/package-lock.json +21 -16
- package/examples/vue-users/package.json +2 -2
- package/package.json +2 -2
- package/scripts/setup-agents.ts +9 -7
package/docs/AI-CONTEXT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# EEN API Toolkit - AI Reference
|
|
2
2
|
|
|
3
|
-
> **Version:** 0.3.
|
|
3
|
+
> **Version:** 0.3.38
|
|
4
4
|
>
|
|
5
5
|
> This documentation is optimized for AI assistants. It provides focused, domain-specific
|
|
6
6
|
> references to help you understand and use the een-api-toolkit efficiently.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
| Implementing OAuth login | [AI-AUTH.md](./ai-reference/AI-AUTH.md) | ~2K |
|
|
18
18
|
| Working with users | [AI-USERS.md](./ai-reference/AI-USERS.md) | ~1.5K |
|
|
19
19
|
| Working with cameras or bridges | [AI-DEVICES.md](./ai-reference/AI-DEVICES.md) | ~3K |
|
|
20
|
+
| Working with layouts | [AI-GROUPING.md](./ai-reference/AI-GROUPING.md) | ~3K |
|
|
20
21
|
| Live video, images, HLS playback | [AI-MEDIA.md](./ai-reference/AI-MEDIA.md) | ~4K |
|
|
21
22
|
| Events, alerts, metrics, SSE | [AI-EVENTS.md](./ai-reference/AI-EVENTS.md) | ~3.5K |
|
|
22
23
|
|
|
@@ -30,6 +31,7 @@ Specialized agents are available in `.claude/agents/` for domain-specific tasks:
|
|
|
30
31
|
| `een-auth-agent` | Implementing login/logout, auth callbacks, route guards, token refresh |
|
|
31
32
|
| `een-users-agent` | Listing users, user profiles, user management features |
|
|
32
33
|
| `een-devices-agent` | Working with cameras or bridges, filtering by status/tags |
|
|
34
|
+
| `een-grouping-agent` | Layouts CRUD, camera pane management, layout settings |
|
|
33
35
|
| `een-media-agent` | Live video, camera previews, HLS playback, recorded images |
|
|
34
36
|
| `een-events-agent` | Events, alerts, metrics, real-time SSE subscriptions |
|
|
35
37
|
|
|
@@ -65,6 +67,7 @@ Then follow the context files and instructions specified within.
|
|
|
65
67
|
| [vue-users](../examples/vue-users/) | User management with pagination | `src/views/Users.vue` |
|
|
66
68
|
| [vue-cameras](../examples/vue-cameras/) | Camera listing with status filters | `src/views/Cameras.vue` |
|
|
67
69
|
| [vue-bridges](../examples/vue-bridges/) | Bridge listing with device info | `src/views/Bridges.vue` |
|
|
70
|
+
| [vue-layouts](../examples/vue-layouts/) | Layout CRUD with camera panes | `src/views/Layouts.vue` |
|
|
68
71
|
| [vue-media](../examples/vue-media/) | Live and recorded image viewing | `src/views/LiveCamera.vue` |
|
|
69
72
|
| [vue-feeds](../examples/vue-feeds/) | Live video streaming | `src/views/Feeds.vue` |
|
|
70
73
|
| [vue-events](../examples/vue-events/) | Events with bounding boxes | `src/components/EventsModal.vue` |
|
|
@@ -107,6 +110,15 @@ Then follow the context files and instructions specified within.
|
|
|
107
110
|
| `getBridges(params?)` | List all bridges (paginated) |
|
|
108
111
|
| `getBridge(bridgeId, params?)` | Get a specific bridge |
|
|
109
112
|
|
|
113
|
+
### Layouts
|
|
114
|
+
| Function | Purpose |
|
|
115
|
+
|----------|---------|
|
|
116
|
+
| `getLayouts(params?)` | List all layouts (paginated) |
|
|
117
|
+
| `getLayout(layoutId, params?)` | Get a specific layout |
|
|
118
|
+
| `createLayout(params)` | Create a new layout |
|
|
119
|
+
| `updateLayout(layoutId, params)` | Update a layout |
|
|
120
|
+
| `deleteLayout(layoutId)` | Delete a layout |
|
|
121
|
+
|
|
110
122
|
### Media
|
|
111
123
|
| Function | Purpose |
|
|
112
124
|
|----------|---------|
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Layouts API - EEN API Toolkit
|
|
2
|
+
|
|
3
|
+
> **Version:** 0.3.30
|
|
4
|
+
>
|
|
5
|
+
> Complete reference for layout management (camera grouping).
|
|
6
|
+
> Load this document when working with layouts.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Layouts organize multiple camera views into a grid for monitoring. Each layout contains:
|
|
13
|
+
- **Name** - Display name for the layout
|
|
14
|
+
- **Settings** - Display configuration (columns, aspect ratio, borders)
|
|
15
|
+
- **Panes** - Array of camera positions in the grid
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Layout Types
|
|
20
|
+
|
|
21
|
+
### Layout
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
interface Layout {
|
|
25
|
+
id: string
|
|
26
|
+
name: string
|
|
27
|
+
accountId: string
|
|
28
|
+
panes: LayoutPane[]
|
|
29
|
+
settings: LayoutSettings
|
|
30
|
+
effectivePermissions?: LayoutPermissions
|
|
31
|
+
resourceCounts?: { cameras?: number }
|
|
32
|
+
resourceStatusCounts?: { cameras?: CameraStatusCounts }
|
|
33
|
+
qRelevance?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface LayoutPane {
|
|
37
|
+
id: number // Unique pane ID within layout
|
|
38
|
+
name: string // Display name
|
|
39
|
+
type: 'preview' | 'compositePreview'
|
|
40
|
+
size: 1 | 2 | 3 // Grid size (1=small, 2=medium, 3=large)
|
|
41
|
+
cameraId: string // Camera to display
|
|
42
|
+
compositeId?: string | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface LayoutSettings {
|
|
46
|
+
showCameraBorder: boolean
|
|
47
|
+
showCameraName: boolean
|
|
48
|
+
cameraAspectRatio: '16x9' | '4x3'
|
|
49
|
+
paneColumns: number // 1-6 columns
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface LayoutPermissions {
|
|
53
|
+
read?: boolean
|
|
54
|
+
edit?: boolean
|
|
55
|
+
delete?: boolean
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Parameters
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
interface ListLayoutsParams {
|
|
63
|
+
pageSize?: number // Results per page
|
|
64
|
+
pageToken?: string // Pagination token
|
|
65
|
+
include?: ListLayoutsInclude[] // Additional fields
|
|
66
|
+
sort?: ListLayoutsSort[] // Sort order
|
|
67
|
+
name?: string // Exact name match
|
|
68
|
+
name__in?: string[] // Names (any match)
|
|
69
|
+
name__contains?: string // Partial name match
|
|
70
|
+
id__in?: string[] // Filter by IDs
|
|
71
|
+
q?: string // Full-text search
|
|
72
|
+
qRelevance__gte?: number // Min relevance (0.0-1.0)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type ListLayoutsInclude =
|
|
76
|
+
| 'effectivePermissions'
|
|
77
|
+
| 'resourceCounts'
|
|
78
|
+
| 'resourceStatusCounts'
|
|
79
|
+
| 'qRelevance'
|
|
80
|
+
|
|
81
|
+
type ListLayoutsSort =
|
|
82
|
+
| '+name' | '-name'
|
|
83
|
+
| '+rotationOrder'
|
|
84
|
+
| '+qRelevance' | '-qRelevance'
|
|
85
|
+
|
|
86
|
+
interface CreateLayoutParams {
|
|
87
|
+
name: string // Required
|
|
88
|
+
settings: LayoutSettings // Required
|
|
89
|
+
panes?: LayoutPane[] // Optional initial panes
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface UpdateLayoutParams {
|
|
93
|
+
name?: string
|
|
94
|
+
settings?: Partial<LayoutSettings>
|
|
95
|
+
panes?: LayoutPane[] // Replaces existing panes
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Layout Functions
|
|
102
|
+
|
|
103
|
+
### getLayouts(params?)
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { getLayouts } from 'een-api-toolkit'
|
|
107
|
+
|
|
108
|
+
// Basic usage
|
|
109
|
+
const { data, error } = await getLayouts()
|
|
110
|
+
|
|
111
|
+
// With pagination and includes
|
|
112
|
+
const { data } = await getLayouts({
|
|
113
|
+
pageSize: 50,
|
|
114
|
+
include: ['resourceCounts', 'effectivePermissions']
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Search layouts
|
|
118
|
+
const { data } = await getLayouts({
|
|
119
|
+
q: 'lobby',
|
|
120
|
+
qRelevance__gte: 0.5
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Filter by name
|
|
124
|
+
const { data } = await getLayouts({
|
|
125
|
+
name__contains: 'entrance'
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### getLayout(layoutId, params?)
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { getLayout } from 'een-api-toolkit'
|
|
133
|
+
|
|
134
|
+
const { data, error } = await getLayout('layout-123')
|
|
135
|
+
|
|
136
|
+
// With additional fields
|
|
137
|
+
const { data: detailed } = await getLayout('layout-123', {
|
|
138
|
+
include: ['effectivePermissions', 'resourceStatusCounts']
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (data) {
|
|
142
|
+
console.log(`Layout: ${data.name}`)
|
|
143
|
+
console.log(`Panes: ${data.panes.length}`)
|
|
144
|
+
console.log(`Columns: ${data.settings.paneColumns}`)
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### createLayout(params)
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { createLayout, type LayoutSettings } from 'een-api-toolkit'
|
|
152
|
+
|
|
153
|
+
const settings: LayoutSettings = {
|
|
154
|
+
showCameraBorder: true,
|
|
155
|
+
showCameraName: true,
|
|
156
|
+
cameraAspectRatio: '16x9',
|
|
157
|
+
paneColumns: 3
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create empty layout
|
|
161
|
+
const { data, error } = await createLayout({
|
|
162
|
+
name: 'Main Lobby View',
|
|
163
|
+
settings
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// Create with panes
|
|
167
|
+
const { data } = await createLayout({
|
|
168
|
+
name: 'Entrance Cameras',
|
|
169
|
+
settings,
|
|
170
|
+
panes: [
|
|
171
|
+
{ id: 1, name: 'Front Door', type: 'preview', size: 2, cameraId: 'cam-123' },
|
|
172
|
+
{ id: 2, name: 'Side Gate', type: 'preview', size: 1, cameraId: 'cam-456' }
|
|
173
|
+
]
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
if (data) {
|
|
177
|
+
console.log(`Created layout: ${data.id}`)
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### updateLayout(layoutId, params)
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { updateLayout } from 'een-api-toolkit'
|
|
185
|
+
|
|
186
|
+
// Update name
|
|
187
|
+
const { error } = await updateLayout('layout-123', {
|
|
188
|
+
name: 'Updated Layout Name'
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Update settings (partial)
|
|
192
|
+
const { error } = await updateLayout('layout-123', {
|
|
193
|
+
settings: {
|
|
194
|
+
paneColumns: 4,
|
|
195
|
+
showCameraName: false
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Replace panes
|
|
200
|
+
const { error } = await updateLayout('layout-123', {
|
|
201
|
+
panes: [
|
|
202
|
+
{ id: 1, name: 'New Pane', type: 'preview', size: 1, cameraId: 'cam-789' }
|
|
203
|
+
]
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
if (!error) {
|
|
207
|
+
console.log('Layout updated successfully')
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### deleteLayout(layoutId)
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { deleteLayout } from 'een-api-toolkit'
|
|
215
|
+
|
|
216
|
+
const { error } = await deleteLayout('layout-123')
|
|
217
|
+
|
|
218
|
+
if (error) {
|
|
219
|
+
if (error.code === 'NOT_FOUND') {
|
|
220
|
+
console.log('Layout already deleted')
|
|
221
|
+
} else if (error.code === 'FORBIDDEN') {
|
|
222
|
+
console.log('No permission to delete')
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
console.log('Layout deleted successfully')
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Filter Patterns
|
|
232
|
+
|
|
233
|
+
| Filter | Example | Description |
|
|
234
|
+
|--------|---------|-------------|
|
|
235
|
+
| `name` | `'Main Lobby'` | Exact name match |
|
|
236
|
+
| `name__in` | `['Lobby', 'Entrance']` | Any name matches |
|
|
237
|
+
| `name__contains` | `'lobby'` | Partial name match |
|
|
238
|
+
| `id__in` | `['id1', 'id2']` | Filter by IDs |
|
|
239
|
+
| `q` | `'front door'` | Full-text search |
|
|
240
|
+
| `qRelevance__gte` | `0.5` | Min search relevance |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Vue Components
|
|
245
|
+
|
|
246
|
+
### Layouts.vue
|
|
247
|
+
|
|
248
|
+
```vue
|
|
249
|
+
<script setup lang="ts">
|
|
250
|
+
import { ref, computed, onMounted } from 'vue'
|
|
251
|
+
import { getLayouts, createLayout, updateLayout, deleteLayout, type Layout, type EenError, type ListLayoutsParams, type LayoutSettings } from 'een-api-toolkit'
|
|
252
|
+
|
|
253
|
+
const layouts = ref<Layout[]>([])
|
|
254
|
+
const loading = ref(false)
|
|
255
|
+
const error = ref<EenError | null>(null)
|
|
256
|
+
const nextPageToken = ref<string | undefined>(undefined)
|
|
257
|
+
|
|
258
|
+
const hasNextPage = computed(() => !!nextPageToken.value)
|
|
259
|
+
|
|
260
|
+
const params = ref<ListLayoutsParams>({
|
|
261
|
+
pageSize: 20,
|
|
262
|
+
include: ['resourceCounts', 'effectivePermissions']
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
async function fetchLayouts(fetchParams?: ListLayoutsParams, append = false) {
|
|
266
|
+
loading.value = true
|
|
267
|
+
error.value = null
|
|
268
|
+
|
|
269
|
+
const mergedParams = { ...params.value, ...fetchParams }
|
|
270
|
+
const result = await getLayouts(mergedParams)
|
|
271
|
+
|
|
272
|
+
if (result.error) {
|
|
273
|
+
error.value = result.error
|
|
274
|
+
if (!append) layouts.value = []
|
|
275
|
+
nextPageToken.value = undefined
|
|
276
|
+
} else {
|
|
277
|
+
if (append) {
|
|
278
|
+
layouts.value = [...layouts.value, ...result.data.results]
|
|
279
|
+
} else {
|
|
280
|
+
layouts.value = result.data.results
|
|
281
|
+
}
|
|
282
|
+
nextPageToken.value = result.data.nextPageToken
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
loading.value = false
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function fetchNextPage() {
|
|
289
|
+
if (!nextPageToken.value) return
|
|
290
|
+
return fetchLayouts({ ...params.value, pageToken: nextPageToken.value }, true)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function handleCreate(name: string, settings: LayoutSettings) {
|
|
294
|
+
const result = await createLayout({ name, settings })
|
|
295
|
+
if (result.data) {
|
|
296
|
+
await fetchLayouts()
|
|
297
|
+
}
|
|
298
|
+
return result
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function handleUpdate(layoutId: string, name: string, settings: Partial<LayoutSettings>) {
|
|
302
|
+
const result = await updateLayout(layoutId, { name, settings })
|
|
303
|
+
if (!result.error) {
|
|
304
|
+
await fetchLayouts()
|
|
305
|
+
}
|
|
306
|
+
return result
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function handleDelete(layoutId: string) {
|
|
310
|
+
const result = await deleteLayout(layoutId)
|
|
311
|
+
if (!result.error) {
|
|
312
|
+
await fetchLayouts()
|
|
313
|
+
}
|
|
314
|
+
return result
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
onMounted(() => fetchLayouts())
|
|
318
|
+
</script>
|
|
319
|
+
|
|
320
|
+
<template>
|
|
321
|
+
<div class="layouts">
|
|
322
|
+
<div class="header">
|
|
323
|
+
<h2>Layouts</h2>
|
|
324
|
+
<button @click="fetchLayouts" :disabled="loading">
|
|
325
|
+
{{ loading ? 'Loading...' : 'Refresh' }}
|
|
326
|
+
</button>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<div v-if="loading && layouts.length === 0" class="loading">
|
|
330
|
+
Loading layouts...
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div v-else-if="error" class="error">
|
|
334
|
+
Error: {{ error.message }}
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div v-else>
|
|
338
|
+
<div v-if="layouts.length > 0" class="layout-grid">
|
|
339
|
+
<div v-for="layout in layouts" :key="layout.id" class="layout-card">
|
|
340
|
+
<h3>{{ layout.name }}</h3>
|
|
341
|
+
<p>Panes: {{ layout.panes.length }}</p>
|
|
342
|
+
<p>Columns: {{ layout.settings.paneColumns }}</p>
|
|
343
|
+
<div v-if="layout.effectivePermissions">
|
|
344
|
+
<span v-if="layout.effectivePermissions.edit">Can Edit</span>
|
|
345
|
+
<span v-if="layout.effectivePermissions.delete">Can Delete</span>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<p v-else>No layouts found.</p>
|
|
351
|
+
|
|
352
|
+
<div v-if="hasNextPage" class="pagination">
|
|
353
|
+
<button @click="fetchNextPage" :disabled="loading">
|
|
354
|
+
{{ loading ? 'Loading...' : 'Load More' }}
|
|
355
|
+
</button>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</template>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Error Handling
|
|
365
|
+
|
|
366
|
+
| Error Code | HTTP Status | Meaning | Action |
|
|
367
|
+
|------------|-------------|---------|--------|
|
|
368
|
+
| AUTH_REQUIRED | 401 | Not authenticated | Redirect to login |
|
|
369
|
+
| FORBIDDEN | 403 | No permission | Show access denied |
|
|
370
|
+
| NOT_FOUND | 404 | Layout doesn't exist | Show "not found" |
|
|
371
|
+
| VALIDATION_ERROR | 400 | Invalid request | Show validation error |
|
|
372
|
+
| RATE_LIMITED | 429 | Too many requests | Retry with backoff |
|
|
373
|
+
| API_ERROR | 5xx | Server error | Show error, allow retry |
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Best Practices
|
|
378
|
+
|
|
379
|
+
1. **Check permissions before edit/delete**
|
|
380
|
+
```typescript
|
|
381
|
+
if (layout.effectivePermissions?.edit) {
|
|
382
|
+
// Show edit button
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
2. **Use includes sparingly** - Only request fields you need
|
|
387
|
+
```typescript
|
|
388
|
+
// Only include what you'll display
|
|
389
|
+
const { data } = await getLayouts({
|
|
390
|
+
include: ['resourceCounts'] // Skip permissions if not needed
|
|
391
|
+
})
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
3. **Handle empty panes array**
|
|
395
|
+
```typescript
|
|
396
|
+
// Layout can have empty panes array
|
|
397
|
+
const paneCount = layout.panes?.length || 0
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
4. **Validate panes before save**
|
|
401
|
+
```typescript
|
|
402
|
+
// Remove panes without cameras
|
|
403
|
+
const validPanes = panes.filter(p => p.cameraId)
|
|
404
|
+
await updateLayout(layoutId, { panes: validPanes })
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Reference Examples
|
|
410
|
+
|
|
411
|
+
- `examples/vue-layouts/` - Complete CRUD example with modal
|
|
Binary file
|
|
@@ -20,7 +20,7 @@ const TIMEOUTS = {
|
|
|
20
20
|
OAUTH_REDIRECT: 30000,
|
|
21
21
|
ELEMENT_VISIBLE: 15000,
|
|
22
22
|
PASSWORD_VISIBLE: 10000,
|
|
23
|
-
AUTH_COMPLETE:
|
|
23
|
+
AUTH_COMPLETE: 45000, // Increased for CI stability (OAuth callback can be slow)
|
|
24
24
|
UI_UPDATE: 10000,
|
|
25
25
|
PROXY_CHECK: 5000
|
|
26
26
|
} as const
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"vue-router": "^4.2.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@playwright/test": "
|
|
20
|
+
"@playwright/test": "1.58.0",
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.0",
|
|
22
22
|
"dotenv": "^17.2.3",
|
|
23
23
|
"typescript": "~5.8.0",
|
|
@@ -26,12 +26,15 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"../..": {
|
|
29
|
-
"version": "0.3.
|
|
29
|
+
"version": "0.3.34",
|
|
30
30
|
"license": "MIT",
|
|
31
|
+
"bin": {
|
|
32
|
+
"een-setup-agents": "scripts/setup-agents.ts"
|
|
33
|
+
},
|
|
31
34
|
"devDependencies": {
|
|
32
35
|
"@eslint/js": "^9.39.2",
|
|
33
36
|
"@marp-team/marp-cli": "^4.2.3",
|
|
34
|
-
"@playwright/test": "
|
|
37
|
+
"@playwright/test": "1.58.0",
|
|
35
38
|
"@types/node": "^25.0.3",
|
|
36
39
|
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
37
40
|
"@typescript-eslint/parser": "^8.51.0",
|
|
@@ -564,13 +567,13 @@
|
|
|
564
567
|
"license": "MIT"
|
|
565
568
|
},
|
|
566
569
|
"node_modules/@playwright/test": {
|
|
567
|
-
"version": "1.
|
|
568
|
-
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.
|
|
569
|
-
"integrity": "sha512-
|
|
570
|
+
"version": "1.58.0",
|
|
571
|
+
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz",
|
|
572
|
+
"integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==",
|
|
570
573
|
"dev": true,
|
|
571
574
|
"license": "Apache-2.0",
|
|
572
575
|
"dependencies": {
|
|
573
|
-
"playwright": "1.
|
|
576
|
+
"playwright": "1.58.0"
|
|
574
577
|
},
|
|
575
578
|
"bin": {
|
|
576
579
|
"playwright": "cli.js"
|
|
@@ -1415,13 +1418,13 @@
|
|
|
1415
1418
|
}
|
|
1416
1419
|
},
|
|
1417
1420
|
"node_modules/playwright": {
|
|
1418
|
-
"version": "1.
|
|
1419
|
-
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.
|
|
1420
|
-
"integrity": "sha512-
|
|
1421
|
+
"version": "1.58.0",
|
|
1422
|
+
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
|
|
1423
|
+
"integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
|
|
1421
1424
|
"dev": true,
|
|
1422
1425
|
"license": "Apache-2.0",
|
|
1423
1426
|
"dependencies": {
|
|
1424
|
-
"playwright-core": "1.
|
|
1427
|
+
"playwright-core": "1.58.0"
|
|
1425
1428
|
},
|
|
1426
1429
|
"bin": {
|
|
1427
1430
|
"playwright": "cli.js"
|
|
@@ -1434,9 +1437,9 @@
|
|
|
1434
1437
|
}
|
|
1435
1438
|
},
|
|
1436
1439
|
"node_modules/playwright-core": {
|
|
1437
|
-
"version": "1.
|
|
1438
|
-
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.
|
|
1439
|
-
"integrity": "sha512-
|
|
1440
|
+
"version": "1.58.0",
|
|
1441
|
+
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
|
|
1442
|
+
"integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
|
|
1440
1443
|
"dev": true,
|
|
1441
1444
|
"license": "Apache-2.0",
|
|
1442
1445
|
"bin": {
|