add docker + debug

This commit is contained in:
mahek 2025-07-21 16:38:07 +02:00
parent 848a79a04e
commit 1f4193f9c0
26 changed files with 21051 additions and 28 deletions

20
frontend/.eslintrc.js Normal file
View file

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
parser: '@babel/eslint-parser',
requireConfigFile: false,
ecmaVersion: 2020,
sourceType: 'module'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

18
frontend/Dockerfile Normal file
View file

@ -0,0 +1,18 @@
FROM node:16-alpine
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application
COPY . .
# Expose port 8012
EXPOSE 8012
# Start the application
CMD ["npm", "run", "serve"]

5
frontend/babel.config.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

181
frontend/docs/USER_GUIDE.md Normal file
View file

@ -0,0 +1,181 @@
# Guide d'utilisation de Managerr
## Table des matières
1. [Introduction](#introduction)
2. [Premiers pas](#premiers-pas)
- [Inscription](#inscription)
- [Connexion](#connexion)
3. [Interface principale](#interface-principale)
- [Barre de navigation](#barre-de-navigation)
- [Thème clair/sombre](#thème-clairsombre)
4. [Tableau de bord](#tableau-de-bord)
- [Agenda mensuel](#agenda-mensuel)
- [Prochains téléchargements](#prochains-téléchargements)
5. [Gestion des films](#gestion-des-films)
- [Liste des films](#liste-des-films)
- [Films manquants](#films-manquants)
- [Films à venir](#films-à-venir)
- [Historique](#historique-des-films)
6. [Gestion des séries](#gestion-des-séries)
- [Liste des séries](#liste-des-séries)
- [Épisodes manquants](#épisodes-manquants)
- [Épisodes à venir](#épisodes-à-venir)
- [Historique](#historique-des-séries)
7. [Paramètres](#paramètres)
- [Configuration de Sonarr](#configuration-de-sonarr)
- [Configuration de Radarr](#configuration-de-radarr)
- [Test des connexions](#test-des-connexions)
8. [Conseils et astuces](#conseils-et-astuces)
9. [Dépannage](#dépannage)
## Introduction
Managerr est une application web qui vous permet de gérer vos films et séries TV à partir d'une interface unifiée en utilisant les API de Sonarr et Radarr. Ce guide vous aidera à prendre en main l'application et à utiliser toutes ses fonctionnalités.
## Premiers pas
### Inscription
1. Accédez à la page d'accueil de l'application
2. Cliquez sur "S'inscrire"
3. Remplissez le formulaire avec votre nom d'utilisateur, votre email et un mot de passe sécurisé
4. Cliquez sur "Créer un compte"
### Connexion
1. Accédez à la page d'accueil de l'application
2. Cliquez sur "Se connecter"
3. Entrez votre email et votre mot de passe
4. Cliquez sur "Connexion"
## Interface principale
### Barre de navigation
La barre de navigation vous permet d'accéder aux différentes sections de l'application :
- **Tableau de bord** : Vue d'ensemble avec l'agenda mensuel
- **Films** : Gestion de votre collection de films
- **Séries** : Gestion de vos séries TV
- **Paramètres** : Configuration des API Sonarr et Radarr
- **Déconnexion** : Pour quitter votre session
### Thème clair/sombre
Managerr prend en charge les thèmes clair et sombre :
- Le bouton de bascule du thème se trouve dans la barre de navigation
- Par défaut, l'application utilise les préférences de votre système
- Vous pouvez basculer manuellement entre les modes clair et sombre à tout moment
## Tableau de bord
### Agenda mensuel
L'agenda mensuel affiche tous les films et épisodes à venir :
- Naviguez entre les mois avec les flèches de navigation
- Chaque jour contenant des sorties est marqué d'un indicateur
- Cliquez sur un jour pour voir les détails des sorties prévues
### Prochains téléchargements
Cette section affiche la liste des prochains films et épisodes qui seront téléchargés, triés par date.
## Gestion des films
### Liste des films
- Affiche tous les films de votre bibliothèque Radarr
- Recherche et filtre par titre, genre, année, etc.
- Tri par différents critères (titre, date d'ajout, etc.)
- Cliquez sur un film pour voir ses détails
### Films manquants
- Affiche les films qui sont dans votre bibliothèque mais pas encore téléchargés
- Indique le statut de recherche pour chaque film
- Permet de déclencher une recherche manuelle
### Films à venir
- Affiche les films dont la sortie est prévue prochainement
- Indique les dates de sortie au cinéma et en Blu-ray/DVD
### Historique des films
- Affiche l'historique des téléchargements de films
- Inclut les informations sur la qualité et la date de téléchargement
## Gestion des séries
### Liste des séries
- Affiche toutes les séries de votre bibliothèque Sonarr
- Recherche et filtre par titre, genre, réseau, etc.
- Tri par différents critères (titre, date d'ajout, etc.)
- Cliquez sur une série pour voir ses détails et la liste des saisons/épisodes
### Épisodes manquants
- Affiche les épisodes qui sont dans votre bibliothèque mais pas encore téléchargés
- Regroupés par série et saison
- Permet de déclencher une recherche manuelle
### Épisodes à venir
- Affiche les épisodes dont la diffusion est prévue prochainement
- Indique les dates de diffusion et les chaînes/réseaux
### Historique des séries
- Affiche l'historique des téléchargements d'épisodes
- Inclut les informations sur la qualité et la date de téléchargement
## Paramètres
### Configuration de Sonarr
1. Accédez à la page "Paramètres"
2. Dans la section Sonarr, entrez :
- L'URL de votre serveur Sonarr (ex : http://192.168.1.100:8989)
- Votre clé API Sonarr (disponible dans les paramètres de Sonarr)
3. Cliquez sur "Tester la connexion" pour vérifier que tout fonctionne
4. Cliquez sur "Enregistrer" pour sauvegarder vos paramètres
### Configuration de Radarr
1. Accédez à la page "Paramètres"
2. Dans la section Radarr, entrez :
- L'URL de votre serveur Radarr (ex : http://192.168.1.100:7878)
- Votre clé API Radarr (disponible dans les paramètres de Radarr)
3. Cliquez sur "Tester la connexion" pour vérifier que tout fonctionne
4. Cliquez sur "Enregistrer" pour sauvegarder vos paramètres
### Test des connexions
- Le bouton "Tester la connexion" vérifie que :
- L'URL est accessible
- La clé API est valide
- La version de l'API est compatible
- Un message vous informera du résultat du test
## Conseils et astuces
- **Mise en cache** : L'application met en cache certaines données pour améliorer les performances. Si vous ne voyez pas les changements récents, essayez de rafraîchir la page.
- **Mode responsive** : L'application s'adapte automatiquement à tous les appareils, y compris les smartphones et les tablettes.
- **Raccourcis clavier** : Utilisez la touche "T" pour basculer rapidement entre les thèmes clair et sombre.
## Dépannage
### Problèmes de connexion aux API
1. Vérifiez que vos serveurs Sonarr et Radarr sont en cours d'exécution
2. Assurez-vous que les URLs sont correctes et incluent le port
3. Vérifiez que vos clés API sont valides
4. Assurez-vous que votre réseau permet les connexions aux ports de Sonarr et Radarr
### Problèmes d'affichage
1. Essayez d'actualiser la page
2. Videz le cache de votre navigateur
3. Essayez un autre navigateur pour voir si le problème persiste
Si vous rencontrez d'autres problèmes, n'hésitez pas à ouvrir une issue sur GitHub ou à contacter l'équipe de support.

14268
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,19 +10,23 @@
"dependencies": {
"axios": "^1.6.0",
"core-js": "^3.33.0",
"date-fns": "^2.30.0",
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"vuex": "^4.1.0",
"date-fns": "^2.30.0"
"vuex": "^4.1.0"
},
"devDependencies": {
"@babel/core": "^7.28.0",
"@babel/eslint-parser": "^7.28.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"compression-webpack-plugin": "^11.1.0",
"eslint": "^8.50.0",
"eslint-plugin-vue": "^9.17.0",
"image-webpack-loader": "^8.1.0",
"sass": "^1.69.0",
"sass-loader": "^13.3.0"
}

View file

@ -0,0 +1,133 @@
// services/apiService.js
// Service pour gérer les appels API avec mise en cache
import axios from 'axios'
import store from '@/store'
import router from '@/router'
import cacheService from './cacheService'
const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_URL || 'http://localhost:5000/api',
headers: {
'Content-Type': 'application/json'
}
})
// Intercepteur pour ajouter le token d'authentification
apiClient.interceptors.request.use(
config => {
const token = store.getters.token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// Intercepteur pour gérer les erreurs
apiClient.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 401) {
// Déconnexion automatique si le token n'est plus valide
store.dispatch('logout')
router.push('/login')
}
return Promise.reject(error)
}
)
export default {
// Méthodes génériques avec cache
async get(url, params = {}, useCache = true, cacheTTL = 5 * 60 * 1000) {
const cacheKey = `get:${url}:${JSON.stringify(params)}`
// Vérifier le cache si demandé
if (useCache && cacheService.has(cacheKey)) {
return Promise.resolve(cacheService.get(cacheKey))
}
try {
const response = await apiClient.get(url, { params })
// Mettre en cache la réponse si demandé
if (useCache) {
cacheService.set(cacheKey, response.data, cacheTTL)
}
return response.data
} catch (error) {
console.error('API GET error:', error)
throw error
}
},
async post(url, data = {}, useCache = false, cacheTTL = 5 * 60 * 1000) {
// Pour les POST, on n'utilise le cache que dans des cas spéciaux
// indiqués par le paramètre useCache
const cacheKey = `post:${url}:${JSON.stringify(data)}`
if (useCache && cacheService.has(cacheKey)) {
return Promise.resolve(cacheService.get(cacheKey))
}
try {
const response = await apiClient.post(url, data)
if (useCache) {
cacheService.set(cacheKey, response.data, cacheTTL)
}
// Invalider le cache GET pour ce endpoint
this.invalidateCache(url)
return response.data
} catch (error) {
console.error('API POST error:', error)
throw error
}
},
async put(url, data = {}) {
try {
const response = await apiClient.put(url, data)
// Invalider le cache pour ce endpoint
this.invalidateCache(url)
return response.data
} catch (error) {
console.error('API PUT error:', error)
throw error
}
},
async delete(url) {
try {
const response = await apiClient.delete(url)
// Invalider le cache pour ce endpoint
this.invalidateCache(url)
return response.data
} catch (error) {
console.error('API DELETE error:', error)
throw error
}
},
// Méthode pour invalider le cache lié à une URL
invalidateCache(url) {
Object.keys(cacheService.cache).forEach(key => {
if (key.includes(url)) {
cacheService.invalidate(key)
}
})
},
// Méthode pour vider tout le cache
clearCache() {
cacheService.clear()
}
}

View file

@ -0,0 +1,69 @@
// services/cacheService.js
// Service pour gérer la mise en cache des données
class CacheService {
constructor() {
this.cache = {};
this.defaultTTL = 5 * 60 * 1000; // 5 minutes en millisecondes
}
/**
* Récupère une entrée en cache
* @param {string} key - La clé de l'entrée
* @returns {Object|null} - L'entrée ou null si non trouvée ou expirée
*/
get(key) {
const item = this.cache[key];
if (!item) return null;
// Vérifier si l'entrée est expirée
if (Date.now() > item.expiry) {
// Suppression automatique des entrées expirées
delete this.cache[key];
return null;
}
return item.value;
}
/**
* Ajoute une entrée dans le cache
* @param {string} key - La clé de l'entrée
* @param {any} value - La valeur à stocker
* @param {number} ttl - Durée de vie en millisecondes (par défaut: 5 minutes)
*/
set(key, value, ttl = this.defaultTTL) {
this.cache[key] = {
value,
expiry: Date.now() + ttl
};
}
/**
* Vérifie si une entrée existe et est valide
* @param {string} key - La clé de l'entrée
* @returns {boolean} - Vrai si l'entrée existe et n'est pas expirée
*/
has(key) {
return this.get(key) !== null;
}
/**
* Supprime une entrée du cache
* @param {string} key - La clé de l'entrée
*/
invalidate(key) {
delete this.cache[key];
}
/**
* Vide tout le cache
*/
clear() {
this.cache = {};
}
}
// Export d'une instance singleton
export default new CacheService();

View file

@ -113,6 +113,17 @@ export default createStore({
// Effacer les erreurs
clearError({ commit }) {
commit('CLEAR_ERROR')
},
// Vérifier s'il existe des utilisateurs dans la base de données
async checkUsersExist() {
try {
const response = await axios.get(`${API_URL}/auth/check-users-exist`)
return response.data.usersExist
} catch (error) {
console.error('Erreur lors de la vérification des utilisateurs', error)
return true // Par défaut, on suppose qu'il existe des utilisateurs pour éviter des comportements inattendus
}
}
},

View file

@ -0,0 +1,61 @@
<template>
<div class="home">
<h1>Bienvenue sur Managerr</h1>
<p>La plateforme de gestion pour Sonarr et Radarr</p>
<div class="actions">
<router-link to="/login" class="btn btn-primary">Se connecter</router-link>
<router-link to="/register" class="btn btn-secondary">Créer un compte</router-link>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
async created() {
try {
const usersExist = await this.$store.dispatch('checkUsersExist')
if (!usersExist) {
// S'il n'y a pas d'utilisateurs, rediriger directement vers la page d'inscription
this.$router.push('/register')
} else if (this.$store.getters.isAuthenticated) {
// Si l'utilisateur est déjà authentifié, rediriger vers le tableau de bord
this.$router.push('/dashboard')
}
} catch (error) {
console.error('Erreur lors de la vérification des utilisateurs:', error)
}
}
}
</script>
<style scoped>
.home {
text-align: center;
padding: 2rem;
}
.actions {
margin-top: 2rem;
display: flex;
justify-content: center;
gap: 1rem;
}
.btn {
padding: 0.75rem 1.5rem;
border-radius: 4px;
text-decoration: none;
font-weight: bold;
}
.btn-primary {
background-color: #4CAF50;
color: white;
}
.btn-secondary {
background-color: #2196F3;
color: white;
}
</style>

View file

@ -53,7 +53,7 @@ export default {
...mapGetters(['error', 'loading'])
},
methods: {
...mapActions(['login', 'clearError']),
...mapActions(['login', 'clearError', 'checkUsersExist']),
async handleLogin() {
try {
await this.login({
@ -64,10 +64,17 @@ export default {
} catch (error) {
// L'erreur est gérée dans le store
}
},
async checkFirstConnection() {
const usersExist = await this.checkUsersExist()
if (!usersExist) {
this.$router.push('/register')
}
}
},
created() {
this.clearError()
this.checkFirstConnection()
}
}
</script>

View file

@ -0,0 +1,48 @@
<template>
<div class="not-found">
<h1>404</h1>
<h2>Page non trouvée</h2>
<p>La page que vous cherchez n'existe pas ou a été déplacée.</p>
<router-link to="/" class="home-link">Retour à l'accueil</router-link>
</div>
</template>
<script>
export default {
name: 'NotFound'
}
</script>
<style scoped>
.not-found {
text-align: center;
padding: 4rem 1rem;
}
h1 {
font-size: 6rem;
margin: 0;
color: #e74c3c;
}
h2 {
font-size: 2rem;
margin-top: 1rem;
}
p {
margin-top: 1rem;
color: #666;
}
.home-link {
display: inline-block;
margin-top: 2rem;
padding: 0.75rem 1.5rem;
background-color: #3498db;
color: white;
text-decoration: none;
border-radius: 4px;
font-weight: bold;
}
</style>

View file

@ -0,0 +1,148 @@
<template>
<div class="register">
<h2>{{ isFirstUser ? 'Création du compte administrateur' : 'Inscription' }}</h2>
<div v-if="isFirstUser" class="first-user-message">
<p>Bienvenue sur Managerr ! Aucun compte n'existe encore dans le système.</p>
<p>Veuillez créer un premier compte qui aura les droits d'administrateur.</p>
</div>
<form @submit.prevent="register">
<div class="form-group">
<label for="username">Nom d'utilisateur</label>
<input type="text" id="username" v-model="username" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" v-model="email" required>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" v-model="password" required>
</div>
<div class="form-group">
<label for="confirmPassword">Confirmer le mot de passe</label>
<input type="password" id="confirmPassword" v-model="confirmPassword" required>
</div>
<div class="form-actions">
<button type="submit" class="btn-register">
{{ isFirstUser ? 'Créer le compte administrateur' : 'S\'inscrire' }}
</button>
</div>
<p v-if="!isFirstUser" class="login-link">Déjà un compte ? <router-link to="/login">Se connecter</router-link></p>
</form>
</div>
</template>
<script>
export default {
name: 'Register',
data() {
return {
username: '',
email: '',
password: '',
confirmPassword: '',
isFirstUser: false
}
},
async created() {
// Vérifier s'il s'agit du premier utilisateur
try {
const usersExist = await this.$store.dispatch('checkUsersExist')
this.isFirstUser = !usersExist
} catch (error) {
console.error('Erreur lors de la vérification des utilisateurs:', error)
}
},
methods: {
register() {
// Validation simple
if (this.password !== this.confirmPassword) {
alert("Les mots de passe ne correspondent pas");
return;
}
// Appel API pour s'inscrire
this.$store.dispatch('register', {
username: this.username,
email: this.email,
password: this.password
}).then(() => {
this.$router.push('/dashboard');
}).catch(error => {
console.error('Erreur d\'inscription:', error);
alert("Échec de l'inscription. Veuillez réessayer.");
});
}
}
}
</script>
<style scoped>
.register {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
background-color: white;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.form-actions {
margin-top: 1.5rem;
}
.btn-register {
width: 100%;
padding: 0.75rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
}
.login-link {
text-align: center;
margin-top: 1rem;
}
.login-link a {
color: #2196F3;
text-decoration: none;
}
.first-user-message {
background-color: #e3f2fd;
border-left: 4px solid #2196F3;
padding: 1rem;
margin-bottom: 1.5rem;
border-radius: 4px;
}
.first-user-message p {
margin: 0.5rem 0;
color: #0d47a1;
}
</style>

38
frontend/vue.config.js Normal file
View file

@ -0,0 +1,38 @@
const { defineConfig } = require('@vue/cli-service')
const path = require('path')
// Configuration simplifiée pour éviter les erreurs potentielles
module.exports = defineConfig({
devServer: {
port: 8012, // Nouveau port pour le serveur de développement
host: '0.0.0.0', // Permet d'accéder à l'application depuis l'extérieur du conteneur
allowedHosts: 'all' // Autorise tous les hôtes à accéder à l'application
},
transpileDependencies: true,
productionSourceMap: false, // Désactive les source maps en production pour réduire la taille
lintOnSave: false, // Désactive temporairement la vérification ESLint au démarrage
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all', // Divise les chunks pour améliorer le chargement
minSize: 20000,
maxSize: 250000,
}
},
performance: {
hints: 'warning', // Affiche des avertissements pour les assets trop grands
maxAssetSize: 512000, // Taille maximale d'un asset (500 KiB)
maxEntrypointSize: 512000 // Taille maximale d'un point d'entrée (500 KiB)
}
},
chainWebpack: config => {
// Préchargement des routes pour améliorer la navigation (seulement si le plugin existe)
if (config.plugins.has('preload')) {
config.plugin('preload')
.tap(args => {
args[0].include = 'initial';
return args;
});
}
}
});