diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js
index 2b722d9..9329e7a 100644
--- a/backend/controllers/adminController.js
+++ b/backend/controllers/adminController.js
@@ -45,6 +45,7 @@ class AdminController {
this.deleteRegionDistance = this.deleteRegionDistance.bind(this);
this.createNPCs = this.createNPCs.bind(this);
this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this);
+ this.getNPCsCreationStatus = this.getNPCsCreationStatus.bind(this);
}
async getOpenInterests(req, res) {
@@ -393,6 +394,7 @@ class AdminController {
if (countValue < 1 || countValue > 500) {
return res.status(400).json({ error: 'Count must be between 1 and 500' });
}
+ console.log('[createNPCs] Request received:', { userId, regionIds, minAge, maxAge, minTitleId, maxTitleId, count: countValue });
const result = await AdminService.createNPCs(userId, {
regionIds: regionIds && regionIds.length > 0 ? regionIds : null,
minAge: parseInt(minAge) || 0,
@@ -401,11 +403,13 @@ class AdminController {
maxTitleId: parseInt(maxTitleId) || 19,
count: countValue
});
+ console.log('[createNPCs] Job created:', result);
res.status(200).json(result);
} catch (error) {
- console.log(error);
+ console.error('[createNPCs] Error:', error);
+ console.error('[createNPCs] Error stack:', error.stack);
const status = error.message === 'noaccess' ? 403 : 500;
- res.status(status).json({ error: error.message });
+ res.status(status).json({ error: error.message || 'Internal server error' });
}
}
@@ -421,6 +425,20 @@ class AdminController {
}
}
+ async getNPCsCreationStatus(req, res) {
+ try {
+ const { userid: userId } = req.headers;
+ const { jobId } = req.params;
+ const status = await AdminService.getNPCsCreationStatus(userId, jobId);
+ res.status(200).json(status);
+ } catch (error) {
+ console.log(error);
+ const status = error.message === 'noaccess' || error.message === 'Access denied' ? 403 :
+ error.message === 'Job not found' ? 404 : 500;
+ res.status(status).json({ error: error.message });
+ }
+ }
+
async getRoomTypes(req, res) {
try {
const userId = req.headers.userid;
diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js
index af99583..6964f8f 100644
--- a/backend/routers/adminRouter.js
+++ b/backend/routers/adminRouter.js
@@ -47,6 +47,7 @@ router.get('/falukant/region-distances', authenticate, adminController.getRegion
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
+router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
// --- Minigames Admin ---
diff --git a/backend/services/adminService.js b/backend/services/adminService.js
index 44e067d..e1f190e 100644
--- a/backend/services/adminService.js
+++ b/backend/services/adminService.js
@@ -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;
}
}
diff --git a/backend/services/npcCreationJobService.js b/backend/services/npcCreationJobService.js
new file mode 100644
index 0000000..d09fb80
--- /dev/null
+++ b/backend/services/npcCreationJobService.js
@@ -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();
diff --git a/frontend/src/components/AppContent.vue b/frontend/src/components/AppContent.vue
index 0981712..901a284 100644
--- a/frontend/src/components/AppContent.vue
+++ b/frontend/src/components/AppContent.vue
@@ -1,6 +1,8 @@
-