feat: rules midgame priority system, mission enhancements, and requisition shop improvements

- Add midgame priority ranking for rules (midgamePriority field)
- Enhance rules search to support midgame filtering and sorting
- Improve mission simulation and midgame tracking
- Fix RequisitionShop player name handling
- Add rules midplay simulation tests
- Update MissionTab with midgame scene tracking
- Add backend logging for rules updates
This commit is contained in:
2026-06-24 12:09:25 +02:00
parent 7d038dd942
commit 7873f450da
18 changed files with 7418 additions and 2453 deletions

View File

@@ -12956,3 +12956,168 @@ Connected to MongoDB
[2026-06-23T13:17:14.755Z] MariaDB: Error getting all weapons {"message":"Table 'deathwatch.weapons' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM weapons ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.weapons' doesn't exist"}
[2026-06-23T13:17:14.768Z] MariaDB: Error getting all bestiary {"message":"Table 'deathwatch.bestiary' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM bestiary ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.bestiary' doesn't exist"}
[2026-06-23T13:33:33.058Z] MariaDB: Tables created successfully
[2026-06-23T21:19:35.818Z] SESSION: Session validation successful gm
[2026-06-23T21:19:35.872Z] API: Fetch player gm success
[2026-06-23T21:19:45.935Z] MariaDB: Updated mission progress 9
[2026-06-23T21:19:52.803Z] MariaDB: Updated mission progress 9
[2026-06-23T21:23:05.809Z] Session deleted: session_gm_1782248674070_qhcsas
[2026-06-23T21:23:05.811Z] SESSION: Logout session_gm_1782248674070_qhcsas
[2026-06-23T21:23:09.669Z] Session created: session_anders_1782249789618_j7t7ao
[2026-06-23T21:23:09.671Z] API: Player login anders success
[2026-06-23T21:23:09.694Z] API: Fetch player anders success
[2026-06-23T21:23:09.699Z] SESSION: Session validation successful anders
[2026-06-23T21:23:09.718Z] API: Fetch player anders success
[2026-06-23T21:31:19.816Z] SESSION: Session validation successful anders
[2026-06-23T21:31:19.842Z] API: Fetch player anders success
[2026-06-23T21:42:44.161Z] SESSION: Session validation successful anders
[2026-06-23T21:42:44.190Z] API: Fetch player anders success
[2026-06-23T21:43:01.779Z] MariaDB: Updated mission progress 9
[2026-06-23T21:43:02.800Z] MariaDB: Updated mission progress 9
[2026-06-23T21:43:26.760Z] MariaDB: Updated mission progress 9
[2026-06-23T21:43:28.139Z] MariaDB: Updated mission progress 9
[2026-06-23T21:51:15.547Z] MariaDB: Error creating tables {"code":"ER_ACCESS_DENIED_ERROR","errno":1045,"sqlState":"28000","sqlMessage":"Access denied for user 'deathwatch'@'unifi' (using password: YES)"}
[2026-06-23T21:51:24.282Z] API: Fetch all players (public) 5
[2026-06-23T21:51:29.207Z] API: Fetch player anders success
[2026-06-23T21:51:34.975Z] API: Fetch player anders success
[2026-06-23T21:51:35.043Z] API: Fetch player christoffer success
[2026-06-23T21:51:35.092Z] API: Fetch player claes success
[2026-06-23T21:51:35.143Z] API: Fetch player phillip success
[2026-06-23T22:13:55.577Z] API: Fetch all players (public) 5
[2026-06-23T22:28:05.676Z] API: Fetch all players (public) 5
[2026-06-23T22:28:05.682Z] SESSION: Session validation successful anders
[2026-06-23T22:28:05.707Z] API: Fetch player anders success
[2026-06-23T22:28:24.067Z] API: Fetch all players (public) 5
[2026-06-23T22:28:24.073Z] SESSION: Session validation successful anders
[2026-06-23T22:28:24.096Z] API: Fetch player anders success
[2026-06-23T22:28:26.641Z] Session deleted: session_anders_1782249789618_j7t7ao
[2026-06-23T22:28:26.643Z] SESSION: Logout session_anders_1782249789618_j7t7ao
[2026-06-23T22:28:50.408Z] Session created: session_gm_1782253730353_zuylf6
[2026-06-23T22:28:50.410Z] API: Player login gm success
[2026-06-23T22:28:50.438Z] API: Fetch player gm success
[2026-06-23T22:28:50.445Z] SESSION: Session validation successful gm
[2026-06-23T22:28:50.472Z] API: Fetch player gm success
[2026-06-23T22:29:21.031Z] MariaDB: Updated mission progress 9
[2026-06-23T22:29:29.169Z] MariaDB: Error getting all bestiary {"message":"Table 'deathwatch.bestiary' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM bestiary ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.bestiary' doesn't exist"}
[2026-06-23T22:29:29.171Z] MariaDB: Error getting all weapons {"message":"Table 'deathwatch.weapons' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM weapons ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.weapons' doesn't exist"}
[2026-06-23T22:30:35.715Z] API: Fetch all players (public) 5
[2026-06-23T22:30:40.608Z] API: Fetch player anders success
[2026-06-23T22:30:48.559Z] API: Fetch all players (public) 5
[2026-06-23T22:30:52.186Z] API: Fetch player anders success
[2026-06-23T22:31:04.535Z] API: Fetch player anders success
[2026-06-23T22:31:04.599Z] API: Fetch player christoffer success
[2026-06-23T22:31:04.689Z] API: Fetch player claes success
[2026-06-23T22:31:04.754Z] API: Fetch player phillip success
[2026-06-23T22:37:40.170Z] MariaDB: Tables created successfully
[2026-06-23T22:37:44.798Z] API: Fetch player anders success
[2026-06-23T22:37:50.139Z] API: Fetch player christoffer success
[2026-06-23T22:40:12.754Z] API: Fetch all players (public) 5
[2026-06-23T22:40:17.142Z] API: Fetch player anders success
[2026-06-23T22:42:05.342Z] API: Fetch player anders success
[2026-06-23T22:42:05.417Z] API: Fetch player christoffer success
[2026-06-23T22:42:05.507Z] API: Fetch player claes success
[2026-06-23T22:42:05.587Z] API: Fetch player phillip success
[2026-06-23T22:51:32.054Z] API: Fetch player anders success
[2026-06-23T22:51:41.076Z] API: Fetch player anders success
[2026-06-23T22:51:41.132Z] API: Fetch player christoffer success
[2026-06-23T22:51:41.186Z] API: Fetch player claes success
[2026-06-23T22:51:41.247Z] API: Fetch player phillip success
[2026-06-24T08:13:37.788Z] MariaDB: Error creating tables {"code":"ER_ACCESS_DENIED_ERROR","errno":1045,"sqlState":"28000","sqlMessage":"Access denied for user 'deathwatch'@'unifi' (using password: YES)"}
[2026-06-24T08:22:57.268Z] API: Fetch all players (public) 5
[2026-06-24T08:23:16.201Z] MariaDB: Updated mission progress 9
[2026-06-24T08:23:16.425Z] MariaDB: Updated mission progress 9
[2026-06-24T08:25:06.391Z] MariaDB: Tables created successfully
[2026-06-24T08:25:34.126Z] MariaDB: Tables created successfully
[2026-06-24T08:26:39.167Z] MariaDB: Tables created successfully
[2026-06-24T08:27:43.791Z] MariaDB: Tables created successfully
[2026-06-24T08:30:55.243Z] MariaDB: Tables created successfully
[2026-06-24T08:31:05.243Z] Simulation #1 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T08:31:12.631Z] Simulation #2 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T08:31:12.901Z] Simulation #3 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
[2026-06-24T08:31:49.070Z] API: Fetch all players (public) 5
[2026-06-24T08:31:49.074Z] SESSION: Session validation successful gm
[2026-06-24T08:31:49.208Z] API: Fetch player gm success
[2026-06-24T08:31:50.273Z] API: Fetch all players (public) 5
[2026-06-24T08:32:40.599Z] API: Fetch all players (public) 5
[2026-06-24T08:33:05.674Z] API: Fetch player anders success
[2026-06-24T08:33:25.244Z] API: Fetch all players (public) 5
[2026-06-24T08:33:27.960Z] MariaDB: Created player andreas
[2026-06-24T08:33:27.961Z] API: Created player andreas success
[2026-06-24T08:33:32.571Z] API: Fetch all players (public) 6
[2026-06-24T08:33:56.731Z] API: Fetch player andreas success
[2026-06-24T08:34:05.777Z] MariaDB: Updated player andreas
[2026-06-24T08:34:05.779Z] API: Updated player andreas success
[2026-06-24T08:34:12.700Z] API: Fetch player andreas success
[2026-06-24T08:35:01.308Z] API: Fetch all players (public) 6
[2026-06-24T08:35:10.183Z] Simulation #4 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T08:35:10.211Z] API: Fetch all players (public) 6
[2026-06-24T08:35:21.222Z] Simulation #5 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T08:35:21.254Z] API: Fetch all players (public) 6
[2026-06-24T08:38:11.460Z] MariaDB: Updated player andreas
[2026-06-24T08:38:11.464Z] API: Updated player andreas success
[2026-06-24T08:38:20.757Z] API: Fetch player andreas success
[2026-06-24T08:47:21.904Z] MariaDB: Updated player anders
[2026-06-24T08:47:21.910Z] API: Updated player anders success
[2026-06-24T08:47:35.419Z] MariaDB: Updated player andreas
[2026-06-24T08:47:35.421Z] API: Updated player andreas success
[2026-06-24T08:47:53.448Z] MariaDB: Updated player christoffer
[2026-06-24T08:47:53.450Z] API: Updated player christoffer success
[2026-06-24T08:48:09.440Z] MariaDB: Updated player claes
[2026-06-24T08:48:09.442Z] API: Updated player claes success
[2026-06-24T08:48:29.596Z] MariaDB: Updated player phillip
[2026-06-24T08:48:29.598Z] API: Updated player phillip success
[2026-06-24T08:48:36.934Z] API: Fetch player anders success
[2026-06-24T08:48:36.980Z] API: Fetch player andreas success
[2026-06-24T08:48:37.026Z] API: Fetch player christoffer success
[2026-06-24T08:48:37.083Z] API: Fetch player claes success
[2026-06-24T08:48:37.144Z] API: Fetch player phillip success
[2026-06-24T08:50:32.259Z] MariaDB: Updated player andreas
[2026-06-24T08:50:32.263Z] API: Updated player andreas success
[2026-06-24T08:50:41.668Z] API: Fetch player andreas success
[2026-06-24T08:51:03.555Z] MariaDB: Updated player andreas
[2026-06-24T08:51:03.556Z] API: Updated player andreas success
[2026-06-24T08:51:09.123Z] API: Fetch player andreas success
[2026-06-24T08:52:00.746Z] Simulation #6 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T08:52:10.863Z] Simulation #7 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T08:55:01.129Z] API: Fetch all players (public) 6
[2026-06-24T08:55:01.154Z] SESSION: Session validation successful gm
[2026-06-24T08:55:01.179Z] API: Fetch player gm success
[2026-06-24T08:56:18.671Z] API: Fetch all players (public) 6
[2026-06-24T08:56:22.973Z] Simulation #8 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T08:56:23.004Z] API: Fetch all players (public) 6
[2026-06-24T08:56:45.435Z] Simulation #9 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T08:56:45.467Z] API: Fetch all players (public) 6
[2026-06-24T08:56:52.641Z] Simulation #10 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T08:56:52.675Z] API: Fetch all players (public) 6
[2026-06-24T09:00:12.185Z] API: Fetch all players (public) 6
[2026-06-24T09:00:22.010Z] MariaDB: Updated mission progress 9
[2026-06-24T09:02:33.620Z] MariaDB: Tables created successfully
[2026-06-24T09:03:16.974Z] Simulation #11 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
[2026-06-24T09:03:53.171Z] MariaDB: Tables created successfully
[2026-06-24T09:03:59.717Z] Simulation #12 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T09:04:00.015Z] Simulation #13 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T09:04:00.514Z] Simulation #14 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
[2026-06-24T09:04:00.826Z] Simulation #15 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
[2026-06-24T09:04:01.082Z] Simulation #16 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
[2026-06-24T09:04:33.097Z] API: Fetch all players (public) 6
[2026-06-24T09:04:33.132Z] SESSION: Session validation successful gm
[2026-06-24T09:04:33.159Z] API: Fetch player gm success
[2026-06-24T09:04:34.550Z] API: Fetch all players (public) 6
[2026-06-24T09:04:43.038Z] Simulation #17 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T09:04:43.072Z] API: Fetch all players (public) 6
[2026-06-24T09:04:48.507Z] Simulation #18 completed: The Hunt for Fabius Bile — VICTORY
[2026-06-24T09:04:48.538Z] API: Fetch all players (public) 6
[2026-06-24T09:09:43.564Z] API: Fetch all players (public) 6
[2026-06-24T09:09:51.120Z] SESSION: Session validation successful gm
[2026-06-24T09:09:51.130Z] API: Fetch all players (public) 6
[2026-06-24T09:09:51.144Z] API: Fetch player gm success
[2026-06-24T09:09:51.795Z] API: Fetch all players (public) 6
[2026-06-24T09:09:51.798Z] SESSION: Session validation successful gm
[2026-06-24T09:09:51.821Z] API: Fetch player gm success
[2026-06-24T09:09:51.976Z] API: Fetch all players (public) 6
[2026-06-24T09:09:51.987Z] SESSION: Session validation successful gm
[2026-06-24T09:09:52.010Z] API: Fetch player gm success
[2026-06-24T09:16:50.410Z] API: Fetch all players (public) 6
[2026-06-24T09:16:50.423Z] SESSION: Session validation successful gm
[2026-06-24T09:16:50.443Z] API: Fetch player gm success
[2026-06-24T09:17:13.311Z] MariaDB: Updated mission progress 9
[2026-06-24T10:07:25.442Z] API: Fetch all players (public) 6

View File

@@ -6,6 +6,19 @@ const router = express.Router();
console.log('Rules routes registered (MariaDB)');
const rulesJsonPath = path.join(__dirname, '..', 'rules', 'rules-database.json');
function parseMaybeJson(value, fallback = []) {
if (Array.isArray(value)) return value;
if (!value) return fallback;
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : fallback;
} catch (e) {
return fallback;
}
}
// Helpers to clean up OCR/extracted text
function cleanText(s) {
if (!s) return s;
@@ -32,45 +45,75 @@ function cleanTitle(t) {
return s;
}
// Fetch all rules from MariaDB
function normalizeRule(r) {
const ruleId = r.rule_id || r.ruleId || r.id;
return {
id: ruleId || r.id,
numericId: Number.isInteger(r.id) ? r.id : undefined,
rule_id: ruleId,
title: cleanTitle(r.title),
summary: cleanText(r.summary || ''),
content: cleanText(r.content),
page: r.page,
pageEnd: r.page_end || r.pageEnd || r.page,
source: r.source,
sourceAbbr: r.source_abbr || r.sourceAbbr,
category: r.category,
tags: parseMaybeJson(r.tags),
aliases: parseMaybeJson(r.aliases),
relatedRules: parseMaybeJson(r.related_rules || r.relatedRules),
sourceMethod: r.source_method || r.sourceMethod || 'unknown',
confidence: r.confidence == null ? null : Number(r.confidence),
midgamePriority: r.midgame_priority || r.midgamePriority || 0
};
}
function getRulesFromJson() {
try {
if (!fs.existsSync(rulesJsonPath)) return [];
const data = JSON.parse(fs.readFileSync(rulesJsonPath, 'utf8'));
return (data.rules || []).map(normalizeRule);
} catch (e) {
console.error('Failed to read rules JSON fallback:', e);
logToFile('Error reading rules JSON fallback:', e);
return [];
}
}
function mergeRules(dbRules, jsonRules) {
const byId = new Map();
for (const rule of dbRules || []) {
byId.set(String(rule.rule_id || rule.id), rule);
}
for (const rule of jsonRules || []) {
byId.set(String(rule.rule_id || rule.id), rule);
}
return Array.from(byId.values());
}
// Fetch all rules from MariaDB and merge in the generated JSON midgame database.
async function getAllRules() {
try {
const rows = await rulesHelpers.getAll();
return rows.map(r => ({
id: r.id,
rule_id: r.rule_id,
title: cleanTitle(r.title),
content: cleanText(r.content),
page: r.page,
source: r.source,
sourceAbbr: r.source_abbr,
category: r.category
}));
return mergeRules(rows && rows.length ? rows.map(normalizeRule) : [], getRulesFromJson());
} catch (e) {
console.error('Failed to read rules from MariaDB:', e);
logToFile('Error getting all rules:', e);
return [];
return getRulesFromJson();
}
}
async function getRuleById(ruleId) {
try {
const rows = await rulesHelpers.getAll();
const row = rows.find(r => r.rule_id === ruleId || r.id === ruleId);
const sourceRows = mergeRules(rows && rows.length ? rows.map(normalizeRule) : [], getRulesFromJson());
const row = sourceRows.find(r => String(r.rule_id) === String(ruleId) || String(r.id) === String(ruleId) || String(r.numericId) === String(ruleId));
if (!row) return null;
return {
id: row.rule_id || row.id,
title: cleanTitle(row.title),
content: cleanText(row.content),
page: row.page,
source: row.source,
sourceAbbr: row.source_abbr,
category: row.category
};
return row;
} catch (e) {
console.error('Failed to read rule by id:', e);
logToFile('Error getting rule by id:', ruleId, e);
return null;
return getRulesFromJson().find(r => String(r.rule_id) === String(ruleId) || String(r.id) === String(ruleId)) || null;
}
}
@@ -79,7 +122,9 @@ router.get('/categories', async (req, res) => {
try {
const rows = await getAllRules();
const categories = [...new Set(rows.map(r => r.category).filter(Boolean))];
const categoryList = categories.map(cat => ({ id: cat, name: cleanTitle(cat) }));
const categoryList = categories
.sort((a, b) => a.localeCompare(b))
.map(cat => ({ id: cat, name: cleanTitle(cat.replace(/_/g, ' ')) }));
res.json([{ id: 'all', name: 'All Rules' }, ...categoryList]);
} catch (error) {
console.error('Categories error:', error);
@@ -90,41 +135,82 @@ router.get('/categories', async (req, res) => {
// Search rules (MariaDB)
router.get('/search', async (req, res) => {
try {
const { q: query, category, limit = 20 } = req.query;
const { q: query, category, tag, source, midgame, limit = 20 } = req.query;
if (!query || !query.trim()) return res.json([]);
const limitInt = Math.max(1, parseInt(limit) || 20);
const allRules = await getAllRules();
const term = query.toLowerCase();
const term = query.trim().toLowerCase();
const tokens = term.split(/\s+/).filter(Boolean);
const includesTerm = (value, t = term) => String(value || '').toLowerCase().includes(t);
const arrayIncludes = (arr, t = term) => (arr || []).some(v => includesTerm(v, t));
const tokenMatch = (rule) => {
const haystacks = [
rule.title,
rule.summary,
rule.content,
rule.category,
...(rule.tags || []),
...(rule.aliases || [])
].map(v => String(v || '').toLowerCase());
return tokens.every(token => haystacks.some(v => v.includes(token)));
};
const score = (rule, t) => {
const title = (rule.title || '').toLowerCase();
const summary = (rule.summary || '').toLowerCase();
const content = (rule.content || '').toLowerCase();
const tags = (rule.tags || []).map(v => String(v).toLowerCase());
const aliases = (rule.aliases || []).map(v => String(v).toLowerCase());
const exactTitle = title === t;
const titleStarts = title.startsWith(t);
const titleContains = title.includes(t);
const exactAlias = aliases.includes(t);
const aliasContains = aliases.some(a => a.includes(t));
const tagContains = tags.some(a => a.includes(t));
const summaryContains = summary.includes(t);
const isCanonical = rule.source === 'Core Rulebook';
const tokenHits = tokens.reduce((sum, token) => {
return sum +
(title.includes(token) ? 8 : 0) +
(aliases.some(a => a.includes(token)) ? 6 : 0) +
(tags.some(a => a.includes(token)) ? 4 : 0) +
(summary.includes(token) ? 3 : 0) +
(content.includes(token) ? 1 : 0);
}, 0);
// Higher = shown first
return (exactTitle ? 100 : 0) +
(exactAlias ? 90 : 0) +
(titleStarts ? 50 : 0) +
(titleContains ? 20 : 0) +
(aliasContains ? 18 : 0) +
(tagContains ? 15 : 0) +
(summaryContains ? 8 : 0) +
(isCanonical ? 10 : 0) +
(Number(rule.midgamePriority) || 0) +
tokenHits +
(content.includes(t) ? 1 : 0);
};
let filtered = allRules.filter(rule => {
const titleMatch = rule.title && rule.title.toLowerCase().includes(term);
const contentMatch = rule.content && rule.content.toLowerCase().includes(term);
const titleMatch = includesTerm(rule.title);
const summaryMatch = includesTerm(rule.summary);
const contentMatch = includesTerm(rule.content);
const tagMatch = arrayIncludes(rule.tags);
const aliasMatch = arrayIncludes(rule.aliases);
const categoryMatch = !category || category === 'all' || rule.category === category;
return (titleMatch || contentMatch) && categoryMatch;
const sourceMatch = !source || source === 'all' || rule.sourceAbbr === source || rule.source === source;
const midgameMatch = !midgame || midgame === 'false' || Number(rule.midgamePriority || 0) >= 70;
const explicitTagMatch = !tag || tag === 'all' || (rule.tags || []).some(t => String(t).toLowerCase() === String(tag).toLowerCase());
return (titleMatch || summaryMatch || contentMatch || tagMatch || aliasMatch || tokenMatch(rule)) && categoryMatch && sourceMatch && midgameMatch && explicitTagMatch;
});
// If category filtering yielded no results, try without category filter
if (filtered.length === 0 && category && category !== 'all') {
filtered = allRules.filter(rule => {
const titleMatch = rule.title && rule.title.toLowerCase().includes(term);
const contentMatch = rule.content && rule.content.toLowerCase().includes(term);
return titleMatch || contentMatch;
const sourceMatch = !source || source === 'all' || rule.sourceAbbr === source || rule.source === source;
return sourceMatch && (includesTerm(rule.title) || includesTerm(rule.summary) || includesTerm(rule.content) || arrayIncludes(rule.tags) || arrayIncludes(rule.aliases) || tokenMatch(rule));
});
}
@@ -157,7 +243,7 @@ router.get('/rule/:id', async (req, res) => {
// Get random rules for discovery
router.get('/random', async (req, res) => {
try {
const { count = 5, category } = req.query;
const { count = 5, category, midgame } = req.query;
const max = Math.max(1, parseInt(count) || 5);
const allRules = await getAllRules();
@@ -166,6 +252,9 @@ router.get('/random', async (req, res) => {
if (category && category !== 'all') {
filtered = allRules.filter(rule => rule.category === category);
}
if (midgame && midgame !== 'false') {
filtered = filtered.filter(rule => Number(rule.midgamePriority || 0) >= 70);
}
// Shuffle and pick random rules
const shuffled = filtered.sort(() => 0.5 - Math.random());
@@ -186,6 +275,8 @@ router.get('/stats', async (req, res) => {
const categoryCount = {};
const sourceCount = {};
const methodCount = {};
let midgameRules = 0;
allRules.forEach(rule => {
if (rule.category) {
@@ -194,12 +285,17 @@ router.get('/stats', async (req, res) => {
if (rule.source) {
sourceCount[rule.source] = (sourceCount[rule.source] || 0) + 1;
}
if (rule.sourceMethod) {
methodCount[rule.sourceMethod] = (methodCount[rule.sourceMethod] || 0) + 1;
}
if (Number(rule.midgamePriority || 0) >= 70) midgameRules += 1;
});
const categories = Object.entries(categoryCount).map(([category, c]) => ({ category, c }));
const sources = Object.entries(sourceCount).map(([source, c]) => ({ source, c }));
const sourceMethods = Object.entries(methodCount).map(([sourceMethod, c]) => ({ sourceMethod, c }));
res.json({ totalRules, categories, sources, searchTerms: 0 });
res.json({ totalRules, midgameRules, categories, sources, sourceMethods, searchTerms: 0 });
} catch (error) {
console.error('Stats error:', error);
res.status(500).json({ error: 'Failed to get stats' });

File diff suppressed because one or more lines are too long