Fixes from real fresh-install feedback (Framework node .81) + its log bundle:
Backend:
- websocket: subscribe before initial snapshot — broadcasts in the gap were
silently lost, stranding clients on stale state until a hard refresh
(the "everything needs ctrl-r" bug: My Apps stuck Loading, App Store
stuck Checking, containers-scanned never arriving)
- crash recovery: check the crash marker BEFORE writing our own PID —
recovery had never run on any node (always saw its own PID and skipped);
PID-reuse guard via /proc cmdline
- boot status: pending-boot-starts registry (recovery, stack recovery,
reconciler, adoption) — scanner overlays queued-but-down apps as
Restarting instead of Stopped after a reboot; scanner-authored
Restarting resolves immediately on a settled scan (no transitional wedge)
- install deps: bounded wait (36x5s) when a dependency is installed but
still starting ("Waiting for Bitcoin to start…") instead of instant
rejection; dependency-gate rejections remove the optimistic entry (no
phantom Stopped tile) and surface as a notification
- seed backup: auth.setup persists the onboarding mnemonic as the
encrypted seed backup (reveal previously failed on EVERY node — nothing
ever wrote master_seed.enc); seed.restore stashes too; error sanitizer
lets seed/2FA errors through instead of "Check server logs"
- lnd: bitcoind.rpchost resolved from the running Bitcoin variant
(hardcoded bitcoin-knots broke Core nodes); manifest uses derived_env
- bitcoin status: clean human message for connection-reset/startup; raw
URLs + os-error chains no longer reach the app card
- fedimint-clientd: chown /var/lib/archipelago/fmcd to 1000:1000 (root-
created dir crash-looped the rootless container, EACCES) — first-boot
script + pre-start self-heal
- log volume (>1GB/day on a day-old node): journald caps drop-in (ISO +
bootstrap self-heal), bitcoind -printtoconsole=0 everywhere (90% of the
journal was IBD UpdateTip spam), tracing default debug→info
Frontend:
- Login: Enter advances to confirm field then submits; submit always
clickable with inline errors (was silently disabled on mismatch);
Restart Onboarding needs a confirming second click (the mismatch →
"onboarding restarted" trap)
- sync store: 30s state reconciliation + refetch on re-entrant connect;
20s containers-scanned escape hatch so Checking can never show forever;
fresh empty node reaches the real "no apps yet" state
- intro video: CRF20 re-encode (SSIM 0.988) + faststart — moov was at EOF
so playback needed the full 15MB first (the intro lag)
- backgrounds: 10 heaviest JPEGs → WebP q90 (9.4MB→6.6MB); 7 stayed JPEG
(WebP larger on noisy sources)
- Web5ConnectedNodes: drop unused template ref that failed vue-tsc -b
ISO/kiosk:
- nginx: /assets/ 404s no longer cached immutable for a year; HTTPS block
gained the missing /assets/ location (served index.html as images)
- kiosk: launcher/service spliced from configs/ at ISO build (stale
heredoc force-disabled GPU); MemoryHigh/Max 1200/1500→2200/2800M (kiosk
rode the reclaim throttle = the lag); firmware-intel-graphics +
firmware-amd-graphics (trixie split DMC blobs out of misc-nonfree)
Verified: cargo test 898/898 green, npm run build green with dist
contents confirmed (webp refs, lnd.png, faststart video, new strings).
Handover for ISO build + deploy: docs/HANDOVER-2026-07-02-iso-feedback.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
184 lines
7.4 KiB
TypeScript
184 lines
7.4 KiB
TypeScript
import { type RouteLocationNormalizedLoaded } from 'vue-router'
|
|
|
|
/** Tab order for vertical transitions between main navigation items */
|
|
const TAB_ORDER = [
|
|
'/dashboard',
|
|
'/dashboard/apps',
|
|
'/dashboard/marketplace',
|
|
'/dashboard/cloud',
|
|
'/dashboard/mesh',
|
|
'/dashboard/server',
|
|
'/dashboard/web5',
|
|
'/dashboard/fleet',
|
|
'/dashboard/chat',
|
|
'/dashboard/settings'
|
|
]
|
|
|
|
/** Web5 group sub-tab order for mobile horizontal swipe transitions */
|
|
const WEB5_TAB_ORDER = ['/dashboard/web5', '/dashboard/cloud', '/dashboard/server', '/dashboard/mesh']
|
|
|
|
/** Route-to-background image mapping */
|
|
export const ROUTE_BACKGROUNDS: Record<string, string> = {
|
|
'/dashboard': 'bg-home.webp',
|
|
'/dashboard/': 'bg-home.webp',
|
|
'/dashboard/apps': 'bg-myapps.webp',
|
|
'/dashboard/discover': 'bg-appstore.webp',
|
|
'/dashboard/marketplace': 'bg-appstore.webp',
|
|
'/dashboard/cloud': 'bg-cloud.webp',
|
|
'/dashboard/mesh': 'bg-mesh.webp',
|
|
'/dashboard/server': 'bg-network.jpg',
|
|
'/dashboard/web5': 'bg-web5.jpg',
|
|
'/dashboard/server/federation': 'bg-web5.jpg',
|
|
'/dashboard/monitoring': 'bg-web5.jpg',
|
|
'/dashboard/fleet': 'bg-web5.jpg',
|
|
'/dashboard/settings': 'bg-settings.webp',
|
|
'/dashboard/chat': 'bg-aiui.jpg',
|
|
}
|
|
|
|
export function isDetailRoute(path: string): boolean {
|
|
return (path.includes('/apps/') && !path.endsWith('/apps')) ||
|
|
(path.includes('/marketplace/') && !path.endsWith('/marketplace'))
|
|
}
|
|
|
|
/**
|
|
* Creates a route transition tracker that determines the appropriate
|
|
* CSS transition name based on navigation direction and route depth.
|
|
*/
|
|
export function useRouteTransitions() {
|
|
let previousPath = ''
|
|
let previousTab = ''
|
|
|
|
function getTransitionName(currentRoute: RouteLocationNormalizedLoaded): string {
|
|
const currentPath = currentRoute.path
|
|
|
|
if (!previousPath) {
|
|
previousPath = currentPath
|
|
return 'fade'
|
|
}
|
|
|
|
// Chat transitions: directional slide
|
|
const isChat = currentPath === '/dashboard/chat'
|
|
const wasChat = previousPath === '/dashboard/chat'
|
|
if (isChat) {
|
|
previousPath = currentPath
|
|
return 'chat-open'
|
|
}
|
|
if (wasChat) {
|
|
previousPath = currentPath
|
|
return 'chat-close'
|
|
}
|
|
|
|
const isAppDetails = currentPath.includes('/apps/') && !currentPath.endsWith('/apps')
|
|
const isAppsList = currentPath === '/dashboard/apps'
|
|
const wasAppDetails = previousPath.includes('/apps/') && !previousPath.endsWith('/apps')
|
|
const wasAppsList = previousPath === '/dashboard/apps'
|
|
|
|
const isMarketplaceDetails = currentPath.includes('/marketplace/') && !currentPath.endsWith('/marketplace')
|
|
const isMarketplaceList = currentPath === '/dashboard/marketplace'
|
|
const wasMarketplaceDetails = previousPath.includes('/marketplace/') && !previousPath.endsWith('/marketplace')
|
|
const wasMarketplaceList = previousPath === '/dashboard/marketplace'
|
|
|
|
const isCloudFolder = currentPath.includes('/cloud/') && !currentPath.endsWith('/cloud')
|
|
const isCloudList = currentPath === '/dashboard/cloud'
|
|
const wasCloudFolder = previousPath.includes('/cloud/') && !previousPath.endsWith('/cloud')
|
|
const wasCloudList = previousPath === '/dashboard/cloud'
|
|
|
|
const isFederation = currentPath === '/dashboard/server/federation'
|
|
const wasFederation = previousPath === '/dashboard/server/federation'
|
|
const isMonitoring = currentPath === '/dashboard/monitoring'
|
|
const wasMonitoring = previousPath === '/dashboard/monitoring'
|
|
const isFleet = currentPath === '/dashboard/fleet'
|
|
const wasFleet = previousPath === '/dashboard/fleet'
|
|
const isWeb5 = currentPath === '/dashboard/web5'
|
|
const wasWeb5 = previousPath === '/dashboard/web5'
|
|
// Any Web5 sub-detail (networking-profits, credentials, …) animates as a
|
|
// depth push from/back-to the Web5 tab — same feel as Find Nodes.
|
|
const isWeb5Detail = currentPath.startsWith('/dashboard/web5/')
|
|
const wasWeb5Detail = previousPath.startsWith('/dashboard/web5/')
|
|
|
|
let transitionName = 'fade'
|
|
|
|
// Mobile: Horizontal slide transitions between sub-tabs
|
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
|
const isServices = currentPath === '/dashboard/apps' && (currentRoute.query.tab === 'services' || currentRoute.query.tab === 'websites')
|
|
const wasServices = previousTab === 'services' || previousTab === 'websites'
|
|
const currentAppsIdx = isServices ? 2
|
|
: currentPath === '/dashboard/marketplace' ? 1
|
|
: currentPath === '/dashboard/apps' ? 0 : -1
|
|
const prevAppsIdx = wasServices ? 2
|
|
: previousPath === '/dashboard/marketplace' ? 1
|
|
: previousPath === '/dashboard/apps' ? 0 : -1
|
|
|
|
const currentWeb5Idx = WEB5_TAB_ORDER.indexOf(currentPath)
|
|
const prevWeb5Idx = WEB5_TAB_ORDER.indexOf(previousPath)
|
|
|
|
if (currentAppsIdx !== -1 && prevAppsIdx !== -1 && currentAppsIdx !== prevAppsIdx) {
|
|
transitionName = currentAppsIdx > prevAppsIdx ? 'slide-left' : 'slide-right'
|
|
} else if (currentWeb5Idx !== -1 && prevWeb5Idx !== -1 && currentWeb5Idx !== prevWeb5Idx) {
|
|
transitionName = currentWeb5Idx > prevWeb5Idx ? 'slide-left' : 'slide-right'
|
|
} else {
|
|
const currentIndex = TAB_ORDER.indexOf(currentPath)
|
|
const previousIndex = TAB_ORDER.indexOf(previousPath)
|
|
|
|
if (currentIndex !== -1 && previousIndex !== -1 && currentIndex !== previousIndex) {
|
|
transitionName = currentIndex > previousIndex ? 'slide-down' : 'slide-up'
|
|
}
|
|
}
|
|
}
|
|
// Desktop depth transitions: list <-> detail
|
|
else if (wasAppsList && isAppDetails) {
|
|
transitionName = 'depth-forward'
|
|
} else if (wasAppDetails && isAppsList) {
|
|
transitionName = 'depth-back'
|
|
} else if (wasMarketplaceList && isMarketplaceDetails) {
|
|
transitionName = 'depth-forward'
|
|
} else if (wasMarketplaceDetails && isMarketplaceList) {
|
|
transitionName = 'depth-back'
|
|
} else if (wasCloudList && isCloudFolder) {
|
|
transitionName = 'depth-forward'
|
|
} else if (wasCloudFolder && isCloudList) {
|
|
transitionName = 'depth-back'
|
|
} else if (wasWeb5 && isFederation) {
|
|
transitionName = 'depth-forward'
|
|
} else if (wasFederation && isWeb5) {
|
|
transitionName = 'depth-back'
|
|
} else if (wasWeb5 && isMonitoring) {
|
|
transitionName = 'depth-forward'
|
|
} else if (wasMonitoring && isWeb5) {
|
|
transitionName = 'depth-back'
|
|
} else if (wasWeb5 && isFleet) {
|
|
transitionName = 'depth-forward'
|
|
} else if (wasFleet && isWeb5) {
|
|
transitionName = 'depth-back'
|
|
} else if (wasWeb5 && isWeb5Detail) {
|
|
transitionName = 'depth-forward'
|
|
} else if (wasWeb5Detail && isWeb5) {
|
|
transitionName = 'depth-back'
|
|
} else if (wasMarketplaceList && isAppDetails) {
|
|
transitionName = 'depth-forward'
|
|
} else if (wasAppDetails && isMarketplaceList) {
|
|
transitionName = 'depth-back'
|
|
}
|
|
// Desktop: no transition between Apps <-> Marketplace (same-page tab feel)
|
|
else if ((wasAppsList && isMarketplaceList) || (wasMarketplaceList && isAppsList)) {
|
|
transitionName = 'fade'
|
|
}
|
|
// Vertical transition: between main tabs (desktop)
|
|
else {
|
|
const currentIndex = TAB_ORDER.indexOf(currentPath)
|
|
const previousIndex = TAB_ORDER.indexOf(previousPath)
|
|
|
|
if (currentIndex !== -1 && previousIndex !== -1 && currentIndex !== previousIndex) {
|
|
transitionName = currentIndex > previousIndex ? 'slide-down' : 'slide-up'
|
|
}
|
|
}
|
|
|
|
previousPath = currentPath
|
|
previousTab = (currentRoute.query.tab as string) || ''
|
|
|
|
return transitionName
|
|
}
|
|
|
|
return { getTransitionName }
|
|
}
|