perf(api): чистить старые журналы и облегчить синк и dashboard

- раз в сутки удаляются notification_log и sync_log старше 90 дней и
  vps_health_checks старше 30 дней; синки, зависшие в running дольше
  часа, помечаются ошибкой
- dashboard stats не грузит тарифы, домены и проекты
- запросы характеристик VPS в BILLmanager идут по 4 параллельно

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-06 22:36:53 +07:00
1 parent 0b9b1cad54
commit 80af888cdb
9 files changed
+199 -17

No files matched your search

+1
View File
@@ -98,3 +98,4 @@ export {
consolidateProviderApiFromAccounts,
heuristicBillmanagerProviderApi,
} from './maintenance/consolidate-api.js'
export { pruneOldLogs, RETENTION_DAYS, type PruneResult } from './maintenance/retention.js'
@@ -0,0 +1,29 @@
import Database from 'better-sqlite3'
import { describe, expect, it } from 'vitest'
import { ensureRuntimeSchema, resetRuntimeMigrate } from '../runtime-migrate.js'
import { pruneOldLogs } from './retention.js'
describe('pruneOldLogs', () => {
it('drops old rows, keeps fresh ones, closes stale running syncs', () => {
const db = new Database(':memory:')
resetRuntimeMigrate()
ensureRuntimeSchema(db)
resetRuntimeMigrate()
const now = new Date('2026-06-01T00:00:00Z')
const old = '2026-01-01T00:00:00Z'
const fresh = '2026-05-31T23:30:00Z'
db.exec(`PRAGMA foreign_keys = OFF`)
for (const [id, at] of [['n-old', old], ['n-new', fresh]]) {
db.prepare(`INSERT INTO notification_log (id, event, channel, status, createdAt) VALUES (?, 'e', 'telegram', 'sent', ?)`).run(id, at)
db.prepare(`INSERT INTO vps_health_checks (id, vpsId, checkedAt, status) VALUES (?, 'v', ?, 'up')`).run(id, at)
}
db.prepare(`INSERT INTO sync_log (id, accountId, startedAt, status) VALUES ('s-old', 'a', ?, 'ok')`).run(old)
db.prepare(`INSERT INTO sync_log (id, accountId, startedAt, status) VALUES ('s-stuck', 'a', '2026-05-31T20:00:00Z', 'running')`).run()
db.prepare(`INSERT INTO sync_log (id, accountId, startedAt, status) VALUES ('s-live', 'a', ?, 'running')`).run(fresh)
expect(pruneOldLogs(db, now)).toEqual({ notificationLog: 1, syncLog: 1, vpsHealthChecks: 1, staleSyncs: 1 })
const status = (id: string) => (db.prepare(`SELECT status FROM sync_log WHERE id = ?`).get(id) as { status: string }).status
expect(status('s-stuck')).toBe('error')
expect(status('s-live')).toBe('running')
})
})
+42
View File
@@ -0,0 +1,42 @@
import type Database from 'better-sqlite3'
const DAY_MS = 24 * 60 * 60 * 1000
export const RETENTION_DAYS = {
notificationLog: 90,
syncLog: 90,
vpsHealthChecks: 30,
} as const
/** A sync that is still "running" after this long was cut off by a crash/restart. */
const STALE_RUNNING_SYNC_MS = 60 * 60 * 1000
export interface PruneResult {
notificationLog: number
syncLog: number
vpsHealthChecks: number
staleSyncs: number
}
/** Delete old journal rows (all spaces) and close sync_log rows left 'running' by a dead process. */
export function pruneOldLogs(sqlite: Database.Database, now = new Date()): PruneResult {
const before = (days: number) => new Date(now.getTime() - days * DAY_MS).toISOString()
const nowIso = now.toISOString()
return sqlite.transaction(() => ({
staleSyncs: sqlite
.prepare(
`UPDATE sync_log SET status = 'error', finishedAt = ?, error = 'Прервано: процесс перезапущен'
WHERE status = 'running' AND startedAt < ?`,
)
.run(nowIso, new Date(now.getTime() - STALE_RUNNING_SYNC_MS).toISOString()).changes,
notificationLog: sqlite
.prepare(`DELETE FROM notification_log WHERE createdAt < ?`)
.run(before(RETENTION_DAYS.notificationLog)).changes,
syncLog: sqlite
.prepare(`DELETE FROM sync_log WHERE startedAt < ?`)
.run(before(RETENTION_DAYS.syncLog)).changes,
vpsHealthChecks: sqlite
.prepare(`DELETE FROM vps_health_checks WHERE checkedAt < ?`)
.run(before(RETENTION_DAYS.vpsHealthChecks)).changes,
}))()
}
+17
View File
@@ -49,6 +49,23 @@ function listVpsWithShared(): VpsDto[] {
return [...owned, ...shared]
}
export type DashboardSnapshot = Pick<
Snapshot,
'vps' | 'providers' | 'providerAccounts' | 'payments' | 'balanceLedger' | 'syncLog'
>
/** What dashboard stats read — skips tariffs, domains, projects and settings of the full snapshot. */
export function getDashboardSnapshot(): DashboardSnapshot {
return {
vps: listVpsWithShared(),
providers: providersRepository.list(),
providerAccounts: providerAccountsRepository.list(),
payments: paymentsRepository.list(),
balanceLedger: balanceLedgerRepository.list(),
syncLog: syncLogRepository.listRecent(50),
}
}
export function getSnapshot(): Snapshot {
const spaceId = getCurrentSpaceId()
return {