refactor: update dependencies and remove unused code

- Added new dependencies: `adler2`, `crc32fast`, `flate2`, `miniz_oxide`, and `libredox`.
- Updated existing dependencies: `tokio-rustls` to version 0.26.4 and `filetime` to version 0.2.27.
- Removed the `backup.rs` file as it is no longer needed.
- Introduced tests for configuration and credential management.
- Enhanced the `identity` module to generate W3C compliant DID documents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-12 00:19:30 +00:00
parent 2a867b32a8
commit 6fee6befed
347 changed files with 18703 additions and 46785 deletions

View File

@@ -0,0 +1,138 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useAudioPlayer } from '../useAudioPlayer'
// Mock HTMLAudioElement
class MockAudio {
src = ''
currentTime = 0
duration = 120
paused = true
private listeners: Record<string, Array<() => void>> = {}
addEventListener(event: string, handler: () => void) {
if (!this.listeners[event]) this.listeners[event] = []
this.listeners[event].push(handler)
}
removeEventListener() {
// no-op for tests
}
play() {
this.paused = false
this.emit('play')
return Promise.resolve()
}
pause() {
this.paused = true
this.emit('pause')
}
private emit(event: string) {
const handlers = this.listeners[event] || []
handlers.forEach(h => h())
}
// Helper to simulate events in tests
simulateEvent(event: string) {
this.emit(event)
}
}
vi.stubGlobal('Audio', MockAudio)
describe('useAudioPlayer', () => {
beforeEach(() => {
// Reset singleton state by stopping any active playback
const player = useAudioPlayer()
player.stop()
})
it('returns all expected properties', () => {
const player = useAudioPlayer()
expect(player.play).toBeTypeOf('function')
expect(player.pause).toBeTypeOf('function')
expect(player.seek).toBeTypeOf('function')
expect(player.stop).toBeTypeOf('function')
expect(player.playing).toBeDefined()
expect(player.currentName).toBeDefined()
expect(player.currentTime).toBeDefined()
expect(player.duration).toBeDefined()
expect(player.progress).toBeDefined()
expect(player.currentSrc).toBeDefined()
expect(player.error).toBeDefined()
})
it('starts in stopped state', () => {
const player = useAudioPlayer()
expect(player.playing.value).toBe(false)
expect(player.currentSrc.value).toBeNull()
expect(player.currentName.value).toBe('')
})
it('play sets playing state and current source', () => {
const player = useAudioPlayer()
player.play('/audio/test.mp3', 'Test Track')
expect(player.playing.value).toBe(true)
expect(player.currentSrc.value).toBe('/audio/test.mp3')
expect(player.currentName.value).toBe('Test Track')
})
it('play toggles pause when same source is playing', () => {
const player = useAudioPlayer()
player.play('/audio/test.mp3', 'Test')
expect(player.playing.value).toBe(true)
// Play same source again — should pause
player.play('/audio/test.mp3', 'Test')
expect(player.playing.value).toBe(false)
})
it('play switches to new source', () => {
const player = useAudioPlayer()
player.play('/audio/first.mp3', 'First')
player.play('/audio/second.mp3', 'Second')
expect(player.currentSrc.value).toBe('/audio/second.mp3')
expect(player.currentName.value).toBe('Second')
})
it('pause pauses playback', () => {
const player = useAudioPlayer()
player.play('/audio/test.mp3', 'Test')
player.pause()
expect(player.playing.value).toBe(false)
})
it('stop resets all state', () => {
const player = useAudioPlayer()
player.play('/audio/test.mp3', 'Test')
player.stop()
expect(player.playing.value).toBe(false)
expect(player.currentSrc.value).toBeNull()
expect(player.currentName.value).toBe('')
})
it('progress computes correctly', () => {
const player = useAudioPlayer()
expect(player.progress.value).toBe(0) // duration is 0
player.currentTime.value = 30
player.duration.value = 120
expect(player.progress.value).toBe(25) // 30/120 * 100
})
it('progress is 0 when duration is 0', () => {
const player = useAudioPlayer()
player.duration.value = 0
player.currentTime.value = 10
expect(player.progress.value).toBe(0)
})
it('shared state across multiple useAudioPlayer calls', () => {
const p1 = useAudioPlayer()
const p2 = useAudioPlayer()
p1.play('/audio/shared.mp3', 'Shared')
expect(p2.currentSrc.value).toBe('/audio/shared.mp3')
expect(p2.playing.value).toBe(true)
})
})

View File

@@ -0,0 +1,202 @@
import { describe, it, expect } from 'vitest'
import { ref } from 'vue'
import { getFileCategory, useFileType, formatSize, formatDate } from '../useFileType'
describe('getFileCategory', () => {
it('returns folder for directories', () => {
expect(getFileCategory('', true)).toBe('folder')
expect(getFileCategory('jpg', true)).toBe('folder')
})
it('identifies image extensions', () => {
expect(getFileCategory('jpg', false)).toBe('image')
expect(getFileCategory('jpeg', false)).toBe('image')
expect(getFileCategory('png', false)).toBe('image')
expect(getFileCategory('gif', false)).toBe('image')
expect(getFileCategory('webp', false)).toBe('image')
expect(getFileCategory('svg', false)).toBe('image')
expect(getFileCategory('bmp', false)).toBe('image')
expect(getFileCategory('ico', false)).toBe('image')
})
it('identifies audio extensions', () => {
expect(getFileCategory('mp3', false)).toBe('audio')
expect(getFileCategory('flac', false)).toBe('audio')
expect(getFileCategory('wav', false)).toBe('audio')
expect(getFileCategory('ogg', false)).toBe('audio')
expect(getFileCategory('aac', false)).toBe('audio')
expect(getFileCategory('m4a', false)).toBe('audio')
})
it('identifies video extensions', () => {
expect(getFileCategory('mp4', false)).toBe('video')
expect(getFileCategory('mkv', false)).toBe('video')
expect(getFileCategory('avi', false)).toBe('video')
expect(getFileCategory('mov', false)).toBe('video')
expect(getFileCategory('webm', false)).toBe('video')
})
it('identifies document extensions', () => {
expect(getFileCategory('pdf', false)).toBe('document')
expect(getFileCategory('doc', false)).toBe('document')
expect(getFileCategory('docx', false)).toBe('document')
expect(getFileCategory('txt', false)).toBe('document')
expect(getFileCategory('md', false)).toBe('document')
})
it('identifies spreadsheet extensions', () => {
expect(getFileCategory('xls', false)).toBe('spreadsheet')
expect(getFileCategory('xlsx', false)).toBe('spreadsheet')
expect(getFileCategory('csv', false)).toBe('spreadsheet')
expect(getFileCategory('ods', false)).toBe('spreadsheet')
})
it('identifies archive extensions', () => {
expect(getFileCategory('zip', false)).toBe('archive')
expect(getFileCategory('tar', false)).toBe('archive')
expect(getFileCategory('gz', false)).toBe('archive')
expect(getFileCategory('rar', false)).toBe('archive')
expect(getFileCategory('7z', false)).toBe('archive')
})
it('returns file for unknown extensions', () => {
expect(getFileCategory('xyz', false)).toBe('file')
expect(getFileCategory('', false)).toBe('file')
expect(getFileCategory('bin', false)).toBe('file')
})
})
describe('useFileType', () => {
it('returns correct category and computed values for an image', () => {
const ext = ref('jpg')
const isDir = ref(false)
const result = useFileType(ext, isDir)
expect(result.category.value).toBe('image')
expect(result.isImage.value).toBe(true)
expect(result.isAudio.value).toBe(false)
expect(result.isVideo.value).toBe(false)
expect(result.iconColor.value).toBe('text-blue-400')
expect(result.badgeLabel.value).toBe('Image')
})
it('returns correct values for audio', () => {
const ext = ref('mp3')
const isDir = ref(false)
const result = useFileType(ext, isDir)
expect(result.category.value).toBe('audio')
expect(result.isAudio.value).toBe(true)
expect(result.isImage.value).toBe(false)
expect(result.iconColor.value).toBe('text-orange-400')
expect(result.badgeLabel.value).toBe('Audio')
})
it('returns correct values for video', () => {
const ext = ref('mp4')
const isDir = ref(false)
const result = useFileType(ext, isDir)
expect(result.category.value).toBe('video')
expect(result.isVideo.value).toBe(true)
expect(result.iconColor.value).toBe('text-purple-400')
})
it('returns folder when isDir is true', () => {
const ext = ref('jpg')
const isDir = ref(true)
const result = useFileType(ext, isDir)
expect(result.category.value).toBe('folder')
expect(result.isImage.value).toBe(false)
expect(result.iconColor.value).toBe('text-amber-400')
expect(result.badgeLabel.value).toBe('Folder')
})
it('reacts to ref changes', () => {
const ext = ref('jpg')
const isDir = ref(false)
const result = useFileType(ext, isDir)
expect(result.category.value).toBe('image')
ext.value = 'mp3'
expect(result.category.value).toBe('audio')
expect(result.isAudio.value).toBe(true)
expect(result.isImage.value).toBe(false)
})
it('provides icon paths for each category', () => {
const ext = ref('pdf')
const isDir = ref(false)
const result = useFileType(ext, isDir)
expect(result.iconPaths.value).toBeDefined()
expect(result.iconPaths.value.length).toBeGreaterThan(0)
})
it('provides badge class for each category', () => {
const ext = ref('zip')
const isDir = ref(false)
const result = useFileType(ext, isDir)
expect(result.badgeClass.value).toContain('bg-yellow')
})
})
describe('formatSize', () => {
it('formats 0 bytes', () => {
expect(formatSize(0)).toBe('0 B')
})
it('formats bytes', () => {
expect(formatSize(500)).toBe('500 B')
})
it('formats kilobytes', () => {
expect(formatSize(1024)).toBe('1.0 KB')
expect(formatSize(1536)).toBe('1.5 KB')
})
it('formats megabytes', () => {
expect(formatSize(1048576)).toBe('1.0 MB')
})
it('formats gigabytes', () => {
expect(formatSize(1073741824)).toBe('1.0 GB')
})
it('formats terabytes', () => {
expect(formatSize(1099511627776)).toBe('1.0 TB')
})
})
describe('formatDate', () => {
it('returns "Just now" for very recent dates', () => {
const now = new Date().toISOString()
expect(formatDate(now)).toBe('Just now')
})
it('returns minutes ago for recent dates', () => {
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString()
expect(formatDate(fiveMinAgo)).toBe('5m ago')
})
it('returns hours ago for dates within 24h', () => {
const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString()
expect(formatDate(threeHoursAgo)).toBe('3h ago')
})
it('returns days ago for dates within a week', () => {
const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
expect(formatDate(twoDaysAgo)).toBe('2d ago')
})
it('returns formatted date for older dates', () => {
const oldDate = new Date('2025-01-15').toISOString()
const result = formatDate(oldDate)
// Should be a locale date string, not a relative time
expect(result).toMatch(/\d/)
expect(result).not.toContain('ago')
})
})

View File

@@ -0,0 +1,97 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { useMarketplaceApp } from '../useMarketplaceApp'
describe('useMarketplaceApp', () => {
beforeEach(() => {
const { clearCurrentApp } = useMarketplaceApp()
clearCurrentApp()
})
it('getCurrentApp returns null initially', () => {
const { getCurrentApp } = useMarketplaceApp()
expect(getCurrentApp()).toBeNull()
})
it('setCurrentApp stores a full app', () => {
const { setCurrentApp, getCurrentApp } = useMarketplaceApp()
setCurrentApp({
id: 'bitcoin',
title: 'Bitcoin Core',
version: '25.0',
icon: '/icons/btc.png',
category: 'Finance',
description: 'Bitcoin node',
author: 'Satoshi',
source: 'github',
manifestUrl: 'https://example.com/manifest',
url: 'https://example.com',
repoUrl: 'https://github.com/bitcoin/bitcoin',
s9pkUrl: '',
dockerImage: 'bitcoin:25.0',
})
const app = getCurrentApp()
expect(app).not.toBeNull()
expect(app!.id).toBe('bitcoin')
expect(app!.title).toBe('Bitcoin Core')
expect(app!.version).toBe('25.0')
})
it('setCurrentApp with partial app fills defaults', () => {
const { setCurrentApp, getCurrentApp } = useMarketplaceApp()
setCurrentApp({ id: 'lnd' })
const app = getCurrentApp()
expect(app).not.toBeNull()
expect(app!.id).toBe('lnd')
expect(app!.title).toBe('')
expect(app!.version).toBe('')
expect(app!.icon).toBe('')
expect(app!.dockerImage).toBe('')
})
it('manifestUrl falls back to s9pkUrl then url', () => {
const { setCurrentApp, getCurrentApp } = useMarketplaceApp()
setCurrentApp({ id: 'test', s9pkUrl: 'https://s9pk.example.com/app.s9pk' })
const app = getCurrentApp()
expect(app!.manifestUrl).toBe('https://s9pk.example.com/app.s9pk')
expect(app!.url).toBe('https://s9pk.example.com/app.s9pk')
})
it('url falls back to s9pkUrl then manifestUrl', () => {
const { setCurrentApp, getCurrentApp } = useMarketplaceApp()
setCurrentApp({ id: 'test', manifestUrl: 'https://manifest.example.com' })
const app = getCurrentApp()
expect(app!.url).toBe('https://manifest.example.com')
})
it('clearCurrentApp sets app to null', () => {
const { setCurrentApp, clearCurrentApp, getCurrentApp } = useMarketplaceApp()
setCurrentApp({ id: 'bitcoin' })
expect(getCurrentApp()).not.toBeNull()
clearCurrentApp()
expect(getCurrentApp()).toBeNull()
})
it('shared state across multiple useMarketplaceApp calls', () => {
const instance1 = useMarketplaceApp()
const instance2 = useMarketplaceApp()
instance1.setCurrentApp({ id: 'mempool', title: 'Mempool' })
const app = instance2.getCurrentApp()
expect(app!.id).toBe('mempool')
expect(app!.title).toBe('Mempool')
})
it('handles description as object', () => {
const { setCurrentApp, getCurrentApp } = useMarketplaceApp()
setCurrentApp({
id: 'test',
description: { short: 'Short desc', long: 'Long description' },
})
const app = getCurrentApp()
expect(app!.description).toEqual({ short: 'Short desc', long: 'Long description' })
})
})

View File

@@ -0,0 +1,178 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
const mockPush = vi.fn()
vi.mock('vue-router', () => ({
useRouter: () => ({ push: mockPush }),
}))
vi.mock('@/api/rpc-client', () => ({
rpcClient: {
getReceivedMessages: vi.fn(),
},
}))
import { useMessageToast } from '../useMessageToast'
import { rpcClient } from '@/api/rpc-client'
const mockedRpc = vi.mocked(rpcClient)
describe('useMessageToast', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
// Reset shared singleton state
const toast = useMessageToast()
toast.stopPolling()
toast.receivedMessages.value = []
toast.lastMessageCount.value = 0
toast.loadingMessages.value = false
toast.toastMessage.value = { show: false, text: '' }
})
afterEach(() => {
const toast = useMessageToast()
toast.stopPolling()
vi.useRealTimers()
})
it('starts with empty state', () => {
const toast = useMessageToast()
expect(toast.receivedMessages.value).toEqual([])
expect(toast.lastMessageCount.value).toBe(0)
expect(toast.loadingMessages.value).toBe(false)
expect(toast.toastMessage.value.show).toBe(false)
expect(toast.unreadCount.value).toBe(0)
})
it('loadReceivedMessages fetches and stores messages', async () => {
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [
{ from_pubkey: 'abc', message: 'Hello', timestamp: '2026-01-01' },
],
})
const toast = useMessageToast()
await toast.loadReceivedMessages()
expect(toast.receivedMessages.value.length).toBe(1)
expect(toast.lastMessageCount.value).toBe(1)
expect(toast.loadingMessages.value).toBe(false)
})
it('does not show toast on initial load', async () => {
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [{ from_pubkey: 'a', message: 'Hi', timestamp: '2026-01-01' }],
})
const toast = useMessageToast()
await toast.loadReceivedMessages()
expect(toast.toastMessage.value.show).toBe(false)
})
it('shows toast when new messages arrive after initial load', async () => {
const toast = useMessageToast()
// Initial load
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [{ from_pubkey: 'a', message: 'First', timestamp: '2026-01-01' }],
})
await toast.loadReceivedMessages()
// New message arrives
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [
{ from_pubkey: 'a', message: 'First', timestamp: '2026-01-01' },
{ from_pubkey: 'b', message: 'Second', timestamp: '2026-01-02' },
],
})
await toast.loadReceivedMessages()
expect(toast.toastMessage.value.show).toBe(true)
expect(toast.toastMessage.value.text).toBe('Second')
})
it('shows count for multiple new messages', async () => {
const toast = useMessageToast()
// Initial load
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [{ from_pubkey: 'a', message: 'One', timestamp: '2026-01-01' }],
})
await toast.loadReceivedMessages()
// Multiple new messages
mockedRpc.getReceivedMessages.mockResolvedValue({
messages: [
{ from_pubkey: 'a', message: 'One', timestamp: '2026-01-01' },
{ from_pubkey: 'b', message: 'Two', timestamp: '2026-01-02' },
{ from_pubkey: 'c', message: 'Three', timestamp: '2026-01-03' },
],
})
await toast.loadReceivedMessages()
expect(toast.toastMessage.value.show).toBe(true)
expect(toast.toastMessage.value.text).toBe('2 new messages')
})
it('unreadCount reflects difference', async () => {
const toast = useMessageToast()
toast.receivedMessages.value = [
{ from_pubkey: 'a', message: 'Hi', timestamp: '2026-01-01' },
{ from_pubkey: 'b', message: 'Hey', timestamp: '2026-01-02' },
]
toast.lastMessageCount.value = 1
expect(toast.unreadCount.value).toBe(1)
})
it('unreadCount is never negative', () => {
const toast = useMessageToast()
toast.receivedMessages.value = []
toast.lastMessageCount.value = 5
expect(toast.unreadCount.value).toBe(0)
})
it('markAsRead syncs lastMessageCount', () => {
const toast = useMessageToast()
toast.receivedMessages.value = [
{ from_pubkey: 'a', message: 'Hi', timestamp: '2026-01-01' },
{ from_pubkey: 'b', message: 'Hey', timestamp: '2026-01-02' },
]
toast.lastMessageCount.value = 0
toast.markAsRead()
expect(toast.lastMessageCount.value).toBe(2)
expect(toast.unreadCount.value).toBe(0)
})
it('dismissToastAndOpenMessages clears toast and navigates', () => {
const toast = useMessageToast()
toast.toastMessage.value = { show: true, text: 'New message' }
toast.dismissToastAndOpenMessages()
expect(toast.toastMessage.value.show).toBe(false)
expect(mockPush).toHaveBeenCalledWith({ path: '/dashboard/web5', query: { tab: 'messages' } })
})
it('stops polling on 401 error', async () => {
const toast = useMessageToast()
mockedRpc.getReceivedMessages.mockRejectedValue(new Error('401 Unauthorized'))
toast.startPolling()
// Wait for initial load triggered by startPolling
await vi.advanceTimersByTimeAsync(0)
// Polling should have stopped, so advancing time should NOT call again
vi.clearAllMocks()
await vi.advanceTimersByTimeAsync(60000)
expect(mockedRpc.getReceivedMessages).not.toHaveBeenCalled()
})
it('startPolling does not create duplicate timers', () => {
const toast = useMessageToast()
mockedRpc.getReceivedMessages.mockResolvedValue({ messages: [] })
toast.startPolling()
toast.startPolling()
toast.startPolling()
// Should only have one timer — verify by stopping and checking no more calls
toast.stopPolling()
})
})

View File

@@ -0,0 +1,131 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { useToast } from '../useToast'
describe('useToast', () => {
beforeEach(() => {
vi.useFakeTimers()
// Get a fresh toast instance and clear any leftover state
const { toasts, dismiss } = useToast()
// Dismiss all existing toasts
for (const t of [...toasts.value]) {
dismiss(t.id)
}
vi.advanceTimersByTime(500)
})
afterEach(() => {
vi.useRealTimers()
})
it('creates a success toast', () => {
const { success, toasts } = useToast()
success('Operation complete')
expect(toasts.value.length).toBeGreaterThanOrEqual(1)
const toast = toasts.value[toasts.value.length - 1]!
expect(toast.message).toBe('Operation complete')
expect(toast.variant).toBe('success')
expect(toast.dismissing).toBe(false)
})
it('creates an error toast', () => {
const { error, toasts } = useToast()
error('Something went wrong')
const toast = toasts.value[toasts.value.length - 1]!
expect(toast.message).toBe('Something went wrong')
expect(toast.variant).toBe('error')
})
it('creates an info toast', () => {
const { info, toasts } = useToast()
info('FYI: Node syncing')
const toast = toasts.value[toasts.value.length - 1]!
expect(toast.message).toBe('FYI: Node syncing')
expect(toast.variant).toBe('info')
})
it('auto-dismisses toast after duration', () => {
const { success, toasts } = useToast()
success('Will auto-dismiss')
const toast = toasts.value[toasts.value.length - 1]!
const toastId = toast.id
expect(toasts.value.some((t) => t.id === toastId)).toBe(true)
// After 3000ms, the toast should start dismissing
vi.advanceTimersByTime(3000)
const dismissingToast = toasts.value.find((t) => t.id === toastId)
if (dismissingToast) {
expect(dismissingToast.dismissing).toBe(true)
}
// After another 300ms, the toast should be fully removed
vi.advanceTimersByTime(300)
expect(toasts.value.some((t) => t.id === toastId)).toBe(false)
})
it('dismiss marks toast as dismissing then removes it', () => {
const { info, toasts, dismiss } = useToast()
info('Dismissable')
const toast = toasts.value[toasts.value.length - 1]!
dismiss(toast.id)
// Should be marked as dismissing
const found = toasts.value.find((t) => t.id === toast.id)
if (found) {
expect(found.dismissing).toBe(true)
}
// After 300ms animation delay, should be removed
vi.advanceTimersByTime(300)
expect(toasts.value.some((t) => t.id === toast.id)).toBe(false)
})
it('dismiss is a no-op for nonexistent toast ID', () => {
const { dismiss, toasts } = useToast()
const countBefore = toasts.value.length
dismiss(999999)
expect(toasts.value.length).toBe(countBefore)
})
it('each toast gets a unique ID', () => {
const { info, toasts } = useToast()
info('First')
info('Second')
info('Third')
const ids = toasts.value.slice(-3).map((t) => t.id)
const uniqueIds = new Set(ids)
expect(uniqueIds.size).toBe(3)
})
it('caps visible toasts at 5', () => {
const { info, toasts } = useToast()
for (let i = 0; i < 7; i++) {
info(`Toast ${i}`)
}
expect(toasts.value.length).toBeLessThanOrEqual(5)
})
it('toasts ref is readonly', () => {
const { toasts } = useToast()
// The readonly wrapper prevents direct mutation
expect(typeof toasts.value).toBe('object')
})
})

View File

@@ -93,7 +93,7 @@ export function stopSynthwave() {
if (introAudio) {
if (introGain && audioContext) {
const t = audioContext.currentTime
introGain.gain.setValueAtTime(1, t)
introGain.gain.setValueAtTime(introGain.gain.value, t)
introGain.gain.linearRampToValueAtTime(0.001, t + 0.2)
}
setTimeout(() => {
@@ -104,6 +104,23 @@ export function stopSynthwave() {
}
}
/** Stop ALL login audio and close AudioContext. Call on route change to dashboard. */
export function stopAllAudio() {
// Stop synthwave loop
if (introAudio) {
introAudio.pause()
introAudio = null
introGain = null
}
// Stop intro typing
stopIntroTyping()
// Close AudioContext to kill any lingering BufferSource nodes (playLoopStart)
if (audioContext) {
audioContext.close().catch(() => {})
audioContext = null
}
}
/** Pop sound - plays when intro initiator (tap to start) is pressed */
export function playPop() {
const audio = new Audio('/assets/audio/pop.mp3')

View File

@@ -14,6 +14,8 @@ export interface MarketplaceAppInfo {
repoUrl: string
s9pkUrl: string
dockerImage: string
/** External web URL for iframe-based web apps (no container needed) */
webUrl?: string
}
// Simple in-memory store for the current marketplace app
@@ -36,6 +38,7 @@ export function useMarketplaceApp() {
repoUrl: app.repoUrl ?? '',
s9pkUrl: app.s9pkUrl ?? '',
dockerImage: app.dockerImage ?? '',
webUrl: (app as Record<string, unknown>).webUrl as string | undefined,
}
}

View File

@@ -48,7 +48,7 @@ export function useMessageToast() {
stopPolling()
return
}
console.error('Failed to load messages:', e)
if (import.meta.env.DEV) console.error('Failed to load messages:', e)
} finally {
loadingMessages.value = false
}