@buoy-gg/time-machine 7.0.17 → 7.0.18
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/HeaderReloadButton.js +61 -0
- package/lib/commonjs/components/HeaderReloadButton.js.map +1 -0
- package/lib/commonjs/components/TimeMachineBody.js +343 -127
- package/lib/commonjs/components/TimeMachineBody.js.map +1 -1
- package/lib/commonjs/components/TimeMachineModal.js +1 -0
- package/lib/commonjs/components/TimeMachineModal.js.map +1 -1
- package/lib/commonjs/components/preview/ItemDetailScreen.js +189 -0
- package/lib/commonjs/components/preview/ItemDetailScreen.js.map +1 -0
- package/lib/commonjs/components/preview/RestorePreviewScreen.js +1056 -0
- package/lib/commonjs/components/preview/RestorePreviewScreen.js.map +1 -0
- package/lib/commonjs/components/preview/verdictMeta.js +58 -0
- package/lib/commonjs/components/preview/verdictMeta.js.map +1 -0
- package/lib/commonjs/preset.js +11 -2
- package/lib/commonjs/preset.js.map +1 -1
- package/lib/commonjs/store/snapshotVault.js +19 -0
- package/lib/commonjs/store/snapshotVault.js.map +1 -1
- package/lib/commonjs/store/timeMachineStore.js +298 -7
- package/lib/commonjs/store/timeMachineStore.js.map +1 -1
- package/lib/commonjs/sync/timeMachineSyncAdapter.js +62 -2
- package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/module/components/HeaderReloadButton.js +56 -0
- package/lib/module/components/HeaderReloadButton.js.map +1 -0
- package/lib/module/components/TimeMachineBody.js +345 -129
- package/lib/module/components/TimeMachineBody.js.map +1 -1
- package/lib/module/components/TimeMachineModal.js +1 -0
- package/lib/module/components/TimeMachineModal.js.map +1 -1
- package/lib/module/components/preview/ItemDetailScreen.js +184 -0
- package/lib/module/components/preview/ItemDetailScreen.js.map +1 -0
- package/lib/module/components/preview/RestorePreviewScreen.js +1051 -0
- package/lib/module/components/preview/RestorePreviewScreen.js.map +1 -0
- package/lib/module/components/preview/verdictMeta.js +53 -0
- package/lib/module/components/preview/verdictMeta.js.map +1 -0
- package/lib/module/preset.js +11 -2
- package/lib/module/preset.js.map +1 -1
- package/lib/module/store/snapshotVault.js +18 -0
- package/lib/module/store/snapshotVault.js.map +1 -1
- package/lib/module/store/timeMachineStore.js +299 -8
- package/lib/module/store/timeMachineStore.js.map +1 -1
- package/lib/module/sync/timeMachineSyncAdapter.js +62 -2
- package/lib/module/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/typescript/components/HeaderReloadButton.d.ts +2 -0
- package/lib/typescript/components/HeaderReloadButton.d.ts.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/preview/ItemDetailScreen.d.ts +11 -0
- package/lib/typescript/components/preview/ItemDetailScreen.d.ts.map +1 -0
- package/lib/typescript/components/preview/RestorePreviewScreen.d.ts +15 -0
- package/lib/typescript/components/preview/RestorePreviewScreen.d.ts.map +1 -0
- package/lib/typescript/components/preview/verdictMeta.d.ts +10 -0
- package/lib/typescript/components/preview/verdictMeta.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +15 -3
- package/lib/typescript/preset.d.ts.map +1 -1
- package/lib/typescript/store/snapshotVault.d.ts +7 -0
- package/lib/typescript/store/snapshotVault.d.ts.map +1 -1
- package/lib/typescript/store/timeMachineStore.d.ts +49 -2
- package/lib/typescript/store/timeMachineStore.d.ts.map +1 -1
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts +36 -0
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +1 -1
- package/package.json +13 -6
- package/src/components/HeaderReloadButton.tsx +45 -0
- package/src/components/TimeMachineBody.tsx +416 -137
- package/src/components/TimeMachineModal.tsx +1 -0
- package/src/components/preview/ItemDetailScreen.tsx +172 -0
- package/src/components/preview/RestorePreviewScreen.tsx +1089 -0
- package/src/components/preview/verdictMeta.ts +40 -0
- package/src/preset.tsx +11 -2
- package/src/store/snapshotVault.ts +28 -0
- package/src/store/timeMachineStore.ts +330 -7
- package/src/sync/timeMachineSyncAdapter.ts +63 -3
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Time Machine body — capture bar, provider status strip
|
|
3
|
-
* the
|
|
4
|
-
*
|
|
2
|
+
* Time Machine body — snapshot list, capture bar, provider status strip and
|
|
3
|
+
* the Restore Preview flow (tap a snapshot row → item-level diff of what a
|
|
4
|
+
* restore would change; tap an item → tree/split detail).
|
|
5
|
+
*
|
|
6
|
+
* Shared by the RN modal and the inline (desktop) tool view. The host must
|
|
7
|
+
* NOT wrap this in a scroll view — each screen owns its scrolling (the
|
|
8
|
+
* preview is a virtualized FlatList; wrapping it would defeat that).
|
|
5
9
|
*/
|
|
6
10
|
import { useState } from "react";
|
|
7
11
|
import {
|
|
8
12
|
Alert,
|
|
9
13
|
Platform,
|
|
14
|
+
ScrollView,
|
|
10
15
|
StyleSheet,
|
|
11
16
|
Text,
|
|
12
17
|
TextInput,
|
|
13
18
|
TouchableOpacity,
|
|
14
19
|
View,
|
|
15
20
|
} from "react-native";
|
|
16
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
Plus,
|
|
23
|
+
EventPickerModal,
|
|
24
|
+
Home,
|
|
25
|
+
Trash2,
|
|
26
|
+
buoyColors,
|
|
27
|
+
} from "@buoy-gg/shared-ui";
|
|
28
|
+
import {
|
|
29
|
+
JotaiIcon,
|
|
30
|
+
QueryIcon,
|
|
31
|
+
ReduxIcon,
|
|
32
|
+
StorageIcon,
|
|
33
|
+
TimeMachineIcon,
|
|
34
|
+
ZustandIcon,
|
|
35
|
+
} from "@buoy-gg/floating-tools-core";
|
|
36
|
+
import type {
|
|
37
|
+
EventPickerItem,
|
|
38
|
+
StateSnapshotPreviewItem,
|
|
39
|
+
} from "@buoy-gg/shared-ui";
|
|
40
|
+
import { useToolHost } from "@buoy-gg/core";
|
|
17
41
|
import { useIsPro } from "@buoy-gg/license";
|
|
18
42
|
import { useSnapshotProviders, useTimeMachineState } from "../hooks/useTimeMachine";
|
|
19
43
|
import { timeMachineStore } from "../store/timeMachineStore";
|
|
20
|
-
import type { RestoreOutcome } from "../store/timeMachineStore";
|
|
44
|
+
import type { PreviewSideRef, RestoreOutcome } from "../store/timeMachineStore";
|
|
21
45
|
import type { SnapshotMeta } from "../store/snapshotVault";
|
|
46
|
+
import { RestorePreviewScreen } from "./preview/RestorePreviewScreen";
|
|
47
|
+
import { ItemDetailScreen } from "./preview/ItemDetailScreen";
|
|
22
48
|
|
|
23
49
|
const FREE_SNAPSHOT_LIMIT = 2;
|
|
24
50
|
|
|
51
|
+
const SOURCE_ICONS: Record<
|
|
52
|
+
string,
|
|
53
|
+
React.ComponentType<{ size?: number; color?: string }>
|
|
54
|
+
> = {
|
|
55
|
+
storage: StorageIcon,
|
|
56
|
+
redux: ReduxIcon,
|
|
57
|
+
zustand: ZustandIcon,
|
|
58
|
+
jotai: JotaiIcon,
|
|
59
|
+
query: QueryIcon,
|
|
60
|
+
};
|
|
61
|
+
|
|
25
62
|
function timeAgo(ts: number): string {
|
|
26
63
|
const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
27
64
|
if (s < 60) return `${s}s ago`;
|
|
@@ -104,12 +141,31 @@ function OutcomePanel({ outcome }: { outcome: RestoreOutcome }) {
|
|
|
104
141
|
);
|
|
105
142
|
}
|
|
106
143
|
|
|
144
|
+
type Screen =
|
|
145
|
+
| { kind: "list" }
|
|
146
|
+
| { kind: "preview" }
|
|
147
|
+
| {
|
|
148
|
+
kind: "item";
|
|
149
|
+
sourceId: string;
|
|
150
|
+
sourceLabel: string;
|
|
151
|
+
item: StateSnapshotPreviewItem;
|
|
152
|
+
};
|
|
153
|
+
|
|
107
154
|
export function TimeMachineBody() {
|
|
108
|
-
const { snapshots, hasLoaded, busy, lastOutcome, remoteProviders } =
|
|
155
|
+
const { snapshots, hasLoaded, busy, lastOutcome, preview, remoteProviders } =
|
|
109
156
|
useTimeMachineState();
|
|
110
157
|
const localProviders = useSnapshotProviders();
|
|
111
158
|
const isPro = useIsPro();
|
|
159
|
+
const host = useToolHost();
|
|
112
160
|
const [name, setName] = useState("");
|
|
161
|
+
const [screen, setScreen] = useState<Screen>({ kind: "list" });
|
|
162
|
+
const [picker, setPicker] = useState<"left" | "right" | null>(null);
|
|
163
|
+
|
|
164
|
+
// Header search (when hosted by the core shell) filters the snapshot list.
|
|
165
|
+
const listQuery = host.search.trim().toLowerCase();
|
|
166
|
+
const visibleSnapshots = listQuery
|
|
167
|
+
? snapshots.filter((s) => s.name.toLowerCase().includes(listQuery))
|
|
168
|
+
: snapshots;
|
|
113
169
|
|
|
114
170
|
// On-device: live registry probes. Buoy Desktop: statuses mirrored from
|
|
115
171
|
// the device's sync payload.
|
|
@@ -126,12 +182,11 @@ export function TimeMachineBody() {
|
|
|
126
182
|
setName("");
|
|
127
183
|
};
|
|
128
184
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const restore = (meta: SnapshotMeta, mode: "live" | "reload") => {
|
|
185
|
+
const restore = (
|
|
186
|
+
meta: SnapshotMeta,
|
|
187
|
+
mode: "live" | "reload",
|
|
188
|
+
excludeKeys?: string[]
|
|
189
|
+
) => {
|
|
135
190
|
if (busy) return;
|
|
136
191
|
if (meta.baseline) {
|
|
137
192
|
confirmDestructive(
|
|
@@ -142,7 +197,7 @@ export function TimeMachineBody() {
|
|
|
142
197
|
);
|
|
143
198
|
return;
|
|
144
199
|
}
|
|
145
|
-
void timeMachineStore.restore(meta.id, mode);
|
|
200
|
+
void timeMachineStore.restore(meta.id, mode, excludeKeys);
|
|
146
201
|
};
|
|
147
202
|
|
|
148
203
|
const remove = (meta: SnapshotMeta) => {
|
|
@@ -154,27 +209,181 @@ export function TimeMachineBody() {
|
|
|
154
209
|
);
|
|
155
210
|
};
|
|
156
211
|
|
|
212
|
+
const wipeAll = () => {
|
|
213
|
+
if (busy) return;
|
|
214
|
+
confirmDestructive(
|
|
215
|
+
"Wipe all app state?",
|
|
216
|
+
"This deletes all app storage and reloads the app — like a fresh install.",
|
|
217
|
+
"Wipe & reload",
|
|
218
|
+
() => void timeMachineStore.wipeAll()
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const openPreview = (meta: SnapshotMeta) => {
|
|
223
|
+
void timeMachineStore.buildPreview({ snapshotId: meta.id });
|
|
224
|
+
setScreen({ kind: "preview" });
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const closePreview = () => {
|
|
228
|
+
timeMachineStore.clearPreview();
|
|
229
|
+
setScreen({ kind: "list" });
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// ── Preview / item screens ──
|
|
233
|
+
if (screen.kind === "item" && preview) {
|
|
234
|
+
return (
|
|
235
|
+
<ItemDetailScreen
|
|
236
|
+
sourceLabel={screen.sourceLabel}
|
|
237
|
+
item={screen.item}
|
|
238
|
+
leftLabel={
|
|
239
|
+
preview.left === "current"
|
|
240
|
+
? "CURRENT"
|
|
241
|
+
: snapshots.find(
|
|
242
|
+
(s) => s.id === (preview.left as { snapshotId: string }).snapshotId
|
|
243
|
+
)?.name ?? "snapshot"
|
|
244
|
+
}
|
|
245
|
+
rightLabel={
|
|
246
|
+
preview.right === "current"
|
|
247
|
+
? "CURRENT"
|
|
248
|
+
: snapshots.find(
|
|
249
|
+
(s) =>
|
|
250
|
+
s.id === (preview.right as { snapshotId: string }).snapshotId
|
|
251
|
+
)?.name ?? "snapshot"
|
|
252
|
+
}
|
|
253
|
+
onBack={() => setScreen({ kind: "preview" })}
|
|
254
|
+
/>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (screen.kind === "preview" && preview) {
|
|
259
|
+
const rightMeta =
|
|
260
|
+
preview.right === "current"
|
|
261
|
+
? undefined
|
|
262
|
+
: snapshots.find(
|
|
263
|
+
(s) => s.id === (preview.right as { snapshotId: string }).snapshotId
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const pickerItems: EventPickerItem[] = [];
|
|
267
|
+
if (picker) {
|
|
268
|
+
const otherRef = picker === "left" ? preview.right : preview.left;
|
|
269
|
+
const otherId =
|
|
270
|
+
otherRef === "current" ? null : (otherRef as { snapshotId: string }).snapshotId;
|
|
271
|
+
if (picker === "left") {
|
|
272
|
+
pickerItems.push({
|
|
273
|
+
index: 0,
|
|
274
|
+
label: "Current state",
|
|
275
|
+
timestamp: "live",
|
|
276
|
+
relativeTime: "now",
|
|
277
|
+
selected: preview.left === "current",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
snapshots.forEach((s, i) => {
|
|
281
|
+
const ref = { snapshotId: s.id };
|
|
282
|
+
const current = picker === "left" ? preview.left : preview.right;
|
|
283
|
+
pickerItems.push({
|
|
284
|
+
index: picker === "left" ? i + 1 : i,
|
|
285
|
+
label: s.baseline ? `⌂ ${s.name}` : s.name,
|
|
286
|
+
timestamp: new Date(s.createdAt).toLocaleTimeString(),
|
|
287
|
+
relativeTime: timeAgo(s.createdAt),
|
|
288
|
+
disabled: s.id === otherId || (picker === "right" && s.baseline),
|
|
289
|
+
selected:
|
|
290
|
+
current !== "current" &&
|
|
291
|
+
(current as { snapshotId: string }).snapshotId === s.id,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const onPickerSelect = (index: number) => {
|
|
297
|
+
const side = picker;
|
|
298
|
+
setPicker(null);
|
|
299
|
+
if (!side) return;
|
|
300
|
+
let ref: PreviewSideRef;
|
|
301
|
+
if (side === "left") {
|
|
302
|
+
ref = index === 0 ? "current" : { snapshotId: snapshots[index - 1].id };
|
|
303
|
+
void timeMachineStore.buildPreview(preview.right, ref);
|
|
304
|
+
} else {
|
|
305
|
+
ref = { snapshotId: snapshots[index].id };
|
|
306
|
+
void timeMachineStore.buildPreview(ref, preview.left);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<View style={styles.screenFill}>
|
|
312
|
+
<RestorePreviewScreen
|
|
313
|
+
preview={preview}
|
|
314
|
+
snapshots={snapshots}
|
|
315
|
+
busy={busy !== null}
|
|
316
|
+
onBack={closePreview}
|
|
317
|
+
onOpenItem={(sourceId, item) => {
|
|
318
|
+
const source = preview.sources.find((s) => s.id === sourceId);
|
|
319
|
+
setScreen({
|
|
320
|
+
kind: "item",
|
|
321
|
+
sourceId,
|
|
322
|
+
sourceLabel: source?.label ?? sourceId,
|
|
323
|
+
item,
|
|
324
|
+
});
|
|
325
|
+
}}
|
|
326
|
+
onRestore={(mode, excludeKeys) => {
|
|
327
|
+
if (!rightMeta || busy) return;
|
|
328
|
+
if (rightMeta.baseline) {
|
|
329
|
+
confirmDestructive(
|
|
330
|
+
"Restore fresh install?",
|
|
331
|
+
"This wipes all app storage and reloads the app.",
|
|
332
|
+
"Wipe & reload",
|
|
333
|
+
() => void timeMachineStore.restore(rightMeta.id, "reload")
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Land back on the list, where the outcome panel reports the
|
|
338
|
+
// results — the preview's diff is stale the moment restore runs.
|
|
339
|
+
void timeMachineStore
|
|
340
|
+
.restore(rightMeta.id, mode, excludeKeys)
|
|
341
|
+
.then(() => closePreview());
|
|
342
|
+
}}
|
|
343
|
+
onPickSide={setPicker}
|
|
344
|
+
onRefresh={() =>
|
|
345
|
+
void timeMachineStore.buildPreview(preview.right, preview.left)
|
|
346
|
+
}
|
|
347
|
+
/>
|
|
348
|
+
{picker && (
|
|
349
|
+
<EventPickerModal
|
|
350
|
+
isOpen
|
|
351
|
+
onClose={() => setPicker(null)}
|
|
352
|
+
title={picker === "left" ? "Compare from" : "Compare to"}
|
|
353
|
+
position={picker}
|
|
354
|
+
items={pickerItems}
|
|
355
|
+
onSelect={onPickerSelect}
|
|
356
|
+
/>
|
|
357
|
+
)}
|
|
358
|
+
</View>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── List screen ──
|
|
157
363
|
const unavailable = providers.filter(
|
|
158
364
|
(p) => p.detail && (!p.canCapture || !p.canRestore)
|
|
159
365
|
);
|
|
160
366
|
|
|
161
367
|
return (
|
|
162
|
-
<
|
|
368
|
+
<ScrollView style={styles.screenFill} contentContainerStyle={styles.container}>
|
|
163
369
|
{lastOutcome && <OutcomePanel outcome={lastOutcome} />}
|
|
164
370
|
|
|
165
|
-
{/* Capture
|
|
166
|
-
<View style={styles.
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
371
|
+
{/* Capture card */}
|
|
372
|
+
<View style={styles.captureCard}>
|
|
373
|
+
<View style={styles.captureField}>
|
|
374
|
+
<Plus size={14} color={buoyColors.textMuted} />
|
|
375
|
+
<TextInput
|
|
376
|
+
style={styles.captureInput}
|
|
377
|
+
value={name}
|
|
378
|
+
onChangeText={setName}
|
|
379
|
+
placeholder="Name this restore point…"
|
|
380
|
+
placeholderTextColor={buoyColors.textMuted}
|
|
381
|
+
editable={!captureDisabled}
|
|
382
|
+
onSubmitEditing={capture}
|
|
383
|
+
returnKeyType="done"
|
|
384
|
+
testID="time-machine-name-input"
|
|
385
|
+
/>
|
|
386
|
+
</View>
|
|
178
387
|
<TouchableOpacity
|
|
179
388
|
style={[styles.captureButton, captureDisabled && styles.buttonDisabled]}
|
|
180
389
|
onPress={capture}
|
|
@@ -186,16 +395,6 @@ export function TimeMachineBody() {
|
|
|
186
395
|
</Text>
|
|
187
396
|
</TouchableOpacity>
|
|
188
397
|
</View>
|
|
189
|
-
<TouchableOpacity
|
|
190
|
-
style={[styles.baselineButton, captureDisabled && styles.buttonDisabled]}
|
|
191
|
-
onPress={captureBaseline}
|
|
192
|
-
disabled={captureDisabled}
|
|
193
|
-
testID="time-machine-capture-baseline"
|
|
194
|
-
>
|
|
195
|
-
<Text style={styles.baselineButtonText}>
|
|
196
|
-
⌂ CAPTURE FRESH-INSTALL BASELINE
|
|
197
|
-
</Text>
|
|
198
|
-
</TouchableOpacity>
|
|
199
398
|
{atFreeLimit && (
|
|
200
399
|
<Text style={styles.freeLimitNote}>
|
|
201
400
|
Free plan: {FREE_SNAPSHOT_LIMIT} restore points. Upgrade to Pro for
|
|
@@ -203,24 +402,30 @@ export function TimeMachineBody() {
|
|
|
203
402
|
</Text>
|
|
204
403
|
)}
|
|
205
404
|
|
|
206
|
-
{/*
|
|
207
|
-
<View style={styles.
|
|
208
|
-
{
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
405
|
+
{/* Sources — detection at a glance */}
|
|
406
|
+
<View style={styles.sectionRow}>
|
|
407
|
+
<Text style={styles.sectionTitle}>Sources</Text>
|
|
408
|
+
<View style={styles.sourceTiles}>
|
|
409
|
+
{providers.map((p) => {
|
|
410
|
+
const IconComp = SOURCE_ICONS[p.id] ?? TimeMachineIcon;
|
|
411
|
+
const dotColor = !p.canCapture
|
|
412
|
+
? buoyColors.textMuted
|
|
413
|
+
: p.canRestore
|
|
414
|
+
? buoyColors.success
|
|
415
|
+
: buoyColors.warning;
|
|
416
|
+
return (
|
|
417
|
+
<View key={p.id} style={styles.sourceTile}>
|
|
418
|
+
<IconComp size={16} />
|
|
419
|
+
<View
|
|
420
|
+
style={[styles.sourceTileDot, { backgroundColor: dotColor }]}
|
|
421
|
+
/>
|
|
422
|
+
</View>
|
|
423
|
+
);
|
|
424
|
+
})}
|
|
425
|
+
{providers.length === 0 && (
|
|
426
|
+
<Text style={styles.emptyText}>none registered</Text>
|
|
427
|
+
)}
|
|
428
|
+
</View>
|
|
224
429
|
</View>
|
|
225
430
|
{unavailable.map((p) => (
|
|
226
431
|
<Text key={p.id} style={styles.providerDetail}>
|
|
@@ -228,86 +433,129 @@ export function TimeMachineBody() {
|
|
|
228
433
|
</Text>
|
|
229
434
|
))}
|
|
230
435
|
|
|
231
|
-
{/*
|
|
436
|
+
{/* Restore points */}
|
|
437
|
+
<Text style={[styles.sectionTitle, styles.sectionSpacing]}>
|
|
438
|
+
Restore points
|
|
439
|
+
</Text>
|
|
232
440
|
{hasLoaded && snapshots.length === 0 && (
|
|
233
441
|
<Text style={styles.emptyText}>
|
|
234
|
-
No
|
|
235
|
-
|
|
442
|
+
No restore points yet. Set up the state you want to return to, then
|
|
443
|
+
capture it.
|
|
236
444
|
</Text>
|
|
237
445
|
)}
|
|
238
|
-
{
|
|
446
|
+
{visibleSnapshots.map((meta) => {
|
|
239
447
|
const warnings = warningCount(meta);
|
|
240
448
|
return (
|
|
241
|
-
<
|
|
449
|
+
<TouchableOpacity
|
|
450
|
+
key={meta.id}
|
|
451
|
+
style={styles.snapshotCard}
|
|
452
|
+
onPress={() => openPreview(meta)}
|
|
453
|
+
testID={`time-machine-snap-${meta.id}`}
|
|
454
|
+
>
|
|
455
|
+
<View style={styles.snapshotIconTile}>
|
|
456
|
+
{meta.baseline ? (
|
|
457
|
+
<Home size={17} color={buoyColors.textSecondary} />
|
|
458
|
+
) : (
|
|
459
|
+
<TimeMachineIcon size={18} />
|
|
460
|
+
)}
|
|
461
|
+
</View>
|
|
242
462
|
<View style={styles.snapshotInfo}>
|
|
243
463
|
<Text style={styles.snapshotName} numberOfLines={1}>
|
|
244
|
-
{meta.baseline ? "⌂ " : ""}
|
|
245
464
|
{meta.name}
|
|
246
465
|
</Text>
|
|
247
|
-
<Text style={styles.snapshotMeta}>
|
|
248
|
-
{timeAgo(meta.createdAt)}
|
|
466
|
+
<Text style={styles.snapshotMeta} numberOfLines={1}>
|
|
249
467
|
{meta.baseline
|
|
250
|
-
? "
|
|
251
|
-
:
|
|
468
|
+
? "wipes to fresh install"
|
|
469
|
+
: `${timeAgo(meta.createdAt)} · ${formatBytes(meta.sizeBytes)} · ${Object.keys(meta.sources).length} sources`}
|
|
470
|
+
{meta.scope?.length
|
|
471
|
+
? ` · scoped (${meta.scope.length})`
|
|
472
|
+
: ""}
|
|
252
473
|
{warnings > 0 ? ` · ⚠ ${warnings}` : ""}
|
|
253
474
|
</Text>
|
|
254
475
|
</View>
|
|
255
|
-
{!meta.baseline
|
|
476
|
+
{!meta.baseline ? (
|
|
256
477
|
<TouchableOpacity
|
|
257
|
-
style={[styles.
|
|
478
|
+
style={[styles.rowAction, busy && styles.buttonDisabled]}
|
|
258
479
|
onPress={() => restore(meta, "live")}
|
|
259
480
|
disabled={busy !== null}
|
|
481
|
+
accessibilityLabel="Restore"
|
|
482
|
+
>
|
|
483
|
+
<Text style={styles.rowActionLabel}>RESTORE</Text>
|
|
484
|
+
</TouchableOpacity>
|
|
485
|
+
) : (
|
|
486
|
+
<TouchableOpacity
|
|
487
|
+
style={[styles.rowAction, busy && styles.buttonDisabled]}
|
|
488
|
+
onPress={() => restore(meta, "reload")}
|
|
489
|
+
disabled={busy !== null}
|
|
490
|
+
accessibilityLabel="Wipe and reload"
|
|
260
491
|
>
|
|
261
|
-
<Text style={styles.
|
|
492
|
+
<Text style={styles.rowActionLabelDanger}>WIPE</Text>
|
|
262
493
|
</TouchableOpacity>
|
|
263
494
|
)}
|
|
264
495
|
<TouchableOpacity
|
|
265
|
-
style={
|
|
266
|
-
onPress={() => restore(meta, "reload")}
|
|
267
|
-
disabled={busy !== null}
|
|
268
|
-
>
|
|
269
|
-
<Text style={styles.rowButtonText}>
|
|
270
|
-
{meta.baseline ? "RESTORE" : "↻"}
|
|
271
|
-
</Text>
|
|
272
|
-
</TouchableOpacity>
|
|
273
|
-
<TouchableOpacity
|
|
274
|
-
style={styles.deleteButton}
|
|
496
|
+
style={styles.rowAction}
|
|
275
497
|
onPress={() => remove(meta)}
|
|
276
498
|
hitSlop={{ top: 8, bottom: 8, left: 4, right: 8 }}
|
|
499
|
+
accessibilityLabel="Delete"
|
|
277
500
|
>
|
|
278
|
-
<
|
|
501
|
+
<Trash2 size={14} color={buoyColors.error} />
|
|
279
502
|
</TouchableOpacity>
|
|
280
|
-
</
|
|
503
|
+
</TouchableOpacity>
|
|
281
504
|
);
|
|
282
505
|
})}
|
|
283
|
-
|
|
506
|
+
|
|
507
|
+
{/* Destructive utility — deliberately last and understated. */}
|
|
508
|
+
<TouchableOpacity
|
|
509
|
+
style={[styles.wipeAllButton, busy && styles.buttonDisabled]}
|
|
510
|
+
onPress={wipeAll}
|
|
511
|
+
disabled={busy !== null}
|
|
512
|
+
testID="time-machine-wipe-all"
|
|
513
|
+
>
|
|
514
|
+
<Trash2 size={13} color={buoyColors.error} />
|
|
515
|
+
<Text style={styles.wipeAllText}>Wipe all app state…</Text>
|
|
516
|
+
</TouchableOpacity>
|
|
517
|
+
<Text style={styles.wipeAllCaption}>
|
|
518
|
+
Deletes all app storage and reloads — like a fresh install.
|
|
519
|
+
</Text>
|
|
520
|
+
</ScrollView>
|
|
284
521
|
);
|
|
285
522
|
}
|
|
286
523
|
|
|
287
524
|
const styles = StyleSheet.create({
|
|
525
|
+
screenFill: {
|
|
526
|
+
flex: 1,
|
|
527
|
+
},
|
|
288
528
|
container: {
|
|
289
529
|
padding: 12,
|
|
290
530
|
gap: 8,
|
|
291
531
|
},
|
|
292
|
-
|
|
532
|
+
captureCard: {
|
|
293
533
|
flexDirection: "row",
|
|
534
|
+
alignItems: "center",
|
|
294
535
|
gap: 8,
|
|
295
536
|
},
|
|
296
|
-
|
|
537
|
+
captureField: {
|
|
297
538
|
flex: 1,
|
|
298
|
-
|
|
539
|
+
flexDirection: "row",
|
|
540
|
+
alignItems: "center",
|
|
541
|
+
gap: 8,
|
|
542
|
+
backgroundColor: buoyColors.card,
|
|
299
543
|
borderColor: buoyColors.border,
|
|
300
544
|
borderWidth: 1,
|
|
301
|
-
borderRadius:
|
|
302
|
-
color: buoyColors.text,
|
|
545
|
+
borderRadius: 10,
|
|
303
546
|
paddingHorizontal: 10,
|
|
304
|
-
|
|
547
|
+
},
|
|
548
|
+
captureInput: {
|
|
549
|
+
flex: 1,
|
|
550
|
+
color: buoyColors.text,
|
|
305
551
|
fontSize: 13,
|
|
552
|
+
paddingVertical: 9,
|
|
306
553
|
},
|
|
307
554
|
captureButton: {
|
|
308
555
|
backgroundColor: buoyColors.primary,
|
|
309
|
-
borderRadius:
|
|
310
|
-
paddingHorizontal:
|
|
556
|
+
borderRadius: 10,
|
|
557
|
+
paddingHorizontal: 16,
|
|
558
|
+
paddingVertical: 10,
|
|
311
559
|
justifyContent: "center",
|
|
312
560
|
},
|
|
313
561
|
captureButtonText: {
|
|
@@ -316,19 +564,6 @@ const styles = StyleSheet.create({
|
|
|
316
564
|
fontWeight: "700",
|
|
317
565
|
letterSpacing: 0.5,
|
|
318
566
|
},
|
|
319
|
-
baselineButton: {
|
|
320
|
-
borderColor: buoyColors.border,
|
|
321
|
-
borderWidth: 1,
|
|
322
|
-
borderRadius: 6,
|
|
323
|
-
paddingVertical: 8,
|
|
324
|
-
alignItems: "center",
|
|
325
|
-
},
|
|
326
|
-
baselineButtonText: {
|
|
327
|
-
color: buoyColors.textSecondary,
|
|
328
|
-
fontSize: 11,
|
|
329
|
-
fontWeight: "600",
|
|
330
|
-
letterSpacing: 0.5,
|
|
331
|
-
},
|
|
332
567
|
buttonDisabled: {
|
|
333
568
|
opacity: 0.4,
|
|
334
569
|
},
|
|
@@ -336,83 +571,127 @@ const styles = StyleSheet.create({
|
|
|
336
571
|
color: buoyColors.warning,
|
|
337
572
|
fontSize: 11,
|
|
338
573
|
},
|
|
339
|
-
|
|
574
|
+
sectionRow: {
|
|
340
575
|
flexDirection: "row",
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
marginTop:
|
|
576
|
+
alignItems: "center",
|
|
577
|
+
justifyContent: "space-between",
|
|
578
|
+
marginTop: 12,
|
|
579
|
+
gap: 10,
|
|
580
|
+
},
|
|
581
|
+
sectionTitle: {
|
|
582
|
+
color: buoyColors.text,
|
|
583
|
+
fontSize: 14,
|
|
584
|
+
fontWeight: "700",
|
|
344
585
|
},
|
|
345
|
-
|
|
586
|
+
sectionSpacing: {
|
|
587
|
+
marginTop: 14,
|
|
588
|
+
},
|
|
589
|
+
sourceTiles: {
|
|
346
590
|
flexDirection: "row",
|
|
347
|
-
|
|
348
|
-
|
|
591
|
+
gap: 6,
|
|
592
|
+
flexWrap: "wrap",
|
|
593
|
+
justifyContent: "flex-end",
|
|
594
|
+
flex: 1,
|
|
595
|
+
},
|
|
596
|
+
sourceTile: {
|
|
597
|
+
width: 32,
|
|
598
|
+
height: 32,
|
|
599
|
+
borderRadius: 9,
|
|
349
600
|
backgroundColor: buoyColors.card,
|
|
350
601
|
borderColor: buoyColors.border,
|
|
351
602
|
borderWidth: 1,
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
paddingVertical: 3,
|
|
603
|
+
alignItems: "center",
|
|
604
|
+
justifyContent: "center",
|
|
355
605
|
},
|
|
356
|
-
|
|
606
|
+
sourceTileDot: {
|
|
607
|
+
position: "absolute",
|
|
608
|
+
top: 3,
|
|
609
|
+
right: 3,
|
|
357
610
|
width: 6,
|
|
358
611
|
height: 6,
|
|
359
612
|
borderRadius: 3,
|
|
360
613
|
},
|
|
361
|
-
providerLabel: {
|
|
362
|
-
color: buoyColors.textSecondary,
|
|
363
|
-
fontSize: 11,
|
|
364
|
-
},
|
|
365
614
|
providerDetail: {
|
|
366
615
|
color: buoyColors.textMuted,
|
|
367
616
|
fontSize: 10,
|
|
617
|
+
marginTop: 4,
|
|
368
618
|
},
|
|
369
619
|
emptyText: {
|
|
370
620
|
color: buoyColors.textMuted,
|
|
371
621
|
fontSize: 12,
|
|
372
622
|
marginTop: 8,
|
|
373
623
|
},
|
|
374
|
-
|
|
624
|
+
snapshotCard: {
|
|
375
625
|
flexDirection: "row",
|
|
376
626
|
alignItems: "center",
|
|
377
627
|
backgroundColor: buoyColors.card,
|
|
378
628
|
borderColor: buoyColors.border,
|
|
379
629
|
borderWidth: 1,
|
|
380
|
-
borderRadius:
|
|
381
|
-
|
|
382
|
-
|
|
630
|
+
borderRadius: 12,
|
|
631
|
+
paddingHorizontal: 12,
|
|
632
|
+
paddingVertical: 10,
|
|
633
|
+
marginTop: 8,
|
|
634
|
+
gap: 10,
|
|
635
|
+
},
|
|
636
|
+
snapshotIconTile: {
|
|
637
|
+
width: 36,
|
|
638
|
+
height: 36,
|
|
639
|
+
borderRadius: 10,
|
|
640
|
+
backgroundColor: buoyColors.hover,
|
|
641
|
+
alignItems: "center",
|
|
642
|
+
justifyContent: "center",
|
|
383
643
|
},
|
|
384
644
|
snapshotInfo: {
|
|
385
645
|
flex: 1,
|
|
386
646
|
minWidth: 0,
|
|
647
|
+
gap: 2,
|
|
387
648
|
},
|
|
388
649
|
snapshotName: {
|
|
389
650
|
color: buoyColors.text,
|
|
390
|
-
fontSize:
|
|
391
|
-
fontWeight: "
|
|
651
|
+
fontSize: 14,
|
|
652
|
+
fontWeight: "700",
|
|
392
653
|
},
|
|
393
654
|
snapshotMeta: {
|
|
394
655
|
color: buoyColors.textMuted,
|
|
395
656
|
fontSize: 10,
|
|
396
|
-
|
|
657
|
+
fontVariant: ["tabular-nums"],
|
|
397
658
|
},
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
paddingHorizontal: 8,
|
|
403
|
-
paddingVertical: 6,
|
|
659
|
+
rowAction: {
|
|
660
|
+
padding: 4,
|
|
661
|
+
justifyContent: "center",
|
|
662
|
+
alignItems: "center",
|
|
404
663
|
},
|
|
405
|
-
|
|
664
|
+
rowActionLabel: {
|
|
406
665
|
color: buoyColors.primary,
|
|
407
|
-
fontSize:
|
|
408
|
-
fontWeight: "
|
|
666
|
+
fontSize: 12,
|
|
667
|
+
fontWeight: "600",
|
|
409
668
|
},
|
|
410
|
-
|
|
411
|
-
|
|
669
|
+
rowActionLabelDanger: {
|
|
670
|
+
color: buoyColors.error,
|
|
671
|
+
fontSize: 12,
|
|
672
|
+
fontWeight: "600",
|
|
412
673
|
},
|
|
413
|
-
|
|
674
|
+
wipeAllButton: {
|
|
675
|
+
flexDirection: "row",
|
|
676
|
+
alignItems: "center",
|
|
677
|
+
justifyContent: "center",
|
|
678
|
+
gap: 7,
|
|
679
|
+
borderColor: `${buoyColors.error}55`,
|
|
680
|
+
borderWidth: 1,
|
|
681
|
+
borderRadius: 10,
|
|
682
|
+
paddingVertical: 10,
|
|
683
|
+
marginTop: 18,
|
|
684
|
+
},
|
|
685
|
+
wipeAllText: {
|
|
686
|
+
color: buoyColors.error,
|
|
687
|
+
fontSize: 12.5,
|
|
688
|
+
fontWeight: "600",
|
|
689
|
+
},
|
|
690
|
+
wipeAllCaption: {
|
|
414
691
|
color: buoyColors.textMuted,
|
|
415
|
-
fontSize:
|
|
692
|
+
fontSize: 10,
|
|
693
|
+
textAlign: "center",
|
|
694
|
+
marginTop: 6,
|
|
416
695
|
},
|
|
417
696
|
outcomePanel: {
|
|
418
697
|
backgroundColor: buoyColors.card,
|