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
+109
View File
@@ -0,0 +1,109 @@
<template>
<div class="logo-gradient-border flex-shrink-0 inline-block overflow-hidden">
<!-- Neode logo - white or coloured -->
<svg
class="w-14 h-14 block logo-svg"
viewBox="0 0 1024 1024"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="Neode"
>
<defs>
<linearGradient :id="gradientId" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f9aa4b" />
<stop offset="50%" stop-color="#f7931a" />
<stop offset="100%" stop-color="#e68a19" />
</linearGradient>
</defs>
<rect width="1024" height="1024" fill="#030202" />
<rect
v-for="(r, i) in rects"
:key="i"
:x="r.x"
:y="r.y"
:width="r.w"
:height="r.h"
:fill="mode === 'coloured' ? `url(#${gradientId})` : 'white'"
class="logo-square"
:class="{ 'logo-square-coloured': mode === 'coloured' }"
:style="{ '--delay': delays[i] + 'ms' }"
/>
</svg>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
const gradientId = 'logo-color-' + Math.random().toString(36).slice(2, 11)
// Parsed from favico-black.svg path - 20 rects
const rects = [
{ x: 357.614, y: 318, w: 71.007, h: 70.936 },
{ x: 436.152, y: 318, w: 72.082, h: 70.936 },
{ x: 515.766, y: 318, w: 72.082, h: 70.936 },
{ x: 595.379, y: 318, w: 71.007, h: 70.936 },
{ x: 595.379, y: 396.46, w: 71.007, h: 72.011 },
{ x: 673.917, y: 396.46, w: 72.083, h: 72.011 },
{ x: 278, y: 475.994, w: 72.083, h: 72.012 },
{ x: 357.614, y: 475.994, w: 71.007, h: 72.012 },
{ x: 436.152, y: 475.994, w: 72.082, h: 72.012 },
{ x: 515.766, y: 475.994, w: 72.082, h: 72.012 },
{ x: 595.379, y: 475.994, w: 71.007, h: 72.012 },
{ x: 673.917, y: 475.994, w: 72.083, h: 72.012 },
{ x: 278, y: 555.529, w: 72.083, h: 70.936 },
{ x: 357.614, y: 555.529, w: 71.007, h: 70.936 },
{ x: 595.379, y: 555.529, w: 71.007, h: 70.936 },
{ x: 673.917, y: 555.529, w: 72.083, h: 70.936 },
{ x: 357.614, y: 633.989, w: 71.007, h: 72.011 },
{ x: 436.152, y: 633.989, w: 72.082, h: 72.011 },
{ x: 515.766, y: 633.989, w: 72.082, h: 72.011 },
{ x: 595.379, y: 633.989, w: 71.007, h: 72.011 },
]
// Stagger delays (ms) - spread over ~4.5s load phase
const delays = [0, 2341, 467, 1890, 312, 3456, 123, 2789, 567, 4123, 901, 1456, 234, 3789, 2678, 456, 847, 2912, 1891, 423]
type Mode = 'normal' | 'coloured'
const mode = ref<Mode>('normal')
let cycleCount = 0
let intervalId: ReturnType<typeof setInterval> | null = null
const CYCLE_MS = 10000
onMounted(() => {
intervalId = setInterval(() => {
cycleCount++
mode.value = cycleCount % 4 === 3 ? 'coloured' : 'normal'
}, CYCLE_MS)
})
onBeforeUnmount(() => {
if (intervalId) clearInterval(intervalId)
})
</script>
<style scoped>
.logo-svg {
will-change: auto;
}
.logo-square {
opacity: 0;
animation: logo-square-in 10s cubic-bezier(0.4, 0, 0.2, 1) infinite;
animation-delay: var(--delay, 0ms);
animation-fill-mode: both;
}
.logo-square-coloured {
filter: drop-shadow(0 0 2px rgba(247, 147, 26, 0.4));
}
/* 045%: squares load in. 45100%: full logo visible */
@keyframes logo-square-in {
0% { opacity: 0; transform: scale(0.95); }
4% { opacity: 1; transform: scale(1); }
45% { opacity: 1; transform: scale(1); }
100% { opacity: 1; transform: scale(1); }
}
</style>
@@ -0,0 +1,21 @@
<template>
<div
v-if="store.isActive"
class="flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10"
title="Controller connected - use arrows & Enter to navigate"
>
<svg class="w-5 h-5 text-amber-400/90 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<rect x="4" y="8" width="16" height="10" rx="2" stroke-width="2" />
<circle cx="9" cy="13" r="1.5" fill="currentColor" />
<circle cx="15" cy="13" r="1.5" fill="currentColor" />
<path stroke-linecap="round" stroke-width="2" d="M12 10v2M11 11h2" />
</svg>
<span class="text-xs text-white/70 hidden sm:inline">Controller</span>
</div>
</template>
<script setup lang="ts">
import { useControllerStore } from '@/stores/controller'
const store = useControllerStore()
</script>
@@ -0,0 +1,58 @@
<template>
<Teleport to="body">
<Transition name="modal">
<div
v-if="show"
class="fixed inset-0 z-[3000] flex items-center justify-center p-4"
@click="$emit('close')"
>
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div
@click.stop
class="glass-card p-6 max-w-lg w-full relative z-10 max-h-[80vh] overflow-y-auto"
>
<div class="flex items-start justify-between gap-4 mb-4">
<h3 class="text-xl font-semibold text-white">{{ title }}</h3>
<button
@click="$emit('close')"
class="p-2 rounded-lg hover:bg-white/10 text-white/70 hover:text-white transition-colors"
aria-label="Close"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="text-white/80 prose prose-invert max-w-none">
<p class="whitespace-pre-wrap">{{ content }}</p>
</div>
<div v-if="relatedPath" class="mt-4">
<router-link
:to="relatedPath"
class="inline-flex items-center gap-2 px-4 py-2 glass-button rounded-lg text-sm font-medium"
@click="$emit('close')"
>
Go to related page
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</router-link>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
defineProps<{
show: boolean
title: string
content: string
relatedPath?: string
}>()
defineEmits<{
close: []
}>()
</script>
+361
View File
@@ -0,0 +1,361 @@
<template>
<Teleport to="body">
<Transition name="spotlight">
<div
v-if="spotlightStore.isOpen"
class="fixed inset-0 z-[2500] flex items-center justify-center p-4"
@click.self="spotlightStore.close()"
>
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div
ref="panelRef"
class="glass-card w-full max-w-2xl relative z-10 overflow-hidden flex flex-col"
:style="panelStyle"
@mousedown="onPanelMouseDown"
>
<!-- Header: drag handle grip + search -->
<div class="flex items-center gap-3 px-4 py-3 border-b border-white/10">
<div
ref="dragHandleRef"
class="flex items-center justify-center w-8 h-8 rounded cursor-grab hover:bg-white/10 transition-colors shrink-0"
:class="{ 'cursor-grabbing': isDragging }"
title="Drag to move"
>
<svg class="w-4 h-4 text-white/50" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 6h2v2H8V6zm0 5h2v2H8v-2zm0 5h2v2H8v-2zm5-10h2v2h-2V6zm0 5h2v2h-2v-2zm0 5h2v2h-2v-2z" />
</svg>
</div>
<div class="flex-1 flex items-center gap-3 min-w-0">
<svg class="w-5 h-5 text-white/60 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
ref="inputRef"
v-model="query"
type="text"
placeholder="Search or type a command..."
class="flex-1 bg-transparent text-white placeholder-white/50 outline-none text-base"
@keydown="onInputKeydown"
/>
</div>
<kbd class="hidden sm:inline-flex px-2 py-1 text-xs text-white/50 bg-white/10 rounded">Esc</kbd>
</div>
<div class="flex-1 overflow-y-auto max-h-[60vh] min-h-[200px]">
<!-- Recent items (when no query and we have recent) -->
<div v-if="!query.trim() && spotlightStore.recentItems.length > 0" class="p-2 border-b border-white/10">
<div class="px-3 py-2 text-xs font-medium text-white/50 uppercase tracking-wider">Recent</div>
<button
v-for="(item, idx) in spotlightStore.recentItems"
:key="`recent-${item.id}-${item.timestamp}`"
type="button"
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors"
:class="getItemClass(idx)"
@click="selectRecent(item)"
>
<span class="text-white/90">{{ item.label }}</span>
<span class="text-xs text-white/40">{{ item.type }}</span>
</button>
</div>
<!-- Search results or help tree -->
<template v-if="query.trim()">
<div v-if="filteredItems.length > 0" class="p-2">
<button
v-for="(item, idx) in filteredItems"
:key="item.id + item.section"
type="button"
class="w-full flex items-center justify-between gap-3 px-3 py-2.5 rounded-lg text-left transition-colors"
:class="getItemClass(idx)"
@click="selectItem(item)"
>
<span class="text-white/90">{{ item.label }}</span>
<span class="text-xs text-white/40">{{ item.section }}</span>
</button>
</div>
<div v-else class="p-8 text-center text-white/50">
No results for "{{ query }}"
</div>
</template>
<template v-else>
<!-- Help tree when no search -->
<div v-for="section in helpTree" :key="section.id" class="p-2">
<div class="px-3 py-2 text-xs font-medium text-white/50 uppercase tracking-wider">{{ section.label }}</div>
<button
v-for="(item, idx) in section.items"
:key="item.id"
type="button"
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors"
:class="getItemClass(recentOffset + getFlatIndex(section.id, idx))"
@click="selectHelpItem(section, item)"
>
<span class="text-white/90">{{ item.label }}</span>
</button>
</div>
</template>
<!-- AI Assistant placeholder -->
<div class="p-2 border-t border-white/10">
<div class="px-3 py-2 text-xs font-medium text-white/50 uppercase tracking-wider">AI Assistant</div>
<div class="px-3 py-3 rounded-lg bg-white/5 text-white/50 text-sm">
Coming soon ask questions about your node, apps, and Bitcoin.
</div>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import Fuse from 'fuse.js'
import { useSpotlightStore } from '@/stores/spotlight'
import { helpTree, flattenForSearch, type SearchableItem } from '@/data/helpTree'
const router = useRouter()
const spotlightStore = useSpotlightStore()
const inputRef = ref<HTMLInputElement | null>(null)
const panelRef = ref<HTMLElement | null>(null)
const dragHandleRef = ref<HTMLElement | null>(null)
const query = ref('')
const isDragging = ref(false)
const dragStart = ref<{ x: number; y: number; panelX: number; panelY: number } | null>(null)
const searchableItems = flattenForSearch()
const fuse = new Fuse(searchableItems, {
keys: ['label', 'section'],
threshold: 0.4,
})
const filteredItems = computed(() => {
const q = query.value.trim()
if (!q) return []
const results = fuse.search(q)
return results.map((r) => r.item)
})
const recentOffset = computed(() =>
!query.value.trim() && spotlightStore.recentItems.length > 0 ? spotlightStore.recentItems.length : 0
)
const selectableCount = computed(() => {
if (query.value.trim()) return filteredItems.value.length
return recentOffset.value + searchableItems.length
})
const panelStyle = computed(() => {
const pos = savedPosition.value
if (!pos) return {}
return {
transform: `translate(${pos.x}px, ${pos.y}px)`,
margin: 0,
}
})
const SAVED_POSITION_KEY = 'archipelago-spotlight-position'
const savedPosition = ref<{ x: number; y: number } | null>(null)
function loadSavedPosition() {
try {
const raw = localStorage.getItem(SAVED_POSITION_KEY)
if (raw) {
const parsed = JSON.parse(raw)
savedPosition.value = { x: parsed.x ?? 0, y: parsed.y ?? 0 }
} else {
savedPosition.value = null
}
} catch {
savedPosition.value = null
}
}
function savePosition(x: number, y: number) {
savedPosition.value = { x, y }
try {
localStorage.setItem(SAVED_POSITION_KEY, JSON.stringify({ x, y }))
} catch {
// ignore
}
}
function getFlatIndex(sectionId: string, itemIdx: number): number {
let idx = 0
for (const s of helpTree) {
if (s.id === sectionId) return idx + itemIdx
idx += s.items.length
}
return -1
}
function getItemClass(index: number) {
const selected = spotlightStore.selectedIndex
return index === selected
? 'bg-amber-500/20 text-amber-200'
: 'hover:bg-white/10 text-white/90'
}
function selectItem(item: SearchableItem) {
spotlightStore.addRecentItem({
id: item.id,
label: item.label,
path: item.path,
type: item.type,
})
spotlightStore.close()
if (item.path) {
router.push(item.path)
} else if (item.content) {
spotlightStore.showHelpModal({ title: item.label, content: item.content, relatedPath: item.relatedPath })
}
}
function selectHelpItem(section: { id: string }, item: { id: string; label: string; path?: string; content?: string; relatedPath?: string }) {
const type = section.id === 'navigate' ? 'navigate' : section.id === 'learn' ? 'learn' : 'action'
spotlightStore.addRecentItem({
id: item.id,
label: item.label,
path: item.path,
type,
})
spotlightStore.close()
if (item.path) {
router.push(item.path)
} else if (item.content) {
spotlightStore.showHelpModal({ title: item.label, content: item.content, relatedPath: item.relatedPath })
}
}
function selectRecent(item: { id: string; label: string; path?: string; type: 'navigate' | 'learn' | 'action' }) {
spotlightStore.close()
if (item.path) {
router.push(item.path)
return
}
if (item.type === 'learn') {
for (const s of helpTree) {
const helpItem = s.items.find((i) => i.id === item.id)
if (helpItem?.content) {
spotlightStore.showHelpModal({ title: helpItem.label, content: helpItem.content, relatedPath: helpItem.relatedPath })
return
}
}
}
}
function onInputKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
spotlightStore.close()
e.preventDefault()
e.stopPropagation()
return
}
if (e.key === 'ArrowDown') {
e.preventDefault()
spotlightStore.setSelectedIndex(
Math.min(spotlightStore.selectedIndex + 1, Math.max(0, selectableCount.value - 1))
)
return
}
if (e.key === 'ArrowUp') {
e.preventDefault()
spotlightStore.setSelectedIndex(Math.max(spotlightStore.selectedIndex - 1, 0))
return
}
if (e.key === 'Enter') {
e.preventDefault()
const idx = spotlightStore.selectedIndex
if (query.value.trim()) {
const item = filteredItems.value[idx]
if (item) selectItem(item)
return
}
if (idx < recentOffset.value) {
const item = spotlightStore.recentItems[idx]
if (item) selectRecent(item)
return
}
const helpIdx = idx - recentOffset.value
let count = 0
for (const s of helpTree) {
for (const item of s.items) {
if (count === helpIdx) {
selectHelpItem(s, item)
return
}
count++
}
}
}
}
function onPanelMouseDown(e: MouseEvent) {
if (!dragHandleRef.value?.contains(e.target as Node)) return
isDragging.value = true
const rect = panelRef.value?.getBoundingClientRect()
if (!rect) return
const currentX = savedPosition.value?.x ?? 0
const currentY = savedPosition.value?.y ?? 0
dragStart.value = { x: e.clientX, y: e.clientY, panelX: currentX, panelY: currentY }
}
function onMouseMove(e: MouseEvent) {
if (!dragStart.value) return
const dx = e.clientX - dragStart.value.x
const dy = e.clientY - dragStart.value.y
savePosition(dragStart.value.panelX + dx, dragStart.value.panelY + dy)
}
function onMouseUp() {
isDragging.value = false
dragStart.value = null
}
watch(
() => spotlightStore.isOpen,
(open) => {
if (open) {
query.value = ''
loadSavedPosition()
nextTick(() => {
inputRef.value?.focus()
spotlightStore.setSelectedIndex(0)
})
}
}
)
watch(
[query, filteredItems],
() => {
spotlightStore.setSelectedIndex(0)
}
)
onMounted(() => {
loadSavedPosition()
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
})
onBeforeUnmount(() => {
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
})
</script>
<style scoped>
.spotlight-enter-active,
.spotlight-leave-active {
transition: opacity 0.2s ease;
}
.spotlight-enter-from,
.spotlight-leave-to {
opacity: 0;
}
</style>