qdadm 0.30.0 → 0.32.0

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 (47) hide show
  1. package/package.json +2 -1
  2. package/src/components/forms/FormPage.vue +1 -1
  3. package/src/components/layout/AppLayout.vue +13 -1
  4. package/src/components/layout/Zone.vue +40 -23
  5. package/src/composables/index.js +1 -0
  6. package/src/composables/useAuth.js +44 -4
  7. package/src/composables/useCurrentEntity.js +44 -0
  8. package/src/composables/useFormPageBuilder.js +3 -3
  9. package/src/composables/useNavContext.js +24 -8
  10. package/src/debug/AuthCollector.js +340 -0
  11. package/src/debug/Collector.js +235 -0
  12. package/src/debug/DebugBridge.js +163 -0
  13. package/src/debug/DebugModule.js +215 -0
  14. package/src/debug/EntitiesCollector.js +403 -0
  15. package/src/debug/ErrorCollector.js +66 -0
  16. package/src/debug/LocalStorageAdapter.js +150 -0
  17. package/src/debug/SignalCollector.js +87 -0
  18. package/src/debug/ToastCollector.js +82 -0
  19. package/src/debug/ZonesCollector.js +300 -0
  20. package/src/debug/components/DebugBar.vue +1232 -0
  21. package/src/debug/components/ObjectTree.vue +194 -0
  22. package/src/debug/components/index.js +8 -0
  23. package/src/debug/components/panels/AuthPanel.vue +174 -0
  24. package/src/debug/components/panels/EntitiesPanel.vue +712 -0
  25. package/src/debug/components/panels/EntriesPanel.vue +188 -0
  26. package/src/debug/components/panels/ToastsPanel.vue +112 -0
  27. package/src/debug/components/panels/ZonesPanel.vue +232 -0
  28. package/src/debug/components/panels/index.js +8 -0
  29. package/src/debug/index.js +31 -0
  30. package/src/entity/EntityManager.js +142 -20
  31. package/src/entity/auth/CompositeAuthAdapter.js +212 -0
  32. package/src/entity/auth/factory.js +207 -0
  33. package/src/entity/auth/factory.test.js +257 -0
  34. package/src/entity/auth/index.js +14 -0
  35. package/src/entity/storage/MockApiStorage.js +51 -2
  36. package/src/entity/storage/index.js +9 -2
  37. package/src/index.js +7 -0
  38. package/src/kernel/Kernel.js +468 -48
  39. package/src/kernel/KernelContext.js +385 -0
  40. package/src/kernel/Module.js +111 -0
  41. package/src/kernel/ModuleLoader.js +573 -0
  42. package/src/kernel/SignalBus.js +2 -7
  43. package/src/kernel/index.js +14 -0
  44. package/src/toast/ToastBridgeModule.js +70 -0
  45. package/src/toast/ToastListener.vue +47 -0
  46. package/src/toast/index.js +15 -0
  47. package/src/toast/useSignalToast.js +113 -0
@@ -0,0 +1,712 @@
1
+ <script setup>
2
+ /**
3
+ * EntitiesPanel - Entities collector display with expandable details
4
+ */
5
+ import { ref, onMounted } from 'vue'
6
+ import ObjectTree from '../ObjectTree.vue'
7
+
8
+ const props = defineProps({
9
+ collector: { type: Object, required: true },
10
+ entries: { type: Array, required: true }
11
+ })
12
+
13
+ const emit = defineEmits(['update'])
14
+
15
+ // Mark all entities as seen when panel is viewed
16
+ onMounted(() => {
17
+ props.collector.markSeen?.()
18
+ })
19
+
20
+ const expandedEntities = ref(new Set())
21
+ const loadingCache = ref(new Set())
22
+ const testingFetch = ref(new Set())
23
+ const testResults = ref(new Map()) // entityName -> { success, count, error, status }
24
+
25
+ function toggleExpand(name) {
26
+ if (expandedEntities.value.has(name)) {
27
+ expandedEntities.value.delete(name)
28
+ } else {
29
+ expandedEntities.value.add(name)
30
+ }
31
+ // Trigger reactivity
32
+ expandedEntities.value = new Set(expandedEntities.value)
33
+ }
34
+
35
+ function isExpanded(name) {
36
+ return expandedEntities.value.has(name)
37
+ }
38
+
39
+ async function loadCache(entityName) {
40
+ if (loadingCache.value.has(entityName)) return
41
+ loadingCache.value.add(entityName)
42
+ loadingCache.value = new Set(loadingCache.value)
43
+ try {
44
+ await props.collector.refreshCache(entityName, true) // reload=true
45
+ emit('update')
46
+ } finally {
47
+ loadingCache.value.delete(entityName)
48
+ loadingCache.value = new Set(loadingCache.value)
49
+ }
50
+ }
51
+
52
+ function invalidateCache(entityName) {
53
+ props.collector.refreshCache(entityName, false) // just invalidate
54
+ emit('update')
55
+ }
56
+
57
+ function isLoading(name) {
58
+ return loadingCache.value.has(name)
59
+ }
60
+
61
+ function isTesting(name) {
62
+ return testingFetch.value.has(name)
63
+ }
64
+
65
+ function getTestResult(name) {
66
+ return testResults.value.get(name)
67
+ }
68
+
69
+ async function testFetch(entityName) {
70
+ if (testingFetch.value.has(entityName)) return
71
+ testingFetch.value.add(entityName)
72
+ testingFetch.value = new Set(testingFetch.value)
73
+ testResults.value.delete(entityName)
74
+ testResults.value = new Map(testResults.value)
75
+ try {
76
+ const result = await props.collector.testFetch(entityName)
77
+ testResults.value.set(entityName, result)
78
+ testResults.value = new Map(testResults.value)
79
+ } finally {
80
+ testingFetch.value.delete(entityName)
81
+ testingFetch.value = new Set(testingFetch.value)
82
+ }
83
+ }
84
+
85
+ function getCapabilityIcon(cap) {
86
+ const icons = {
87
+ supportsTotal: 'pi-hashtag',
88
+ supportsFilters: 'pi-filter',
89
+ supportsPagination: 'pi-ellipsis-h',
90
+ supportsCaching: 'pi-database',
91
+ requiresAuth: 'pi-lock'
92
+ }
93
+ return icons[cap] || 'pi-question-circle'
94
+ }
95
+
96
+ function getCapabilityLabel(cap) {
97
+ const labels = {
98
+ supportsTotal: 'Total count',
99
+ supportsFilters: 'Filters',
100
+ supportsPagination: 'Pagination',
101
+ supportsCaching: 'Caching',
102
+ requiresAuth: 'Requires authentication'
103
+ }
104
+ return labels[cap] || cap
105
+ }
106
+ </script>
107
+
108
+ <template>
109
+ <div class="entities-panel">
110
+ <div v-if="entries[0]?.type === 'status'" class="entities-status">
111
+ {{ entries[0].message }}
112
+ </div>
113
+ <div v-else v-for="entity in entries" :key="entity.name" class="entity-item" :class="{ 'entity-active': entity.hasActivity }">
114
+ <div class="entity-header" @click="toggleExpand(entity.name)">
115
+ <button class="entity-expand">
116
+ <i :class="['pi', isExpanded(entity.name) ? 'pi-chevron-down' : 'pi-chevron-right']" />
117
+ </button>
118
+ <i class="pi pi-database" />
119
+ <span class="entity-name">{{ entity.name }}</span>
120
+ <div class="entity-perms-icons">
121
+ <i
122
+ class="pi pi-plus"
123
+ :class="entity.permissions.canCreate ? 'perm-icon-granted' : 'perm-icon-denied'"
124
+ :title="entity.permissions.canCreate ? 'Create allowed' : 'Create denied'"
125
+ />
126
+ <i
127
+ class="pi pi-pencil"
128
+ :class="entity.permissions.canUpdate ? 'perm-icon-granted' : 'perm-icon-denied'"
129
+ :title="entity.permissions.canUpdate ? 'Update allowed' : 'Update denied'"
130
+ />
131
+ <i
132
+ class="pi pi-trash"
133
+ :class="entity.permissions.canDelete ? 'perm-icon-granted' : 'perm-icon-denied'"
134
+ :title="entity.permissions.canDelete ? 'Delete allowed' : 'Delete denied'"
135
+ />
136
+ <i
137
+ v-if="entity.permissions.readOnly"
138
+ class="pi pi-eye perm-icon-readonly"
139
+ title="Read-only entity"
140
+ />
141
+ </div>
142
+ <span class="entity-label">{{ entity.label }}</span>
143
+ <span v-if="entity.cache.enabled" class="entity-cache" :class="{ 'entity-cache-valid': entity.cache.valid }">
144
+ <i :class="['pi', entity.cache.valid ? 'pi-check-circle' : 'pi-hourglass']" />
145
+ <template v-if="entity.cache.valid">{{ entity.cache.itemCount }}/{{ entity.cache.total }}</template>
146
+ <template v-else>pending</template>
147
+ </span>
148
+ <span v-else class="entity-cache entity-cache-disabled">
149
+ <i class="pi pi-ban" />
150
+ </span>
151
+ </div>
152
+
153
+ <!-- Collapsed summary -->
154
+ <div v-if="!isExpanded(entity.name)" class="entity-summary">
155
+ <span class="entity-storage">{{ entity.storage.type }}</span>
156
+ <span v-if="entity.storage.endpoint" class="entity-endpoint">{{ entity.storage.endpoint }}</span>
157
+ <span class="entity-fields">{{ entity.fields.count }} fields</span>
158
+ </div>
159
+
160
+ <!-- Expanded details -->
161
+ <div v-else class="entity-details">
162
+ <div class="entity-row">
163
+ <span class="entity-key">Storage:</span>
164
+ <span class="entity-value">{{ entity.storage.type }}</span>
165
+ <span v-if="entity.storage.endpoint" class="entity-endpoint">{{ entity.storage.endpoint }}</span>
166
+ </div>
167
+ <div v-if="entity.storage.capabilities && Object.keys(entity.storage.capabilities).length > 0" class="entity-row">
168
+ <span class="entity-key">Caps:</span>
169
+ <div class="entity-capabilities">
170
+ <span
171
+ v-for="(enabled, cap) in entity.storage.capabilities"
172
+ :key="cap"
173
+ class="entity-cap"
174
+ :class="[cap === 'requiresAuth' && enabled ? 'entity-cap-auth' : (enabled ? 'entity-cap-enabled' : 'entity-cap-disabled')]"
175
+ :title="getCapabilityLabel(cap) + (enabled ? ' ✓' : ' ✗')"
176
+ >
177
+ <i :class="['pi', getCapabilityIcon(cap)]" />
178
+ </span>
179
+ </div>
180
+ </div>
181
+ <div v-if="entity.cache.enabled" class="entity-row">
182
+ <span class="entity-key">Cache:</span>
183
+ <span class="entity-value">
184
+ {{ entity.cache.valid ? 'Valid' : 'Not loaded' }}
185
+ <template v-if="entity.cache.valid">
186
+ ({{ entity.cache.itemCount }} items, threshold {{ entity.cache.threshold }})
187
+ </template>
188
+ </span>
189
+ <button
190
+ v-if="!entity.cache.valid"
191
+ class="entity-load-btn"
192
+ :disabled="isLoading(entity.name)"
193
+ @click.stop="loadCache(entity.name)"
194
+ >
195
+ <i :class="['pi', isLoading(entity.name) ? 'pi-spin pi-spinner' : 'pi-download']" />
196
+ {{ isLoading(entity.name) ? 'Loading...' : 'Load' }}
197
+ </button>
198
+ <button
199
+ v-else
200
+ class="entity-invalidate-btn"
201
+ title="Invalidate cache"
202
+ @click.stop="invalidateCache(entity.name)"
203
+ >
204
+ <i class="pi pi-times-circle" />
205
+ </button>
206
+ </div>
207
+ <div v-else class="entity-row">
208
+ <span class="entity-key">Cache:</span>
209
+ <span class="entity-value entity-cache-na">Disabled</span>
210
+ </div>
211
+ <!-- Test Fetch row - always visible for testing auth protection -->
212
+ <div class="entity-row">
213
+ <span class="entity-key">Test:</span>
214
+ <span v-if="getTestResult(entity.name)" class="entity-test-result" :class="getTestResult(entity.name).success ? 'test-success' : 'test-error'">
215
+ <template v-if="getTestResult(entity.name).success">
216
+ <i class="pi pi-check-circle" />
217
+ {{ getTestResult(entity.name).count }} items
218
+ </template>
219
+ <template v-else>
220
+ <i class="pi pi-times-circle" />
221
+ {{ getTestResult(entity.name).status || 'ERR' }}: {{ getTestResult(entity.name).error }}
222
+ </template>
223
+ </span>
224
+ <span v-else class="entity-value entity-test-na">-</span>
225
+ <button
226
+ class="entity-test-btn"
227
+ :disabled="isTesting(entity.name)"
228
+ @click.stop="testFetch(entity.name)"
229
+ >
230
+ <i :class="['pi', isTesting(entity.name) ? 'pi-spin pi-spinner' : 'pi-download']" />
231
+ {{ isTesting(entity.name) ? 'Testing...' : 'Fetch' }}
232
+ </button>
233
+ </div>
234
+ <div class="entity-row">
235
+ <span class="entity-key">Fields:</span>
236
+ <span class="entity-value">{{ entity.fields.count }} fields</span>
237
+ <span v-if="entity.fields.required.length > 0" class="entity-required">
238
+ {{ entity.fields.required.length }} required
239
+ </span>
240
+ </div>
241
+ <div v-if="entity.relations.parents.length > 0 || entity.relations.children.length > 0" class="entity-row">
242
+ <span class="entity-key">Relations:</span>
243
+ <span class="entity-value">
244
+ <span v-for="p in entity.relations.parents" :key="p.key" class="entity-relation">
245
+ <i class="pi pi-arrow-up" />{{ p.key }}→{{ p.entity }}
246
+ </span>
247
+ <span v-for="c in entity.relations.children" :key="c.key" class="entity-relation">
248
+ <i class="pi pi-arrow-down" />{{ c.key }}→{{ c.entity }}
249
+ </span>
250
+ </span>
251
+ </div>
252
+ <!-- Operation stats -->
253
+ <div v-if="entity.stats" class="entity-stats">
254
+ <div class="entity-stats-header">
255
+ <i class="pi pi-chart-bar" />
256
+ <span>Stats</span>
257
+ </div>
258
+ <div class="entity-stats-grid">
259
+ <div class="entity-stat">
260
+ <span class="entity-stat-value">{{ entity.stats.list }}</span>
261
+ <span class="entity-stat-label">list</span>
262
+ </div>
263
+ <div class="entity-stat">
264
+ <span class="entity-stat-value">{{ entity.stats.get }}</span>
265
+ <span class="entity-stat-label">get</span>
266
+ </div>
267
+ <div class="entity-stat">
268
+ <span class="entity-stat-value">{{ entity.stats.create }}</span>
269
+ <span class="entity-stat-label">create</span>
270
+ </div>
271
+ <div class="entity-stat">
272
+ <span class="entity-stat-value">{{ entity.stats.update }}</span>
273
+ <span class="entity-stat-label">update</span>
274
+ </div>
275
+ <div class="entity-stat">
276
+ <span class="entity-stat-value">{{ entity.stats.delete }}</span>
277
+ <span class="entity-stat-label">delete</span>
278
+ </div>
279
+ <div class="entity-stat entity-stat-cache">
280
+ <span class="entity-stat-value" :class="{ 'stat-positive': entity.stats.cacheHits > 0 }">
281
+ {{ entity.stats.cacheHits }}
282
+ </span>
283
+ <span class="entity-stat-label">hits</span>
284
+ </div>
285
+ <div class="entity-stat entity-stat-cache">
286
+ <span class="entity-stat-value" :class="{ 'stat-negative': entity.stats.cacheMisses > 0 }">
287
+ {{ entity.stats.cacheMisses }}
288
+ </span>
289
+ <span class="entity-stat-label">miss</span>
290
+ </div>
291
+ <div class="entity-stat entity-stat-max">
292
+ <span class="entity-stat-value">{{ entity.stats.maxItemsSeen }}</span>
293
+ <span class="entity-stat-label">items</span>
294
+ </div>
295
+ <div class="entity-stat entity-stat-max">
296
+ <span class="entity-stat-value">{{ entity.stats.maxTotal }}</span>
297
+ <span class="entity-stat-label">total</span>
298
+ </div>
299
+ </div>
300
+ </div>
301
+ <!-- Cached items -->
302
+ <div v-if="entity.cache.items && entity.cache.items.length > 0" class="entity-items">
303
+ <div class="entity-items-header">
304
+ <i class="pi pi-list" />
305
+ <span>Cached Items ({{ entity.cache.items.length }}/{{ entity.cache.itemCount }})</span>
306
+ </div>
307
+ <div class="entity-items-list">
308
+ <div v-for="(item, idx) in entity.cache.items.slice(0, 10)" :key="idx" class="entity-item-row">
309
+ <ObjectTree :data="item" :maxDepth="3" :collapsed="true" />
310
+ </div>
311
+ <div v-if="entity.cache.items.length > 10" class="entity-items-more">
312
+ ... et {{ entity.cache.items.length - 10 }} de plus
313
+ </div>
314
+ </div>
315
+ </div>
316
+
317
+ <!-- Full entity config tree -->
318
+ <details class="entity-config">
319
+ <summary>Configuration complète</summary>
320
+ <ObjectTree :data="entity" :maxDepth="6" />
321
+ </details>
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </template>
326
+
327
+ <style scoped>
328
+ .entities-panel {
329
+ padding: 8px;
330
+ display: flex;
331
+ flex-direction: column;
332
+ gap: 8px;
333
+ }
334
+ .entities-status {
335
+ color: #71717a;
336
+ font-size: 12px;
337
+ padding: 12px;
338
+ text-align: center;
339
+ }
340
+ .entity-item {
341
+ background: #27272a;
342
+ border-radius: 4px;
343
+ padding: 8px;
344
+ border-left: 3px solid transparent;
345
+ transition: border-color 0.2s ease;
346
+ }
347
+ .entity-active {
348
+ border-left-color: #f59e0b;
349
+ background: linear-gradient(90deg, rgba(245, 158, 11, 0.1) 0%, #27272a 30%);
350
+ }
351
+ .entity-header {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 8px;
355
+ color: #3b82f6;
356
+ cursor: pointer;
357
+ }
358
+ .entity-header:hover {
359
+ color: #60a5fa;
360
+ }
361
+ .entity-expand {
362
+ display: flex;
363
+ align-items: center;
364
+ justify-content: center;
365
+ width: 16px;
366
+ height: 16px;
367
+ padding: 0;
368
+ background: transparent;
369
+ border: none;
370
+ color: #71717a;
371
+ cursor: pointer;
372
+ font-size: 10px;
373
+ }
374
+ .entity-expand:hover {
375
+ color: #f4f4f5;
376
+ }
377
+ .entity-name {
378
+ font-weight: 600;
379
+ font-family: 'JetBrains Mono', monospace;
380
+ }
381
+ .entity-perms-icons {
382
+ display: flex;
383
+ gap: 4px;
384
+ margin-left: 6px;
385
+ }
386
+ .entity-perms-icons .pi {
387
+ font-size: 12px;
388
+ cursor: help;
389
+ }
390
+ .perm-icon-granted {
391
+ color: #22c55e;
392
+ }
393
+ .perm-icon-denied {
394
+ color: #52525b;
395
+ }
396
+ .perm-icon-readonly {
397
+ color: #f59e0b;
398
+ margin-left: 2px;
399
+ }
400
+ .entity-label {
401
+ color: #a1a1aa;
402
+ font-size: 11px;
403
+ }
404
+ .entity-cache {
405
+ margin-left: auto;
406
+ display: flex;
407
+ align-items: center;
408
+ gap: 4px;
409
+ padding: 2px 6px;
410
+ background: #3f3f46;
411
+ border-radius: 3px;
412
+ font-size: 10px;
413
+ color: #71717a;
414
+ }
415
+ .entity-cache-valid {
416
+ background: rgba(34, 197, 94, 0.2);
417
+ color: #22c55e;
418
+ }
419
+ .entity-cache-disabled {
420
+ background: transparent;
421
+ color: #52525b;
422
+ font-size: 9px;
423
+ }
424
+ .entity-cache-na {
425
+ color: #52525b;
426
+ font-style: italic;
427
+ }
428
+ .entity-load-btn {
429
+ display: inline-flex;
430
+ align-items: center;
431
+ gap: 4px;
432
+ padding: 2px 8px;
433
+ background: #3f3f46;
434
+ border: none;
435
+ border-radius: 3px;
436
+ color: #a1a1aa;
437
+ cursor: pointer;
438
+ font-size: 10px;
439
+ margin-left: auto;
440
+ }
441
+ .entity-load-btn:hover {
442
+ background: #52525b;
443
+ color: #f4f4f5;
444
+ }
445
+ .entity-load-btn:disabled {
446
+ opacity: 0.5;
447
+ cursor: not-allowed;
448
+ }
449
+ .entity-invalidate-btn {
450
+ display: inline-flex;
451
+ align-items: center;
452
+ justify-content: center;
453
+ width: 20px;
454
+ height: 20px;
455
+ padding: 0;
456
+ background: transparent;
457
+ border: none;
458
+ border-radius: 3px;
459
+ color: #71717a;
460
+ cursor: pointer;
461
+ margin-left: auto;
462
+ }
463
+ .entity-invalidate-btn:hover {
464
+ background: rgba(239, 68, 68, 0.2);
465
+ color: #ef4444;
466
+ }
467
+ .entity-test-btn {
468
+ display: inline-flex;
469
+ align-items: center;
470
+ gap: 4px;
471
+ padding: 2px 8px;
472
+ background: #3b82f6;
473
+ border: none;
474
+ border-radius: 3px;
475
+ color: #fff;
476
+ cursor: pointer;
477
+ font-size: 10px;
478
+ margin-left: auto;
479
+ }
480
+ .entity-test-btn:hover {
481
+ background: #2563eb;
482
+ }
483
+ .entity-test-btn:disabled {
484
+ opacity: 0.5;
485
+ cursor: not-allowed;
486
+ }
487
+ .entity-test-result {
488
+ display: inline-flex;
489
+ align-items: center;
490
+ gap: 4px;
491
+ padding: 2px 6px;
492
+ border-radius: 3px;
493
+ font-size: 10px;
494
+ margin-left: 8px;
495
+ }
496
+ .test-success {
497
+ background: rgba(34, 197, 94, 0.2);
498
+ color: #22c55e;
499
+ }
500
+ .test-error {
501
+ background: rgba(239, 68, 68, 0.2);
502
+ color: #ef4444;
503
+ }
504
+ .entity-test-na {
505
+ color: #52525b;
506
+ font-style: italic;
507
+ }
508
+
509
+ /* Collapsed summary */
510
+ .entity-summary {
511
+ display: flex;
512
+ align-items: center;
513
+ gap: 8px;
514
+ margin-left: 24px;
515
+ margin-top: 4px;
516
+ font-size: 11px;
517
+ color: #71717a;
518
+ }
519
+ .entity-storage {
520
+ color: #a1a1aa;
521
+ }
522
+ .entity-fields {
523
+ color: #71717a;
524
+ }
525
+
526
+ /* Expanded details */
527
+ .entity-details {
528
+ font-size: 11px;
529
+ margin-left: 24px;
530
+ margin-top: 8px;
531
+ padding-top: 8px;
532
+ border-top: 1px solid #3f3f46;
533
+ }
534
+ .entity-row {
535
+ display: flex;
536
+ align-items: center;
537
+ gap: 6px;
538
+ margin-bottom: 3px;
539
+ }
540
+ .entity-key {
541
+ color: #71717a;
542
+ min-width: 60px;
543
+ }
544
+ .entity-value {
545
+ color: #d4d4d8;
546
+ }
547
+ .entity-endpoint {
548
+ color: #a1a1aa;
549
+ font-family: 'JetBrains Mono', monospace;
550
+ font-size: 10px;
551
+ padding: 1px 4px;
552
+ background: #3f3f46;
553
+ border-radius: 2px;
554
+ }
555
+ .entity-capabilities {
556
+ display: flex;
557
+ gap: 4px;
558
+ flex-wrap: wrap;
559
+ }
560
+ .entity-cap {
561
+ display: flex;
562
+ align-items: center;
563
+ justify-content: center;
564
+ width: 20px;
565
+ height: 20px;
566
+ border-radius: 3px;
567
+ cursor: help;
568
+ }
569
+ .entity-cap .pi {
570
+ font-size: 11px;
571
+ }
572
+ .entity-cap-enabled {
573
+ background: rgba(34, 197, 94, 0.2);
574
+ color: #22c55e;
575
+ }
576
+ .entity-cap-disabled {
577
+ background: rgba(239, 68, 68, 0.15);
578
+ color: #71717a;
579
+ }
580
+ .entity-cap-auth {
581
+ background: rgba(245, 158, 11, 0.2);
582
+ color: #f59e0b;
583
+ }
584
+ .entity-required {
585
+ color: #f59e0b;
586
+ font-size: 10px;
587
+ }
588
+ .entity-relation {
589
+ display: inline-flex;
590
+ align-items: center;
591
+ gap: 2px;
592
+ padding: 1px 4px;
593
+ background: #3f3f46;
594
+ border-radius: 2px;
595
+ margin-right: 4px;
596
+ font-size: 10px;
597
+ }
598
+ .entity-relation .pi {
599
+ font-size: 8px;
600
+ }
601
+ /* Stats */
602
+ .entity-stats {
603
+ margin-top: 8px;
604
+ padding-top: 8px;
605
+ border-top: 1px solid #3f3f46;
606
+ }
607
+ .entity-stats-header {
608
+ display: flex;
609
+ align-items: center;
610
+ gap: 6px;
611
+ color: #a1a1aa;
612
+ font-size: 11px;
613
+ margin-bottom: 6px;
614
+ }
615
+ .entity-stats-header .pi {
616
+ font-size: 10px;
617
+ }
618
+ .entity-stats-grid {
619
+ display: flex;
620
+ gap: 8px;
621
+ flex-wrap: wrap;
622
+ }
623
+ .entity-stat {
624
+ display: flex;
625
+ flex-direction: column;
626
+ align-items: center;
627
+ padding: 4px 8px;
628
+ background: #1f1f23;
629
+ border-radius: 4px;
630
+ min-width: 40px;
631
+ }
632
+ .entity-stat-value {
633
+ font-size: 14px;
634
+ font-weight: 600;
635
+ color: #d4d4d8;
636
+ font-family: 'JetBrains Mono', monospace;
637
+ }
638
+ .entity-stat-label {
639
+ font-size: 9px;
640
+ color: #71717a;
641
+ text-transform: uppercase;
642
+ }
643
+ .entity-stat-cache {
644
+ border-left: 1px solid #3f3f46;
645
+ padding-left: 12px;
646
+ margin-left: 4px;
647
+ }
648
+ .entity-stat-max {
649
+ border-left: 1px solid #3f3f46;
650
+ padding-left: 12px;
651
+ margin-left: 4px;
652
+ }
653
+ .entity-stat-max .entity-stat-value {
654
+ color: #60a5fa;
655
+ }
656
+ .stat-positive {
657
+ color: #22c55e;
658
+ }
659
+ .stat-negative {
660
+ color: #f59e0b;
661
+ }
662
+ /* Cached items */
663
+ .entity-items {
664
+ margin-top: 8px;
665
+ padding-top: 8px;
666
+ border-top: 1px solid #3f3f46;
667
+ }
668
+ .entity-items-header {
669
+ display: flex;
670
+ align-items: center;
671
+ gap: 6px;
672
+ color: #a1a1aa;
673
+ font-size: 11px;
674
+ margin-bottom: 6px;
675
+ }
676
+ .entity-items-header .pi {
677
+ font-size: 10px;
678
+ }
679
+ .entity-items-list {
680
+ display: flex;
681
+ flex-direction: column;
682
+ gap: 4px;
683
+ max-height: 200px;
684
+ overflow-y: auto;
685
+ }
686
+ .entity-item-row {
687
+ background: #1f1f23;
688
+ border-radius: 3px;
689
+ padding: 4px 6px;
690
+ }
691
+ .entity-items-more {
692
+ color: #71717a;
693
+ font-size: 10px;
694
+ font-style: italic;
695
+ padding: 4px;
696
+ }
697
+
698
+ /* Full config tree */
699
+ .entity-config {
700
+ margin-top: 8px;
701
+ padding-top: 8px;
702
+ border-top: 1px solid #3f3f46;
703
+ }
704
+ .entity-config summary {
705
+ cursor: pointer;
706
+ color: #71717a;
707
+ font-size: 11px;
708
+ }
709
+ .entity-config summary:hover {
710
+ color: #a1a1aa;
711
+ }
712
+ </style>