een-api-toolkit 0.3.85 → 0.3.91

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 (50) hide show
  1. package/.claude/agents/een-devices-agent.md +21 -0
  2. package/.claude/agents/een-ptz-agent.md +235 -0
  3. package/CHANGELOG.md +51 -22
  4. package/dist/index.cjs +3 -3
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.ts +400 -0
  7. package/dist/index.js +1079 -951
  8. package/dist/index.js.map +1 -1
  9. package/docs/AI-CONTEXT.md +3 -1
  10. package/docs/ai-reference/AI-AUTH.md +1 -1
  11. package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
  12. package/docs/ai-reference/AI-DEVICES.md +1 -1
  13. package/docs/ai-reference/AI-EVENT-DATA-SCHEMAS.md +1 -1
  14. package/docs/ai-reference/AI-EVENTS.md +1 -1
  15. package/docs/ai-reference/AI-GROUPING.md +1 -1
  16. package/docs/ai-reference/AI-JOBS.md +1 -1
  17. package/docs/ai-reference/AI-MEDIA.md +1 -1
  18. package/docs/ai-reference/AI-PTZ.md +158 -0
  19. package/docs/ai-reference/AI-SETUP.md +1 -1
  20. package/docs/ai-reference/AI-USERS.md +1 -1
  21. package/examples/vue-ptz/.env.example +4 -0
  22. package/examples/vue-ptz/README.md +221 -0
  23. package/examples/vue-ptz/e2e/app.spec.ts +58 -0
  24. package/examples/vue-ptz/e2e/auth.spec.ts +296 -0
  25. package/examples/vue-ptz/index.html +13 -0
  26. package/examples/vue-ptz/package-lock.json +1729 -0
  27. package/examples/vue-ptz/package.json +29 -0
  28. package/examples/vue-ptz/playwright.config.ts +49 -0
  29. package/examples/vue-ptz/screenshot-ptz.png +0 -0
  30. package/examples/vue-ptz/src/App.vue +154 -0
  31. package/examples/vue-ptz/src/components/ApiLog.vue +387 -0
  32. package/examples/vue-ptz/src/components/CameraSelector.vue +155 -0
  33. package/examples/vue-ptz/src/components/DirectionPad.vue +350 -0
  34. package/examples/vue-ptz/src/components/LiveVideoPlayer.vue +248 -0
  35. package/examples/vue-ptz/src/components/PositionDisplay.vue +206 -0
  36. package/examples/vue-ptz/src/components/PositionInput.vue +190 -0
  37. package/examples/vue-ptz/src/components/PresetManager.vue +538 -0
  38. package/examples/vue-ptz/src/composables/useApiLog.ts +89 -0
  39. package/examples/vue-ptz/src/main.ts +22 -0
  40. package/examples/vue-ptz/src/router/index.ts +61 -0
  41. package/examples/vue-ptz/src/views/Callback.vue +76 -0
  42. package/examples/vue-ptz/src/views/Home.vue +199 -0
  43. package/examples/vue-ptz/src/views/Login.vue +32 -0
  44. package/examples/vue-ptz/src/views/Logout.vue +59 -0
  45. package/examples/vue-ptz/src/views/PtzControl.vue +173 -0
  46. package/examples/vue-ptz/src/vite-env.d.ts +12 -0
  47. package/examples/vue-ptz/tsconfig.json +21 -0
  48. package/examples/vue-ptz/tsconfig.node.json +11 -0
  49. package/examples/vue-ptz/vite.config.ts +12 -0
  50. package/package.json +1 -1
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "een-api-toolkit-ptz-example",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "stop": "lsof -ti :3333 2>/dev/null | xargs -r kill -9 || echo 'Port 3333 is free'",
8
+ "dev": "npm run stop && vite",
9
+ "build": "vue-tsc && vite build",
10
+ "preview": "vite preview",
11
+ "test:e2e": "playwright test",
12
+ "test:e2e:ui": "playwright test --ui"
13
+ },
14
+ "dependencies": {
15
+ "@een/live-video-web-sdk": "^1.10.2",
16
+ "een-api-toolkit": "file:../..",
17
+ "pinia": "^3.0.4",
18
+ "vue": "^3.4.0",
19
+ "vue-router": "^4.2.0"
20
+ },
21
+ "devDependencies": {
22
+ "@playwright/test": "1.58.0",
23
+ "@vitejs/plugin-vue": "^6.0.0",
24
+ "dotenv": "^17.2.3",
25
+ "typescript": "~5.8.0",
26
+ "vite": "^7.3.0",
27
+ "vue-tsc": "^3.2.1"
28
+ }
29
+ }
@@ -0,0 +1,49 @@
1
+ import { defineConfig, devices } from '@playwright/test'
2
+ import dotenv from 'dotenv'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ // Load .env files: parent .env first, then local .env overrides parent values
9
+ dotenv.config({ path: path.resolve(__dirname, '../../.env') })
10
+ dotenv.config({ path: path.resolve(__dirname, '.env'), override: true })
11
+
12
+ const redirectUri = process.env.VITE_REDIRECT_URI || 'http://127.0.0.1:3333'
13
+ if (!redirectUri.startsWith('http://127.0.0.1:') && !redirectUri.startsWith('http://localhost:')) {
14
+ throw new Error('VITE_REDIRECT_URI must use localhost or 127.0.0.1 for security')
15
+ }
16
+ const baseURL = redirectUri
17
+
18
+ // Export for use in test files
19
+ export { baseURL }
20
+
21
+ export default defineConfig({
22
+ testDir: './e2e',
23
+ testMatch: '**/*.spec.ts',
24
+ fullyParallel: false,
25
+ forbidOnly: !!process.env.CI,
26
+ retries: 0,
27
+ maxFailures: 1,
28
+ workers: 1,
29
+ reporter: [['html', { open: 'never' }]],
30
+ timeout: 30000,
31
+ use: {
32
+ baseURL,
33
+ trace: 'on-first-retry',
34
+ video: 'retain-on-failure'
35
+ },
36
+ outputDir: './e2e-results/',
37
+ projects: [
38
+ {
39
+ name: 'chromium',
40
+ use: { ...devices['Desktop Chrome'] }
41
+ }
42
+ ],
43
+ webServer: {
44
+ command: 'npm run dev',
45
+ url: baseURL,
46
+ reuseExistingServer: !process.env.CI,
47
+ timeout: 30000
48
+ }
49
+ })
@@ -0,0 +1,154 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, computed, watch } from 'vue'
3
+ import { useAuthStore, getCurrentUser } from 'een-api-toolkit'
4
+
5
+ const authStore = useAuthStore()
6
+ const isAuthenticated = computed(() => authStore.isAuthenticated)
7
+
8
+ // Initialize auth store from localStorage on app mount
9
+ onMounted(() => {
10
+ authStore.initialize()
11
+ })
12
+
13
+ // Fetch user profile when authenticated
14
+ watch(
15
+ () => authStore.isAuthenticated,
16
+ (authenticated) => {
17
+ if (authenticated && !authStore.userProfile) {
18
+ getCurrentUser()
19
+ }
20
+ },
21
+ { immediate: true }
22
+ )
23
+ </script>
24
+
25
+ <template>
26
+ <div id="app">
27
+ <header>
28
+ <div class="header-top">
29
+ <h1>EEN PTZ Control Example</h1>
30
+ <p v-if="authStore.userProfile" class="user-info" data-testid="header-user-info">
31
+ {{ authStore.userProfile.firstName }} {{ authStore.userProfile.lastName }}
32
+ ({{ authStore.userProfile.id }})
33
+ </p>
34
+ </div>
35
+ <nav>
36
+ <router-link to="/" data-testid="nav-home">Home</router-link>
37
+ <router-link v-if="!isAuthenticated" to="/login" data-testid="nav-login">Login</router-link>
38
+ <router-link v-if="isAuthenticated" to="/ptz" data-testid="nav-ptz">PTZ Control</router-link>
39
+ <router-link v-if="isAuthenticated" to="/logout" data-testid="nav-logout">Logout</router-link>
40
+ </nav>
41
+ </header>
42
+ <main>
43
+ <router-view />
44
+ </main>
45
+ </div>
46
+ </template>
47
+
48
+ <style>
49
+ * {
50
+ box-sizing: border-box;
51
+ margin: 0;
52
+ padding: 0;
53
+ }
54
+
55
+ body {
56
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
57
+ line-height: 1.6;
58
+ color: #333;
59
+ background: #f5f5f5;
60
+ }
61
+
62
+ #app {
63
+ max-width: 1200px;
64
+ margin: 0 auto;
65
+ padding: 20px;
66
+ }
67
+
68
+ header {
69
+ background: #2c3e50;
70
+ color: white;
71
+ padding: 20px;
72
+ border-radius: 8px;
73
+ margin-bottom: 20px;
74
+ }
75
+
76
+ .header-top {
77
+ display: flex;
78
+ justify-content: space-between;
79
+ align-items: baseline;
80
+ margin-bottom: 15px;
81
+ }
82
+
83
+ .user-info {
84
+ font-size: 0.85rem;
85
+ color: rgba(255, 255, 255, 0.7);
86
+ }
87
+
88
+ nav {
89
+ display: flex;
90
+ gap: 15px;
91
+ }
92
+
93
+ nav a {
94
+ color: white;
95
+ text-decoration: none;
96
+ padding: 8px 16px;
97
+ border-radius: 4px;
98
+ transition: background-color 0.2s;
99
+ }
100
+
101
+ nav a:hover {
102
+ background: rgba(255, 255, 255, 0.1);
103
+ }
104
+
105
+ nav a.router-link-active {
106
+ background: rgba(255, 255, 255, 0.2);
107
+ }
108
+
109
+ main {
110
+ background: white;
111
+ padding: 30px;
112
+ border-radius: 8px;
113
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
114
+ }
115
+
116
+ h2 {
117
+ color: #2c3e50;
118
+ margin-bottom: 20px;
119
+ }
120
+
121
+ button {
122
+ background: #3498db;
123
+ color: white;
124
+ border: none;
125
+ padding: 10px 20px;
126
+ border-radius: 4px;
127
+ cursor: pointer;
128
+ font-size: 1rem;
129
+ transition: background-color 0.2s;
130
+ }
131
+
132
+ button:hover {
133
+ background: #2980b9;
134
+ }
135
+
136
+ button:disabled {
137
+ background: #bdc3c7;
138
+ cursor: not-allowed;
139
+ }
140
+
141
+ .loading {
142
+ text-align: center;
143
+ padding: 40px;
144
+ color: #666;
145
+ }
146
+
147
+ .error {
148
+ color: #e74c3c;
149
+ padding: 15px;
150
+ background: #fdf0ef;
151
+ border-radius: 4px;
152
+ margin-bottom: 20px;
153
+ }
154
+ </style>
@@ -0,0 +1,387 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { useApiLog, matchPresetName, isHomePreset, type ApiLogEntry } from '../composables/useApiLog'
4
+
5
+ const props = defineProps<{
6
+ cameraId: string | null
7
+ }>()
8
+
9
+ const { entries, clear } = useApiLog()
10
+ const selectedEntry = ref<ApiLogEntry | null>(null)
11
+
12
+ function formatTime(date: Date): string {
13
+ const h = String(date.getHours()).padStart(2, '0')
14
+ const m = String(date.getMinutes()).padStart(2, '0')
15
+ const s = String(date.getSeconds()).padStart(2, '0')
16
+ const ms = String(date.getMilliseconds()).padStart(3, '0')
17
+ return `${h}:${m}:${s}.${ms}`
18
+ }
19
+
20
+ function closeModal() {
21
+ selectedEntry.value = null
22
+ }
23
+
24
+ function handleOverlayClick(event: MouseEvent) {
25
+ if (event.target === event.currentTarget) {
26
+ closeModal()
27
+ }
28
+ }
29
+
30
+ function fmt(n: unknown): string {
31
+ return typeof n === 'number' ? n.toFixed(2) : '?'
32
+ }
33
+
34
+ function entryIsAtHome(entry: ApiLogEntry): boolean {
35
+ if (entry.error || entry.functionName !== 'getPtzPosition') return false
36
+ const r = entry.response as Record<string, unknown> | undefined
37
+ if (!r) return false
38
+ const name = matchPresetName(r as { x?: number; y?: number; z?: number })
39
+ return isHomePreset(name)
40
+ }
41
+
42
+ function entrySummary(entry: ApiLogEntry): string {
43
+ const p = entry.params as Record<string, unknown> | undefined
44
+ const r = entry.response as Record<string, unknown> | undefined
45
+ if (entry.error) {
46
+ const r = entry.response as Record<string, unknown> | undefined
47
+ if (!r) return ''
48
+ const parts: string[] = []
49
+ if (r.status != null) parts.push(`Status: ${r.status}`)
50
+ if (r.message) parts.push(String(r.message))
51
+ return parts.join(' - ')
52
+ }
53
+ switch (entry.functionName) {
54
+ case 'getPtzPosition': {
55
+ if (!r) return ''
56
+ const name = matchPresetName(r as { x?: number; y?: number; z?: number })
57
+ const pos = `x=${fmt(r.x)} y=${fmt(r.y)} z=${fmt(r.z)}`
58
+ return name ? `[${name}] ${pos}` : pos
59
+ }
60
+ case 'movePtz': {
61
+ const move = p?.move as Record<string, unknown> | undefined
62
+ if (!move) return ''
63
+ const mt = move.moveType as string
64
+ if (mt === 'direction') {
65
+ const dir = (move.direction as string[])?.join(',') ?? ''
66
+ return `direction ${dir} ${move.stepSize ?? ''}`
67
+ }
68
+ if (mt === 'centerOn') {
69
+ return `centerOn x=${fmt(move.relativeX)} y=${fmt(move.relativeY)}`
70
+ }
71
+ if (mt === 'position') {
72
+ return `position x=${fmt(move.x)} y=${fmt(move.y)} z=${fmt(move.z)}`
73
+ }
74
+ return mt
75
+ }
76
+ case 'getPtzSettings': {
77
+ if (!r) return ''
78
+ const presets = r.presets as unknown[] | undefined
79
+ return `mode=${r.mode ?? '?'} presets=${presets?.length ?? 0}`
80
+ }
81
+ case 'updatePtzSettings': {
82
+ const s = p?.settings as Record<string, unknown> | undefined
83
+ if (!s) return ''
84
+ return Object.keys(s).join(', ')
85
+ }
86
+ default:
87
+ return ''
88
+ }
89
+ }
90
+ </script>
91
+
92
+ <template>
93
+ <div class="api-log" data-testid="api-log">
94
+ <div class="log-header">
95
+ <h3>API Call Log ({{ entries.length }})</h3>
96
+ <button v-if="entries.length > 0" @click="clear" class="clear-btn" data-testid="clear-log">
97
+ Clear
98
+ </button>
99
+ </div>
100
+
101
+ <div v-if="entries.length === 0" class="empty-log">
102
+ No API calls logged yet.
103
+ </div>
104
+
105
+ <div v-else class="log-list" data-testid="log-list">
106
+ <div
107
+ v-for="entry in entries"
108
+ :key="entry.id"
109
+ class="log-entry"
110
+ :class="{ 'log-entry-error': entry.error }"
111
+ @click="selectedEntry = entry"
112
+ data-testid="log-entry"
113
+ >
114
+ <span class="log-time">{{ formatTime(entry.timestamp) }}</span>
115
+ <span class="log-fn" :class="{ 'log-fn-error': entry.error }">
116
+ {{ entry.functionName }}
117
+ </span>
118
+ <svg v-if="entryIsAtHome(entry)" class="log-home-icon" viewBox="0 0 24 24" width="14" height="14" fill="#27ae60">
119
+ <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
120
+ </svg>
121
+ <span v-if="entrySummary(entry)" class="log-summary">{{ entrySummary(entry) }}</span>
122
+ <span v-if="entry.count > 1" class="log-count">{{ entry.count }}</span>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Modal -->
127
+ <div v-if="selectedEntry" class="modal-overlay" @click="handleOverlayClick" data-testid="log-modal">
128
+ <div class="modal-content">
129
+ <div class="modal-header">
130
+ <h3>{{ selectedEntry.functionName }}</h3>
131
+ <button @click="closeModal" class="modal-close" data-testid="close-modal">&times;</button>
132
+ </div>
133
+ <div class="modal-body">
134
+ <div v-if="props.cameraId" class="modal-section">
135
+ <h4>Camera</h4>
136
+ <pre><code>{{ props.cameraId }}</code></pre>
137
+ </div>
138
+ <div class="modal-section">
139
+ <h4>Time (last call)</h4>
140
+ <pre>{{ selectedEntry.timestamp.toISOString() }}</pre>
141
+ </div>
142
+ <div v-if="selectedEntry.count > 1" class="modal-section">
143
+ <h4>Repeat Count</h4>
144
+ <pre>{{ selectedEntry.count }} identical calls</pre>
145
+ </div>
146
+ <div class="modal-section">
147
+ <h4>Parameters</h4>
148
+ <pre class="json-block">{{ JSON.stringify(selectedEntry.params, null, 2) }}</pre>
149
+ </div>
150
+ <div class="modal-section">
151
+ <h4>Response</h4>
152
+ <pre class="json-block" :class="{ 'json-error': selectedEntry.error }">{{ JSON.stringify(selectedEntry.response, null, 2) }}</pre>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </template>
159
+
160
+ <style scoped>
161
+ .api-log {
162
+ margin-top: 20px;
163
+ border: 1px solid #ddd;
164
+ border-radius: 8px;
165
+ background: #f8f9fa;
166
+ flex: 1;
167
+ display: flex;
168
+ flex-direction: column;
169
+ min-height: 0;
170
+ overflow: hidden;
171
+ }
172
+
173
+ .log-header {
174
+ display: flex;
175
+ justify-content: space-between;
176
+ align-items: center;
177
+ padding: 10px 15px;
178
+ border-bottom: 1px solid #ddd;
179
+ }
180
+
181
+ .log-header h3 {
182
+ font-size: 14px;
183
+ color: #2c3e50;
184
+ margin: 0;
185
+ }
186
+
187
+ .clear-btn {
188
+ padding: 4px 10px;
189
+ font-size: 11px;
190
+ background: #6c757d;
191
+ border-radius: 4px;
192
+ }
193
+
194
+ .clear-btn:hover {
195
+ background: #5a6268;
196
+ }
197
+
198
+ .empty-log {
199
+ padding: 20px;
200
+ text-align: center;
201
+ color: #999;
202
+ font-style: italic;
203
+ font-size: 13px;
204
+ }
205
+
206
+ .log-list {
207
+ flex: 1;
208
+ overflow-y: auto;
209
+ min-height: 0;
210
+ }
211
+
212
+ .log-entry {
213
+ display: flex;
214
+ align-items: center;
215
+ gap: 10px;
216
+ padding: 6px 15px;
217
+ cursor: pointer;
218
+ border-bottom: 1px solid #eee;
219
+ font-size: 13px;
220
+ transition: background 0.1s;
221
+ overflow: hidden;
222
+ }
223
+
224
+ .log-entry:last-child {
225
+ border-bottom: none;
226
+ }
227
+
228
+ .log-entry:hover {
229
+ background: #e9ecef;
230
+ }
231
+
232
+ .log-entry-error {
233
+ background: #fff5f5;
234
+ }
235
+
236
+ .log-entry-error:hover {
237
+ background: #ffe0e0;
238
+ }
239
+
240
+ .log-time {
241
+ font-family: monospace;
242
+ color: #888;
243
+ white-space: nowrap;
244
+ font-size: 12px;
245
+ }
246
+
247
+ .log-fn {
248
+ font-weight: 600;
249
+ color: #2c3e50;
250
+ white-space: nowrap;
251
+ flex-shrink: 0;
252
+ }
253
+
254
+ .log-fn-error {
255
+ color: #e74c3c;
256
+ }
257
+
258
+ .log-home-icon {
259
+ flex-shrink: 0;
260
+ }
261
+
262
+ .log-summary {
263
+ color: #888;
264
+ font-family: monospace;
265
+ font-size: 11px;
266
+ white-space: nowrap;
267
+ overflow: hidden;
268
+ text-overflow: ellipsis;
269
+ min-width: 0;
270
+ flex: 1;
271
+ }
272
+
273
+ .log-count {
274
+ flex-shrink: 0;
275
+ background: #6c757d;
276
+ color: white;
277
+ font-size: 11px;
278
+ font-weight: 600;
279
+ padding: 0 6px;
280
+ border-radius: 10px;
281
+ min-width: 20px;
282
+ text-align: center;
283
+ line-height: 18px;
284
+ }
285
+
286
+ .log-entry-error .log-count {
287
+ background: #e74c3c;
288
+ }
289
+
290
+ /* Modal */
291
+ .modal-overlay {
292
+ position: fixed;
293
+ top: 0;
294
+ left: 0;
295
+ right: 0;
296
+ bottom: 0;
297
+ background: rgba(0, 0, 0, 0.5);
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ z-index: 1000;
302
+ }
303
+
304
+ .modal-content {
305
+ background: white;
306
+ border-radius: 8px;
307
+ width: 90%;
308
+ max-width: 700px;
309
+ max-height: 80vh;
310
+ display: flex;
311
+ flex-direction: column;
312
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
313
+ }
314
+
315
+ .modal-header {
316
+ display: flex;
317
+ justify-content: space-between;
318
+ align-items: center;
319
+ padding: 12px 20px;
320
+ border-bottom: 1px solid #ddd;
321
+ }
322
+
323
+ .modal-header h3 {
324
+ margin: 0;
325
+ font-size: 16px;
326
+ color: #2c3e50;
327
+ }
328
+
329
+ .modal-close {
330
+ background: none;
331
+ border: none;
332
+ font-size: 24px;
333
+ color: #666;
334
+ cursor: pointer;
335
+ padding: 0 4px;
336
+ line-height: 1;
337
+ }
338
+
339
+ .modal-close:hover {
340
+ color: #333;
341
+ }
342
+
343
+ .modal-body {
344
+ padding: 15px 20px;
345
+ overflow-y: auto;
346
+ }
347
+
348
+ .modal-section {
349
+ margin-bottom: 15px;
350
+ }
351
+
352
+ .modal-section:last-child {
353
+ margin-bottom: 0;
354
+ }
355
+
356
+ .modal-section h4 {
357
+ font-size: 12px;
358
+ color: #666;
359
+ margin-bottom: 4px;
360
+ text-transform: uppercase;
361
+ }
362
+
363
+ .json-block {
364
+ background: #f4f4f4;
365
+ padding: 10px;
366
+ border-radius: 4px;
367
+ font-size: 12px;
368
+ font-family: monospace;
369
+ overflow-x: auto;
370
+ white-space: pre-wrap;
371
+ word-break: break-word;
372
+ max-height: 200px;
373
+ overflow-y: auto;
374
+ margin: 0;
375
+ }
376
+
377
+ .json-error {
378
+ background: #fff5f5;
379
+ border: 1px solid #f8d7da;
380
+ }
381
+
382
+ pre {
383
+ margin: 0;
384
+ font-family: monospace;
385
+ font-size: 12px;
386
+ }
387
+ </style>