backups: a schedule, a location, and a timer that survives a restart
Restore goes. What it would read from was never decided, and a disabled button with a tooltip explaining it is not built is worse than no button. In its place, backups. `cron` was already a dependency and used nowhere, so the schedule is a real cron expression — a backup wants to happen at an HOUR, and an interval counted from whenever somebody pressed save drifts across the clock until it runs mid-afternoon. Cron alone is not enough, and this is the part worth writing down: it does not run a job it was down for. A daily backup at three on a machine that restarts overnight is skipped silently — the schedule is still there, the next fire is still in the future, and a month later there is one file. So `lastBackupAt` is in the database, and both the startup and a fifteen-minute tick compare it against the schedule's own period, computed from two successive fires. A backup that should already have happened happens now. The job handle is module-level and stopped before a new one is made. Two saves leaving two jobs is a bug that surfaces as backups arriving in pairs, weeks later, with nothing connecting it to a settings form. Back up now exists because a scheduled job nobody has ever run by hand is a guess — it is how you learn the location is writable and pg_dump can reach the databases without waiting until three to find out it cannot. The list underneath is a readdir of the location rather than a record of what should be there: a schedule that has been failing quietly looks exactly like a working one until you count the files. pg_dump per database, not pg_dumpall — a cluster dump would also carry Officer's own, which is not this plugin's to copy around. And a failed run still records the timestamp, or one undumpable database would have the catch-up tick retrying every fifteen minutes for the rest of the week. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+6
-18
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ArrowUpFromLine, Check, CircleCheck, CircleDashed, Database, DatabaseBackup, Loader2, Plus } from 'lucide-react';
|
||||
import { ArrowUpFromLine, Check, CircleCheck, CircleDashed, Database, Loader2, Plus } from 'lucide-react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { readableError } from './errors';
|
||||
import { Block } from './Block';
|
||||
import { Console } from './Console';
|
||||
import { BackupsRow } from './BackupsRow';
|
||||
import { useFlash } from './useFlash';
|
||||
|
||||
/**
|
||||
@@ -185,9 +186,6 @@ export const DatabaseBlock = () => {
|
||||
);
|
||||
|
||||
const row = (label: string, target: 'production' | 'development', state: Named) => {
|
||||
// "Schema pushed" is simply: something is in it. See the note on `tables`.
|
||||
const pushed = (state.tables ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -196,7 +194,7 @@ export const DatabaseBlock = () => {
|
||||
</div>
|
||||
|
||||
<span className="shrink-0 text-xs text-muted-foreground/70">
|
||||
{state.tables === null ? '' : pushed ? `${state.tables} tables` : 'empty'}
|
||||
{state.tables === null ? '' : state.tables > 0 ? `${state.tables} tables` : 'empty'}
|
||||
</span>
|
||||
|
||||
<div className="flex shrink-0 gap-2">
|
||||
@@ -215,19 +213,6 @@ export const DatabaseBlock = () => {
|
||||
Push schema
|
||||
</Button>
|
||||
|
||||
{/* Nothing to restore INTO until there are tables to restore into — a data-only load against
|
||||
an empty database fails on every insert, and that failure would read as a bad backup.
|
||||
Still disabled beyond that: what a backup IS here has not been decided, and a button that
|
||||
guesses at the source of a restore is not a thing to put near a client's data. */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
title={pushed ? 'Not built yet — no backup source chosen' : 'Push the schema first'}
|
||||
>
|
||||
<DatabaseBackup className="size-3.5" />
|
||||
Restore from backup
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -255,6 +240,9 @@ export const DatabaseBlock = () => {
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
<Console output={output} failed={failed} />
|
||||
|
||||
{/* Below the two databases, because it is about both of them rather than either. */}
|
||||
<BackupsRow />
|
||||
|
||||
{/* The common case is both. Doing them one at a time is still there for when it is not. */}
|
||||
<div className="flex justify-end border-t border-border pt-3">
|
||||
<Button size="sm" onClick={() => void push('both')} disabled={busy !== null}>
|
||||
|
||||
Reference in New Issue
Block a user