From b38d2593b30fd286216ee7c87052a6949885a936 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Thu, 4 Dec 2025 22:43:01 +0100 Subject: [PATCH] =?UTF-8?q?premi=C3=A8re=20version=20de=20la=20web=20UI=20?= =?UTF-8?q?du=20control=20point?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plan_d_implementation_webui_control_point.md | 447 ++++++++++++++++++ pmoapp/webapp/package-lock.json | 187 +++++++- pmoapp/webapp/package.json | 5 +- pmoapp/webapp/src/App.vue | 38 +- .../webapp/src/assets/styles/pmocontrol.css | 436 +++++++++++++++++ pmoapp/webapp/src/assets/styles/variables.css | 113 +++++ .../src/components/pmocontrol/ActionMenu.vue | 283 +++++++++++ .../src/components/pmocontrol/Breadcrumb.vue | 156 ++++++ .../components/pmocontrol/ContainerItem.vue | 179 +++++++ .../components/pmocontrol/CurrentTrack.vue | 172 +++++++ .../components/pmocontrol/MediaBrowser.vue | 268 +++++++++++ .../src/components/pmocontrol/MediaItem.vue | 185 ++++++++ .../components/pmocontrol/MediaServerCard.vue | 162 +++++++ .../pmocontrol/PlaylistBindingPanel.vue | 180 +++++++ .../src/components/pmocontrol/QueueItem.vue | 114 +++++ .../src/components/pmocontrol/QueueViewer.vue | 152 ++++++ .../components/pmocontrol/RendererCard.vue | 294 ++++++++++++ .../src/components/pmocontrol/StatusBadge.vue | 52 ++ .../pmocontrol/TransportControls.vue | 97 ++++ .../components/pmocontrol/VolumeControl.vue | 102 ++++ pmoapp/webapp/src/main.ts | 57 ++- pmoapp/webapp/src/router/index.ts | 67 ++- pmoapp/webapp/src/services/pmocontrol/api.ts | 230 +++++++++ pmoapp/webapp/src/services/pmocontrol/sse.ts | 208 ++++++++ .../webapp/src/services/pmocontrol/types.ts | 138 ++++++ pmoapp/webapp/src/stores/mediaServers.ts | 144 ++++++ pmoapp/webapp/src/stores/playback.ts | 92 ++++ pmoapp/webapp/src/stores/renderers.ts | 301 ++++++++++++ pmoapp/webapp/src/stores/ui.ts | 111 +++++ pmoapp/webapp/src/views/DashboardView.vue | 233 +++++++++ pmoapp/webapp/src/views/MediaServerView.vue | 237 ++++++++++ pmoapp/webapp/src/views/RendererView.vue | 283 +++++++++++ pmoapp/webapp/tsconfig.app.json | 4 + pmoapp/webapp/vite.config.ts | 6 + 34 files changed, 5713 insertions(+), 20 deletions(-) create mode 100644 Plan_d_implementation_webui_control_point.md create mode 100644 pmoapp/webapp/src/assets/styles/pmocontrol.css create mode 100644 pmoapp/webapp/src/assets/styles/variables.css create mode 100644 pmoapp/webapp/src/components/pmocontrol/ActionMenu.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/Breadcrumb.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/ContainerItem.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/MediaItem.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/MediaServerCard.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/PlaylistBindingPanel.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/QueueItem.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/RendererCard.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/StatusBadge.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/TransportControls.vue create mode 100644 pmoapp/webapp/src/components/pmocontrol/VolumeControl.vue create mode 100644 pmoapp/webapp/src/services/pmocontrol/api.ts create mode 100644 pmoapp/webapp/src/services/pmocontrol/sse.ts create mode 100644 pmoapp/webapp/src/services/pmocontrol/types.ts create mode 100644 pmoapp/webapp/src/stores/mediaServers.ts create mode 100644 pmoapp/webapp/src/stores/playback.ts create mode 100644 pmoapp/webapp/src/stores/renderers.ts create mode 100644 pmoapp/webapp/src/stores/ui.ts create mode 100644 pmoapp/webapp/src/views/DashboardView.vue create mode 100644 pmoapp/webapp/src/views/MediaServerView.vue create mode 100644 pmoapp/webapp/src/views/RendererView.vue diff --git a/Plan_d_implementation_webui_control_point.md b/Plan_d_implementation_webui_control_point.md new file mode 100644 index 00000000..a4774778 --- /dev/null +++ b/Plan_d_implementation_webui_control_point.md @@ -0,0 +1,447 @@ +# PMOControl WebUI - Design Recommendations & Implementation Plan + +## Executive Summary + +Based on my analysis of the existing codebase, I'm providing comprehensive recommendations for implementing a Vue.js WebUI for PMOControl. The system already has: +- A complete REST API with OpenAPI documentation (`/api/control/*`) +- SSE endpoints for real-time updates (`/api/control/events/*`) +- Vue 3 + TypeScript + Vite setup +- Existing components (GenericMusicPlayer, UpnpExplorer, Cache Managers, LogView) + +--- + +## Design Decisions & Recommendations + +### 1. State Management: **Use Pinia** + +**Recommendation: Pinia (Vue 3's official state management)** + +**Rationale:** +- **Centralized real-time state**: Essential for managing SSE updates from multiple sources (renderers, media servers) +- **Multi-client synchronization**: Single source of truth for renderer states, volumes, playback positions +- **TypeScript native**: Better type inference than Vuex +- **DevTools integration**: Built-in debugging for SSE event flows +- **Composition API friendly**: Matches existing Vue 3 patterns in codebase +- **Performance**: Lightweight (~1KB), modular stores +- **Official Vue 3 recommendation**: Future-proof choice + +**Store Architecture:** +```typescript +// stores/renderers.ts - Renderer state (SSE updates) +// stores/mediaServers.ts - Media server state (SSE updates) +// stores/playback.ts - Current playback session +// stores/ui.ts - UI state (selected renderer, view preferences) +``` + +**Benefits for your use case:** +- Handle 20+ concurrent clients with shared state +- Real-time SSE event synchronization across all views +- Easy to scale with multi-renderer, multi-server, multi-session architecture + +--- + +### 2. UI Component Library: **Headless UI + Custom Components** + +**Recommendation: Hybrid approach - Headless UI components + custom styling** + +**Component Library: Shadcn-vue (Headless UI primitives)** + +**Rationale:** +- **Lightweight & performant**: Only import what you need +- **Full style control**: Match "carte uniforme, responsive, colorée selon statut" spec exactly +- **TypeScript-first**: Perfect type safety +- **Accessibility built-in**: ARIA compliance out of the box +- **No theme lock-in**: Complete CSS freedom +- **Composable primitives**: Card, Dialog, Dropdown, Slider components + +**Why NOT a full framework (Vuetify, Element Plus)?** +- Heavy bundle size (100-500KB vs ~10KB for headless) +- Theme customization overhead +- Your spec requires custom status-based coloring +- Performance critical with 20 concurrent clients + +**Alternative if you prefer pre-styled:** PrimeVue +- Good performance +- Customizable themes +- Strong TypeScript support +- But: 150KB+ bundle size + +**Custom Components to Build:** +- `RendererCard` - Status-colored cards for each renderer +- `TransportControls` - Play/Pause/Stop/Next buttons +- `VolumeControl` - Slider with mute toggle +- `QueueViewer` - Playlist display with drag-drop +- `MediaServerBrowser` - Container navigation + +--- + +### 3. Existing Components: **Reorganize into Debug Section** + +**Recommendation: Keep existing components, create new PMOControl home** + +**Structure:** +``` +/app (root) → PMOControl Dashboard (NEW) +/app/debug → Dropdown menu + ├─ /logs → LogView + ├─ /upnp → UpnpExplorer + ├─ /covers-cache → CoverCacheManager + ├─ /audio-cache → AudioCacheManager + ├─ /api-dashboard → APIDashboard + └─ /radio-paradise → RadioParadiseExplorer +``` + +**Rationale:** +- Existing components are valuable for development/debugging +- Don't break existing functionality +- PMOControl becomes primary interface as specified +- Debug tools remain accessible but not prominent +- Matches current App.vue dropdown pattern + +**Home Screen (/) - PMOControl Dashboard:** +- Grid of renderer cards (status-colored) +- Active playback session viewer +- Quick controls (play/pause/volume) +- Media server browser panel + +--- + +### 4. Responsive Design: **Mobile-first with 3 breakpoints** + +**Recommendation: Follow existing 768px pattern + add tablet/desktop** + +**Breakpoints:** +```css +/* Mobile: < 768px (existing pattern) */ +- Single column layout +- Stacked renderer cards +- Bottom-fixed playback controls +- Collapsible media browser + +/* Tablet: 768px - 1024px */ +- Two column layout +- Grid of renderer cards (2 columns) +- Side panel for media browser +- Floating playback controls + +/* Desktop: > 1024px */ +- Three column layout +- Renderer cards grid (3-4 columns) +- Persistent media browser sidebar +- Always-visible playback controls +``` + +**Target Devices:** +- **Primary**: Desktop browsers (control station) +- **Secondary**: Tablets (remote control) +- **Tertiary**: Mobile phones (quick controls) + +**Performance considerations:** +- Virtualized lists for 20+ renderers (use vue-virtual-scroller) +- Lazy load album art +- Throttle SSE position updates (max 1/sec per renderer) + +--- + +### 5. Icons: **Lucide Icons (SVG library)** + +**Recommendation: Lucide Icons (NOT emoji)** + +**Rationale:** +- **Professional appearance**: Emojis inconsistent across platforms +- **Customizable**: Size, color, stroke width +- **Lightweight**: Tree-shakeable SVG imports (~1KB per icon) +- **Status coloring**: Icons can match card status colors +- **Accessibility**: Proper ARIA labels +- **Vue components**: `lucide-vue-next` package + +**Icon mapping:** +```typescript +Play → PlayCircle +Pause → PauseCircle +Stop → StopCircle +Next → SkipForward +Volume → Volume2 / VolumeX (muted) +Renderer → Speaker / MonitorSpeaker +Server → Server / Database +Queue → ListMusic +``` + +**Alternative if you prefer minimal bundle:** Heroicons +- Smaller set (fewer icons) +- Tailwind CSS integration +- But: less comprehensive for music player needs + +**Why NOT emoji:** +- Platform inconsistencies (iOS ≠ Android ≠ Windows) +- No color control +- Accessibility issues +- Unprofessional for production UI + +--- + +## Technical Architecture + +### Real-time SSE Integration + +**SSE Event Handling:** +```typescript +// services/controlPointSSE.ts +class ControlPointSSE { + private eventSource: EventSource + private renderersStore: ReturnType + + connect() { + this.eventSource = new EventSource('/api/control/events') + + this.eventSource.addEventListener('control', (e) => { + const event = JSON.parse(e.data) + + if (event.category === 'renderer') { + this.handleRendererEvent(event) + } else if (event.category === 'media_server') { + this.handleServerEvent(event) + } + }) + } + + handleRendererEvent(event: RendererEventPayload) { + switch (event.type) { + case 'state_changed': + this.renderersStore.updateState(event.renderer_id, event.state) + break + case 'volume_changed': + this.renderersStore.updateVolume(event.renderer_id, event.volume) + break + // ... handle all event types + } + } +} +``` + +**Store Integration:** +```typescript +// stores/renderers.ts +export const useRenderersStore = defineStore('renderers', () => { + const renderers = ref>(new Map()) + + // SSE updates + function updateState(id: string, state: string) { + const renderer = renderers.value.get(id) + if (renderer) { + renderer.transport_state = state + } + } + + // REST API calls + async function play(id: string) { + await fetch(`/api/control/renderers/${id}/play`, { method: 'POST' }) + // SSE will update state automatically + } + + return { renderers, updateState, play } +}) +``` + +### Performance Optimizations + +**For 20+ concurrent clients:** + +1. **Throttle position updates**: + ```typescript + const throttledPositionUpdate = throttle((id, pos) => { + store.updatePosition(id, pos) + }, 1000) // Max 1 update/second + ``` + +2. **Virtual scrolling** for renderer lists: + ```bash + npm install vue-virtual-scroller + ``` + +3. **Lazy load album art**: + ```vue + + ``` + +4. **Debounce volume sliders**: + ```typescript + const debouncedVolumeChange = debounce((id, vol) => { + api.setVolume(id, vol) + }, 300) + ``` + +5. **Memoize computed properties**: + ```typescript + const activeRenderers = computed(() => + renderers.value.filter(r => r.online) + ) + ``` + +--- + +## Implementation Roadmap + +### Phase 1: Core Infrastructure (Week 1) +1. Install Pinia + configure stores +2. Install Lucide Icons +3. Create SSE service layer +4. Setup store structure (renderers, servers, playback, ui) +5. Connect SSE events to stores + +### Phase 2: UI Components (Week 2) +5. Build RendererCard component (status-colored) +6. Build TransportControls component +7. Build VolumeControl component +8. Build QueueViewer component +9. Create responsive grid layouts + +### Phase 3: Dashboard Assembly (Week 3) +10. Create PMOControl home view +11. Integrate all components +12. Add media server browser panel +13. Implement responsive breakpoints +14. Add loading states & error handling + +### Phase 4: Polish & Testing (Week 4) +15. Test with 20+ concurrent clients +16. Performance profiling & optimization +17. Accessibility audit (ARIA, keyboard nav) +18. Cross-browser testing +19. Mobile/tablet testing +20. Documentation + +--- + +## Dependencies to Install + +```json +{ + "dependencies": { + "pinia": "^2.2.8", + "lucide-vue-next": "^0.470.0", + "vue-virtual-scroller": "^2.0.0-beta.8" + }, + "devDependencies": { + // Already installed: vue, vue-router, typescript, vite + } +} +``` + +**Total bundle size estimate:** +15KB gzipped (Pinia + Lucide + Virtual Scroller) + +--- + +## Status-based Coloring Scheme + +Based on "carte uniforme, responsive, colorée selon statut" spec: + +```css +/* Renderer Card Status Colors */ +.renderer-card.playing { + border-color: #22c55e; /* green */ + background: linear-gradient(135deg, #22c55e10, transparent); +} + +.renderer-card.paused { + border-color: #f59e0b; /* amber */ + background: linear-gradient(135deg, #f59e0b10, transparent); +} + +.renderer-card.stopped { + border-color: #6b7280; /* gray */ + background: linear-gradient(135deg, #6b728010, transparent); +} + +.renderer-card.offline { + border-color: #ef4444; /* red */ + background: linear-gradient(135deg, #ef444410, transparent); + opacity: 0.6; +} + +.renderer-card.transitioning { + border-color: #3b82f6; /* blue */ + background: linear-gradient(135deg, #3b82f610, transparent); + animation: pulse 2s infinite; +} +``` + +--- + +## Answers to Your Specific Questions + +### 1. State Management? +**Answer: Pinia** - Vue 3 official, perfect for SSE real-time updates, TypeScript native, lightweight + +### 2. UI Component Library? +**Answer: Headless UI (Shadcn-vue) + Custom Components** - Full control over status-based styling, lightweight, no theme lock-in + +### 3. Keep existing components? +**Answer: Yes, reorganize into Debug section** - Keep valuable dev tools, make PMOControl the new home screen + +### 4. Responsive breakpoints? +**Answer: Mobile-first with 3 breakpoints** - <768px (mobile), 768-1024px (tablet), >1024px (desktop) + +### 5. Icons? +**Answer: Lucide Icons (SVG library)** - Professional, customizable, status-colored, NOT emoji + +--- + +## Risk Mitigation + +**Potential challenges:** + +1. **SSE connection management across tabs** + - Solution: Use BroadcastChannel API for cross-tab sync + - Fallback: LocalStorage events + +2. **20+ renderers performance** + - Solution: Virtual scrolling + throttled updates + - Monitor: Chrome DevTools Performance profiler + +3. **Network reliability (SSE reconnection)** + - Solution: Exponential backoff reconnection + - UI indicator for connection status + +4. **Album art loading (CORS, 404s)** + - Solution: Proxy through backend + - Fallback: Default placeholder image + +5. **Browser compatibility (SSE support)** + - Chrome/Edge: Native support ✅ + - Firefox: Native support ✅ + - Safari: Native support ✅ + - IE11: Use EventSource polyfill + +--- + +## Success Metrics + +**Performance targets:** +- Initial load: <2s (FCP) +- SSE event latency: <100ms +- UI interaction: <16ms (60fps) +- Memory usage: <50MB with 20 renderers +- Bundle size: <250KB gzipped + +**Functionality checklist:** +- [ ] Display all discovered renderers in real-time +- [ ] Show accurate playback state (play/pause/stop) +- [ ] Volume control works across all renderer types +- [ ] Queue display syncs with server +- [ ] Media server browsing functional +- [ ] Playlist attachment working +- [ ] Responsive on mobile/tablet/desktop +- [ ] Accessible (WCAG AA compliance) +- [ ] 20+ concurrent clients supported + +--- + +## Next Steps + +1. **Review & approve** this plan with stakeholders +2. **Clarify any ambiguities** in requirements +3. **Set up development environment** (install dependencies) +4. **Begin Phase 1** (Core Infrastructure) + +Would you like me to proceed with implementation, or do you have questions about any of these recommendations? diff --git a/pmoapp/webapp/package-lock.json b/pmoapp/webapp/package-lock.json index ffa3542e..09afc506 100644 --- a/pmoapp/webapp/package-lock.json +++ b/pmoapp/webapp/package-lock.json @@ -9,9 +9,12 @@ "version": "0.0.0", "dependencies": { "dompurify": "^3.2.7", + "lucide-vue-next": "^0.555.0", "marked": "^16.3.0", + "pinia": "^3.0.4", "vue": "^3.5.21", - "vue-router": "^4.5.1" + "vue-router": "^4.5.1", + "vue-virtual-scroller": "^2.0.0-beta.8" }, "devDependencies": { "@types/dompurify": "^3.0.5", @@ -957,6 +960,36 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-kit/node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, "node_modules/@vue/language-core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.0.tgz", @@ -1057,6 +1090,30 @@ "dev": true, "license": "MIT" }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1165,6 +1222,33 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lucide-vue-next": { + "version": "0.555.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.555.0.tgz", + "integrity": "sha512-7hczPsiMD/y+VNLpal5Q5Wv09kQxlHS0l/cM1xagrd+MA3i5umMm+PUXqllvsbgwAl3PHv27fo59h4PN02GM5A==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", @@ -1186,6 +1270,12 @@ "node": ">= 20" } }, + "node_modules/mitt": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", + "license": "MIT" + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", @@ -1218,6 +1308,12 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1238,6 +1334,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -1266,6 +1392,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.52.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", @@ -1317,6 +1449,27 @@ "node": ">=0.10.0" } }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1454,6 +1607,24 @@ } } }, + "node_modules/vue-observe-visibility": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz", + "integrity": "sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-resize": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz", + "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-router": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", @@ -1485,6 +1656,20 @@ "peerDependencies": { "typescript": ">=5.0.0" } + }, + "node_modules/vue-virtual-scroller": { + "version": "2.0.0-beta.8", + "resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-beta.8.tgz", + "integrity": "sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==", + "license": "MIT", + "dependencies": { + "mitt": "^2.1.0", + "vue-observe-visibility": "^2.0.0-alpha.1", + "vue-resize": "^2.0.0-alpha.1" + }, + "peerDependencies": { + "vue": "^3.2.0" + } } } } diff --git a/pmoapp/webapp/package.json b/pmoapp/webapp/package.json index bffcc82e..e71fba0b 100644 --- a/pmoapp/webapp/package.json +++ b/pmoapp/webapp/package.json @@ -10,9 +10,12 @@ }, "dependencies": { "dompurify": "^3.2.7", + "lucide-vue-next": "^0.555.0", "marked": "^16.3.0", + "pinia": "^3.0.4", "vue": "^3.5.21", - "vue-router": "^4.5.1" + "vue-router": "^4.5.1", + "vue-virtual-scroller": "^2.0.0-beta.8" }, "devDependencies": { "@types/dompurify": "^3.0.5", diff --git a/pmoapp/webapp/src/App.vue b/pmoapp/webapp/src/App.vue index f78be3a9..e34a2665 100644 --- a/pmoapp/webapp/src/App.vue +++ b/pmoapp/webapp/src/App.vue @@ -1,7 +1,7 @@