feat(myTischtennis): implement asynchronous team data fetching and job status tracking

- Added a new endpoint to start an asynchronous job for fetching team data, allowing for non-blocking operations.
- Implemented job status tracking to retrieve the status of ongoing fetch jobs, enhancing user experience with real-time updates.
- Updated the frontend to initiate async fetch requests and poll for job completion, improving data retrieval efficiency and user feedback.
This commit is contained in:
Torsten Schulz (local)
2026-03-02 13:32:57 +01:00
parent e26bc22e19
commit 3df8f6fd81
3 changed files with 172 additions and 16 deletions

View File

@@ -3,14 +3,132 @@ import myTischtennisService from '../services/myTischtennisService.js';
import MemberService from '../services/memberService.js';
import autoFetchMatchResultsService from '../services/autoFetchMatchResultsService.js';
import apiLogService from '../services/apiLogService.js';
import axios from 'axios';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import User from '../models/User.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
import { randomUUID } from 'crypto';
const teamDataFetchJobs = new Map();
const TEAM_DATA_JOB_TTL_MS = 60 * 60 * 1000;
const cleanupFinishedTeamDataJobs = () => {
const now = Date.now();
for (const [jobId, job] of teamDataFetchJobs.entries()) {
if (job.finishedAt && (now - job.finishedAt) > TEAM_DATA_JOB_TTL_MS) {
teamDataFetchJobs.delete(jobId);
}
}
};
class MyTischtennisUrlController {
async startFetchTeamDataJob(req, res, next) {
try {
const { clubTeamId } = req.body || {};
if (!clubTeamId) {
throw new HttpError('clubTeamId is required', 400);
}
cleanupFinishedTeamDataJobs();
const jobId = randomUUID();
const startedAt = Date.now();
teamDataFetchJobs.set(jobId, {
jobId,
status: 'queued',
startedAt,
updatedAt: startedAt,
finishedAt: null,
clubTeamId,
result: null,
error: null
});
const authHeaders = {
authcode: req.headers.authcode,
userid: req.headers.userid
};
const internalPort = process.env.PORT || 3050;
const internalUrl = `http://127.0.0.1:${internalPort}/api/mytischtennis/fetch-team-data`;
// Background execution; response is returned immediately.
(async () => {
const job = teamDataFetchJobs.get(jobId);
if (!job) return;
job.status = 'running';
job.updatedAt = Date.now();
try {
const response = await axios.post(
internalUrl,
{ clubTeamId },
{
headers: authHeaders,
timeout: 10 * 60 * 1000,
validateStatus: () => true
}
);
if (response.status >= 200 && response.status < 300 && response.data?.success) {
job.status = 'completed';
job.result = response.data;
job.error = null;
} else {
job.status = 'failed';
job.result = null;
job.error = response.data?.error || response.data?.message || `Job failed with status ${response.status}`;
}
} catch (error) {
job.status = 'failed';
job.result = null;
job.error = error?.message || String(error);
} finally {
job.updatedAt = Date.now();
job.finishedAt = Date.now();
}
})();
return res.status(202).json({
success: true,
jobId,
status: 'queued'
});
} catch (error) {
next(error);
}
}
async getFetchTeamDataJobStatus(req, res, next) {
try {
const { jobId } = req.params;
cleanupFinishedTeamDataJobs();
const job = teamDataFetchJobs.get(jobId);
if (!job) {
throw new HttpError('Job not found', 404);
}
return res.status(200).json({
success: true,
job: {
jobId: job.jobId,
status: job.status,
startedAt: job.startedAt,
updatedAt: job.updatedAt,
finishedAt: job.finishedAt,
clubTeamId: job.clubTeamId,
result: job.result,
error: job.error
}
});
} catch (error) {
next(error);
}
}
/**
* Parse myTischtennis URL and return configuration data
* POST /api/mytischtennis/parse-url

View File

@@ -64,6 +64,12 @@ router.post('/configure-league', myTischtennisUrlController.configureLeague);
// POST /api/mytischtennis/fetch-team-data - Manually fetch team data
router.post('/fetch-team-data', myTischtennisUrlController.fetchTeamData);
// POST /api/mytischtennis/fetch-team-data/async - Start async manual fetch
router.post('/fetch-team-data/async', myTischtennisUrlController.startFetchTeamDataJob);
// GET /api/mytischtennis/fetch-team-data/jobs/:jobId - Get async fetch job status
router.get('/fetch-team-data/jobs/:jobId', myTischtennisUrlController.getFetchTeamDataJobStatus);
// GET /api/mytischtennis/team-url/:teamId - Get myTischtennis URL for team
router.get('/team-url/:teamId', myTischtennisUrlController.getTeamUrl);