Skip to content

Commit 37d0e80

Browse files
committed
fix: tolerate legacy run history without targets
1 parent beef811 commit 37d0e80

6 files changed

Lines changed: 71 additions & 19 deletions

File tree

internal/history/history.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ func (s *Store) List() ([]Run, error) {
8181
if err := json.Unmarshal([]byte(row.Payload), &run); err != nil {
8282
return nil, err
8383
}
84+
if run.Targets == nil {
85+
run.Targets = []TargetResult{}
86+
}
8487
runs = append(runs, run)
8588
}
8689
return runs, nil

internal/history/history_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package history
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/ca-x/nowledge-mem-snap/internal/persist"
9+
)
10+
11+
func TestListNormalizesLegacyNullTargets(t *testing.T) {
12+
client, err := persist.OpenClient(t.TempDir())
13+
if err != nil {
14+
t.Fatalf("OpenClient: %v", err)
15+
}
16+
t.Cleanup(func() {
17+
if err := client.Close(); err != nil {
18+
t.Fatalf("close client: %v", err)
19+
}
20+
})
21+
22+
startedAt := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
23+
payload := `{"id":"legacy","task_key":"task","task_name":"Task","source_key":"source","status":"success","object_name":"backup.zip","encrypted":false,"size_bytes":42,"started_at":"2026-01-02T03:04:05Z","targets":null}`
24+
if err := client.RunRecord.Create().
25+
SetTenant("tenant").
26+
SetRunID("legacy").
27+
SetPayload(payload).
28+
SetStartedAt(startedAt).
29+
Exec(context.Background()); err != nil {
30+
t.Fatalf("create run record: %v", err)
31+
}
32+
33+
runs, err := NewStore(client, "tenant", 100).List()
34+
if err != nil {
35+
t.Fatalf("List: %v", err)
36+
}
37+
if len(runs) != 1 {
38+
t.Fatalf("len(runs) = %d, want 1", len(runs))
39+
}
40+
if runs[0].Targets == nil {
41+
t.Fatal("Targets is nil, want empty slice")
42+
}
43+
if len(runs[0].Targets) != 0 {
44+
t.Fatalf("len(Targets) = %d, want 0", len(runs[0].Targets))
45+
}
46+
}

web/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "nowledge-mem-snap-web",
33
"private": true,
4-
"version": "0.2.0",
4+
"version": "0.2.1",
55
"type": "module",
66
"scripts": {
77
"dev": "vite --host 0.0.0.0",

web/src/pages/RunsPage.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,24 @@ export function RunsPage({ runs, locale }: { runs: Run[]; locale: string }) {
1111
return (
1212
<Panel title={t('runs')}>
1313
<div className="runs">
14-
{runs.length === 0 ? <Empty text={t('noBackupsYet')} /> : runs.map((run) => (
15-
<Card key={run.id} color="purple" pattern="purple" className="run">
16-
<div className="item-head"><h3>{run.task_name}</h3><span className={`badge ${run.status}`}>{statusLabel(run.status, t)}</span></div>
17-
<p>{new Date(run.started_at).toLocaleString(locale)} · {formatBytes(run.size_bytes)} · {run.encrypted ? t('encrypted') : t('zip')}</p>
18-
<code>{run.object_name}</code>
19-
<div className="target-results">
20-
{run.targets.map((target) => (
21-
<span key={target.target_name}>
22-
<ShieldCheck size={14} /> {target.target_name}: {statusLabel(target.status, t)}
23-
{target.retention_deleted ? ` · ${t('deleted')} ${target.retention_deleted}` : ''}
24-
</span>
25-
))}
26-
</div>
27-
</Card>
28-
))}
14+
{runs.length === 0 ? <Empty text={t('noBackupsYet')} /> : runs.map((run) => {
15+
const targets = run.targets ?? [];
16+
return (
17+
<Card key={run.id} color="purple" pattern="purple" className="run">
18+
<div className="item-head"><h3>{run.task_name}</h3><span className={`badge ${run.status}`}>{statusLabel(run.status, t)}</span></div>
19+
<p>{new Date(run.started_at).toLocaleString(locale)} · {formatBytes(run.size_bytes)} · {run.encrypted ? t('encrypted') : t('zip')}</p>
20+
<code>{run.object_name}</code>
21+
<div className="target-results">
22+
{targets.map((target) => (
23+
<span key={target.target_name}>
24+
<ShieldCheck size={14} /> {target.target_name}: {statusLabel(target.status, t)}
25+
{target.retention_deleted ? ` · ${t('deleted')} ${target.retention_deleted}` : ''}
26+
</span>
27+
))}
28+
</div>
29+
</Card>
30+
);
31+
})}
2932
</div>
3033
</Panel>
3134
);

web/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export type Run = {
155155
encrypted: boolean;
156156
size_bytes: number;
157157
started_at: string;
158-
targets: Array<{ target_name: string; status: string; bytes: number; retention_deleted?: number; error?: string }>;
158+
targets?: Array<{ target_name: string; status: string; bytes: number; retention_deleted?: number; error?: string }> | null;
159159
};
160160

161161
export type SourceRoot = {

0 commit comments

Comments
 (0)