Files
Lauri Ojansivu d9cf523f9e Admin Panel / Problems: Data health, a nightly scan of broken references, with repairs.
The scan counts what points at nothing: cards on a missing or another board's
list or swimlane, ghost and duplicate board members, boards with no active
admin, custom field values and boards that no longer exist, checklist items
without a checklist, and subtasks or linked cards whose card is gone. It runs
nightly when the CPU is low, claimed so that only one pod scans or repairs.

A repair that only moves data reuses the upgrade steps; one that deletes asks
twice, and the server refuses it without that confirmation. Every repair edits
with $pull or a guarded update, so a concurrent edit is not overwritten, and a
member dedupe keeps the admin entry. Legacy scalar boardId and string boardIds
of custom fields are understood.

Translations done with low confidence: ay, csb, lld, kok, wa-RR, ve-CC, bi, tpi.

Thanks to Raymond-Voogt and xet7 !

Fixes #6818.
2026-10-27 17:08:15 +02:00

45 lines
1.6 KiB
JavaScript

import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import { ReactiveCache } from '/imports/reactiveCache';
import { startDataHealthScan, repairDataHealthCheck } from '/server/lib/dataHealthScan';
const { CHECKS } = require('/models/lib/dataHealth');
// Admin Panel → Problems → Summary: Start (Data health) and the Repair button behind each
// "Data health: ..." problem (models/lib/dataHealth.js says which checks have a
// repair and which ask for confirmation first). A repair that deletes data
// ('confirm') runs only when the call says confirmed === true - the page sends
// that on the second click - so a bare call, or an old page, cannot delete.
async function requireAdmin() {
const user = await ReactiveCache.getCurrentUser();
if (!user || !user.isAdmin) {
throw new Meteor.Error('not-authorized', 'Admin only');
}
return user;
}
Meteor.methods({
async dataHealthScanNow() {
await requireAdmin();
startDataHealthScan();
return { started: true };
},
async repairDataHealth(checkId, confirmed) {
check(checkId, String);
check(confirmed, Match.Optional(Boolean));
await requireAdmin();
const kind = (CHECKS.find(c => c.id === checkId) || {}).repair;
if (kind === 'confirm' && confirmed !== true) {
throw new Meteor.Error('data-health-confirm', 'This repair deletes data: confirm it first.');
}
this.unblock();
try {
return await repairDataHealthCheck(checkId, { confirmed: confirmed === true });
} catch (error) {
throw new Meteor.Error('data-health-repair', (error && error.message) || String(error));
}
},
});