290 lines
9.0 KiB
Vue
290 lines
9.0 KiB
Vue
<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>
|