fix: EmptyState по центру и импорт SQLite без 413
Docker / build (push) Failing after 19s

Центрирование заглушек в графиках/Frame; лимит тела backup 100 MiB;
WAL checkpoint при экспорте и очистка -wal/-shm при импорте.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-19 04:41:42 +07:00
co-authored by Cursor
parent 2931d6ee0f
commit e079a61811
12 changed files with 284 additions and 62 deletions
+66 -36
View File
@@ -1,7 +1,13 @@
import type { FastifyPluginAsync } from 'fastify'
import { desc } from 'drizzle-orm'
import { existsSync, readFileSync } from 'node:fs'
import { getDb, getDbPath, reloadDatabaseFromBuffer, schema } from '@cfdm/db'
import { existsSync } from 'node:fs'
import {
getDb,
getDbPath,
readDatabaseFileBuffer,
reloadDatabaseFromBuffer,
schema,
} from '@cfdm/db'
import { getSnapshot } from '@cfdm/db/repositories/snapshot'
import { importJsonSnapshot, type BackupPayload } from '../services/backup-import.js'
@@ -9,10 +15,26 @@ import { restartScheduler } from '../services/scheduler.js'
const BACKUP_VERSION = 1
/** Лимит тела для импорта бэкапа (Fastify default = 1 MiB → 413). */
function backupBodyLimitBytes(): number {
const raw = process.env.BACKUP_BODY_LIMIT_BYTES
if (raw) {
const n = Number(raw)
if (Number.isFinite(n) && n > 0) return Math.floor(n)
}
return 100 * 1024 * 1024 // 100 MiB
}
export const backupRoutes: FastifyPluginAsync = async (app) => {
app.addContentTypeParser('application/octet-stream', { parseAs: 'buffer' }, (_req, body, done) => {
done(null, body)
})
const bodyLimit = backupBodyLimitBytes()
app.addContentTypeParser(
'application/octet-stream',
{ parseAs: 'buffer', bodyLimit },
(_req, body, done) => {
done(null, body)
},
)
app.get('/api/backup/json', async (_req, reply) => {
const syncLog = getDb()
@@ -37,41 +59,49 @@ export const backupRoutes: FastifyPluginAsync = async (app) => {
if (!existsSync(dbPath)) {
return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Файл базы не найден' } })
}
const buf = readFileSync(dbPath)
const buf = readDatabaseFileBuffer()
reply.header('Content-Type', 'application/octet-stream')
reply.header('Content-Disposition', 'attachment; filename="vps-tracker.db"')
return reply.send(buf)
})
app.post('/api/backup/json', async (req, reply) => {
const payload = req.body
if (!payload || typeof payload !== 'object') {
return reply.code(400).send({ error: { code: 'VALIDATION', message: 'Неверное тело запроса' } })
}
try {
importJsonSnapshot(payload as BackupPayload)
restartScheduler()
return { ok: true }
} catch (err) {
req.log.error(err)
const message = err instanceof Error ? err.message : 'Импорт не удался'
return reply.code(500).send({ error: { code: 'INTERNAL_ERROR', message } })
}
})
app.post(
'/api/backup/json',
{ bodyLimit },
async (req, reply) => {
const payload = req.body
if (!payload || typeof payload !== 'object') {
return reply.code(400).send({ error: { code: 'VALIDATION', message: 'Неверное тело запроса' } })
}
try {
importJsonSnapshot(payload as BackupPayload)
restartScheduler()
return { ok: true }
} catch (err) {
req.log.error(err)
const message = err instanceof Error ? err.message : 'Импорт не удался'
return reply.code(500).send({ error: { code: 'INTERNAL_ERROR', message } })
}
},
)
app.post('/api/backup/database', async (req, reply) => {
const buf = req.body as Buffer
if (!Buffer.isBuffer(buf) || !buf.length) {
return reply.code(400).send({ error: { code: 'VALIDATION', message: 'Пустой файл' } })
}
try {
reloadDatabaseFromBuffer(buf)
restartScheduler()
return { ok: true }
} catch (err) {
req.log.error(err)
const message = err instanceof Error ? err.message : 'Восстановление не удалось'
return reply.code(500).send({ error: { code: 'INTERNAL_ERROR', message } })
}
})
app.post(
'/api/backup/database',
{ bodyLimit },
async (req, reply) => {
const buf = req.body as Buffer
if (!Buffer.isBuffer(buf) || !buf.length) {
return reply.code(400).send({ error: { code: 'VALIDATION', message: 'Пустой файл' } })
}
try {
reloadDatabaseFromBuffer(buf)
restartScheduler()
return { ok: true }
} catch (err) {
req.log.error(err)
const message = err instanceof Error ? err.message : 'Восстановление не удалось'
return reply.code(500).send({ error: { code: 'INTERNAL_ERROR', message } })
}
},
)
}