Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9c3311eff | ||
|
|
338ae9a6de | ||
|
|
8f397b03f9 | ||
|
|
48d5fd0045 | ||
|
|
a44cbe478a | ||
|
|
519b4b209a | ||
|
|
2c82b498f0 | ||
|
|
efd1ae41de | ||
|
|
0c591a997e | ||
|
|
91e1059f56 |
+3
-3
@@ -7,10 +7,10 @@
|
||||
- "Add Service" in the Tor panel now works for every app, not just a fixed list — the node reads the app's actual web port, so apps like Gitea, Jellyfin, Nextcloud, and Uptime Kuma no longer fail with "see server logs".
|
||||
- Renaming your node now genuinely renames it everywhere: the machine's hostname, its .local network name (re-announced immediately), the local hosts file, and the HTTPS certificate all follow — so http and https links using your node's name keep working right after a rename.
|
||||
- The node no longer mistakes a VPN tunnel for its own address. On fresh installs with NetBird, apps could launch on an internal 10.x address instead of your LAN IP; the node now reads its address from the actual network route, fixing app launch links, generated app configs, and VPN setup.
|
||||
- Your cloud got a real layout: Apps-style tabs with categories for Folders, My Files, and Peer Files, readable file rows, and a search that also finds files shared by your federated peer nodes.
|
||||
- The first-login dashboard entrance animation is back, and "Replay Intro" in Settings actually replays it.
|
||||
- Your cloud got a real layout: Apps-style tabs with categories for Folders, My Files, and Peer Files, readable file rows, a search that also finds files shared by your federated peer nodes — and music now opens in the bottom-bar player instead of a broken preview window.
|
||||
- The first-login experience flows again: the dashboard entrance animation is back — and you can actually hear it now (its sound was silently swallowed before, including on replays) — "Replay Intro" in Settings actually replays it, opening a direct link to an inner page no longer detours through the splash screen, the login screen keeps the intro video until your first login (switching to rotating backgrounds after), and the intro video streams three times lighter so it starts instantly.
|
||||
- Changing DNS settings no longer blanks the page, and the DNS and WiFi dialogs now cover the whole app instead of only the right panel.
|
||||
- The public demo is richer and truer: Ark wallet flows, working DNS and Tor service management, and a library of peer content with previews that never break.
|
||||
- The public demo is richer and truer: the intro plays on every fresh visit, Ark wallet flows, working DNS and Tor service management, and a library of peer content with previews that never break.
|
||||
- Assorted fixes: failed installs clean up after themselves properly, and the transactions view fits mobile screens (capped at 60% of the visible viewport).
|
||||
|
||||
## v1.7.100-alpha (2026-07-14)
|
||||
|
||||
Generated
+1
-1
@@ -95,7 +95,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.100-alpha"
|
||||
version = "1.7.101-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.100-alpha"
|
||||
version = "1.7.101-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -187,8 +187,9 @@ http {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
# Cache static assets (media too — the intro video/audio are versioned
|
||||
# with ?v=N query busters, so immutable is safe)
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|mp4|webm|mp3|woff2?)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* End-to-end verification of the FIRST-VISIT cinematic:
|
||||
* splash (tap logo → alien typing → Welcome Noderunner + speech + synthwave
|
||||
* → Archipelago logo) → onboarding intro → login (video background, finale
|
||||
* armed) → dashboard full reveal (background zoom + interface assembly +
|
||||
* oomph) → welcome typing — and that a SECOND login stays deliberately
|
||||
* low-key.
|
||||
*
|
||||
* Run against the local demo stack:
|
||||
* DEMO=1 node mock-backend.js (port 5959)
|
||||
* VITE_DEMO=1 npx vite --port 8100
|
||||
* ARCHY_BASE_URL=http://localhost:8100 npx playwright test e2e/intro-experience.spec.ts
|
||||
*
|
||||
* Audio can't be heard headless, so every HTMLMediaElement.play() and
|
||||
* WebAudio oscillator start is recorded into window.__audioLog via an init
|
||||
* script — the assertions check the right cues fired at the right phases.
|
||||
*/
|
||||
import { test, expect, type Page } from '@playwright/test'
|
||||
|
||||
// Real Chrome (new headless): the bundled headless-shell has no working video
|
||||
// pipeline (0 fps, ~80% dropped frames), which makes every media assertion
|
||||
// meaningless there. Requires Google Chrome installed.
|
||||
test.use({ channel: 'chrome' })
|
||||
|
||||
const BASE = process.env.ARCHY_BASE_URL ?? 'http://localhost:8100'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__audioLog: string[]
|
||||
__videoStats: { stalls: number; waiting: number; dropped: number; total: number; readyStateAtPlay: number }
|
||||
__revealSeen: { zoom?: boolean; glass?: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
async function instrumentAudioAndVideo(page: Page) {
|
||||
await page.addInitScript(() => {
|
||||
window.__audioLog = []
|
||||
window.__videoStats = { stalls: 0, waiting: 0, dropped: 0, total: 0, readyStateAtPlay: -1 }
|
||||
|
||||
// The dashboard reveal classes live only ~8s; a slow run can burn that
|
||||
// window between two sequential expect() polls. Record their appearance
|
||||
// the moment it happens instead, and let the test assert on the record.
|
||||
window.__revealSeen = {}
|
||||
new MutationObserver(() => {
|
||||
if (!window.__revealSeen.zoom && document.querySelector('.zoom-reveal-bg')) window.__revealSeen.zoom = true
|
||||
if (!window.__revealSeen.glass && document.querySelector('.glass-throw-active')) window.__revealSeen.glass = true
|
||||
}).observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] })
|
||||
|
||||
const origPlay = HTMLMediaElement.prototype.play
|
||||
HTMLMediaElement.prototype.play = function (...args) {
|
||||
const src = (this.currentSrc || this.src || (this.querySelector?.('source') as HTMLSourceElement | null)?.src || 'unknown')
|
||||
window.__audioLog.push(`media-play:${src.split('/').pop()}`)
|
||||
if (this.tagName === 'VIDEO') {
|
||||
const v = this as HTMLVideoElement
|
||||
if (window.__videoStats.readyStateAtPlay === -1) window.__videoStats.readyStateAtPlay = v.readyState
|
||||
v.addEventListener('stalled', () => { window.__videoStats.stalls++ })
|
||||
v.addEventListener('waiting', () => { window.__videoStats.waiting++ })
|
||||
}
|
||||
return origPlay.apply(this, args)
|
||||
}
|
||||
|
||||
// WebAudio: oscillator/buffer starts = synth pops, oomph layers, synthwave.
|
||||
const OrigOsc = OscillatorNode.prototype.start
|
||||
OscillatorNode.prototype.start = function (...args) {
|
||||
window.__audioLog.push('osc-start')
|
||||
return OrigOsc.apply(this, args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function freshVisit(page: Page) {
|
||||
await page.goto(BASE + '/')
|
||||
await page.evaluate(() => { localStorage.clear(); sessionStorage.clear() })
|
||||
await page.goto(BASE + '/')
|
||||
}
|
||||
|
||||
/** Walk the full splash from tap-to-start through completion (real time). */
|
||||
async function runSplash(page: Page, { skip }: { skip: boolean }) {
|
||||
// Phase 1: tap-to-start — "Enter to Exit" + logo (the overlay animates away
|
||||
// on click, so don't wait for post-click actionability)
|
||||
await expect(page.getByText('Enter to Exit')).toBeVisible({ timeout: 15_000 })
|
||||
await page.locator('.tap-to-start-logo').click({ noWaitAfter: true, force: true })
|
||||
|
||||
// Phase 2: alien typing begins (first line types out)
|
||||
await expect(page.getByText('In the future there will be 3 types', { exact: false }))
|
||||
.toBeVisible({ timeout: 10_000 })
|
||||
|
||||
if (skip) {
|
||||
await page.getByRole('button', { name: 'Skip Intro' }).click({ noWaitAfter: true })
|
||||
} else {
|
||||
// Let all four lines type out for real (~20s)
|
||||
await expect(page.getByText('And Noderunners...', { exact: false })).toBeVisible({ timeout: 45_000 })
|
||||
}
|
||||
|
||||
// Phase 3+4 (Welcome Noderunner → logo) mount the background video. The
|
||||
// text is only on screen ~6s, so verify the phases via durable signals:
|
||||
// the video's live health here, and the audio log (speech/song) after.
|
||||
await page.waitForFunction(() => !!document.querySelector('video'), { timeout: 30_000 })
|
||||
// Wait for actual smooth playback (cold-cache buffering right after mount
|
||||
// is masked by the design's 0.3-opacity fade — smoothness is what matters).
|
||||
await page.waitForFunction(() => {
|
||||
const v = document.querySelector('video')
|
||||
return !!v && v.readyState >= 3 && v.currentTime > 0.3
|
||||
}, { timeout: 20_000 })
|
||||
const s1 = await page.evaluate(() => {
|
||||
const v = document.querySelector('video')!
|
||||
const q = v.getVideoPlaybackQuality?.()
|
||||
return { t: v.currentTime, dropped: q?.droppedVideoFrames ?? 0, total: q?.totalVideoFrames ?? 0 }
|
||||
})
|
||||
await page.waitForTimeout(2_000)
|
||||
const s2 = await page.evaluate(() => {
|
||||
const v = document.querySelector('video')
|
||||
if (!v) return null
|
||||
const q = v.getVideoPlaybackQuality?.()
|
||||
return { t: v.currentTime, dropped: q?.droppedVideoFrames ?? 0, total: q?.totalVideoFrames ?? 0 }
|
||||
})
|
||||
expect(s2).not.toBeNull()
|
||||
// The 8.1s video loops; a loop wrap makes (t - t1) negative — treat as full progress.
|
||||
const progressed = s2!.t >= s1.t ? s2!.t - s1.t : s2!.t + (8.1 - s1.t)
|
||||
expect(progressed).toBeGreaterThan(1.2) // ≥1.2s progress in 2s wall = playing smoothly
|
||||
// Steady-state frame drops over the sample window (startup catch-up excluded).
|
||||
const dTotal = s2!.total - s1.total
|
||||
const dDropped = s2!.dropped - s1.dropped
|
||||
if (dTotal > 30) expect(dDropped / dTotal).toBeLessThan(0.2)
|
||||
|
||||
// Splash completes → demo routes to the onboarding intro
|
||||
await page.waitForURL('**/onboarding/intro', { timeout: 60_000 })
|
||||
}
|
||||
|
||||
async function enterDemoAndLogin(page: Page) {
|
||||
// The CTA unmounts mid-click when the router transitions away — dispatch
|
||||
// once, swallow the detach retry, and trust the URL change instead.
|
||||
const cta = page.getByRole('button', { name: /Enter the demo/ })
|
||||
await cta.waitFor({ timeout: 15_000 })
|
||||
await Promise.all([
|
||||
page.waitForURL('**/login', { timeout: 15_000 }),
|
||||
cta.click({ noWaitAfter: true }).catch(() => {}),
|
||||
])
|
||||
// Demo prefills the password; the finale flag must be armed at this point.
|
||||
expect(await page.evaluate(() => sessionStorage.getItem('archy_onboarding_finale'))).toBe('1')
|
||||
const loginBtn = page.getByRole('button', { name: /log ?in/i })
|
||||
await loginBtn.waitFor({ timeout: 10_000 })
|
||||
await Promise.all([
|
||||
page.waitForURL('**/dashboard**', { timeout: 25_000 }),
|
||||
loginBtn.click({ noWaitAfter: true }).catch(() => {}),
|
||||
])
|
||||
}
|
||||
|
||||
test.describe('first-visit cinematic', () => {
|
||||
test('full no-skip run: sounds, video health, zoom reveal, welcome typing', async ({ page }) => {
|
||||
test.setTimeout(240_000)
|
||||
await instrumentAudioAndVideo(page)
|
||||
await freshVisit(page)
|
||||
|
||||
await runSplash(page, { skip: false })
|
||||
|
||||
// Cinematic audio fired: intro typing loop, the Welcome Noderunner speech
|
||||
// and the synthwave bed (cosmic-updrift) — the exact regression reported.
|
||||
const log = await page.evaluate(() => window.__audioLog.join('|'))
|
||||
expect(log).toContain('welcome-noderunner.mp3')
|
||||
expect(log).toContain('cosmic-updrift.mp3')
|
||||
|
||||
await enterDemoAndLogin(page)
|
||||
|
||||
// FULL first-entry reveal: big background zoom + glass assembly classes
|
||||
// (recorded by the init-script observer the instant they appear — the
|
||||
// classes only live ~8s and sequential polling can miss the window).
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__revealSeen), { timeout: 15_000 })
|
||||
.toMatchObject({ zoom: true, glass: true })
|
||||
|
||||
// Welcome typing kicks in ~4s into the reveal and animates the home cards.
|
||||
await expect(page.locator('.home-card-animate').first()).toBeVisible({ timeout: 15_000 })
|
||||
|
||||
// The reveal runs 8s, then the zoom layer class clears.
|
||||
await expect(page.locator('.zoom-reveal-bg')).toHaveCount(0, { timeout: 20_000 })
|
||||
|
||||
// The dashboard oomph is WebAudio oscillators — at least the login pop +
|
||||
// oomph layers must have started after the splash's own sounds.
|
||||
const oscCount = await page.evaluate(() => window.__audioLog.filter(e => e === 'osc-start').length)
|
||||
expect(oscCount).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('skip-intro run still gets speech, song and the dashboard reveal', async ({ page }) => {
|
||||
test.setTimeout(180_000)
|
||||
await instrumentAudioAndVideo(page)
|
||||
await freshVisit(page)
|
||||
|
||||
await runSplash(page, { skip: true })
|
||||
|
||||
const log = await page.evaluate(() => window.__audioLog.join('|'))
|
||||
expect(log).toContain('welcome-noderunner.mp3')
|
||||
expect(log).toContain('cosmic-updrift.mp3')
|
||||
|
||||
await enterDemoAndLogin(page)
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__revealSeen.zoom), { timeout: 15_000 })
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
test('second login is deliberately low-key (no zoom reveal)', async ({ page }) => {
|
||||
test.setTimeout(180_000)
|
||||
await instrumentAudioAndVideo(page)
|
||||
await freshVisit(page)
|
||||
await runSplash(page, { skip: true })
|
||||
await enterDemoAndLogin(page)
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__revealSeen.zoom), { timeout: 15_000 })
|
||||
.toBe(true)
|
||||
|
||||
// End the session the way logout does (auth token only — the intro/login
|
||||
// flags survive) and revisit login directly. An authenticated /login visit
|
||||
// would just bounce back to the dashboard via the router guard.
|
||||
await page.evaluate(() => localStorage.removeItem('neode-auth'))
|
||||
// Direct /login navigation (no splash — not a root boot) and re-login.
|
||||
await page.goto(BASE + '/login')
|
||||
await page.locator('#login-password').waitFor({ timeout: 15_000 })
|
||||
// First login happened → static rotated background, not the video.
|
||||
await expect(page.locator('.bg-login-static')).toBeVisible({ timeout: 10_000 })
|
||||
expect(await page.evaluate(() => localStorage.getItem('neode_first_login_done'))).toBe('1')
|
||||
|
||||
const reloginBtn = page.getByRole('button', { name: /log ?in/i })
|
||||
await Promise.all([
|
||||
page.waitForURL('**/dashboard**', { timeout: 25_000 }),
|
||||
reloginBtn.click({ noWaitAfter: true }).catch(() => {}),
|
||||
])
|
||||
// Low-key entrance: NO zoom reveal on a regular re-login. The observer
|
||||
// reset with the /login reload, so it would have caught even a transient
|
||||
// reveal since then.
|
||||
await page.waitForTimeout(1500)
|
||||
expect(await page.evaluate(() => window.__revealSeen.zoom ?? false)).toBe(false)
|
||||
await expect(page.locator('.zoom-reveal-bg')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('demo replays the cinematic on a fresh boot at root', async ({ page }) => {
|
||||
test.setTimeout(120_000)
|
||||
await freshVisit(page)
|
||||
await runSplash(page, { skip: true })
|
||||
await enterDemoAndLogin(page)
|
||||
|
||||
// Reload at root = a fresh boot → the splash must return even though
|
||||
// neode_intro_seen is now set.
|
||||
await page.goto(BASE + '/')
|
||||
await expect(page.getByText('Enter to Exit')).toBeVisible({ timeout: 15_000 })
|
||||
})
|
||||
|
||||
test('video is warmed before the splash needs it', async ({ page }) => {
|
||||
await freshVisit(page)
|
||||
// The warm-up is a detached <video preload=auto> kept on window (Chromium
|
||||
// has no <link rel=preload as=video>). Give it a moment to buffer.
|
||||
await expect
|
||||
.poll(async () => page.evaluate(() => {
|
||||
const w = (window as unknown as { __introVideoWarm?: HTMLVideoElement }).__introVideoWarm
|
||||
return w ? { src: w.src, readyState: w.readyState } : null
|
||||
}), { timeout: 15_000 })
|
||||
.toMatchObject({ src: expect.stringContaining('video-intro.mp4') })
|
||||
const ready = await page.evaluate(() =>
|
||||
(window as unknown as { __introVideoWarm?: HTMLVideoElement }).__introVideoWarm?.readyState ?? 0)
|
||||
expect(ready).toBeGreaterThanOrEqual(1) // metadata in = download underway
|
||||
})
|
||||
})
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.100-alpha",
|
||||
"version": "1.7.101-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.100-alpha",
|
||||
"version": "1.7.101-alpha",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.7.100-alpha",
|
||||
"version": "1.7.101-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
Binary file not shown.
+37
-3
@@ -400,14 +400,19 @@ onMounted(async () => {
|
||||
// onboarded node).
|
||||
let replayRequested = sessionStorage.getItem('archipelago_replay_intro') === '1'
|
||||
if (replayRequested) sessionStorage.removeItem('archipelago_replay_intro')
|
||||
// The route object still holds the router's START_LOCATION ('/') here —
|
||||
// the initial navigation resolves asynchronously — so a deep-route load
|
||||
// (e.g. a direct /login visit) would masquerade as a root boot. The URL
|
||||
// bar is the only truthful source this early.
|
||||
const bootPath = window.location.pathname
|
||||
// Public demo: every fresh boot at the root (first visit or a browser
|
||||
// refresh) starts with the typing splash for the full effect. In-session SPA
|
||||
// navigation never remounts App, and deep-route refreshes keep their place.
|
||||
const { IS_DEMO } = await import('@/composables/useDemoIntro')
|
||||
if (IS_DEMO && route.path === '/') replayRequested = true
|
||||
if (IS_DEMO && bootPath === '/') replayRequested = true
|
||||
let onboardingComplete: boolean | null = localStorage.getItem('neode_onboarding_complete') === '1' ? true : null
|
||||
const splashCandidate = !seenIntro
|
||||
&& (fromBoot || (route.path === '/' && import.meta.env.VITE_DEV_MODE !== 'boot'))
|
||||
&& (fromBoot || (bootPath === '/' && import.meta.env.VITE_DEV_MODE !== 'boot'))
|
||||
|
||||
if (splashCandidate && onboardingComplete !== true) {
|
||||
try {
|
||||
@@ -427,12 +432,30 @@ onMounted(async () => {
|
||||
|
||||
if (shouldShowIntroSplash({
|
||||
seenIntro,
|
||||
routePath: route.path,
|
||||
routePath: bootPath,
|
||||
fromBoot,
|
||||
devMode: import.meta.env.VITE_DEV_MODE,
|
||||
onboardingComplete,
|
||||
replayRequested,
|
||||
})) {
|
||||
// The intro is definitely playing — unmute the whole cinematic (speech,
|
||||
// synthwave, pops) even on browsers whose localStorage says onboarding is
|
||||
// complete; the sound gate otherwise silences replays.
|
||||
const { enableCinematicSounds } = await import('@/composables/useLoginSounds')
|
||||
enableCinematicSounds()
|
||||
// Kick off the intro video download NOW: the splash's <video> element only
|
||||
// mounts ~20s in (after the typing sequence), and on a cold cache the file
|
||||
// would otherwise start fetching mid-sequence and stutter. A detached
|
||||
// <video preload=auto> is used because Chromium does not support
|
||||
// <link rel=preload as=video> — the media cache then serves the splash's
|
||||
// element, which uses the identical URL.
|
||||
try {
|
||||
const warm = document.createElement('video')
|
||||
warm.muted = true
|
||||
warm.preload = 'auto'
|
||||
warm.src = '/assets/video/video-intro.mp4?v=8'
|
||||
;(window as unknown as { __introVideoWarm?: HTMLVideoElement }).__introVideoWarm = warm
|
||||
} catch { /* ignore */ }
|
||||
// Coming from boot screen — show the full splash intro (Enter to Exit → typing → logo)
|
||||
showSplash.value = true
|
||||
} else {
|
||||
@@ -505,6 +528,17 @@ async function handleSplashComplete() {
|
||||
return
|
||||
}
|
||||
|
||||
// Demo: the cinematic always continues into the onboarding intro page —
|
||||
// the mock backend reports "onboarded", which would otherwise route
|
||||
// straight to /login and cut the sequence short.
|
||||
{
|
||||
const { IS_DEMO } = await import('@/composables/useDemoIntro')
|
||||
if (IS_DEMO) {
|
||||
router.push('/onboarding/intro').catch(() => {})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { checkOnboardingStatus } = await import('@/composables/useOnboarding')
|
||||
const seenOnboarding = await checkOnboardingStatus()
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
preload="auto"
|
||||
poster="/assets/img/bg-intro.jpg"
|
||||
>
|
||||
<source src="/assets/video/video-intro.mp4?v=7" type="video/mp4">
|
||||
<source src="/assets/video/video-intro.mp4?v=8" type="video/mp4">
|
||||
<!-- Fallback to image if video fails -->
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
@@ -266,16 +266,12 @@ watch([showWelcome, showLogo], ([welcome, logo]) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Check if user has seen intro
|
||||
// Also detect returning users who cleared cache: if we're on a /dashboard route,
|
||||
// the backend session is still active so the user has been here before.
|
||||
const storedSeenIntro = localStorage.getItem('neode_intro_seen') === '1'
|
||||
const isOnDashboard = window.location.pathname.startsWith('/dashboard')
|
||||
const seenIntro = storedSeenIntro || isOnDashboard
|
||||
// Persist the flag so subsequent loads don't re-check
|
||||
if (!storedSeenIntro && isOnDashboard) {
|
||||
localStorage.setItem('neode_intro_seen', '1')
|
||||
}
|
||||
// NOTE: whether the intro should play is decided ONCE by App.vue
|
||||
// (shouldShowIntroSplash) — this component is only mounted when the answer was
|
||||
// yes, so it must not re-check neode_intro_seen itself. The old internal
|
||||
// re-check silently skipped the whole sequence for any browser that had seen
|
||||
// the intro before, even when App.vue explicitly asked for a replay (demo
|
||||
// fresh visits, the Replay Intro link).
|
||||
|
||||
function handleEnterKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && showTapToStart.value && !tapStartTransitioning.value) {
|
||||
@@ -468,13 +464,7 @@ function startAlienIntro() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (seenIntro) {
|
||||
showSplash.value = false
|
||||
document.body.classList.add('splash-complete')
|
||||
emit('complete')
|
||||
} else {
|
||||
window.addEventListener('keydown', handleEnterKey)
|
||||
}
|
||||
window.addEventListener('keydown', handleEnterKey)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -8,14 +8,27 @@
|
||||
* exempt and continue to play regardless.
|
||||
*/
|
||||
|
||||
/** One-way, per-page-load override: when App.vue decides to run the intro
|
||||
* splash (demo fresh boot, prod first install, Replay Intro), the WHOLE
|
||||
* cinematic — speech, synthwave, pops — must be audible even though
|
||||
* localStorage already says onboarding is complete. Without this the
|
||||
* replayed intro runs silent (lost "Welcome Noderunner" + song). */
|
||||
let cinematicMode = false
|
||||
export function enableCinematicSounds() {
|
||||
cinematicMode = true
|
||||
}
|
||||
|
||||
/** True when the node has not yet completed onboarding — i.e. we're
|
||||
* still in the first-install cinematic. Reads the localStorage cache
|
||||
* set by useOnboarding (which is re-seeded from the backend on each
|
||||
* successful check), so this stays correct after a browser clear
|
||||
* once the onboarding-complete probe runs. Sound calls that fire
|
||||
* before that probe completes will fall through silent on an already-
|
||||
* onboarded node — which is exactly what we want. */
|
||||
* still in the first-install cinematic — or when the intro is being
|
||||
* deliberately replayed this page load (see enableCinematicSounds).
|
||||
* Reads the localStorage cache set by useOnboarding (which is
|
||||
* re-seeded from the backend on each successful check), so this stays
|
||||
* correct after a browser clear once the onboarding-complete probe
|
||||
* runs. Sound calls that fire before that probe completes will fall
|
||||
* through silent on an already-onboarded node — which is exactly what
|
||||
* we want: ordinary re-logins stay quiet. */
|
||||
function isFirstInstallPhase(): boolean {
|
||||
if (cinematicMode) return true
|
||||
try {
|
||||
return localStorage.getItem('neode_onboarding_complete') !== '1'
|
||||
} catch {
|
||||
@@ -260,7 +273,10 @@ export function playKeyboardTypingSound() {
|
||||
*/
|
||||
export function playDashboardLoadOomph(force = false) {
|
||||
if (!force && !isFirstInstallPhase()) return
|
||||
const ctx = getContext()
|
||||
// The login→dashboard route change runs stopAllAudio(), which CLOSES the
|
||||
// AudioContext — so the context must be recreated here, not just fetched.
|
||||
// The login click's sticky user activation lets the fresh context run.
|
||||
const ctx = ensureContext()
|
||||
if (!ctx) return
|
||||
|
||||
try {
|
||||
|
||||
@@ -69,12 +69,22 @@ onMounted(() => {
|
||||
}, 2100)
|
||||
})
|
||||
|
||||
/** Any exit from the intro INTO login is the end of the cinematic — hand the
|
||||
* login the one-shot finale flag so the dashboard plays its full first-entry
|
||||
* reveal (zoom + oomph). OnboardingDone sets the same flag at the end of the
|
||||
* full wizard; these are the shortcut exits that used to skip it (which is
|
||||
* why the demo never got the big entrance). */
|
||||
function armOnboardingFinale() {
|
||||
try { sessionStorage.setItem('archy_onboarding_finale', '1') } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function goToOptions() {
|
||||
playNavSound('action')
|
||||
// Demo: skip the onboarding wizard (seed/identity setup) entirely — go straight
|
||||
// to login, which is prefilled with the demo password.
|
||||
if (isDemo) {
|
||||
localStorage.setItem('neode_onboarding_complete', '1')
|
||||
armOnboardingFinale()
|
||||
router.push('/login').catch(() => {})
|
||||
return
|
||||
}
|
||||
@@ -89,6 +99,7 @@ function goToRestore() {
|
||||
function goToLogin() {
|
||||
playNavSound('action')
|
||||
localStorage.setItem('neode_onboarding_complete', '1')
|
||||
armOnboardingFinale()
|
||||
router.push('/login').catch(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
@pause.prevent="handleVideoPause"
|
||||
@ended="handleVideoEnded"
|
||||
>
|
||||
<source src="/assets/video/video-intro.mp4?v=7" type="video/mp4">
|
||||
<source src="/assets/video/video-intro.mp4?v=8" type="video/mp4">
|
||||
</video>
|
||||
|
||||
<!-- Login: static background + archipelago-style glitch (no zoom) -->
|
||||
|
||||
@@ -374,10 +374,10 @@ init()
|
||||
<p>"Add Service" in the Tor panel now works for every app, not just a fixed list — the node reads the app's actual web port, so apps like Gitea, Jellyfin, Nextcloud, and Uptime Kuma no longer fail with "see server logs".</p>
|
||||
<p>Renaming your node now genuinely renames it everywhere: the machine's hostname, its .local network name (re-announced immediately), the local hosts file, and the HTTPS certificate all follow — so http and https links using your node's name keep working right after a rename.</p>
|
||||
<p>The node no longer mistakes a VPN tunnel for its own address. On fresh installs with NetBird, apps could launch on an internal 10.x address instead of your LAN IP; the node now reads its address from the actual network route, fixing app launch links, generated app configs, and VPN setup.</p>
|
||||
<p>Your cloud got a real layout: Apps-style tabs with categories for Folders, My Files, and Peer Files, readable file rows, and a search that also finds files shared by your federated peer nodes.</p>
|
||||
<p>The first-login dashboard entrance animation is back, and "Replay Intro" in Settings actually replays it.</p>
|
||||
<p>Your cloud got a real layout: Apps-style tabs with categories for Folders, My Files, and Peer Files, readable file rows, a search that also finds files shared by your federated peer nodes — and music now opens in the bottom-bar player instead of a broken preview window.</p>
|
||||
<p>The first-login experience flows again: the dashboard entrance animation is back — and you can actually hear it now (its sound was silently swallowed before, including on replays) — "Replay Intro" in Settings actually replays it, opening a direct link to an inner page no longer detours through the splash screen, the login screen keeps the intro video until your first login (switching to rotating backgrounds after), and the intro video streams three times lighter so it starts instantly.</p>
|
||||
<p>Changing DNS settings no longer blanks the page, and the DNS and WiFi dialogs now cover the whole app instead of only the right panel.</p>
|
||||
<p>The public demo is richer and truer: Ark wallet flows, working DNS and Tor service management, and a library of peer content with previews that never break.</p>
|
||||
<p>The public demo is richer and truer: the intro plays on every fresh visit, Ark wallet flows, working DNS and Tor service management, and a library of peer content with previews that never break.</p>
|
||||
<p>Assorted fixes: failed installs clean up after themselves properly, and the transactions view fits mobile screens (capped at 60% of the visible viewport).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+24
-24
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"changelog": [
|
||||
"Bitcoin now supports multiple versions of both Bitcoin Core and Bitcoin Knots: install the version you want, switch between them, pin a version, or let it auto-update — and switching is designed to be safe, with no surprise resyncs.",
|
||||
"Lightning grew up: your LND wallet's recovery seed is captured at setup and kept as an encrypted backup you can reveal from Settings, there's a new Channels tab with a fee control when opening channels, and on-chain and Lightning balances now show side by side.",
|
||||
"Installing Lightning (and other Bitcoin-dependent apps) on a fresh node no longer fails repeatedly — the node now waits until Bitcoin is genuinely ready to answer before starting them, and Bitcoin sizes its storage to your actual disk and its memory cache to your RAM, so small machines stop swapping and stalling.",
|
||||
"The wallet understands more money: Cashu v4 tokens are supported, you can pay for a peer's files from either your Cashu or Fedimint ecash, and the Transactions view now shows your Lightning, Cashu, and Fedimint activity together — with a payment confirmation screen and an automatic refund if a purchase fails.",
|
||||
"Mesh radios got a major upgrade: Meshtastic direct messages are now true end-to-end-encrypted radio messages that interoperate with off-the-shelf Meshtastic phone apps, your radio's region and a shared channel are provisioned automatically, and a new setup window appears when a radio is plugged in — with board pictures, full radio settings, and signal-strength indicators.",
|
||||
"Reticulum joins as a third mesh radio protocol with RNode LoRa hardware support, including sending images and voice messages over the radio — and every chat message now carries a small pill showing how it travelled (Mesh, FIPS, or Tor).",
|
||||
"Your node can manage an OpenWrt router: set up its internet uplink from the UI with a Wi-Fi network scan, turn it into a TollGate pay-for-Wi-Fi hotspot with a real captive portal, and sweep the router's earnings into your node's wallet. The gateway's status appears on the Home screen's Network tile.",
|
||||
"Peering is now trust-aware: \"Invite a Peer\" grants view-only Observer access while \"Link Your Nodes\" grants Trusted access, incoming requests ask for your confirmation with an optional message, Node Visibility is a single clear switch plus a list of discoverable nodes you can peer with, and the Fleet view shows your trusted nodes' health.",
|
||||
"Updates and apps are verified end-to-end: release updates are cryptographically signed and checked against a key baked into your node, app definitions arrive via the signed catalog, and container images are checked against trusted sources before anything installs or runs.",
|
||||
"Dozens of reliability fixes: failed installs no longer leave phantom app cards, uninstalling can't hang forever, apps you stopped stay stopped, crashed apps heal themselves (even \"running\" containers whose process actually died), the login page no longer refresh-loops, and the mobile layout fits real phone screens instead of hiding the last row behind the browser bar."
|
||||
"The wallet speaks Ark: a new Ark tab shows your Ark balance and history, you can send and receive over the Ark protocol, pay Lightning invoices from your Ark balance, and Ark payments appear in the transactions view with their own filter chip.",
|
||||
"Every app you install now automatically gets its own private .onion address — your apps are reachable over Tor a few seconds after install, with no manual \"Add Service\" step.",
|
||||
"\"Add Service\" in the Tor panel now works for every app, not just a fixed list — the node reads the app's actual web port, so apps like Gitea, Jellyfin, Nextcloud, and Uptime Kuma no longer fail with \"see server logs\".",
|
||||
"Renaming your node now genuinely renames it everywhere: the machine's hostname, its .local network name (re-announced immediately), the local hosts file, and the HTTPS certificate all follow — so http and https links using your node's name keep working right after a rename.",
|
||||
"The node no longer mistakes a VPN tunnel for its own address. On fresh installs with NetBird, apps could launch on an internal 10.x address instead of your LAN IP; the node now reads its address from the actual network route, fixing app launch links, generated app configs, and VPN setup.",
|
||||
"Your cloud got a real layout: Apps-style tabs with categories for Folders, My Files, and Peer Files, readable file rows, a search that also finds files shared by your federated peer nodes — and music now opens in the bottom-bar player instead of a broken preview window.",
|
||||
"The first-login experience flows again: the dashboard entrance animation is back — and you can actually hear it now (its sound was silently swallowed before, including on replays) — \"Replay Intro\" in Settings actually replays it, opening a direct link to an inner page no longer detours through the splash screen, the login screen keeps the intro video until your first login (switching to rotating backgrounds after), and the intro video streams three times lighter so it starts instantly.",
|
||||
"Changing DNS settings no longer blanks the page, and the DNS and WiFi dialogs now cover the whole app instead of only the right panel.",
|
||||
"The public demo is richer and truer: the intro plays on every fresh visit, Ark wallet flows, working DNS and Tor service management, and a library of peer content with previews that never break.",
|
||||
"Assorted fixes: failed installs clean up after themselves properly, and the transactions view fits mobile screens (capped at 60% of the visible viewport)."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.7.100-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.100-alpha/archipelago",
|
||||
"current_version": "1.7.101-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.101-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.7.100-alpha",
|
||||
"sha256": "9d317e8f308557af7f4f5928ccb39d452f61ff7eeff2024ef61067f72afc469a",
|
||||
"size_bytes": 49792496
|
||||
"new_version": "1.7.101-alpha",
|
||||
"sha256": "4e9d66f583a6b6119e381782bbc378bfc26e20dc710bfda7b57b6569b4efbb48",
|
||||
"size_bytes": 50100808
|
||||
},
|
||||
{
|
||||
"current_version": "1.7.100-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.100-alpha/archipelago-frontend-1.7.100-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.100-alpha.tar.gz",
|
||||
"new_version": "1.7.100-alpha",
|
||||
"sha256": "b5962bc779f9e238b842398b13ef7727570afb181fcecbc14a1645cb96bdfb77",
|
||||
"size_bytes": 173254632
|
||||
"current_version": "1.7.101-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.101-alpha/archipelago-frontend-1.7.101-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.101-alpha.tar.gz",
|
||||
"new_version": "1.7.101-alpha",
|
||||
"sha256": "67c19515f39d193089d3c73994b6d30571f12f840f1051bc2bdd61ff57b327d4",
|
||||
"size_bytes": 164830686
|
||||
}
|
||||
],
|
||||
"release_date": "2026-07-14",
|
||||
"signature": "646e24ffddfb7f3769ca81e9f8c788d021636b9715a3d285b97a561116ce9224fe9b61a441f9d379feca549862e89e0cd31cb8eed6fd14ad177c97781cf4d10f",
|
||||
"release_date": "2026-07-15",
|
||||
"signature": "9b70df9dcac2d83989dab6092a1fad3587a641d6db0484a515462009431cff68c3b1ea625280a4cd3dc8d9f145bab1602a3404e7efd71ad7ff2d7e91089f3701",
|
||||
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
|
||||
"version": "1.7.100-alpha"
|
||||
"version": "1.7.101-alpha"
|
||||
}
|
||||
|
||||
+24
-24
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"changelog": [
|
||||
"Bitcoin now supports multiple versions of both Bitcoin Core and Bitcoin Knots: install the version you want, switch between them, pin a version, or let it auto-update — and switching is designed to be safe, with no surprise resyncs.",
|
||||
"Lightning grew up: your LND wallet's recovery seed is captured at setup and kept as an encrypted backup you can reveal from Settings, there's a new Channels tab with a fee control when opening channels, and on-chain and Lightning balances now show side by side.",
|
||||
"Installing Lightning (and other Bitcoin-dependent apps) on a fresh node no longer fails repeatedly — the node now waits until Bitcoin is genuinely ready to answer before starting them, and Bitcoin sizes its storage to your actual disk and its memory cache to your RAM, so small machines stop swapping and stalling.",
|
||||
"The wallet understands more money: Cashu v4 tokens are supported, you can pay for a peer's files from either your Cashu or Fedimint ecash, and the Transactions view now shows your Lightning, Cashu, and Fedimint activity together — with a payment confirmation screen and an automatic refund if a purchase fails.",
|
||||
"Mesh radios got a major upgrade: Meshtastic direct messages are now true end-to-end-encrypted radio messages that interoperate with off-the-shelf Meshtastic phone apps, your radio's region and a shared channel are provisioned automatically, and a new setup window appears when a radio is plugged in — with board pictures, full radio settings, and signal-strength indicators.",
|
||||
"Reticulum joins as a third mesh radio protocol with RNode LoRa hardware support, including sending images and voice messages over the radio — and every chat message now carries a small pill showing how it travelled (Mesh, FIPS, or Tor).",
|
||||
"Your node can manage an OpenWrt router: set up its internet uplink from the UI with a Wi-Fi network scan, turn it into a TollGate pay-for-Wi-Fi hotspot with a real captive portal, and sweep the router's earnings into your node's wallet. The gateway's status appears on the Home screen's Network tile.",
|
||||
"Peering is now trust-aware: \"Invite a Peer\" grants view-only Observer access while \"Link Your Nodes\" grants Trusted access, incoming requests ask for your confirmation with an optional message, Node Visibility is a single clear switch plus a list of discoverable nodes you can peer with, and the Fleet view shows your trusted nodes' health.",
|
||||
"Updates and apps are verified end-to-end: release updates are cryptographically signed and checked against a key baked into your node, app definitions arrive via the signed catalog, and container images are checked against trusted sources before anything installs or runs.",
|
||||
"Dozens of reliability fixes: failed installs no longer leave phantom app cards, uninstalling can't hang forever, apps you stopped stay stopped, crashed apps heal themselves (even \"running\" containers whose process actually died), the login page no longer refresh-loops, and the mobile layout fits real phone screens instead of hiding the last row behind the browser bar."
|
||||
"The wallet speaks Ark: a new Ark tab shows your Ark balance and history, you can send and receive over the Ark protocol, pay Lightning invoices from your Ark balance, and Ark payments appear in the transactions view with their own filter chip.",
|
||||
"Every app you install now automatically gets its own private .onion address — your apps are reachable over Tor a few seconds after install, with no manual \"Add Service\" step.",
|
||||
"\"Add Service\" in the Tor panel now works for every app, not just a fixed list — the node reads the app's actual web port, so apps like Gitea, Jellyfin, Nextcloud, and Uptime Kuma no longer fail with \"see server logs\".",
|
||||
"Renaming your node now genuinely renames it everywhere: the machine's hostname, its .local network name (re-announced immediately), the local hosts file, and the HTTPS certificate all follow — so http and https links using your node's name keep working right after a rename.",
|
||||
"The node no longer mistakes a VPN tunnel for its own address. On fresh installs with NetBird, apps could launch on an internal 10.x address instead of your LAN IP; the node now reads its address from the actual network route, fixing app launch links, generated app configs, and VPN setup.",
|
||||
"Your cloud got a real layout: Apps-style tabs with categories for Folders, My Files, and Peer Files, readable file rows, a search that also finds files shared by your federated peer nodes — and music now opens in the bottom-bar player instead of a broken preview window.",
|
||||
"The first-login experience flows again: the dashboard entrance animation is back — and you can actually hear it now (its sound was silently swallowed before, including on replays) — \"Replay Intro\" in Settings actually replays it, opening a direct link to an inner page no longer detours through the splash screen, the login screen keeps the intro video until your first login (switching to rotating backgrounds after), and the intro video streams three times lighter so it starts instantly.",
|
||||
"Changing DNS settings no longer blanks the page, and the DNS and WiFi dialogs now cover the whole app instead of only the right panel.",
|
||||
"The public demo is richer and truer: the intro plays on every fresh visit, Ark wallet flows, working DNS and Tor service management, and a library of peer content with previews that never break.",
|
||||
"Assorted fixes: failed installs clean up after themselves properly, and the transactions view fits mobile screens (capped at 60% of the visible viewport)."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.7.100-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.100-alpha/archipelago",
|
||||
"current_version": "1.7.101-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.101-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.7.100-alpha",
|
||||
"sha256": "9d317e8f308557af7f4f5928ccb39d452f61ff7eeff2024ef61067f72afc469a",
|
||||
"size_bytes": 49792496
|
||||
"new_version": "1.7.101-alpha",
|
||||
"sha256": "4e9d66f583a6b6119e381782bbc378bfc26e20dc710bfda7b57b6569b4efbb48",
|
||||
"size_bytes": 50100808
|
||||
},
|
||||
{
|
||||
"current_version": "1.7.100-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.100-alpha/archipelago-frontend-1.7.100-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.100-alpha.tar.gz",
|
||||
"new_version": "1.7.100-alpha",
|
||||
"sha256": "b5962bc779f9e238b842398b13ef7727570afb181fcecbc14a1645cb96bdfb77",
|
||||
"size_bytes": 173254632
|
||||
"current_version": "1.7.101-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.101-alpha/archipelago-frontend-1.7.101-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.101-alpha.tar.gz",
|
||||
"new_version": "1.7.101-alpha",
|
||||
"sha256": "67c19515f39d193089d3c73994b6d30571f12f840f1051bc2bdd61ff57b327d4",
|
||||
"size_bytes": 164830686
|
||||
}
|
||||
],
|
||||
"release_date": "2026-07-14",
|
||||
"signature": "646e24ffddfb7f3769ca81e9f8c788d021636b9715a3d285b97a561116ce9224fe9b61a441f9d379feca549862e89e0cd31cb8eed6fd14ad177c97781cf4d10f",
|
||||
"release_date": "2026-07-15",
|
||||
"signature": "9b70df9dcac2d83989dab6092a1fad3587a641d6db0484a515462009431cff68c3b1ea625280a4cd3dc8d9f145bab1602a3404e7efd71ad7ff2d7e91089f3701",
|
||||
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
|
||||
"version": "1.7.100-alpha"
|
||||
"version": "1.7.101-alpha"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user