Make rollback a GRUB snapshot reboot
Some checks failed
dev release / build (push) Has been cancelled

Updates leads with “Boot a snapshot from GRUB (BOS snapshots)”
and links to Snapshots. Snapshots is the list to pick in GRUB,
not snapper rollback.
This commit is contained in:
Breadway 2026-08-16 00:48:25 +08:00
parent 0798fad697
commit 3aad09c9ef
4 changed files with 36 additions and 8 deletions

3
frontend/src/lib/nav.ts Normal file
View file

@ -0,0 +1,3 @@
/** Sidebar page switch. Set from +page.svelte; views call it to jump. */
export type Navigate = (page: string) => void;
export const NAVIGATE_KEY = "bos-settings-navigate";

View file

@ -42,7 +42,7 @@
if (!selected) return; if (!selected) return;
if ( if (
confirm( confirm(
`Boot into snapshot #${selected}? Snapshots on BOS are booted directly from the GRUB menu (under "BOS snapshots"), not rolled back in place. Reboot now and pick this snapshot there.`, `Reboot to pick snapshot #${selected} in GRUB? BOS boots snapshots from the GRUB “BOS snapshots” submenu. This does not run snapper rollback.`,
) )
) { ) {
invoke("reboot_system"); invoke("reboot_system");
@ -62,9 +62,10 @@
</script> </script>
<ViewScaffold title="Snapshots"> <ViewScaffold title="Snapshots">
<Hint text="This is how you undo a bad update: reboot and pick the snapshot in GRUB (BOS snapshots)." />
<Group <Group
title="System snapshots" title="What to pick in GRUB"
hint="Created automatically by snap-pac on each pacman transaction. Boot into one from the GRUB menu to recover; delete old ones here." hint="Number, date, and description match the GRUB “BOS snapshots” submenu. Reboot, then choose that entry. This page does not run snapper rollback."
wide wide
> >
<div class="list"> <div class="list">
@ -75,6 +76,11 @@
{:else if snapshots.length === 0} {:else if snapshots.length === 0}
<EmptyState icon={History} title="No snapshots yet" hint="Snapshots are created automatically on every pacman transaction." /> <EmptyState icon={History} title="No snapshots yet" hint="Snapshots are created automatically on every pacman transaction." />
{:else} {:else}
<div class="row header" aria-hidden="true">
<span class="number">#</span>
<span class="date">Date</span>
<span class="desc">Description</span>
</div>
{#each snapshots as snap (snap.number)} {#each snapshots as snap (snap.number)}
<button <button
class="row" class="row"
@ -91,7 +97,7 @@
<div class="btn-row"> <div class="btn-row">
<button onclick={refresh}>Refresh</button> <button onclick={refresh}>Refresh</button>
<button disabled={!selected} onclick={bootIntoSelected}>Boot into selected…</button> <button disabled={!selected} onclick={bootIntoSelected}>Reboot to pick in GRUB</button>
<button class="destructive" disabled={!selected} onclick={deleteSelected}>Delete selected</button> <button class="destructive" disabled={!selected} onclick={deleteSelected}>Delete selected</button>
</div> </div>
</Group> </Group>
@ -121,7 +127,15 @@
min-width: 0; min-width: 0;
} }
.row:hover { .row.header {
background: transparent;
cursor: default;
opacity: 0.55;
font-size: var(--font-size-secondary, 12px);
padding-top: 0;
}
.row:hover:not(.header) {
background-color: color-mix(in srgb, var(--surface), var(--on-surface) 8%); background-color: color-mix(in srgb, var(--surface), var(--on-surface) 8%);
} }

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { runStreamed } from "$lib/streaming"; import { runStreamed } from "$lib/streaming";
import ViewScaffold from "$lib/components/ViewScaffold.svelte"; import ViewScaffold from "$lib/components/ViewScaffold.svelte";
@ -7,9 +7,12 @@
import Hint from "$lib/components/Hint.svelte"; import Hint from "$lib/components/Hint.svelte";
import EmptyState from "$lib/components/EmptyState.svelte"; import EmptyState from "$lib/components/EmptyState.svelte";
import LogView from "$lib/components/LogView.svelte"; import LogView from "$lib/components/LogView.svelte";
import { NAVIGATE_KEY, type Navigate } from "$lib/nav";
import Download from "@lucide/svelte/icons/download"; import Download from "@lucide/svelte/icons/download";
import Cpu from "@lucide/svelte/icons/cpu"; import Cpu from "@lucide/svelte/icons/cpu";
const navigate = getContext<Navigate | undefined>(NAVIGATE_KEY);
interface PendingUpdate { interface PendingUpdate {
name: string; name: string;
current: string; current: string;
@ -156,9 +159,13 @@
<Group <Group
title="Rollback" title="Rollback"
hint="BOS pins GRUB to rootflags=subvol=@, so snapper rollback does not change the running root. Boot a snapshot from the GRUB “BOS snapshots” submenu (grub-btrfs), or use the Snapshots page to reboot and pick one there." hint="Boot a snapshot from GRUB (BOS snapshots)."
> >
<Hint text="Snapshots lists number, date, and description so you know which GRUB entry to pick. snapper rollback will not change what GRUB boots (rootflags=subvol=@)." />
<Hint text="Bakery also has bakery rollback <pkg> for a single ecosystem binary — that is not a system rollback." /> <Hint text="Bakery also has bakery rollback <pkg> for a single ecosystem binary — that is not a system rollback." />
<div class="btn-row">
<button disabled={!navigate} onclick={() => navigate?.("snapshots")}>Open Snapshots</button>
</div>
</Group> </Group>
<LogView lines={log} /> <LogView lines={log} />

View file

@ -1,14 +1,18 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount, setContext } from "svelte";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import Sidebar from "$lib/components/Sidebar.svelte"; import Sidebar from "$lib/components/Sidebar.svelte";
import Placeholder from "$lib/components/Placeholder.svelte"; import Placeholder from "$lib/components/Placeholder.svelte";
import { DEFAULT_PAGE } from "$lib/sidebar"; import { DEFAULT_PAGE } from "$lib/sidebar";
import { NAVIGATE_KEY, type Navigate } from "$lib/nav";
import { initTheme } from "$lib/theme"; import { initTheme } from "$lib/theme";
import { VIEWS } from "$lib/views/registry"; import { VIEWS } from "$lib/views/registry";
let activePage = $state(DEFAULT_PAGE); let activePage = $state(DEFAULT_PAGE);
let ActiveView = $derived(VIEWS[activePage]); let ActiveView = $derived(VIEWS[activePage]);
setContext<Navigate>(NAVIGATE_KEY, (page) => {
activePage = page;
});
onMount(() => { onMount(() => {
initTheme(); initTheme();