Puts Marketplace.vue's data on the cache and adds the subtle background-
refresh indicator D-05 calls for:
- RefreshIndicator.vue: presentational, state-driven (ResourceLoadState).
Renders nothing for ready/idle/loading — a first load is the view's own
skeleton's job, not this component's — and a role="status"
aria-live="polite" spinner only while refreshing. A fixed-size outer slot
keeps its appearance/disappearance from ever shifting layout.
- Marketplace.vue: loadCommunityMarketplace()/loadBitcoinPruneStatus() move
onto useCachedResource behind shared keys app-catalog (300s TTL, persist —
near-static catalog, D-06 discretion) and bitcoin.prune-status (default
30s TTL, persist — both non-sensitive/small per T-02-01). app-catalog is a
shared key so Discover.vue's identical loader picks up the same entry
without its own conversion in 02-04. Error handling is keep-last-value
(D-07): a rejected refresh sets the existing communityError banner ref,
never a toast, and prior app cards stay on screen.
- Side-effect audit (the precedent 02-04 repeats across remaining tabs):
marketplaceAnimationDone is genuinely once-per-session intro state, stays
in onMounted; the two data loads needed no onMounted/onActivated hook of
their own at all — useCachedResource's internal onActivated (wired in
Task 1) already revalidates them on every kept-alive tab re-entry,
staleness-gated so a quick revisit issues no fetch. No interval,
subscription or window listener exists in this view, so no onDeactivated
teardown was needed either.
- Tests: RefreshIndicator's full render-nothing/render-something matrix
added to keepAliveTabs.test.ts (no router dependency, safe to colocate).
The D-07 rejected-refresh-keeps-content-and-no-toast test mounts
Marketplace.vue itself but lives in a new file,
views/__tests__/MarketplaceRefresh.test.ts, following the in-repo
CloudPeersRefresh.test.ts mount-the-view pattern — vi.mock('vue-router')
is hoisted file-wide, so colocating it in keepAliveTabs.test.ts would
clobber that file's real createRouter/createMemoryHistory used by the
DashboardRouterView tests (Rule 3 auto-fix; deviation from the plan's
literal single-test-file file list, documented here for the SUMMARY).
Marketplace.vue gains a defineExpose({ loadCommunityMarketplace,
loadBitcoinPruneStatus }) for tests, mirroring Cloud.vue's existing
defineExpose({ loadPeers }).
Full suite (87 files / 709 tests), type-check and build all green; the
built Marketplace chunk carries refresh-indicator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Archipelago Web UI
Vue 3 + TypeScript + Vite + Tailwind CSS + Pinia
The web interface for Archipelago — a self-sovereign Bitcoin Node OS.
Quick Start
cd neode-ui
npm install
npm start
Visit http://localhost:8100 — login with password: password123
This starts:
- Mock backend on port 5959 (no Docker required)
- Vite dev server on port 8100 with HMR
Stop with npm stop.
Architecture
neode-ui/
├── src/
│ ├── api/ # RPC client (rpc-client.ts), WebSocket, container client
│ ├── stores/ # Pinia stores (app, container, appLauncher, monitoring)
│ ├── views/ # Page components (Dashboard, Marketplace, Settings, etc.)
│ ├── components/ # Reusable components (SplashScreen, AppSession, etc.)
│ ├── router/ # Vue Router configuration
│ ├── types/ # TypeScript type definitions
│ └── style.css # Global styles + Tailwind utilities
├── public/assets/ # Static assets (images, fonts, app icons, audio)
├── mock-backend.js # Mock backend server (simulates Rust backend)
├── docker/ # Docker configs (nginx, entrypoint)
└── vite.config.ts # Vite config with backend proxy
Dev Modes
The mock backend supports multiple startup modes via VITE_DEV_MODE:
| Mode | Command | Behavior |
|---|---|---|
| default | npm start |
Fully set up, login screen |
| existing | VITE_DEV_MODE=existing npm run dev:mock |
Same as default |
| setup | VITE_DEV_MODE=setup npm run dev:mock |
First-time password setup flow |
| onboarding | VITE_DEV_MODE=onboarding npm run dev:mock |
Post-setup onboarding flow |
| boot | npm run dev:boot |
25s simulated boot sequence |
Mock Backend
The mock backend (mock-backend.js) simulates the full Rust backend for local development:
Pre-installed apps (always visible in My Apps):
- Bitcoin Core, LND, Electrs, Mempool, FileBrowser, LoraBell, Fedimint
Marketplace: 30+ curated apps with Docker images, install/uninstall simulation
Features simulated:
- Authentication (login, password change, TOTP 2FA)
- System metrics (CPU, memory, disk — randomized for realism)
- Node identity (DID, Nostr pubkey, Tor address)
- Federation (3 mock nodes with apps, metrics, trust levels)
- Mesh networking (4 LoRa peers, encrypted messaging, invoices)
- Peer-to-peer messaging
- FileBrowser API (mock file system with Music, Documents, Photos, Videos)
- DWN sync status
- Transport layer (mesh/LAN/Tor routing)
- Notifications (5 realistic entries)
- Claude AI chat proxy (requires
ANTHROPIC_API_KEY)
Container runtime: If Docker/Podman is available, the mock backend will run real containers for installed apps. Otherwise, it simulates them.
Demo Deployment (Portainer)
Deploy the demo via Docker Compose for showcasing:
docker compose -f docker-compose.demo.yml build
docker compose -f docker-compose.demo.yml up -d
Or deploy through Portainer Stacks:
- Stacks > Add stack > name:
archy-demo - Web editor: paste
docker-compose.demo.ymlcontents - Add environment variable:
ANTHROPIC_API_KEY(for Claude chat) - Deploy
Access at http://your-host:4848 — password: password123
Development Commands
npm start # Start mock backend + Vite (recommended)
npm stop # Stop all servers
npm run dev:mock # Same as start, without port cleanup
npm run dev:boot # Boot mode (simulated startup delay)
npm run backend:mock # Mock backend only
npm run dev # Vite only (needs backend running separately)
npm run dev:real # Vite with real Rust backend
npm run build # Production build (outputs to ../web/dist/neode-ui/)
npm run build:docker # Build for Docker (no type checking)
npm run type-check # TypeScript type checking
npm test # Run tests
Design System
Glass Classes
| Class | Use |
|---|---|
.glass-card |
Content containers, modals, panels |
.glass-button |
ALL buttons (primary and secondary) |
.path-option-card |
Interactive cards with hover lift |
.info-card |
Status badges, metric displays |
Tokens
- Font: Avenir Next (primary), Montserrat (
font-archipelago) - Glass:
bg: rgba(0,0,0,0.60),blur: 24px,border: rgba(255,255,255,0.22) - Accent:
#fb923c(Bitcoin orange),#4ade80(green),#ef4444(red) - Text:
rgba(255,255,255,0.9)primary,rgba(255,255,255,0.6)muted
Rules
- Global CSS classes in
style.cssonly — never inline Tailwind in components .gradient-buttonis banned — use.glass-button- All components use
<script setup lang="ts">
API
import { rpcClient } from '@/api/rpc-client'
await rpcClient.login('password')
await rpcClient.startPackage('bitcoin')
const metrics = await rpcClient.getMetrics()
State management via Pinia stores. WebSocket patches applied automatically.
Build Output
- Dev build:
../web/dist/neode-ui/ - Docker build:
dist/(deployed to nginx) - Production deploy: via
scripts/deploy-to-target.sh --live
License
MIT