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

82 lines
3.6 KiB
JavaScript

import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import os from 'os';
import { ReactiveCache } from '/imports/reactiveCache';
import {
getProblemsOverview,
getInProgress,
getLoginProblems,
} from '/server/lib/systemStatus';
import { getCurrentCpu } from '/server/lib/cpuMonitor';
import { getCurrentMemory } from '/server/lib/ramMonitor';
import { getCurrentDisks } from '/server/lib/diskMonitor';
import { dataHealthNow } from '/server/lib/dataHealthScan';
// Admin-gated methods behind the Admin Panel / Problems "Status" overview. The
// same data is available without the Admin Panel via `snap run wekan.problems`.
async function requireAdmin() {
const user = await ReactiveCache.getCurrentUser();
if (!user || !user.isAdmin) {
throw new Meteor.Error('not-authorized', 'Admin only');
}
return user;
}
// Problem-specific detail, mirroring the `snap run wekan.problems <area>`
// sub-commands. Kept in one place so the Admin Panel and the snap command agree.
function cpuDetail() {
const cpus = os.cpus() || [];
const mem = process.memoryUsage();
return {
area: 'cpu',
title: 'CPU usage',
// The live figure shown above the search box on the CPU usage page. Same
// sample the CPU monitor writes its event rows from, so the header and the
// rows below it never disagree.
current: getCurrentCpu(),
loadAverage: os.loadavg(), // 1 / 5 / 15 min
cpuCount: cpus.length,
cpuModel: cpus.length ? cpus[0].model : '',
uptimeSeconds: Math.round(process.uptime()),
rssBytes: mem.rss,
heapUsedBytes: mem.heapUsed,
note: 'Load average is machine-wide (1/5/15 min). rss/heap are the WeKan server process. Sustained load near or above the CPU count means the machine is saturated. If `top` shows the ferretdb process pegged near 100%/200% for a long time, the FerretDB v1 (SQLite) backend is saturated under WeKan\'s poll load (symptoms: pages take minutes, counts stuck at 0, raw i18n keys). Mitigation: run on MongoDB (snap: `snap set wekan database=mongodb`) or migrate text data across, and check for a migration/repair still running.',
};
}
Meteor.methods({
// The full Problems "Status" overview: everything in progress + problems found.
async systemStatusReport() {
await requireAdmin();
const [overview, loginProblems, dataHealth] = await Promise.all([
getProblemsOverview(),
getLoginProblems(),
dataHealthNow(),
]);
return { overview, loginProblems, dataHealth };
},
// Just the in-progress migrations/repairs (cheap; polled by the Status page).
async migrationsInProgress() {
await requireAdmin();
return getInProgress();
},
// Detail for one problem area ('cpu', 'ram', 'disk'); mirrors the snap sub-command.
// `area` MUST be check()ed: Meteor runs with audit-argument-checks, so a method
// that receives an argument it never checks throws "Did not check() all arguments"
// on every call. Nothing passed an argument here until the CPU usage page started
// polling problemDetailReport('cpu'), which then failed every 5 seconds.
async problemDetailReport(area) {
check(area, Match.OneOf(String, null, undefined));
await requireAdmin();
if (area === 'cpu') return cpuDetail();
if (area === 'ram') return { area: 'ram', current: getCurrentMemory() };
if (area === 'disk') return { area: 'disk', current: getCurrentDisks() };
if (area === 'login') return { area: 'login', checks: await getLoginProblems() };
if (area === 'status' || !area) return getProblemsOverview();
throw new Meteor.Error('unknown-area', `Unknown problem area: ${area}`);
},
});