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:
co-authored by
Claude Opus 4.7
parent
508f8e1786
commit
a9d8895395
@@ -357,7 +357,7 @@ async function downloadUpdate() {
|
||||
total_bytes: number
|
||||
downloaded_bytes: number
|
||||
components_downloaded: number
|
||||
}>({ method: 'update.download' })
|
||||
}>({ method: 'update.download', timeout: 1_800_000 })
|
||||
downloadPercent.value = 100
|
||||
downloaded.value = true
|
||||
const sizeMB = (res.downloaded_bytes / 1_048_576).toFixed(1)
|
||||
@@ -403,7 +403,7 @@ async function applyUpdateGit() {
|
||||
applying.value = true
|
||||
statusMessage.value = ''
|
||||
try {
|
||||
await rpcClient.call({ method: 'update.git-apply' })
|
||||
await rpcClient.call({ method: 'update.git-apply', timeout: 900_000 })
|
||||
showStatus(t('systemUpdate.gitApplyStarted'))
|
||||
updateInfo.value = null
|
||||
} catch (e) {
|
||||
@@ -418,7 +418,7 @@ async function applyUpdate() {
|
||||
applying.value = true
|
||||
statusMessage.value = ''
|
||||
try {
|
||||
await rpcClient.call({ method: 'update.apply' })
|
||||
await rpcClient.call({ method: 'update.apply', timeout: 300_000 })
|
||||
showStatus(t('systemUpdate.applySuccess'))
|
||||
updateInfo.value = null
|
||||
downloaded.value = false
|
||||
|
||||
@@ -359,8 +359,7 @@
|
||||
<div v-if="profileSuccess" class="mt-3 alert-success"><p class="text-xs">{{ profileSuccess }}</p></div>
|
||||
<div class="flex gap-3 mt-5">
|
||||
<button @click="closeProfileEditor" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">Cancel</button>
|
||||
<button @click="saveProfile" :disabled="profileSaving" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium">{{ profileSaving ? 'Saving...' : 'Save' }}</button>
|
||||
<button @click="publishProfile" :disabled="profilePublishing" class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium">{{ profilePublishing ? 'Publishing...' : 'Save & Publish' }}</button>
|
||||
<button @click="publishProfile" :disabled="profilePublishing" class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium">{{ profilePublishing ? 'Saving & publishing…' : 'Save' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -405,16 +404,14 @@ const keyViewerCopied = ref<string | null>(null)
|
||||
// Profile editor
|
||||
const profileEditorIdentity = ref<ManagedIdentity | null>(null)
|
||||
const profileForm = ref<IdentityProfile>({})
|
||||
const profileSaving = ref(false)
|
||||
const profilePublishing = ref(false)
|
||||
const avatarUploading = ref(false)
|
||||
const bannerUploading = ref(false)
|
||||
|
||||
// Upload to local blob store + set the corresponding profile URL so
|
||||
// the kind:0 event (publish) includes a reachable picture/banner. The
|
||||
// returned `self_test_url` is a capability-signed /blob/<cid>?cap=…
|
||||
// path — works locally. For external nostr clients to see the image,
|
||||
// swap to a public image host later.
|
||||
// Upload to the node's blob store and drop the returned public URL into
|
||||
// the profile field. The /api/blob endpoint marks these blobs public, so
|
||||
// the URL served back (`public_url`, onion-rooted when Tor is up) is
|
||||
// reachable by external Nostr clients fetching kind:0 metadata.
|
||||
async function uploadAsset(ev: Event, field: 'picture' | 'banner') {
|
||||
const input = ev.target as HTMLInputElement
|
||||
const file = input?.files?.[0]
|
||||
@@ -435,10 +432,10 @@ async function uploadAsset(ev: Event, field: 'picture' | 'banner') {
|
||||
body: buf,
|
||||
})
|
||||
if (!resp.ok) throw new Error(`upload failed: HTTP ${resp.status}`)
|
||||
const { self_test_url } = await resp.json() as { self_test_url?: string }
|
||||
if (!self_test_url) throw new Error('blob API returned no URL')
|
||||
// Assign and let the <img> in the header preview react.
|
||||
profileForm.value[field] = self_test_url
|
||||
const { public_url, self_test_url } = await resp.json() as { public_url?: string; self_test_url?: string }
|
||||
const url = public_url || self_test_url
|
||||
if (!url) throw new Error('blob API returned no URL')
|
||||
profileForm.value[field] = url
|
||||
} catch (e: unknown) {
|
||||
profileError.value = e instanceof Error ? e.message : `${field} upload failed`
|
||||
} finally {
|
||||
@@ -577,26 +574,6 @@ function closeProfileEditor() {
|
||||
profileSuccess.value = ''
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
if (!profileEditorIdentity.value || profileSaving.value) return
|
||||
profileSaving.value = true
|
||||
profileError.value = ''
|
||||
profileSuccess.value = ''
|
||||
try {
|
||||
await rpcClient.call({
|
||||
method: 'identity.update-profile',
|
||||
params: { id: profileEditorIdentity.value.id, ...profileForm.value },
|
||||
})
|
||||
await loadIdentities()
|
||||
profileSuccess.value = 'Profile saved'
|
||||
setTimeout(() => { profileSuccess.value = '' }, 3000)
|
||||
} catch (err: unknown) {
|
||||
profileError.value = err instanceof Error ? err.message : 'Failed to save'
|
||||
} finally {
|
||||
profileSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function publishProfile() {
|
||||
if (!profileEditorIdentity.value || profilePublishing.value) return
|
||||
profilePublishing.value = true
|
||||
|
||||
Reference in New Issue
Block a user