feat: implement Jira auto-sync scheduler and UI configuration

- Added `jiraAutoSync` and `jiraSyncInterval` fields to user preferences for scheduler configuration.
- Created `JiraScheduler` service to manage automatic synchronization with Jira based on user settings.
- Updated API route to handle scheduler actions and configuration updates.
- Introduced `JiraSchedulerConfig` component for user interface to control scheduler settings.
- Enhanced `TODO.md` to reflect completed tasks related to Jira synchronization features.
This commit is contained in:
Julien Froidefond
2025-09-21 11:30:41 +02:00
parent a0e2a78372
commit 799a21df5c
9 changed files with 581 additions and 10 deletions

View File

@@ -0,0 +1,201 @@
import { userPreferencesService } from './user-preferences';
import { JiraService } from './jira';
export interface JiraSchedulerConfig {
enabled: boolean;
interval: 'hourly' | 'daily' | 'weekly';
}
export class JiraScheduler {
private timer: NodeJS.Timeout | null = null;
private isRunning = false;
/**
* Démarre le planificateur de synchronisation Jira automatique
*/
async start(): Promise<void> {
if (this.isRunning) {
console.log('⚠️ Jira scheduler is already running');
return;
}
const config = await this.getConfig();
if (!config.enabled) {
console.log('📋 Automatic Jira sync is disabled');
return;
}
// Vérifier que Jira est configuré
const jiraConfig = await userPreferencesService.getJiraConfig();
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) {
console.log('⚠️ Jira not configured, scheduler cannot start');
return;
}
const intervalMs = this.getIntervalMs(config.interval);
// Première synchronisation immédiate (optionnelle)
// this.performScheduledSync();
// Planifier les synchronisations suivantes
this.timer = setInterval(() => {
this.performScheduledSync();
}, intervalMs);
this.isRunning = true;
console.log(`✅ Jira scheduler started with ${config.interval} interval`);
}
/**
* Arrête le planificateur
*/
stop(): void {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.isRunning = false;
console.log('🛑 Jira scheduler stopped');
}
/**
* Redémarre le planificateur (utile lors des changements de config)
*/
async restart(): Promise<void> {
this.stop();
await this.start();
}
/**
* Vérifie si le planificateur fonctionne
*/
isActive(): boolean {
return this.isRunning && this.timer !== null;
}
/**
* Effectue une synchronisation planifiée
*/
private async performScheduledSync(): Promise<void> {
try {
console.log('🔄 Starting scheduled Jira sync...');
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) {
console.log('⚠️ Jira config incomplete, skipping scheduled sync');
return;
}
// Créer le service Jira
const jiraService = new JiraService({
baseUrl: jiraConfig.baseUrl,
email: jiraConfig.email,
apiToken: jiraConfig.apiToken,
projectKey: jiraConfig.projectKey,
ignoredProjects: jiraConfig.ignoredProjects || []
});
// Tester la connexion d'abord
const connectionOk = await jiraService.testConnection();
if (!connectionOk) {
console.error('❌ Scheduled Jira sync failed: connection error');
return;
}
// Effectuer la synchronisation
const result = await jiraService.syncTasks();
if (result.success) {
console.log(`✅ Scheduled Jira sync completed: ${result.tasksCreated} created, ${result.tasksUpdated} updated, ${result.tasksSkipped} skipped`);
} else {
console.error(`❌ Scheduled Jira sync failed: ${result.errors.join(', ')}`);
}
} catch (error) {
console.error('❌ Scheduled Jira sync error:', error);
}
}
/**
* Convertit l'intervalle en millisecondes
*/
private getIntervalMs(interval: JiraSchedulerConfig['interval']): number {
const intervals = {
hourly: 60 * 60 * 1000, // 1 heure
daily: 24 * 60 * 60 * 1000, // 24 heures
weekly: 7 * 24 * 60 * 60 * 1000, // 7 jours
};
return intervals[interval];
}
/**
* Obtient le prochain moment de synchronisation
*/
async getNextSyncTime(): Promise<Date | null> {
if (!this.isRunning || !this.timer) {
return null;
}
const config = await this.getConfig();
const intervalMs = this.getIntervalMs(config.interval);
return new Date(Date.now() + intervalMs);
}
/**
* Récupère la configuration du scheduler depuis les user preferences
*/
private async getConfig(): Promise<JiraSchedulerConfig> {
try {
const [jiraConfig, schedulerConfig] = await Promise.all([
userPreferencesService.getJiraConfig(),
userPreferencesService.getJiraSchedulerConfig()
]);
return {
enabled: schedulerConfig.jiraAutoSync &&
jiraConfig.enabled &&
!!jiraConfig.baseUrl &&
!!jiraConfig.email &&
!!jiraConfig.apiToken,
interval: schedulerConfig.jiraSyncInterval
};
} catch (error) {
console.error('Error getting Jira scheduler config:', error);
return {
enabled: false,
interval: 'daily'
};
}
}
/**
* Obtient les stats du planificateur
*/
async getStatus() {
const config = await this.getConfig();
const jiraConfig = await userPreferencesService.getJiraConfig();
return {
isRunning: this.isRunning,
isEnabled: config.enabled,
interval: config.interval,
nextSync: await this.getNextSyncTime(),
jiraConfigured: !!(jiraConfig.baseUrl && jiraConfig.email && jiraConfig.apiToken),
};
}
}
// Instance singleton
export const jiraScheduler = new JiraScheduler();
// Auto-start du scheduler
// Démarrer avec un délai pour laisser l'app s'initialiser
setTimeout(() => {
console.log('🚀 Auto-starting Jira scheduler...');
jiraScheduler.start();
}, 6000); // 6 secondes, après le backup scheduler

View File

@@ -30,7 +30,9 @@ const DEFAULT_PREFERENCES: UserPreferences = {
email: '',
apiToken: '',
ignoredProjects: []
}
},
jiraAutoSync: false,
jiraSyncInterval: 'daily'
};
/**
@@ -56,9 +58,29 @@ class UserPreferencesService {
}
});
// S'assurer que les nouveaux champs existent (migration douce)
await this.ensureJiraSchedulerFields();
return userPrefs;
}
/**
* S'assure que les champs jiraAutoSync et jiraSyncInterval existent
*/
private async ensureJiraSchedulerFields(): Promise<void> {
try {
await prisma.$executeRaw`
UPDATE user_preferences
SET jiraAutoSync = COALESCE(jiraAutoSync, ${DEFAULT_PREFERENCES.jiraAutoSync}),
jiraSyncInterval = COALESCE(jiraSyncInterval, ${DEFAULT_PREFERENCES.jiraSyncInterval})
WHERE id = 'default'
`;
} catch (error) {
// Ignorer les erreurs si les colonnes n'existent pas encore
console.debug('Migration douce des champs scheduler Jira:', error);
}
}
// === FILTRES KANBAN ===
/**
@@ -216,22 +238,76 @@ class UserPreferencesService {
}
}
// === CONFIGURATION SCHEDULER JIRA ===
/**
* Sauvegarde les préférences du scheduler Jira
*/
async saveJiraSchedulerConfig(jiraAutoSync: boolean, jiraSyncInterval: 'hourly' | 'daily' | 'weekly'): Promise<void> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
// Utiliser une requête SQL brute temporairement pour éviter les problèmes de types
await prisma.$executeRaw`
UPDATE user_preferences
SET jiraAutoSync = ${jiraAutoSync}, jiraSyncInterval = ${jiraSyncInterval}
WHERE id = ${userPrefs.id}
`;
} catch (error) {
console.warn('Erreur lors de la sauvegarde de la config scheduler Jira:', error);
throw error;
}
}
/**
* Récupère les préférences du scheduler Jira
*/
async getJiraSchedulerConfig(): Promise<{ jiraAutoSync: boolean; jiraSyncInterval: 'hourly' | 'daily' | 'weekly' }> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
// Utiliser une requête SQL brute pour récupérer les nouveaux champs
const result = await prisma.$queryRaw<Array<{ jiraAutoSync: number; jiraSyncInterval: string }>>`
SELECT jiraAutoSync, jiraSyncInterval FROM user_preferences WHERE id = ${userPrefs.id}
`;
if (result.length > 0) {
return {
jiraAutoSync: Boolean(result[0].jiraAutoSync),
jiraSyncInterval: (result[0].jiraSyncInterval as 'hourly' | 'daily' | 'weekly') || DEFAULT_PREFERENCES.jiraSyncInterval
};
}
return {
jiraAutoSync: DEFAULT_PREFERENCES.jiraAutoSync,
jiraSyncInterval: DEFAULT_PREFERENCES.jiraSyncInterval
};
} catch (error) {
console.warn('Erreur lors de la récupération de la config scheduler Jira:', error);
return {
jiraAutoSync: DEFAULT_PREFERENCES.jiraAutoSync,
jiraSyncInterval: DEFAULT_PREFERENCES.jiraSyncInterval
};
}
}
/**
* Récupère toutes les préférences utilisateur
*/
async getAllPreferences(): Promise<UserPreferences> {
const [kanbanFilters, viewPreferences, columnVisibility, jiraConfig] = await Promise.all([
const [kanbanFilters, viewPreferences, columnVisibility, jiraConfig, jiraSchedulerConfig] = await Promise.all([
this.getKanbanFilters(),
this.getViewPreferences(),
this.getColumnVisibility(),
this.getJiraConfig()
this.getJiraConfig(),
this.getJiraSchedulerConfig()
]);
return {
kanbanFilters,
viewPreferences,
columnVisibility,
jiraConfig
jiraConfig,
jiraAutoSync: jiraSchedulerConfig.jiraAutoSync,
jiraSyncInterval: jiraSchedulerConfig.jiraSyncInterval
};
}
@@ -243,7 +319,8 @@ class UserPreferencesService {
this.saveKanbanFilters(preferences.kanbanFilters),
this.saveViewPreferences(preferences.viewPreferences),
this.saveColumnVisibility(preferences.columnVisibility),
this.saveJiraConfig(preferences.jiraConfig)
this.saveJiraConfig(preferences.jiraConfig),
this.saveJiraSchedulerConfig(preferences.jiraAutoSync, preferences.jiraSyncInterval)
]);
}