Version 2.0.0 - update of everything
This commit is contained in:
57 files changed
+9407
-364
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user