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

@ -110,10 +110,11 @@ Référence : `instruction.md` | Mis à jour : 2026-03-21
| Règle | État | Priorité |
|-------|------|----------|
| Icônes LineIcons Duotone uniquement | ❌ Symbols Unicode utilisés | Haute |
| Dashboard widgets add/remove/drag-drop | ❌ Non implémenté | Haute |
| Sidebar gauche/droite per-user | ❌ Gauche fixe uniquement | Moyenne |
| Préférences utilisateur en DB | ❌ localStorage uniquement | Moyenne |
| Icônes LineIcons Duotone uniquement | ✅ Intégrés (css/ + toutes les pages) | Haute |
| Dashboard widgets add/remove/drag-drop | ✅ Widget system + DnD natif HTML5 | Haute |
| Sidebar gauche/droite per-user | ✅ `data-sidebar` CSS + profilePage | Moyenne |
| Page profil préférences (thème/sidebar/langue) | ✅ profile.html créée | Moyenne |
| Préférences utilisateur en DB | ❌ localStorage uniquement | Basse |
| Historique mises à jour | ❌ Non affiché | Basse |
| Mises à jour CORE/modules depuis interface | ❌ Non implémenté | Basse |
| Page blocage migration | ❌ Non implémenté | Basse |

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -488,10 +488,13 @@ input, select, textarea { font-family: inherit; font-size: inherit; }
}
.sidebar-icon {
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
flex-shrink: 0;
width: 1.25rem;
text-align: center;
line-height: 1;
}
.sidebar-label {
@ -594,3 +597,27 @@ html.is-animating .transition-fade {
}
[x-cloak] { display: none !important; }
/* ── Sidebar droite ─────────────────────────────────────────────────────────── */
[data-sidebar="right"] .sidebar {
left: auto;
right: 0;
border-right: none;
border-left: 1px solid var(--neu-border);
}
[data-sidebar="right"] .main-layout {
margin-left: 0;
margin-right: var(--sidebar-width);
transition: margin-right 0.2s ease;
}
[data-sidebar="right"] .sidebar.collapsed ~ .main-layout {
margin-right: var(--sidebar-width-collapsed);
}
@media (max-width: 768px) {
[data-sidebar="right"] .main-layout {
margin-right: var(--sidebar-width-collapsed);
}
}

View file

@ -445,3 +445,167 @@
.confirm-summary { padding: 1rem; display: flex; flex-direction: column; gap: .5rem; border-radius: .5rem; }
.confirm-row { display: flex; gap: 1rem; font-size: .875rem; }
.confirm-label { font-weight: 600; min-width: 80px; color: var(--neu-text-muted); }
/* ── Page header ─────────────────────────────────────────────────────────────── */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.25rem;
gap: 1rem;
}
.page-title { font-size: 1.25rem; font-weight: 700; }
/* ── Dashboard widgets ───────────────────────────────────────────────────────── */
.widgets-grid {
display: grid;
gap: 1.25rem;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
.widget {
min-height: 160px;
cursor: grab;
user-select: none;
transition: var(--neu-transition);
}
.widget:active { cursor: grabbing; }
.widget:hover { transform: translateY(-2px); }
.widget-title {
font-size: .875rem;
font-weight: 700;
color: var(--neu-text-muted);
text-transform: uppercase;
letter-spacing: .5px;
margin: 0 0 .75rem 0;
}
/* Widget config panel */
.widget-config {
margin-bottom: 1.25rem;
padding: 1rem;
}
.widget-config-title {
font-weight: 700;
margin: 0 0 .75rem 0;
font-size: .9rem;
}
.widget-config-row {
display: flex;
align-items: center;
gap: .75rem;
padding: .5rem .25rem;
border-radius: .375rem;
cursor: grab;
transition: background .15s;
}
.widget-config-row:hover { background: rgba(108, 142, 244, 0.06); }
.widget-toggle-label {
display: flex;
align-items: center;
gap: .5rem;
margin-left: auto;
font-size: .8rem;
color: var(--neu-text-muted);
cursor: pointer;
}
.drag-handle { color: var(--neu-text-muted); font-size: 1rem; }
/* Widget: status (stats) */
.stat-rows { display: flex; flex-direction: column; gap: .5rem; }
.stat-row {
display: flex;
align-items: center;
gap: .75rem;
font-size: .9rem;
}
.stat-icon { font-size: 1.1rem; width: 1.25rem; flex-shrink: 0; }
.stat-num { font-size: 1.5rem; font-weight: 700; min-width: 2rem; }
.stat-label { color: var(--neu-text-muted); }
/* Widget: lxc-list */
.lxc-row {
display: flex;
align-items: center;
gap: .5rem;
padding: .375rem 0;
border-bottom: 1px solid var(--neu-border);
font-size: .85rem;
}
.lxc-row:last-child { border-bottom: none; }
.lxc-name { flex: 1; font-weight: 500; }
.lxc-cpu, .lxc-ram { color: var(--neu-text-muted); font-size: .8rem; min-width: 3.5rem; text-align: right; }
/* Widget: links */
.links-grid {
display: flex;
flex-wrap: wrap;
gap: .75rem;
}
.link-btn {
flex: 1;
min-width: 80px;
justify-content: center;
gap: .375rem;
}
/* ── Profil / préférences ────────────────────────────────────────────────────── */
.settings-section {
margin-bottom: 1.25rem;
}
.settings-section h3 {
display: flex;
align-items: center;
gap: .5rem;
font-size: 1rem;
font-weight: 700;
margin: 0 0 1rem 0;
padding-bottom: .75rem;
border-bottom: 1px solid var(--neu-border);
}
.profile-info { display: flex; flex-direction: column; gap: .5rem; }
.profile-row {
display: flex;
align-items: center;
gap: 1rem;
font-size: .9rem;
padding: .375rem 0;
}
.profile-label {
font-weight: 600;
color: var(--neu-text-muted);
min-width: 100px;
}
.profile-value { color: var(--neu-text); }
.badge-admin {
background: rgba(108, 142, 244, 0.15);
color: var(--neu-primary);
padding: 2px 8px;
border-radius: 9999px;
font-size: .75rem;
font-weight: 600;
}
.badge-user {
background: rgba(127, 136, 153, 0.15);
color: var(--neu-text-muted);
padding: 2px 8px;
border-radius: 9999px;
font-size: .75rem;
font-weight: 600;
}
/* Groupe de boutons (thème, sidebar position) */
.btn-group {
display: flex;
gap: .5rem;
flex-wrap: wrap;
}
/* ── Logo auth LineIcons ─────────────────────────────────────────────────────── */
.logo-icon {
font-size: 2.5rem;
color: var(--neu-primary);
display: block;
line-height: 1;
}

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProxmoxPanel — Dashboard</title>
@ -9,119 +9,171 @@
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
<script src="/js/app.js"></script>
<script src="/js/vendors/alpine.min.js" defer></script>
</head>
<body x-data>
<body x-data x-init="$store.ui.init()">
<!-- Sidebar -->
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
<div class="sidebar-header">
<span class="sidebar-logo"></span>
<i class="sidebar-logo lnid-layout-1"></i>
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
<button class="sidebar-toggle" @click="toggle()"></button>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
<i class="lnid-menu-hamburger-1"></i>
</button>
</div>
<nav class="sidebar-nav">
<template x-for="item in navItems" :key="item.id">
<a class="sidebar-link"
:class="{ active: isActive(item.id) }"
:href="item.href"
@click.prevent="navigate(item.href)">
<span class="sidebar-icon" x-text="item.icon"></span>
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
<i class="sidebar-icon" :class="item.iconClass"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
</a>
</template>
</nav>
<div class="sidebar-footer" x-show="!collapsed">
<span class="sidebar-user" x-text="$store.auth.user?.username || ''"></span>
<button class="neu-btn neu-btn--sm" @click="$store.auth.logout()"></button>
<div class="sidebar-footer">
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
<i class="sidebar-icon lnid-user-circle-1"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
</a>
</div>
</aside>
<div class="main-layout">
<!-- Navbar -->
<nav class="navbar" x-data="navbar()" x-cloak>
<h2 class="navbar-title" x-text="t('nav.dashboard')"></h2>
<div class="navbar-actions">
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
<option value="fr">FR</option>
<option value="en">EN</option>
<option value="fr">FR</option><option value="en">EN</option>
</select>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()"
:title="theme==='dark' ? t('navbar.lightMode') : t('navbar.darkMode')">
<span x-text="theme === 'dark' ? '☀' : '🌙'"></span>
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
<i class="lnid-power-button"></i>
</button>
</div>
</nav>
<!-- Contenu swappé -->
<main id="swup" class="page-content transition-fade" x-data="dashboardPage()" x-cloak>
<!-- Stats -->
<div class="stats-grid">
<div class="neu-card stat-card">
<div class="stat-icon"></div>
<div class="stat-value" x-text="runningCount"></div>
<div class="stat-label">En cours</div>
</div>
<div class="neu-card stat-card">
<div class="stat-icon"></div>
<div class="stat-value" x-text="stoppedCount"></div>
<div class="stat-label">Arrêtés</div>
</div>
<div class="neu-card stat-card">
<div class="stat-icon"></div>
<div class="stat-value" x-text="lxcList.length"></div>
<div class="stat-label">LXC</div>
</div>
<div class="neu-card stat-card">
<div class="stat-icon"></div>
<div class="stat-value" x-text="vmList.length"></div>
<div class="stat-label">VM</div>
</div>
<div class="page-header">
<h2 class="page-title" x-text="t('dashboard.welcome').replace('{name}', $store.auth.user?.username || '')"></h2>
<button class="neu-btn neu-btn--sm" @click="configOpen = !configOpen" :title="t('dashboard.addWidget')">
<i class="lnid-layout-1"></i>
<span x-show="!configOpen" x-text="t('dashboard.addWidget')"></span>
<span x-show="configOpen"></span>
</button>
</div>
<!-- Status WS -->
<div class="ws-status" :class="wsStatus">
<!-- Config panel widgets -->
<div class="widget-config neu-card" x-show="configOpen" x-transition>
<h4 class="widget-config-title">Configurer les widgets</h4>
<template x-for="(w, idx) in widgets" :key="w.id">
<div class="widget-config-row"
draggable="true"
@dragstart="onDragStart(idx)"
@dragover.prevent
@drop.prevent="onDrop(idx)">
<i class="lnid-menu-hamburger-1 drag-handle"></i>
<span x-text="w.label"></span>
<label class="widget-toggle-label">
<input type="checkbox" x-model="w.visible" @change="saveWidgets()" />
<span x-text="w.visible ? 'Visible' : 'Masqué'"></span>
</label>
</div>
</template>
</div>
<!-- WS status -->
<div class="ws-status" :class="wsStatus" x-show="wsStatus !== 'connected'">
<span x-show="wsStatus === 'connecting'">⌛ Connexion…</span>
<span x-show="wsStatus === 'ok'">● Live</span>
<span x-show="wsStatus === 'disconnected'">⚠ Déconnecté (reconnexion…)</span>
<span x-show="wsStatus === 'error'">✗ Erreur WebSocket</span>
</div>
<!-- LXC List -->
<div class="section">
<h3 class="section-title">Containers LXC</h3>
<div class="resource-grid" x-show="lxcList.length > 0">
<template x-for="r in lxcList" :key="r.vmid">
<div class="neu-card resource-card" :class="r.status">
<div class="resource-header">
<span class="resource-name" x-text="r.name || 'LXC ' + r.vmid"></span>
<span class="resource-id" x-text="'#' + r.vmid"></span>
<span class="resource-badge" :class="r.status" x-text="r.status"></span>
<!-- Widgets -->
<div class="widgets-grid">
<template x-for="(w, idx) in widgets.filter(w => w.visible)" :key="w.id">
<div class="neu-card widget"
draggable="true"
@dragstart="onDragStart(widgets.indexOf(w))"
@dragover.prevent
@drop.prevent="onDrop(widgets.indexOf(w))">
<!-- Widget: status -->
<template x-if="w.id === 'status'">
<div class="widget-status">
<h4 class="widget-title" x-text="t('dashboard.lxcStatus')"></h4>
<div class="stat-rows">
<div class="stat-row">
<i class="lnid-play stat-icon" style="color:var(--neu-success)"></i>
<span class="stat-num" x-text="running.length"></span>
<span class="stat-label" x-text="t('dashboard.running')"></span>
</div>
<div class="resource-metrics" x-show="r.status === 'running'">
<div class="metric">
<span class="metric-label">CPU</span>
<div class="metric-bar">
<div class="metric-fill" :style="'width:' + Math.round((r.cpu||0)*100) + '%;background:' + cpuColor(Math.round((r.cpu||0)*100))"></div>
<div class="stat-row">
<i class="lnid-stop stat-icon" style="color:var(--neu-danger)"></i>
<span class="stat-num" x-text="stopped.length"></span>
<span class="stat-label">Arrêtés</span>
</div>
<span class="metric-val" x-text="Math.round((r.cpu||0)*100) + '%'"></span>
</div>
<div class="metric">
<span class="metric-label">RAM</span>
<div class="metric-bar">
<div class="metric-fill" :style="'width:' + Math.round((r.mem||0)/(r.maxmem||1)*100) + '%'"></div>
</div>
<span class="metric-val" x-text="formatMem(r.mem)"></span>
<div class="stat-row">
<i class="lnid-server-1 stat-icon" style="color:var(--neu-info)"></i>
<span class="stat-num" x-text="lxc.length"></span>
<span class="stat-label" x-text="t('dashboard.lxcCount')"></span>
</div>
</div>
</div>
</template>
<!-- Widget: lxc-list -->
<template x-if="w.id === 'lxc-list'">
<div class="widget-lxc">
<h4 class="widget-title" x-text="t('dashboard.lxcStatus')"></h4>
<div class="loading-state" x-show="wsStatus === 'connecting'">
<div class="spinner-lg"></div>
</div>
<p class="empty-state" x-show="lxcList.length === 0 && wsStatus === 'ok'">Aucun container</p>
<div class="loading" x-show="wsStatus === 'connecting'">Chargement…</div>
<div x-show="wsStatus !== 'connecting'">
<template x-for="item in lxc" :key="item.vmid">
<div class="lxc-row">
<i :class="item.status === 'running' ? 'lnid-play' : 'lnid-stop'"
:style="'color:' + (item.status === 'running' ? 'var(--neu-success)' : 'var(--neu-danger)')"></i>
<span class="lxc-name" x-text="item.name || 'CT' + item.vmid"></span>
<span class="lxc-cpu" x-text="Math.round((item.cpu || 0) * 100) + '%'"></span>
<span class="lxc-ram" x-text="Math.round((item.mem || 0) / 1048576) + 'M'"></span>
</div>
</template>
<p class="empty-state" x-show="lxc.length === 0" x-text="t('dashboard.noData')"></p>
</div>
</div>
</template>
<!-- Widget: links -->
<template x-if="w.id === 'links'">
<div class="widget-links">
<h4 class="widget-title" x-text="t('dashboard.widgetShortcut')"></h4>
<div class="links-grid">
<a class="neu-btn link-btn" href="/proxmox.html" @click.prevent="navigate('/proxmox.html')">
<i class="lnid-server-1"></i> Proxmox
</a>
<a class="neu-btn link-btn" href="/terminal.html" @click.prevent="navigate('/terminal.html')">
<i class="lnid-terminal"></i> Terminal
</a>
<a class="neu-btn link-btn" href="/updates.html" @click.prevent="navigate('/updates.html')">
<i class="lnid-arrow-upward"></i> Updates
</a>
</div>
</div>
</template>
</div>
</template>
<p class="empty-state" x-show="widgets.filter(w => w.visible).length === 0">
Aucun widget actif. Cliquez sur "Configurer" pour en ajouter.
</p>
</div>
</main>
</div>

View file

@ -9,6 +9,7 @@
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
<script src="/js/app.js"></script>
@ -18,7 +19,7 @@
<div class="auth-layout" x-data="installPage()" x-cloak>
<div class="install-card neu-card">
<div class="auth-logo">
<span class="logo-icon"></span>
<i class="logo-icon lnid-layout-1"></i>
<h1 class="auth-title">ProxmoxPanel</h1>
<p class="auth-subtitle" x-text="t('install.subtitle')"></p>
</div>

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: [],

View file

@ -8,7 +8,8 @@
"logs": "Logs",
"services": "Services",
"settings": "Settings",
"modules": "Modules"
"modules": "Modules",
"profile": "Profile"
},
"navbar": {
"darkMode": "Dark mode",

View file

@ -8,7 +8,8 @@
"logs": "Journaux",
"services": "Services",
"settings": "Paramètres",
"modules": "Modules"
"modules": "Modules",
"profile": "Profil"
},
"navbar": {
"darkMode": "Mode sombre",

View file

@ -9,6 +9,7 @@
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
<script src="/js/app.js"></script>
@ -18,7 +19,7 @@
<div class="auth-layout" x-data="loginPage()" x-cloak>
<div class="auth-card neu-card">
<div class="auth-logo">
<span class="logo-icon"></span>
<i class="logo-icon lnid-layout-1"></i>
<h1 class="auth-title">ProxmoxPanel</h1>
<p class="auth-subtitle" x-text="t('login.subtitle')"></p>
</div>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProxmoxPanel — Modules</title>
@ -9,6 +9,7 @@
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
<script src="/js/app.js"></script>
@ -18,18 +19,26 @@
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
<div class="sidebar-header">
<span class="sidebar-logo"></span>
<i class="sidebar-logo lnid-layout-1"></i>
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()"></button>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
<i class="lnid-menu-hamburger-1"></i>
</button>
</div>
<nav class="sidebar-nav">
<template x-for="item in navItems" :key="item.id">
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
<span class="sidebar-icon" x-text="item.icon"></span>
<i class="sidebar-icon" :class="item.iconClass"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
</a>
</template>
</nav>
<div class="sidebar-footer">
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
<i class="sidebar-icon lnid-user-circle-1"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
</a>
</div>
</aside>
<div class="main-layout">
@ -39,10 +48,12 @@
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
<option value="fr">FR</option><option value="en">EN</option>
</select>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
<span x-text="theme==='dark'?'☀':'🌙'"></span>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
<i class="lnid-power-button"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()"></button>
</div>
</nav>
@ -58,7 +69,9 @@
<template x-for="mod in modules" :key="mod.id">
<div class="neu-card module-card" :class="{ disabled: !mod.is_enabled }">
<div class="module-header">
<div class="module-icon" x-text="mod.icon || '⬡'"></div>
<div class="module-icon">
<i :class="mod.icon || 'lnid-puzzle'"></i>
</div>
<div class="module-info">
<span class="module-name" x-text="mod.name || mod.id"></span>
<span class="module-desc" x-text="mod.description || ''"></span>

160
frontend/profile.html Normal file
View file

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProxmoxPanel — Profil</title>
<link rel="stylesheet" href="/css/neu.css" />
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
<script src="/js/app.js"></script>
<script src="/js/vendors/alpine.min.js" defer></script>
</head>
<body x-data x-init="$store.ui.init()">
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
<div class="sidebar-header">
<i class="sidebar-logo lnid-layout-1"></i>
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
<i class="lnid-menu-hamburger-1"></i>
</button>
</div>
<nav class="sidebar-nav">
<template x-for="item in navItems" :key="item.id">
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
<i class="sidebar-icon" :class="item.iconClass"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
</a>
</template>
</nav>
<div class="sidebar-footer">
<a class="sidebar-link active" href="/profile.html" @click.prevent="navigate('/profile.html')">
<i class="sidebar-icon lnid-user-circle-1"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
</a>
</div>
</aside>
<div class="main-layout">
<nav class="navbar" x-data="navbar()" x-cloak>
<h2 class="navbar-title" x-text="t('nav.profile')"></h2>
<div class="navbar-actions">
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
<option value="fr">FR</option><option value="en">EN</option>
</select>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
<i class="lnid-power-button"></i>
</button>
</div>
</nav>
<main id="swup" class="page-content transition-fade">
<div x-data="profilePage()" x-cloak>
<!-- Informations du compte -->
<div class="neu-card settings-section" x-show="user">
<h3 class="section-title">
<i class="lnid-user-circle-1"></i>
Compte
</h3>
<div class="profile-info">
<div class="profile-row">
<span class="profile-label">Utilisateur</span>
<span class="profile-value" x-text="user?.username"></span>
</div>
<div class="profile-row">
<span class="profile-label">Rôle</span>
<span class="profile-value">
<span class="badge" :class="user?.is_admin ? 'badge-admin' : 'badge-user'"
x-text="user?.is_admin ? 'Administrateur' : 'Utilisateur'"></span>
</span>
</div>
</div>
</div>
<!-- Apparence -->
<div class="neu-card settings-section">
<h3 class="section-title">
<i class="lnid-sun-1"></i>
Apparence
</h3>
<div class="form-group">
<label class="form-label">Thème</label>
<div class="btn-group">
<button class="neu-btn"
:class="{ 'neu-btn--primary': theme === 'dark' }"
@click="setTheme('dark')">
<i class="lnid-moon-half-left-1"></i> Sombre
</button>
<button class="neu-btn"
:class="{ 'neu-btn--primary': theme === 'light' }"
@click="setTheme('light')">
<i class="lnid-sun-1"></i> Clair
</button>
</div>
</div>
<div class="form-group">
<label class="form-label" x-text="t('settings.sidebarPosition')"></label>
<div class="btn-group">
<button class="neu-btn"
:class="{ 'neu-btn--primary': sidebarPosition === 'left' }"
@click="setSidebarPosition('left')">
<i class="lnid-layout-1"></i> <span x-text="t('settings.left')"></span>
</button>
<button class="neu-btn"
:class="{ 'neu-btn--primary': sidebarPosition === 'right' }"
@click="setSidebarPosition('right')">
<i class="lnid-layout-2"></i> <span x-text="t('settings.right')"></span>
</button>
</div>
</div>
</div>
<!-- Langue -->
<div class="neu-card settings-section">
<h3 class="section-title">
<i class="lnid-gear-1"></i>
Langue
</h3>
<div class="form-group">
<label class="form-label" x-text="t('settings.defaultLang')"></label>
<div class="btn-group">
<button class="neu-btn"
:class="{ 'neu-btn--primary': lang === 'fr' }"
@click="setLang('fr')">
Français
</button>
<button class="neu-btn"
:class="{ 'neu-btn--primary': lang === 'en' }"
@click="setLang('en')">
English
</button>
</div>
</div>
</div>
<!-- Déconnexion -->
<div class="neu-card settings-section">
<button class="neu-btn neu-btn--danger" @click="$store.auth.logout()">
<i class="lnid-power-button"></i>
<span x-text="t('navbar.logout')"></span>
</button>
</div>
</div>
</main>
</div>
</body>
</html>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProxmoxPanel — Proxmox</title>
@ -9,6 +9,7 @@
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
<script src="/js/app.js"></script>
@ -18,18 +19,26 @@
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
<div class="sidebar-header">
<span class="sidebar-logo"></span>
<i class="sidebar-logo lnid-layout-1"></i>
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()"></button>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
<i class="lnid-menu-hamburger-1"></i>
</button>
</div>
<nav class="sidebar-nav">
<template x-for="item in navItems" :key="item.id">
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
<span class="sidebar-icon" x-text="item.icon"></span>
<i class="sidebar-icon" :class="item.iconClass"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
</a>
</template>
</nav>
<div class="sidebar-footer">
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
<i class="sidebar-icon lnid-user-circle-1"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
</a>
</div>
</aside>
<div class="main-layout">
@ -40,9 +49,11 @@
<option value="fr">FR</option><option value="en">EN</option>
</select>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
<span x-text="theme==='dark'?'☀':'🌙'"></span>
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
<i class="lnid-power-button"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')"></button>
</div>
</nav>
@ -51,6 +62,7 @@
<div class="ws-status" :class="wsStatus">
<span x-show="wsStatus==='connecting'">⌛ Connexion WebSocket…</span>
<span x-show="wsStatus==='connected'" style="color:var(--neu-success)">● Live</span>
<span x-show="wsStatus==='ok'" style="color:var(--neu-success)">● Live</span>
<span x-show="wsStatus==='disconnected'" style="color:var(--neu-warning)">⚠ Reconnexion…</span>
<span x-show="wsStatus==='error'" style="color:var(--neu-danger)">✗ Erreur WebSocket</span>
@ -81,17 +93,17 @@
<div class="resource-actions">
<button class="neu-btn neu-btn--sm neu-btn--success" x-show="r.status!=='running'"
@click="action(r.vmid,'lxc','start')" :disabled="actionLoading[r.vmid+'-start']">
▶ Start
<i class="lnid-play"></i> <span x-text="t('proxmox.start')"></span>
</button>
<button class="neu-btn neu-btn--sm neu-btn--danger" x-show="r.status==='running'"
@click="action(r.vmid,'lxc','stop')" :disabled="actionLoading[r.vmid+'-stop']">
⏹ Stop
<i class="lnid-stop"></i> <span x-text="t('proxmox.stop')"></span>
</button>
</div>
</div>
</template>
</div>
<p class="empty-state" x-show="resources.filter(r=>r.type==='lxc').length===0&&wsStatus==='ok'">Aucun container LXC</p>
<p class="empty-state" x-show="resources.filter(r=>r.type==='lxc').length===0&&wsStatus!=='connecting'">Aucun container LXC</p>
</div>
<!-- VMs -->
@ -114,17 +126,17 @@
<div class="resource-actions">
<button class="neu-btn neu-btn--sm neu-btn--success" x-show="r.status!=='running'"
@click="action(r.vmid,'qemu','start')" :disabled="actionLoading[r.vmid+'-start']">
▶ Start
<i class="lnid-play"></i> <span x-text="t('proxmox.start')"></span>
</button>
<button class="neu-btn neu-btn--sm neu-btn--danger" x-show="r.status==='running'"
@click="action(r.vmid,'qemu','stop')" :disabled="actionLoading[r.vmid+'-stop']">
⏹ Stop
<i class="lnid-stop"></i> <span x-text="t('proxmox.stop')"></span>
</button>
</div>
</div>
</template>
</div>
<p class="empty-state" x-show="resources.filter(r=>r.type==='qemu').length===0&&wsStatus==='ok'">Aucune VM</p>
<p class="empty-state" x-show="resources.filter(r=>r.type==='qemu').length===0&&wsStatus!=='connecting'">Aucune VM</p>
</div>
</div>
</main>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProxmoxPanel — Paramètres</title>
@ -9,6 +9,7 @@
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
<script src="/js/app.js"></script>
@ -18,18 +19,26 @@
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
<div class="sidebar-header">
<span class="sidebar-logo"></span>
<i class="sidebar-logo lnid-layout-1"></i>
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()"></button>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
<i class="lnid-menu-hamburger-1"></i>
</button>
</div>
<nav class="sidebar-nav">
<template x-for="item in navItems" :key="item.id">
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
<span class="sidebar-icon" x-text="item.icon"></span>
<i class="sidebar-icon" :class="item.iconClass"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
</a>
</template>
</nav>
<div class="sidebar-footer">
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
<i class="sidebar-icon lnid-user-circle-1"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
</a>
</div>
</aside>
<div class="main-layout">
@ -39,10 +48,12 @@
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
<option value="fr">FR</option><option value="en">EN</option>
</select>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
<span x-text="theme==='dark'?'☀':'🌙'"></span>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
<i class="lnid-power-button"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()"></button>
</div>
</nav>
@ -125,11 +136,13 @@
<!-- Save actions -->
<div class="save-bar">
<div class="save-feedback">
<span class="save-success" x-show="saved">✓ Paramètres sauvegardés</span>
<span class="save-success" x-show="saved">
<i class="lnid-check-circle-1"></i> Paramètres sauvegardés
</span>
<span class="save-error" x-show="error" x-text="error"></span>
</div>
<button class="neu-btn neu-btn--primary" @click="save()" :disabled="saving">
<span x-show="!saving">💾 Sauvegarder</span>
<span x-show="!saving"><i class="lnid-check-circle-1"></i> Sauvegarder</span>
<span x-show="saving"><span class="spinner-sm"></span></span>
</button>
</div>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProxmoxPanel — Terminal</title>
@ -9,6 +9,7 @@
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
<link rel="stylesheet" href="/css/xterm.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
@ -21,18 +22,26 @@
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
<div class="sidebar-header">
<span class="sidebar-logo"></span>
<i class="sidebar-logo lnid-layout-1"></i>
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()"></button>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
<i class="lnid-menu-hamburger-1"></i>
</button>
</div>
<nav class="sidebar-nav">
<template x-for="item in navItems" :key="item.id">
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
<span class="sidebar-icon" x-text="item.icon"></span>
<i class="sidebar-icon" :class="item.iconClass"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
</a>
</template>
</nav>
<div class="sidebar-footer">
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
<i class="sidebar-icon lnid-user-circle-1"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
</a>
</div>
</aside>
<div class="main-layout terminal-wrapper">
@ -42,17 +51,21 @@
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
<option value="fr">FR</option><option value="en">EN</option>
</select>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
<span x-text="theme==='dark'?'☀':'🌙'"></span>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
<i class="lnid-power-button"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()"></button>
</div>
</nav>
<main id="swup" class="page-content transition-fade terminal-layout">
<div class="terminal-toolbar">
<span id="terminal-status" class="terminal-status">⌛ Connexion…</span>
<button id="terminal-clear" class="neu-btn neu-btn--sm">✕ Effacer</button>
<button id="terminal-clear" class="neu-btn neu-btn--sm">
<i class="lnid-trash-1"></i> Effacer
</button>
</div>
<div id="terminal-container" class="terminal-container"></div>
</main>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProxmoxPanel — Mises à jour</title>
@ -9,6 +9,7 @@
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
<script src="/js/app.js"></script>
@ -18,18 +19,26 @@
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
<div class="sidebar-header">
<span class="sidebar-logo"></span>
<i class="sidebar-logo lnid-layout-1"></i>
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()"></button>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
<i class="lnid-menu-hamburger-1"></i>
</button>
</div>
<nav class="sidebar-nav">
<template x-for="item in navItems" :key="item.id">
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
<span class="sidebar-icon" x-text="item.icon"></span>
<i class="sidebar-icon" :class="item.iconClass"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
</a>
</template>
</nav>
<div class="sidebar-footer">
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
<i class="sidebar-icon lnid-user-circle-1"></i>
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
</a>
</div>
</aside>
<div class="main-layout">
@ -39,10 +48,12 @@
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
<option value="fr">FR</option><option value="en">EN</option>
</select>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
<span x-text="theme==='dark'?'☀':'🌙'"></span>
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
<i class="lnid-power-button"></i>
</button>
<button class="neu-btn neu-btn--sm" @click="logout()"></button>
</div>
</nav>
@ -58,11 +69,11 @@
</div>
<div class="page-actions-right">
<button class="neu-btn" @click="checkAll()" :disabled="loading || targets.some(t=>t.checking)">
Tout vérifier
<i class="lnid-refresh-circle-1-clockwise"></i> Tout vérifier
</button>
<button class="neu-btn neu-btn--primary" @click="updateAll()"
:disabled="loading || jobStatus === 'running' || totalPackages === 0">
Tout mettre à jour
<i class="lnid-arrow-upward"></i> Tout mettre à jour
</button>
</div>
</div>
@ -96,14 +107,16 @@
Non vérifié
</span>
<span x-show="!target.checking && target.packages !== null && target.packages.length === 0"
class="up-to-date">✓ À jour</span>
class="up-to-date">
<i class="lnid-check-circle-1"></i> À jour
</span>
<span x-show="!target.checking && target.packages !== null && target.packages.length > 0"
class="has-updates">
<span x-text="target.packages.length"></span> paquet(s) à mettre à jour
</span>
</div>
<!-- Package list (collapsible) -->
<!-- Package list -->
<div class="package-list" x-show="target.packages && target.packages.length > 0">
<template x-for="pkg in target.packages" :key="pkg.name">
<div class="package-row">
@ -119,15 +132,14 @@
<!-- Card actions -->
<div class="target-actions">
<button class="neu-btn neu-btn--sm" @click="checkTarget(target)"
:disabled="target.checking">
↻ Vérifier
<button class="neu-btn neu-btn--sm" @click="checkTarget(target)" :disabled="target.checking">
<i class="lnid-refresh-circle-1-clockwise"></i> Vérifier
</button>
<button class="neu-btn neu-btn--sm neu-btn--primary"
@click="updateTarget(target)"
:disabled="target.updating || jobStatus === 'running' || !target.packages || target.packages.length === 0"
x-show="target.status !== 'stopped'">
<span x-show="!target.updating"> Mettre à jour</span>
<span x-show="!target.updating"><i class="lnid-arrow-upward"></i> Mettre à jour</span>
<span x-show="target.updating"><span class="spinner-sm"></span> En cours…</span>
</button>
</div>