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:
parent
9739dbaee8
commit
5f6681dd17
19 changed files with 11717 additions and 148 deletions
|
|
@ -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: [],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue