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:
Torsten Schulz (local)
2025-10-09 00:18:41 +02:00
parent 806cb527d4
commit 993e12d4a5
47 changed files with 1983 additions and 683 deletions

View 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();

View File

@@ -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();

View 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();