feat(identity,update): default avatars, public blobs, long-running downloads

Follow-up to 1fb71b4b on the same v1.7.0-alpha line.

Identity avatars
  • New module `avatar.rs` generates two deterministic SVG styles keyed
    off the pubkey: a 5×5 mirrored identicon for sub-identities and a
    hexagonal-network motif for the master (seed index 0) identity.
    Both returned as base64 data URLs, so a fresh identity has a
    recognisable picture before the user uploads anything.
  • `IdentityManager::create()` and `create_from_seed()` populate
    `profile.picture` on creation. Index 0 gets the node SVG; all
    other seed-derived + ad-hoc identities get the identicon.

Blob store — public flag for profile assets
  • `BlobMeta.public` (default false) added; `BlobStore::put()` takes
    a `public: bool`. Missing in legacy meta files = false.
  • `POST /api/blob` now stores uploads with public=true and returns
    `public_url` alongside `self_test_url`. public_url is
    `http://<node-onion>/blob/<cid>` (no cap) if Tor has published the
    archipelago hidden service, else falls back to the local path.
  • `GET /blob/<cid>` bypasses the HMAC capability check when the
    requested blob is flagged public — external Nostr clients fetching
    a kind-0 `picture` URL can't hold a cap.
  • Mesh callers (content_ref attachments, dispatch rehydration) pin
    public=false explicitly so nothing leaks out of the mesh path.

Profile editor UX
  • Collapsed Save + Save & Publish into one button — the Save action
    now persists locally AND publishes the kind-0 metadata event in
    one step. Uploads store `public_url` into `profile.picture` /
    `profile.banner` so the published URL is reachable by external
    clients.

Update client — the 15-second cliff
  • Frontend `rpcClient.call` for `update.download` now has an
    explicit 30-minute timeout (was falling back to the default 15 s).
    `update.apply` gets 5 min, `update.git-apply` gets 15 min. Matches
    what the backend is actually willing to wait for.
  • Backend `load_state()` reconciles `state.current_version` with
    `CARGO_PKG_VERSION` on every start. Sideloaded or reflashed nodes
    were stuck advertising the old version even with a new binary in
    place, which kept re-offering the same release as an update.

Manifest changelog rewritten for fleet readers per the saved feedback
(no function names, no file paths). Artefacts refreshed:
  binary   12f838c5…5ba82d  40381864
  frontend dc3b63af…e9a8370 76984288

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-20 10:03:38 -04:00
parent 508f8e1786
commit a9d8895395
11 changed files with 335 additions and 71 deletions

View File

@@ -162,6 +162,14 @@ impl IdentityManager {
let id = uuid::Uuid::new_v4().to_string();
let created_at = chrono::Utc::now().to_rfc3339();
// Every new identity gets a deterministic default avatar derived from
// its pubkey. Non-seed identities aren't the master node, so they use
// the 5×5 identicon (never the hexagonal node silhouette).
let default_profile = IdentityProfile {
picture: Some(crate::avatar::identicon(&pubkey_hex)),
..Default::default()
};
let identity_file = IdentityFile {
id: id.clone(),
name: name.clone(),
@@ -172,7 +180,7 @@ impl IdentityManager {
created_at: created_at.clone(),
nostr_secret_hex: None,
nostr_pubkey_hex: None,
profile: None,
profile: Some(default_profile),
derivation_index: None,
};
@@ -230,6 +238,14 @@ impl IdentityManager {
let nostr_secret_hex = nostr_keys.secret_key().display_secret().to_string();
let nostr_pubkey_hex = nostr_keys.public_key().to_hex();
// Derivation index 0 is the primary seed-derived identity — the
// "master" node identity — and gets the distinctive hexagonal SVG.
// Later indices get the standard identicon.
let default_profile = IdentityProfile {
picture: Some(crate::avatar::default_picture(&pubkey_hex, index == 0)),
..Default::default()
};
let identity_file = IdentityFile {
id: id.clone(),
name: name.clone(),
@@ -240,7 +256,7 @@ impl IdentityManager {
created_at: created_at.clone(),
nostr_secret_hex: Some(nostr_secret_hex),
nostr_pubkey_hex: Some(nostr_pubkey_hex),
profile: None,
profile: Some(default_profile),
derivation_index: Some(index),
};