Profile-post backfill + prune disposable first-run persona on import

Two bugs the v0.6.2 Discover story was going to expose:

1) Many named personas don't have a profile post yet, so the planned
   profile-post-driven Discover listing would show them as headless.
   Existing personas predate the Phase 2d profile-post primitive, and
   imported personas arrive with a display_name but no matching post.

2) Fresh install always generates a blank disposable persona before
   the user can pick fresh-vs-import. If the user picks import, that
   blank persona lingers forever — visible in the Personas list, a
   potential default, confusing.

Fixes:

Profile-post backfill (node.rs):
- New `Node::backfill_profile_posts_for_named_personas`: scans posting
  identities, skips unnamed or already-covered ones, and emits a
  signed profile post at the persona's own `created_at` timestamp. Run
  once from `Node::open_with_bind` after migrations. Idempotent via
  the new `Storage::has_profile_post_by_author` check. Chronology is
  preserved (post timestamp matches persona creation), so a later
  genuine profile update the user authors always wins the `> old_ts`
  monotonicity check on receivers.
- `Node::create_posting_identity(name)`: if `name` is non-empty, emits
  the profile post inline so new named personas are Discover-able the
  moment they're created. Uses the new `publish_profile_post_as`
  helper, which signs with the persona's own secret (not the default
  posting secret) and propagates via `update_neighbor_manifests_as`.

First-run persona marker + targeted prune (node.rs + storage.rs + import):
- `Node::open_with_bind`'s auto-gen block now also writes
  `first_run_auto_persona_id = <hex>` into the settings kv. This marks
  the specific disposable persona from the fresh-install flow; later
  prune logic uses this id, not a "any empty persona" rule, so manual
  empty personas are never touched.
- `Node::try_prune_first_run_auto_persona`: deletes the marked id iff
  all four gates pass — still exists, no display_name, no authored
  posts, no authored reactions/comments, and it's no longer the
  current default. Any one failure → clear the marker and keep the
  persona. New storage helpers `has_any_post_by_author`,
  `has_any_engagement_by_author` back the check.
- `set_profile` clears the marker when a non-empty name lands on the
  marked persona (user claimed it).
- `storage.delete_setting(key)` — new one-line helper.
- `import_as_personas_cmd` in tauri-app calls the prune after import
  completes; the cmd's return message reports "(cleared blank starter
  persona)" when it fires.
- New `get_first_run_auto_persona_id` Tauri command so the frontend
  can filter the blank persona out of the Personas list while it
  still exists.
- Frontend `loadPersonas` filters the marker id out of
  `personasCache` before rendering.

Tests: 124 / 124 core tests pass.
This commit is contained in:
Scott Reimers 2026-04-23 08:47:30 -04:00
parent d990da5bda
commit e74bd4e6c6
4 changed files with 308 additions and 6 deletions

View file

@ -1416,6 +1416,20 @@ async fn get_update_channel(state: State<'_, AppNode>) -> Result<String, String>
.unwrap_or_else(|| "stable".to_string()))
}
/// Return the hex id of the disposable fresh-install persona (if it still
/// exists and hasn't been claimed). Frontend uses this to filter the blank
/// starter out of the Personas list so the user never sees a "ghost"
/// persona waiting between install and their first-run choice.
/// Returns an empty string when there's no disposable persona to hide.
#[tauri::command]
async fn get_first_run_auto_persona_id(state: State<'_, AppNode>) -> Result<String, String> {
let node = get_node(&state).await;
let storage = node.storage.get().await;
Ok(storage.get_setting("first_run_auto_persona_id")
.map_err(|e| e.to_string())?
.unwrap_or_default())
}
/// Persist the user's preferred update channel.
#[tauri::command]
async fn set_update_channel(state: State<'_, AppNode>, channel: String) -> Result<(), String> {
@ -2804,7 +2818,17 @@ async fn import_as_personas_cmd(
&node.storage,
&node.blob_store,
).await.map_err(|e| e.to_string())?;
Ok(result.message)
// Drop the disposable fresh-install persona if it's still pristine.
// Safe-by-construction: the helper bails unless ALL of [no name, no
// posts, no engagement, no longer the default] hold.
let pruned = node.try_prune_first_run_auto_persona().await
.unwrap_or(false);
let msg = if pruned {
format!("{} (cleared blank starter persona)", result.message)
} else {
result.message
};
Ok(msg)
}
#[tauri::command]
@ -3023,6 +3047,7 @@ pub fn run() {
get_update_channel,
set_update_channel,
open_url_external,
get_first_run_auto_persona_id,
list_connections,
worm_lookup,
list_social_routes,