Add NPC creation status tracking and progress reporting in Admin module

- Implemented getNPCsCreationStatus method in AdminController to retrieve the status of NPC creation jobs.
- Enhanced AdminService to manage NPC creation jobs, including job ID generation, progress updates, and error handling.
- Updated frontend CreateNPCView to display progress of NPC creation, including estimated time remaining and job status.
- Added localization strings for progress reporting in both German and English.
- Improved overall user experience by providing real-time feedback during NPC creation processes.
This commit is contained in:
Torsten Schulz (local)
2026-01-07 17:09:54 +01:00
parent b34dcac685
commit c322eb1e5a
8 changed files with 347 additions and 25 deletions

View File

@@ -26,6 +26,8 @@ import Room from '../models/chat/room.js';
import UserParam from '../models/community/user_param.js';
import TitleOfNobility from "../models/falukant/type/title_of_nobility.js";
import { sequelize } from '../utils/sequelize.js';
import npcCreationJobService from './npcCreationJobService.js';
import { v4 as uuidv4 } from 'uuid';
class AdminService {
async hasUserAccess(userId, section) {
@@ -1113,7 +1115,7 @@ class AdminService {
count // Anzahl der zu erstellenden NPCs
} = options;
// Hole alle Städte, wenn keine spezifischen Regionen angegeben
// Berechne zuerst die Gesamtanzahl, um den Job richtig zu initialisieren
let targetRegions = [];
if (regionIds && regionIds.length > 0) {
targetRegions = await RegionData.findAll({
@@ -1140,7 +1142,6 @@ class AdminService {
throw new Error('No cities found');
}
// Hole alle Titles im Bereich
const titles = await TitleOfNobility.findAll({
where: {
id: {
@@ -1154,13 +1155,56 @@ class AdminService {
throw new Error('No titles found in specified range');
}
const genders = ['male', 'female'];
const createdNPCs = [];
const totalNPCs = targetRegions.length * titles.length * count;
// Erstelle NPCs in einer Transaktion
// Für jede Stadt-Titel-Kombination wird die angegebene Anzahl erstellt
await sequelize.transaction(async (t) => {
// Erstelle Job-ID
const jobId = uuidv4();
npcCreationJobService.createJob(userId, jobId);
npcCreationJobService.updateProgress(jobId, 0, totalNPCs);
npcCreationJobService.setStatus(jobId, 'running');
// Starte asynchronen Prozess
this._createNPCsAsync(jobId, userId, {
regionIds,
minAge,
maxAge,
minTitleId,
maxTitleId,
count,
targetRegions,
titles
}).catch(error => {
console.error('Error in _createNPCsAsync:', error);
const errorMessage = error?.message || error?.toString() || 'Unknown error occurred';
npcCreationJobService.setError(jobId, errorMessage);
});
return { jobId };
}
async _createNPCsAsync(jobId, userId, options) {
try {
const {
regionIds,
minAge,
maxAge,
minTitleId,
maxTitleId,
count,
targetRegions,
titles
} = options;
const genders = ['male', 'female'];
const createdNPCs = [];
const totalNPCs = targetRegions.length * titles.length * count;
let currentNPC = 0;
console.log(`[NPC Creation Job ${jobId}] Starting creation of ${totalNPCs} NPCs`);
// Erstelle NPCs in einer Transaktion
// Für jede Stadt-Titel-Kombination wird die angegebene Anzahl erstellt
await sequelize.transaction(async (t) => {
for (const region of targetRegions) {
for (const title of titles) {
// Erstelle 'count' NPCs für diese Stadt-Titel-Kombination
@@ -1218,18 +1262,40 @@ class AdminService {
region: region.name,
title: title.labelTr
});
// Update Progress
currentNPC++;
npcCreationJobService.updateProgress(jobId, currentNPC, totalNPCs);
}
}
}
});
});
return {
success: true,
count: createdNPCs.length,
countPerCombination: count,
totalCombinations: targetRegions.length * titles.length,
npcs: createdNPCs
};
console.log(`[NPC Creation Job ${jobId}] Completed: ${createdNPCs.length} NPCs created`);
// Job abschließen
npcCreationJobService.setResult(jobId, {
success: true,
count: createdNPCs.length,
countPerCombination: count,
totalCombinations: targetRegions.length * titles.length,
npcs: createdNPCs
});
} catch (error) {
console.error(`[NPC Creation Job ${jobId}] Error:`, error);
throw error; // Re-throw für den catch-Block in createNPCs
}
}
getNPCsCreationStatus(userId, jobId) {
const job = npcCreationJobService.getJob(jobId);
if (!job) {
throw new Error('Job not found');
}
if (job.userId !== userId) {
throw new Error('Access denied');
}
return job;
}
}

View File

@@ -0,0 +1,86 @@
// In-Memory Job-Status-Service für NPC-Erstellung
// Für Produktion sollte man Redis oder eine Datenbank verwenden
const jobs = new Map();
class NPCCreationJobService {
createJob(userId, jobId) {
jobs.set(jobId, {
userId,
status: 'pending',
progress: 0,
total: 0,
current: 0,
startTime: Date.now(),
estimatedTimeRemaining: null,
error: null,
result: null
});
return jobId;
}
getJob(jobId) {
return jobs.get(jobId);
}
updateProgress(jobId, current, total) {
const job = jobs.get(jobId);
if (!job) return;
job.current = current;
job.total = total;
job.progress = total > 0 ? Math.round((current / total) * 100) : 0;
// Berechne verbleibende Zeit basierend auf bisheriger Geschwindigkeit
if (current > 0 && job.progress < 100) {
const elapsed = Date.now() - job.startTime;
const avgTimePerItem = elapsed / current;
const remaining = total - current;
job.estimatedTimeRemaining = Math.round(remaining * avgTimePerItem);
}
}
setStatus(jobId, status) {
const job = jobs.get(jobId);
if (!job) return;
job.status = status;
}
setError(jobId, error) {
const job = jobs.get(jobId);
if (!job) return;
job.status = 'error';
job.error = error;
}
setResult(jobId, result) {
const job = jobs.get(jobId);
if (!job) return;
job.status = 'completed';
job.result = result;
job.progress = 100;
job.estimatedTimeRemaining = 0;
}
deleteJob(jobId) {
jobs.delete(jobId);
}
// Cleanup alte Jobs (älter als 1 Stunde)
cleanupOldJobs() {
const oneHourAgo = Date.now() - (60 * 60 * 1000);
for (const [jobId, job] of jobs.entries()) {
if (job.startTime < oneHourAgo) {
jobs.delete(jobId);
}
}
}
}
// Cleanup alle 10 Minuten
setInterval(() => {
const service = new NPCCreationJobService();
service.cleanupOldJobs();
}, 10 * 60 * 1000);
export default new NPCCreationJobService();