feat: LineIcons Duotone, page profil, widgets dashboard, sidebar gauche/droite

- Intégration LineIcons Duotone (css/ + toutes les pages)
- Remplacement de tous les symboles Unicode par des icônes lnid-*
- Page profile.html : préférences thème, position sidebar, langue
- Dashboard : système de widgets add/remove + drag-and-drop natif
- Sidebar gauche/droite configurable per-user (data-sidebar CSS + FOUC script)
- Store ui : sidebarPosition, applySidebarPosition(), setSidebarPosition()
- Composant profilePage() dans app.js
- nav.profile ajouté dans fr.json et en.json
- SUIVI.md mis à jour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
enzo 2026-03-21 18:38:48 +01:00
parent 9739dbaee8
commit 5f6681dd17
19 changed files with 11717 additions and 148 deletions

View file

@ -97,11 +97,12 @@ document.addEventListener('alpine:init', () => {
Alpine.store('ui', {
theme: localStorage.getItem('pxp_theme') || 'dark',
sidebarCollapsed: localStorage.getItem('pxp_sidebar') === 'true',
sidebarPosition: localStorage.getItem('pxp_sidebar_pos') || 'left',
currentPage: '',
init() {
this.applyTheme()
// Detect current page from URL
this.applySidebarPosition()
const path = window.location.pathname
this.currentPage = path.replace(/^\/|\.html$/g, '') || 'index'
},
@ -116,6 +117,16 @@ document.addEventListener('alpine:init', () => {
this.applyTheme()
},
applySidebarPosition() {
document.documentElement.setAttribute('data-sidebar', this.sidebarPosition)
},
setSidebarPosition(pos) {
this.sidebarPosition = pos
localStorage.setItem('pxp_sidebar_pos', pos)
this.applySidebarPosition()
},
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed
localStorage.setItem('pxp_sidebar', this.sidebarCollapsed)
@ -168,12 +179,12 @@ document.addEventListener('alpine:init', () => {
get currentPage() { return Alpine.store('ui').currentPage },
navItems: [
{ id: 'dashboard', icon: '⊞', labelKey: 'nav.dashboard', href: '/dashboard.html' },
{ id: 'proxmox', icon: '⬡', labelKey: 'nav.proxmox', href: '/proxmox.html' },
{ id: 'updates', icon: '↑', labelKey: 'nav.updates', href: '/updates.html' },
{ id: 'terminal', icon: '', labelKey: 'nav.terminal', href: '/terminal.html' },
{ id: 'settings', icon: '⚙', labelKey: 'nav.settings', href: '/settings.html' },
{ id: 'modules', icon: '⬡', labelKey: 'nav.modules', href: '/modules.html' },
{ id: 'dashboard', iconClass: 'lnid-dashboard-square-1', labelKey: 'nav.dashboard', href: '/dashboard.html' },
{ id: 'proxmox', iconClass: 'lnid-server-1', labelKey: 'nav.proxmox', href: '/proxmox.html' },
{ id: 'updates', iconClass: 'lnid-arrow-upward', labelKey: 'nav.updates', href: '/updates.html' },
{ id: 'terminal', iconClass: 'lnid-terminal', labelKey: 'nav.terminal', href: '/terminal.html' },
{ id: 'settings', iconClass: 'lnid-gear-1', labelKey: 'nav.settings', href: '/settings.html' },
{ id: 'modules', iconClass: 'lnid-puzzle', labelKey: 'nav.modules', href: '/modules.html' },
],
isActive(id) {
@ -305,9 +316,37 @@ document.addEventListener('alpine:init', () => {
resources: [],
ws: null,
wsStatus: 'connecting',
configOpen: false,
dragSrcIdx: null,
widgets: (function() {
try {
return JSON.parse(localStorage.getItem('pxp_widgets') || 'null') || [
{ id: 'status', visible: true, label: 'Statut LXC' },
{ id: 'lxc-list', visible: true, label: 'Liste LXC' },
{ id: 'links', visible: false, label: 'Raccourcis' },
]
} catch(e) {
return [
{ id: 'status', visible: true, label: 'Statut LXC' },
{ id: 'lxc-list', visible: true, label: 'Liste LXC' },
{ id: 'links', visible: false, label: 'Raccourcis' },
]
}
})(),
saveWidgets() { localStorage.setItem('pxp_widgets', JSON.stringify(this.widgets)) },
onDragStart(idx) { this.dragSrcIdx = idx },
onDrop(idx) {
if (this.dragSrcIdx === null || this.dragSrcIdx === idx) return
const moved = this.widgets.splice(this.dragSrcIdx, 1)[0]
this.widgets.splice(idx, 0, moved)
this.dragSrcIdx = null
this.saveWidgets()
},
async init() {
// Chargement immédiat via HTTP, puis WS pour le temps réel
await this.fetchResources()
this.connectWS()
},
@ -321,7 +360,7 @@ document.addEventListener('alpine:init', () => {
const res = await apiFetch('/api/proxmox/resources')
if (res.ok) {
this.resources = await res.json() || []
this.wsStatus = 'ok'
this.wsStatus = 'connected'
}
} catch (e) { /* WS prendra le relais */ }
},
@ -332,10 +371,9 @@ document.addEventListener('alpine:init', () => {
this.ws = new WebSocket(`${proto}://${location.host}/ws/proxmox?token=${token}`)
this.ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
// Le backend publie type="resources_update", payload=[...]
if (msg.type === 'resources_update') {
this.resources = msg.payload || []
this.wsStatus = 'ok'
this.wsStatus = 'connected'
}
}
this.ws.onclose = () => {
@ -345,14 +383,51 @@ document.addEventListener('alpine:init', () => {
this.ws.onerror = () => { this.wsStatus = 'error' }
},
get runningCount() { return this.resources.filter(r => r.status === 'running').length },
get stoppedCount() { return this.resources.filter(r => r.status !== 'running').length },
get lxcList() { return this.resources.filter(r => r.type === 'lxc') },
get lxc() { return this.resources.filter(r => r.type === 'lxc') },
get running() { return this.lxc.filter(r => r.status === 'running') },
get stopped() { return this.lxc.filter(r => r.status !== 'running') },
// compat
get runningCount() { return this.running.length },
get stoppedCount() { return this.stopped.length },
get lxcList() { return this.lxc },
get vmList() { return this.resources.filter(r => r.type === 'qemu') },
t(key) { return Alpine.store('i18n').t(key) },
}))
// ── Composant: profilePage ──────────────────────────────────────────────
Alpine.data('profilePage', () => ({
theme: '',
sidebarPosition: '',
lang: '',
init() {
this.theme = Alpine.store('ui').theme
this.sidebarPosition = Alpine.store('ui').sidebarPosition
this.lang = Alpine.store('i18n').lang
},
setTheme(t) {
this.theme = t
Alpine.store('ui').theme = t
Alpine.store('ui').applyTheme()
localStorage.setItem('pxp_theme', t)
},
setSidebarPosition(pos) {
this.sidebarPosition = pos
Alpine.store('ui').setSidebarPosition(pos)
},
async setLang(lang) {
this.lang = lang
await Alpine.store('i18n').setLang(lang)
},
get user() { return Alpine.store('auth').user },
t(key) { return Alpine.store('i18n').t(key) },
}))
// ── Composant: proxmoxPage ──────────────────────────────────────────────
Alpine.data('proxmoxPage', () => ({
resources: [],