Merge remote-tracking branch 'origin/main' into ark-merge
This commit is contained in:
@@ -262,12 +262,12 @@ describe('useAppLauncherStore', () => {
|
||||
it('opens known prepackaged websites in new tab on desktop when requested', () => {
|
||||
const store = useAppLauncherStore()
|
||||
|
||||
store.open({ url: 'https://nwnn.l484.com', title: 'Next Web News Network', openInNewTab: true })
|
||||
store.open({ url: 'https://botfights.net', title: 'BotFights', openInNewTab: true })
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(store.panelAppId).toBe(null)
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'https://nwnn.l484.com',
|
||||
'https://botfights.net',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
@@ -281,12 +281,12 @@ describe('useAppLauncherStore', () => {
|
||||
})
|
||||
const store = useAppLauncherStore()
|
||||
|
||||
store.open({ url: 'https://present.l484.com', title: 'Arch Presentation', openInNewTab: true })
|
||||
store.open({ url: 'https://botfights.net', title: 'BotFights', openInNewTab: true })
|
||||
|
||||
// Iframeable prepackaged sites stay in-app via the store panel (no route
|
||||
// change, no background swap) just like every other mobile launch.
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(store.panelAppId).toBe('arch-presentation')
|
||||
expect(store.panelAppId).toBe('botfights')
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -328,12 +328,6 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
// Check external URLs
|
||||
const EXTERNAL_APP_HOSTS: Record<string, string> = {
|
||||
'botfights.net': 'botfights',
|
||||
'nwnn.l484.com': 'nwnn',
|
||||
'484.kitchen': '484-kitchen',
|
||||
'cta.tx1138.com': 'call-the-operator',
|
||||
'present.l484.com': 'arch-presentation',
|
||||
'syntropy.institute': 'syntropy-institute',
|
||||
'teeminuszero.net': 't-zero',
|
||||
'nostrudel.ninja': 'nostrudel',
|
||||
}
|
||||
return EXTERNAL_APP_HOSTS[u.hostname] || null
|
||||
|
||||
@@ -23,6 +23,31 @@ export interface MeshStatus {
|
||||
receive_block_headers?: boolean
|
||||
/** Operator-configured LoRa region (e.g. "EU_868"), for the Device tab. */
|
||||
region?: string | null
|
||||
/** Persisted config: LoRa region the driver programs on fresh radios. */
|
||||
lora_region?: string | null
|
||||
/** Persisted config: firmware pin (meshcore|meshtastic|reticulum), null = auto-detect. */
|
||||
device_kind?: string | null
|
||||
/** USB identity per detected port (for the setup modal's board image). */
|
||||
detected_device_info?: Array<{
|
||||
path: string
|
||||
vid?: string | null
|
||||
pid?: string | null
|
||||
product?: string | null
|
||||
manufacturer?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
/** Params accepted by mesh.configure (superset of the status fields). */
|
||||
export interface MeshConfigureParams {
|
||||
enabled?: boolean
|
||||
device_path?: string
|
||||
channel_name?: string
|
||||
broadcast_identity?: boolean
|
||||
advert_name?: string
|
||||
announce_block_headers?: boolean
|
||||
receive_block_headers?: boolean
|
||||
lora_region?: string
|
||||
device_kind?: string
|
||||
}
|
||||
|
||||
export interface MeshPeer {
|
||||
@@ -234,6 +259,38 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Global device-detection (for the app-wide "device detected" modal) ──
|
||||
// Ports the user dismissed the setup modal for, persisted per browser.
|
||||
const DETECT_DISMISS_KEY = 'archipelago.mesh.detect-dismissed.v1'
|
||||
const dismissedDetectedPaths = ref<Set<string>>(new Set(
|
||||
JSON.parse(localStorage.getItem(DETECT_DISMISS_KEY) || '[]') as string[]
|
||||
))
|
||||
const undismissedDetectedDevices = computed(() => {
|
||||
const s = status.value
|
||||
if (!s || s.device_connected || s.enabled) return []
|
||||
return (s.detected_devices || []).filter(p => !dismissedDetectedPaths.value.has(p))
|
||||
})
|
||||
function dismissDetectedDevice(path: string) {
|
||||
dismissedDetectedPaths.value.add(path)
|
||||
dismissedDetectedPaths.value = new Set(dismissedDetectedPaths.value)
|
||||
localStorage.setItem(DETECT_DISMISS_KEY, JSON.stringify([...dismissedDetectedPaths.value]))
|
||||
}
|
||||
let globalDetectTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** App-wide light poll so the detected-device modal works on every page
|
||||
* (the Mesh view's own 5s poll takes over while it is mounted). */
|
||||
function startGlobalDetection(intervalMs = 15000) {
|
||||
if (globalDetectTimer) return
|
||||
// Never poll unauthenticated — a 401 from a background poll on the
|
||||
// login page caused a login-refresh loop (2026-07-14, .116).
|
||||
const authed = () => {
|
||||
try { return !!localStorage.getItem('neode-auth') } catch { return false }
|
||||
}
|
||||
if (authed()) void fetchStatus()
|
||||
globalDetectTimer = setInterval(() => {
|
||||
if (document.visibilityState === 'visible' && authed()) void fetchStatus()
|
||||
}, intervalMs)
|
||||
}
|
||||
|
||||
async function fetchPeers() {
|
||||
try {
|
||||
const res = await rpcClient.call<{ peers: MeshPeer[]; count: number }>({
|
||||
@@ -455,12 +512,12 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function configure(config: Partial<MeshStatus>) {
|
||||
async function configure(config: MeshConfigureParams) {
|
||||
try {
|
||||
error.value = null
|
||||
await rpcClient.call<{ configured: boolean }>({
|
||||
method: 'mesh.configure',
|
||||
params: config,
|
||||
params: config as Record<string, unknown>,
|
||||
})
|
||||
await fetchStatus()
|
||||
} catch (err: unknown) {
|
||||
@@ -851,6 +908,9 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
blockHeaders,
|
||||
latestBlockHeight,
|
||||
fetchStatus,
|
||||
undismissedDetectedDevices,
|
||||
dismissDetectedDevice,
|
||||
startGlobalDetection,
|
||||
fetchPeers,
|
||||
fetchMessages,
|
||||
sendMessage,
|
||||
|
||||
Reference in New Issue
Block a user