feat(sv): Backups card — offsite DR status (Core-4 -> Farm) + /api/backups (2.6.0)
migration 026 backup_runs; POST ingest (owner) from offsite-backup.sh, GET for the Sacred Valley card showing last run, per-guest sizes, Farm free, schedule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ import { router as aiUsageRouter } from './routes/ai_usage.js';
|
||||
import { router as infraRouter } from './routes/infra.js';
|
||||
import { router as clusterRouter } from './routes/cluster.js';
|
||||
import { router as storageRouter } from './routes/storage.js';
|
||||
import { router as backupsRouter } from './routes/backups.js';
|
||||
import { router as kuttRouter } from './routes/kutt.js';
|
||||
|
||||
export function mountApi(app) {
|
||||
@@ -52,6 +53,7 @@ export function mountApi(app) {
|
||||
api.use('/infra', infraRouter);
|
||||
api.use('/cluster', clusterRouter);
|
||||
api.use('/storage', storageRouter);
|
||||
api.use('/backups', backupsRouter);
|
||||
api.use('/little-blue', littleblueRouter);
|
||||
api.use('/ai-usage', aiUsageRouter);
|
||||
api.use('/projects', projectsRouter);
|
||||
|
||||
30
lib/api/routes/backups.js
Normal file
30
lib/api/routes/backups.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { asyncWrap } from '../errors.js';
|
||||
import { requireOwner } from '../cap.js';
|
||||
import { validate } from '../validate.js';
|
||||
import * as backups from '../../db/repos/backups.js';
|
||||
|
||||
export const router = Router();
|
||||
|
||||
export const ingest = z.object({
|
||||
ok: z.boolean().optional(),
|
||||
total_bytes: z.number().int().nonnegative().nullable().optional(),
|
||||
won_free_bytes: z.number().int().nonnegative().nullable().optional(),
|
||||
guests: z.array(z.object({
|
||||
vmid: z.union([z.number().int(), z.string()]),
|
||||
name: z.string().max(64),
|
||||
bytes: z.number().int().nonnegative()
|
||||
})).max(50).nullable().optional(),
|
||||
duration_sec: z.number().int().nonnegative().nullable().optional()
|
||||
});
|
||||
|
||||
// POST /api/backups — the offsite-backup script reports a run (owner only).
|
||||
router.post('/', requireOwner, validate({ body: ingest }), asyncWrap(async (req, res) => {
|
||||
res.status(201).json(await backups.record(req.body));
|
||||
}));
|
||||
|
||||
// GET /api/backups — latest run + count, for the Sacred Valley "Backups" card.
|
||||
router.get('/', asyncWrap(async (_req, res) => {
|
||||
res.json({ latest: await backups.latest(), count: await backups.count(), schedule: 'Sun 02:00' });
|
||||
}));
|
||||
12
lib/db/migrations/026_backup_runs.sql
Normal file
12
lib/db/migrations/026_backup_runs.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- 026_backup_runs.sql
|
||||
-- Offsite DR backup run history, fed by /usr/local/bin/offsite-backup.sh on CT 300
|
||||
-- (Core-4 vzdump -> Farm/Won). Powers the Sacred Valley "Backups" card.
|
||||
CREATE TABLE IF NOT EXISTS backup_runs (
|
||||
id serial PRIMARY KEY,
|
||||
ran_at timestamptz NOT NULL DEFAULT now(),
|
||||
ok boolean NOT NULL DEFAULT true,
|
||||
total_bytes bigint,
|
||||
won_free_bytes bigint,
|
||||
guests jsonb, -- [{vmid,name,bytes}]
|
||||
duration_sec integer
|
||||
);
|
||||
21
lib/db/repos/backups.js
Normal file
21
lib/db/repos/backups.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { pool } from '../pool.js';
|
||||
|
||||
export async function record({ ok = true, total_bytes = null, won_free_bytes = null,
|
||||
guests = null, duration_sec = null }) {
|
||||
const { rows: [r] } = await pool.query(
|
||||
`INSERT INTO backup_runs (ok, total_bytes, won_free_bytes, guests, duration_sec)
|
||||
VALUES ($1,$2,$3,$4,$5) RETURNING *`,
|
||||
[ok, total_bytes, won_free_bytes, guests ? JSON.stringify(guests) : null, duration_sec]);
|
||||
return r;
|
||||
}
|
||||
|
||||
export async function latest() {
|
||||
const { rows: [r] } = await pool.query(
|
||||
`SELECT * FROM backup_runs ORDER BY id DESC LIMIT 1`);
|
||||
return r || null;
|
||||
}
|
||||
|
||||
export async function count() {
|
||||
const { rows: [r] } = await pool.query(`SELECT count(*)::int AS n FROM backup_runs`);
|
||||
return r.n;
|
||||
}
|
||||
Reference in New Issue
Block a user