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

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>
: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>
<!-- 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>
</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>
<!-- 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>
<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 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>
<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>
<span class="metric-val" x-text="formatMem(r.mem)"></span>
</div>
</div>
</div>
</template>
</div>
<p class="empty-state" x-show="lxcList.length === 0 && wsStatus === 'ok'">Aucun container</p>
<div class="loading" x-show="wsStatus === 'connecting'">Chargement…</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>
<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>