Update Fedimint configuration and enhance onboarding process

- Upgraded Fedimint version to v0.10.0 in docker-compose.yml and manifest.yml, adding support for the built-in Guardian UI.
- Modified .gitignore to exclude deploy-config.sh script.
- Enhanced onboarding process in AuthManager to persist onboarding state and validate password strength during user setup.
- Updated API to handle onboarding completion and password change requests, ensuring a smoother user experience.
- Improved configuration management to support Nostr discovery and Tor proxy settings, enhancing node identity features.
This commit is contained in:
Dorian
2026-02-17 15:03:34 +00:00
parent 6035c93289
commit 1073d9fd2c
73 changed files with 5870 additions and 478 deletions

View File

@@ -0,0 +1,170 @@
/**
* Controller / gamepad-style navigation for Archipelago.
* Supports Rii X8 (keyboard/d-pad) and standard gamepads.
* - Arrow keys / d-pad: navigate between focusable elements
* - Enter / A button: activate
* - Escape / B button: back
* - Game-like navigation sounds and visual feedback
*/
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { useControllerStore } from '@/stores/controller'
import { useSpotlightStore } from '@/stores/spotlight'
const FOCUSABLE_SELECTOR = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
'[data-controller-focus]',
].join(', ')
function getFocusableElements(container: Document | HTMLElement = document): HTMLElement[] {
return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(
(el) => !el.hasAttribute('disabled') && el.offsetParent !== null
)
}
function playNavSound(type: 'move' | 'select' | 'back' = 'move') {
try {
const ctx = new (window.AudioContext || (window as any).webkitAudioContext)()
const osc = ctx.createOscillator()
const gain = ctx.createGain()
osc.connect(gain)
gain.connect(ctx.destination)
gain.gain.value = 0.08
osc.frequency.value = type === 'select' ? 880 : type === 'back' ? 220 : 440
osc.type = 'sine'
osc.start()
osc.stop(ctx.currentTime + 0.05)
} catch {
// Audio not supported or blocked
}
}
export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
const store = useControllerStore()
const isControllerActive = ref(false)
const gamepadCount = ref(0)
watch([isControllerActive, gamepadCount], () => {
store.setActive(isControllerActive.value)
store.setGamepadCount(gamepadCount.value)
}, { immediate: true })
let keyNavTimeout: ReturnType<typeof setTimeout> | null = null
let pollIntervalId: ReturnType<typeof setInterval> | null = null
function checkGamepads() {
const gamepads = navigator.getGamepads?.()
const count = gamepads ? Array.from(gamepads).filter((g) => g?.connected).length : 0
if (count !== gamepadCount.value) {
gamepadCount.value = count
isControllerActive.value = count > 0
}
}
function handleKeyDown(e: KeyboardEvent) {
const navKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', 'Escape']
if (!navKeys.includes(e.key)) return
// Ignore when typing in inputs
const target = e.target as HTMLElement
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
if (e.key !== 'Escape') return
}
const root = containerRef?.value ?? document
const focusable = getFocusableElements(root)
const currentIndex = focusable.indexOf(document.activeElement as HTMLElement)
if (e.key === 'Escape') {
if (useSpotlightStore().isOpen) {
useSpotlightStore().close()
e.preventDefault()
e.stopPropagation()
return
}
playNavSound('back')
window.history.back()
e.preventDefault()
return
}
if (e.key === 'Enter') {
if (currentIndex >= 0 && focusable[currentIndex]) {
playNavSound('select')
;(focusable[currentIndex] as HTMLElement).click()
}
e.preventDefault()
return
}
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
isControllerActive.value = true
if (keyNavTimeout) clearTimeout(keyNavTimeout)
keyNavTimeout = setTimeout(() => {
isControllerActive.value = gamepadCount.value > 0
}, 3000)
let nextIndex = currentIndex
const isForward = e.key === 'ArrowDown' || e.key === 'ArrowRight'
if (focusable.length === 0) return
if (currentIndex < 0) {
nextIndex = isForward ? 0 : focusable.length - 1
} else {
nextIndex = isForward ? currentIndex + 1 : currentIndex - 1
if (nextIndex < 0) nextIndex = focusable.length - 1
if (nextIndex >= focusable.length) nextIndex = 0
}
const next = focusable[nextIndex]
if (next) {
playNavSound('move')
next.focus()
next.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
e.preventDefault()
}
}
}
function handleGamepadInput() {
checkGamepads()
}
function handleGamepadConnected() {
const gamepads = navigator.getGamepads?.()
gamepadCount.value = gamepads ? Array.from(gamepads).filter((g) => g?.connected).length : 1
isControllerActive.value = true
}
function handleGamepadDisconnected() {
const gamepads = navigator.getGamepads?.()
gamepadCount.value = gamepads ? Array.from(gamepads).filter((g) => g?.connected).length : 0
isControllerActive.value = gamepadCount.value > 0
}
onMounted(() => {
checkGamepads()
window.addEventListener('keydown', handleKeyDown, true)
window.addEventListener('gamepadconnected', handleGamepadConnected)
window.addEventListener('gamepaddisconnected', handleGamepadDisconnected)
pollIntervalId = setInterval(handleGamepadInput, 500)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeyDown, true)
window.removeEventListener('gamepadconnected', handleGamepadConnected)
window.removeEventListener('gamepaddisconnected', handleGamepadDisconnected)
if (pollIntervalId) clearInterval(pollIntervalId)
if (keyNavTimeout) clearTimeout(keyNavTimeout)
})
return {
isControllerActive,
gamepadCount,
}
}

View File

@@ -0,0 +1,86 @@
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
export interface ReceivedMessage {
from_pubkey: string
message: string
timestamp: string
}
const MESSAGE_POLL_INTERVAL = 30000 // 30s
// Shared state (singleton) so toast works across route changes
const receivedMessages = ref<ReceivedMessage[]>([])
const lastMessageCount = ref(0)
const loadingMessages = ref(false)
const toastMessage = ref<{ show: boolean; text: string }>({ show: false, text: '' })
let pollTimer: ReturnType<typeof setInterval> | null = null
export function useMessageToast() {
const router = useRouter()
const unreadCount = computed(() =>
Math.max(0, receivedMessages.value.length - lastMessageCount.value)
)
async function loadReceivedMessages() {
loadingMessages.value = true
try {
const res = await rpcClient.getReceivedMessages()
const msgs = (res.messages || []) as ReceivedMessage[]
receivedMessages.value = msgs
// New messages since last check? (don't show toast on initial load)
if (msgs.length > lastMessageCount.value && lastMessageCount.value > 0) {
const newCount = msgs.length - lastMessageCount.value
const latest = msgs[msgs.length - 1]
toastMessage.value = {
show: true,
text: (newCount === 1 ? latest?.message : null) ?? `${newCount} new messages`,
}
} else {
lastMessageCount.value = msgs.length
}
} catch (e) {
console.error('Failed to load messages:', e)
} finally {
loadingMessages.value = false
}
}
function startPolling() {
if (pollTimer) return
loadReceivedMessages()
pollTimer = setInterval(loadReceivedMessages, MESSAGE_POLL_INTERVAL)
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
function markAsRead() {
lastMessageCount.value = receivedMessages.value.length
}
function dismissToastAndOpenMessages() {
toastMessage.value = { show: false, text: '' }
markAsRead()
router.push({ path: '/dashboard/web5', query: { tab: 'messages' } })
}
return {
receivedMessages,
lastMessageCount,
loadingMessages,
toastMessage,
unreadCount,
loadReceivedMessages,
startPolling,
stopPolling,
markAsRead,
dismissToastAndOpenMessages,
}
}

View File

@@ -0,0 +1,20 @@
/**
* Onboarding state - prefers backend, falls back to localStorage for mock/offline.
*/
import { rpcClient } from '@/api/rpc-client'
export async function isOnboardingComplete(): Promise<boolean> {
try {
return await rpcClient.isOnboardingComplete()
} catch {
return localStorage.getItem('neode_onboarding_complete') === '1'
}
}
export async function completeOnboarding(): Promise<void> {
try {
await rpcClient.completeOnboarding()
} finally {
localStorage.setItem('neode_onboarding_complete', '1')
}
}