Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/allmystuff-protocol/src/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ pub enum Request {
#[serde(default)]
purge: bool,
},
/// Forget **every** joined network on the daemon at once — a bulk
/// `NetworkRemove{purge:true}` that tears each down and deletes its roster +
/// signed governance state, keeping the device identity. The daemon exits
/// afterward so a fresh one reloads clean (matched by an app restart), rather
/// than a stale in-memory cache re-persisting what was just removed. Needs a
/// daemon that speaks this op; an older one replies with a parse error.
ForgetAllNetworks,
/// Factory reset — the daemon wipes its **entire** state directory
/// (`~/.myownmesh`: identity, config, every network, and the co-located
/// AllMyStuff ownership record) and exits so a fresh daemon mints a new
/// identity on empty state. The device becomes brand-new to every peer.
FactoryReset,
NetworkUpdate {
config: Value,
},
Expand Down
29 changes: 29 additions & 0 deletions gui/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,33 @@ async fn fleet_leave(state: State<'_, AppState>) -> Result<(), String> {
Ok(())
}

/// Danger Zone: leave the fleet and forget every network the daemon holds
/// (keeps this device's identity). The daemon exits afterward; the caller
/// follows with `restart_app` so the whole stack reloads clean.
#[tauri::command]
async fn reset_networking(state: State<'_, AppState>) -> Result<(), String> {
state
.node
.request("reset_networking", json!({}))
.await
.map_err(|e| e.to_string())?;
Ok(())
}

/// Danger Zone: factory reset — wipe this device's entire state (identity,
/// config, all networks, fleet ownership) so it's brand-new to every peer. The
/// node clears its ownership and the daemon wipes `~/.myownmesh` and exits; the
/// caller follows with `restart_app`.
#[tauri::command]
async fn factory_reset(state: State<'_, AppState>) -> Result<(), String> {
state
.node
.request("factory_reset", json!({}))
.await
.map_err(|e| e.to_string())?;
Ok(())
}

/// Evict a device from the fleet (owner-only; the daemon enforces it). `code`
/// is the owner's custody second factor when fleet MFA is enrolled.
#[tauri::command]
Expand Down Expand Up @@ -2505,6 +2532,8 @@ fn main() {
open_room_window,
owned_roster,
fleet_leave,
reset_networking,
factory_reset,
fleet_kick,
fleet_set_name,
fleet_grant_role,
Expand Down
30 changes: 30 additions & 0 deletions gui/src/store.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import {
fileSend,
fleetKick,
fleetLeave,
resetNetworking,
factoryReset,
fleetSetName,
fleetGrantRole,
fleetRevokeRole,
Expand Down Expand Up @@ -7722,6 +7724,34 @@ class AppStore {
// No toast — the Fleet pane drops to its "No fleet yet" state.
}

// ---- Danger Zone (Settings → Updates) ----
// Each level reboots the whole stack after it clears state. The mesh daemon
// is the real datastore, so a reset that only deletes state on one layer can
// be undone by another layer's in-memory cache re-persisting it — the reboot
// is what makes the wipe actually stick. `restartApp()` relaunches the app
// (and the supervised node + daemon come back on clean state), so these calls
// normally end by relaunching rather than returning.

/** Leave the fleet, then reboot. */
async dangerLeaveFleet() {
await this.leaveFleet();
await restartApp();
}

/** Leave the fleet and forget every network (keeps the device identity),
* then reboot. */
async dangerResetNetworking() {
await resetNetworking();
await restartApp();
}

/** Factory reset — wipe identity, config, all networks and fleet ownership,
* then reboot into a brand-new device. */
async dangerFactoryReset() {
await factoryReset();
await restartApp();
}

/** Evict a member from the fleet (owner-only). Routes through the governance
* helper, so it prompts for the custody code when fleet MFA is enrolled. */
async kickFleetMember(device: string) {
Expand Down
18 changes: 18 additions & 0 deletions gui/src/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,24 @@ export async function fleetLeave(): Promise<void> {
await invoke("fleet_leave");
}

/** Danger Zone: leave the fleet and forget every network the daemon holds,
* keeping the device identity. The daemon exits so it reloads clean — pair
* with `restartApp()`. No-op in web mode. */
export async function resetNetworking(): Promise<void> {
if (!isTauri()) return;
const { invoke } = await import("@tauri-apps/api/core");
await invoke("reset_networking");
}

/** Danger Zone: factory reset — wipe this device's entire state (identity,
* config, all networks, fleet ownership) for a brand-new start. The daemon
* wipes `~/.myownmesh` and exits — pair with `restartApp()`. No-op in web. */
export async function factoryReset(): Promise<void> {
if (!isTauri()) return;
const { invoke } = await import("@tauri-apps/api/core");
await invoke("factory_reset");
}

/** Evict a device from the fleet (owner-only; the backend enforces it).
* `code` is the owner's custody second factor when fleet MFA is enrolled.
* Throws with the reason when refused. */
Expand Down
204 changes: 204 additions & 0 deletions gui/src/ui/settings/UpdatesSection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,35 @@
const d = new Date(at * 1000);
return d.toLocaleString();
}

// ---- Danger Zone ----
// Two-click arming so a reset can't fire on a stray click. Each level reboots
// the whole stack (node + daemon + app) after clearing state: the daemon is
// the real datastore, so without the reboot an in-memory cache can re-persist
// ("resurrect") what was wiped. Desktop only — it needs the local daemon and
// a relaunch.
let armed = $state<null | "leave" | "network" | "factory">(null);
let resetting = $state<null | "leave" | "network" | "factory">(null);
let resetError = $state<string | null>(null);

async function runReset(kind: "leave" | "network" | "factory") {
if (armed !== kind) {
armed = kind; // first click arms; a second confirms
return;
}
armed = null;
resetting = kind;
resetError = null;
try {
if (kind === "leave") await app.dangerLeaveFleet();
else if (kind === "network") await app.dangerResetNetworking();
else await app.dangerFactoryReset();
// The app relaunches on success, so control doesn't normally return here.
} catch (e) {
resetError = String(e);
resetting = null;
}
}
</script>

<div class="section">
Expand Down Expand Up @@ -161,6 +190,93 @@
</section>
{/if}
{/if}

<!-- Danger Zone — desktop only (it needs the local daemon and a relaunch).
Each action reboots the node + daemon + app so cleared state genuinely
flushes instead of being resurrected from an in-memory cache. -->
{#if !web && !mobile}
<section class="danger">
<div class="danger-head">⚠ Danger Zone</div>
<p class="danger-lead">
Each of these clears state and then <b>restarts the app and the mesh
daemon</b>, so every layer reloads from disk. Without the reboot, cached
state can quietly reappear.
</p>

<div class="danger-row">
<div class="danger-copy">
<div class="danger-title">Leave the fleet</div>
<div class="danger-desc">
Remove this device from its fleet — drops ownership, the fleet key,
and the fleet's signed roster. Keeps your other meshes and settings.
</div>
</div>
<button
class="danger-btn"
class:armed={armed === "leave"}
disabled={resetting !== null}
onclick={() => runReset("leave")}
>
{resetting === "leave"
? "Restarting…"
: armed === "leave"
? "Confirm — reboots"
: "Leave fleet"}
</button>
</div>

<div class="danger-row">
<div class="danger-copy">
<div class="danger-title">Reset networking</div>
<div class="danger-desc">
Leave the fleet <b>and</b> forget every mesh — all rosters and signed
state — keeping this device's identity. A clean networking slate.
</div>
</div>
<button
class="danger-btn"
class:armed={armed === "network"}
disabled={resetting !== null}
onclick={() => runReset("network")}
>
{resetting === "network"
? "Restarting…"
: armed === "network"
? "Confirm — reboots"
: "Reset networking"}
</button>
</div>

<div class="danger-row">
<div class="danger-copy">
<div class="danger-title">Factory reset</div>
<div class="danger-desc">
Erase <b>everything</b> — identity, config, every mesh, and fleet
ownership. This device becomes brand-new to all peers. No undo.
</div>
</div>
<button
class="danger-btn nuke"
class:armed={armed === "factory"}
disabled={resetting !== null}
onclick={() => runReset("factory")}
>
{resetting === "factory"
? "Resetting…"
: armed === "factory"
? "Confirm wipe — reboots"
: "Factory reset"}
</button>
</div>

{#if armed}
<button class="danger-cancel" onclick={() => (armed = null)}>Cancel</button>
{/if}
{#if resetError}
<p class="danger-err">Reset failed: {resetError}</p>
{/if}
</section>
{/if}
</div>

<style>
Expand Down Expand Up @@ -292,4 +408,92 @@
.feed {
text-transform: capitalize;
}

/* ---- Danger Zone ---- */
.danger {
margin-top: 1.4rem;
padding: 0.9rem;
border: 1px solid var(--danger);
border-radius: var(--r-sm);
background: var(--danger-soft);
display: flex;
flex-direction: column;
gap: 0.7rem;
}
.danger-head {
color: var(--danger);
font-weight: 700;
font-size: 0.9rem;
letter-spacing: 0.02em;
}
.danger-lead {
color: var(--ink-soft);
font-size: 0.78rem;
margin: 0;
line-height: 1.4;
}
.danger-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
padding-top: 0.6rem;
border-top: 1px solid var(--line);
}
.danger-copy {
min-width: 0;
}
.danger-title {
color: var(--ink);
font-size: 0.85rem;
font-weight: 600;
}
.danger-desc {
color: var(--ink-soft);
font-size: 0.76rem;
line-height: 1.4;
margin-top: 0.15rem;
}
.danger-btn {
flex: 0 0 auto;
white-space: nowrap;
background: var(--danger-soft);
color: var(--danger);
border: 1px solid var(--danger);
border-radius: var(--r-sm);
padding: 0.4rem 0.7rem;
font-size: 0.8rem;
cursor: pointer;
}
.danger-btn:hover:not(:disabled) {
filter: brightness(1.15);
}
.danger-btn.armed {
background: var(--danger);
color: #fff;
font-weight: 600;
}
.danger-btn:disabled {
opacity: 0.6;
cursor: default;
}
.danger-cancel {
align-self: flex-start;
background: none;
border: none;
color: var(--ink-faint);
font-size: 0.76rem;
cursor: pointer;
text-decoration: underline;
padding: 0;
}
.danger-err {
color: var(--danger);
background: var(--danger-soft);
border: 1px solid var(--danger);
border-radius: var(--r-sm);
padding: 0.45rem 0.6rem;
font-size: 0.8rem;
margin: 0;
}
</style>
Loading
Loading