1
0

ajout de stats sur server.js + amélioration diverses

This commit is contained in:
unfr 2025-08-11 13:43:53 +02:00
parent e87da06111
commit ff974bd43c
3 changed files with 357 additions and 195 deletions

View File

@ -1,5 +1,6 @@
const express = require('express'); const express = require('express');
const session = require('express-session'); const session = require('express-session');
const FileStore = require('session-file-store')(session);
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const AnsiToHtml = require('ansi-to-html'); const AnsiToHtml = require('ansi-to-html');
@ -19,9 +20,17 @@ const port = config.port;
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(session({ app.use(session({
store: new FileStore({
path: './sessions', // dossier où stocker les fichiers
ttl: 24 * 60 * 60, // durée de vie en secondes (ici 1 jour)
retries: 0
}),
secret: config.sessionSecret, secret: config.sessionSecret,
resave: false, resave: false,
saveUninitialized: false saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000 // 1 jour en ms
}
})); }));
app.use(express.static('public')); app.use(express.static('public'));
@ -98,6 +107,16 @@ autopostRouter.get('/', async (req, res) => {
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
try { try {
const [stats] = await db.query(`
SELECT
COUNT(*) AS total,
SUM(status = 0) AS attente,
SUM(status = 1) AS termine,
SUM(status = 2) AS erreur,
SUM(status = 3) AS deja,
SUM(status = 4) AS encours
FROM \`${config.DB_TABLE}\`
`);
// Récupérer le nombre total d'enregistrements // Récupérer le nombre total d'enregistrements
const [countResult] = await db.query(`SELECT COUNT(*) as total FROM \`${config.DB_TABLE}\``); const [countResult] = await db.query(`SELECT COUNT(*) as total FROM \`${config.DB_TABLE}\``);
const totalRecords = countResult[0].total; const totalRecords = countResult[0].total;
@ -120,13 +139,105 @@ autopostRouter.get('/', async (req, res) => {
</head> </head>
<body class="bg-slate-900 text-white font-sans p-4"> <body class="bg-slate-900 text-white font-sans p-4">
<div class="w-full px-4"> <div class="w-full px-4">
<h1 class="text-3xl font-bold mb-4">Suivi Autopost</h1> <div class="mb-6">
<p class="mb-4"> <div class="flex items-center justify-between">
<a href="/autopost/logout" class="text-blue-400 hover:underline">Déconnexion</a> <h1 class="text-2xl md:text-3xl font-extrabold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-cyan-300">
</p> Suivi Autopost
</h1>
<a href="/autopost/logout"
class="inline-flex items-center gap-2 px-3 py-2 rounded-xl bg-red-600/90 hover:bg-red-600 text-white shadow-lg shadow-red-900/20 transition">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H6a2 2 0 01-2-2V7a2 2 0 012-2h5a2 2 0 012 2v1"/>
</svg>
Déconnexion
</a>
</div>
<p class="mt-1 text-sm text-gray-400">Vue densemble des traitements en cours et de létat des envois.</p>
</div>
<!-- Stat cards -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<!-- En attente -->
<div class="group rounded-2xl p-3 md:p-5 bg-gradient-to-br from-cyan-500 to-cyan-400 text-black shadow-xl ring-1 ring-white/10 transition transform hover:-translate-y-0.5 hover:shadow-2xl cursor-pointer filter-card" data-status="0">
<div class="flex items-center gap-3">
<div class="rounded-xl bg-black/10 p-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6v6l4 2"/>
</svg>
</div>
<div class="text-sm/5 font-medium">En attente</div>
</div>
<div class="mt-3 text-3xl font-extrabold tabular-nums">${stats[0].attente}</div>
</div>
<!-- Terminé -->
<div class="group rounded-2xl p-4 md:p-5 bg-gradient-to-br from-green-300 to-emerald-300 text-black shadow-xl ring-1 ring-white/10 transition transform hover:-translate-y-0.5 hover:shadow-2xl cursor-pointer filter-card" data-status="1">
<div class="flex items-center gap-3">
<div class="rounded-xl bg-black/10 p-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7"/>
</svg>
</div>
<div class="text-sm/5 font-medium">Terminé</div>
</div>
<div class="mt-3 text-3xl font-extrabold tabular-nums">${stats[0].termine}</div>
</div>
<!-- Erreur -->
<div class="group rounded-2xl p-4 md:p-5 bg-gradient-to-br from-rose-300 to-red-300 text-black shadow-xl ring-1 ring-white/10 transition transform hover:-translate-y-0.5 hover:shadow-2xl cursor-pointer filter-card" data-status="2">
<div class="flex items-center gap-3">
<div class="rounded-xl bg-black/10 p-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v3m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
</svg>
</div>
<div class="text-sm/5 font-medium">Erreur</div>
</div>
<div class="mt-3 text-3xl font-extrabold tabular-nums">${stats[0].erreur}</div>
</div>
<!-- Déjà dispo -->
<div class="group rounded-2xl p-4 md:p-5 bg-gradient-to-br from-pink-300 to-fuchsia-300 text-black shadow-xl ring-1 ring-white/10 transition transform hover:-translate-y-0.5 hover:shadow-2xl cursor-pointer filter-card" data-status="3">
<div class="flex items-center gap-3">
<div class="rounded-xl bg-black/10 p-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7h8M8 11h8M8 15h6"/>
</svg>
</div>
<div class="text-sm/5 font-medium">Déjà dispo</div>
</div>
<div class="mt-3 text-3xl font-extrabold tabular-nums">${stats[0].deja}</div>
</div>
</div>
<!-- Bouton réinitialiser -->
<div class="mb-4 text-center">
<button id="showAll"
class="hidden px-5 py-2 rounded-xl bg-gradient-to-br from-gray-600 to-gray-800 text-white font-semibold shadow-lg hover:from-gray-500 hover:to-gray-700 transition transform hover:-translate-y-0.5">
Tout afficher
</button>
</div>
<!-- Search -->
<div class="mb-4">
<label for="searchInput" class="sr-only">Rechercher</label>
<div class="relative">
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg class="w-5 h-5 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-4.35-4.35M10 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16z"/>
</svg>
</span>
<input id="searchInput" type="text" placeholder="Rechercher…"
class="w-full pl-10 pr-3 py-2 rounded-xl bg-gray-800/80 border border-gray-700 focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition placeholder:text-gray-400" />
</div>
</div>
<input type="text" id="searchInput" placeholder="Rechercher..."
class="p-2 mb-4 border border-gray-300 rounded w-full"/>
<nav aria-label="Page navigation" class="mb-4"> <nav aria-label="Page navigation" class="mb-4">
<ul class="inline-flex items-center -space-x-px"> <ul class="inline-flex items-center -space-x-px">
<li> <li>
@ -147,16 +258,16 @@ autopostRouter.get('/', async (req, res) => {
</ul> </ul>
</nav> </nav>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full bg-gray-800"> <table class="min-w-full border border-gray-700 shadow-lg rounded-lg overflow-hidden">
<thead> <thead class="bg-gray-900 text-gray-300 uppercase text-sm">
<tr> <tr>
<th class="px-4 py-2 border border-gray-700 whitespace-nowrap">Name</th> <th class="px-4 py-2">Name</th>
<th class="px-4 py-2 border border-gray-700 whitespace-nowrap">Status</th> <th class="px-4 py-2">Status</th>
<th class="px-4 py-2 border border-gray-700">ID</th> <th class="px-4 py-2">ID</th>
<th class="px-4 py-2 border border-gray-700 whitespace-nowrap">Actions</th> <th class="px-4 py-2">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="divide-y divide-gray-700">
`; `;
rows.forEach(row => { rows.forEach(row => {
@ -258,168 +369,13 @@ autopostRouter.get('/', async (req, res) => {
</div> </div>
<script> <script>
function updateTable(rows) { function updateTable(rows) {
let tbody = $("table tbody"); var tbody = $("table tbody");
tbody.empty(); tbody.empty();
rows.forEach(function(row) { rows.forEach(function(row) {
let statusText = ''; var statusText = '';
let statusClass = ''; var statusClass = '';
switch (parseInt(row.status)) { switch (parseInt(row.status)) {
case 0:
statusText = 'EN ATTENTE';
statusClass = 'bg-cyan-500 text-black font-bold';
break;
case 1:
statusText = 'ENVOI TERMINÉ';
statusClass = 'bg-green-300 text-black font-bold';
break;
case 2:
statusText = 'ERREUR';
statusClass = 'bg-red-300 text-black font-bold';
break;
case 3:
statusText = 'DEJA DISPONIBLE';
statusClass = 'bg-pink-300 text-black font-bold';
break;
case 4:
statusText = 'EN COURS';
statusClass = 'bg-yellow-300 text-black font-bold';
break;
default:
statusText = 'INCONNU';
}
let logLink = parseInt(row.status) === 1 || parseInt(row.status) === 2
? ' | <a href="#" class="log-link text-blue-400 hover:underline" data-filename="'+row.nom+'">Log</a>'
: '';
let mediainfoLink = parseInt(row.status) === 0 || parseInt(row.status) === 1 || parseInt(row.status) === 2
? ' | <a href="#" class="mediainfo-link text-blue-400 hover:underline" data-filename="'+row.nom+'">Mediainfo</a>'
: '';
let dlLink = parseInt(row.status) === 1
? ' | <a href="/autopost/dl?name='+encodeURIComponent(row.nom)+'" class="dl-link text-blue-400 hover:underline">DL</a>'
: '';
let tr = \`
<tr id="row-\${row.id}" class="odd:bg-gray-800 even:bg-gray-700">
<td class="px-4 py-2 border border-gray-700 whitespace-nowrap">\${row.nom}</td>
<td class="px-4 py-2 border border-gray-700 status-text whitespace-nowrap \${statusClass}">\${statusText}</td>
<td class="px-4 py-2 border border-gray-700">\${row.id}</td>
<td class="px-4 py-2 border border-gray-700">
<a href="#" class="edit-link text-blue-400 hover:underline" data-id="\${row.id}" data-status="\${row.status}">Editer</a> |
<a href="#" class="delete-link text-blue-400 hover:underline" data-id="\${row.id}">Supprimer</a>\${logLink}\${mediainfoLink}\${dlLink}
</td>
</tr>\`;
tbody.append(tr);
});
}
$(document).ready(function(){
$("#searchInput").on("keyup", function() {
let q = $(this).val();
$.ajax({
url: '/autopost/search',
type: 'GET',
data: { q: q },
dataType: 'json',
success: function(data) {
updateTable(data);
},
error: function() {
alert("Erreur lors de la recherche.");
}
});
});
$(document).on("click", ".edit-link", function(e) {
e.preventDefault();
let releaseId = $(this).data('id');
let currentStatus = $(this).data('status');
$('#releaseId').val(releaseId);
$('#modalReleaseId').text(releaseId);
$('#statusSelect').val(currentStatus);
$('#editModal').removeClass('hidden').fadeIn();
});
$(document).on("click", ".delete-link", function(e) {
e.preventDefault();
if (confirm("Êtes-vous sûr de vouloir supprimer cet enregistrement ?")) {
let releaseId = $(this).data('id');
$.ajax({
url: '/autopost/delete/' + releaseId,
type: 'POST',
success: function(data) {
$('#row-' + releaseId).fadeOut('slow', function(){
$(this).remove();
});
},
error: function() {
alert("Erreur lors de la suppression.");
}
});
}
});
$(document).on("click", ".log-link", function(e) {
e.preventDefault();
let filename = $(this).data("filename");
$.ajax({
url: '/autopost/log',
type: 'GET',
data: { name: filename },
dataType: 'json',
success: function(data) {
$("#logContent").html(data.content);
$("#logModal").removeClass('hidden').fadeIn();
},
error: function() {
alert("Erreur lors du chargement du fichier log.");
}
});
});
$(document).on("click", ".mediainfo-link", function(e) {
e.preventDefault();
let filename = $(this).data("filename");
$.ajax({
url: '/autopost/mediainfo',
type: 'GET',
data: { name: filename },
dataType: 'json',
success: function(data) {
$("#mediainfoContent").text(data.content);
$("#mediainfoModal").removeClass('hidden').fadeIn();
},
error: function() {
alert("Erreur lors du chargement du fichier mediainfo.");
}
});
});
$('.close').click(function(){
$(this).closest('.fixed').fadeOut(function(){
$(this).addClass('hidden');
});
});
$('.fixed').click(function(e) {
if (e.target === this) {
$(this).fadeOut(function(){
$(this).addClass('hidden');
});
}
});
$('#editForm').submit(function(e){
e.preventDefault();
let releaseId = $('#releaseId').val();
let newStatus = $('#statusSelect').val();
$.ajax({
url: '/autopost/edit/' + releaseId,
type: 'POST',
data: { status: newStatus },
success: function(data) {
let statusText = '';
let statusClass = '';
switch(parseInt(newStatus)) {
case 0: case 0:
statusText = 'EN ATTENTE'; statusText = 'EN ATTENTE';
statusClass = 'bg-cyan-500 text-black font-bold'; statusClass = 'bg-cyan-500 text-black font-bold';
@ -443,23 +399,229 @@ autopostRouter.get('/', async (req, res) => {
default: default:
statusText = 'INCONNU'; statusText = 'INCONNU';
} }
let row = $('#row-' + releaseId);
// Mise à jour uniquement de la cellule status var logLink = (parseInt(row.status) === 1 || parseInt(row.status) === 2)
row.find('.status-text') ? ' | <a href="#" class="log-link text-blue-400 hover:underline" data-filename="'+row.nom+'">Log</a>'
.removeClass('bg-cyan-500 bg-green-300 bg-red-300 bg-pink-300') : '';
.addClass(statusClass) var mediainfoLink = (parseInt(row.status) === 0 || parseInt(row.status) === 1 || parseInt(row.status) === 2)
.text(statusText); ? ' | <a href="#" class="mediainfo-link text-blue-400 hover:underline" data-filename="'+row.nom+'">Mediainfo</a>'
$('#editModal').fadeOut(function(){ : '';
var dlLink = (parseInt(row.status) === 1)
? ' | <a href="/autopost/dl?name='+encodeURIComponent(row.nom)+'" class="dl-link text-blue-400 hover:underline">DL</a>'
: '';
var tr = '<tr id="row-'+row.id+'" data-status="'+row.status+'" class="odd:bg-gray-800 even:bg-gray-700">'+
'<td class="px-4 py-2 border border-gray-700 whitespace-nowrap">'+row.nom+'</td>'+
'<td class="px-4 py-2 border border-gray-700 status-text whitespace-nowrap '+statusClass+'">'+statusText+'</td>'+
'<td class="px-4 py-2 border border-gray-700">'+row.id+'</td>'+
'<td class="px-4 py-2 border border-gray-700">'+
'<a href="#" class="edit-link text-blue-400 hover:underline" data-id="'+row.id+'" data-status="'+row.status+'">Editer</a> | '+
'<a href="#" class="delete-link text-blue-400 hover:underline" data-id="'+row.id+'">Supprimer</a>'+
logLink+mediainfoLink+dlLink+
'</td>'+
'</tr>';
tbody.append(tr);
});
}
// Filtrage combiné avec recherche
var currentFilter = null; // null => pas de filtre actif
function applyFilterAndSearch() {
var q = ($("#searchInput").val() || "").toLowerCase();
$("table tbody tr").each(function() {
var $tr = $(this);
var name = $tr.find("td:first").text().toLowerCase();
var status = parseInt($tr.data("status"), 10);
var matchFilter = (currentFilter === null) || (status === currentFilter);
var matchSearch = !q || name.indexOf(q) !== -1;
$tr.toggle(matchFilter && matchSearch);
});
}
$(document).ready(function(){
// Recherche AJAX
$("#searchInput").on("keyup", function() {
var q = $(this).val();
$.ajax({
url: '/autopost/search',
type: 'GET',
data: { q: q },
dataType: 'json',
success: function(data) {
updateTable(data);
applyFilterAndSearch(); // conserve filtre actif
},
error: function() {
alert("Erreur lors de la recherche.");
}
});
});
function toggleShowAllButton() {
if (currentFilter === null) {
$('#showAll').addClass('hidden');
} else {
$('#showAll').removeClass('hidden');
}
}
// Filtrage par clic sur une card
$(document).on("click", ".filter-card", function() {
currentFilter = parseInt($(this).data("status"), 10);
$(".filter-card").removeClass("ring-4 ring-white/40");
$(this).addClass("ring-4 ring-white/40");
applyFilterAndSearch();
toggleShowAllButton();
});
// Bouton tout afficher
$(document).on("click", "#showAll", function() {
currentFilter = null;
$(".filter-card").removeClass("ring-4 ring-white/40");
applyFilterAndSearch();
toggleShowAllButton();
});
// Edition
$(document).on("click", ".edit-link", function(e) {
e.preventDefault();
var releaseId = $(this).data('id');
var currentStatus = $(this).data('status');
$('#releaseId').val(releaseId);
$('#modalReleaseId').text(releaseId);
$('#statusSelect').val(currentStatus);
$('#editModal').removeClass('hidden').fadeIn();
});
// Suppression
$(document).on("click", ".delete-link", function(e) {
e.preventDefault();
if (confirm("Êtes-vous sûr de vouloir supprimer cet enregistrement ?")) {
var releaseId = $(this).data('id');
$.ajax({
url: '/autopost/delete/' + releaseId,
type: 'POST',
success: function(data) {
$('#row-' + releaseId).fadeOut('slow', function(){
$(this).remove();
});
},
error: function() {
alert("Erreur lors de la suppression.");
}
});
}
});
// Affichage log
$(document).on("click", ".log-link", function(e) {
e.preventDefault();
var filename = $(this).data("filename");
$.ajax({
url: '/autopost/log',
type: 'GET',
data: { name: filename },
dataType: 'json',
success: function(data) {
$("#logContent").html(data.content);
$("#logModal").removeClass('hidden').fadeIn();
},
error: function() {
alert("Erreur lors du chargement du fichier log.");
}
});
});
// Affichage mediainfo
$(document).on("click", ".mediainfo-link", function(e) {
e.preventDefault();
var filename = $(this).data("filename");
$.ajax({
url: '/autopost/mediainfo',
type: 'GET',
data: { name: filename },
dataType: 'json',
success: function(data) {
$("#mediainfoContent").text(data.content);
$("#mediainfoModal").removeClass('hidden').fadeIn();
},
error: function() {
alert("Erreur lors du chargement du fichier mediainfo.");
}
});
});
// Fermeture modales
$('.close').click(function(){
$(this).closest('.fixed').fadeOut(function(){
$(this).addClass('hidden'); $(this).addClass('hidden');
}); });
}, });
error: function() {
alert("Erreur lors de la mise à jour."); $('.fixed').click(function(e) {
} if (e.target === this) {
$(this).fadeOut(function(){
$(this).addClass('hidden');
});
}
});
// Edition formulaire
$('#editForm').submit(function(e){
e.preventDefault();
var releaseId = $('#releaseId').val();
var newStatus = $('#statusSelect').val();
$.ajax({
url: '/autopost/edit/' + releaseId,
type: 'POST',
data: { status: newStatus },
success: function(data) {
var statusText = '';
var statusClass = '';
switch(parseInt(newStatus)) {
case 0:
statusText = 'EN ATTENTE';
statusClass = 'bg-cyan-500 text-black font-bold';
break;
case 1:
statusText = 'ENVOI TERMINÉ';
statusClass = 'bg-green-300 text-black font-bold';
break;
case 2:
statusText = 'ERREUR';
statusClass = 'bg-red-300 text-black font-bold';
break;
case 3:
statusText = 'DEJA DISPONIBLE';
statusClass = 'bg-pink-300 text-black font-bold';
break;
case 4:
statusText = 'EN COURS';
statusClass = 'bg-yellow-300 text-black font-bold';
break;
default:
statusText = 'INCONNU';
}
var row = $('#row-' + releaseId);
row.find('.status-text')
.removeClass('bg-cyan-500 bg-green-300 bg-red-300 bg-pink-300 bg-yellow-300')
.addClass(statusClass)
.text(statusText);
$('#editModal').fadeOut(function(){
$(this).addClass('hidden');
});
},
error: function() {
alert("Erreur lors de la mise à jour.");
}
});
});
}); });
}); </script>
});
</script>
</body> </body>
</html> </html>
`; `;

View File

@ -363,7 +363,7 @@ export NVM_DIR="$HOME/.nvm"
# Vérification des modules npm nécessaires # Vérification des modules npm nécessaires
log "Vérification des modules npm requis..." log "Vérification des modules npm requis..."
modules=("express" "express-session" "sqlite3" "ansi-to-html" "@tailwindcss/browser" "autoprefixer" "jquery" "mysql2") modules=("express" "express-session" "sqlite3" "ansi-to-html" "@tailwindcss/browser" "autoprefixer" "jquery" "mysql2" "session-file-store")
missing_modules=() missing_modules=()
for module in "${modules[@]}"; do for module in "${modules[@]}"; do

View File

@ -209,7 +209,7 @@ export NVM_DIR="$HOME/.nvm"
# Vérification des modules npm nécessaires # Vérification des modules npm nécessaires
log "Vérification des modules npm requis..." log "Vérification des modules npm requis..."
modules=("express" "express-session" "sqlite3" "ansi-to-html" "@tailwindcss/browser" "jquery" "mysql2") modules=("express" "express-session" "sqlite3" "ansi-to-html" "@tailwindcss/browser" "jquery" "mysql2" "session-file-store")
missing_modules=() missing_modules=()
for module in "${modules[@]}"; do for module in "${modules[@]}"; do