feat: Add Docker image update system (TrueNAS Scale inspired)
- Implement UpdateService for image version checking and atomic updates - Add DockerComposeManager for centralized docker-compose management - Create 12 docker-compose references in /home/innotex/Docker - Add 13 new API endpoints (6 for images, 7 for compose management) - Add comprehensive documentation and examples
This commit is contained in:
@@ -1,63 +1,86 @@
|
||||
<template>
|
||||
<div id="app" class="min-h-screen bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-2xl font-bold text-blue-500">InnotexBoard</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span v-if="authStore.isAuthenticated" class="text-gray-300">
|
||||
{{ authStore.username }}
|
||||
<div id="app" class="min-h-screen bg-gray-50">
|
||||
<!-- Navigation -->
|
||||
<nav class="sticky top-0 z-50 bg-white border-b border-gray-300 shadow-md">
|
||||
<div class="max-w-full mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div class="flex justify-between items-center h-24">
|
||||
<!-- Logo -->
|
||||
<router-link to="/" class="flex items-center space-x-4 group">
|
||||
<img
|
||||
src="./assets/images/logo.png"
|
||||
alt="InnotexBoard Logo"
|
||||
class="w-32 h-20 group-hover:scale-110 transition-transform rounded-lg shadow-lg object-contain bg-white rounded-lg p-1"
|
||||
style="background: transparent;"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-3xl font-bold bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent group-hover:from-blue-500 group-hover:to-cyan-400 transition-all">
|
||||
InnotexBoard
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 mt-1">Votre Homelab Personnel</span>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<!-- User & Actions -->
|
||||
<div class="flex items-center space-x-6">
|
||||
<span v-if="authStore.isAuthenticated" class="text-sm text-gray-700 px-3 py-1 bg-blue-100 rounded-full border border-blue-200">
|
||||
👤 {{ authStore.username }}
|
||||
</span>
|
||||
<button
|
||||
v-if="authStore.isAuthenticated"
|
||||
@click="handleLogout"
|
||||
class="btn btn-danger btn-small"
|
||||
v-if="!authStore.isAuthenticated"
|
||||
@click="goToLogin"
|
||||
class="px-4 py-2 text-sm font-semibold text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-500 hover:to-blue-600 rounded-lg transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/30"
|
||||
>
|
||||
Déconnexion
|
||||
🔐 Connexion
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="handleLogout"
|
||||
class="px-4 py-2 text-sm font-semibold text-red-600 bg-red-100 hover:bg-red-200 border border-red-300 rounded-lg transition-all duration-300"
|
||||
>
|
||||
🚪 Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Layout -->
|
||||
<div v-if="authStore.isAuthenticated" class="flex">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-gray-800 border-r border-gray-700 min-h-screen">
|
||||
<nav class="p-4 space-y-2">
|
||||
<aside class="w-64 bg-white border-r border-gray-200 min-h-screen sticky top-20">
|
||||
<nav class="p-6 space-y-2">
|
||||
<router-link
|
||||
to="/dashboard"
|
||||
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
|
||||
:class="{ 'bg-gray-700': $route.path === '/dashboard' }"
|
||||
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
|
||||
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/dashboard' }"
|
||||
>
|
||||
📊 Dashboard
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/containers"
|
||||
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
|
||||
:class="{ 'bg-gray-700': $route.path === '/containers' }"
|
||||
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
|
||||
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/containers' }"
|
||||
>
|
||||
🐳 Conteneurs Docker
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/disks"
|
||||
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
|
||||
:class="{ 'bg-gray-700': $route.path === '/disks' }"
|
||||
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
|
||||
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/disks' }"
|
||||
>
|
||||
💾 Disques et Partitions
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/packages"
|
||||
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
|
||||
:class="{ 'bg-gray-700': $route.path === '/packages' }"
|
||||
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
|
||||
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/packages' }"
|
||||
>
|
||||
📦 App Store
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/shortcuts"
|
||||
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
|
||||
:class="{ 'bg-gray-700': $route.path === '/shortcuts' }"
|
||||
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
|
||||
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/shortcuts' }"
|
||||
>
|
||||
🔗 Raccourcis Services
|
||||
</router-link>
|
||||
@@ -65,7 +88,7 @@
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1">
|
||||
<main class="flex-1 overflow-auto">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
@@ -80,6 +103,10 @@ import { useRouter } from 'vue-router'
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const goToLogin = () => {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
|
||||
18
frontend/src/assets/images/innotex-logo.svg
Normal file
18
frontend/src/assets/images/innotex-logo.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background -->
|
||||
<circle cx="100" cy="100" r="100" fill="#1a1a2e"/>
|
||||
|
||||
<!-- Outer ring -->
|
||||
<circle cx="100" cy="100" r="95" fill="none" stroke="#00d9ff" stroke-width="2"/>
|
||||
<circle cx="100" cy="100" r="90" fill="none" stroke="#0066ff" stroke-width="1" opacity="0.5"/>
|
||||
|
||||
<!-- Inner circle -->
|
||||
<circle cx="100" cy="100" r="75" fill="none" stroke="#ffffff" stroke-width="3"/>
|
||||
|
||||
<!-- Text/Letter I -->
|
||||
<text x="100" y="115" font-size="80" font-weight="bold" fill="#ffffff" text-anchor="middle" font-family="Arial, sans-serif">I</text>
|
||||
|
||||
<!-- Accent dots -->
|
||||
<circle cx="55" cy="40" r="4" fill="#00d9ff"/>
|
||||
<circle cx="145" cy="160" r="4" fill="#0066ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 749 B |
BIN
frontend/src/assets/images/logo.png
Normal file
BIN
frontend/src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
394
frontend/src/assets/innotex-theme.css
Normal file
394
frontend/src/assets/innotex-theme.css
Normal file
@@ -0,0 +1,394 @@
|
||||
/* ============================================
|
||||
CHARTE GRAPHIQUE INNOTEX
|
||||
============================================ */
|
||||
|
||||
:root {
|
||||
/* Couleurs primaires Innotex - Blanc avec accents bleu/cyan */
|
||||
--color-innotex-white: #ffffff;
|
||||
--color-innotex-light-gray: #f8f9fa;
|
||||
--color-innotex-gray-light: #e9ecef;
|
||||
--color-innotex-gray-medium: #dee2e6;
|
||||
--color-innotex-gray-dark: #495057;
|
||||
|
||||
/* Accents de couleur */
|
||||
--color-accent-primary: #0066ff; /* Bleu électrique */
|
||||
--color-accent-secondary: #00d9ff; /* Cyan vif */
|
||||
--color-accent-tertiary: #0052cc; /* Bleu foncé */
|
||||
|
||||
/* Palettes fonctionnelles */
|
||||
--color-success: #00c896;
|
||||
--color-warning: #ffa500;
|
||||
--color-error: #ff4444;
|
||||
--color-info: #0066ff;
|
||||
|
||||
/* Backgrounds */
|
||||
--bg-primary: #f5f7fb;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #f0f3f7;
|
||||
--bg-surface: #e9ecef;
|
||||
|
||||
/* Text */
|
||||
--text-primary: #1a1a2e;
|
||||
--text-secondary: #495057;
|
||||
--text-muted: #868e96;
|
||||
|
||||
/* Borders */
|
||||
--border-color: #dee2e6;
|
||||
--border-color-light: #e9ecef;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||
--shadow-xl: 0 20px 50px rgba(0, 0, 0, 0.2);
|
||||
|
||||
/* Gradients */
|
||||
--gradient-accent: linear-gradient(135deg, var(--color-accent-primary) 0%, var(--color-accent-secondary) 100%);
|
||||
--gradient-light: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
|
||||
--gradient-neon: linear-gradient(135deg, var(--color-accent-primary) 0%, var(--color-accent-tertiary) 100%);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease-in-out;
|
||||
--transition-normal: 300ms ease-in-out;
|
||||
--transition-slow: 500ms ease-in-out;
|
||||
|
||||
/* Border radius */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESET & BASE STYLES
|
||||
============================================ */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SCROLLBAR STYLING
|
||||
============================================ */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-accent-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-accent-secondary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TYPOGRAPHY
|
||||
============================================ */
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent-primary);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-accent-secondary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BUTTONS
|
||||
============================================ */
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transition: left var(--transition-fast);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gradient-accent);
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0, 102, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color-light);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: rgba(255, 68, 68, 0.1);
|
||||
color: #ff4444;
|
||||
border: 1px solid rgba(255, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: rgba(255, 68, 68, 0.2);
|
||||
border-color: #ff4444;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
INPUTS & FORMS
|
||||
============================================ */
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
transition: all var(--transition-fast);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
|
||||
background: rgba(0, 102, 255, 0.05);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CARDS & CONTAINERS
|
||||
============================================ */
|
||||
|
||||
.card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--border-color-light);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-elevated {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.card-accent {
|
||||
border-left: 4px solid var(--color-accent-primary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BADGES
|
||||
============================================ */
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: rgba(0, 200, 150, 0.15);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: rgba(255, 165, 0, 0.15);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background: rgba(255, 68, 68, 0.15);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: rgba(0, 102, 255, 0.15);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITIES
|
||||
============================================ */
|
||||
|
||||
.glow {
|
||||
animation: glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% {
|
||||
text-shadow: 0 0 5px rgba(0, 102, 255, 0.5);
|
||||
}
|
||||
50% {
|
||||
text-shadow: 0 0 20px rgba(0, 102, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-in {
|
||||
animation: slideIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.2rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/innotex-theme.css'
|
||||
import './assets/styles.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -1,77 +1,35 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
|
||||
<!-- Header -->
|
||||
<div class="sticky top-0 z-40 backdrop-blur-xl bg-gray-900/80 border-b border-gray-700/50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-white mb-2">
|
||||
<span class="bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
|
||||
InnotexBoard
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-gray-400">Votre Homelab Personnel</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span v-if="authStore.isAuthenticated" class="text-sm text-gray-300 bg-gray-800 px-3 py-1 rounded-full">
|
||||
{{ authStore.username }}
|
||||
</span>
|
||||
<button
|
||||
v-if="authStore.isAuthenticated"
|
||||
@click="goToAdmin"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition text-sm font-medium"
|
||||
>
|
||||
⚙️ Gérer
|
||||
</button>
|
||||
<button
|
||||
v-if="!authStore.isAuthenticated"
|
||||
@click="goToLogin"
|
||||
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition text-sm font-medium"
|
||||
>
|
||||
Connexion
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="handleLogout"
|
||||
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition text-sm font-medium"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- Main Content -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<main class="max-w-full mx-auto px-4 sm:px-6 lg:px-8 py-12 bg-gray-50 min-h-screen">
|
||||
<!-- Quick Stats (if authenticated) -->
|
||||
<div v-if="authStore.isAuthenticated" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-12">
|
||||
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
|
||||
<div class="text-gray-400 text-sm mb-1">Total Services</div>
|
||||
<div class="text-3xl font-bold text-white">{{ shortcuts.length }}</div>
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 hover:border-blue-400 hover:shadow-md transition">
|
||||
<div class="text-gray-600 text-sm mb-1">📊 Total Services</div>
|
||||
<div class="text-3xl font-bold text-gray-900">{{ shortcuts.length }}</div>
|
||||
</div>
|
||||
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
|
||||
<div class="text-gray-400 text-sm mb-1">Catégories</div>
|
||||
<div class="text-3xl font-bold text-white">{{ categories.size }}</div>
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 hover:border-cyan-400 hover:shadow-md transition">
|
||||
<div class="text-gray-600 text-sm mb-1">🏷️ Catégories</div>
|
||||
<div class="text-3xl font-bold text-gray-900">{{ categories.size }}</div>
|
||||
</div>
|
||||
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
|
||||
<div class="text-gray-400 text-sm mb-1">État</div>
|
||||
<div class="text-3xl font-bold text-green-400">✓ Actif</div>
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 hover:border-green-400 hover:shadow-md transition">
|
||||
<div class="text-gray-600 text-sm mb-1">✨ État</div>
|
||||
<div class="text-3xl font-bold text-green-600">Actif</div>
|
||||
</div>
|
||||
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
|
||||
<div class="text-gray-400 text-sm mb-1">Dernier Update</div>
|
||||
<div class="text-lg font-bold text-white">{{ lastUpdate }}</div>
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 hover:border-purple-400 hover:shadow-md transition">
|
||||
<div class="text-gray-600 text-sm mb-1">⏱️ Dernier Update</div>
|
||||
<div class="text-lg font-bold text-gray-900">{{ lastUpdate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shortcuts Grid by Category -->
|
||||
<div v-if="shortcuts.length > 0">
|
||||
<div v-for="(categoryShortcuts, category) in groupedShortcuts" :key="category" class="mb-12">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="h-1 w-8 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mr-3"></div>
|
||||
<h2 class="text-2xl font-bold text-white capitalize">{{ getCategoryTitle(category) }}</h2>
|
||||
<span class="ml-3 px-3 py-1 bg-gray-700/50 text-gray-300 text-sm rounded-full">
|
||||
{{ categoryShortcuts.length }}
|
||||
<div v-for="(categoryShortcuts, category) in groupedShortcuts" :key="category" class="mb-16">
|
||||
<div class="flex items-center mb-8">
|
||||
<div class="h-1 w-2 bg-gradient-to-r from-blue-600 to-cyan-500 rounded-full mr-4"></div>
|
||||
<h2 class="text-3xl font-bold text-gray-900 capitalize">{{ getCategoryTitle(category) }}</h2>
|
||||
<span class="ml-4 px-3 py-1 bg-blue-100 text-blue-700 text-sm rounded-full border border-blue-300">
|
||||
{{ categoryShortcuts.length }} service<span v-if="categoryShortcuts.length > 1">s</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -82,34 +40,34 @@
|
||||
:href="shortcut.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group relative bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700/50 hover:border-gray-600 rounded-xl p-6 transition-all duration-300 hover:shadow-2xl hover:shadow-blue-500/20 overflow-hidden"
|
||||
:style="{ borderLeftColor: shortcut.color || '#3b82f6', borderLeftWidth: '4px' }"
|
||||
class="group relative block bg-white border-2 border-gray-200 hover:border-blue-500 rounded-xl p-6 transition-all duration-300 hover:shadow-xl overflow-hidden cursor-pointer"
|
||||
:style="{ borderTopColor: shortcut.color || '#0066ff', borderTopWidth: '3px' }"
|
||||
>
|
||||
<!-- Background gradient on hover -->
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-cyan-50/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10">
|
||||
<!-- Icon -->
|
||||
<div class="text-4xl mb-4 inline-block p-3 bg-gray-700/50 group-hover:bg-gray-600/50 rounded-lg transition">
|
||||
<div class="text-6xl mb-4 inline-block p-4 bg-blue-50 group-hover:bg-blue-100 rounded-xl transition transform group-hover:scale-110 border border-blue-200 group-hover:border-blue-400">
|
||||
{{ shortcut.icon }}
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h3 class="font-bold text-white text-lg mb-2 group-hover:text-blue-400 transition truncate">
|
||||
<h3 class="font-bold text-gray-900 text-lg mb-2 group-hover:text-blue-600 transition truncate">
|
||||
{{ shortcut.name }}
|
||||
</h3>
|
||||
|
||||
<!-- Description -->
|
||||
<p v-if="shortcut.description" class="text-gray-400 text-sm mb-4 line-clamp-2">
|
||||
<p v-if="shortcut.description" class="text-gray-600 text-sm mb-4 line-clamp-2 group-hover:text-gray-700 transition">
|
||||
{{ shortcut.description }}
|
||||
</p>
|
||||
|
||||
<!-- URL -->
|
||||
<div class="flex items-center text-gray-500 text-xs group-hover:text-gray-300 transition">
|
||||
<div class="flex items-center text-gray-500 text-xs group-hover:text-blue-600 transition">
|
||||
<span class="truncate">{{ getHostname(shortcut.url) }}</span>
|
||||
<svg class="w-4 h-4 ml-2 opacity-0 group-hover:opacity-100 transition transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-13.5-2.5H21m0 0l-3-3m3 3l-3 3" />
|
||||
<svg class="w-4 h-4 ml-2 opacity-50 group-hover:opacity-100 transition transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4m-4-4l4-4m0 0l4 4m-4-4v12" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,10 +76,10 @@
|
||||
<button
|
||||
v-if="authStore.isAuthenticated"
|
||||
@click.prevent.stop="deleteShortcut(shortcut.id)"
|
||||
class="absolute top-2 right-2 p-2 bg-red-500/0 hover:bg-red-500 text-red-400 hover:text-white rounded-lg transition opacity-0 group-hover:opacity-100 duration-300"
|
||||
title="Supprimer"
|
||||
class="absolute top-3 right-3 p-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition opacity-0 group-hover:opacity-100 duration-300 hover:scale-110 transform"
|
||||
title="Supprimer ce raccourci"
|
||||
>
|
||||
✕
|
||||
🗑️
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
@@ -129,31 +87,36 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="flex flex-col items-center justify-center py-20">
|
||||
<div class="text-6xl mb-4">🔗</div>
|
||||
<h3 class="text-2xl font-bold text-white mb-2">Aucun Service Configuré</h3>
|
||||
<p class="text-gray-400 mb-4">
|
||||
<div v-else class="flex flex-col items-center justify-center py-32">
|
||||
<div class="text-8xl mb-6">🔗</div>
|
||||
<h3 class="text-3xl font-bold text-gray-900 mb-3">Aucun Service Configuré</h3>
|
||||
<p class="text-gray-600 mb-8 text-center max-w-md">
|
||||
<span v-if="authStore.isAuthenticated">
|
||||
Ajoutez vos premiers services pour les afficher ici
|
||||
Ajoutez vos premiers services pour commencer à centraliser vos accès
|
||||
</span>
|
||||
<span v-else>
|
||||
Connectez-vous pour gérer vos services
|
||||
Connectez-vous pour gérer et configurer vos services
|
||||
</span>
|
||||
</p>
|
||||
<button
|
||||
v-if="authStore.isAuthenticated"
|
||||
@click="goToAdmin"
|
||||
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition font-medium"
|
||||
class="px-8 py-3 bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 text-white rounded-lg transition font-medium shadow-lg shadow-blue-600/30 hover:shadow-xl hover:shadow-blue-600/50"
|
||||
>
|
||||
Ajouter un Service
|
||||
✨ Ajouter un Service
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-gray-800 mt-20 py-8">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-gray-500 text-sm">
|
||||
<p>InnotexBoard v1.0 • Votre centre de contrôle homelab personnel</p>
|
||||
<footer class="border-t border-gray-200 mt-20 py-8 bg-white shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<p class="text-gray-700 text-sm font-semibold">
|
||||
InnotexBoard v1.0
|
||||
</p>
|
||||
<p class="text-gray-600 text-xs">
|
||||
Technologie : FastAPI • Vue.js 3 • TailwindCSS • WebSocket
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -1,55 +1,107 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="card border-blue-600/50">
|
||||
<h2 class="text-3xl font-bold text-center mb-8 text-blue-400">InnotexBoard</h2>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-gray-100 relative overflow-hidden">
|
||||
<!-- Animated background elements -->
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute top-20 left-10 w-96 h-96 bg-blue-200/20 rounded-full blur-3xl"></div>
|
||||
<div class="absolute bottom-20 right-10 w-96 h-96 bg-cyan-200/20 rounded-full blur-3xl"></div>
|
||||
<div class="absolute top-1/2 left-1/2 w-96 h-96 bg-blue-100/10 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2"></div>
|
||||
</div>
|
||||
|
||||
<div class="w-full max-w-md px-4 relative z-10">
|
||||
<!-- Card Container -->
|
||||
<div class="bg-white border border-gray-200 rounded-2xl shadow-lg p-8 backdrop-blur-sm">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<img
|
||||
src="../assets/images/logo.png"
|
||||
alt="InnotexBoard Logo"
|
||||
class="w-32 h-20 mx-auto mb-4 object-contain"
|
||||
style="background: transparent;"
|
||||
/>
|
||||
<h1 class="text-3xl font-bold mb-2">
|
||||
<span class="bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
|
||||
InnotexBoard
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-gray-600 text-sm">Accédez à votre centre de contrôle</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form @submit.prevent="handleLogin" class="space-y-6">
|
||||
<!-- Username Input -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Nom d'utilisateur
|
||||
<label for="username" class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
👤 Identifiant
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="credentials.username"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
|
||||
class="w-full px-4 py-3 bg-gray-50 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition"
|
||||
placeholder="votre_utilisateur"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Password Input -->
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Mot de passe
|
||||
<label for="password" class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
🔐 Mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="credentials.password"
|
||||
type="password"
|
||||
required
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
|
||||
class="w-full px-4 py-3 bg-gray-50 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400 text-sm">
|
||||
{{ error }}
|
||||
<!-- Error Message -->
|
||||
<div v-if="error" class="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-start gap-3">
|
||||
<span class="text-lg">⚠️</span>
|
||||
<div>{{ error }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="w-full btn btn-primary py-2 font-medium"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 disabled:from-gray-400 disabled:to-gray-400 text-white font-semibold rounded-lg transition-all duration-300 transform hover:scale-105 disabled:scale-100 flex items-center justify-center gap-2 shadow-lg shadow-blue-600/30 hover:shadow-xl hover:shadow-blue-600/50"
|
||||
>
|
||||
<span v-if="loading">Connexion en cours...</span>
|
||||
<span v-else>Se connecter</span>
|
||||
<span v-if="loading">⏳ Connexion...</span>
|
||||
<span v-else>🚀 Se connecter</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="text-center text-gray-400 text-sm mt-6">
|
||||
⚙️ Authentification via PAM du système Debian
|
||||
</p>
|
||||
<!-- Divider -->
|
||||
<div class="mt-6 relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-3 bg-white text-gray-500">ou</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Footer -->
|
||||
<div class="mt-6 pt-6 space-y-2 text-center border-t border-gray-200">
|
||||
<p class="text-gray-600 text-xs flex items-center justify-center gap-2">
|
||||
<span>⚙️</span>
|
||||
<span>Authentification via PAM du système</span>
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs">
|
||||
Vos données sont sécurisées et chiffrées
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Text -->
|
||||
<div class="mt-6 text-center text-gray-500 text-xs">
|
||||
<p>Besoin d'aide ? Contactez votre administrateur système</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,7 +131,7 @@ const handleLogin = async () => {
|
||||
console.log('Login success result:', success)
|
||||
|
||||
if (success) {
|
||||
console.log('Navigating to dashboard')
|
||||
console.log('Navigating to homelab')
|
||||
await router.push('/')
|
||||
} else {
|
||||
error.value = 'Identifiants incorrects. Veuillez réessayer.'
|
||||
|
||||
Reference in New Issue
Block a user