@buoy-gg/time-machine 7.0.22 → 7.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/TimeMachineBody.js +147 -677
- package/lib/commonjs/components/TimeMachineBody.js.map +1 -1
- package/lib/commonjs/components/TimeMachineModal.js +10 -11
- package/lib/commonjs/components/TimeMachineModal.js.map +1 -1
- package/lib/commonjs/components/confirm.js +39 -0
- package/lib/commonjs/components/confirm.js.map +1 -0
- package/lib/commonjs/components/format.js +24 -0
- package/lib/commonjs/components/format.js.map +1 -0
- package/lib/commonjs/components/list/CaptureBar.js +95 -0
- package/lib/commonjs/components/list/CaptureBar.js.map +1 -0
- package/lib/commonjs/components/list/RestoreOutcomeCard.js +162 -0
- package/lib/commonjs/components/list/RestoreOutcomeCard.js.map +1 -0
- package/lib/commonjs/components/list/SnapshotRow.js +141 -0
- package/lib/commonjs/components/list/SnapshotRow.js.map +1 -0
- package/lib/commonjs/components/list/SourcesStrip.js +126 -0
- package/lib/commonjs/components/list/SourcesStrip.js.map +1 -0
- package/lib/commonjs/components/list/UndoDeleteBar.js +81 -0
- package/lib/commonjs/components/list/UndoDeleteBar.js.map +1 -0
- package/lib/commonjs/components/list/WipeAllRow.js +46 -0
- package/lib/commonjs/components/list/WipeAllRow.js.map +1 -0
- package/lib/commonjs/components/preview/ItemDiffPanel.js +77 -0
- package/lib/commonjs/components/preview/ItemDiffPanel.js.map +1 -0
- package/lib/commonjs/components/preview/PreviewFlow.js +51 -0
- package/lib/commonjs/components/preview/PreviewFlow.js.map +1 -0
- package/lib/commonjs/components/preview/RestorePointNameRow.js +83 -0
- package/lib/commonjs/components/preview/RestorePointNameRow.js.map +1 -0
- package/lib/commonjs/components/preview/RestorePreviewScreen.js +549 -635
- package/lib/commonjs/components/preview/RestorePreviewScreen.js.map +1 -1
- package/lib/commonjs/components/preview/verdictMeta.js +8 -8
- package/lib/commonjs/components/preview/verdictMeta.js.map +1 -1
- package/lib/commonjs/components/sources.js +72 -0
- package/lib/commonjs/components/sources.js.map +1 -0
- package/lib/commonjs/store/snapshotVault.js +93 -9
- package/lib/commonjs/store/snapshotVault.js.map +1 -1
- package/lib/commonjs/store/timeMachineStore.js +125 -6
- package/lib/commonjs/store/timeMachineStore.js.map +1 -1
- package/lib/commonjs/sync/timeMachineSyncAdapter.js +41 -0
- package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/commonjs/ui/SwipeActionsRow.js +167 -0
- package/lib/commonjs/ui/SwipeActionsRow.js.map +1 -0
- package/lib/commonjs/ui/primitives.js +164 -0
- package/lib/commonjs/ui/primitives.js.map +1 -0
- package/lib/commonjs/ui/tmTheme.js +103 -0
- package/lib/commonjs/ui/tmTheme.js.map +1 -0
- package/lib/module/components/TimeMachineBody.js +150 -680
- package/lib/module/components/TimeMachineBody.js.map +1 -1
- package/lib/module/components/TimeMachineModal.js +11 -12
- package/lib/module/components/TimeMachineModal.js.map +1 -1
- package/lib/module/components/confirm.js +34 -0
- package/lib/module/components/confirm.js.map +1 -0
- package/lib/module/components/format.js +19 -0
- package/lib/module/components/format.js.map +1 -0
- package/lib/module/components/list/CaptureBar.js +90 -0
- package/lib/module/components/list/CaptureBar.js.map +1 -0
- package/lib/module/components/list/RestoreOutcomeCard.js +156 -0
- package/lib/module/components/list/RestoreOutcomeCard.js.map +1 -0
- package/lib/module/components/list/SnapshotRow.js +138 -0
- package/lib/module/components/list/SnapshotRow.js.map +1 -0
- package/lib/module/components/list/SourcesStrip.js +121 -0
- package/lib/module/components/list/SourcesStrip.js.map +1 -0
- package/lib/module/components/list/UndoDeleteBar.js +78 -0
- package/lib/module/components/list/UndoDeleteBar.js.map +1 -0
- package/lib/module/components/list/WipeAllRow.js +41 -0
- package/lib/module/components/list/WipeAllRow.js.map +1 -0
- package/lib/module/components/preview/ItemDiffPanel.js +72 -0
- package/lib/module/components/preview/ItemDiffPanel.js.map +1 -0
- package/lib/module/components/preview/PreviewFlow.js +46 -0
- package/lib/module/components/preview/PreviewFlow.js.map +1 -0
- package/lib/module/components/preview/RestorePointNameRow.js +78 -0
- package/lib/module/components/preview/RestorePointNameRow.js.map +1 -0
- package/lib/module/components/preview/RestorePreviewScreen.js +552 -638
- package/lib/module/components/preview/RestorePreviewScreen.js.map +1 -1
- package/lib/module/components/preview/verdictMeta.js +9 -8
- package/lib/module/components/preview/verdictMeta.js.map +1 -1
- package/lib/module/components/sources.js +66 -0
- package/lib/module/components/sources.js.map +1 -0
- package/lib/module/store/snapshotVault.js +87 -9
- package/lib/module/store/snapshotVault.js.map +1 -1
- package/lib/module/store/timeMachineStore.js +125 -7
- package/lib/module/store/timeMachineStore.js.map +1 -1
- package/lib/module/sync/timeMachineSyncAdapter.js +41 -0
- package/lib/module/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/module/ui/SwipeActionsRow.js +162 -0
- package/lib/module/ui/SwipeActionsRow.js.map +1 -0
- package/lib/module/ui/primitives.js +156 -0
- package/lib/module/ui/primitives.js.map +1 -0
- package/lib/module/ui/tmTheme.js +99 -0
- package/lib/module/ui/tmTheme.js.map +1 -0
- package/lib/typescript/components/TimeMachineBody.d.ts.map +1 -1
- package/lib/typescript/components/TimeMachineModal.d.ts.map +1 -1
- package/lib/typescript/components/confirm.d.ts +2 -0
- package/lib/typescript/components/confirm.d.ts.map +1 -0
- package/lib/typescript/components/format.d.ts +4 -0
- package/lib/typescript/components/format.d.ts.map +1 -0
- package/lib/typescript/components/list/CaptureBar.d.ts +6 -0
- package/lib/typescript/components/list/CaptureBar.d.ts.map +1 -0
- package/lib/typescript/components/list/RestoreOutcomeCard.d.ts +11 -0
- package/lib/typescript/components/list/RestoreOutcomeCard.d.ts.map +1 -0
- package/lib/typescript/components/list/SnapshotRow.d.ts +12 -0
- package/lib/typescript/components/list/SnapshotRow.d.ts.map +1 -0
- package/lib/typescript/components/list/SourcesStrip.d.ts +5 -0
- package/lib/typescript/components/list/SourcesStrip.d.ts.map +1 -0
- package/lib/typescript/components/list/UndoDeleteBar.d.ts +8 -0
- package/lib/typescript/components/list/UndoDeleteBar.d.ts.map +1 -0
- package/lib/typescript/components/list/WipeAllRow.d.ts +5 -0
- package/lib/typescript/components/list/WipeAllRow.d.ts.map +1 -0
- package/lib/typescript/components/preview/ItemDiffPanel.d.ts +5 -0
- package/lib/typescript/components/preview/ItemDiffPanel.d.ts.map +1 -0
- package/lib/typescript/components/preview/PreviewFlow.d.ts +9 -0
- package/lib/typescript/components/preview/PreviewFlow.d.ts.map +1 -0
- package/lib/typescript/components/preview/RestorePointNameRow.d.ts +5 -0
- package/lib/typescript/components/preview/RestorePointNameRow.d.ts.map +1 -0
- package/lib/typescript/components/preview/RestorePreviewScreen.d.ts +1 -5
- package/lib/typescript/components/preview/RestorePreviewScreen.d.ts.map +1 -1
- package/lib/typescript/components/preview/verdictMeta.d.ts +5 -0
- package/lib/typescript/components/preview/verdictMeta.d.ts.map +1 -1
- package/lib/typescript/components/sources.d.ts +36 -0
- package/lib/typescript/components/sources.d.ts.map +1 -0
- package/lib/typescript/store/snapshotVault.d.ts +48 -0
- package/lib/typescript/store/snapshotVault.d.ts.map +1 -1
- package/lib/typescript/store/timeMachineStore.d.ts +35 -1
- package/lib/typescript/store/timeMachineStore.d.ts.map +1 -1
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts +17 -0
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/ui/SwipeActionsRow.d.ts +32 -0
- package/lib/typescript/ui/SwipeActionsRow.d.ts.map +1 -0
- package/lib/typescript/ui/primitives.d.ts +57 -0
- package/lib/typescript/ui/primitives.d.ts.map +1 -0
- package/lib/typescript/ui/tmTheme.d.ts +67 -0
- package/lib/typescript/ui/tmTheme.d.ts.map +1 -0
- package/package.json +7 -7
- package/src/components/TimeMachineBody.tsx +183 -817
- package/src/components/TimeMachineModal.tsx +10 -11
- package/src/components/confirm.ts +35 -0
- package/src/components/format.ts +17 -0
- package/src/components/list/CaptureBar.tsx +100 -0
- package/src/components/list/RestoreOutcomeCard.tsx +159 -0
- package/src/components/list/SnapshotRow.tsx +155 -0
- package/src/components/list/SourcesStrip.tsx +124 -0
- package/src/components/list/UndoDeleteBar.tsx +76 -0
- package/src/components/list/WipeAllRow.tsx +42 -0
- package/src/components/preview/ItemDiffPanel.tsx +77 -0
- package/src/components/preview/PreviewFlow.tsx +70 -0
- package/src/components/preview/RestorePointNameRow.tsx +80 -0
- package/src/components/preview/RestorePreviewScreen.tsx +535 -535
- package/src/components/preview/verdictMeta.ts +8 -8
- package/src/components/sources.ts +92 -0
- package/src/store/snapshotVault.ts +131 -9
- package/src/store/timeMachineStore.ts +136 -5
- package/src/sync/timeMachineSyncAdapter.ts +30 -0
- package/src/ui/SwipeActionsRow.tsx +199 -0
- package/src/ui/primitives.tsx +157 -0
- package/src/ui/tmTheme.ts +107 -0
- package/lib/commonjs/components/preview/ItemDetailScreen.js +0 -189
- package/lib/commonjs/components/preview/ItemDetailScreen.js.map +0 -1
- package/lib/module/components/preview/ItemDetailScreen.js +0 -184
- package/lib/module/components/preview/ItemDetailScreen.js.map +0 -1
- package/lib/typescript/components/preview/ItemDetailScreen.d.ts +0 -11
- package/lib/typescript/components/preview/ItemDetailScreen.d.ts.map +0 -1
- package/src/components/preview/ItemDetailScreen.tsx +0 -172
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* paired with a distinct glyph (true minus U+2212, approx U+2248) — the
|
|
4
4
|
* repo-wide accessibility convention for diff surfaces.
|
|
5
5
|
*/
|
|
6
|
-
import { buoyColors } from "@buoy-gg/shared-ui";
|
|
7
6
|
import type { StateSnapshotPreviewVerdict } from "@buoy-gg/shared-ui";
|
|
7
|
+
import { tm } from "../../ui/tmTheme";
|
|
8
8
|
|
|
9
9
|
export interface VerdictMeta {
|
|
10
10
|
glyph: string;
|
|
@@ -15,25 +15,25 @@ export interface VerdictMeta {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export const VERDICT_META: Record<StateSnapshotPreviewVerdict, VerdictMeta> = {
|
|
18
|
-
added: { glyph: "+", color:
|
|
19
|
-
removed: { glyph: "−", color:
|
|
20
|
-
changed: { glyph: "≈", color:
|
|
18
|
+
added: { glyph: "+", color: tm.color.accent, label: "added", applies: true },
|
|
19
|
+
removed: { glyph: "−", color: tm.color.danger, label: "removed", applies: true },
|
|
20
|
+
changed: { glyph: "≈", color: tm.color.info, label: "changed", applies: true },
|
|
21
21
|
"too-large": {
|
|
22
22
|
glyph: "≈",
|
|
23
|
-
color:
|
|
23
|
+
color: tm.color.warning,
|
|
24
24
|
label: "changed (large)",
|
|
25
25
|
applies: true,
|
|
26
26
|
},
|
|
27
|
-
unchanged: { glyph: "=", color:
|
|
27
|
+
unchanged: { glyph: "=", color: tm.color.labelTertiary, label: "unchanged", applies: false },
|
|
28
28
|
"wont-apply": {
|
|
29
29
|
glyph: "⊘",
|
|
30
|
-
color:
|
|
30
|
+
color: tm.color.warning,
|
|
31
31
|
label: "won't apply",
|
|
32
32
|
applies: false,
|
|
33
33
|
},
|
|
34
34
|
recomputes: {
|
|
35
35
|
glyph: "↻",
|
|
36
|
-
color:
|
|
36
|
+
color: tm.color.labelTertiary,
|
|
37
37
|
label: "recomputes",
|
|
38
38
|
applies: false,
|
|
39
39
|
},
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Time Machine can capture from, and how healthy each source is.
|
|
3
|
+
*
|
|
4
|
+
* On device these are live registry probes; on Buoy Desktop the same shapes
|
|
5
|
+
* arrive mirrored in the sync payload. Route is folded in as a source even
|
|
6
|
+
* though it is a device capability rather than a registered provider — from
|
|
7
|
+
* the user's side "can this snapshot take me back to that screen?" is the
|
|
8
|
+
* same question as "will my Redux store come back".
|
|
9
|
+
*/
|
|
10
|
+
import type { ComponentType } from "react";
|
|
11
|
+
import {
|
|
12
|
+
JotaiIcon,
|
|
13
|
+
QueryIcon,
|
|
14
|
+
ReduxIcon,
|
|
15
|
+
RoutesIcon,
|
|
16
|
+
StorageIcon,
|
|
17
|
+
TimeMachineIcon,
|
|
18
|
+
ZustandIcon,
|
|
19
|
+
} from "@buoy-gg/floating-tools-core";
|
|
20
|
+
import { useSnapshotProviders, useTimeMachineState } from "../hooks/useTimeMachine";
|
|
21
|
+
import { canNavigate } from "../store/routeRestore";
|
|
22
|
+
|
|
23
|
+
export interface SourceStatus {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
canCapture: boolean;
|
|
27
|
+
canRestore: boolean;
|
|
28
|
+
/** Why it is in this state — shown on the Sources screen. */
|
|
29
|
+
detail?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Buoy icons paint their knock-out parts with `fill: "bg"`, defaulting to a
|
|
34
|
+
* near-black — so whatever surface an icon sits on has to be handed in as
|
|
35
|
+
* `bgColor` or those cut-outs read as black holes (storage's floppy slot and
|
|
36
|
+
* the route glyph are the two that use it).
|
|
37
|
+
*/
|
|
38
|
+
export type SourceIcon = ComponentType<{
|
|
39
|
+
size?: number;
|
|
40
|
+
color?: string;
|
|
41
|
+
bgColor?: string;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
const ICONS: Record<string, SourceIcon> = {
|
|
45
|
+
storage: StorageIcon,
|
|
46
|
+
redux: ReduxIcon,
|
|
47
|
+
zustand: ZustandIcon,
|
|
48
|
+
jotai: JotaiIcon,
|
|
49
|
+
query: QueryIcon,
|
|
50
|
+
route: RoutesIcon,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function sourceIcon(id: string): SourceIcon {
|
|
54
|
+
return ICONS[id] ?? TimeMachineIcon;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Id of the route tile — a device capability, not a snapshot provider. */
|
|
58
|
+
export const ROUTE_SOURCE_ID = "route";
|
|
59
|
+
|
|
60
|
+
export function useSourceStatuses(): SourceStatus[] {
|
|
61
|
+
const { remoteProviders, remoteRoute } = useTimeMachineState();
|
|
62
|
+
const localProviders = useSnapshotProviders();
|
|
63
|
+
|
|
64
|
+
const providers: SourceStatus[] =
|
|
65
|
+
remoteProviders ??
|
|
66
|
+
localProviders.map((p) => ({ id: p.id, label: p.label, ...p.status() }));
|
|
67
|
+
|
|
68
|
+
const routeAvailable = remoteProviders
|
|
69
|
+
? remoteRoute?.available === true
|
|
70
|
+
: canNavigate();
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
...providers,
|
|
74
|
+
{
|
|
75
|
+
id: ROUTE_SOURCE_ID,
|
|
76
|
+
label: "Route",
|
|
77
|
+
canCapture: routeAvailable,
|
|
78
|
+
canRestore: routeAvailable,
|
|
79
|
+
// No live pathname here: these screens only re-render on store changes,
|
|
80
|
+
// so a route printed as status goes stale the moment you navigate with
|
|
81
|
+
// the tool still open. Each restore point shows the route it holds.
|
|
82
|
+
detail: routeAvailable
|
|
83
|
+
? "Recorded with every snapshot — a restore can take you back"
|
|
84
|
+
: "No Expo Router detected — snapshots record no route",
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A source is "attached" when a snapshot would actually pick it up. */
|
|
90
|
+
export function isAttached(source: SourceStatus): boolean {
|
|
91
|
+
return source.canCapture;
|
|
92
|
+
}
|
|
@@ -42,14 +42,37 @@ export interface SnapshotMeta {
|
|
|
42
42
|
* User-saved restore scope: compound "sourceId::itemKey" strings. When set,
|
|
43
43
|
* restoring this snapshot touches ONLY these items — "this restore point
|
|
44
44
|
* means just my cart". Absent = full restore.
|
|
45
|
+
*
|
|
46
|
+
* Set through the MCP/adapter `setScope` action. The tool's own UI writes
|
|
47
|
+
* `excludedKeys` instead — see there for why the two exist.
|
|
45
48
|
*/
|
|
46
49
|
scope?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Items the user switched OFF in the restore preview and saved: compound
|
|
52
|
+
* "sourceId::itemKey" strings that every later restore of this snapshot
|
|
53
|
+
* leaves untouched, including the one-tap RESTORE on the list.
|
|
54
|
+
*
|
|
55
|
+
* The inverse of `scope`, and the right default for "I unchecked one of
|
|
56
|
+
* these ten keys": naming what to SKIP lets everything else through, so a
|
|
57
|
+
* key that only starts differing tomorrow is still restored. An include
|
|
58
|
+
* list built from today's diff would silently freeze the restore point to
|
|
59
|
+
* whatever happened to differ when it was saved.
|
|
60
|
+
*/
|
|
61
|
+
excludedKeys?: string[];
|
|
47
62
|
/**
|
|
48
63
|
* The route the app was on when this snapshot was captured. Absent when the
|
|
49
64
|
* app has no expo-router (nothing to return to). A restore can navigate back
|
|
50
65
|
* to it — see routeRestore.ts.
|
|
51
66
|
*/
|
|
52
67
|
route?: SnapshotRoute;
|
|
68
|
+
/**
|
|
69
|
+
* Whether restoring this point should also navigate back to `route`.
|
|
70
|
+
* Undefined means "yes, if there is a route" — the useful default, and the
|
|
71
|
+
* reason there is no longer a prompt on every restore.
|
|
72
|
+
*/
|
|
73
|
+
restoreRoute?: boolean;
|
|
74
|
+
/** When this point was last restored, for the "restored 4m ago" marker. */
|
|
75
|
+
lastRestoredAt?: number;
|
|
53
76
|
/** Source ids captured, with their capture warnings. */
|
|
54
77
|
sources: Record<string, SnapshotSourceMeta>;
|
|
55
78
|
}
|
|
@@ -114,18 +137,17 @@ export async function deleteSnapshot(
|
|
|
114
137
|
return next;
|
|
115
138
|
}
|
|
116
139
|
|
|
117
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Rewrite one snapshot's metadata in BOTH places it lives — the index and the
|
|
142
|
+
* snapshot's own record — so a reload can't resurrect the old value from
|
|
143
|
+
* whichever one was missed.
|
|
144
|
+
*/
|
|
145
|
+
async function patchMeta(
|
|
118
146
|
id: string,
|
|
119
|
-
|
|
147
|
+
patch: (meta: SnapshotMeta) => SnapshotMeta,
|
|
120
148
|
metas: SnapshotMeta[]
|
|
121
149
|
): Promise<SnapshotMeta[]> {
|
|
122
|
-
const apply = (m: SnapshotMeta): SnapshotMeta =>
|
|
123
|
-
if (m.id !== id) return m;
|
|
124
|
-
const next = { ...m };
|
|
125
|
-
if (scope && scope.length > 0) next.scope = scope;
|
|
126
|
-
else delete next.scope;
|
|
127
|
-
return next;
|
|
128
|
-
};
|
|
150
|
+
const apply = (m: SnapshotMeta): SnapshotMeta => (m.id === id ? patch(m) : m);
|
|
129
151
|
const next = metas.map(apply);
|
|
130
152
|
await persistentStorage.setItem(INDEX_KEY, JSON.stringify(next));
|
|
131
153
|
const stored = await loadSnapshot(id);
|
|
@@ -136,6 +158,106 @@ export async function setSnapshotScope(
|
|
|
136
158
|
return next;
|
|
137
159
|
}
|
|
138
160
|
|
|
161
|
+
/** Set (or clear with null/empty) the "only these items" restore scope. */
|
|
162
|
+
export function setSnapshotScope(
|
|
163
|
+
id: string,
|
|
164
|
+
scope: string[] | null,
|
|
165
|
+
metas: SnapshotMeta[]
|
|
166
|
+
): Promise<SnapshotMeta[]> {
|
|
167
|
+
return patchMeta(
|
|
168
|
+
id,
|
|
169
|
+
(m) => {
|
|
170
|
+
const next = { ...m };
|
|
171
|
+
if (scope && scope.length > 0) next.scope = scope;
|
|
172
|
+
else delete next.scope;
|
|
173
|
+
return next;
|
|
174
|
+
},
|
|
175
|
+
metas
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Set (or clear with null/empty) the items a restore of this snapshot must
|
|
181
|
+
* leave alone. Saving exclusions also clears any legacy `scope`: the two
|
|
182
|
+
* answer the same question in opposite directions, and the one the user just
|
|
183
|
+
* chose in the preview wins.
|
|
184
|
+
*/
|
|
185
|
+
export function setSnapshotExclusions(
|
|
186
|
+
id: string,
|
|
187
|
+
excludedKeys: string[] | null,
|
|
188
|
+
metas: SnapshotMeta[]
|
|
189
|
+
): Promise<SnapshotMeta[]> {
|
|
190
|
+
return patchMeta(
|
|
191
|
+
id,
|
|
192
|
+
(m) => {
|
|
193
|
+
const next = { ...m };
|
|
194
|
+
delete next.scope;
|
|
195
|
+
if (excludedKeys && excludedKeys.length > 0)
|
|
196
|
+
next.excludedKeys = excludedKeys;
|
|
197
|
+
else delete next.excludedKeys;
|
|
198
|
+
return next;
|
|
199
|
+
},
|
|
200
|
+
metas
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Turn this point's route restore on or off — the row's route toggle. */
|
|
205
|
+
export function setSnapshotRestoreRoute(
|
|
206
|
+
id: string,
|
|
207
|
+
restoreRoute: boolean,
|
|
208
|
+
metas: SnapshotMeta[]
|
|
209
|
+
): Promise<SnapshotMeta[]> {
|
|
210
|
+
return patchMeta(id, (m) => ({ ...m, restoreRoute }), metas);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Stamp a restore so the list can say which point you are currently in. */
|
|
214
|
+
export function markSnapshotRestored(
|
|
215
|
+
id: string,
|
|
216
|
+
at: number,
|
|
217
|
+
metas: SnapshotMeta[]
|
|
218
|
+
): Promise<SnapshotMeta[]> {
|
|
219
|
+
return patchMeta(id, (m) => ({ ...m, lastRestoredAt: at }), metas);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Copy a stored snapshot under a new id and name. The payload is copied by
|
|
224
|
+
* reference into a fresh record — the vault serializes it on write, so the
|
|
225
|
+
* two never share mutable state.
|
|
226
|
+
*/
|
|
227
|
+
export async function duplicateSnapshot(
|
|
228
|
+
id: string,
|
|
229
|
+
name: string,
|
|
230
|
+
metas: SnapshotMeta[]
|
|
231
|
+
): Promise<{ metas: SnapshotMeta[]; meta: SnapshotMeta } | null> {
|
|
232
|
+
const stored = await loadSnapshot(id);
|
|
233
|
+
if (!stored) return null;
|
|
234
|
+
const copy: StoredSnapshot = {
|
|
235
|
+
meta: {
|
|
236
|
+
...stored.meta,
|
|
237
|
+
id: `${stored.meta.id}_copy_${Date.now().toString(36)}`,
|
|
238
|
+
name,
|
|
239
|
+
createdAt: Date.now(),
|
|
240
|
+
lastRestoredAt: undefined,
|
|
241
|
+
},
|
|
242
|
+
data: stored.data,
|
|
243
|
+
};
|
|
244
|
+
delete copy.meta.lastRestoredAt;
|
|
245
|
+
return { metas: await saveSnapshot(copy, metas), meta: copy.meta };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Read a whole record so a delete can be held in memory and undone. */
|
|
249
|
+
export async function readSnapshot(id: string): Promise<StoredSnapshot | null> {
|
|
250
|
+
return loadSnapshot(id);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Put a held record back exactly as it was. */
|
|
254
|
+
export async function restoreDeletedSnapshot(
|
|
255
|
+
stored: StoredSnapshot,
|
|
256
|
+
metas: SnapshotMeta[]
|
|
257
|
+
): Promise<SnapshotMeta[]> {
|
|
258
|
+
return saveSnapshot(stored, metas);
|
|
259
|
+
}
|
|
260
|
+
|
|
139
261
|
export async function renameSnapshot(
|
|
140
262
|
id: string,
|
|
141
263
|
name: string,
|
|
@@ -28,11 +28,17 @@ import {
|
|
|
28
28
|
import { objectDiff } from "@buoy-gg/shared-ui/dataViewer";
|
|
29
29
|
import {
|
|
30
30
|
deleteSnapshot,
|
|
31
|
+
duplicateSnapshot,
|
|
31
32
|
loadIndex,
|
|
32
33
|
loadSnapshot,
|
|
34
|
+
markSnapshotRestored,
|
|
35
|
+
readSnapshot,
|
|
33
36
|
renameSnapshot,
|
|
37
|
+
restoreDeletedSnapshot,
|
|
34
38
|
saveSnapshot,
|
|
39
|
+
setSnapshotRestoreRoute,
|
|
35
40
|
setSnapshotScope,
|
|
41
|
+
setSnapshotExclusions,
|
|
36
42
|
type SnapshotMeta,
|
|
37
43
|
type StoredSnapshot,
|
|
38
44
|
} from "./snapshotVault";
|
|
@@ -62,6 +68,29 @@ export interface RestoreOutcome {
|
|
|
62
68
|
route?: RouteRestoreResult;
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
/**
|
|
72
|
+
* What to call a capture nobody named. The screen you were on plus the time
|
|
73
|
+
* is the pair that makes a restore point recognizable an hour later —
|
|
74
|
+
* "Untitled snapshot" never was, and typing mid-flow is the friction that
|
|
75
|
+
* produced it.
|
|
76
|
+
*/
|
|
77
|
+
function defaultSnapshotName(route: { pathname: string } | null | undefined): string {
|
|
78
|
+
const now = new Date();
|
|
79
|
+
const clock = `${String(now.getHours()).padStart(2, "0")}:${String(
|
|
80
|
+
now.getMinutes()
|
|
81
|
+
).padStart(2, "0")}`;
|
|
82
|
+
return route ? `${route.pathname} · ${clock}` : `Snapshot · ${clock}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Will restoring this point also navigate back to its route? Having a route
|
|
87
|
+
* means wanting it back unless the point says otherwise — the same rule
|
|
88
|
+
* `restore` applies, exported so the UI can't drift from it.
|
|
89
|
+
*/
|
|
90
|
+
export function willRestoreRoute(meta: SnapshotMeta): boolean {
|
|
91
|
+
return !!meta.route && (meta.restoreRoute ?? true);
|
|
92
|
+
}
|
|
93
|
+
|
|
65
94
|
/** Extra knobs a restore accepts beyond mode + exclusions. */
|
|
66
95
|
export interface RestoreOptions {
|
|
67
96
|
/**
|
|
@@ -118,6 +147,11 @@ export interface TimeMachineState {
|
|
|
118
147
|
* (Buoy Desktop). Null on-device, where the live router is probed directly.
|
|
119
148
|
*/
|
|
120
149
|
remoteRoute: RouteToolStatus | null;
|
|
150
|
+
/**
|
|
151
|
+
* The last deleted record, held in memory so the list can offer an undo.
|
|
152
|
+
* The vault row is already gone — this is the only copy until it expires.
|
|
153
|
+
*/
|
|
154
|
+
recentlyDeleted: { stored: StoredSnapshot; at: number } | null;
|
|
121
155
|
}
|
|
122
156
|
|
|
123
157
|
/** Whether this device can capture and return to routes, and where it is now. */
|
|
@@ -153,6 +187,7 @@ class TimeMachineStore {
|
|
|
153
187
|
preview: null,
|
|
154
188
|
remoteProviders: null,
|
|
155
189
|
remoteRoute: null,
|
|
190
|
+
recentlyDeleted: null,
|
|
156
191
|
};
|
|
157
192
|
/** Monotonic token — a preview build checks it after every await and
|
|
158
193
|
* abandons itself when a newer build (or clearPreview) supersedes it. */
|
|
@@ -245,7 +280,7 @@ class TimeMachineStore {
|
|
|
245
280
|
const snapshot: StoredSnapshot = {
|
|
246
281
|
meta: {
|
|
247
282
|
id: nextId(),
|
|
248
|
-
name: name.trim() ||
|
|
283
|
+
name: name.trim() || defaultSnapshotName(route),
|
|
249
284
|
createdAt: Date.now(),
|
|
250
285
|
sizeBytes: 0,
|
|
251
286
|
...(route ? { route } : {}),
|
|
@@ -362,7 +397,11 @@ class TimeMachineStore {
|
|
|
362
397
|
return bySource;
|
|
363
398
|
};
|
|
364
399
|
|
|
365
|
-
|
|
400
|
+
// No explicit list means "use what the user saved in the preview" —
|
|
401
|
+
// that is what makes an unchecked item stay unchecked forever, for the
|
|
402
|
+
// one-tap RESTORE on the list and for MCP alike.
|
|
403
|
+
const effectiveExcludes = excludeKeys ?? stored.meta.excludedKeys ?? [];
|
|
404
|
+
const excludeBySource = splitCompound(effectiveExcludes);
|
|
366
405
|
// A saved scope makes this a targeted restore point. An explicit
|
|
367
406
|
// exclusion list from the preview already reflects the scope (the
|
|
368
407
|
// preview seeds its selection from it), so it takes precedence.
|
|
@@ -371,7 +410,11 @@ class TimeMachineStore {
|
|
|
371
410
|
? splitCompound(stored.meta.scope)
|
|
372
411
|
: null;
|
|
373
412
|
|
|
374
|
-
|
|
413
|
+
// No prompt any more: the point carries its own answer, and having a
|
|
414
|
+
// route means wanting it back unless you said otherwise. An explicit
|
|
415
|
+
// option still wins, so MCP and the desktop can force either way.
|
|
416
|
+
const wantsRoute =
|
|
417
|
+
options?.restoreRoute ?? willRestoreRoute(stored.meta);
|
|
375
418
|
|
|
376
419
|
if (mode === "reload") {
|
|
377
420
|
// Storage only; everything in-memory rebuilds after the reload.
|
|
@@ -398,7 +441,11 @@ class TimeMachineStore {
|
|
|
398
441
|
}
|
|
399
442
|
}
|
|
400
443
|
outcome.willReload = this.scheduleReload("buoy time-machine restore");
|
|
401
|
-
|
|
444
|
+
// Stamped BEFORE the reload is armed — the JS realm is about to die.
|
|
445
|
+
this.setState({
|
|
446
|
+
lastOutcome: outcome,
|
|
447
|
+
snapshots: await markSnapshotRestored(id, outcome.at, this.state.snapshots),
|
|
448
|
+
});
|
|
402
449
|
return outcome;
|
|
403
450
|
}
|
|
404
451
|
|
|
@@ -416,7 +463,10 @@ class TimeMachineStore {
|
|
|
416
463
|
if (wantsRoute) {
|
|
417
464
|
outcome.route = navigateToRoute(stored.meta.route!);
|
|
418
465
|
}
|
|
419
|
-
this.setState({
|
|
466
|
+
this.setState({
|
|
467
|
+
lastOutcome: outcome,
|
|
468
|
+
snapshots: await markSnapshotRestored(id, outcome.at, this.state.snapshots),
|
|
469
|
+
});
|
|
420
470
|
return outcome;
|
|
421
471
|
} finally {
|
|
422
472
|
this.setState({ busy: null });
|
|
@@ -461,16 +511,78 @@ class TimeMachineStore {
|
|
|
461
511
|
}
|
|
462
512
|
}
|
|
463
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Delete, holding the whole record in memory so `undoDelete` can put it
|
|
516
|
+
* back. Swipe-and-tap is deliberate but not recoverable, and those are
|
|
517
|
+
* different properties — a restore point can represent state that takes
|
|
518
|
+
* ten minutes and a cooperative backend to rebuild.
|
|
519
|
+
*/
|
|
464
520
|
async delete(id: string): Promise<void> {
|
|
465
521
|
if (remoteHandler) {
|
|
466
522
|
await remoteHandler("delete", { id });
|
|
467
523
|
return;
|
|
468
524
|
}
|
|
469
525
|
await this.ensureLoaded();
|
|
526
|
+
const stored = await readSnapshot(id);
|
|
470
527
|
const snapshots = await deleteSnapshot(id, this.state.snapshots);
|
|
528
|
+
this.setState({
|
|
529
|
+
snapshots,
|
|
530
|
+
recentlyDeleted: stored ? { stored, at: Date.now() } : null,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Put the last deleted point back. No-op once the window has passed. */
|
|
535
|
+
async undoDelete(): Promise<void> {
|
|
536
|
+
const held = this.state.recentlyDeleted;
|
|
537
|
+
if (!held) return;
|
|
538
|
+
const snapshots = await restoreDeletedSnapshot(
|
|
539
|
+
held.stored,
|
|
540
|
+
this.state.snapshots
|
|
541
|
+
);
|
|
542
|
+
this.setState({ snapshots, recentlyDeleted: null });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Drop the undo offer — on timeout, or when the user moves on. */
|
|
546
|
+
clearRecentlyDeleted(): void {
|
|
547
|
+
if (this.state.recentlyDeleted) this.setState({ recentlyDeleted: null });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Turn this point's route restore on or off. */
|
|
551
|
+
async setRestoreRoute(id: string, restoreRoute: boolean): Promise<void> {
|
|
552
|
+
if (remoteHandler) {
|
|
553
|
+
await remoteHandler("setRestoreRoute", { id, restoreRoute });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
await this.ensureLoaded();
|
|
557
|
+
const snapshots = await setSnapshotRestoreRoute(
|
|
558
|
+
id,
|
|
559
|
+
restoreRoute,
|
|
560
|
+
this.state.snapshots
|
|
561
|
+
);
|
|
471
562
|
this.setState({ snapshots });
|
|
472
563
|
}
|
|
473
564
|
|
|
565
|
+
/** Copy a point so a variation can start from a known-good base. */
|
|
566
|
+
async duplicate(id: string): Promise<SnapshotMeta | null> {
|
|
567
|
+
if (remoteHandler) {
|
|
568
|
+
const result = (await remoteHandler("duplicate", { id })) as {
|
|
569
|
+
snapshot: SnapshotMeta;
|
|
570
|
+
} | null;
|
|
571
|
+
return result?.snapshot ?? null;
|
|
572
|
+
}
|
|
573
|
+
await this.ensureLoaded();
|
|
574
|
+
const source = this.state.snapshots.find((s) => s.id === id);
|
|
575
|
+
if (!source) return null;
|
|
576
|
+
const result = await duplicateSnapshot(
|
|
577
|
+
id,
|
|
578
|
+
`${source.name} copy`,
|
|
579
|
+
this.state.snapshots
|
|
580
|
+
);
|
|
581
|
+
if (!result) return null;
|
|
582
|
+
this.setState({ snapshots: result.metas });
|
|
583
|
+
return result.meta;
|
|
584
|
+
}
|
|
585
|
+
|
|
474
586
|
async rename(id: string, name: string): Promise<void> {
|
|
475
587
|
if (remoteHandler) {
|
|
476
588
|
await remoteHandler("rename", { id, name });
|
|
@@ -496,6 +608,25 @@ class TimeMachineStore {
|
|
|
496
608
|
this.setState({ snapshots });
|
|
497
609
|
}
|
|
498
610
|
|
|
611
|
+
/**
|
|
612
|
+
* Save (or clear with null/empty) the items every later restore of this
|
|
613
|
+
* snapshot must leave untouched — what the restore preview's checkboxes
|
|
614
|
+
* write when you save a selection.
|
|
615
|
+
*/
|
|
616
|
+
async setExclusions(id: string, excludedKeys: string[] | null): Promise<void> {
|
|
617
|
+
if (remoteHandler) {
|
|
618
|
+
await remoteHandler("setExclusions", { id, excludedKeys });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
await this.ensureLoaded();
|
|
622
|
+
const snapshots = await setSnapshotExclusions(
|
|
623
|
+
id,
|
|
624
|
+
excludedKeys,
|
|
625
|
+
this.state.snapshots
|
|
626
|
+
);
|
|
627
|
+
this.setState({ snapshots });
|
|
628
|
+
}
|
|
629
|
+
|
|
499
630
|
clearLastOutcome(): void {
|
|
500
631
|
this.setState({ lastOutcome: null });
|
|
501
632
|
}
|
|
@@ -138,6 +138,36 @@ export const timeMachineSyncAdapter = {
|
|
|
138
138
|
return { renamed: true, id, name };
|
|
139
139
|
},
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Save (or clear with null/empty) the items a restore of this snapshot
|
|
143
|
+
* must leave alone — the persisted form of the preview's checkboxes.
|
|
144
|
+
*/
|
|
145
|
+
setExclusions: async (params: unknown) => {
|
|
146
|
+
const { id, excludedKeys } =
|
|
147
|
+
(params as { id?: string; excludedKeys?: string[] | null }) ?? {};
|
|
148
|
+
if (!id) throw new Error("setExclusions requires `id`");
|
|
149
|
+
await timeMachineStore.setExclusions(id, excludedKeys ?? null);
|
|
150
|
+
return { id, excluded: excludedKeys?.length ?? 0 };
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/** Turn a restore point's route restore on or off. */
|
|
154
|
+
setRestoreRoute: async (params: unknown) => {
|
|
155
|
+
const { id, restoreRoute } =
|
|
156
|
+
(params as { id?: string; restoreRoute?: boolean }) ?? {};
|
|
157
|
+
if (!id) throw new Error("setRestoreRoute requires `id`");
|
|
158
|
+
await timeMachineStore.setRestoreRoute(id, restoreRoute !== false);
|
|
159
|
+
return { id, restoreRoute: restoreRoute !== false };
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/** Copy a restore point so a variation can start from a known base. */
|
|
163
|
+
duplicate: async (params: unknown) => {
|
|
164
|
+
const { id } = (params as { id?: string }) ?? {};
|
|
165
|
+
if (!id) throw new Error("duplicate requires `id`");
|
|
166
|
+
const snapshot = await timeMachineStore.duplicate(id);
|
|
167
|
+
if (!snapshot) throw new Error(`No snapshot with id "${id}"`);
|
|
168
|
+
return { snapshot };
|
|
169
|
+
},
|
|
170
|
+
|
|
141
171
|
/** Save (or clear with null/empty) a snapshot's targeted-restore scope. */
|
|
142
172
|
setScope: async (params: unknown) => {
|
|
143
173
|
const { id, scope } =
|