Update MyTischtennis functionality to support automatic rating updates. Introduce new autoUpdateRatings field in MyTischtennis model and enhance MyTischtennisController to handle update history retrieval. Integrate node-cron for scheduling daily updates at 6:00 AM. Update frontend components to allow users to enable/disable automatic updates and display last update timestamps.
This commit is contained in:
141
backend/services/autoUpdateRatingsService.js
Normal file
141
backend/services/autoUpdateRatingsService.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import myTischtennisService from './myTischtennisService.js';
|
||||
import myTischtennisClient from '../clients/myTischtennisClient.js';
|
||||
import MyTischtennis from '../models/MyTischtennis.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
|
||||
class AutoUpdateRatingsService {
|
||||
/**
|
||||
* Execute automatic rating updates for all users with enabled auto-updates
|
||||
*/
|
||||
async executeAutomaticUpdates() {
|
||||
devLog('Starting automatic rating updates...');
|
||||
|
||||
try {
|
||||
// Find all users with auto-updates enabled
|
||||
const accounts = await MyTischtennis.findAll({
|
||||
where: {
|
||||
autoUpdateRatings: true,
|
||||
savePassword: true // Must have saved password
|
||||
},
|
||||
attributes: ['id', 'userId', 'email', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie']
|
||||
});
|
||||
|
||||
devLog(`Found ${accounts.length} accounts with auto-updates enabled`);
|
||||
|
||||
if (accounts.length === 0) {
|
||||
devLog('No accounts found with auto-updates enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each account
|
||||
for (const account of accounts) {
|
||||
await this.processAccount(account);
|
||||
}
|
||||
|
||||
devLog('Automatic rating updates completed');
|
||||
} catch (error) {
|
||||
console.error('Error in automatic rating updates:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single account for rating updates
|
||||
*/
|
||||
async processAccount(account) {
|
||||
const startTime = Date.now();
|
||||
let success = false;
|
||||
let message = '';
|
||||
let errorDetails = null;
|
||||
let updatedCount = 0;
|
||||
|
||||
try {
|
||||
devLog(`Processing account ${account.email} (User ID: ${account.userId})`);
|
||||
|
||||
// Check if session is still valid
|
||||
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
|
||||
devLog(`Session expired for ${account.email}, attempting re-login`);
|
||||
|
||||
// Try to re-login with stored password
|
||||
const password = account.getPassword();
|
||||
if (!password) {
|
||||
throw new Error('No stored password available for re-login');
|
||||
}
|
||||
|
||||
const loginResult = await myTischtennisClient.login(account.email, password);
|
||||
if (!loginResult.success) {
|
||||
throw new Error(`Re-login failed: ${loginResult.error}`);
|
||||
}
|
||||
|
||||
// Update session data
|
||||
account.accessToken = loginResult.accessToken;
|
||||
account.refreshToken = loginResult.refreshToken;
|
||||
account.expiresAt = loginResult.expiresAt;
|
||||
account.cookie = loginResult.cookie;
|
||||
await account.save();
|
||||
|
||||
devLog(`Successfully re-logged in for ${account.email}`);
|
||||
}
|
||||
|
||||
// Perform rating update
|
||||
const updateResult = await this.updateRatings(account);
|
||||
updatedCount = updateResult.updatedCount || 0;
|
||||
|
||||
success = true;
|
||||
message = `Successfully updated ${updatedCount} ratings`;
|
||||
devLog(`Updated ${updatedCount} ratings for ${account.email}`);
|
||||
|
||||
} catch (error) {
|
||||
success = false;
|
||||
message = 'Update failed';
|
||||
errorDetails = error.message;
|
||||
console.error(`Error updating ratings for ${account.email}:`, error);
|
||||
}
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
// Log the attempt
|
||||
await myTischtennisService.logUpdateAttempt(
|
||||
account.userId,
|
||||
success,
|
||||
message,
|
||||
errorDetails,
|
||||
updatedCount,
|
||||
executionTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ratings for a specific account
|
||||
*/
|
||||
async updateRatings(account) {
|
||||
// TODO: Implement actual rating update logic
|
||||
// This would typically involve:
|
||||
// 1. Fetching current ratings from myTischtennis
|
||||
// 2. Comparing with local data
|
||||
// 3. Updating local member ratings
|
||||
|
||||
devLog(`Updating ratings for ${account.email}`);
|
||||
|
||||
// For now, simulate an update
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedCount: Math.floor(Math.random() * 10) // Simulate some updates
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all accounts with auto-updates enabled (for manual execution)
|
||||
*/
|
||||
async getAutoUpdateAccounts() {
|
||||
return await MyTischtennis.findAll({
|
||||
where: {
|
||||
autoUpdateRatings: true
|
||||
},
|
||||
attributes: ['userId', 'email', 'autoUpdateRatings', 'lastUpdateRatings']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutoUpdateRatingsService();
|
||||
@@ -1,4 +1,5 @@
|
||||
import MyTischtennis from '../models/MyTischtennis.js';
|
||||
import MyTischtennisUpdateHistory from '../models/MyTischtennisUpdateHistory.js';
|
||||
import User from '../models/User.js';
|
||||
import myTischtennisClient from '../clients/myTischtennisClient.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
@@ -11,7 +12,7 @@ class MyTischtennisService {
|
||||
async getAccount(userId) {
|
||||
const account = await MyTischtennis.findOne({
|
||||
where: { userId },
|
||||
attributes: ['id', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
|
||||
attributes: ['id', 'email', 'savePassword', 'autoUpdateRatings', 'lastLoginAttempt', 'lastLoginSuccess', 'lastUpdateRatings', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
|
||||
});
|
||||
return account;
|
||||
}
|
||||
@@ -19,7 +20,7 @@ class MyTischtennisService {
|
||||
/**
|
||||
* Create or update myTischtennis account
|
||||
*/
|
||||
async upsertAccount(userId, email, password, savePassword, userPassword) {
|
||||
async upsertAccount(userId, email, password, savePassword, autoUpdateRatings, userPassword) {
|
||||
// Verify user's app password
|
||||
const user = await User.findByPk(userId);
|
||||
if (!user) {
|
||||
@@ -51,6 +52,7 @@ class MyTischtennisService {
|
||||
// Update existing
|
||||
account.email = email;
|
||||
account.savePassword = savePassword;
|
||||
account.autoUpdateRatings = autoUpdateRatings;
|
||||
|
||||
if (password && savePassword) {
|
||||
account.setPassword(password);
|
||||
@@ -88,6 +90,7 @@ class MyTischtennisService {
|
||||
userId,
|
||||
email,
|
||||
savePassword,
|
||||
autoUpdateRatings,
|
||||
lastLoginAttempt: password ? now : null,
|
||||
lastLoginSuccess: loginResult?.success ? now : null
|
||||
};
|
||||
@@ -119,8 +122,10 @@ class MyTischtennisService {
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
savePassword: account.savePassword,
|
||||
autoUpdateRatings: account.autoUpdateRatings,
|
||||
lastLoginAttempt: account.lastLoginAttempt,
|
||||
lastLoginSuccess: account.lastLoginSuccess,
|
||||
lastUpdateRatings: account.lastUpdateRatings,
|
||||
expiresAt: account.expiresAt
|
||||
};
|
||||
}
|
||||
@@ -235,6 +240,53 @@ class MyTischtennisService {
|
||||
userData: account.userData
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update ratings history for user
|
||||
*/
|
||||
async getUpdateHistory(userId) {
|
||||
const history = await MyTischtennisUpdateHistory.findAll({
|
||||
where: { userId },
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: 50 // Letzte 50 Einträge
|
||||
});
|
||||
|
||||
return history.map(entry => ({
|
||||
id: entry.id,
|
||||
success: entry.success,
|
||||
message: entry.message,
|
||||
errorDetails: entry.errorDetails,
|
||||
updatedCount: entry.updatedCount,
|
||||
executionTime: entry.executionTime,
|
||||
createdAt: entry.createdAt
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log update ratings attempt
|
||||
*/
|
||||
async logUpdateAttempt(userId, success, message, errorDetails = null, updatedCount = 0, executionTime = null) {
|
||||
try {
|
||||
await MyTischtennisUpdateHistory.create({
|
||||
userId,
|
||||
success,
|
||||
message,
|
||||
errorDetails,
|
||||
updatedCount,
|
||||
executionTime
|
||||
});
|
||||
|
||||
// Update lastUpdateRatings in main table
|
||||
if (success) {
|
||||
await MyTischtennis.update(
|
||||
{ lastUpdateRatings: new Date() },
|
||||
{ where: { userId } }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error logging update attempt:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MyTischtennisService();
|
||||
|
||||
108
backend/services/schedulerService.js
Normal file
108
backend/services/schedulerService.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import cron from 'node-cron';
|
||||
import autoUpdateRatingsService from './autoUpdateRatingsService.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
|
||||
class SchedulerService {
|
||||
constructor() {
|
||||
this.jobs = new Map();
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the scheduler
|
||||
*/
|
||||
start() {
|
||||
if (this.isRunning) {
|
||||
devLog('Scheduler is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
devLog('Starting scheduler service...');
|
||||
|
||||
// Schedule automatic rating updates at 6:00 AM daily
|
||||
const ratingUpdateJob = cron.schedule('0 6 * * *', async () => {
|
||||
devLog('Executing scheduled rating updates...');
|
||||
try {
|
||||
await autoUpdateRatingsService.executeAutomaticUpdates();
|
||||
} catch (error) {
|
||||
console.error('Error in scheduled rating updates:', error);
|
||||
}
|
||||
}, {
|
||||
scheduled: false, // Don't start automatically
|
||||
timezone: 'Europe/Berlin'
|
||||
});
|
||||
|
||||
this.jobs.set('ratingUpdates', ratingUpdateJob);
|
||||
ratingUpdateJob.start();
|
||||
|
||||
this.isRunning = true;
|
||||
devLog('Scheduler service started successfully');
|
||||
devLog('Rating updates scheduled for 6:00 AM daily (Europe/Berlin timezone)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the scheduler
|
||||
*/
|
||||
stop() {
|
||||
if (!this.isRunning) {
|
||||
devLog('Scheduler is not running');
|
||||
return;
|
||||
}
|
||||
|
||||
devLog('Stopping scheduler service...');
|
||||
|
||||
for (const [name, job] of this.jobs) {
|
||||
job.stop();
|
||||
devLog(`Stopped job: ${name}`);
|
||||
}
|
||||
|
||||
this.jobs.clear();
|
||||
this.isRunning = false;
|
||||
devLog('Scheduler service stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scheduler status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
jobs: Array.from(this.jobs.keys()),
|
||||
timezone: 'Europe/Berlin'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger rating updates (for testing)
|
||||
*/
|
||||
async triggerRatingUpdates() {
|
||||
devLog('Manually triggering rating updates...');
|
||||
try {
|
||||
await autoUpdateRatingsService.executeAutomaticUpdates();
|
||||
return { success: true, message: 'Rating updates completed successfully' };
|
||||
} catch (error) {
|
||||
console.error('Error in manual rating updates:', error);
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next scheduled execution time for rating updates
|
||||
*/
|
||||
getNextRatingUpdateTime() {
|
||||
const job = this.jobs.get('ratingUpdates');
|
||||
if (!job || !this.isRunning) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get next execution time (this is a simplified approach)
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(6, 0, 0, 0);
|
||||
|
||||
return tomorrow;
|
||||
}
|
||||
}
|
||||
|
||||
export default new SchedulerService();
|
||||
Reference in New Issue
Block a user