feat(ui): mesh Refresh/Broadcast feedback, Set Recommended modal with probe progress bar, live list refresh
Demo images / Build & push demo images (push) Successful in 3m43s
Demo images / Build & push demo images (push) Successful in 3m43s
- Refresh button: real handler — calls the new mesh.refresh (radio
re-query) plus contacts/federation/outbox re-reads, disabled with a
spinner while running (was an unawaited cache repaint with no feedback
that skipped half the list's data sources).
- Broadcast button: success ('Sent ✓') and failure states with the error
in the tooltip; failures no longer vanish as unhandled rejections.
- The store's 5s status poll no longer wipes the error banner each tick.
- Contacts/aliases, federation nodes and the outbox badge refresh every
~30s (were mount-only and went permanently stale).
- Peer-list empty state keys on the merged list, so federation rows and
the channel rows still render with no radio attached.
- Device setup modal: 'Set Recommended' naming, probe progress bar with
stage labels instead of an anonymous spinner.
- Device panel: name save clears properly (empty = fall back to server
name) and the confirmation reflects the new live apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a8c4694c36
commit
3f76b4960a
@@ -38,6 +38,8 @@ const activeChatChannel = ref<{ index: number; name: string } | null>(null)
|
||||
const messageText = ref('')
|
||||
const sendError = ref('')
|
||||
const broadcasting = ref(false)
|
||||
const broadcastResult = ref<string | null>(null) // 'ok' | error message
|
||||
const refreshing = ref(false)
|
||||
const configuring = ref(false)
|
||||
const connectingDevice = ref<string | null>(null)
|
||||
// Device-detected onboarding now lives in the global MeshDeviceSetupModal (App.vue).
|
||||
@@ -383,12 +385,22 @@ onMounted(async () => {
|
||||
archPollInterval = setInterval(loadArchMessages, 15000)
|
||||
}
|
||||
if (!pollInterval) {
|
||||
let tick = 0
|
||||
pollInterval = setInterval(() => {
|
||||
mesh.fetchStatus()
|
||||
mesh.fetchPeers()
|
||||
mesh.fetchMessages()
|
||||
mesh.fetchDeadmanStatus()
|
||||
mesh.fetchBlockHeaders()
|
||||
// Contacts/aliases, federation nodes and the outbox badge previously
|
||||
// loaded ONCE at mount and went permanently stale — new federation
|
||||
// peers or renames never appeared without a full page reload. Every
|
||||
// 6th tick (~30s) keeps them fresh without adding per-5s load.
|
||||
if (++tick % 6 === 0) {
|
||||
void refreshContacts()
|
||||
void refreshFederationNodes()
|
||||
void refreshOutboxCount()
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
@@ -1021,7 +1033,37 @@ function onChatWheel(e: WheelEvent) {
|
||||
|
||||
async function handleBroadcast() {
|
||||
broadcasting.value = true
|
||||
try { await mesh.broadcastIdentity() } finally { broadcasting.value = false }
|
||||
broadcastResult.value = null
|
||||
try {
|
||||
await mesh.broadcastIdentity()
|
||||
broadcastResult.value = 'ok'
|
||||
} catch (e) {
|
||||
broadcastResult.value = e instanceof Error ? e.message : 'Broadcast failed'
|
||||
} finally {
|
||||
broadcasting.value = false
|
||||
setTimeout(() => { broadcastResult.value = null }, 4000)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
if (refreshing.value) return
|
||||
refreshing.value = true
|
||||
try {
|
||||
// Backend first: re-query the radio's contact table (mesh.refresh), then
|
||||
// re-read EVERYTHING the list is built from — peers, contacts/aliases,
|
||||
// federation nodes, outbox — not just the mesh caches.
|
||||
await Promise.allSettled([
|
||||
mesh.refreshRadio(),
|
||||
mesh.refreshAll(),
|
||||
refreshContacts(),
|
||||
refreshFederationNodes(),
|
||||
refreshOutboxCount(),
|
||||
])
|
||||
// Radio contact refresh is async on the backend — pick up its result.
|
||||
await mesh.fetchPeers()
|
||||
} finally {
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleEnabled() {
|
||||
@@ -1830,8 +1872,14 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
|
||||
<button class="glass-button mesh-action-btn" :disabled="configuring" @click="handleToggleEnabled">
|
||||
{{ mesh.status?.enabled ? 'Disable' : 'Enable' }}
|
||||
</button>
|
||||
<button class="glass-button mesh-action-btn" :disabled="!mesh.status?.device_connected || broadcasting" @click="handleBroadcast">
|
||||
{{ broadcasting ? 'Sending...' : 'Broadcast' }}
|
||||
<button
|
||||
class="glass-button mesh-action-btn"
|
||||
:class="broadcastResult === 'ok' ? 'mesh-action-ok' : ''"
|
||||
:disabled="!mesh.status?.device_connected || broadcasting"
|
||||
:title="broadcastResult && broadcastResult !== 'ok' ? broadcastResult : 'Announce this node so nearby radios learn about it'"
|
||||
@click="handleBroadcast"
|
||||
>
|
||||
{{ broadcasting ? 'Sending…' : broadcastResult === 'ok' ? 'Sent ✓' : broadcastResult ? 'Failed ✕' : 'Broadcast' }}
|
||||
</button>
|
||||
<button
|
||||
class="glass-button mesh-action-btn"
|
||||
@@ -1841,7 +1889,10 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
|
||||
>
|
||||
{{ transport.meshOnly ? 'Go Online' : 'Off-Grid' }}
|
||||
</button>
|
||||
<button class="glass-button mesh-action-btn" @click="mesh.refreshAll()">Refresh</button>
|
||||
<button class="glass-button mesh-action-btn" :disabled="refreshing" @click="handleRefresh">
|
||||
<span v-if="refreshing" class="mesh-refresh-spinner" aria-hidden="true"></span>
|
||||
{{ refreshing ? 'Refreshing…' : 'Refresh' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Peers list -->
|
||||
@@ -1871,7 +1922,10 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
|
||||
>×</button>
|
||||
</div>
|
||||
|
||||
<div v-if="mesh.peers.length === 0 && !mesh.status?.device_connected" class="mesh-empty">
|
||||
<!-- Only claim "no peers" when the MERGED list (radio + federation)
|
||||
is truly empty — with no radio attached the federation rows and
|
||||
the two channel rows must still render. -->
|
||||
<div v-if="displayedPeers.length === 0 && !mesh.status?.device_connected" class="mesh-empty">
|
||||
No peers discovered yet.
|
||||
</div>
|
||||
|
||||
|
||||
@@ -142,12 +142,15 @@ async function saveSettings() {
|
||||
lora_region: form.value.region,
|
||||
device_kind: form.value.deviceKind,
|
||||
channel_name: form.value.channel.trim() || 'archipelago',
|
||||
...(form.value.name.trim() ? { advert_name: form.value.name.trim() } : {}),
|
||||
// Always sent: an empty string CLEARS the custom mesh name (backend
|
||||
// maps "" -> None -> fall back to the server name). The old omit-when-
|
||||
// empty made clearing impossible once a name was ever set.
|
||||
advert_name: form.value.name.trim(),
|
||||
broadcast_identity: form.value.broadcastIdentity,
|
||||
...(rfParams ? { lora_radio_params: rfParams } : {}),
|
||||
})
|
||||
saveDone.value = true
|
||||
setTimeout(() => { saveDone.value = false }, 3000)
|
||||
setTimeout(() => { saveDone.value = false }, 5000)
|
||||
} catch (e) {
|
||||
saveError.value = e instanceof Error ? e.message : 'Failed to save mesh settings'
|
||||
} finally {
|
||||
@@ -286,7 +289,7 @@ async function saveSettings() {
|
||||
>
|
||||
{{ saving ? 'Saving…' : 'Save Settings' }}
|
||||
</button>
|
||||
<span v-if="saveDone" class="text-xs text-green-400">Saved — applies on next radio session</span>
|
||||
<span v-if="saveDone" class="text-xs text-green-400">Saved — applying to the radio now…</span>
|
||||
<span v-if="saveError" class="text-xs text-red-400">{{ saveError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +83,19 @@
|
||||
.mesh-offgrid-active { border-color: rgba(251, 146, 60, 0.4) !important; color: #fb923c !important; }
|
||||
.mesh-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
||||
.mesh-action-btn { flex: 1; padding: 8px 0; font-size: 0.8rem; }
|
||||
.mesh-action-ok { color: #34d399; border-color: rgba(52, 211, 153, 0.4); }
|
||||
.mesh-refresh-spinner {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 4px;
|
||||
border-radius: 9999px;
|
||||
border: 2px solid rgba(251, 146, 60, 0.7);
|
||||
border-top-color: transparent;
|
||||
animation: mesh-refresh-spin 0.8s linear infinite;
|
||||
vertical-align: -1px;
|
||||
}
|
||||
@keyframes mesh-refresh-spin { to { transform: rotate(360deg); } }
|
||||
.mesh-peers-card { padding: 14px; flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
||||
.mesh-peers-card .mesh-section-title { margin-bottom: 10px; flex-shrink: 0; }
|
||||
.mesh-peer-list { display: flex; flex-direction: column; gap: 4px; overflow-y: auto; flex: 1; min-height: 0; }
|
||||
|
||||
Reference in New Issue
Block a user