2026-07-06 11:26:50 -05:00
|
|
|
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
|
|
|
|
|
import type { Req, Res, Next } from '../types/http';
|
2026-05-03 19:51:57 -05:00
|
|
|
const express = require('express');
|
2026-07-06 14:23:53 -05:00
|
|
|
const router = express.Router();
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const os = require('os');
|
refactor(server): migrate middleware, workers, and db layer to TypeScript (.cts)
- middleware/ (5): requireAuth, securityHeaders, errorFormatter, csrf,
rateLimiter — fully typed against the shared http Req/Res/Next types.
- workers/dailyWorker — fully typed.
- db/ (4): database, subscriptionCatalogSeed, migrations/versionedMigrations,
migrations/legacyReconcileMigrations — large dynamic schema/migration infra,
converted with @ts-nocheck (typing deferred). All requires updated to .cts.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:34:08 -05:00
|
|
|
const { getDb, getSetting } = require('../db/database.cts');
|
2026-07-06 10:47:16 -05:00
|
|
|
const { getStatusRuntime, recordError } = require('../services/statusRuntime.cts');
|
2026-07-06 11:06:05 -05:00
|
|
|
const { listBackups } = require('../services/backupService.cts');
|
2026-07-06 11:00:01 -05:00
|
|
|
const { getScheduleStatus } = require('../services/backupScheduler.cts');
|
2026-07-06 10:47:16 -05:00
|
|
|
const { checkForUpdates } = require('../services/updateCheckService.cts');
|
2026-07-06 11:00:01 -05:00
|
|
|
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts');
|
2026-07-06 10:47:16 -05:00
|
|
|
const { getBankSyncConfig } = require('../services/bankSyncConfigService.cts');
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
|
2026-07-05 13:48:44 -05:00
|
|
|
const { localDateString } = require('../utils/dates.mts');
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
let pkg;
|
2026-07-06 14:23:53 -05:00
|
|
|
try {
|
|
|
|
|
pkg = require('../package.json');
|
|
|
|
|
} catch {
|
|
|
|
|
pkg = { name: 'bill-tracker', version: '1.0.0' };
|
|
|
|
|
}
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
function errorMessage(err) {
|
|
|
|
|
return err?.message || String(err);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 21:42:34 -05:00
|
|
|
// SQLite stores datetime('now') as "YYYY-MM-DD HH:MM:SS" (UTC, no Z).
|
|
|
|
|
// Without the Z suffix, JS Date parses it as LOCAL time, creating timezone
|
|
|
|
|
// inconsistencies when mixed with ISO strings that do have Z.
|
|
|
|
|
// This ensures all timestamps sent to clients are unambiguous UTC ISO strings.
|
|
|
|
|
function toIso(sqliteOrIso) {
|
|
|
|
|
if (!sqliteOrIso) return null;
|
|
|
|
|
const s = String(sqliteOrIso).trim();
|
|
|
|
|
// Already ISO with Z or offset — return as-is
|
|
|
|
|
if (s.includes('T') && (s.endsWith('Z') || s.match(/[+-]\d{2}:\d{2}$/))) return s;
|
|
|
|
|
// SQLite format "YYYY-MM-DD HH:MM:SS" — append Z to declare it UTC
|
|
|
|
|
if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(s)) return s.replace(' ', 'T') + 'Z';
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
function monthRange(now) {
|
|
|
|
|
const year = now.getFullYear();
|
|
|
|
|
const month = now.getMonth() + 1;
|
|
|
|
|
const daysInMonth = new Date(year, month, 0).getDate();
|
|
|
|
|
const monthStr = String(month).padStart(2, '0');
|
|
|
|
|
return {
|
|
|
|
|
year,
|
|
|
|
|
month,
|
|
|
|
|
start: `${year}-${monthStr}-01`,
|
|
|
|
|
end: `${year}-${monthStr}-${String(daysInMonth).padStart(2, '0')}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET /api/status
|
2026-07-06 11:26:50 -05:00
|
|
|
router.get('/', async (req: Req, res: Res) => {
|
2026-05-03 19:51:57 -05:00
|
|
|
const runtimeState = getStatusRuntime();
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
|
|
|
|
|
|
|
|
let db;
|
|
|
|
|
let database = {
|
|
|
|
|
ok: false,
|
|
|
|
|
connected: false,
|
|
|
|
|
status: 'error',
|
|
|
|
|
size_bytes: 0,
|
|
|
|
|
size_mb: '0.00',
|
|
|
|
|
last_modified: null,
|
|
|
|
|
last_error: null,
|
|
|
|
|
};
|
|
|
|
|
let stats = {
|
|
|
|
|
active_sessions: null,
|
|
|
|
|
users: null,
|
|
|
|
|
active_bills: null,
|
|
|
|
|
total_payments: null,
|
|
|
|
|
};
|
|
|
|
|
let notifications = {
|
|
|
|
|
ok: true,
|
|
|
|
|
enabled: false,
|
|
|
|
|
configured: false,
|
|
|
|
|
smtp_configured: false,
|
|
|
|
|
last_test_at: runtimeState.notifications.last_test_at,
|
|
|
|
|
last_sent_at: null,
|
|
|
|
|
last_error: runtimeState.notifications.last_error,
|
|
|
|
|
};
|
|
|
|
|
let backups = {
|
|
|
|
|
ok: true,
|
|
|
|
|
enabled: false,
|
|
|
|
|
configured: false,
|
|
|
|
|
last_backup_at: null,
|
|
|
|
|
last_backup_path: null,
|
|
|
|
|
backup_count: null,
|
|
|
|
|
keep_count: null,
|
|
|
|
|
last_error: null,
|
|
|
|
|
};
|
|
|
|
|
let tracker = {
|
|
|
|
|
ok: true,
|
|
|
|
|
current_year: now.getFullYear(),
|
|
|
|
|
current_month: now.getMonth() + 1,
|
|
|
|
|
bill_count: null,
|
|
|
|
|
payment_count: null,
|
|
|
|
|
bills_this_month: null,
|
|
|
|
|
payments_this_month: null,
|
|
|
|
|
overdue_count: null,
|
|
|
|
|
skipped_count: null,
|
|
|
|
|
last_error: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
db = getDb();
|
|
|
|
|
db.prepare('SELECT 1 AS ok').get();
|
|
|
|
|
|
|
|
|
|
const dbPath = db.name;
|
|
|
|
|
let dbSizeBytes = 0;
|
|
|
|
|
let dbLastModified = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const stat = fs.statSync(dbPath);
|
2026-07-06 14:23:53 -05:00
|
|
|
dbSizeBytes = stat.size;
|
2026-05-03 19:51:57 -05:00
|
|
|
dbLastModified = stat.mtime.toISOString();
|
|
|
|
|
} catch {
|
|
|
|
|
// The DB may be in-memory or on a volume that cannot be stat'ed.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
database = {
|
|
|
|
|
ok: true,
|
|
|
|
|
connected: true,
|
|
|
|
|
status: 'connected',
|
|
|
|
|
// Filesystem path intentionally omitted — not safe to expose to all users
|
|
|
|
|
size_bytes: dbSizeBytes,
|
|
|
|
|
size_mb: (dbSizeBytes / 1024 / 1024).toFixed(2),
|
|
|
|
|
last_modified: dbLastModified,
|
|
|
|
|
last_error: null,
|
|
|
|
|
};
|
|
|
|
|
} catch (err) {
|
|
|
|
|
database.last_error = errorMessage(err);
|
|
|
|
|
recordError('Database', err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (db) {
|
|
|
|
|
try {
|
|
|
|
|
stats = {
|
2026-07-06 14:23:53 -05:00
|
|
|
active_sessions: db
|
|
|
|
|
.prepare("SELECT COUNT(*) AS n FROM sessions WHERE expires_at > datetime('now')")
|
|
|
|
|
.get().n,
|
|
|
|
|
users: db.prepare('SELECT COUNT(*) AS n FROM users').get().n,
|
|
|
|
|
active_bills: db.prepare('SELECT COUNT(*) AS n FROM bills WHERE active = 1').get().n,
|
|
|
|
|
total_payments: db.prepare('SELECT COUNT(*) AS n FROM payments').get().n,
|
2026-05-03 19:51:57 -05:00
|
|
|
};
|
|
|
|
|
} catch (err) {
|
|
|
|
|
recordError('Status Statistics', err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const enabled = getSetting('notify_smtp_enabled') === 'true';
|
|
|
|
|
const host = getSetting('notify_smtp_host');
|
|
|
|
|
const sender = getSetting('notify_sender_address');
|
|
|
|
|
const port = getSetting('notify_smtp_port');
|
|
|
|
|
const username = getSetting('notify_smtp_username');
|
|
|
|
|
const password = getSetting('notify_smtp_password');
|
|
|
|
|
const configured = !!(host && sender && port && (!username || password));
|
2026-07-06 14:23:53 -05:00
|
|
|
const lastSent =
|
|
|
|
|
db.prepare('SELECT MAX(sent_date) AS sent_date FROM notifications').get()?.sent_date ||
|
|
|
|
|
null;
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
notifications = {
|
|
|
|
|
...notifications,
|
|
|
|
|
ok: (!enabled || configured) && !notifications.last_error,
|
|
|
|
|
enabled,
|
|
|
|
|
configured,
|
|
|
|
|
smtp_configured: configured,
|
|
|
|
|
last_sent_at: lastSent,
|
|
|
|
|
};
|
|
|
|
|
} catch (err) {
|
|
|
|
|
notifications = {
|
|
|
|
|
...notifications,
|
|
|
|
|
ok: false,
|
|
|
|
|
last_error: errorMessage(err),
|
|
|
|
|
};
|
|
|
|
|
recordError('Notifications Status', err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-04 21:35:02 -05:00
|
|
|
const enabled = getSetting('backup_schedule_enabled') === 'true';
|
2026-05-03 19:51:57 -05:00
|
|
|
const keepCount = parseInt(getSetting('backup_keep_count') || '', 10);
|
|
|
|
|
const managedBackups = listBackups();
|
|
|
|
|
const latestBackup = managedBackups[0] || null;
|
|
|
|
|
const schedule = getScheduleStatus();
|
|
|
|
|
|
|
|
|
|
backups = {
|
|
|
|
|
ok: true,
|
|
|
|
|
enabled,
|
|
|
|
|
configured: true,
|
|
|
|
|
scheduled_enabled: schedule.enabled,
|
|
|
|
|
scheduled_frequency: schedule.frequency,
|
|
|
|
|
scheduled_time: schedule.time,
|
|
|
|
|
next_backup_at: schedule.next_run_at,
|
|
|
|
|
last_backup_at: latestBackup?.modified_at || null,
|
|
|
|
|
last_backup_path: latestBackup?.id || null,
|
|
|
|
|
last_backup_id: latestBackup?.id || null,
|
|
|
|
|
backup_count: managedBackups.length,
|
|
|
|
|
count: managedBackups.length,
|
|
|
|
|
keep_count: schedule.retention_count ?? (Number.isInteger(keepCount) ? keepCount : null),
|
2026-07-06 14:23:53 -05:00
|
|
|
retention_count:
|
|
|
|
|
schedule.retention_count ?? (Number.isInteger(keepCount) ? keepCount : null),
|
2026-05-03 19:51:57 -05:00
|
|
|
last_error: schedule.last_error || null,
|
|
|
|
|
};
|
|
|
|
|
} catch (err) {
|
|
|
|
|
backups = {
|
|
|
|
|
...backups,
|
|
|
|
|
ok: false,
|
|
|
|
|
last_error: errorMessage(err),
|
|
|
|
|
};
|
|
|
|
|
recordError('Backups Status', err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const range = monthRange(now);
|
2026-05-29 19:21:46 -05:00
|
|
|
const todayDay = now.getDate();
|
2026-05-03 19:51:57 -05:00
|
|
|
const billCount = db.prepare('SELECT COUNT(*) AS n FROM bills WHERE active = 1').get().n;
|
2026-07-06 14:23:53 -05:00
|
|
|
const paymentCount = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`SELECT COUNT(*) AS n FROM payments WHERE paid_date BETWEEN ? AND ? AND deleted_at IS NULL AND ${accountingActiveSql()}`,
|
|
|
|
|
)
|
|
|
|
|
.get(range.start, range.end).n;
|
|
|
|
|
const skippedCount = db
|
|
|
|
|
.prepare(
|
|
|
|
|
'SELECT COUNT(*) AS n FROM monthly_bill_state WHERE year = ? AND month = ? AND is_skipped = 1',
|
|
|
|
|
)
|
|
|
|
|
.get(range.year, range.month).n;
|
|
|
|
|
const overdueCount = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-29 19:21:46 -05:00
|
|
|
SELECT COUNT(*) AS n FROM bills b
|
|
|
|
|
WHERE b.active = 1
|
2026-05-30 21:20:51 -05:00
|
|
|
AND COALESCE(NULLIF(b.cycle_type, ''), 'monthly') = 'monthly'
|
2026-05-29 19:21:46 -05:00
|
|
|
AND CAST(b.due_day AS INTEGER) < ?
|
|
|
|
|
AND NOT EXISTS (
|
|
|
|
|
SELECT 1 FROM payments p
|
2026-06-07 01:05:48 -05:00
|
|
|
WHERE p.bill_id = b.id AND p.paid_date BETWEEN ? AND ? AND p.deleted_at IS NULL AND ${accountingActiveSql('p')}
|
2026-05-29 19:21:46 -05:00
|
|
|
)
|
|
|
|
|
AND NOT EXISTS (
|
|
|
|
|
SELECT 1 FROM monthly_bill_state mbs
|
|
|
|
|
WHERE mbs.bill_id = b.id AND mbs.year = ? AND mbs.month = ? AND mbs.is_skipped = 1
|
|
|
|
|
)
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(todayDay, range.start, range.end, range.year, range.month).n;
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
tracker = {
|
|
|
|
|
ok: true,
|
|
|
|
|
current_year: range.year,
|
|
|
|
|
current_month: range.month,
|
|
|
|
|
bill_count: billCount,
|
|
|
|
|
payment_count: paymentCount,
|
|
|
|
|
bills_this_month: billCount,
|
|
|
|
|
payments_this_month: paymentCount,
|
2026-05-29 19:21:46 -05:00
|
|
|
overdue_count: overdueCount,
|
2026-05-03 19:51:57 -05:00
|
|
|
skipped_count: skippedCount,
|
|
|
|
|
last_error: null,
|
|
|
|
|
};
|
|
|
|
|
} catch (err) {
|
|
|
|
|
tracker = {
|
|
|
|
|
...tracker,
|
|
|
|
|
ok: false,
|
|
|
|
|
last_error: errorMessage(err),
|
|
|
|
|
};
|
|
|
|
|
recordError('Tracker Status', err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 19:21:46 -05:00
|
|
|
// Bank sync (SimpleFIN) status
|
|
|
|
|
let bankSync = {
|
|
|
|
|
enabled: false,
|
|
|
|
|
running: false,
|
|
|
|
|
source_count: 0,
|
|
|
|
|
account_count: 0,
|
|
|
|
|
transaction_count: 0,
|
|
|
|
|
interval_hours: null,
|
|
|
|
|
sync_days: null,
|
|
|
|
|
last_sync_at: null,
|
|
|
|
|
next_run_at: null,
|
|
|
|
|
last_error: null,
|
|
|
|
|
};
|
|
|
|
|
try {
|
|
|
|
|
const config = getBankSyncConfig();
|
|
|
|
|
const workerStatus = getBankSyncWorkerStatus();
|
|
|
|
|
bankSync = {
|
|
|
|
|
enabled: config.enabled,
|
|
|
|
|
running: workerStatus.running,
|
|
|
|
|
interval_hours: config.sync_interval_hours,
|
|
|
|
|
sync_days: config.sync_days,
|
|
|
|
|
source_count: 0,
|
|
|
|
|
account_count: 0,
|
|
|
|
|
transaction_count: 0,
|
|
|
|
|
last_sync_at: null,
|
|
|
|
|
next_run_at: workerStatus.next_run_at,
|
|
|
|
|
last_error: null,
|
|
|
|
|
};
|
|
|
|
|
if (db) {
|
2026-07-06 14:23:53 -05:00
|
|
|
const sourceRow = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-29 19:21:46 -05:00
|
|
|
SELECT COUNT(*) AS source_count, MAX(last_sync_at) AS last_sync_at
|
|
|
|
|
FROM data_sources WHERE type = 'provider_sync' AND provider = 'simplefin'
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get();
|
|
|
|
|
const accountRow = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-29 19:21:46 -05:00
|
|
|
SELECT COUNT(fa.id) AS account_count
|
|
|
|
|
FROM financial_accounts fa
|
|
|
|
|
INNER JOIN data_sources ds ON ds.id = fa.data_source_id
|
|
|
|
|
WHERE ds.type = 'provider_sync' AND ds.provider = 'simplefin'
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get();
|
|
|
|
|
const txRow = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-29 19:21:46 -05:00
|
|
|
SELECT COUNT(t.id) AS transaction_count
|
|
|
|
|
FROM transactions t
|
|
|
|
|
INNER JOIN data_sources ds ON ds.id = t.data_source_id
|
|
|
|
|
WHERE ds.type = 'provider_sync' AND ds.provider = 'simplefin'
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get();
|
|
|
|
|
const errorRow = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-29 19:21:46 -05:00
|
|
|
SELECT last_error FROM data_sources
|
2026-05-29 19:58:52 -05:00
|
|
|
WHERE type = 'provider_sync' AND provider = 'simplefin'
|
|
|
|
|
AND status = 'error' AND last_error IS NOT NULL
|
2026-05-29 19:21:46 -05:00
|
|
|
ORDER BY updated_at DESC LIMIT 1
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get();
|
2026-05-29 19:21:46 -05:00
|
|
|
bankSync = {
|
|
|
|
|
...bankSync,
|
|
|
|
|
source_count: sourceRow.source_count ?? 0,
|
|
|
|
|
account_count: accountRow.account_count ?? 0,
|
|
|
|
|
transaction_count: txRow.transaction_count ?? 0,
|
2026-06-04 21:42:34 -05:00
|
|
|
last_sync_at: toIso(sourceRow.last_sync_at),
|
2026-05-29 19:21:46 -05:00
|
|
|
last_error: errorRow?.last_error || null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
recordError('Bank Sync Status', err);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
// Cleanup status — safe read-only summary from settings, no paths or secrets
|
|
|
|
|
let cleanup = { ok: true, last_run_at: null, last_result: null };
|
|
|
|
|
try {
|
|
|
|
|
const raw = getSetting('cleanup_last_result');
|
|
|
|
|
cleanup = {
|
|
|
|
|
ok: true,
|
2026-06-04 21:42:34 -05:00
|
|
|
last_run_at: toIso(getSetting('cleanup_last_run_at')),
|
2026-05-03 19:51:57 -05:00
|
|
|
last_result: raw ? JSON.parse(raw) : null,
|
|
|
|
|
};
|
2026-07-06 14:23:53 -05:00
|
|
|
} catch {
|
|
|
|
|
/* non-fatal */
|
|
|
|
|
}
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
const memoryMb = Number((process.memoryUsage().rss / 1024 / 1024).toFixed(1));
|
|
|
|
|
const runtime = {
|
|
|
|
|
ok: true,
|
|
|
|
|
node: process.version,
|
|
|
|
|
node_version: process.version,
|
|
|
|
|
platform: process.platform,
|
|
|
|
|
arch: process.arch,
|
|
|
|
|
uptime_seconds: uptimeSeconds,
|
|
|
|
|
memory_mb: memoryMb,
|
|
|
|
|
};
|
|
|
|
|
const server = {
|
|
|
|
|
ok: true,
|
|
|
|
|
time: now.toISOString(),
|
|
|
|
|
now: now.toISOString(),
|
2026-06-10 19:42:51 -05:00
|
|
|
today: localDateString(now),
|
2026-05-03 19:51:57 -05:00
|
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || process.env.TZ || null,
|
|
|
|
|
utc_offset: -now.getTimezoneOffset() / 60,
|
|
|
|
|
env: process.env.NODE_ENV || 'development',
|
|
|
|
|
};
|
|
|
|
|
const worker = {
|
|
|
|
|
ok: !runtimeState.worker.last_error,
|
|
|
|
|
enabled: runtimeState.worker.enabled,
|
|
|
|
|
running: runtimeState.worker.running,
|
|
|
|
|
started_at: runtimeState.worker.started_at,
|
|
|
|
|
last_run_at: runtimeState.worker.last_run_at,
|
|
|
|
|
last_success_at: runtimeState.worker.last_run_at,
|
|
|
|
|
next_run_at: runtimeState.worker.next_run_at,
|
|
|
|
|
last_error: runtimeState.worker.last_error,
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-14 21:00:07 -05:00
|
|
|
// Update check — non-blocking; uses cached result if available
|
2026-07-06 14:23:53 -05:00
|
|
|
let update = {
|
|
|
|
|
current_version: pkg.version,
|
|
|
|
|
latest_version: null,
|
|
|
|
|
up_to_date: null,
|
|
|
|
|
has_update: false,
|
|
|
|
|
error: null,
|
|
|
|
|
last_checked_at: null,
|
|
|
|
|
};
|
|
|
|
|
try {
|
|
|
|
|
update = await checkForUpdates();
|
|
|
|
|
} catch {
|
|
|
|
|
/* non-fatal */
|
|
|
|
|
}
|
2026-05-14 21:00:07 -05:00
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
const recentErrors = getStatusRuntime().recentErrors;
|
|
|
|
|
const ok = database.ok && tracker.ok;
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
ok,
|
|
|
|
|
app: {
|
|
|
|
|
name: pkg.name,
|
|
|
|
|
version: pkg.version,
|
|
|
|
|
environment: server.env,
|
|
|
|
|
uptime_seconds: uptimeSeconds,
|
|
|
|
|
},
|
|
|
|
|
runtime,
|
|
|
|
|
database,
|
|
|
|
|
db: database,
|
|
|
|
|
stats,
|
|
|
|
|
statistics: stats,
|
|
|
|
|
worker,
|
|
|
|
|
notifications,
|
|
|
|
|
backups,
|
|
|
|
|
backup: backups,
|
|
|
|
|
server,
|
|
|
|
|
tracker,
|
2026-05-29 19:21:46 -05:00
|
|
|
bank_sync: bankSync,
|
|
|
|
|
simplefin: bankSync,
|
2026-05-03 19:51:57 -05:00
|
|
|
cleanup,
|
2026-05-14 21:00:07 -05:00
|
|
|
update,
|
2026-05-03 19:51:57 -05:00
|
|
|
recent_errors: recentErrors,
|
|
|
|
|
errors: recentErrors,
|
|
|
|
|
version: pkg.version,
|
|
|
|
|
environment: server.env,
|
|
|
|
|
uptime_seconds: uptimeSeconds,
|
|
|
|
|
node_version: process.version,
|
|
|
|
|
platform: process.platform,
|
|
|
|
|
hostname: os.hostname(),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
module.exports = router;
|