Version 2.0.0 - update of everything

This commit is contained in:
raphix committed 2026-10-07 14:05:02 +02:00
1 parent e006d48c0a
commit 6d81a8c1c0
57 files changed
+9407 -364

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
<template>
<DefaultSplash v-if="!isAdmin">
<h1 class="denied-title"><Icon icon="fa-solid fa-lock"/> Accès refusé</h1>
<Error>Cette partition est réservée aux administrateurs de Subsonics.</Error>
<Button icon="fa-solid fa-arrow-left" @click="router.push('/servers')">Revenir au concert</Button>
</DefaultSplash>
<div v-else class="admin-container">
<div class="left-side">
<Box padding="closed" class="nav-box">
<SubsonicsLogo class="logo"/>
<p class="admin-label"><Icon icon="fa-solid fa-star"/> Administration</p>
<nav class="admin-nav">
<button v-for="item in sections" :key="item.id"
:class="['nav-item', { active: adminStore.section === item.id }]"
@click="adminStore.setSection(item.id)">
<Icon class="nav-icon" :icon="item.icon"/>
<span>{{ item.label }}</span>
<span v-if="badges[item.id]" :class="['nav-badge', badges[item.id].type]">{{ badges[item.id].text }}</span>
</button>
</nav>
<Button class="back" icon="fa-solid fa-arrow-left" @click="router.push('/servers')">Retour aux serveurs</Button>
</Box>
<Account class="account"/>
</div>
<Box class="main" padding="closed">
<component :is="current.component" :key="current.id"/>
</Box>
<AdminToasts/>
</div>
</template>
<script setup>
/**
* Tableau de bord d'administration : navigation par sections + zone principale.
* Les poussées temps réel ("/ADMIN/*\/UPDATE") sont enregistrées au montage et retirées au démontage,
* car routes.js fait socket.removeAllListeners() à chaque navigation.
*/
import { computed, onMounted, onUnmounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/userStore';
import { useAdminStore } from '@/stores/adminStore';
import Box from '@/components/UI/Box.vue';
import Button from '@/components/UI/Button.vue';
import Error from '@/components/UI/Error.vue';
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
import SubsonicsLogo from '@/components/UI/SubsonicsLogo.vue';
import Account from '@/components/Layout/Account.vue';
import AdminToasts from './AdminToasts.vue';
import AdminOverview from './AdminOverview.vue';
import AdminPlayers from './AdminPlayers.vue';
import AdminServers from './AdminServers.vue';
import AdminUsers from './AdminUsers.vue';
import AdminLogs from './AdminLogs.vue';
import AdminWarp from './AdminWarp.vue';
import AdminYoutube from './AdminYoutube.vue';
import AdminMetrics from './AdminMetrics.vue';
import AdminMaintenance from './AdminMaintenance.vue';
import AdminPlaylists from './AdminPlaylists.vue';
import AdminConfig from './AdminConfig.vue';
const router = useRouter();
const userStore = useUserStore();
const adminStore = useAdminStore();
// Source de vérité : les labels (userInfo est remplacé à chaque /USER/READY)
const isAdmin = computed(() => !!userStore.userInfo?.labels?.includes('ADMIN'));
const sections = [
{ id: 'overview', label: "Vue d'ensemble", icon: "fa-solid fa-gauge-high", component: AdminOverview },
{ id: 'players', label: "Players", icon: "fa-solid fa-music", component: AdminPlayers },
{ id: 'servers', label: "Serveurs", icon: "fa-solid fa-server", component: AdminServers },
{ id: 'users', label: "Comptes", icon: "fa-solid fa-users", component: AdminUsers },
{ id: 'playlists', label: "Playlists", icon: "fa-solid fa-list", component: AdminPlaylists },
{ id: 'logs', label: "Logs", icon: "fa-solid fa-scroll", component: AdminLogs },
{ id: 'warp', label: "WARP", icon: "fa-solid fa-cloud", component: AdminWarp },
{ id: 'youtube', label: "YouTube", icon: "fa-brands fa-youtube", component: AdminYoutube },
{ id: 'metrics', label: "Métriques", icon: "fa-solid fa-chart-simple", component: AdminMetrics },
{ id: 'config', label: "Configuration", icon: "fa-solid fa-sliders", component: AdminConfig },
{ id: 'maintenance', label: "Maintenance", icon: "fa-solid fa-screwdriver-wrench", component: AdminMaintenance }
];
const current = computed(() => sections.find(s => s.id === adminStore.section) || sections[0]);
// Petites pastilles d'état dans la navigation
const badges = computed(() => {
const result = {};
const playing = adminStore.players.filter(p => p.status === 'playing').length;
if (adminStore.players.length) result.players = { text: playing + "/" + adminStore.players.length, type: playing ? 'good' : 'neutral' };
const warp = adminStore.warp || adminStore.overview?.warp;
if (warp?.configured) result.warp = warp.healthy ? { text: "OK", type: 'good' } : { text: "KO", type: 'bad' };
// YouTube : statut complet si la section a été chargée, sinon le résumé de la vue d'ensemble
const yt = adminStore.youtube;
const ytCookies = yt?.cookies || adminStore.overview?.ytdlp?.cookies;
const failureRate = yt ? yt.diagnostics?.recentFailureRate : adminStore.overview?.ytdlp?.recentFailureRate;
const recentCount = yt ? yt.diagnostics?.recentCount : adminStore.overview?.ytdlp?.recentCount;
// La majorité des dernières lectures échoue (au moins 4 tentatives) : problème, même sans compte
const failing = Number(recentCount) >= 4 && Number(failureRate) >= 0.5;
if (failing || (ytCookies?.present && !ytCookies.valid)) result.youtube = { text: "!", type: 'bad' };
else if (ytCookies?.valid) result.youtube = { text: "OK", type: 'good' };
if (adminStore.overview?.logs?.interrupted) result.logs = { text: "!", type: 'bad' };
const online = adminStore.presence?.counts?.online ?? adminStore.overview?.web?.usersOnline;
if (online) result.users = { text: online, type: 'neutral' };
// Paramètres modifiés en attente d'un redémarrage (seulement si la section a été chargée)
if (adminStore.config?.restartRequired) result.config = { text: "!", type: 'warn' };
return result;
});
function start() {
adminStore.attach();
adminStore.fetchOverview();
adminStore.fetchPlayers();
}
onMounted(() => {
document.title = "Administration - Subsonics";
if (isAdmin.value) start();
});
onUnmounted(() => {
adminStore.detach();
});
// Droit retiré ou accordé en direct (/USER/READY recharge userInfo)
watch(isAdmin, (value) => {
if (value) start();
else adminStore.detach();
});
</script>
<style scoped>
.admin-container {
padding: 15px;
width: 100vw;
height: 100vh;
box-sizing: border-box;
display: grid;
grid-template-columns: minmax(240px, 0.7fr) 3fr;
gap: 15px;
}
.left-side {
display: flex;
flex-direction: column;
gap: 15px;
height: 100%;
min-height: 0;
}
.nav-box {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
min-height: 0;
overflow-y: auto;
}
.logo :deep(h1) {
font-size: 20px;
}
.logo :deep(.img) {
width: 70px;
height: 70px;
}
.admin-label {
margin: 0;
text-align: center;
color: var(--admin-color);
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.admin-nav {
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px;
border: none;
border-radius: 10px;
background-color: transparent;
color: var(--text);
font-size: 0.95em;
text-align: left;
cursor: pointer;
transition: all 0.2s ease;
}
.nav-item:hover {
background-color: var(--tertiary);
}
.nav-item.active {
background-color: var(--main);
color: #FFFFFF;
}
.nav-icon {
width: 18px;
}
.nav-badge {
margin-left: auto;
font-size: 0.7em;
padding: 2px 7px;
border-radius: 20px;
background-color: var(--quaternary);
color: #FFFFFF;
font-family: monospace;
}
.nav-badge.good {
background-color: color-mix(in srgb, var(--mod-color) 70%, black);
}
.nav-badge.warn {
background-color: var(--text-warning);
}
.nav-badge.bad {
background-color: var(--text-error);
}
.back {
width: 100%;
}
.main {
min-width: 0;
min-height: 0;
overflow-y: auto;
padding: 15px !important;
}
.denied-title {
display: flex;
align-items: center;
gap: 10px;
}
@media screen and (max-width: 768px), screen and (max-height: 607px) {
.admin-container {
display: flex;
flex-direction: column;
height: auto;
min-height: 100vh;
}
.left-side {
height: auto;
}
.nav-box {
overflow: visible;
}
.admin-nav {
flex-direction: row;
overflow-x: auto;
padding-bottom: 5px;
}
.nav-item {
width: auto;
white-space: nowrap;
flex-shrink: 0;
border-radius: 20px;
background-color: var(--tertiary);
}
.account {
display: none;
}
.main {
overflow: visible;
}
}
</style>