Files
archy/neode-ui/src/stores/goals.ts
T
archipelagoandClaude Fable 5 3aebbcbbb8 feat(setup): Zeus lightning journey — fund-wallet step, IBD timer + finish-setup toast, channel suggestions
Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node)
gains a guided path to a working channel:

- New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing
  its initial sync: while syncing it shows a live progress bar with a
  counting-down time-remaining estimate; once synced it shows the on-chain
  balance and a "Fund Wallet" button that opens the receive modal with a
  fresh address, QR, and the Zeus channel limits (min 150,000 / max
  1,500,000 sats) noted.
- When the sync finishes while a Lightning setup is mid-flight, a toast
  pops with a "Finish setup" link straight back to the wizard (toasts now
  support action links). If several setups are in flight, one is chosen —
  the shared steps complete the lightning part of any of them.
- Channel steps are Zeus-branded (logo + copy) and land on the Lightning
  Channels screen, which now carries an "Open a channel with Zeus" card
  that prefills the open-channel modal with the Olympus peer URI, 150k
  sats, and private-channel checked. "Get Zeus" links to zeusln.com.
- Setup completion now actually requires walking the manual steps (fund,
  open channel, configure) — previously any step whose app was installed
  was silently auto-ticked and running apps marked the whole goal done.
- Completion CTAs now go to the app you just set up ("Go to my shop
  (BTCPay)" etc.) instead of the generic services list; iframe apps open
  in the on-top app overlay.
- On-chain send modal gains a "Send all funds" sweep toggle, backed by
  LND's send_all on the backend (amount no longer required when sweeping).
- Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress
  contract the UI reads and simulates a ~90s IBD ramp per visitor so the
  timer, toast, and fund flow can all be demoed live; channel list data
  fixed (status/channel_point/liquidity totals — the panel previously
  crashed on the missing status field); sendcoins supports send_all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:51:12 -04:00

162 lines
4.8 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { GoalProgress, GoalStatus } from '@/types/goals'
import { GOALS } from '@/data/goals'
import { useAppStore } from './app'
const STORAGE_KEY = 'archipelago-goal-progress'
/** App ID aliases — goal definitions use canonical IDs but the backend may register under variant names */
const APP_ALIASES: Record<string, string[]> = {
immich: ['immich-server', 'immich-app', 'immich_server'],
nextcloud: ['nextcloud-aio', 'nextcloud-server'],
'bitcoin-knots': ['bitcoin', 'bitcoin-core'],
}
/** Pre-built reverse lookup: variant pkgId → Set of canonical appIds it matches */
const ALIAS_REVERSE = new Map<string, Set<string>>()
for (const [canonical, aliases] of Object.entries(APP_ALIASES)) {
for (const alias of aliases) {
let set = ALIAS_REVERSE.get(alias)
if (!set) { set = new Set(); ALIAS_REVERSE.set(alias, set) }
set.add(canonical)
}
}
function matchesAppId(pkgId: string, appId: string): boolean {
if (pkgId === appId) return true
const reverseHits = ALIAS_REVERSE.get(pkgId)
return reverseHits ? reverseHits.has(appId) : false
}
export const useGoalStore = defineStore('goals', () => {
const progress = ref<Record<string, GoalProgress>>({})
function load() {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) progress.value = JSON.parse(raw)
} catch {
/* ignore corrupt data */
}
}
function save() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(progress.value)) } catch { /* localStorage full or unavailable */ }
}
function getGoalStatus(goalId: string): GoalStatus {
const goal = GOALS.find((g) => g.id === goalId)
if (!goal) return 'not-started'
// Goals with no required apps use manual progress tracking
if (goal.requiredApps.length === 0) {
return progress.value[goalId]?.status || 'not-started'
}
const appStore = useAppStore()
const packages = appStore.packages
// Auto-sync install step completion from actual package state
// This ensures steps tick when apps are installed outside the wizard
let didSync = false
for (const step of goal.steps) {
if (step.appId && step.action === 'install') {
const isInstalled = Object.keys(packages).some((pkgId) => matchesAppId(pkgId, step.appId!))
if (isInstalled) {
if (!progress.value[goalId]) {
progress.value[goalId] = {
goalId,
status: 'in-progress',
currentStepIndex: 0,
completedSteps: [],
startedAt: Date.now(),
}
didSync = true
}
if (!progress.value[goalId].completedSteps.includes(step.id)) {
progress.value[goalId].completedSteps.push(step.id)
didSync = true
}
}
}
}
if (didSync) save()
const allRunning = goal.requiredApps.every((appId) =>
Object.entries(packages).some(
([pkgId, pkg]) => matchesAppId(pkgId, appId) && pkg.state === 'running',
),
)
// Manual steps (fund the wallet, open a channel, configure the store…)
// must be walked through too — running apps alone don't finish a goal.
const done = progress.value[goalId]?.completedSteps ?? []
const allManualDone = goal.steps
.filter((s) => s.action !== 'install')
.every((s) => done.includes(s.id))
if (allRunning && allManualDone) return 'completed'
const anyInstalled = goal.requiredApps.some((appId) =>
Object.keys(packages).some((pkgId) => matchesAppId(pkgId, appId)),
)
if (allRunning || anyInstalled || progress.value[goalId]) return 'in-progress'
return 'not-started'
}
const goalStatuses = computed(() => {
const statuses: Record<string, GoalStatus> = {}
for (const goal of GOALS) {
statuses[goal.id] = getGoalStatus(goal.id)
}
return statuses
})
function startGoal(goalId: string) {
progress.value[goalId] = {
goalId,
status: 'in-progress',
currentStepIndex: 0,
completedSteps: [],
startedAt: Date.now(),
}
save()
}
function completeStep(goalId: string, stepId: string) {
const p = progress.value[goalId]
if (!p) return
if (!p.completedSteps.includes(stepId)) {
p.completedSteps.push(stepId)
}
const goal = GOALS.find((g) => g.id === goalId)
if (goal && p.completedSteps.length >= goal.steps.length) {
p.status = 'completed'
} else {
p.currentStepIndex = Math.min(p.currentStepIndex + 1, (goal?.steps.length ?? 1) - 1)
}
save()
}
function resetGoal(goalId: string) {
delete progress.value[goalId]
save()
}
// Load on store creation
load()
return {
progress,
goalStatuses,
getGoalStatus,
startGoal,
completeStep,
resetGoal,
}
})