feat: LineIcons Duotone, page profil, widgets dashboard, sidebar gauche/droite
- Intégration LineIcons Duotone (css/ + toutes les pages) - Remplacement de tous les symboles Unicode par des icônes lnid-* - Page profile.html : préférences thème, position sidebar, langue - Dashboard : système de widgets add/remove + drag-and-drop natif - Sidebar gauche/droite configurable per-user (data-sidebar CSS + FOUC script) - Store ui : sidebarPosition, applySidebarPosition(), setSidebarPosition() - Composant profilePage() dans app.js - nav.profile ajouté dans fr.json et en.json - SUIVI.md mis à jour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9739dbaee8
commit
5f6681dd17
19 changed files with 11717 additions and 148 deletions
9
SUIVI.md
9
SUIVI.md
|
|
@ -110,10 +110,11 @@ Référence : `instruction.md` | Mis à jour : 2026-03-21
|
||||||
|
|
||||||
| Règle | État | Priorité |
|
| Règle | État | Priorité |
|
||||||
|-------|------|----------|
|
|-------|------|----------|
|
||||||
| Icônes LineIcons Duotone uniquement | ❌ Symbols Unicode utilisés | Haute |
|
| Icônes LineIcons Duotone uniquement | ✅ Intégrés (css/ + toutes les pages) | Haute |
|
||||||
| Dashboard widgets add/remove/drag-drop | ❌ Non implémenté | Haute |
|
| Dashboard widgets add/remove/drag-drop | ✅ Widget system + DnD natif HTML5 | Haute |
|
||||||
| Sidebar gauche/droite per-user | ❌ Gauche fixe uniquement | Moyenne |
|
| Sidebar gauche/droite per-user | ✅ `data-sidebar` CSS + profilePage | Moyenne |
|
||||||
| Préférences utilisateur en DB | ❌ localStorage uniquement | 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 |
|
| Historique mises à jour | ❌ Non affiché | Basse |
|
||||||
| Mises à jour CORE/modules depuis interface | ❌ Non implémenté | Basse |
|
| Mises à jour CORE/modules depuis interface | ❌ Non implémenté | Basse |
|
||||||
| Page blocage migration | ❌ Non implémenté | Basse |
|
| Page blocage migration | ❌ Non implémenté | Basse |
|
||||||
|
|
|
||||||
11023
frontend/css/lineicons-duotone.css
Normal file
11023
frontend/css/lineicons-duotone.css
Normal file
File diff suppressed because it is too large
Load diff
BIN
frontend/css/lineicons-duotone.ttf
Normal file
BIN
frontend/css/lineicons-duotone.ttf
Normal file
Binary file not shown.
BIN
frontend/css/lineicons-duotone.woff
Normal file
BIN
frontend/css/lineicons-duotone.woff
Normal file
Binary file not shown.
BIN
frontend/css/lineicons-duotone.woff2
Normal file
BIN
frontend/css/lineicons-duotone.woff2
Normal file
Binary file not shown.
|
|
@ -488,10 +488,13 @@ input, select, textarea { font-family: inherit; font-size: inherit; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-icon {
|
.sidebar-icon {
|
||||||
font-size: 1.1rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
text-align: center;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-label {
|
.sidebar-label {
|
||||||
|
|
@ -594,3 +597,27 @@ html.is-animating .transition-fade {
|
||||||
}
|
}
|
||||||
|
|
||||||
[x-cloak] { display: none !important; }
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -445,3 +445,167 @@
|
||||||
.confirm-summary { padding: 1rem; display: flex; flex-direction: column; gap: .5rem; border-radius: .5rem; }
|
.confirm-summary { padding: 1rem; display: flex; flex-direction: column; gap: .5rem; border-radius: .5rem; }
|
||||||
.confirm-row { display: flex; gap: 1rem; font-size: .875rem; }
|
.confirm-row { display: flex; gap: 1rem; font-size: .875rem; }
|
||||||
.confirm-label { font-weight: 600; min-width: 80px; color: var(--neu-text-muted); }
|
.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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ProxmoxPanel — Dashboard</title>
|
<title>ProxmoxPanel — Dashboard</title>
|
||||||
|
|
@ -9,119 +9,171 @@
|
||||||
<link rel="stylesheet" href="/css/dark.css" />
|
<link rel="stylesheet" href="/css/dark.css" />
|
||||||
<link rel="stylesheet" href="/css/light.css" />
|
<link rel="stylesheet" href="/css/light.css" />
|
||||||
<link rel="stylesheet" href="/css/pages.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/htmx.min.js"></script>
|
||||||
<script src="/js/vendors/swup.iife.js"></script>
|
<script src="/js/vendors/swup.iife.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
<script src="/js/vendors/alpine.min.js" defer></script>
|
<script src="/js/vendors/alpine.min.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body x-data>
|
<body x-data x-init="$store.ui.init()">
|
||||||
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||||
<div class="sidebar-header">
|
<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>
|
<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>
|
</div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<template x-for="item in navItems" :key="item.id">
|
<template x-for="item in navItems" :key="item.id">
|
||||||
<a class="sidebar-link"
|
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
|
||||||
:class="{ active: isActive(item.id) }"
|
<i class="sidebar-icon" :class="item.iconClass"></i>
|
||||||
:href="item.href"
|
|
||||||
@click.prevent="navigate(item.href)">
|
|
||||||
<span class="sidebar-icon" x-text="item.icon"></span>
|
|
||||||
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="sidebar-footer" x-show="!collapsed">
|
<div class="sidebar-footer">
|
||||||
<span class="sidebar-user" x-text="$store.auth.user?.username || ''"></span>
|
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
|
||||||
<button class="neu-btn neu-btn--sm" @click="$store.auth.logout()">⏻</button>
|
<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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
<!-- Navbar -->
|
|
||||||
<nav class="navbar" x-data="navbar()" x-cloak>
|
<nav class="navbar" x-data="navbar()" x-cloak>
|
||||||
<h2 class="navbar-title" x-text="t('nav.dashboard')"></h2>
|
<h2 class="navbar-title" x-text="t('nav.dashboard')"></h2>
|
||||||
<div class="navbar-actions">
|
<div class="navbar-actions">
|
||||||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
||||||
<option value="fr">FR</option>
|
<option value="fr">FR</option><option value="en">EN</option>
|
||||||
<option value="en">EN</option>
|
|
||||||
</select>
|
</select>
|
||||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()"
|
<button class="neu-btn neu-btn--sm" @click="toggleTheme()"
|
||||||
:title="theme === 'dark' ? t('navbar.lightMode') : t('navbar.darkMode')">
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Contenu swappé -->
|
|
||||||
<main id="swup" class="page-content transition-fade" x-data="dashboardPage()" x-cloak>
|
<main id="swup" class="page-content transition-fade" x-data="dashboardPage()" x-cloak>
|
||||||
<!-- Stats -->
|
|
||||||
<div class="stats-grid">
|
<div class="page-header">
|
||||||
<div class="neu-card stat-card">
|
<h2 class="page-title" x-text="t('dashboard.welcome').replace('{name}', $store.auth.user?.username || '')"></h2>
|
||||||
<div class="stat-icon">▶</div>
|
<button class="neu-btn neu-btn--sm" @click="configOpen = !configOpen" :title="t('dashboard.addWidget')">
|
||||||
<div class="stat-value" x-text="runningCount"></div>
|
<i class="lnid-layout-1"></i>
|
||||||
<div class="stat-label">En cours</div>
|
<span x-show="!configOpen" x-text="t('dashboard.addWidget')"></span>
|
||||||
</div>
|
<span x-show="configOpen">✕</span>
|
||||||
<div class="neu-card stat-card">
|
</button>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Status WS -->
|
<!-- Config panel widgets -->
|
||||||
<div class="ws-status" :class="wsStatus">
|
<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 === 'connecting'">⌛ Connexion…</span>
|
||||||
<span x-show="wsStatus === 'ok'">● Live</span>
|
|
||||||
<span x-show="wsStatus === 'disconnected'">⚠ Déconnecté (reconnexion…)</span>
|
<span x-show="wsStatus === 'disconnected'">⚠ Déconnecté (reconnexion…)</span>
|
||||||
<span x-show="wsStatus === 'error'">✗ Erreur WebSocket</span>
|
<span x-show="wsStatus === 'error'">✗ Erreur WebSocket</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- LXC List -->
|
<!-- Widgets -->
|
||||||
<div class="section">
|
<div class="widgets-grid">
|
||||||
<h3 class="section-title">Containers LXC</h3>
|
<template x-for="(w, idx) in widgets.filter(w => w.visible)" :key="w.id">
|
||||||
<div class="resource-grid" x-show="lxcList.length > 0">
|
<div class="neu-card widget"
|
||||||
<template x-for="r in lxcList" :key="r.vmid">
|
draggable="true"
|
||||||
<div class="neu-card resource-card" :class="r.status">
|
@dragstart="onDragStart(widgets.indexOf(w))"
|
||||||
<div class="resource-header">
|
@dragover.prevent
|
||||||
<span class="resource-name" x-text="r.name || 'LXC ' + r.vmid"></span>
|
@drop.prevent="onDrop(widgets.indexOf(w))">
|
||||||
<span class="resource-id" x-text="'#' + r.vmid"></span>
|
|
||||||
<span class="resource-badge" :class="r.status" x-text="r.status"></span>
|
<!-- Widget: status -->
|
||||||
</div>
|
<template x-if="w.id === 'status'">
|
||||||
<div class="resource-metrics" x-show="r.status === 'running'">
|
<div class="widget-status">
|
||||||
<div class="metric">
|
<h4 class="widget-title" x-text="t('dashboard.lxcStatus')"></h4>
|
||||||
<span class="metric-label">CPU</span>
|
<div class="stat-rows">
|
||||||
<div class="metric-bar">
|
<div class="stat-row">
|
||||||
<div class="metric-fill" :style="'width:' + Math.round((r.cpu||0)*100) + '%;background:' + cpuColor(Math.round((r.cpu||0)*100))"></div>
|
<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>
|
||||||
<span class="metric-val" x-text="Math.round((r.cpu||0)*100) + '%'"></span>
|
<div class="stat-row">
|
||||||
</div>
|
<i class="lnid-stop stat-icon" style="color:var(--neu-danger)"></i>
|
||||||
<div class="metric">
|
<span class="stat-num" x-text="stopped.length"></span>
|
||||||
<span class="metric-label">RAM</span>
|
<span class="stat-label">Arrêtés</span>
|
||||||
<div class="metric-bar">
|
</div>
|
||||||
<div class="metric-fill" :style="'width:' + Math.round((r.mem||0)/(r.maxmem||1)*100) + '%'"></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>
|
</div>
|
||||||
<span class="metric-val" x-text="formatMem(r.mem)"></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
|
||||||
</div>
|
<!-- Widget: lxc-list -->
|
||||||
<p class="empty-state" x-show="lxcList.length === 0 && wsStatus === 'ok'">Aucun container</p>
|
<template x-if="w.id === 'lxc-list'">
|
||||||
<div class="loading" x-show="wsStatus === 'connecting'">Chargement…</div>
|
<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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<link rel="stylesheet" href="/css/dark.css" />
|
<link rel="stylesheet" href="/css/dark.css" />
|
||||||
<link rel="stylesheet" href="/css/light.css" />
|
<link rel="stylesheet" href="/css/light.css" />
|
||||||
<link rel="stylesheet" href="/css/pages.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/htmx.min.js"></script>
|
||||||
<script src="/js/vendors/swup.iife.js"></script>
|
<script src="/js/vendors/swup.iife.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|
@ -18,7 +19,7 @@
|
||||||
<div class="auth-layout" x-data="installPage()" x-cloak>
|
<div class="auth-layout" x-data="installPage()" x-cloak>
|
||||||
<div class="install-card neu-card">
|
<div class="install-card neu-card">
|
||||||
<div class="auth-logo">
|
<div class="auth-logo">
|
||||||
<span class="logo-icon">⬡</span>
|
<i class="logo-icon lnid-layout-1"></i>
|
||||||
<h1 class="auth-title">ProxmoxPanel</h1>
|
<h1 class="auth-title">ProxmoxPanel</h1>
|
||||||
<p class="auth-subtitle" x-text="t('install.subtitle')"></p>
|
<p class="auth-subtitle" x-text="t('install.subtitle')"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,12 @@ document.addEventListener('alpine:init', () => {
|
||||||
Alpine.store('ui', {
|
Alpine.store('ui', {
|
||||||
theme: localStorage.getItem('pxp_theme') || 'dark',
|
theme: localStorage.getItem('pxp_theme') || 'dark',
|
||||||
sidebarCollapsed: localStorage.getItem('pxp_sidebar') === 'true',
|
sidebarCollapsed: localStorage.getItem('pxp_sidebar') === 'true',
|
||||||
|
sidebarPosition: localStorage.getItem('pxp_sidebar_pos') || 'left',
|
||||||
currentPage: '',
|
currentPage: '',
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.applyTheme()
|
this.applyTheme()
|
||||||
// Detect current page from URL
|
this.applySidebarPosition()
|
||||||
const path = window.location.pathname
|
const path = window.location.pathname
|
||||||
this.currentPage = path.replace(/^\/|\.html$/g, '') || 'index'
|
this.currentPage = path.replace(/^\/|\.html$/g, '') || 'index'
|
||||||
},
|
},
|
||||||
|
|
@ -116,6 +117,16 @@ document.addEventListener('alpine:init', () => {
|
||||||
this.applyTheme()
|
this.applyTheme()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
applySidebarPosition() {
|
||||||
|
document.documentElement.setAttribute('data-sidebar', this.sidebarPosition)
|
||||||
|
},
|
||||||
|
|
||||||
|
setSidebarPosition(pos) {
|
||||||
|
this.sidebarPosition = pos
|
||||||
|
localStorage.setItem('pxp_sidebar_pos', pos)
|
||||||
|
this.applySidebarPosition()
|
||||||
|
},
|
||||||
|
|
||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
this.sidebarCollapsed = !this.sidebarCollapsed
|
this.sidebarCollapsed = !this.sidebarCollapsed
|
||||||
localStorage.setItem('pxp_sidebar', this.sidebarCollapsed)
|
localStorage.setItem('pxp_sidebar', this.sidebarCollapsed)
|
||||||
|
|
@ -168,12 +179,12 @@ document.addEventListener('alpine:init', () => {
|
||||||
get currentPage() { return Alpine.store('ui').currentPage },
|
get currentPage() { return Alpine.store('ui').currentPage },
|
||||||
|
|
||||||
navItems: [
|
navItems: [
|
||||||
{ id: 'dashboard', icon: '⊞', labelKey: 'nav.dashboard', href: '/dashboard.html' },
|
{ id: 'dashboard', iconClass: 'lnid-dashboard-square-1', labelKey: 'nav.dashboard', href: '/dashboard.html' },
|
||||||
{ id: 'proxmox', icon: '⬡', labelKey: 'nav.proxmox', href: '/proxmox.html' },
|
{ id: 'proxmox', iconClass: 'lnid-server-1', labelKey: 'nav.proxmox', href: '/proxmox.html' },
|
||||||
{ id: 'updates', icon: '↑', labelKey: 'nav.updates', href: '/updates.html' },
|
{ id: 'updates', iconClass: 'lnid-arrow-upward', labelKey: 'nav.updates', href: '/updates.html' },
|
||||||
{ id: 'terminal', icon: '❯', labelKey: 'nav.terminal', href: '/terminal.html' },
|
{ id: 'terminal', iconClass: 'lnid-terminal', labelKey: 'nav.terminal', href: '/terminal.html' },
|
||||||
{ id: 'settings', icon: '⚙', labelKey: 'nav.settings', href: '/settings.html' },
|
{ id: 'settings', iconClass: 'lnid-gear-1', labelKey: 'nav.settings', href: '/settings.html' },
|
||||||
{ id: 'modules', icon: '⬡', labelKey: 'nav.modules', href: '/modules.html' },
|
{ id: 'modules', iconClass: 'lnid-puzzle', labelKey: 'nav.modules', href: '/modules.html' },
|
||||||
],
|
],
|
||||||
|
|
||||||
isActive(id) {
|
isActive(id) {
|
||||||
|
|
@ -305,9 +316,37 @@ document.addEventListener('alpine:init', () => {
|
||||||
resources: [],
|
resources: [],
|
||||||
ws: null,
|
ws: null,
|
||||||
wsStatus: 'connecting',
|
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() {
|
async init() {
|
||||||
// Chargement immédiat via HTTP, puis WS pour le temps réel
|
|
||||||
await this.fetchResources()
|
await this.fetchResources()
|
||||||
this.connectWS()
|
this.connectWS()
|
||||||
},
|
},
|
||||||
|
|
@ -321,7 +360,7 @@ document.addEventListener('alpine:init', () => {
|
||||||
const res = await apiFetch('/api/proxmox/resources')
|
const res = await apiFetch('/api/proxmox/resources')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.resources = await res.json() || []
|
this.resources = await res.json() || []
|
||||||
this.wsStatus = 'ok'
|
this.wsStatus = 'connected'
|
||||||
}
|
}
|
||||||
} catch (e) { /* WS prendra le relais */ }
|
} 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 = new WebSocket(`${proto}://${location.host}/ws/proxmox?token=${token}`)
|
||||||
this.ws.onmessage = (e) => {
|
this.ws.onmessage = (e) => {
|
||||||
const msg = JSON.parse(e.data)
|
const msg = JSON.parse(e.data)
|
||||||
// Le backend publie type="resources_update", payload=[...]
|
|
||||||
if (msg.type === 'resources_update') {
|
if (msg.type === 'resources_update') {
|
||||||
this.resources = msg.payload || []
|
this.resources = msg.payload || []
|
||||||
this.wsStatus = 'ok'
|
this.wsStatus = 'connected'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = () => {
|
||||||
|
|
@ -345,14 +383,51 @@ document.addEventListener('alpine:init', () => {
|
||||||
this.ws.onerror = () => { this.wsStatus = 'error' }
|
this.ws.onerror = () => { this.wsStatus = 'error' }
|
||||||
},
|
},
|
||||||
|
|
||||||
get runningCount() { return this.resources.filter(r => r.status === 'running').length },
|
get lxc() { return this.resources.filter(r => r.type === 'lxc') },
|
||||||
get stoppedCount() { return this.resources.filter(r => r.status !== 'running').length },
|
get running() { return this.lxc.filter(r => r.status === 'running') },
|
||||||
get lxcList() { return this.resources.filter(r => r.type === 'lxc') },
|
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') },
|
get vmList() { return this.resources.filter(r => r.type === 'qemu') },
|
||||||
|
|
||||||
t(key) { return Alpine.store('i18n').t(key) },
|
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 ──────────────────────────────────────────────
|
// ── Composant: proxmoxPage ──────────────────────────────────────────────
|
||||||
Alpine.data('proxmoxPage', () => ({
|
Alpine.data('proxmoxPage', () => ({
|
||||||
resources: [],
|
resources: [],
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
"services": "Services",
|
"services": "Services",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"modules": "Modules"
|
"modules": "Modules",
|
||||||
|
"profile": "Profile"
|
||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"darkMode": "Dark mode",
|
"darkMode": "Dark mode",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
"logs": "Journaux",
|
"logs": "Journaux",
|
||||||
"services": "Services",
|
"services": "Services",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"modules": "Modules"
|
"modules": "Modules",
|
||||||
|
"profile": "Profil"
|
||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"darkMode": "Mode sombre",
|
"darkMode": "Mode sombre",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<link rel="stylesheet" href="/css/dark.css" />
|
<link rel="stylesheet" href="/css/dark.css" />
|
||||||
<link rel="stylesheet" href="/css/light.css" />
|
<link rel="stylesheet" href="/css/light.css" />
|
||||||
<link rel="stylesheet" href="/css/pages.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/htmx.min.js"></script>
|
||||||
<script src="/js/vendors/swup.iife.js"></script>
|
<script src="/js/vendors/swup.iife.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|
@ -18,7 +19,7 @@
|
||||||
<div class="auth-layout" x-data="loginPage()" x-cloak>
|
<div class="auth-layout" x-data="loginPage()" x-cloak>
|
||||||
<div class="auth-card neu-card">
|
<div class="auth-card neu-card">
|
||||||
<div class="auth-logo">
|
<div class="auth-logo">
|
||||||
<span class="logo-icon">⬡</span>
|
<i class="logo-icon lnid-layout-1"></i>
|
||||||
<h1 class="auth-title">ProxmoxPanel</h1>
|
<h1 class="auth-title">ProxmoxPanel</h1>
|
||||||
<p class="auth-subtitle" x-text="t('login.subtitle')"></p>
|
<p class="auth-subtitle" x-text="t('login.subtitle')"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ProxmoxPanel — Modules</title>
|
<title>ProxmoxPanel — Modules</title>
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<link rel="stylesheet" href="/css/dark.css" />
|
<link rel="stylesheet" href="/css/dark.css" />
|
||||||
<link rel="stylesheet" href="/css/light.css" />
|
<link rel="stylesheet" href="/css/light.css" />
|
||||||
<link rel="stylesheet" href="/css/pages.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/htmx.min.js"></script>
|
||||||
<script src="/js/vendors/swup.iife.js"></script>
|
<script src="/js/vendors/swup.iife.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|
@ -18,18 +19,26 @@
|
||||||
|
|
||||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||||
<div class="sidebar-header">
|
<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>
|
<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>
|
</div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<template x-for="item in navItems" :key="item.id">
|
<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)">
|
<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>
|
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</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>
|
</aside>
|
||||||
|
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
|
|
@ -39,10 +48,12 @@
|
||||||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
<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>
|
</select>
|
||||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
|
<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>
|
||||||
<button class="neu-btn neu-btn--sm" @click="logout()">⏻</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
@ -58,7 +69,9 @@
|
||||||
<template x-for="mod in modules" :key="mod.id">
|
<template x-for="mod in modules" :key="mod.id">
|
||||||
<div class="neu-card module-card" :class="{ disabled: !mod.is_enabled }">
|
<div class="neu-card module-card" :class="{ disabled: !mod.is_enabled }">
|
||||||
<div class="module-header">
|
<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">
|
<div class="module-info">
|
||||||
<span class="module-name" x-text="mod.name || mod.id"></span>
|
<span class="module-name" x-text="mod.name || mod.id"></span>
|
||||||
<span class="module-desc" x-text="mod.description || ''"></span>
|
<span class="module-desc" x-text="mod.description || ''"></span>
|
||||||
|
|
|
||||||
160
frontend/profile.html
Normal file
160
frontend/profile.html
Normal 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>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ProxmoxPanel — Proxmox</title>
|
<title>ProxmoxPanel — Proxmox</title>
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<link rel="stylesheet" href="/css/dark.css" />
|
<link rel="stylesheet" href="/css/dark.css" />
|
||||||
<link rel="stylesheet" href="/css/light.css" />
|
<link rel="stylesheet" href="/css/light.css" />
|
||||||
<link rel="stylesheet" href="/css/pages.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/htmx.min.js"></script>
|
||||||
<script src="/js/vendors/swup.iife.js"></script>
|
<script src="/js/vendors/swup.iife.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|
@ -18,18 +19,26 @@
|
||||||
|
|
||||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||||
<div class="sidebar-header">
|
<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>
|
<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>
|
</div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<template x-for="item in navItems" :key="item.id">
|
<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)">
|
<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>
|
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</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>
|
</aside>
|
||||||
|
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
|
|
@ -40,9 +49,11 @@
|
||||||
<option value="fr">FR</option><option value="en">EN</option>
|
<option value="fr">FR</option><option value="en">EN</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
|
<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>
|
||||||
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">⏻</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
@ -51,6 +62,7 @@
|
||||||
|
|
||||||
<div class="ws-status" :class="wsStatus">
|
<div class="ws-status" :class="wsStatus">
|
||||||
<span x-show="wsStatus==='connecting'">⌛ Connexion WebSocket…</span>
|
<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==='ok'" style="color:var(--neu-success)">● Live</span>
|
||||||
<span x-show="wsStatus==='disconnected'" style="color:var(--neu-warning)">⚠ Reconnexion…</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>
|
<span x-show="wsStatus==='error'" style="color:var(--neu-danger)">✗ Erreur WebSocket</span>
|
||||||
|
|
@ -81,17 +93,17 @@
|
||||||
<div class="resource-actions">
|
<div class="resource-actions">
|
||||||
<button class="neu-btn neu-btn--sm neu-btn--success" x-show="r.status!=='running'"
|
<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']">
|
@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>
|
||||||
<button class="neu-btn neu-btn--sm neu-btn--danger" x-show="r.status==='running'"
|
<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']">
|
@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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- VMs -->
|
<!-- VMs -->
|
||||||
|
|
@ -114,17 +126,17 @@
|
||||||
<div class="resource-actions">
|
<div class="resource-actions">
|
||||||
<button class="neu-btn neu-btn--sm neu-btn--success" x-show="r.status!=='running'"
|
<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']">
|
@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>
|
||||||
<button class="neu-btn neu-btn--sm neu-btn--danger" x-show="r.status==='running'"
|
<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']">
|
@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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ProxmoxPanel — Paramètres</title>
|
<title>ProxmoxPanel — Paramètres</title>
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<link rel="stylesheet" href="/css/dark.css" />
|
<link rel="stylesheet" href="/css/dark.css" />
|
||||||
<link rel="stylesheet" href="/css/light.css" />
|
<link rel="stylesheet" href="/css/light.css" />
|
||||||
<link rel="stylesheet" href="/css/pages.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/htmx.min.js"></script>
|
||||||
<script src="/js/vendors/swup.iife.js"></script>
|
<script src="/js/vendors/swup.iife.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|
@ -18,18 +19,26 @@
|
||||||
|
|
||||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||||
<div class="sidebar-header">
|
<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>
|
<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>
|
</div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<template x-for="item in navItems" :key="item.id">
|
<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)">
|
<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>
|
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</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>
|
</aside>
|
||||||
|
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
|
|
@ -39,10 +48,12 @@
|
||||||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
<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>
|
</select>
|
||||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
|
<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>
|
||||||
<button class="neu-btn neu-btn--sm" @click="logout()">⏻</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
@ -125,11 +136,13 @@
|
||||||
<!-- Save actions -->
|
<!-- Save actions -->
|
||||||
<div class="save-bar">
|
<div class="save-bar">
|
||||||
<div class="save-feedback">
|
<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>
|
<span class="save-error" x-show="error" x-text="error"></span>
|
||||||
</div>
|
</div>
|
||||||
<button class="neu-btn neu-btn--primary" @click="save()" :disabled="saving">
|
<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>
|
<span x-show="saving"><span class="spinner-sm"></span></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ProxmoxPanel — Terminal</title>
|
<title>ProxmoxPanel — Terminal</title>
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<link rel="stylesheet" href="/css/dark.css" />
|
<link rel="stylesheet" href="/css/dark.css" />
|
||||||
<link rel="stylesheet" href="/css/light.css" />
|
<link rel="stylesheet" href="/css/light.css" />
|
||||||
<link rel="stylesheet" href="/css/pages.css" />
|
<link rel="stylesheet" href="/css/pages.css" />
|
||||||
|
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
|
||||||
<link rel="stylesheet" href="/css/xterm.css" />
|
<link rel="stylesheet" href="/css/xterm.css" />
|
||||||
<script src="/js/vendors/htmx.min.js"></script>
|
<script src="/js/vendors/htmx.min.js"></script>
|
||||||
<script src="/js/vendors/swup.iife.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>
|
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||||
<div class="sidebar-header">
|
<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>
|
<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>
|
</div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<template x-for="item in navItems" :key="item.id">
|
<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)">
|
<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>
|
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</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>
|
</aside>
|
||||||
|
|
||||||
<div class="main-layout terminal-wrapper">
|
<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)">
|
<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>
|
</select>
|
||||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
|
<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>
|
||||||
<button class="neu-btn neu-btn--sm" @click="logout()">⏻</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main id="swup" class="page-content transition-fade terminal-layout">
|
<main id="swup" class="page-content transition-fade terminal-layout">
|
||||||
<div class="terminal-toolbar">
|
<div class="terminal-toolbar">
|
||||||
<span id="terminal-status" class="terminal-status">⌛ Connexion…</span>
|
<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>
|
||||||
<div id="terminal-container" class="terminal-container"></div>
|
<div id="terminal-container" class="terminal-container"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ProxmoxPanel — Mises à jour</title>
|
<title>ProxmoxPanel — Mises à jour</title>
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<link rel="stylesheet" href="/css/dark.css" />
|
<link rel="stylesheet" href="/css/dark.css" />
|
||||||
<link rel="stylesheet" href="/css/light.css" />
|
<link rel="stylesheet" href="/css/light.css" />
|
||||||
<link rel="stylesheet" href="/css/pages.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/htmx.min.js"></script>
|
||||||
<script src="/js/vendors/swup.iife.js"></script>
|
<script src="/js/vendors/swup.iife.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|
@ -18,18 +19,26 @@
|
||||||
|
|
||||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||||
<div class="sidebar-header">
|
<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>
|
<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>
|
</div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<template x-for="item in navItems" :key="item.id">
|
<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)">
|
<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>
|
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</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>
|
</aside>
|
||||||
|
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
|
|
@ -39,10 +48,12 @@
|
||||||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
<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>
|
</select>
|
||||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
|
<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>
|
||||||
<button class="neu-btn neu-btn--sm" @click="logout()">⏻</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
@ -58,11 +69,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="page-actions-right">
|
<div class="page-actions-right">
|
||||||
<button class="neu-btn" @click="checkAll()" :disabled="loading || targets.some(t=>t.checking)">
|
<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>
|
||||||
<button class="neu-btn neu-btn--primary" @click="updateAll()"
|
<button class="neu-btn neu-btn--primary" @click="updateAll()"
|
||||||
:disabled="loading || jobStatus === 'running' || totalPackages === 0">
|
:disabled="loading || jobStatus === 'running' || totalPackages === 0">
|
||||||
↑ Tout mettre à jour
|
<i class="lnid-arrow-upward"></i> Tout mettre à jour
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -96,14 +107,16 @@
|
||||||
Non vérifié
|
Non vérifié
|
||||||
</span>
|
</span>
|
||||||
<span x-show="!target.checking && target.packages !== null && target.packages.length === 0"
|
<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"
|
<span x-show="!target.checking && target.packages !== null && target.packages.length > 0"
|
||||||
class="has-updates">
|
class="has-updates">
|
||||||
<span x-text="target.packages.length"></span> paquet(s) à mettre à jour
|
<span x-text="target.packages.length"></span> paquet(s) à mettre à jour
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Package list (collapsible) -->
|
<!-- Package list -->
|
||||||
<div class="package-list" x-show="target.packages && target.packages.length > 0">
|
<div class="package-list" x-show="target.packages && target.packages.length > 0">
|
||||||
<template x-for="pkg in target.packages" :key="pkg.name">
|
<template x-for="pkg in target.packages" :key="pkg.name">
|
||||||
<div class="package-row">
|
<div class="package-row">
|
||||||
|
|
@ -119,15 +132,14 @@
|
||||||
|
|
||||||
<!-- Card actions -->
|
<!-- Card actions -->
|
||||||
<div class="target-actions">
|
<div class="target-actions">
|
||||||
<button class="neu-btn neu-btn--sm" @click="checkTarget(target)"
|
<button class="neu-btn neu-btn--sm" @click="checkTarget(target)" :disabled="target.checking">
|
||||||
:disabled="target.checking">
|
<i class="lnid-refresh-circle-1-clockwise"></i> Vérifier
|
||||||
↻ Vérifier
|
|
||||||
</button>
|
</button>
|
||||||
<button class="neu-btn neu-btn--sm neu-btn--primary"
|
<button class="neu-btn neu-btn--sm neu-btn--primary"
|
||||||
@click="updateTarget(target)"
|
@click="updateTarget(target)"
|
||||||
:disabled="target.updating || jobStatus === 'running' || !target.packages || target.packages.length === 0"
|
:disabled="target.updating || jobStatus === 'running' || !target.packages || target.packages.length === 0"
|
||||||
x-show="target.status !== 'stopped'">
|
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>
|
<span x-show="target.updating"><span class="spinner-sm"></span> En cours…</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue