Compare commits
20
Commits
v1.8.2-alpha
..
main
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.8.3-alpha (2026-08-14)
|
||||||
|
|
||||||
|
- **The network map on TVs: no more blank page, no more frozen page — and it moves again.** The map's entrance animation needed a smoothness that TV kiosk hardware can't always deliver, so the page could sit blank until a refresh; the previous fix cured the freeze by stopping the animation entirely, which went too far. Now the map appears instantly with everything already in place, then resumes its calm orbital motion at a gentler pace suited to TVs. Resizing or rotating any screen also redraws the map properly instead of leaving it tiny, stretched, or empty.
|
||||||
|
- **The dashboard's corner logo is back to normal.** The new glossy paint finish was meant for the big emblem on the screensaver, intro, and login screens — it had quietly spread to the small logo in the dashboard header, where it looked wrong. Each screen now gets exactly the treatment intended for it.
|
||||||
|
- **App icons no longer vanish in My Apps.** The freshly restyled Alby Hub and phoenixd icons could render as blank squares in some views — a subtlety in how the icon files declared their size. Fixed at the source, and the icon tool app developers use now produces immune files.
|
||||||
|
|
||||||
## v1.8.2-alpha (2026-08-13)
|
## v1.8.2-alpha (2026-08-13)
|
||||||
|
|
||||||
- **An app that can't be shown inside the dashboard now becomes a tab app by itself.** A few apps refuse to render inside another page no matter what — they break out with their own code or insist on owning the whole browser window. Opening one used to mean staring at a grey pane. Now the dashboard notices, offers the app in its own tab, and remembers: from then on that app's button opens a tab directly (with the little launch icon that tab apps carry), first click, every time. If a later update makes the app embeddable after all, the dashboard notices that too and goes back to embedding it.
|
- **An app that can't be shown inside the dashboard now becomes a tab app by itself.** A few apps refuse to render inside another page no matter what — they break out with their own code or insist on owning the whole browser window. Opening one used to mean staring at a grey pane. Now the dashboard notices, offers the app in its own tab, and remembers: from then on that app's button opens a tab directly (with the little launch icon that tab apps carry), first click, every time. If a later update makes the app embeddable after all, the dashboard notices that too and goes back to embedding it.
|
||||||
|
|||||||
@@ -93,10 +93,11 @@ describe('useAI', () => {
|
|||||||
expect(activeModel.value).toBe('echo')
|
expect(activeModel.value).toBe('echo')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('lists available providers with models', () => {
|
it('lists available providers with models, Routstr first', () => {
|
||||||
const { availableProviders } = useAI()
|
const { availableProviders } = useAI()
|
||||||
expect(availableProviders.value.length).toBe(3)
|
expect(availableProviders.value.length).toBe(4)
|
||||||
const ids = availableProviders.value.map(p => p.id)
|
const ids = availableProviders.value.map(p => p.id)
|
||||||
|
expect(ids[0]).toBe('routstr')
|
||||||
expect(ids).toContain('claude')
|
expect(ids).toContain('claude')
|
||||||
expect(ids).toContain('openrouter')
|
expect(ids).toContain('openrouter')
|
||||||
expect(ids).toContain('mock')
|
expect(ids).toContain('mock')
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
<Transition name="picker">
|
<Transition name="picker">
|
||||||
<div
|
<div
|
||||||
v-if="showModelPicker"
|
v-if="showModelPicker"
|
||||||
class="fixed z-[9999] path-glass-card header-overlay-panel p-3 space-y-3 animate-fade-up-fast shadow-2xl min-w-[220px]"
|
class="fixed z-[9999] path-glass-card header-overlay-panel p-3 space-y-3 animate-fade-up-fast shadow-2xl min-w-[220px] max-h-[70vh] overflow-y-auto"
|
||||||
:style="modelPickerDropdownStyle"
|
:style="modelPickerDropdownStyle"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@@ -332,7 +332,7 @@ const modelDisplayName = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function selectModel(providerId: string, modelId: string) {
|
function selectModel(providerId: string, modelId: string) {
|
||||||
setProvider(providerId as 'claude' | 'openrouter' | 'mock')
|
setProvider(providerId as 'routstr' | 'claude' | 'openrouter' | 'mock')
|
||||||
setModel(modelId)
|
setModel(modelId)
|
||||||
showModelPicker.value = false
|
showModelPicker.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import { useCodeContext } from '@/composables/useCodeContext'
|
|||||||
import { apiFetch } from '@/utils/api-fetch'
|
import { apiFetch } from '@/utils/api-fetch'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
type Provider = 'claude' | 'openrouter' | 'mock'
|
type Provider = 'routstr' | 'claude' | 'openrouter' | 'mock'
|
||||||
|
|
||||||
// API paths are relative to the base URL so they work both in dev (/) and Archy (/aiui/)
|
// API paths are relative to the base URL so they work both in dev (/) and Archy (/aiui/)
|
||||||
const BASE = import.meta.env.BASE_URL || '/'
|
const BASE = import.meta.env.BASE_URL || '/'
|
||||||
const CLAUDE_PATH = `${BASE}api/claude/v1/messages`
|
const CLAUDE_PATH = `${BASE}api/claude/v1/messages`
|
||||||
const OPENROUTER_PATH = `${BASE}api/openrouter`
|
const OPENROUTER_PATH = `${BASE}api/openrouter`
|
||||||
|
const ROUTSTR_MODELS_PATH = `${BASE}api/routstr/models`
|
||||||
|
const ROUTSTR_CHAT_PATH = `${BASE}api/routstr/chat/completions`
|
||||||
|
|
||||||
import { mockFilms } from '@/mocks/films'
|
import { mockFilms } from '@/mocks/films'
|
||||||
import { mockSongs } from '@/mocks/songs'
|
import { mockSongs } from '@/mocks/songs'
|
||||||
@@ -148,8 +150,41 @@ function looksLikeMissingApiKey(err: string): boolean {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Routstr model catalog (fetched from the node's session-gated proxy) ───
|
||||||
|
// The node forwards the live Routstr aggregator's /v1/models; entries carry
|
||||||
|
// sats_pricing so completions are Cashu-paid against the operator's budget.
|
||||||
|
const routstrModels = ref<{ id: string; name: string }[]>([])
|
||||||
|
let routstrModelsFetched = false
|
||||||
|
|
||||||
|
async function refreshRoutstrModels() {
|
||||||
|
if (routstrModelsFetched) return
|
||||||
|
routstrModelsFetched = true
|
||||||
|
try {
|
||||||
|
const res = await apiFetch(ROUTSTR_MODELS_PATH)
|
||||||
|
if (!res.ok) return
|
||||||
|
const data = await res.json()
|
||||||
|
if (Array.isArray(data?.data)) {
|
||||||
|
routstrModels.value = data.data
|
||||||
|
.filter((m: Record<string, unknown>) => typeof m.id === 'string')
|
||||||
|
.map((m: Record<string, unknown>) => ({
|
||||||
|
id: m.id as string,
|
||||||
|
name: (m.name as string) || (m.id as string),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
routstrModelsFetched = false // allow a retry on the next send/open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const availableProviders = computed(() => {
|
const availableProviders = computed(() => {
|
||||||
const providers: { id: Provider; name: string; models: { id: string; name: string }[] }[] = [
|
const providers: { id: Provider; name: string; models: { id: string; name: string }[] }[] = [
|
||||||
|
{
|
||||||
|
id: 'routstr',
|
||||||
|
name: 'Routstr (sats)',
|
||||||
|
models: routstrModels.value.length > 0
|
||||||
|
? routstrModels.value
|
||||||
|
: [{ id: 'routstr-unavailable', name: 'No models — node offline?' }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'claude',
|
id: 'claude',
|
||||||
name: 'Claude (Max)',
|
name: 'Claude (Max)',
|
||||||
@@ -381,6 +416,71 @@ async function streamOpenRouter(
|
|||||||
}, onError, signal)
|
}, onError, signal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routstr: one paid, NON-streaming, OpenAI-shaped completion through the
|
||||||
|
* node's session-gated `/aiui/api/routstr/` forwarder. The node quotes a
|
||||||
|
* price from the live catalog, pays with a Cashu token against the
|
||||||
|
* operator's budget (Settings → System → Routstr AI budget), redeems the
|
||||||
|
* change, and passes the provider's JSON back. The full answer is emitted
|
||||||
|
* as a single token — streaming across a paid hop is the planned follow-up.
|
||||||
|
*/
|
||||||
|
async function streamRoutstr(
|
||||||
|
messages: ChatMessage[],
|
||||||
|
onToken: (text: string) => void,
|
||||||
|
onError: (err: string) => void,
|
||||||
|
systemPrompt: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
|
const wireMessages = [
|
||||||
|
{ role: 'system' as const, content: systemPrompt },
|
||||||
|
...messages.map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content })),
|
||||||
|
]
|
||||||
|
|
||||||
|
const res = await apiFetch(ROUTSTR_CHAT_PATH, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: activeModel.value,
|
||||||
|
messages: wireMessages,
|
||||||
|
stream: false,
|
||||||
|
}),
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
const bodyText = await res.text().catch(() => '')
|
||||||
|
if (!res.ok) {
|
||||||
|
// The node's refusals carry a plain-language error.message (budget not
|
||||||
|
// set, budget spent, wallet can't fund) — surface it verbatim.
|
||||||
|
let msg = `Routstr error ${res.status}`
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(bodyText)
|
||||||
|
// Node refusals use {error:{message}}; the upstream provider nests
|
||||||
|
// its own as {detail:{error:{message}}} or a plain {detail:"..."}.
|
||||||
|
const detail = parsed?.detail
|
||||||
|
msg =
|
||||||
|
parsed?.error?.message ??
|
||||||
|
detail?.error?.message ??
|
||||||
|
(typeof detail === 'string' ? detail : undefined) ??
|
||||||
|
msg
|
||||||
|
} catch { /* keep the status-only message */ }
|
||||||
|
onError(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signal?.aborted) return
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(bodyText)
|
||||||
|
const text = parsed?.choices?.[0]?.message?.content
|
||||||
|
if (typeof text === 'string' && text.length > 0) {
|
||||||
|
onToken(text)
|
||||||
|
} else {
|
||||||
|
onError('Routstr returned an empty response')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
onError('Routstr returned a malformed response')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Embedded-mode chat delegation (D-01/D-17): when AIUI is running inside
|
* Embedded-mode chat delegation (D-01/D-17): when AIUI is running inside
|
||||||
* Archy, the model call, the tool-calling loop, and the model key all live
|
* Archy, the model call, the tool-calling loop, and the model key all live
|
||||||
@@ -619,7 +719,11 @@ export async function streamWithModel(
|
|||||||
activeModel.value = model
|
activeModel.value = model
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (useArchy().isEmbedded.value) {
|
if (provider === 'routstr') {
|
||||||
|
// Explicitly chosen Routstr wins even embedded in Archy — the whole
|
||||||
|
// point of the picker entry is that it is a selection, not a fallback.
|
||||||
|
await streamRoutstr(history, onToken, onError, 'You are a helpful assistant.', signal)
|
||||||
|
} else if (useArchy().isEmbedded.value) {
|
||||||
// D-17: embedded mode delegates the loop, the tools and the key to
|
// D-17: embedded mode delegates the loop, the tools and the key to
|
||||||
// Archy — provider/model selection here doesn't apply node-side.
|
// Archy — provider/model selection here doesn't apply node-side.
|
||||||
await streamViaArchy(history, onToken, onError, signal)
|
await streamViaArchy(history, onToken, onError, signal)
|
||||||
@@ -638,8 +742,9 @@ export async function streamWithModel(
|
|||||||
export function useAI() {
|
export function useAI() {
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
|
|
||||||
// Fetch Wavlake catalog on first use (non-blocking)
|
// Fetch Wavlake + Routstr catalogs on first use (non-blocking)
|
||||||
refreshWavlakeCatalog()
|
refreshWavlakeCatalog()
|
||||||
|
refreshRoutstrModels()
|
||||||
|
|
||||||
function stopGeneration() {
|
function stopGeneration() {
|
||||||
if (currentAbort) {
|
if (currentAbort) {
|
||||||
@@ -712,7 +817,11 @@ export function useAI() {
|
|||||||
const genParams = getConversationParams(chatStore)
|
const genParams = getConversationParams(chatStore)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (useArchy().isEmbedded.value) {
|
if (provider === 'routstr') {
|
||||||
|
// Explicitly chosen Routstr wins even embedded in Archy — a
|
||||||
|
// selection, not a fallback.
|
||||||
|
await streamRoutstr(history, onToken, onError, systemPrompt, signal)
|
||||||
|
} else if (useArchy().isEmbedded.value) {
|
||||||
// D-17: embedded mode delegates the loop, the tools and the key to
|
// D-17: embedded mode delegates the loop, the tools and the key to
|
||||||
// Archy — provider/model selection here doesn't apply node-side.
|
// Archy — provider/model selection here doesn't apply node-side.
|
||||||
await streamViaArchy(history, onToken, onError, signal)
|
await streamViaArchy(history, onToken, onError, signal)
|
||||||
@@ -828,7 +937,11 @@ export function useAI() {
|
|||||||
const genParams = getConversationParams(chatStore)
|
const genParams = getConversationParams(chatStore)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (useArchy().isEmbedded.value) {
|
if (provider === 'routstr') {
|
||||||
|
// Explicitly chosen Routstr wins even embedded in Archy — a
|
||||||
|
// selection, not a fallback.
|
||||||
|
await streamRoutstr(history, onToken, onError, systemPrompt, signal)
|
||||||
|
} else if (useArchy().isEmbedded.value) {
|
||||||
// D-17: embedded mode delegates the loop, the tools and the key to
|
// D-17: embedded mode delegates the loop, the tools and the key to
|
||||||
// Archy — provider/model selection here doesn't apply node-side.
|
// Archy — provider/model selection here doesn't apply node-side.
|
||||||
await streamViaArchy(history, onToken, onError, signal)
|
await streamViaArchy(history, onToken, onError, signal)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ app:
|
|||||||
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
||||||
fi;
|
fi;
|
||||||
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
||||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -allowignoredconf=1 -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -allowignoredconf=1 -printtoconsole=0 -server=1 -prune=50000 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||||
else
|
else
|
||||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -allowignoredconf=1 -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -allowignoredconf=1 -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ app:
|
|||||||
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
||||||
fi;
|
fi;
|
||||||
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
||||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -allowignoredconf=1 -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -allowignoredconf=1 -printtoconsole=0 -server=1 -prune=50000 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||||
else
|
else
|
||||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -allowignoredconf=1 -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -allowignoredconf=1 -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||||
fi
|
fi
|
||||||
|
|||||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.2-alpha"
|
version = "1.8.3-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.2-alpha"
|
version = "1.8.3-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod node_message;
|
|||||||
mod proxy;
|
mod proxy;
|
||||||
mod remote_input;
|
mod remote_input;
|
||||||
mod remote_relay;
|
mod remote_relay;
|
||||||
|
mod routstr_proxy;
|
||||||
mod websocket;
|
mod websocket;
|
||||||
|
|
||||||
use crate::api::rpc::RpcHandler;
|
use crate::api::rpc::RpcHandler;
|
||||||
@@ -449,6 +450,14 @@ impl ApiHandler {
|
|||||||
self.handle_model_proxy(req_with_bytes, p).await
|
self.handle_model_proxy(req_with_bytes, p).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AIUI Routstr proxy — the explicit, user-selected Routstr
|
||||||
|
// provider (model catalog + Cashu-paid completions), same
|
||||||
|
// session-gate discipline as the model proxy above. D-05: paid
|
||||||
|
// requests are refused unless the operator has armed a budget.
|
||||||
|
(_, p) if p.starts_with("/aiui/api/routstr/") => {
|
||||||
|
self.handle_routstr_proxy(req_with_bytes, p).await
|
||||||
|
}
|
||||||
|
|
||||||
// Health — unauthenticated, returns JSON with service status
|
// Health — unauthenticated, returns JSON with service status
|
||||||
(Method::GET, "/health") => {
|
(Method::GET, "/health") => {
|
||||||
let recovery_complete = crate::crash_recovery::is_recovery_complete();
|
let recovery_complete = crate::crash_recovery::is_recovery_complete();
|
||||||
|
|||||||
@@ -84,14 +84,14 @@ async fn route_model_proxy(
|
|||||||
/// call back into `ApiHandler::is_authenticated` — keeping this small and
|
/// call back into `ApiHandler::is_authenticated` — keeping this small and
|
||||||
/// dependency-free is what makes the 401 behaviour unit-testable without
|
/// dependency-free is what makes the 401 behaviour unit-testable without
|
||||||
/// paying for a full `ApiHandler` in every test.
|
/// paying for a full `ApiHandler` in every test.
|
||||||
async fn is_authenticated(session_store: &SessionStore, headers: &HeaderMap) -> bool {
|
pub(super) async fn is_authenticated(session_store: &SessionStore, headers: &HeaderMap) -> bool {
|
||||||
match session::extract_session_cookie(headers) {
|
match session::extract_session_cookie(headers) {
|
||||||
Some(token) => session_store.validate(&token).await,
|
Some(token) => session_store.validate(&token).await,
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unauthorized() -> Response<Body> {
|
pub(super) fn unauthorized() -> Response<Body> {
|
||||||
let body = serde_json::json!({ "error": "Unauthorized" });
|
let body = serde_json::json!({ "error": "Unauthorized" });
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::UNAUTHORIZED)
|
.status(StatusCode::UNAUTHORIZED)
|
||||||
@@ -119,7 +119,7 @@ fn key_not_configured() -> Response<Body> {
|
|||||||
/// backends' egress screen never sees the forwarder path — the standalone
|
/// backends' egress screen never sees the forwarder path — the standalone
|
||||||
/// frontend posts FULL history and images straight here — so the forwarder
|
/// frontend posts FULL history and images straight here — so the forwarder
|
||||||
/// screens for itself. 400, plain-language, never naming what matched.
|
/// screens for itself. 400, plain-language, never naming what matched.
|
||||||
fn blocked_secret_shaped() -> Response<Body> {
|
pub(super) fn blocked_secret_shaped() -> Response<Body> {
|
||||||
let body = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
"error": "Blocked: this request contained secret-shaped content (e.g. a seed phrase, key, or token). It was not sent anywhere."
|
"error": "Blocked: this request contained secret-shaped content (e.g. a seed phrase, key, or token). It was not sent anywhere."
|
||||||
});
|
});
|
||||||
@@ -134,12 +134,12 @@ fn blocked_secret_shaped() -> Response<Body> {
|
|||||||
/// the assistant's secret-shape rules (G-B1) with this node's own secrets
|
/// the assistant's secret-shape rules (G-B1) with this node's own secrets
|
||||||
/// as the deny corpus. Returns Some(kind) — kind only, never the value —
|
/// as the deny corpus. Returns Some(kind) — kind only, never the value —
|
||||||
/// when the content must not leave.
|
/// when the content must not leave.
|
||||||
async fn forward_screen(text: &str, data_dir: &Path) -> Option<&'static str> {
|
pub(super) async fn forward_screen(text: &str, data_dir: &Path) -> Option<&'static str> {
|
||||||
let secrets = crate::assistant::egress::load_known_secrets(&data_dir.join("secrets")).await;
|
let secrets = crate::assistant::egress::load_known_secrets(&data_dir.join("secrets")).await;
|
||||||
crate::assistant::egress::scan_secret_shapes(text, &secrets)
|
crate::assistant::egress::scan_secret_shapes(text, &secrets)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bad_gateway(msg: &str) -> Response<Body> {
|
pub(super) fn bad_gateway(msg: &str) -> Response<Body> {
|
||||||
let body = serde_json::json!({ "error": msg });
|
let body = serde_json::json!({ "error": msg });
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::BAD_GATEWAY)
|
.status(StatusCode::BAD_GATEWAY)
|
||||||
@@ -331,7 +331,7 @@ async fn forward(
|
|||||||
/// same shape as `proxy.rs`'s peer-content Range streamer — so a
|
/// same shape as `proxy.rs`'s peer-content Range streamer — so a
|
||||||
/// token-by-token reply doesn't wait for the full response before the first
|
/// token-by-token reply doesn't wait for the full response before the first
|
||||||
/// byte reaches the browser.
|
/// byte reaches the browser.
|
||||||
fn stream_response(resp: reqwest::Response) -> Result<Response<Body>> {
|
pub(super) fn stream_response(resp: reqwest::Response) -> Result<Response<Body>> {
|
||||||
let status = resp.status().as_u16();
|
let status = resp.status().as_u16();
|
||||||
let headers = resp.headers().clone();
|
let headers = resp.headers().clone();
|
||||||
let mut builder = Response::builder().status(status);
|
let mut builder = Response::builder().status(status);
|
||||||
|
|||||||
@@ -0,0 +1,535 @@
|
|||||||
|
//! Session-gated forwarder for `/aiui/api/routstr/*` — the explicit,
|
||||||
|
//! user-selected Routstr path (as opposed to `assistant/backends/routstr.rs`,
|
||||||
|
//! which is the D-04 fallback leg the operator never chooses directly).
|
||||||
|
//!
|
||||||
|
//! AIUI's model picker lists Routstr as a first-class provider; selecting one
|
||||||
|
//! of its models routes chat completions through here. Same discipline as
|
||||||
|
//! `model_proxy.rs`: auth is re-derived from the request's own session cookie
|
||||||
|
//! (never trusted to nginx), inbound `authorization`/`cookie` headers are
|
||||||
|
//! never forwarded, and every outbound body is egress-screened (S3) before it
|
||||||
|
//! leaves the node.
|
||||||
|
//!
|
||||||
|
//! Payment is Cashu, D-05-gated end to end: a request is refused unless the
|
||||||
|
//! operator has set a non-zero Routstr allowance (Settings → System), the
|
||||||
|
//! quoted price fits the remaining allowance, and `auto_pay_token` (the ONE
|
||||||
|
//! budget-capped payment primitive, T-13-89) agrees to build the token. The
|
||||||
|
//! provider's change (`X-Cashu` / `X-Cashu-Refund` response headers, per
|
||||||
|
//! docs.routstr.com) is redeemed back into the node wallet and only the net
|
||||||
|
//! is recorded against the allowance.
|
||||||
|
//!
|
||||||
|
//! Upstream is the public Routstr aggregator instance routstr.com itself
|
||||||
|
//! ships against (verified live 2026-08-14: `/v1/models` serves the full
|
||||||
|
//! catalog with `sats_pricing`; the canonical `api.routstr.com` host 404s).
|
||||||
|
//! Making the instance operator-configurable — or sourcing it from the Nostr
|
||||||
|
//! provider announcements once those carry real endpoint/pricing content —
|
||||||
|
//! is the planned follow-up, not this file's job.
|
||||||
|
|
||||||
|
use super::ApiHandler;
|
||||||
|
use crate::session::SessionStore;
|
||||||
|
use anyhow::Result;
|
||||||
|
use hyper::{Body, Method, Request, Response, StatusCode};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Mutex as StdMutex;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use super::model_proxy::{
|
||||||
|
bad_gateway, blocked_secret_shaped, forward_screen, is_authenticated, unauthorized,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The live public Routstr aggregator (the same instance routstr.com's own
|
||||||
|
/// frontend queries for `/v1/providers` and `/v1/models`).
|
||||||
|
const ROUTSTR_INSTANCE: &str = "https://routstr.otrta.me";
|
||||||
|
/// Generation cap forced onto every forwarded completion — never unbounded
|
||||||
|
/// (T-13-88), and the completion half of the price quote is arithmetic over
|
||||||
|
/// exactly this figure.
|
||||||
|
const MAX_COMPLETION_TOKENS: u64 = 1024;
|
||||||
|
/// Same round-trip ceiling as `model_proxy.rs`'s Claude/Ollama forwarders.
|
||||||
|
const FORWARD_TIMEOUT_SECS: u64 = 180;
|
||||||
|
/// Models-catalog cache TTL — mirrors `backends/routstr.rs`'s provider
|
||||||
|
/// discovery TTL. The catalog prices every chat request, so it cannot be
|
||||||
|
/// fetched per-message without doubling latency.
|
||||||
|
const MODELS_CACHE_TTL: Duration = Duration::from_secs(300);
|
||||||
|
|
||||||
|
/// One model's sats-denominated pricing, parsed from the aggregator's
|
||||||
|
/// `/v1/models` entries (`sats_pricing`). Rates are sats PER TOKEN (fractional
|
||||||
|
/// floats); `request` is a flat per-request fee in sats. Untrusted input — a
|
||||||
|
/// missing/garbled field parses as 0.0 and simply prices low, which the
|
||||||
|
/// remaining-allowance ceiling still caps.
|
||||||
|
#[derive(Debug, Clone, Default, serde::Deserialize)]
|
||||||
|
struct SatsPricing {
|
||||||
|
#[serde(default)]
|
||||||
|
prompt: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
completion: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
request: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quote a price in whole sats for one completion call: flat request fee +
|
||||||
|
/// prompt rate × (payload chars / 4, the usual chars-per-token rule of thumb)
|
||||||
|
/// + completion rate × the forced `MAX_COMPLETION_TOKENS` cap, +20% margin,
|
||||||
|
/// rounded up, never below 1. Deliberately a pure function so the arithmetic
|
||||||
|
/// is unit-testable; deliberately conservative because the provider's change
|
||||||
|
/// comes back as a Cashu refund and is redeemed — overquoting costs nothing
|
||||||
|
/// but float, underquoting gets the request rejected upstream.
|
||||||
|
fn estimate_price_sats(pricing: &SatsPricing, prompt_chars: usize, completion_tokens: u64) -> u64 {
|
||||||
|
let prompt_tokens = (prompt_chars as f64) / 4.0;
|
||||||
|
let raw = pricing.request
|
||||||
|
+ pricing.prompt * prompt_tokens
|
||||||
|
+ pricing.completion * (completion_tokens as f64);
|
||||||
|
let with_margin = raw * 1.2;
|
||||||
|
(with_margin.ceil() as u64).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process-lifetime cache of the aggregator's models catalog (same pattern as
|
||||||
|
/// `backends/routstr.rs`'s `PROVIDER_CACHE`).
|
||||||
|
static MODELS_CACHE: OnceLock<StdMutex<Option<(Instant, Value)>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn cached_models() -> Option<Value> {
|
||||||
|
let cache = MODELS_CACHE.get_or_init(|| StdMutex::new(None));
|
||||||
|
let guard = cache.lock().expect("routstr models cache poisoned");
|
||||||
|
guard.as_ref().and_then(|(at, models)| {
|
||||||
|
if at.elapsed() < MODELS_CACHE_TTL {
|
||||||
|
Some(models.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_cached_models(models: Value) {
|
||||||
|
let cache = MODELS_CACHE.get_or_init(|| StdMutex::new(None));
|
||||||
|
*cache.lock().expect("routstr models cache poisoned") = Some((Instant::now(), models));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch (or serve cached) the aggregator's `/v1/models` catalog.
|
||||||
|
async fn fetch_models() -> Result<Value> {
|
||||||
|
if let Some(cached) = cached_models() {
|
||||||
|
return Ok(cached);
|
||||||
|
}
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(20))
|
||||||
|
.build()?;
|
||||||
|
let url = format!("{ROUTSTR_INSTANCE}/v1/models");
|
||||||
|
let resp = client.get(&url).send().await?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
anyhow::bail!("routstr models upstream returned HTTP {}", resp.status());
|
||||||
|
}
|
||||||
|
let models: Value = resp.json().await?;
|
||||||
|
set_cached_models(models.clone());
|
||||||
|
Ok(models)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find one model's `sats_pricing` in the catalog by exact id.
|
||||||
|
fn pricing_for_model(models: &Value, model_id: &str) -> Option<SatsPricing> {
|
||||||
|
models
|
||||||
|
.get("data")?
|
||||||
|
.as_array()?
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.get("id").and_then(|v| v.as_str()) == Some(model_id))
|
||||||
|
.and_then(|m| m.get("sats_pricing"))
|
||||||
|
.and_then(|sp| serde_json::from_value(sp.clone()).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_response(status: StatusCode, body: Value) -> Response<Body> {
|
||||||
|
Response::builder()
|
||||||
|
.status(status)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(Body::from(serde_json::to_vec(&body).unwrap_or_default()))
|
||||||
|
.unwrap_or_else(|_| Response::new(Body::from("{}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plain-language refusal naming the UI path that fixes it — never a bare
|
||||||
|
/// status (RULE: every action needs a UI path).
|
||||||
|
fn budget_refusal(msg: String) -> Response<Body> {
|
||||||
|
json_response(
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
json!({ "error": { "message": msg } }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiHandler {
|
||||||
|
/// Entry point wired into the `/aiui/api/routstr/` arm in `mod.rs` —
|
||||||
|
/// thin, like `handle_model_proxy`, so the routing/budget logic below is
|
||||||
|
/// testable without a full `ApiHandler`.
|
||||||
|
pub(super) async fn handle_routstr_proxy(
|
||||||
|
&self,
|
||||||
|
req: Request<Body>,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<Response<Body>> {
|
||||||
|
route_routstr_proxy(&self.session_store, &self.config.data_dir, req, path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn route_routstr_proxy(
|
||||||
|
session_store: &SessionStore,
|
||||||
|
data_dir: &Path,
|
||||||
|
req: Request<Body>,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<Response<Body>> {
|
||||||
|
if !is_authenticated(session_store, req.headers()).await {
|
||||||
|
tracing::warn!("401 routstr proxy {} — session invalid or missing", path);
|
||||||
|
return Ok(unauthorized());
|
||||||
|
}
|
||||||
|
match path.strip_prefix("/aiui/api/routstr/") {
|
||||||
|
Some("models") if req.method() == Method::GET => forward_models().await,
|
||||||
|
Some("chat/completions") if req.method() == Method::POST => {
|
||||||
|
forward_chat(req, data_dir).await
|
||||||
|
}
|
||||||
|
_ => Ok(unauthorized()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /aiui/api/routstr/models — the full catalog, passed through so AIUI
|
||||||
|
/// can render ids/names and show sats pricing. Read-only and unpaid.
|
||||||
|
async fn forward_models() -> Result<Response<Body>> {
|
||||||
|
match fetch_models().await {
|
||||||
|
Ok(models) => Ok(json_response(StatusCode::OK, models)),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("routstr proxy: models upstream failed: {}", e);
|
||||||
|
Ok(bad_gateway("Routstr model catalog is unreachable"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /aiui/api/routstr/chat/completions — one paid, non-streaming,
|
||||||
|
/// OpenAI-shaped completion. Order matters: screen (S3) → budget gate (D-05,
|
||||||
|
/// offline) → price quote → pay → forward → redeem change → record net.
|
||||||
|
async fn forward_chat(req: Request<Body>, data_dir: &Path) -> Result<Response<Body>> {
|
||||||
|
let payload = hyper::body::to_bytes(req.into_body())
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("read request payload: {e}"))?;
|
||||||
|
let payload_str = String::from_utf8_lossy(&payload).to_string();
|
||||||
|
|
||||||
|
// S3: the standalone frontend posts full history straight here with no
|
||||||
|
// assistant loop (and no egress screen) behind it.
|
||||||
|
if let Some(kind) = forward_screen(&payload_str, data_dir).await {
|
||||||
|
tracing::error!(
|
||||||
|
kind,
|
||||||
|
"routstr proxy: blocked chat forward — secret-shaped content"
|
||||||
|
);
|
||||||
|
return Ok(blocked_secret_shaped());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body: Value = match serde_json::from_str(&payload_str) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => {
|
||||||
|
return Ok(json_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
json!({ "error": { "message": "request body is not valid JSON" } }),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(model_id) = body.get("model").and_then(|v| v.as_str()).map(String::from) else {
|
||||||
|
return Ok(json_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
json!({ "error": { "message": "request is missing a model id" } }),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// D-05 gate, checked before any network I/O: a zero allowance means
|
||||||
|
// Routstr is refused outright, with the UI path that arms it.
|
||||||
|
let mut budget = crate::assistant::AssistantBudget::load(data_dir).await;
|
||||||
|
if budget.allowance_sats == 0 {
|
||||||
|
return Ok(budget_refusal(
|
||||||
|
"Routstr is disabled on this node — set a sats budget in Settings → System → \
|
||||||
|
Routstr AI budget to enable it."
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let remaining = budget.remaining_sats();
|
||||||
|
if remaining == 0 {
|
||||||
|
return Ok(budget_refusal(format!(
|
||||||
|
"This period's Routstr budget is spent ({} of {} sats). Raise the allowance in \
|
||||||
|
Settings → System → Routstr AI budget to continue.",
|
||||||
|
budget.spent_sats, budget.allowance_sats
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price the request from the catalog. An unknown model is a caller bug
|
||||||
|
// (the dropdown only offers catalog models), not a reason to guess a
|
||||||
|
// price.
|
||||||
|
let models = match fetch_models().await {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("routstr proxy: cannot price request, models fetch failed: {e}");
|
||||||
|
return Ok(bad_gateway(
|
||||||
|
"Routstr model catalog is unreachable — cannot price this request",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(pricing) = pricing_for_model(&models, &model_id) else {
|
||||||
|
return Ok(json_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
json!({ "error": { "message": format!("unknown Routstr model: {model_id}") } }),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Force the shape this forwarder actually supports: non-streaming, with
|
||||||
|
// an explicit, capped generation limit (T-13-88).
|
||||||
|
let max_tokens = body
|
||||||
|
.get("max_tokens")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(MAX_COMPLETION_TOKENS)
|
||||||
|
.min(MAX_COMPLETION_TOKENS);
|
||||||
|
body["stream"] = json!(false);
|
||||||
|
body["max_tokens"] = json!(max_tokens);
|
||||||
|
|
||||||
|
let price_sats = estimate_price_sats(&pricing, payload_str.len(), max_tokens);
|
||||||
|
if price_sats > remaining {
|
||||||
|
return Ok(budget_refusal(format!(
|
||||||
|
"This request quotes ~{price_sats} sats but only {remaining} sats remain in this \
|
||||||
|
period's Routstr budget (Settings → System → Routstr AI budget)."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pay via the ONE budget-capped primitive (T-13-89) — same call, same
|
||||||
|
// mint list as the fallback leg in backends/routstr.rs.
|
||||||
|
let accepted_mints = crate::wallet::ecash::load_accepted_mints(data_dir)
|
||||||
|
.await
|
||||||
|
.map(|m| m.mints)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let token = match crate::swarm::payment::auto_pay_token(
|
||||||
|
data_dir,
|
||||||
|
&budget.payment_policy(),
|
||||||
|
&accepted_mints,
|
||||||
|
price_sats,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
return Ok(budget_refusal(format!(
|
||||||
|
"The node wallet could not fund this request (~{price_sats} sats) — check the \
|
||||||
|
ecash balance and accepted mints in Settings → Wallet."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(FORWARD_TIMEOUT_SECS))
|
||||||
|
.build()?;
|
||||||
|
let url = format!("{ROUTSTR_INSTANCE}/v1/chat/completions");
|
||||||
|
let resp = match client
|
||||||
|
.post(&url)
|
||||||
|
.header("Authorization", format!("Bearer {token}"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
// The token never reached the provider — reclaim it into our own
|
||||||
|
// wallet so the sats aren't stranded, and record nothing.
|
||||||
|
match crate::wallet::ecash::receive_token(data_dir, &token).await {
|
||||||
|
Ok(_) => tracing::info!(
|
||||||
|
"routstr proxy: upstream send failed ({e}); unsent payment token reclaimed"
|
||||||
|
),
|
||||||
|
Err(re) => tracing::warn!(
|
||||||
|
"routstr proxy: upstream send failed ({e}) AND reclaiming the unsent token \
|
||||||
|
failed ({re}) — {price_sats} sats may be stranded in the token"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return Ok(bad_gateway("Routstr provider is unreachable"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
// Change comes back as a Cashu token header (docs.routstr.com names both
|
||||||
|
// spellings across versions); redeem it so only the net leaves the
|
||||||
|
// allowance.
|
||||||
|
let refund_token = ["x-cashu-refund", "x-cashu"]
|
||||||
|
.iter()
|
||||||
|
.find_map(|h| resp.headers().get(*h))
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.map(String::from);
|
||||||
|
let resp_body = resp.bytes().await.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut reclaimed = 0u64;
|
||||||
|
if let Some(refund) = refund_token {
|
||||||
|
match crate::wallet::ecash::receive_token(data_dir, &refund).await {
|
||||||
|
Ok(sats) => reclaimed = sats,
|
||||||
|
Err(e) => tracing::warn!("routstr proxy: redeeming the change token failed: {e}"),
|
||||||
|
}
|
||||||
|
} else if !status.is_success() {
|
||||||
|
// The provider refused the request (e.g. "mint unreachable") and
|
||||||
|
// sent no change — if it never actually redeemed our token, the
|
||||||
|
// proofs are still ours to take back. If it DID redeem and then
|
||||||
|
// failed, this reclaim fails harmlessly and the spend stands.
|
||||||
|
match crate::wallet::ecash::receive_token(data_dir, &token).await {
|
||||||
|
Ok(sats) => {
|
||||||
|
reclaimed = sats;
|
||||||
|
tracing::info!(
|
||||||
|
"routstr proxy: upstream refused (HTTP {status}); unredeemed payment token \
|
||||||
|
reclaimed ({sats} sats)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => tracing::warn!(
|
||||||
|
"routstr proxy: upstream refused (HTTP {status}) and the payment token could \
|
||||||
|
not be reclaimed ({e}) — treating the {price_sats} sats as spent"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let net_sats = price_sats.saturating_sub(reclaimed);
|
||||||
|
if net_sats > 0 {
|
||||||
|
if let Err(e) = budget.record_spend(data_dir, net_sats).await {
|
||||||
|
tracing::warn!(
|
||||||
|
error = %e,
|
||||||
|
"routstr proxy: failed to persist the budget spend (the payment itself already happened)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::info!(
|
||||||
|
model = %model_id,
|
||||||
|
quoted = price_sats,
|
||||||
|
reclaimed,
|
||||||
|
net = net_sats,
|
||||||
|
status = %status,
|
||||||
|
"routstr proxy: forwarded paid chat completion"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(status.as_u16())
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(Body::from(resp_body))
|
||||||
|
.unwrap_or_else(|_| Response::new(Body::from("{}"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
async fn test_store() -> SessionStore {
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"archy-routstr-proxy-test-sessions-{}.json",
|
||||||
|
rand::RngCore::next_u64(&mut rand::rngs::OsRng)
|
||||||
|
));
|
||||||
|
SessionStore::new_for_tests(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn req(method: &str, path: &str, cookie: Option<&str>, body: &'static str) -> Request<Body> {
|
||||||
|
let mut builder = Request::builder().method(method).uri(path);
|
||||||
|
if let Some(c) = cookie {
|
||||||
|
builder = builder.header("cookie", format!("session={c}"));
|
||||||
|
}
|
||||||
|
builder.body(Body::from(body)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn models_without_session_is_401() {
|
||||||
|
let store = test_store().await;
|
||||||
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
|
let r = req("GET", "/aiui/api/routstr/models", None, "");
|
||||||
|
let resp = route_routstr_proxy(&store, data_dir.path(), r, "/aiui/api/routstr/models")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn chat_without_session_is_401() {
|
||||||
|
let store = test_store().await;
|
||||||
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
|
let r = req("POST", "/aiui/api/routstr/chat/completions", None, "{}");
|
||||||
|
let resp = route_routstr_proxy(
|
||||||
|
&store,
|
||||||
|
data_dir.path(),
|
||||||
|
r,
|
||||||
|
"/aiui/api/routstr/chat/completions",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// D-05: a fresh node (no budget file → zero allowance) refuses the paid
|
||||||
|
/// path BEFORE any pricing/network I/O — this test runs fully offline.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn chat_with_zero_allowance_is_refused_offline() {
|
||||||
|
let store = test_store().await;
|
||||||
|
let token = store.create().await;
|
||||||
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
|
let r = req(
|
||||||
|
"POST",
|
||||||
|
"/aiui/api/routstr/chat/completions",
|
||||||
|
Some(&token),
|
||||||
|
r#"{"model":"some-model","messages":[{"role":"user","content":"hi"}]}"#,
|
||||||
|
);
|
||||||
|
let resp = route_routstr_proxy(
|
||||||
|
&store,
|
||||||
|
data_dir.path(),
|
||||||
|
r,
|
||||||
|
"/aiui/api/routstr/chat/completions",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||||
|
let body = hyper::body::to_bytes(resp.into_body()).await.unwrap();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
let msg = v["error"]["message"].as_str().unwrap();
|
||||||
|
assert!(msg.contains("Settings"), "refusal must name the UI path");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn chat_body_carrying_bip39_is_blocked() {
|
||||||
|
let store = test_store().await;
|
||||||
|
let token = store.create().await;
|
||||||
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::create_dir_all(data_dir.path().join("secrets")).unwrap();
|
||||||
|
let r = req(
|
||||||
|
"POST",
|
||||||
|
"/aiui/api/routstr/chat/completions",
|
||||||
|
Some(&token),
|
||||||
|
r#"{"model":"m","messages":[{"role":"user","content":"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"}]}"#,
|
||||||
|
);
|
||||||
|
let resp = route_routstr_proxy(
|
||||||
|
&store,
|
||||||
|
data_dir.path(),
|
||||||
|
r,
|
||||||
|
"/aiui/api/routstr/chat/completions",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn price_estimate_is_conservative_and_never_zero() {
|
||||||
|
// A free/garbled pricing entry still quotes at least 1 sat.
|
||||||
|
assert_eq!(estimate_price_sats(&SatsPricing::default(), 100, 1024), 1);
|
||||||
|
|
||||||
|
// Live-catalog-shaped numbers (deepseek-v4-flash, 2026-08-14):
|
||||||
|
// request 0.001, prompt ~0.000178/tok, completion ~0.000267/tok.
|
||||||
|
let p = SatsPricing {
|
||||||
|
prompt: 0.000178,
|
||||||
|
completion: 0.000267,
|
||||||
|
request: 0.001,
|
||||||
|
};
|
||||||
|
let quote = estimate_price_sats(&p, 4000, 1024);
|
||||||
|
// ~0.18 + ~0.27 + flat, with margin → rounds up to 1 sat.
|
||||||
|
assert_eq!(quote, 1);
|
||||||
|
|
||||||
|
// A pricier model scales with the prompt.
|
||||||
|
let expensive = SatsPricing {
|
||||||
|
prompt: 0.05,
|
||||||
|
completion: 0.1,
|
||||||
|
request: 1.0,
|
||||||
|
};
|
||||||
|
let quote = estimate_price_sats(&expensive, 40_000, 1024);
|
||||||
|
assert!(quote >= 600, "quote {quote} should reflect real rates");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pricing_lookup_finds_exact_model_id() {
|
||||||
|
let models = json!({ "data": [
|
||||||
|
{ "id": "a-model", "sats_pricing": { "prompt": 0.1, "completion": 0.2, "request": 1.0 } },
|
||||||
|
{ "id": "other", "sats_pricing": { "prompt": 0.3 } }
|
||||||
|
]});
|
||||||
|
let p = pricing_for_model(&models, "a-model").unwrap();
|
||||||
|
assert_eq!(p.request, 1.0);
|
||||||
|
assert!(pricing_for_model(&models, "missing").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -344,6 +344,7 @@ impl RpcHandler {
|
|||||||
"content.indeehub-projects" => self.handle_content_indeehub_projects().await,
|
"content.indeehub-projects" => self.handle_content_indeehub_projects().await,
|
||||||
"system.settings.get" => self.handle_system_settings_get(params).await,
|
"system.settings.get" => self.handle_system_settings_get(params).await,
|
||||||
"system.settings.set" => self.handle_system_settings_set(params).await,
|
"system.settings.set" => self.handle_system_settings_set(params).await,
|
||||||
|
"system.node-ca.generate" => self.handle_system_node_ca_generate().await,
|
||||||
"system.kiosk-display.get" => self.handle_system_kiosk_display_get().await,
|
"system.kiosk-display.get" => self.handle_system_kiosk_display_get().await,
|
||||||
"system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await,
|
"system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await,
|
||||||
"bitcoin.relay-update-settings" => {
|
"bitcoin.relay-update-settings" => {
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ impl RpcHandler {
|
|||||||
"lnd.sendcoins" => self.handle_lnd_sendcoins(params).await,
|
"lnd.sendcoins" => self.handle_lnd_sendcoins(params).await,
|
||||||
"lnd.estimatefee" => self.handle_lnd_estimatefee(params).await,
|
"lnd.estimatefee" => self.handle_lnd_estimatefee(params).await,
|
||||||
"lnd.createinvoice" => self.handle_lnd_createinvoice(params).await,
|
"lnd.createinvoice" => self.handle_lnd_createinvoice(params).await,
|
||||||
|
"lnd.invoicestatus" => self.handle_lnd_invoicestatus(params).await,
|
||||||
"lnd.payinvoice" => self.handle_lnd_payinvoice(params).await,
|
"lnd.payinvoice" => self.handle_lnd_payinvoice(params).await,
|
||||||
"lnd.paymentstatus" => self.handle_lnd_paymentstatus(params).await,
|
"lnd.paymentstatus" => self.handle_lnd_paymentstatus(params).await,
|
||||||
"lnd.create-psbt" => self.handle_lnd_create_psbt(params).await,
|
"lnd.create-psbt" => self.handle_lnd_create_psbt(params).await,
|
||||||
@@ -503,6 +504,7 @@ impl RpcHandler {
|
|||||||
"ai.permissions.set" => self.handle_ai_permissions_set(params).await,
|
"ai.permissions.set" => self.handle_ai_permissions_set(params).await,
|
||||||
"system.settings.get" => self.handle_system_settings_get(params).await,
|
"system.settings.get" => self.handle_system_settings_get(params).await,
|
||||||
"system.settings.set" => self.handle_system_settings_set(params).await,
|
"system.settings.set" => self.handle_system_settings_set(params).await,
|
||||||
|
"system.node-ca.generate" => self.handle_system_node_ca_generate().await,
|
||||||
"system.kiosk-display.get" => self.handle_system_kiosk_display_get().await,
|
"system.kiosk-display.get" => self.handle_system_kiosk_display_get().await,
|
||||||
"system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await,
|
"system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await,
|
||||||
|
|
||||||
|
|||||||
@@ -607,9 +607,77 @@ impl RpcHandler {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
// LND returns r_hash base64-encoded; the lookup endpoint the Receive
|
||||||
|
// flow polls (`lnd.invoicestatus`) wants it hex — hand the UI the
|
||||||
|
// ready-to-use form.
|
||||||
|
let r_hash_hex = {
|
||||||
|
use base64::Engine as _;
|
||||||
|
body.get("r_hash")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.and_then(|b64| base64::engine::general_purpose::STANDARD.decode(b64).ok())
|
||||||
|
.map(hex::encode)
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"payment_request": payment_request,
|
"payment_request": payment_request,
|
||||||
"amount_sats": amount_sats,
|
"amount_sats": amount_sats,
|
||||||
|
"r_hash_hex": r_hash_hex,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// lnd.invoicestatus — is this invoice settled yet? Polled by the wallet's
|
||||||
|
/// Receive flow so a Lightning payment gets the same "money has arrived"
|
||||||
|
/// success screen as on-chain (minus the broadcast step: settlement is
|
||||||
|
/// final). Params: `{ "r_hash_hex": string }`.
|
||||||
|
pub(in crate::api::rpc) async fn handle_lnd_invoicestatus(
|
||||||
|
&self,
|
||||||
|
params: Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||||
|
let r_hash_hex = params
|
||||||
|
.get("r_hash_hex")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'r_hash_hex' parameter"))?;
|
||||||
|
if r_hash_hex.len() != 64 || !r_hash_hex.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
return Err(anyhow::anyhow!("r_hash_hex must be 64 hex characters"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (client, macaroon_hex) = self.lnd_client().await?;
|
||||||
|
let resp = client
|
||||||
|
.get(format!("{LND_REST_BASE_URL}/v1/invoice/{r_hash_hex}"))
|
||||||
|
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Failed to query invoice")?;
|
||||||
|
let status = resp.status();
|
||||||
|
let body: serde_json::Value = resp
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("Failed to parse invoice lookup response")?;
|
||||||
|
if !status.is_success() {
|
||||||
|
let msg = body
|
||||||
|
.get("message")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("Unknown error");
|
||||||
|
return Err(anyhow::anyhow!("Invoice lookup failed: {}", msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
let settled = body
|
||||||
|
.get("state")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s == "SETTLED")
|
||||||
|
.unwrap_or_else(|| body.get("settled").and_then(|v| v.as_bool()).unwrap_or(false));
|
||||||
|
let amt_paid_sat = body
|
||||||
|
.get("amt_paid_sat")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.and_then(|s| s.parse::<i64>().ok())
|
||||||
|
.or_else(|| body.get("amt_paid_sat").and_then(|v| v.as_i64()))
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"settled": settled,
|
||||||
|
"amt_paid_sat": amt_paid_sat,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::RpcHandler;
|
use super::RpcHandler;
|
||||||
use crate::network::router as net_router;
|
use crate::network::router as net_router;
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use archipelago_openwrt::{
|
use archipelago_openwrt::{
|
||||||
detect,
|
detect,
|
||||||
router::Router,
|
router::Router,
|
||||||
@@ -38,7 +38,16 @@ impl RpcHandler {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let routers = detect::scan_subnet(subnet, prefix, &ssh_user, &ssh_password).await;
|
// scan_subnet is `async` in name only: up to 255 SEQUENTIAL blocking
|
||||||
|
// TCP probes at 500ms each (~2 min on a /24 that silently drops),
|
||||||
|
// plus a blocking SSH verify per candidate. Inline, one click of
|
||||||
|
// "scan for routers" held a tokio worker for that whole time.
|
||||||
|
let routers = tokio::task::spawn_blocking(move || {
|
||||||
|
tokio::runtime::Handle::current()
|
||||||
|
.block_on(detect::scan_subnet(subnet, prefix, &ssh_user, &ssh_password))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("openwrt scan task")?;
|
||||||
let ips: Vec<String> = routers.iter().map(|ip| ip.to_string()).collect();
|
let ips: Vec<String> = routers.iter().map(|ip| ip.to_string()).collect();
|
||||||
|
|
||||||
Ok(serde_json::json!({ "routers": ips }))
|
Ok(serde_json::json!({ "routers": ips }))
|
||||||
@@ -87,26 +96,20 @@ impl RpcHandler {
|
|||||||
.or_else(|| saved.password.clone())
|
.or_else(|| saved.password.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// The SSH session is blocking (ssh2 over std TcpStream). Run it on the
|
||||||
|
// blocking pool: inline it used to park a tokio worker for the whole
|
||||||
|
// exchange, and against an unreachable router (the gateway that stayed
|
||||||
|
// behind after a node moved networks) the periodic dashboard poll
|
||||||
|
// stalled unrelated RPCs for tens of seconds — long enough that TOTP
|
||||||
|
// codes expired in flight (framework-pt, 2026-08-15).
|
||||||
|
let status = {
|
||||||
|
let host = host.clone();
|
||||||
|
let ssh_user = ssh_user.clone();
|
||||||
|
let ssh_password = ssh_password.clone();
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<serde_json::Value> {
|
||||||
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
||||||
router.verify_openwrt()?;
|
router.verify_openwrt()?;
|
||||||
|
|
||||||
// Persist the connection so other views (e.g. the Home dashboard's
|
|
||||||
// Network tile) can poll `openwrt.get-status` with no params instead
|
|
||||||
// of every caller needing to carry host/credentials around. Only do
|
|
||||||
// this when the host actually came from params — otherwise every
|
|
||||||
// no-args poll would re-save the same thing it just read.
|
|
||||||
if host_from_params {
|
|
||||||
let _ = net_router::configure_router(
|
|
||||||
&self.config.data_dir,
|
|
||||||
net_router::RouterType::OpenWrt,
|
|
||||||
&host,
|
|
||||||
None,
|
|
||||||
Some(&ssh_user),
|
|
||||||
Some(&ssh_password),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// System info
|
// System info
|
||||||
let release = router
|
let release = router
|
||||||
.run_ok("cat /etc/openwrt_release")
|
.run_ok("cat /etc/openwrt_release")
|
||||||
@@ -163,6 +166,29 @@ impl RpcHandler {
|
|||||||
"wifi_interfaces": wifi_interfaces,
|
"wifi_interfaces": wifi_interfaces,
|
||||||
"wan": wan_status,
|
"wan": wan_status,
|
||||||
}))
|
}))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("openwrt status task")??
|
||||||
|
};
|
||||||
|
|
||||||
|
// Persist the connection so other views (e.g. the Home dashboard's
|
||||||
|
// Network tile) can poll `openwrt.get-status` with no params instead
|
||||||
|
// of every caller needing to carry host/credentials around. Only do
|
||||||
|
// this when the host actually came from params — otherwise every
|
||||||
|
// no-args poll would re-save the same thing it just read.
|
||||||
|
if host_from_params {
|
||||||
|
let _ = net_router::configure_router(
|
||||||
|
&self.config.data_dir,
|
||||||
|
net_router::RouterType::OpenWrt,
|
||||||
|
&host,
|
||||||
|
None,
|
||||||
|
Some(&ssh_user),
|
||||||
|
Some(&ssh_password),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provision TollGate on an OpenWrt router and create the "archipelago" SSID.
|
/// Provision TollGate on an OpenWrt router and create the "archipelago" SSID.
|
||||||
@@ -228,9 +254,21 @@ impl RpcHandler {
|
|||||||
enabled: p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true),
|
enabled: p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Blocking SSH session, and provision runs `opkg install` over it —
|
||||||
|
// minutes of held worker if the router stalls mid-exchange.
|
||||||
|
{
|
||||||
|
let host = host.clone();
|
||||||
|
let ssh_user = ssh_user.clone();
|
||||||
|
let ssh_password = ssh_password.clone();
|
||||||
|
let config = config.clone();
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||||
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
||||||
router.verify_openwrt()?;
|
router.verify_openwrt()?;
|
||||||
tollgate::provision(&router, &config).await?;
|
tokio::runtime::Handle::current().block_on(tollgate::provision(&router, &config))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("openwrt provision task")??;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
@@ -279,10 +317,20 @@ impl RpcHandler {
|
|||||||
.or_else(|| saved.password.clone())
|
.or_else(|| saved.password.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// A radio scan is seconds of SSH round-trips even on a healthy
|
||||||
|
// router; keep it off the runtime.
|
||||||
|
let networks = {
|
||||||
|
let host = host.clone();
|
||||||
|
let ssh_user = ssh_user.clone();
|
||||||
|
let ssh_password = ssh_password.clone();
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<Vec<wifi_scan::ScannedNetwork>> {
|
||||||
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
||||||
router.verify_openwrt()?;
|
router.verify_openwrt()?;
|
||||||
|
wifi_scan::scan_networks(&router)
|
||||||
let networks = wifi_scan::scan_networks(&router)?;
|
})
|
||||||
|
.await
|
||||||
|
.context("openwrt wifi scan task")??
|
||||||
|
};
|
||||||
let result: Vec<serde_json::Value> = networks
|
let result: Vec<serde_json::Value> = networks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|n| {
|
.map(|n| {
|
||||||
@@ -357,9 +405,6 @@ impl RpcHandler {
|
|||||||
let dhcp_limit = p.get("dhcp_limit").and_then(|v| v.as_u64()).unwrap_or(150) as u32;
|
let dhcp_limit = p.get("dhcp_limit").and_then(|v| v.as_u64()).unwrap_or(150) as u32;
|
||||||
let masq = p.get("masq").and_then(|v| v.as_bool()).unwrap_or(true);
|
let masq = p.get("masq").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||||
|
|
||||||
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
|
||||||
router.verify_openwrt()?;
|
|
||||||
|
|
||||||
let config = wan::WispConfig {
|
let config = wan::WispConfig {
|
||||||
ssid: ssid.clone(),
|
ssid: ssid.clone(),
|
||||||
password,
|
password,
|
||||||
@@ -368,7 +413,20 @@ impl RpcHandler {
|
|||||||
dhcp_limit,
|
dhcp_limit,
|
||||||
masq,
|
masq,
|
||||||
};
|
};
|
||||||
wan::configure_wisp(&router, &config)?;
|
// Reconfiguring WAN drops and re-establishes the router's uplink, so
|
||||||
|
// the SSH exchange can stall for its full timeout budget mid-command.
|
||||||
|
{
|
||||||
|
let host = host.clone();
|
||||||
|
let ssh_user = ssh_user.clone();
|
||||||
|
let ssh_password = ssh_password.clone();
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||||
|
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
||||||
|
router.verify_openwrt()?;
|
||||||
|
wan::configure_wisp(&router, &config)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("openwrt configure-wan task")??;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(serde_json::json!({ "ok": true, "host": host, "ssid": ssid }))
|
Ok(serde_json::json!({ "ok": true, "host": host, "ssid": ssid }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2525,7 +2525,7 @@ async fn wait_for_adopted_container(package_id: &str, container_name: &str) -> R
|
|||||||
// bitcoin_data_volume_gb removed with write_bitcoin_conf: it only fed that
|
// bitcoin_data_volume_gb removed with write_bitcoin_conf: it only fed that
|
||||||
// function's volume-aware `prune=` line, which bitcoind never read either
|
// function's volume-aware `prune=` line, which bitcoind never read either
|
||||||
// (see remove_stale_bitcoin_conf). The manifest's shell entrypoint already
|
// (see remove_stale_bitcoin_conf). The manifest's shell entrypoint already
|
||||||
// computes DISK_GB_VALUE and hardcodes -prune=550 on small volumes — a
|
// computes DISK_GB_VALUE and hardcodes -prune=50000 on small volumes — a
|
||||||
// real volume-aware prune fix belongs there, not in a conf file nothing
|
// real volume-aware prune fix belongs there, not in a conf file nothing
|
||||||
// reads. Tracked as follow-up in bitcoin-conf-crash-patch.md.
|
// reads. Tracked as follow-up in bitcoin-conf-crash-patch.md.
|
||||||
|
|
||||||
|
|||||||
@@ -1248,6 +1248,30 @@ impl RpcHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// system.node-ca.generate — run the node's (idempotent) CA setup so the
|
||||||
|
/// dashboard can offer certificate generation as a button. WebUI rule:
|
||||||
|
/// users must never be pointed at a terminal; the script reuses an
|
||||||
|
/// existing CA and only reissues the leaf, so re-running is safe.
|
||||||
|
pub(in crate::api::rpc) async fn handle_system_node_ca_generate(
|
||||||
|
&self,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let script = "/opt/archipelago/scripts/setup-node-ca.sh";
|
||||||
|
if tokio::fs::metadata(script).await.is_err() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"The certificate setup script is not on this node yet — it arrives with the next update."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let status = host_sudo(&["/usr/bin/bash", script]).await?;
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Certificate generation failed (exit {:?}) — see the node log for detail",
|
||||||
|
status.code()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
info!("Node CA generated/reissued via dashboard");
|
||||||
|
Ok(serde_json::json!({ "generated": true }))
|
||||||
|
}
|
||||||
|
|
||||||
/// system.kiosk-display.get — Current kiosk display preset + whether this
|
/// system.kiosk-display.get — Current kiosk display preset + whether this
|
||||||
/// node has a kiosk at all (no kiosk unit -> the Settings section hides).
|
/// node has a kiosk at all (no kiosk unit -> the Settings section hides).
|
||||||
pub(in crate::api::rpc) async fn handle_system_kiosk_display_get(
|
pub(in crate::api::rpc) async fn handle_system_kiosk_display_get(
|
||||||
|
|||||||
@@ -731,6 +731,71 @@ fn icon_markup(app: &GatedPort) -> String {
|
|||||||
format!(r#"<div class="tile">{inner}</div>"#)
|
format!(r#"<div class="tile">{inner}</div>"#)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The dashboard login's badge, reproduced square-for-square: the same 20
|
||||||
|
/// white rects AnimatedLogo.vue draws, with the same 100ms stagger, inside
|
||||||
|
/// the same gradient ring. Inline rather than an `<img>` because the shipped
|
||||||
|
/// `favico-black-v2.svg` bakes its own ring into the artwork — wrapping it in
|
||||||
|
/// the CSS ring drew a ring inside a ring, which is not what /login shows.
|
||||||
|
/// (x, y, width, height) as they appear in AnimatedLogo.vue.
|
||||||
|
const LOGO_RECTS: [(f32, f32, f32, f32); 20] = [
|
||||||
|
(357.614, 318.0, 71.007, 70.936),
|
||||||
|
(436.152, 318.0, 72.082, 70.936),
|
||||||
|
(515.766, 318.0, 72.082, 70.936),
|
||||||
|
(595.379, 318.0, 71.007, 70.936),
|
||||||
|
(595.379, 396.46, 71.007, 72.011),
|
||||||
|
(673.917, 396.46, 72.083, 72.011),
|
||||||
|
(278.0, 475.994, 72.083, 72.012),
|
||||||
|
(357.614, 475.994, 71.007, 72.012),
|
||||||
|
(436.152, 475.994, 72.082, 72.012),
|
||||||
|
(515.766, 475.994, 72.082, 72.012),
|
||||||
|
(595.379, 475.994, 71.007, 72.012),
|
||||||
|
(673.917, 475.994, 72.083, 72.012),
|
||||||
|
(278.0, 555.529, 72.083, 70.936),
|
||||||
|
(357.614, 555.529, 71.007, 70.936),
|
||||||
|
(595.379, 555.529, 71.007, 70.936),
|
||||||
|
(673.917, 555.529, 72.083, 70.936),
|
||||||
|
(357.614, 633.989, 71.007, 72.011),
|
||||||
|
(436.152, 633.989, 72.082, 72.011),
|
||||||
|
(515.766, 633.989, 72.082, 72.011),
|
||||||
|
(595.379, 633.989, 71.007, 72.011),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn logo_markup() -> String {
|
||||||
|
let rects: String = LOGO_RECTS
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, (x, y, w, h))| {
|
||||||
|
format!(
|
||||||
|
r#"<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="white" class="sq" style="--d:{delay}ms"/>"#,
|
||||||
|
delay = i * 100,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
format!(
|
||||||
|
r##"<div class="logo"><svg viewBox="0 0 1024 1024" role="img" aria-label="Archipelago" xmlns="http://www.w3.org/2000/svg"><rect width="1024" height="1024" fill="#030202"/>{rects}</svg></div>"##
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main login's in-button spinner, verbatim from Login.vue.
|
||||||
|
const SPINNER_SVG: &str = r#"<svg class="spin" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle style="opacity:.25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path style="opacity:.75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>"#;
|
||||||
|
|
||||||
|
/// Submit feedback: flip the pressed button into its loading face and stop a
|
||||||
|
/// second press, exactly as /login does. This is the only script on the page,
|
||||||
|
/// and the CSP admits it by hash — not `'unsafe-inline'` — so an injected
|
||||||
|
/// `<script>` still cannot run. Everything works without it (the form is a
|
||||||
|
/// plain POST); losing JS costs only the spinner.
|
||||||
|
const SUBMIT_FEEDBACK_JS: &str = "document.addEventListener('submit',function(e){var b=e.target.querySelector('button');if(b){b.classList.add('loading');b.disabled=true;}});";
|
||||||
|
|
||||||
|
/// `'sha256-…'` CSP source expression for [`SUBMIT_FEEDBACK_JS`]. Computed
|
||||||
|
/// from the constant itself so the two can never drift apart.
|
||||||
|
fn submit_feedback_csp_hash() -> String {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
format!(
|
||||||
|
"'sha256-{}'",
|
||||||
|
base64_encode(&Sha256::digest(SUBMIT_FEEDBACK_JS.as_bytes()))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Icons live with the web UI. Only files under the icon directory are read,
|
/// Icons live with the web UI. Only files under the icon directory are read,
|
||||||
/// and only known image extensions — the path comes from a manifest, which is
|
/// and only known image extensions — the path comes from a manifest, which is
|
||||||
/// signed, but treating it as untrusted costs nothing.
|
/// signed, but treating it as untrusted costs nothing.
|
||||||
@@ -782,7 +847,9 @@ const LOGIN_BACKGROUNDS: [&str; 4] = [
|
|||||||
/// join: the name arrives in a URL, and the gate answers before any
|
/// join: the name arrives in a URL, and the gate answers before any
|
||||||
/// authentication, so nothing here may be caller-controlled beyond this set.
|
/// authentication, so nothing here may be caller-controlled beyond this set.
|
||||||
fn read_ui_asset(name: &str) -> Option<(Vec<u8>, &'static str)> {
|
fn read_ui_asset(name: &str) -> Option<(Vec<u8>, &'static str)> {
|
||||||
let allowed = LOGIN_BACKGROUNDS.contains(&name) || name == "favico-black-v2.svg";
|
// Only the rotating backgrounds: the logo badge is inline SVG now, so no
|
||||||
|
// image asset backs it.
|
||||||
|
let allowed = LOGIN_BACKGROUNDS.contains(&name);
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -904,8 +971,15 @@ main {{ position:relative; z-index:1; width:min(92vw,28rem); }}
|
|||||||
width:5rem; height:5rem; border-radius:9999px; padding:3px;
|
width:5rem; height:5rem; border-radius:9999px; padding:3px;
|
||||||
background:linear-gradient(135deg, rgba(255,255,255,.6) 0%, rgba(0,0,0,.8) 100%);
|
background:linear-gradient(135deg, rgba(255,255,255,.6) 0%, rgba(0,0,0,.8) 100%);
|
||||||
box-shadow:0 8px 24px rgba(0,0,0,.5); }}
|
box-shadow:0 8px 24px rgba(0,0,0,.5); }}
|
||||||
.logo img {{ width:100%; height:100%; border-radius:9999px; display:block;
|
.logo::after {{ content:''; position:absolute; inset:3px; border-radius:9999px;
|
||||||
background:#000; padding:.5rem; }}
|
background:#000; z-index:0; }}
|
||||||
|
.logo svg {{ position:relative; z-index:1; width:100%; height:100%;
|
||||||
|
border-radius:9999px; display:block; }}
|
||||||
|
/* AnimatedLogo.vue's reveal, timing intact: each square fades in on its own
|
||||||
|
100ms-step delay over a 3s loop. */
|
||||||
|
.logo .sq {{ opacity:0; animation:logo-square-in 3s ease-out infinite;
|
||||||
|
animation-delay:var(--d,0ms); animation-fill-mode:both; }}
|
||||||
|
@keyframes logo-square-in {{ 0% {{ opacity:0; }} 15% {{ opacity:1; }} 100% {{ opacity:1; }} }}
|
||||||
/* The app's own tile, in the My Apps shape: 18px-rounded square on dark
|
/* The app's own tile, in the My Apps shape: 18px-rounded square on dark
|
||||||
glass with the same inner highlight and drop shadow. */
|
glass with the same inner highlight and drop shadow. */
|
||||||
.tile {{ width:60px; height:60px; border-radius:18px; margin:0 auto .75rem;
|
.tile {{ width:60px; height:60px; border-radius:18px; margin:0 auto .75rem;
|
||||||
@@ -926,22 +1000,42 @@ input {{ width:100%; padding:.75rem 1rem; margin-bottom:1rem; border-radius:.5re
|
|||||||
input::placeholder {{ color:rgba(255,255,255,.4); }}
|
input::placeholder {{ color:rgba(255,255,255,.4); }}
|
||||||
input:focus {{ outline:none; border-color:rgba(255,255,255,.4);
|
input:focus {{ outline:none; border-color:rgba(255,255,255,.4);
|
||||||
box-shadow:0 0 0 1px rgba(255,255,255,.2); }}
|
box-shadow:0 0 0 1px rgba(255,255,255,.2); }}
|
||||||
button {{ width:100%; min-height:44px; padding:.75rem 1.25rem; border:none;
|
/* .glass-button, longhand — the lift, the lightening and the rim glow on
|
||||||
border-radius:.75rem; background:rgba(0,0,0,.6);
|
hover are what make the dashboard's buttons feel alive; the old flat
|
||||||
|
darken-only hover here read as broken next to /login. */
|
||||||
|
button {{ position:relative; display:inline-flex; align-items:center;
|
||||||
|
justify-content:center; width:100%; min-height:44px; padding:.75rem 1.25rem;
|
||||||
|
border:none; border-radius:.75rem; background:rgba(0,0,0,.6);
|
||||||
backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
|
backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px);
|
||||||
box-shadow:0 8px 24px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.22);
|
box-shadow:0 8px 24px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.22);
|
||||||
color:rgba(255,255,255,.9); font-size:1rem; font-weight:500; cursor:pointer;
|
color:rgba(255,255,255,.9); font-size:1rem; font-weight:500; cursor:pointer;
|
||||||
transition:background-color .2s ease, transform .3s cubic-bezier(.4,0,.2,1); }}
|
transition:transform .3s cubic-bezier(.4,0,.2,1), background-color .2s ease,
|
||||||
button:hover {{ background:rgba(0,0,0,.7); }}
|
box-shadow .3s ease; }}
|
||||||
|
button::before {{ content:''; position:absolute; inset:0; border-radius:inherit;
|
||||||
|
padding:2px; background:linear-gradient(135deg, rgba(0,0,0,.8), transparent);
|
||||||
|
-webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite:xor; mask-composite:exclude; pointer-events:none; }}
|
||||||
|
button:hover {{ transform:translateY(-2px); background:rgba(0,0,0,.35);
|
||||||
|
box-shadow:0 12px 32px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.25); }}
|
||||||
|
button:hover::before {{ background:linear-gradient(135deg, rgba(255,255,255,.3), transparent); }}
|
||||||
button:active {{ transform:translateY(1px); }}
|
button:active {{ transform:translateY(1px); }}
|
||||||
|
button:disabled {{ opacity:.5; cursor:not-allowed; transform:none; }}
|
||||||
|
/* Two faces per button; the submit-feedback script flips .loading on. */
|
||||||
|
button .busy {{ display:none; }}
|
||||||
|
button.loading .idle {{ display:none; }}
|
||||||
|
button.loading .busy {{ display:inline-flex; align-items:center; gap:.5rem; }}
|
||||||
|
.spin {{ width:1.25rem; height:1.25rem; animation:spin 1s linear infinite; }}
|
||||||
|
@keyframes spin {{ to {{ transform:rotate(360deg); }} }}
|
||||||
.err {{ background:rgba(239,68,68,.2); border:1px solid rgba(239,68,68,.4);
|
.err {{ background:rgba(239,68,68,.2); border:1px solid rgba(239,68,68,.4);
|
||||||
color:#fecaca; padding:.75rem; border-radius:.5rem; margin-bottom:1rem;
|
color:#fecaca; padding:.75rem; border-radius:.5rem; margin-bottom:1rem;
|
||||||
font-size:.875rem; text-align:left; }}
|
font-size:.875rem; text-align:left; }}
|
||||||
</style></head>
|
</style></head>
|
||||||
<body>{backgrounds}<main><div class="card">{body}</div></main></body></html>"#,
|
<body>{backgrounds}<main><div class="card">{body}</div></main>
|
||||||
|
<script>{submit_feedback}</script></body></html>"#,
|
||||||
title = esc(title),
|
title = esc(title),
|
||||||
app_name = esc(&app.app_name),
|
app_name = esc(&app.app_name),
|
||||||
body = body,
|
body = body,
|
||||||
|
submit_feedback = SUBMIT_FEEDBACK_JS,
|
||||||
backgrounds = background_layers(),
|
backgrounds = background_layers(),
|
||||||
cycle = LOGIN_BACKGROUNDS.len() as u32 * 9,
|
cycle = LOGIN_BACKGROUNDS.len() as u32 * 9,
|
||||||
hold = 100 / LOGIN_BACKGROUNDS.len() as u32,
|
hold = 100 / LOGIN_BACKGROUNDS.len() as u32,
|
||||||
@@ -961,10 +1055,16 @@ button:active {{ transform:translateY(1px); }}
|
|||||||
// login, on any port or scheme, which is exactly the dashboard.
|
// login, on any port or scheme, which is exactly the dashboard.
|
||||||
// Anything else — another site embedding it to harvest the node
|
// Anything else — another site embedding it to harvest the node
|
||||||
// password — is still refused.
|
// password — is still refused.
|
||||||
|
// script-src admits exactly one script, by hash: the submit-feedback
|
||||||
|
// snippet above. Injected markup (an app name, an error string) still
|
||||||
|
// cannot execute — its hash would not match.
|
||||||
.header(
|
.header(
|
||||||
"Content-Security-Policy",
|
"Content-Security-Policy",
|
||||||
|
format!(
|
||||||
"default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; \
|
"default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; \
|
||||||
form-action 'self'; frame-ancestors 'self' http://*:* https://*:*",
|
script-src {hash}; form-action 'self'; frame-ancestors 'self' http://*:* https://*:*",
|
||||||
|
hash = submit_feedback_csp_hash(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.body(Body::from(html))
|
.body(Body::from(html))
|
||||||
.expect("static response builds")
|
.expect("static response builds")
|
||||||
@@ -975,15 +1075,17 @@ button:active {{ transform:translateY(1px); }}
|
|||||||
/// password by an unexplained page.
|
/// password by an unexplained page.
|
||||||
fn login_page(app: &GatedPort, error: Option<&str>, status: StatusCode) -> Response<Body> {
|
fn login_page(app: &GatedPort, error: Option<&str>, status: StatusCode) -> Response<Body> {
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"<div class="logo"><img src="{prefix}asset/favico-black-v2.svg" alt="Archipelago"></div>
|
r#"{logo}
|
||||||
{icon}
|
{icon}
|
||||||
<h1>Sign in to open {name}</h1>
|
<h1>Sign in to open {name}</h1>
|
||||||
<p class="sub">This app is protected by your node password.</p>
|
<p class="sub">This app is protected by your node password.</p>
|
||||||
{err}
|
{err}
|
||||||
<form method="post" action="{prefix}login">
|
<form method="post" action="{prefix}login">
|
||||||
<input type="password" name="password" placeholder="Node password" autocomplete="current-password" autofocus required>
|
<input type="password" name="password" placeholder="Node password" autocomplete="current-password" autofocus required>
|
||||||
<button type="submit">Sign in</button>
|
<button type="submit"><span class="idle">Sign in</span><span class="busy">{spinner}Signing in…</span></button>
|
||||||
</form>"#,
|
</form>"#,
|
||||||
|
logo = logo_markup(),
|
||||||
|
spinner = SPINNER_SVG,
|
||||||
icon = icon_markup(app),
|
icon = icon_markup(app),
|
||||||
name = esc(&app.app_name),
|
name = esc(&app.app_name),
|
||||||
err = error
|
err = error
|
||||||
@@ -1004,8 +1106,9 @@ fn totp_page(app: &GatedPort, error: Option<&str>, status: StatusCode) -> Respon
|
|||||||
{err}
|
{err}
|
||||||
<form method="post" action="{prefix}totp">
|
<form method="post" action="{prefix}totp">
|
||||||
<input type="text" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" placeholder="000000" autofocus required>
|
<input type="text" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" placeholder="000000" autofocus required>
|
||||||
<button type="submit">Verify</button>
|
<button type="submit"><span class="idle">Verify</span><span class="busy">{spinner}Verifying…</span></button>
|
||||||
</form>"#,
|
</form>"#,
|
||||||
|
spinner = SPINNER_SVG,
|
||||||
icon = icon_markup(app),
|
icon = icon_markup(app),
|
||||||
name = esc(&app.app_name),
|
name = esc(&app.app_name),
|
||||||
err = error
|
err = error
|
||||||
@@ -1200,25 +1303,48 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The login page must render entirely from the gate's own origin: the
|
/// The login page must render entirely from the gate's own origin: the
|
||||||
/// CSP allows no external host, so a background or logo that 404s leaves
|
/// CSP allows no external host, so a background that 404s leaves a black
|
||||||
/// a black page rather than the dashboard's art.
|
/// page rather than the dashboard's art. The badge itself is inline SVG —
|
||||||
|
/// the same 20 squares as the dashboard login's AnimatedLogo — so it can
|
||||||
|
/// never 404 at all.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn login_page_sources_its_art_from_the_gate() {
|
async fn login_page_sources_its_art_from_the_gate() {
|
||||||
let resp = login_page(&app(), None, StatusCode::UNAUTHORIZED);
|
let resp = login_page(&app(), None, StatusCode::UNAUTHORIZED);
|
||||||
let body = hyper::body::to_bytes(resp.into_body()).await.unwrap();
|
let body = hyper::body::to_bytes(resp.into_body()).await.unwrap();
|
||||||
let html = String::from_utf8_lossy(&body).to_string();
|
let html = String::from_utf8_lossy(&body).to_string();
|
||||||
assert!(html.contains(&format!("{GATE_PREFIX}asset/favico-black-v2.svg")));
|
assert_eq!(
|
||||||
|
html.matches(r#"class="sq""#).count(),
|
||||||
|
LOGO_RECTS.len(),
|
||||||
|
"the badge must draw every AnimatedLogo square inline"
|
||||||
|
);
|
||||||
for name in LOGIN_BACKGROUNDS {
|
for name in LOGIN_BACKGROUNDS {
|
||||||
assert!(
|
assert!(
|
||||||
html.contains(&format!("{GATE_PREFIX}asset/{name}")),
|
html.contains(&format!("{GATE_PREFIX}asset/{name}")),
|
||||||
"background {name} is not referenced"
|
"background {name} is not referenced"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Every referenced asset must be one the gate will actually serve.
|
}
|
||||||
// The logo is the sidebar A mark (favico-black-v2.svg) since the
|
|
||||||
// 2026-08-05 login-page rework — the old wordmark is off the
|
/// The only script the challenge pages may run is the submit-feedback
|
||||||
// allowlist on purpose.
|
/// snippet, admitted by hash. The page must carry exactly that script,
|
||||||
assert!(read_ui_asset("favico-black-v2.svg").is_some() || cfg!(not(debug_assertions)));
|
/// and the CSP must name its hash — anything injected has a different
|
||||||
|
/// hash and stays inert.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn submit_feedback_script_is_present_and_hash_pinned() {
|
||||||
|
let resp = login_page(&app(), None, StatusCode::UNAUTHORIZED);
|
||||||
|
let csp = resp.headers()["Content-Security-Policy"]
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
assert!(csp.contains(&format!("script-src {}", submit_feedback_csp_hash())));
|
||||||
|
assert!(!csp.contains("script-src 'unsafe-inline'"));
|
||||||
|
let body = hyper::body::to_bytes(resp.into_body()).await.unwrap();
|
||||||
|
let html = String::from_utf8_lossy(&body);
|
||||||
|
assert!(html.contains(&format!("<script>{SUBMIT_FEEDBACK_JS}</script>")));
|
||||||
|
// Both button faces render: idle label and the spinner face.
|
||||||
|
assert!(html.contains(r#"<span class="idle">Sign in</span>"#));
|
||||||
|
assert!(html.contains("Signing in…"));
|
||||||
|
assert!(html.contains(r#"class="spin""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The allowlist is the whole security boundary for asset serving: the
|
/// The allowlist is the whole security boundary for asset serving: the
|
||||||
|
|||||||
@@ -171,6 +171,30 @@ pub async fn ensure_doctor_installed() {
|
|||||||
Ok(false) => debug!("tor-helper.sh already current"),
|
Ok(false) => debug!("tor-helper.sh already current"),
|
||||||
Err(e) => warn!("tor-helper sync failed (non-fatal): {:#}", e),
|
Err(e) => warn!("tor-helper sync failed (non-fatal): {:#}", e),
|
||||||
}
|
}
|
||||||
|
match run_welcome_banner_sync().await {
|
||||||
|
Ok(true) => info!(
|
||||||
|
"Console welcome banner synchronized (LAN address + .local name, not the WG tunnel IP)"
|
||||||
|
),
|
||||||
|
Ok(false) => debug!("Console welcome banner already current (or not an ISO node)"),
|
||||||
|
Err(e) => warn!("Welcome banner sync failed (non-fatal): {:#}", e),
|
||||||
|
}
|
||||||
|
match run_nginx_listener_repair().await {
|
||||||
|
Ok(true) => info!("nginx HTTPS listeners retargeted to this host's current addresses"),
|
||||||
|
Ok(false) => debug!("nginx listeners already match this host's addresses"),
|
||||||
|
Err(e) => warn!("nginx listener repair failed (non-fatal): {:#}", e),
|
||||||
|
}
|
||||||
|
match run_ha_rpc_proxy_bind_repair().await {
|
||||||
|
Ok(true) => info!(
|
||||||
|
"HA bitcoind RPC forwarder rebound dynamically — survives network moves now"
|
||||||
|
),
|
||||||
|
Ok(false) => debug!("HA bitcoind RPC forwarder absent or already dynamic"),
|
||||||
|
Err(e) => warn!("HA RPC forwarder bind repair failed (non-fatal): {:#}", e),
|
||||||
|
}
|
||||||
|
match run_pull_never_image_repair().await {
|
||||||
|
Ok(n) if n > 0 => info!(retagged = n, "Healed quadlet image refs orphaned by registry rename"),
|
||||||
|
Ok(_) => debug!("All quadlet image refs resolve locally"),
|
||||||
|
Err(e) => warn!("Quadlet image ref repair failed (non-fatal): {:#}", e),
|
||||||
|
}
|
||||||
match run_tor_torrc_repair().await {
|
match run_tor_torrc_repair().await {
|
||||||
Ok(true) => info!("Tor healed at boot (torrc rebuilt and/or daemon restarted)"),
|
Ok(true) => info!("Tor healed at boot (torrc rebuilt and/or daemon restarted)"),
|
||||||
Ok(false) => debug!("Tor healthy and torrc in sync — no heal needed"),
|
Ok(false) => debug!("Tor healthy and torrc in sync — no heal needed"),
|
||||||
@@ -654,6 +678,352 @@ exit 2
|
|||||||
const TOR_HELPER_SH: &str = include_str!("../../../scripts/tor-helper.sh");
|
const TOR_HELPER_SH: &str = include_str!("../../../scripts/tor-helper.sh");
|
||||||
const TOR_HELPER_PATH: &str = "/opt/archipelago/scripts/tor-helper.sh";
|
const TOR_HELPER_PATH: &str = "/opt/archipelago/scripts/tor-helper.sh";
|
||||||
|
|
||||||
|
/// Heal socat forwarder units that were generated with the node's LAN IP
|
||||||
|
/// baked into `bind=`.
|
||||||
|
///
|
||||||
|
/// `archy-ha-btc-rpc-proxy.service` (written on-node during the Pine/HA
|
||||||
|
/// integration) bound socat to the box's DHCP address at generation time —
|
||||||
|
/// pasta containers reach the host via its LAN address, so that was the
|
||||||
|
/// address that worked. Move the box to a new network and the address no
|
||||||
|
/// longer exists: `bind()` fails and the unit restart-loops forever
|
||||||
|
/// (framework-pt after relocating, 2026-08-15: restart counter 2446, and
|
||||||
|
/// Home Assistant's bitcoind sensor dead with it).
|
||||||
|
///
|
||||||
|
/// The rewrite computes the bind address at every service start instead, so
|
||||||
|
/// `Restart=always` itself becomes the heal: plug the box into any network
|
||||||
|
/// and the next restart binds to the new address.
|
||||||
|
const HA_RPC_PROXY_UNIT_PATH: &str = "/etc/systemd/system/archy-ha-btc-rpc-proxy.service";
|
||||||
|
|
||||||
|
/// Parse `TCP-LISTEN:<port>,bind=<ipv4>` + trailing `TCP:<target>` out of a
|
||||||
|
/// socat ExecStart line. Returns (listen_port, target).
|
||||||
|
fn parse_socat_static_bind(exec_line: &str) -> Option<(String, String)> {
|
||||||
|
let after_listen = exec_line.split("TCP-LISTEN:").nth(1)?;
|
||||||
|
let port = after_listen.split(',').next()?.trim();
|
||||||
|
if port.is_empty() || !port.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Only rewrite units pinned to a concrete address; a unit already using
|
||||||
|
// a computed bind (or none) needs no heal.
|
||||||
|
let bind = after_listen.split("bind=").nth(1)?.split(',').next()?.trim();
|
||||||
|
if !bind.chars().all(|c| c.is_ascii_digit() || c == '.') || bind.starts_with("127.") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let target = exec_line.rsplit(" TCP:").next()?.trim();
|
||||||
|
if target.is_empty() || target == exec_line {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((port.to_string(), target.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dynamic_bind_execstart(listen_port: &str, target: &str) -> String {
|
||||||
|
// `$$` survives systemd's own expansion as a literal `$`, so the command
|
||||||
|
// substitution runs in the shell at ExecStart time. If the box has no
|
||||||
|
// default route yet, exit non-zero and let Restart=always retry.
|
||||||
|
format!(
|
||||||
|
"ExecStart=/bin/sh -c 'IP=$$(ip -4 route get 1.1.1.1 | sed -n \"s/.*src \\([0-9.]*\\).*/\\1/p\"); \
|
||||||
|
[ -n \"$$IP\" ] || exit 1; \
|
||||||
|
exec /usr/bin/socat TCP-LISTEN:{listen_port},bind=$$IP,fork,reuseaddr TCP:{target}'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_ha_rpc_proxy_bind_repair() -> Result<bool> {
|
||||||
|
let unit = match tokio::fs::read_to_string(HA_RPC_PROXY_UNIT_PATH).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return Ok(false), // node never grew the forwarder
|
||||||
|
};
|
||||||
|
let Some(exec_line) = unit.lines().find(|l| l.trim_start().starts_with("ExecStart=")) else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
let Some((port, target)) = parse_socat_static_bind(exec_line) else {
|
||||||
|
return Ok(false); // already dynamic (or not the shape we heal)
|
||||||
|
};
|
||||||
|
let healed = unit.replace(exec_line, &dynamic_bind_execstart(&port, &target));
|
||||||
|
let staged = "/var/lib/archipelago/ha-rpc-proxy.staged";
|
||||||
|
if let Some(dir) = Path::new(staged).parent() {
|
||||||
|
tokio::fs::create_dir_all(dir).await.ok();
|
||||||
|
}
|
||||||
|
tokio::fs::write(staged, &healed)
|
||||||
|
.await
|
||||||
|
.context("stage ha-rpc-proxy unit")?;
|
||||||
|
let script = format!(
|
||||||
|
"set -eu\ninstall -m 0644 {staged} {dest}\nsystemctl daemon-reload\nsystemctl restart archy-ha-btc-rpc-proxy 2>/dev/null || true\nexit 0\n",
|
||||||
|
staged = staged,
|
||||||
|
dest = HA_RPC_PROXY_UNIT_PATH
|
||||||
|
);
|
||||||
|
host_sudo(&["sh", "-lc", &script])
|
||||||
|
.await
|
||||||
|
.context("install ha-rpc-proxy unit")?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-point `--pull never` quadlets whose image ref no longer matches local
|
||||||
|
/// storage.
|
||||||
|
///
|
||||||
|
/// The catalog signing pass rewrites image refs (bare-IP registry → domain),
|
||||||
|
/// so a quadlet regenerated with the new ref points at an image the local
|
||||||
|
/// store only holds under the old name. With `--pull never` the app can
|
||||||
|
/// never start again on its own — Home Assistant looped 761 restarts on
|
||||||
|
/// "image not known" (framework-pt, 2026-08-15) while an identical
|
||||||
|
/// `name:tag` sat in storage under the bare-IP ref. If any local image
|
||||||
|
/// shares the wanted `name:tag`, retag it; pulling is deliberately NOT
|
||||||
|
/// attempted here (offline nodes, metered links — the doctor handles pulls).
|
||||||
|
async fn run_pull_never_image_repair() -> Result<usize> {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string());
|
||||||
|
let quadlet_dir = format!("{home}/.config/containers/systemd");
|
||||||
|
let mut wanted: Vec<String> = Vec::new();
|
||||||
|
let mut entries = match tokio::fs::read_dir(&quadlet_dir).await {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => return Ok(0),
|
||||||
|
};
|
||||||
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|e| e.to_str()) != Some("container") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(text) = tokio::fs::read_to_string(&path).await else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for line in text.lines() {
|
||||||
|
if let Some(image) = line.trim().strip_prefix("Image=") {
|
||||||
|
let image = image.trim();
|
||||||
|
if !image.is_empty() {
|
||||||
|
wanted.push(image.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if wanted.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let local = podman_stdout(&["images", "--format", "{{.Repository}}:{{.Tag}}"]).await;
|
||||||
|
let local: Vec<&str> = local
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|l| !l.is_empty() && !l.contains("<none>"))
|
||||||
|
.collect();
|
||||||
|
let mut retagged = 0usize;
|
||||||
|
for want in wanted {
|
||||||
|
if local.iter().any(|l| *l == want) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Same `name:tag`, any registry prefix, is the rename we heal.
|
||||||
|
let Some(name_tag) = want.rsplit('/').next() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !name_tag.contains(':') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let suffix = format!("/{name_tag}");
|
||||||
|
let Some(src) = local.iter().find(|l| l.ends_with(&suffix)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let status = tokio::process::Command::new("podman")
|
||||||
|
.args(["tag", src, &want])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
|
match status {
|
||||||
|
Ok(s) if s.success() => {
|
||||||
|
info!(from = %src, to = %want, "Retagged image for a --pull never quadlet");
|
||||||
|
retagged += 1;
|
||||||
|
}
|
||||||
|
_ => warn!(from = %src, to = %want, "Image retag failed (non-fatal)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(retagged)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn podman_stdout(args: &[&str]) -> String {
|
||||||
|
match tokio::process::Command::new("podman").args(args).output().await {
|
||||||
|
Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).into_owned(),
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep nginx's per-address HTTPS listeners in step with the addresses the
|
||||||
|
/// host actually has, and get nginx running if a boot race killed it.
|
||||||
|
///
|
||||||
|
/// `scripts/setup-node-ca.sh` writes one `listen <addr>:443 ssl;` per LAN
|
||||||
|
/// address at the moment it runs (per-address rather than wildcard on
|
||||||
|
/// purpose: Tailscale holds :443 on the tailnet address). Its idempotency
|
||||||
|
/// guard then never revisits them. Two ways that takes the WHOLE web UI
|
||||||
|
/// down — nginx refuses to start if any listen address is missing, so this
|
||||||
|
/// is not merely an HTTPS outage:
|
||||||
|
/// 1. The node moves networks and the old address no longer exists.
|
||||||
|
/// 2. Even in place, nginx starts before DHCP has assigned the address —
|
||||||
|
/// and nginx.service ships no `Restart=`, so that single failure is
|
||||||
|
/// permanent until a human intervenes.
|
||||||
|
/// Both observed on archi-dev-box, 2026-08-15: nginx dead since boot with
|
||||||
|
/// `bind() to 192.168.63.240:443 failed (99: Cannot assign requested
|
||||||
|
/// address)`, and the dashboard simply unreachable.
|
||||||
|
const NGINX_SITES: [&str; 2] = [
|
||||||
|
"/etc/nginx/sites-available/archipelago-http",
|
||||||
|
"/etc/nginx/sites-available/archipelago",
|
||||||
|
];
|
||||||
|
const NGINX_RESTART_DROPIN: &str = "/etc/systemd/system/nginx.service.d/10-archipelago-restart.conf";
|
||||||
|
|
||||||
|
/// Global IPv4 addresses on this host, minus Tailscale CGNAT (100.64/10) —
|
||||||
|
/// the same exclusion `setup-node-ca.sh` applies, for the same reason.
|
||||||
|
async fn host_lan_addrs() -> Vec<String> {
|
||||||
|
let out = tokio::process::Command::new("ip")
|
||||||
|
.args(["-o", "-4", "addr", "show", "scope", "global"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
let Ok(out) = out else { return Vec::new() };
|
||||||
|
String::from_utf8_lossy(&out.stdout)
|
||||||
|
.lines()
|
||||||
|
.filter_map(|l| l.split_whitespace().nth(3))
|
||||||
|
.filter_map(|cidr| cidr.split('/').next())
|
||||||
|
.filter(|a| !is_cgnat(a))
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_cgnat(addr: &str) -> bool {
|
||||||
|
let mut parts = addr.split('.');
|
||||||
|
let (Some(100), Some(second)) = (
|
||||||
|
parts.next().and_then(|p| p.parse::<u8>().ok()),
|
||||||
|
parts.next().and_then(|p| p.parse::<u8>().ok()),
|
||||||
|
) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
(64..=127).contains(&second)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewrite the `listen <ip>:443 ssl;` set for one config's text. Returns the
|
||||||
|
/// new text when it differs. Lines for absent addresses are dropped and one
|
||||||
|
/// line per present address is kept, preserving the file's indentation.
|
||||||
|
fn retarget_https_listeners(text: &str, present: &[String]) -> Option<String> {
|
||||||
|
let listen_of = |l: &str| -> Option<String> {
|
||||||
|
let t = l.trim();
|
||||||
|
let rest = t.strip_prefix("listen ")?.strip_suffix(":443 ssl;")?;
|
||||||
|
// Only per-address listeners; `listen 443 ssl ...` has no address.
|
||||||
|
rest.split('.').count().eq(&4).then(|| rest.to_string())
|
||||||
|
};
|
||||||
|
if !text.lines().any(|l| listen_of(l).is_some()) {
|
||||||
|
return None; // wildcard-only config; nothing address-pinned to heal
|
||||||
|
}
|
||||||
|
let stale: Vec<String> = text
|
||||||
|
.lines()
|
||||||
|
.filter_map(listen_of)
|
||||||
|
.filter(|a| !present.contains(a))
|
||||||
|
.collect();
|
||||||
|
let existing: Vec<String> = text.lines().filter_map(listen_of).collect();
|
||||||
|
let missing: Vec<&String> = present.iter().filter(|a| !existing.contains(a)).collect();
|
||||||
|
if stale.is_empty() && missing.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let indent = text
|
||||||
|
.lines()
|
||||||
|
.find(|l| listen_of(l).is_some())
|
||||||
|
.map(|l| l[..l.len() - l.trim_start().len()].to_string())
|
||||||
|
.unwrap_or_else(|| " ".to_string());
|
||||||
|
let mut out: Vec<String> = Vec::new();
|
||||||
|
let mut wrote_block = false;
|
||||||
|
for line in text.lines() {
|
||||||
|
match listen_of(line) {
|
||||||
|
Some(_) if !wrote_block => {
|
||||||
|
wrote_block = true;
|
||||||
|
for a in present {
|
||||||
|
out.push(format!("{indent}listen {a}:443 ssl;"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {} // subsequent old listen lines are replaced by the block
|
||||||
|
None => out.push(line.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(out.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_nginx_listener_repair() -> Result<bool> {
|
||||||
|
let present = host_lan_addrs().await;
|
||||||
|
if present.is_empty() {
|
||||||
|
return Ok(false); // no network yet; a later boot pass will do it
|
||||||
|
}
|
||||||
|
let mut changed = false;
|
||||||
|
for site in NGINX_SITES {
|
||||||
|
let Ok(text) = tokio::fs::read_to_string(site).await else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(healed) = retarget_https_listeners(&text, &present) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let staged = "/var/lib/archipelago/nginx-listeners.staged";
|
||||||
|
if let Some(dir) = Path::new(staged).parent() {
|
||||||
|
tokio::fs::create_dir_all(dir).await.ok();
|
||||||
|
}
|
||||||
|
tokio::fs::write(staged, &healed)
|
||||||
|
.await
|
||||||
|
.context("stage nginx listeners")?;
|
||||||
|
// Install behind `nginx -t`, and roll back if the test fails — a bad
|
||||||
|
// config here would take the dashboard down, which is the very
|
||||||
|
// failure this repair exists to prevent.
|
||||||
|
let script = format!(
|
||||||
|
"set -eu\ncp {site} {site}.bak-listeners\ninstall -m 0644 {staged} {site}\n\
|
||||||
|
if ! nginx -t 2>/dev/null; then cp {site}.bak-listeners {site}; exit 3; fi\nexit 0\n"
|
||||||
|
);
|
||||||
|
let status = host_sudo(&["sh", "-lc", &script]).await?;
|
||||||
|
match status.code() {
|
||||||
|
Some(0) => changed = true,
|
||||||
|
Some(3) => warn!(site, "nginx listener repair failed its config test — rolled back"),
|
||||||
|
_ => warn!(site, "nginx listener repair helper failed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Whether or not the config changed: if nginx is down (the boot race, or
|
||||||
|
// it died on an address that has since arrived), start it. And give it a
|
||||||
|
// restart policy so the race stops being fatal in the first place.
|
||||||
|
let script = format!(
|
||||||
|
"set -eu\nmkdir -p $(dirname {dropin})\n\
|
||||||
|
cat > {dropin} <<'EOF'\n[Service]\nRestart=on-failure\nRestartSec=5\n\
|
||||||
|
[Unit]\nStartLimitIntervalSec=300\nStartLimitBurst=10\nEOF\n\
|
||||||
|
systemctl daemon-reload\n\
|
||||||
|
if ! systemctl is-active --quiet nginx; then systemctl reset-failed nginx 2>/dev/null || true; systemctl start nginx 2>/dev/null || true; \
|
||||||
|
elif [ \"${{RELOAD:-1}}\" = 1 ]; then systemctl reload nginx 2>/dev/null || true; fi\nexit 0\n",
|
||||||
|
dropin = NGINX_RESTART_DROPIN
|
||||||
|
);
|
||||||
|
host_sudo(&["sh", "-lc", &script])
|
||||||
|
.await
|
||||||
|
.context("nginx restart policy + start")?;
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The console welcome banner, embedded so the OTA can fix it on deployed
|
||||||
|
/// nodes. `/etc/profile.d/archipelago.sh` is baked by the ISO installer and
|
||||||
|
/// no OTA path touched it, so every node kept whatever its ISO generation
|
||||||
|
/// shipped — including banners that print the node's own WireGuard address
|
||||||
|
/// (10.44.0.1, present on EVERY node) as the "web ui", which is unreachable
|
||||||
|
/// off-tunnel and actively misleading after a move to a new network
|
||||||
|
/// (framework-pt, 2026-08-15). Canonical copy: scripts/welcome-banner.sh;
|
||||||
|
/// the ISO builder inlines the same content for fresh installs.
|
||||||
|
const WELCOME_BANNER_SH: &str = include_str!("../../../scripts/welcome-banner.sh");
|
||||||
|
const WELCOME_BANNER_PATH: &str = "/etc/profile.d/archipelago.sh";
|
||||||
|
|
||||||
|
async fn run_welcome_banner_sync() -> Result<bool> {
|
||||||
|
let current = tokio::fs::read_to_string(WELCOME_BANNER_PATH)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
// Only refresh a banner the installer put there: a dev machine running
|
||||||
|
// the backend from a checkout has no business growing one in /etc.
|
||||||
|
if current.is_empty() || current == WELCOME_BANNER_SH {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let staged = "/var/lib/archipelago/welcome-banner.staged";
|
||||||
|
if let Some(dir) = Path::new(staged).parent() {
|
||||||
|
tokio::fs::create_dir_all(dir).await.ok();
|
||||||
|
}
|
||||||
|
tokio::fs::write(staged, WELCOME_BANNER_SH)
|
||||||
|
.await
|
||||||
|
.context("stage welcome banner")?;
|
||||||
|
let script = format!(
|
||||||
|
"set -eu\ninstall -m 0755 {staged} {dest}\nexit 0\n",
|
||||||
|
staged = staged,
|
||||||
|
dest = WELCOME_BANNER_PATH
|
||||||
|
);
|
||||||
|
host_sudo(&["sh", "-lc", &script])
|
||||||
|
.await
|
||||||
|
.context("install welcome banner")?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_tor_helper_sync() -> Result<bool> {
|
async fn run_tor_helper_sync() -> Result<bool> {
|
||||||
let current = tokio::fs::read_to_string(TOR_HELPER_PATH)
|
let current = tokio::fs::read_to_string(TOR_HELPER_PATH)
|
||||||
.await
|
.await
|
||||||
@@ -1427,6 +1797,74 @@ mod tests {
|
|||||||
heal_stale_web_search_block("location / { try_files $uri /index.html; }").is_none()
|
heal_stale_web_search_block("location / { try_files $uri /index.html; }").is_none()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The exact ExecStart framework-pt shipped with must parse, and the
|
||||||
|
/// rewrite must preserve its listen port and forward target.
|
||||||
|
#[test]
|
||||||
|
fn static_socat_bind_is_parsed_and_rewritten_dynamically() {
|
||||||
|
let line = "ExecStart=/usr/bin/socat TCP-LISTEN:18332,bind=192.168.1.249,fork,reuseaddr TCP:127.0.0.1:8332";
|
||||||
|
let (port, target) = parse_socat_static_bind(line).expect("must parse");
|
||||||
|
assert_eq!(port, "18332");
|
||||||
|
assert_eq!(target, "127.0.0.1:8332");
|
||||||
|
let dynamic = dynamic_bind_execstart(&port, &target);
|
||||||
|
assert!(dynamic.contains("TCP-LISTEN:18332,bind=$$IP"));
|
||||||
|
assert!(dynamic.contains("TCP:127.0.0.1:8332"));
|
||||||
|
assert!(dynamic.contains("route get 1.1.1.1"));
|
||||||
|
// The heal is idempotent: its own output no longer parses as a
|
||||||
|
// static bind (bind=$$IP is not a concrete address).
|
||||||
|
assert!(parse_socat_static_bind(&dynamic).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The archi-dev-box config: one stale address (old network) beside the
|
||||||
|
/// WireGuard one. The stale listener must go — nginx refuses to START
|
||||||
|
/// while it names an address the host lacks — and the current LAN
|
||||||
|
/// address must appear.
|
||||||
|
#[test]
|
||||||
|
fn stale_https_listeners_are_retargeted_to_present_addresses() {
|
||||||
|
let cfg = "server {\n listen 80 default_server;\n listen 10.44.0.1:443 ssl;\n listen 192.168.63.240:443 ssl;\n ssl_certificate /x;\n}\n";
|
||||||
|
let present = vec!["10.44.0.1".to_string(), "192.168.1.50".to_string()];
|
||||||
|
let healed = retarget_https_listeners(cfg, &present).expect("must heal");
|
||||||
|
assert!(healed.contains("listen 192.168.1.50:443 ssl;"));
|
||||||
|
assert!(healed.contains("listen 10.44.0.1:443 ssl;"));
|
||||||
|
assert!(!healed.contains("192.168.63.240"), "stale listener must be dropped");
|
||||||
|
// Untouched lines survive, and the repair is idempotent.
|
||||||
|
assert!(healed.contains("listen 80 default_server;"));
|
||||||
|
assert!(healed.contains("ssl_certificate /x;"));
|
||||||
|
assert!(retarget_https_listeners(&healed, &present).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wildcard_only_configs_and_cgnat_are_left_alone() {
|
||||||
|
// No address-pinned listener → nothing to heal (the ISO's own config).
|
||||||
|
assert!(retarget_https_listeners(
|
||||||
|
"server {\n listen 443 ssl default_server;\n}\n",
|
||||||
|
&["192.168.1.50".to_string()]
|
||||||
|
)
|
||||||
|
.is_none());
|
||||||
|
// Tailscale CGNAT must never become an nginx listener: tailscaled
|
||||||
|
// already holds :443 there, and binding it would fail nginx outright.
|
||||||
|
assert!(is_cgnat("100.69.68.39"));
|
||||||
|
assert!(is_cgnat("100.127.255.1"));
|
||||||
|
assert!(!is_cgnat("100.128.0.1"));
|
||||||
|
assert!(!is_cgnat("192.168.1.50"));
|
||||||
|
assert!(!is_cgnat("10.44.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn socat_units_that_need_no_heal_are_left_alone() {
|
||||||
|
// Loopback bind is intentional (Tor bootstrap forwarder) — not ours.
|
||||||
|
assert!(parse_socat_static_bind(
|
||||||
|
"ExecStart=/usr/bin/socat TCP-LISTEN:18332,bind=127.0.0.1,reuseaddr,fork SOCKS4A:127.0.0.1:x.onion:8332,socksport=9050"
|
||||||
|
)
|
||||||
|
.is_none());
|
||||||
|
// No bind at all.
|
||||||
|
assert!(parse_socat_static_bind(
|
||||||
|
"ExecStart=/usr/bin/socat TCP-LISTEN:18332,fork,reuseaddr TCP:127.0.0.1:8332"
|
||||||
|
)
|
||||||
|
.is_none());
|
||||||
|
// Not a socat line.
|
||||||
|
assert!(parse_socat_static_bind("ExecStart=/usr/bin/true").is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Repair this node's own systemd restart policy.
|
/// Repair this node's own systemd restart policy.
|
||||||
|
|||||||
@@ -97,7 +97,15 @@ async fn get_wan_ip() -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if UPnP is available by attempting SSDP discovery.
|
/// Check if UPnP is available by attempting SSDP discovery.
|
||||||
|
///
|
||||||
|
/// The socket is a blocking `std::net::UdpSocket`, and on a network with no
|
||||||
|
/// UPnP gateway — the normal case right after a node moves — the recv runs
|
||||||
|
/// out its full read timeout. Inline on the runtime that parked a tokio
|
||||||
|
/// worker for those 3s on every call, the same failure shape (smaller blast
|
||||||
|
/// radius) as the OpenWrt SSH connect that stalled the API on framework-pt.
|
||||||
|
/// Keep it on the blocking pool.
|
||||||
async fn check_upnp_available() -> bool {
|
async fn check_upnp_available() -> bool {
|
||||||
|
tokio::task::spawn_blocking(|| {
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
|
|
||||||
let ssdp_request = "M-SEARCH * HTTP/1.1\r\n\
|
let ssdp_request = "M-SEARCH * HTTP/1.1\r\n\
|
||||||
@@ -133,6 +141,9 @@ async fn check_upnp_available() -> bool {
|
|||||||
}
|
}
|
||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a port forward (stored locally; actual UPnP mapping done on request).
|
/// Add a port forward (stored locally; actual UPnP mapping done on request).
|
||||||
@@ -291,9 +302,24 @@ async fn check_tor_connectivity() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check DNS resolution works.
|
/// Check DNS resolution works.
|
||||||
|
///
|
||||||
|
/// `to_socket_addrs` is blocking glibc resolution with no app-level bound:
|
||||||
|
/// against a dead or stale resolver — the moved-network case — it can block
|
||||||
|
/// 5–40s (timeout × attempts × nameservers). This runs on every Server-tab
|
||||||
|
/// load via `network.diagnostics`, so inline it parked a tokio worker each
|
||||||
|
/// refresh. Off the runtime, and bounded so the tile reports "no DNS"
|
||||||
|
/// instead of hanging.
|
||||||
async fn check_dns() -> bool {
|
async fn check_dns() -> bool {
|
||||||
|
let probe = tokio::task::spawn_blocking(|| {
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
"cloudflare.com:443".to_socket_addrs().is_ok()
|
"cloudflare.com:443".to_socket_addrs().is_ok()
|
||||||
|
});
|
||||||
|
match tokio::time::timeout(std::time::Duration::from_secs(5), probe).await {
|
||||||
|
Ok(Ok(ok)) => ok,
|
||||||
|
// Timed out or the task failed: the blocking resolve may still be
|
||||||
|
// running on the pool, but the caller is no longer waiting on it.
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Router Compatibility Abstraction ---
|
// --- Router Compatibility Abstraction ---
|
||||||
|
|||||||
@@ -13,10 +13,30 @@ pub struct Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Router {
|
impl Router {
|
||||||
|
/// Bounded TCP connect. The OS default connect timeout against an
|
||||||
|
/// unreachable RFC1918 address is ~2 minutes; a router that stayed
|
||||||
|
/// behind when its node moved networks turned every status poll into a
|
||||||
|
/// worker-thread hostage for that long, stalling unrelated RPCs
|
||||||
|
/// (framework-pt, 2026-08-15 — even TOTP codes expired in flight).
|
||||||
|
/// Read/write timeouts bound the session the same way once connected.
|
||||||
|
fn bounded_tcp(host: &str, port: u16) -> Result<TcpStream> {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
let addr = format!("{}:{}", host, port);
|
||||||
|
let resolved = addr
|
||||||
|
.to_socket_addrs()
|
||||||
|
.with_context(|| format!("resolve {}", addr))?
|
||||||
|
.next()
|
||||||
|
.with_context(|| format!("no address for {}", addr))?;
|
||||||
|
let tcp = TcpStream::connect_timeout(&resolved, std::time::Duration::from_secs(5))
|
||||||
|
.with_context(|| format!("TCP connect to {}", addr))?;
|
||||||
|
tcp.set_read_timeout(Some(std::time::Duration::from_secs(30))).ok();
|
||||||
|
tcp.set_write_timeout(Some(std::time::Duration::from_secs(30))).ok();
|
||||||
|
Ok(tcp)
|
||||||
|
}
|
||||||
|
|
||||||
/// Connect to an OpenWrt router via SSH using a private key.
|
/// Connect to an OpenWrt router via SSH using a private key.
|
||||||
pub fn connect(host: &str, port: u16, user: &str, key_path: &Path) -> Result<Self> {
|
pub fn connect(host: &str, port: u16, user: &str, key_path: &Path) -> Result<Self> {
|
||||||
let addr = format!("{}:{}", host, port);
|
let tcp = Self::bounded_tcp(host, port)?;
|
||||||
let tcp = TcpStream::connect(&addr).with_context(|| format!("TCP connect to {}", addr))?;
|
|
||||||
|
|
||||||
let mut session = Session::new().context("create SSH session")?;
|
let mut session = Session::new().context("create SSH session")?;
|
||||||
session.set_tcp_stream(tcp);
|
session.set_tcp_stream(tcp);
|
||||||
@@ -34,8 +54,7 @@ impl Router {
|
|||||||
|
|
||||||
/// Connect using a password (fallback for routers not yet provisioned with a key).
|
/// Connect using a password (fallback for routers not yet provisioned with a key).
|
||||||
pub fn connect_password(host: &str, port: u16, user: &str, password: &str) -> Result<Self> {
|
pub fn connect_password(host: &str, port: u16, user: &str, password: &str) -> Result<Self> {
|
||||||
let addr = format!("{}:{}", host, port);
|
let tcp = Self::bounded_tcp(host, port)?;
|
||||||
let tcp = TcpStream::connect(&addr).with_context(|| format!("TCP connect to {}", addr))?;
|
|
||||||
|
|
||||||
let mut session = Session::new().context("create SSH session")?;
|
let mut session = Session::new().context("create SSH session")?;
|
||||||
session.set_tcp_stream(tcp);
|
session.set_tcp_stream(tcp);
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
commit=7b82dfc779f94e068ed4d7b2ada39d6fd8f77dce
|
commit=a4be1b4b7d8e50d16ed602d5f9b3f905a048f9a9
|
||||||
built_at=2026-08-09T19:43:11Z
|
built_at=2026-08-14T17:41:17Z
|
||||||
base_path=/aiui/
|
base_path=/aiui/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
.header-overlay-panel[data-v-cce6627c]{background:#000000e0}.picker-enter-active[data-v-cce6627c]{transition:all .2s cubic-bezier(.22,1,.36,1)}.picker-leave-active[data-v-cce6627c]{transition:all .15s ease-in}.picker-enter-from[data-v-cce6627c],.picker-leave-to[data-v-cce6627c]{opacity:0;transform:translateY(-8px)}.context-menu-enter-active[data-v-13d6c372]{transition:all .15s cubic-bezier(.22,1,.36,1)}.context-menu-leave-active[data-v-13d6c372]{transition:all .1s ease-in}.context-menu-enter-from[data-v-13d6c372],.context-menu-leave-to[data-v-13d6c372]{opacity:0;transform:scale(.95)}.settings-modal-enter-active[data-v-c97db749]{transition:opacity .2s ease-out}.settings-modal-enter-active .glass-card[data-v-c97db749]{transition:all .25s cubic-bezier(.22,1,.36,1)}.settings-modal-leave-active[data-v-c97db749]{transition:opacity .15s ease-in}.settings-modal-enter-from[data-v-c97db749],.settings-modal-leave-to[data-v-c97db749]{opacity:0}
|
.header-overlay-panel[data-v-49f33a03]{background:#000000e0}.picker-enter-active[data-v-49f33a03]{transition:all .2s cubic-bezier(.22,1,.36,1)}.picker-leave-active[data-v-49f33a03]{transition:all .15s ease-in}.picker-enter-from[data-v-49f33a03],.picker-leave-to[data-v-49f33a03]{opacity:0;transform:translateY(-8px)}.context-menu-enter-active[data-v-13d6c372]{transition:all .15s cubic-bezier(.22,1,.36,1)}.context-menu-leave-active[data-v-13d6c372]{transition:all .1s ease-in}.context-menu-enter-from[data-v-13d6c372],.context-menu-leave-to[data-v-13d6c372]{opacity:0;transform:scale(.95)}.settings-modal-enter-active[data-v-c97db749]{transition:opacity .2s ease-out}.settings-modal-enter-active .glass-card[data-v-c97db749]{transition:all .25s cubic-bezier(.22,1,.36,1)}.settings-modal-leave-active[data-v-c97db749]{transition:opacity .15s ease-in}.settings-modal-enter-from[data-v-c97db749],.settings-modal-leave-to[data-v-c97db749]{opacity:0}
|
||||||
+40
-40
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{a as S,D as V,c as r,e as s,E as y,G as g,t as c,F as p,H as h,i as b,g as j,I as B,r as u,k as T,J as U,b as l,n as k}from"./index-8cIrvc8q.js";import{useNostr as E}from"./useNostr-XONW-p_l.js";const F={class:"min-h-screen bg-[#0a0a0a] text-white"},H={class:"sticky top-0 z-10 glass border-b border-white/5"},L={class:"max-w-3xl mx-auto px-4 py-3 flex items-center gap-3"},R={class:"flex-1 min-w-0"},z={class:"text-sm font-semibold text-white/90 truncate"},G={class:"text-xs text-white/40"},P={key:0,class:"flex items-center justify-center h-64"},J={key:1,class:"max-w-3xl mx-auto px-4 py-12 text-center"},Y={class:"text-white/40 text-sm"},q={key:2,class:"max-w-3xl mx-auto px-4 py-6 space-y-4"},K={class:"flex items-center gap-2 mb-2"},O=["textContent"],Z=S({__name:"ConversationViewerPage",setup(Q){const C=B(),{connect:N,fetchNote:A}=E(),v=u(!0),i=u(null),f=u("Shared Conversation"),x=u(null),d=u(null),w=u([]),I=T(()=>d.value?new Date(d.value*1e3).toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"}):"");function D(n){const t=[],o=n.split(`
|
import{a as S,D as V,c as r,e as s,E as y,G as g,t as c,F as p,H as h,i as b,g as j,I as B,r as u,k as T,J as U,b as l,n as k}from"./index-DNCGxUDM.js";import{useNostr as E}from"./useNostr-CNDx2L4S.js";const F={class:"min-h-screen bg-[#0a0a0a] text-white"},H={class:"sticky top-0 z-10 glass border-b border-white/5"},L={class:"max-w-3xl mx-auto px-4 py-3 flex items-center gap-3"},R={class:"flex-1 min-w-0"},z={class:"text-sm font-semibold text-white/90 truncate"},G={class:"text-xs text-white/40"},P={key:0,class:"flex items-center justify-center h-64"},J={key:1,class:"max-w-3xl mx-auto px-4 py-12 text-center"},Y={class:"text-white/40 text-sm"},q={key:2,class:"max-w-3xl mx-auto px-4 py-6 space-y-4"},K={class:"flex items-center gap-2 mb-2"},O=["textContent"],Z=S({__name:"ConversationViewerPage",setup(Q){const C=B(),{connect:N,fetchNote:A}=E(),v=u(!0),i=u(null),f=u("Shared Conversation"),x=u(null),d=u(null),w=u([]),I=T(()=>d.value?new Date(d.value*1e3).toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"}):"");function D(n){const t=[],o=n.split(`
|
||||||
`);let e="",a=[];for(const m of o){const _=m.match(/^##?\s*(?:Human|User|You)/),M=m.match(/^##?\s*(?:Assistant|AI|Claude)/);_||M?(e&&a.length>0&&t.push({role:e,content:a.join(`
|
`);let e="",a=[];for(const m of o){const _=m.match(/^##?\s*(?:Human|User|You)/),M=m.match(/^##?\s*(?:Assistant|AI|Claude)/);_||M?(e&&a.length>0&&t.push({role:e,content:a.join(`
|
||||||
`).trim()}),e=_?"user":"assistant",a=[]):a.push(m)}return e&&a.length>0&&t.push({role:e,content:a.join(`
|
`).trim()}),e=_?"user":"assistant",a=[]):a.push(m)}return e&&a.length>0&&t.push({role:e,content:a.join(`
|
||||||
`).trim()}),t.length===0&&n.trim()&&t.push({role:"assistant",content:n.trim()}),t}return V(async()=>{try{const n=C.params.nostrAddr;if(!n){i.value="No Nostr address provided.";return}await N();let t=null;try{const e=atob(n).split(":");e.length>=2&&(t={dTag:e[0],pubkey:e[1]})}catch{}if(t){const o=await A(t.dTag);if(o){const e=o.tags.find(a=>a[0]==="title");e&&(f.value=e[1]),x.value=o.authorName??null,d.value=o.created_at,w.value=D(o.content)}else i.value="Conversation not found on relays."}else i.value="Invalid Nostr address format."}catch(n){i.value=n instanceof Error?n.message:"Failed to load conversation."}finally{v.value=!1}}),(n,t)=>{const o=U("router-link");return l(),r("div",F,[s("header",H,[s("div",L,[y(o,{to:"/",class:"text-white/40 hover:text-white/70 transition-colors"},{default:g(()=>[...t[0]||(t[0]=[s("svg",{class:"w-5 h-5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor","stroke-width":"2"},[s("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M10 19l-7-7m0 0l7-7m-7 7h18"})],-1)])]),_:1}),s("div",R,[s("h1",z,c(f.value),1),s("p",G,[x.value?(l(),r(p,{key:0},[h("by "+c(x.value),1)],64)):b("",!0),d.value?(l(),r(p,{key:1},[h(" · "+c(I.value),1)],64)):b("",!0)])]),t[1]||(t[1]=s("span",{class:"text-xs px-2 py-1 rounded-full bg-white/5 text-white/40"},"Read-only",-1))])]),v.value?(l(),r("div",P,[...t[2]||(t[2]=[s("div",{class:"w-6 h-6 rounded-full border-2 border-accent/30 border-t-accent animate-spin"},null,-1)])])):i.value?(l(),r("div",J,[s("p",Y,c(i.value),1),y(o,{to:"/",class:"mt-4 inline-block text-accent text-sm hover:underline"},{default:g(()=>[...t[3]||(t[3]=[h(" Go to AIUI ",-1)])]),_:1})])):(l(),r("main",q,[(l(!0),r(p,null,j(w.value,(e,a)=>(l(),r("div",{key:a,class:k(["rounded-xl p-4",e.role==="user"?"bg-white/[0.03] border border-white/5 ml-8":"mr-8"])},[s("div",K,[s("span",{class:k(["text-xs font-bold uppercase tracking-wider",e.role==="user"?"text-accent/70":"text-white/30"])},c(e.role==="user"?"Human":"Assistant"),3)]),s("div",{class:"text-sm text-white/80 leading-relaxed whitespace-pre-wrap break-words",textContent:c(e.content)},null,8,O)],2))),128))])),t[4]||(t[4]=s("footer",{class:"max-w-3xl mx-auto px-4 py-8 text-center"},[s("p",{class:"text-xs text-white/20"}," Shared via AIUI · Powered by Nostr ")],-1))])}}});export{Z as default};
|
`).trim()}),t.length===0&&n.trim()&&t.push({role:"assistant",content:n.trim()}),t}return V(async()=>{try{const n=C.params.nostrAddr;if(!n){i.value="No Nostr address provided.";return}await N();let t=null;try{const e=atob(n).split(":");e.length>=2&&(t={dTag:e[0],pubkey:e[1]})}catch{}if(t){const o=await A(t.dTag);if(o){const e=o.tags.find(a=>a[0]==="title");e&&(f.value=e[1]),x.value=o.authorName??null,d.value=o.created_at,w.value=D(o.content)}else i.value="Conversation not found on relays."}else i.value="Invalid Nostr address format."}catch(n){i.value=n instanceof Error?n.message:"Failed to load conversation."}finally{v.value=!1}}),(n,t)=>{const o=U("router-link");return l(),r("div",F,[s("header",H,[s("div",L,[y(o,{to:"/",class:"text-white/40 hover:text-white/70 transition-colors"},{default:g(()=>[...t[0]||(t[0]=[s("svg",{class:"w-5 h-5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor","stroke-width":"2"},[s("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M10 19l-7-7m0 0l7-7m-7 7h18"})],-1)])]),_:1}),s("div",R,[s("h1",z,c(f.value),1),s("p",G,[x.value?(l(),r(p,{key:0},[h("by "+c(x.value),1)],64)):b("",!0),d.value?(l(),r(p,{key:1},[h(" · "+c(I.value),1)],64)):b("",!0)])]),t[1]||(t[1]=s("span",{class:"text-xs px-2 py-1 rounded-full bg-white/5 text-white/40"},"Read-only",-1))])]),v.value?(l(),r("div",P,[...t[2]||(t[2]=[s("div",{class:"w-6 h-6 rounded-full border-2 border-accent/30 border-t-accent animate-spin"},null,-1)])])):i.value?(l(),r("div",J,[s("p",Y,c(i.value),1),y(o,{to:"/",class:"mt-4 inline-block text-accent text-sm hover:underline"},{default:g(()=>[...t[3]||(t[3]=[h(" Go to AIUI ",-1)])]),_:1})])):(l(),r("main",q,[(l(!0),r(p,null,j(w.value,(e,a)=>(l(),r("div",{key:a,class:k(["rounded-xl p-4",e.role==="user"?"bg-white/[0.03] border border-white/5 ml-8":"mr-8"])},[s("div",K,[s("span",{class:k(["text-xs font-bold uppercase tracking-wider",e.role==="user"?"text-accent/70":"text-white/30"])},c(e.role==="user"?"Human":"Assistant"),3)]),s("div",{class:"text-sm text-white/80 leading-relaxed whitespace-pre-wrap break-words",textContent:c(e.content)},null,8,O)],2))),128))])),t[4]||(t[4]=s("footer",{class:"max-w-3xl mx-auto px-4 py-8 text-center"},[s("p",{class:"text-xs text-white/20"}," Shared via AIUI · Powered by Nostr ")],-1))])}}});export{Z as default};
|
||||||
@@ -1 +1 @@
|
|||||||
import{_ as m}from"./FilmDetail.vue_vue_type_script_setup_true_lang-BhKlPG3Y.js";import"./index-8cIrvc8q.js";export{m as default};
|
import{_ as m}from"./FilmDetail.vue_vue_type_script_setup_true_lang-Cgf4f-Ng.js";import"./index-DNCGxUDM.js";export{m as default};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
import{_ as o}from"./FilmGrid.vue_vue_type_script_setup_true_lang-Dj0SEfcW.js";import"./index-8cIrvc8q.js";import"./useContentImages-7wLVntsF.js";export{o as default};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{_ as o}from"./FilmGrid.vue_vue_type_script_setup_true_lang-CkIQ4bRp.js";import"./index-DNCGxUDM.js";import"./useContentImages-DdjyABL9.js";export{o as default};
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{a as F,b as l,c as r,e as o,n as d,u as a,t as c,f as L,w as S,v as j,F as v,g as m,h as U,i as u,j as E,r as y,k as w,l as z,m as B,p as D}from"./index-8cIrvc8q.js";import{u as G}from"./useContentImages-7wLVntsF.js";const N={class:"h-full flex flex-col"},V={class:"flex items-center justify-between gap-2"},I={class:"flex items-center gap-2 shrink-0"},M={class:"flex flex-wrap gap-1.5"},R=["onClick"],T={class:"flex-1 overflow-y-auto custom-scrollbar px-4 pt-4 pb-16"},q={class:"grid grid-cols-2 sm:grid-cols-3 gap-4"},P=["aria-label","onClick"],A={class:"poster-card flex-1 min-h-0"},H={key:0,class:"absolute inset-0 animate-shimmer"},J=["src","alt","onError"],K=["src","alt"],O={key:3,class:"absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none"},Q={class:"absolute bottom-0 left-0 right-0 p-2"},W={class:"text-xs font-semibold text-white/90 leading-tight truncate"},X={class:"flex items-center gap-1 mt-0.5"},Y={key:0,class:"text-xs text-accent font-bold"},Z={key:1,class:"text-xs text-white/40"},ee={class:"absolute top-1.5 right-1.5 flex gap-0.5"},te={key:0,class:"flex items-center justify-center py-12"},ae=F({__name:"FilmGrid",props:{films:{},title:{default:"Recommended Films"}},emits:["selectFilm"],setup(_){const b=_,{isDark:n}=E(),p=y(""),h=y(null),{coverSrc:x,fallbackSrc:k,onError:C,isLoading:f}=G({items:D(b,"films"),id:t=>t.id,existingUrl:t=>t.posterUrl||t.backdropUrl,fetch:t=>B(t.title,t.year).then(s=>s.posterUrl),fallback:t=>z(t.title,t.year)}),$=w(()=>{const t=new Map;for(const s of b.films)for(const e of s.genres)t.set(e,(t.get(e)??0)+1);return[...t.entries()].sort((s,e)=>e[1]-s[1]).slice(0,8).map(([s])=>s)}),g=w(()=>{let t=b.films;if(p.value){const s=p.value.toLowerCase();t=t.filter(e=>e.title.toLowerCase().includes(s)||e.director.toLowerCase().includes(s)||e.cast.some(i=>i.toLowerCase().includes(s)))}return h.value&&(t=t.filter(s=>s.genres.includes(h.value))),t});return(t,s)=>(l(),r("div",N,[o("div",{class:"p-4 space-y-3",style:U(a(n)?"border-bottom: 1px solid rgba(255, 255, 255, 0.08)":"border-bottom: 1px solid rgba(0, 0, 0, 0.06)")},[o("div",V,[o("h3",{class:d(["text-sm font-bold",a(n)?"text-white/90":"text-gray-900"])},c(_.title),3),o("div",I,[o("span",{class:d(["text-xs font-mono",a(n)?"text-white/30":"text-gray-400"])},c(g.value.length)+" films ",3),L(t.$slots,"header-actions")])]),S(o("input",{"onUpdate:modelValue":s[0]||(s[0]=e=>p.value=e),type:"text",placeholder:"Search films...",class:d(["w-full px-3 py-2 rounded-lg text-base outline-none transition-colors",a(n)?"bg-white/5 text-white/80 placeholder:text-white/25 focus:bg-white/10":"bg-black/3 text-gray-800 placeholder:text-gray-400 focus:bg-black/5"])},null,2),[[j,p.value]]),o("div",M,[(l(!0),r(v,null,m($.value,e=>(l(),r("button",{key:e,class:d(["text-xs px-2 py-1 rounded-md transition-all duration-150",h.value===e?"nav-tab-active":a(n)?"text-white/40 hover:text-white/70 hover:bg-white/5":"text-gray-500 hover:text-gray-800 hover:bg-black/5"]),onClick:i=>h.value=h.value===e?null:e},c(e),11,R))),128))])],4),o("div",T,[o("div",q,[(l(!0),r(v,null,m(g.value,e=>(l(),r("button",{key:e.id,class:"group flex flex-col items-stretch text-left w-full path-glass-bubble rounded-2xl overflow-hidden transition-all duration-200 hover:brightness-105","aria-label":`${e.title} (${e.year})`,onClick:i=>t.$emit("selectFilm",e)},[o("div",A,[o("div",{class:d(["aspect-[2/3] relative w-full overflow-hidden rounded-[10px]",a(x)(e)?"":a(n)?"bg-white/[0.06]":"bg-black/[0.04]"])},[a(f)(e)?(l(),r("div",H)):u("",!0),a(x)(e)?(l(),r("img",{key:1,src:a(x)(e),alt:`${e.title} (${e.year}) directed by ${e.director}`,class:"w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",loading:"lazy",onError:i=>a(C)(e)},null,40,J)):a(f)(e)?u("",!0):(l(),r("img",{key:2,src:a(k)(e),alt:e.title,class:"w-full h-full object-cover"},null,8,K)),a(x)(e)?(l(),r("div",O)):u("",!0),o("div",Q,[o("p",W,c(e.title),1),o("div",X,[e.rating>0?(l(),r("span",Y,"★ "+c(e.rating),1)):u("",!0),e.year>0?(l(),r("span",Z,c(e.year),1)):u("",!0)])]),o("div",ee,[(l(!0),r(v,null,m(e.sources.slice(0,2),i=>(l(),r("span",{key:i.type,class:"text-xs px-1 py-0.5 rounded bg-black/60 text-white/70 backdrop-blur-sm"},c(i.type),1))),128))])],2)])],8,P))),128))]),g.value.length===0?(l(),r("div",te,[o("p",{class:d(["text-sm",a(n)?"text-white/30":"text-gray-400"])}," No films match your search ",2)])):u("",!0)])]))}});export{ae as _};
|
import{a as F,b as l,c as r,e as o,n as d,u as a,t as c,f as L,w as S,v as j,F as v,g as m,h as U,i as u,j as E,r as y,k as w,l as z,m as B,p as D}from"./index-DNCGxUDM.js";import{u as G}from"./useContentImages-DdjyABL9.js";const N={class:"h-full flex flex-col"},V={class:"flex items-center justify-between gap-2"},I={class:"flex items-center gap-2 shrink-0"},M={class:"flex flex-wrap gap-1.5"},R=["onClick"],T={class:"flex-1 overflow-y-auto custom-scrollbar px-4 pt-4 pb-16"},q={class:"grid grid-cols-2 sm:grid-cols-3 gap-4"},P=["aria-label","onClick"],A={class:"poster-card flex-1 min-h-0"},H={key:0,class:"absolute inset-0 animate-shimmer"},J=["src","alt","onError"],K=["src","alt"],O={key:3,class:"absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none"},Q={class:"absolute bottom-0 left-0 right-0 p-2"},W={class:"text-xs font-semibold text-white/90 leading-tight truncate"},X={class:"flex items-center gap-1 mt-0.5"},Y={key:0,class:"text-xs text-accent font-bold"},Z={key:1,class:"text-xs text-white/40"},ee={class:"absolute top-1.5 right-1.5 flex gap-0.5"},te={key:0,class:"flex items-center justify-center py-12"},ae=F({__name:"FilmGrid",props:{films:{},title:{default:"Recommended Films"}},emits:["selectFilm"],setup(_){const b=_,{isDark:n}=E(),p=y(""),h=y(null),{coverSrc:x,fallbackSrc:k,onError:C,isLoading:f}=G({items:D(b,"films"),id:t=>t.id,existingUrl:t=>t.posterUrl||t.backdropUrl,fetch:t=>B(t.title,t.year).then(s=>s.posterUrl),fallback:t=>z(t.title,t.year)}),$=w(()=>{const t=new Map;for(const s of b.films)for(const e of s.genres)t.set(e,(t.get(e)??0)+1);return[...t.entries()].sort((s,e)=>e[1]-s[1]).slice(0,8).map(([s])=>s)}),g=w(()=>{let t=b.films;if(p.value){const s=p.value.toLowerCase();t=t.filter(e=>e.title.toLowerCase().includes(s)||e.director.toLowerCase().includes(s)||e.cast.some(i=>i.toLowerCase().includes(s)))}return h.value&&(t=t.filter(s=>s.genres.includes(h.value))),t});return(t,s)=>(l(),r("div",N,[o("div",{class:"p-4 space-y-3",style:U(a(n)?"border-bottom: 1px solid rgba(255, 255, 255, 0.08)":"border-bottom: 1px solid rgba(0, 0, 0, 0.06)")},[o("div",V,[o("h3",{class:d(["text-sm font-bold",a(n)?"text-white/90":"text-gray-900"])},c(_.title),3),o("div",I,[o("span",{class:d(["text-xs font-mono",a(n)?"text-white/30":"text-gray-400"])},c(g.value.length)+" films ",3),L(t.$slots,"header-actions")])]),S(o("input",{"onUpdate:modelValue":s[0]||(s[0]=e=>p.value=e),type:"text",placeholder:"Search films...",class:d(["w-full px-3 py-2 rounded-lg text-base outline-none transition-colors",a(n)?"bg-white/5 text-white/80 placeholder:text-white/25 focus:bg-white/10":"bg-black/3 text-gray-800 placeholder:text-gray-400 focus:bg-black/5"])},null,2),[[j,p.value]]),o("div",M,[(l(!0),r(v,null,m($.value,e=>(l(),r("button",{key:e,class:d(["text-xs px-2 py-1 rounded-md transition-all duration-150",h.value===e?"nav-tab-active":a(n)?"text-white/40 hover:text-white/70 hover:bg-white/5":"text-gray-500 hover:text-gray-800 hover:bg-black/5"]),onClick:i=>h.value=h.value===e?null:e},c(e),11,R))),128))])],4),o("div",T,[o("div",q,[(l(!0),r(v,null,m(g.value,e=>(l(),r("button",{key:e.id,class:"group flex flex-col items-stretch text-left w-full path-glass-bubble rounded-2xl overflow-hidden transition-all duration-200 hover:brightness-105","aria-label":`${e.title} (${e.year})`,onClick:i=>t.$emit("selectFilm",e)},[o("div",A,[o("div",{class:d(["aspect-[2/3] relative w-full overflow-hidden rounded-[10px]",a(x)(e)?"":a(n)?"bg-white/[0.06]":"bg-black/[0.04]"])},[a(f)(e)?(l(),r("div",H)):u("",!0),a(x)(e)?(l(),r("img",{key:1,src:a(x)(e),alt:`${e.title} (${e.year}) directed by ${e.director}`,class:"w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",loading:"lazy",onError:i=>a(C)(e)},null,40,J)):a(f)(e)?u("",!0):(l(),r("img",{key:2,src:a(k)(e),alt:e.title,class:"w-full h-full object-cover"},null,8,K)),a(x)(e)?(l(),r("div",O)):u("",!0),o("div",Q,[o("p",W,c(e.title),1),o("div",X,[e.rating>0?(l(),r("span",Y,"★ "+c(e.rating),1)):u("",!0),e.year>0?(l(),r("span",Z,c(e.year),1)):u("",!0)])]),o("div",ee,[(l(!0),r(v,null,m(e.sources.slice(0,2),i=>(l(),r("span",{key:i.type,class:"text-xs px-1 py-0.5 rounded bg-black/60 text-white/70 backdrop-blur-sm"},c(i.type),1))),128))])],2)])],8,P))),128))]),g.value.length===0?(l(),r("div",te,[o("p",{class:d(["text-sm",a(n)?"text-white/30":"text-gray-400"])}," No films match your search ",2)])):u("",!0)])]))}});export{ae as _};
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{_ as m}from"./SongDetail.vue_vue_type_script_setup_true_lang-B3om3Z8v.js";import"./index-8cIrvc8q.js";export{m as default};
|
import{_ as m}from"./SongDetail.vue_vue_type_script_setup_true_lang-0mQhUBE8.js";import"./index-DNCGxUDM.js";export{m as default};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{_ as o}from"./SongGrid.vue_vue_type_script_setup_true_lang-70SttNTZ.js";import"./index-DNCGxUDM.js";import"./useContentImages-DdjyABL9.js";export{o as default};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{_ as o}from"./SongGrid.vue_vue_type_script_setup_true_lang-IvAOIQYW.js";import"./index-8cIrvc8q.js";import"./useContentImages-7wLVntsF.js";export{o as default};
|
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{a as z,q as M,x as B,y as E,p as q,b as o,c as r,e as s,n as c,u as a,t as u,f as D,w as F,v as G,F as v,g as m,h as N,i as h,z as U,j as V,r as _,k}from"./index-8cIrvc8q.js";import{u as P}from"./useContentImages-7wLVntsF.js";const R={class:"h-full flex flex-col"},T={class:"flex items-center justify-between gap-2"},I={class:"flex items-center gap-2 shrink-0"},A={class:"flex flex-wrap gap-1.5"},H=["onClick"],J={class:"flex-1 overflow-y-auto custom-scrollbar px-4 pt-4 pb-16"},K={class:"grid grid-cols-2 sm:grid-cols-3 gap-4"},O=["aria-label","onClick"],Q={class:"cover-card flex-1 min-h-0 relative flex items-center justify-center"},W={key:0,class:"absolute inset-0 animate-shimmer"},X=["onClick"],Y=["src","alt","onError"],Z=["src","alt"],tt={key:3,class:"absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none"},et={class:"absolute bottom-0 left-0 right-0 p-2"},st={class:"text-xs font-semibold text-white/90 leading-tight truncate"},lt={key:0,class:"text-xs text-white/40 truncate mt-0.5"},at={class:"absolute top-1.5 right-1.5 flex gap-0.5 flex-wrap justify-end max-w-[60%]"},ot={key:0,class:"flex items-center justify-center py-12"},nt=z({__name:"SongGrid",props:{songs:{},title:{default:"Recommended Songs"}},emits:["selectSong"],setup(g,{emit:C}){const x=g,y=C,{isDark:i}=V(),{play:S}=M(),p=_(""),d=_(null),{coverSrc:f,fallbackSrc:j,onError:$,isLoading:w}=P({items:q(x,"songs"),id:e=>e.id,existingUrl:e=>e.coverUrl,fetch:e=>E(e.title,e.artist,e.album),fallback:e=>B(e.title,e.artist)}),L=k(()=>{const e=new Map;for(const l of x.songs)for(const t of l.genres??[])e.set(t,(e.get(t)??0)+1);return[...e.entries()].sort((l,t)=>t[1]-l[1]).slice(0,8).map(([l])=>l)}),b=k(()=>{let e=x.songs;if(p.value){const l=p.value.toLowerCase();e=e.filter(t=>t.title.toLowerCase().includes(l)||t.artist.toLowerCase().includes(l)||(t.album??"").toLowerCase().includes(l))}return d.value&&(e=e.filter(l=>(l.genres??[]).includes(d.value))),e});return(e,l)=>(o(),r("div",R,[s("div",{class:"p-4 space-y-3",style:N(a(i)?"border-bottom: 1px solid rgba(255, 255, 255, 0.08)":"border-bottom: 1px solid rgba(0, 0, 0, 0.06)")},[s("div",T,[s("h3",{class:c(["text-sm font-bold",a(i)?"text-white/90":"text-gray-900"])},u(g.title),3),s("div",I,[s("span",{class:c(["text-xs font-mono",a(i)?"text-white/30":"text-gray-400"])},u(b.value.length)+" songs ",3),D(e.$slots,"header-actions")])]),F(s("input",{"onUpdate:modelValue":l[0]||(l[0]=t=>p.value=t),type:"text",placeholder:"Search songs...",class:c(["w-full px-3 py-2 rounded-lg text-base outline-none transition-colors",a(i)?"bg-white/5 text-white/80 placeholder:text-white/25 focus:bg-white/10":"bg-black/3 text-gray-800 placeholder:text-gray-400 focus:bg-black/5"])},null,2),[[G,p.value]]),s("div",A,[(o(!0),r(v,null,m(L.value,t=>(o(),r("button",{key:t,class:c(["text-xs px-2 py-1 rounded-md transition-all duration-150",d.value===t?"nav-tab-active":a(i)?"text-white/40 hover:text-white/70 hover:bg-white/5":"text-gray-500 hover:text-gray-800 hover:bg-black/5"]),onClick:n=>d.value=d.value===t?null:t},u(t),11,H))),128))])],4),s("div",J,[s("div",K,[(o(!0),r(v,null,m(b.value,t=>(o(),r("button",{key:t.id,class:"group flex flex-col items-stretch text-left w-full path-glass-bubble rounded-2xl overflow-hidden transition-all duration-200 hover:brightness-105","aria-label":`${t.title} by ${t.artist}`,onClick:n=>y("selectSong",t)},[s("div",Q,[s("div",{class:c(["aspect-square relative w-full overflow-hidden rounded-[10px]",a(f)(t)?"":a(i)?"bg-white/[0.06]":"bg-black/[0.04]"])},[a(w)(t)?(o(),r("div",W)):h("",!0),s("button",{class:"absolute inset-0 flex items-center justify-center z-10 backdrop-blur-sm bg-black/30 opacity-0 group-hover:opacity-100 transition-all duration-200","aria-label":"Play",onClick:U(n=>{a(S)(t),y("selectSong",t)},["stop"])},[...l[1]||(l[1]=[s("span",{class:"w-16 h-16 rounded-full flex items-center justify-center path-glass-icon"},[s("svg",{class:"w-8 h-8 text-white",fill:"currentColor",viewBox:"0 0 24 24"},[s("path",{d:"M8 5v14l11-7L8 5z"})])],-1)])],8,X),a(f)(t)?(o(),r("img",{key:1,src:a(f)(t),alt:`${t.title} by ${t.artist}`,class:"w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",loading:"lazy",onError:n=>a($)(t)},null,40,Y)):a(w)(t)?h("",!0):(o(),r("img",{key:2,src:a(j)(t),alt:t.title,class:"w-full h-full object-cover"},null,8,Z)),a(f)(t)?(o(),r("div",tt)):h("",!0),s("div",et,[s("p",st,u(t.title),1),t.artist?(o(),r("p",lt,u(t.artist),1)):h("",!0)]),s("div",at,[(o(!0),r(v,null,m((t.sources??[]).slice(0,2),n=>(o(),r("span",{key:n.type,class:"text-xs px-1 py-0.5 rounded bg-black/60 text-white/70 backdrop-blur-sm"},u(n.type),1))),128))])],2)])],8,O))),128))]),b.value.length===0?(o(),r("div",ot,[s("p",{class:c(["text-sm",a(i)?"text-white/30":"text-gray-400"])}," No songs match your search ",2)])):h("",!0)])]))}});export{nt as _};
|
import{a as z,q as M,x as B,y as E,p as q,b as o,c as r,e as s,n as c,u as a,t as u,f as D,w as F,v as G,F as v,g as m,h as N,i as h,z as U,j as V,r as _,k}from"./index-DNCGxUDM.js";import{u as P}from"./useContentImages-DdjyABL9.js";const R={class:"h-full flex flex-col"},T={class:"flex items-center justify-between gap-2"},I={class:"flex items-center gap-2 shrink-0"},A={class:"flex flex-wrap gap-1.5"},H=["onClick"],J={class:"flex-1 overflow-y-auto custom-scrollbar px-4 pt-4 pb-16"},K={class:"grid grid-cols-2 sm:grid-cols-3 gap-4"},O=["aria-label","onClick"],Q={class:"cover-card flex-1 min-h-0 relative flex items-center justify-center"},W={key:0,class:"absolute inset-0 animate-shimmer"},X=["onClick"],Y=["src","alt","onError"],Z=["src","alt"],tt={key:3,class:"absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none"},et={class:"absolute bottom-0 left-0 right-0 p-2"},st={class:"text-xs font-semibold text-white/90 leading-tight truncate"},lt={key:0,class:"text-xs text-white/40 truncate mt-0.5"},at={class:"absolute top-1.5 right-1.5 flex gap-0.5 flex-wrap justify-end max-w-[60%]"},ot={key:0,class:"flex items-center justify-center py-12"},nt=z({__name:"SongGrid",props:{songs:{},title:{default:"Recommended Songs"}},emits:["selectSong"],setup(g,{emit:C}){const x=g,y=C,{isDark:i}=V(),{play:S}=M(),p=_(""),d=_(null),{coverSrc:f,fallbackSrc:j,onError:$,isLoading:w}=P({items:q(x,"songs"),id:e=>e.id,existingUrl:e=>e.coverUrl,fetch:e=>E(e.title,e.artist,e.album),fallback:e=>B(e.title,e.artist)}),L=k(()=>{const e=new Map;for(const l of x.songs)for(const t of l.genres??[])e.set(t,(e.get(t)??0)+1);return[...e.entries()].sort((l,t)=>t[1]-l[1]).slice(0,8).map(([l])=>l)}),b=k(()=>{let e=x.songs;if(p.value){const l=p.value.toLowerCase();e=e.filter(t=>t.title.toLowerCase().includes(l)||t.artist.toLowerCase().includes(l)||(t.album??"").toLowerCase().includes(l))}return d.value&&(e=e.filter(l=>(l.genres??[]).includes(d.value))),e});return(e,l)=>(o(),r("div",R,[s("div",{class:"p-4 space-y-3",style:N(a(i)?"border-bottom: 1px solid rgba(255, 255, 255, 0.08)":"border-bottom: 1px solid rgba(0, 0, 0, 0.06)")},[s("div",T,[s("h3",{class:c(["text-sm font-bold",a(i)?"text-white/90":"text-gray-900"])},u(g.title),3),s("div",I,[s("span",{class:c(["text-xs font-mono",a(i)?"text-white/30":"text-gray-400"])},u(b.value.length)+" songs ",3),D(e.$slots,"header-actions")])]),F(s("input",{"onUpdate:modelValue":l[0]||(l[0]=t=>p.value=t),type:"text",placeholder:"Search songs...",class:c(["w-full px-3 py-2 rounded-lg text-base outline-none transition-colors",a(i)?"bg-white/5 text-white/80 placeholder:text-white/25 focus:bg-white/10":"bg-black/3 text-gray-800 placeholder:text-gray-400 focus:bg-black/5"])},null,2),[[G,p.value]]),s("div",A,[(o(!0),r(v,null,m(L.value,t=>(o(),r("button",{key:t,class:c(["text-xs px-2 py-1 rounded-md transition-all duration-150",d.value===t?"nav-tab-active":a(i)?"text-white/40 hover:text-white/70 hover:bg-white/5":"text-gray-500 hover:text-gray-800 hover:bg-black/5"]),onClick:n=>d.value=d.value===t?null:t},u(t),11,H))),128))])],4),s("div",J,[s("div",K,[(o(!0),r(v,null,m(b.value,t=>(o(),r("button",{key:t.id,class:"group flex flex-col items-stretch text-left w-full path-glass-bubble rounded-2xl overflow-hidden transition-all duration-200 hover:brightness-105","aria-label":`${t.title} by ${t.artist}`,onClick:n=>y("selectSong",t)},[s("div",Q,[s("div",{class:c(["aspect-square relative w-full overflow-hidden rounded-[10px]",a(f)(t)?"":a(i)?"bg-white/[0.06]":"bg-black/[0.04]"])},[a(w)(t)?(o(),r("div",W)):h("",!0),s("button",{class:"absolute inset-0 flex items-center justify-center z-10 backdrop-blur-sm bg-black/30 opacity-0 group-hover:opacity-100 transition-all duration-200","aria-label":"Play",onClick:U(n=>{a(S)(t),y("selectSong",t)},["stop"])},[...l[1]||(l[1]=[s("span",{class:"w-16 h-16 rounded-full flex items-center justify-center path-glass-icon"},[s("svg",{class:"w-8 h-8 text-white",fill:"currentColor",viewBox:"0 0 24 24"},[s("path",{d:"M8 5v14l11-7L8 5z"})])],-1)])],8,X),a(f)(t)?(o(),r("img",{key:1,src:a(f)(t),alt:`${t.title} by ${t.artist}`,class:"w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",loading:"lazy",onError:n=>a($)(t)},null,40,Y)):a(w)(t)?h("",!0):(o(),r("img",{key:2,src:a(j)(t),alt:t.title,class:"w-full h-full object-cover"},null,8,Z)),a(f)(t)?(o(),r("div",tt)):h("",!0),s("div",et,[s("p",st,u(t.title),1),t.artist?(o(),r("p",lt,u(t.artist),1)):h("",!0)]),s("div",at,[(o(!0),r(v,null,m((t.sources??[]).slice(0,2),n=>(o(),r("span",{key:n.type,class:"text-xs px-1 py-0.5 rounded bg-black/60 text-white/70 backdrop-blur-sm"},u(n.type),1))),128))])],2)])],8,O))),128))]),b.value.length===0?(o(),r("div",ot,[s("p",{class:c(["text-sm",a(i)?"text-white/30":"text-gray-400"])}," No songs match your search ",2)])):h("",!0)])]))}});export{nt as _};
|
||||||
@@ -1 +1 @@
|
|||||||
import{a as h,J as m,b as d,c as r,e as t,t as s,F as u,g as p,K as x,h as f}from"./index-8cIrvc8q.js";const g={class:"rounded-lg bg-white/[0.03] border border-white/5 p-2.5 mb-1"},b={class:"flex items-center gap-1.5 mb-1"},w={class:"w-5 h-5 rounded-full shrink-0 flex items-center justify-center text-xs font-bold bg-purple-500/20 text-purple-400"},y={class:"text-xs font-semibold text-white/70"},v={class:"text-xs ml-auto text-white/20"},k={class:"text-xs text-white/60 leading-relaxed whitespace-pre-wrap"},T=h({__name:"ThreadNode",props:{node:{},depth:{}},emits:["reply"],setup(e){function i(o){return new Date(o*1e3).toLocaleTimeString("en",{hour:"2-digit",minute:"2-digit"})}return(o,n)=>{const l=m("ThreadNode",!0);return d(),r("div",{style:f({paddingLeft:`${Math.min(e.depth,4)*16}px`})},[t("div",g,[t("div",b,[t("div",w,s(e.node.note.authorName?.charAt(0)?.toUpperCase()??"?"),1),t("span",y,s(e.node.note.authorName??"anon"),1),t("span",v,s(i(e.node.note.created_at)),1)]),t("p",k,s(e.node.note.content),1),t("button",{class:"text-xs text-white/25 hover:text-accent/60 mt-1 transition-colors",onClick:n[0]||(n[0]=a=>o.$emit("reply",e.node.note))}," Reply ")]),(d(!0),r(u,null,p(e.node.children,a=>(d(),x(l,{key:a.note.id,node:a,depth:e.depth+1,onReply:n[1]||(n[1]=c=>o.$emit("reply",c))},null,8,["node","depth"]))),128))],4)}}});export{T as default};
|
import{a as h,J as m,b as d,c as r,e as t,t as s,F as u,g as p,K as x,h as f}from"./index-DNCGxUDM.js";const g={class:"rounded-lg bg-white/[0.03] border border-white/5 p-2.5 mb-1"},b={class:"flex items-center gap-1.5 mb-1"},w={class:"w-5 h-5 rounded-full shrink-0 flex items-center justify-center text-xs font-bold bg-purple-500/20 text-purple-400"},y={class:"text-xs font-semibold text-white/70"},v={class:"text-xs ml-auto text-white/20"},k={class:"text-xs text-white/60 leading-relaxed whitespace-pre-wrap"},T=h({__name:"ThreadNode",props:{node:{},depth:{}},emits:["reply"],setup(e){function i(o){return new Date(o*1e3).toLocaleTimeString("en",{hour:"2-digit",minute:"2-digit"})}return(o,n)=>{const l=m("ThreadNode",!0);return d(),r("div",{style:f({paddingLeft:`${Math.min(e.depth,4)*16}px`})},[t("div",g,[t("div",b,[t("div",w,s(e.node.note.authorName?.charAt(0)?.toUpperCase()??"?"),1),t("span",y,s(e.node.note.authorName??"anon"),1),t("span",v,s(i(e.node.note.created_at)),1)]),t("p",k,s(e.node.note.content),1),t("button",{class:"text-xs text-white/25 hover:text-accent/60 mt-1 transition-colors",onClick:n[0]||(n[0]=a=>o.$emit("reply",e.node.note))}," Reply ")]),(d(!0),r(u,null,p(e.node.children,a=>(d(),x(l,{key:a.note.id,node:a,depth:e.depth+1,onReply:n[1]||(n[1]=c=>o.$emit("reply",c))},null,8,["node","depth"]))),128))],4)}}});export{T as default};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{e as x,c as O,g as m,k as P,h as p,j as w,l as c,m as A,n as I,t as N,o as E}from"./_baseUniq-DAOs4kUj.js";import{aR as g,ar as F,aS as M,aT as T,aU as _,aV as l,aW as $,aX as B,aY as S,aZ as y}from"./mermaid.core-v0oo9NRr.js";var R=/\s/;function G(n){for(var r=n.length;r--&&R.test(n.charAt(r)););return r}var H=/^\s+/;function L(n){return n&&n.slice(0,G(n)+1).replace(H,"")}var o=NaN,W=/^[-+]0x[0-9a-f]+$/i,X=/^0b[01]+$/i,Y=/^0o[0-7]+$/i,q=parseInt;function z(n){if(typeof n=="number")return n;if(x(n))return o;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=L(n);var t=X.test(n);return t||Y.test(n)?q(n.slice(2),t?2:8):W.test(n)?o:+n}var v=1/0,C=17976931348623157e292;function K(n){if(!n)return n===0?n:0;if(n=z(n),n===v||n===-v){var r=n<0?-1:1;return r*C}return n===n?n:0}function U(n){var r=K(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?O(n):[]}var b=Object.prototype,Z=b.hasOwnProperty,dn=F(function(n,r){n=Object(n);var t=-1,i=r.length,a=i>2?r[2]:void 0;for(a&&M(r[0],r[1],a)&&(i=1);++t<i;)for(var f=r[t],e=T(f),s=-1,d=e.length;++s<d;){var u=e[s],h=n[u];(h===void 0||_(h,b[u])&&!Z.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function D(n){return function(r,t,i){var a=Object(r);if(!l(r)){var f=m(t);r=P(r),t=function(s){return f(a[s],s,a)}}var e=n(r,t,i);return e>-1?a[f?r[e]:e]:void 0}}var J=Math.max;function Q(n,r,t){var i=n==null?0:n.length;if(!i)return-1;var a=t==null?0:U(t);return a<0&&(a=J(i+a,0)),p(n,m(r),a)}var hn=D(Q);function V(n,r){var t=-1,i=l(n)?Array(n.length):[];return w(n,function(a,f,e){i[++t]=r(a,f,e)}),i}function gn(n,r){var t=$(n)?c:V;return t(n,m(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function mn(n,r){return n!=null&&A(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var i=-1,a=n.length;++i<a;){var f=n[i],e=r(f);if(e!=null&&(s===void 0?e===e&&!x(e):t(e,s)))var s=e,d=f}return d}function on(n){return n&&n.length?tn(n,B,rn):void 0}function an(n,r,t,i){if(!g(n))return n;r=I(r,n);for(var a=-1,f=r.length,e=f-1,s=n;s!=null&&++a<f;){var d=N(r[a]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(a!=e){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:S(r[a+1])?[]:{})}y(s,d,u),s=s[d]}return n}function vn(n,r,t){for(var i=-1,a=r.length,f={};++i<a;){var e=r[i],s=E(n,e);t(s,e)&&an(f,I(e,n),s)}return f}export{rn as a,tn as b,V as c,vn as d,on as e,fn as f,hn as g,mn as h,dn as i,U as j,un as l,gn as m,K as t};
|
import{e as x,c as O,g as m,k as P,h as p,j as w,l as c,m as A,n as I,t as N,o as E}from"./_baseUniq-T1y-Xwdr.js";import{aR as g,ar as F,aS as M,aT as T,aU as _,aV as l,aW as $,aX as B,aY as S,aZ as y}from"./mermaid.core-CFUktQ8s.js";var R=/\s/;function G(n){for(var r=n.length;r--&&R.test(n.charAt(r)););return r}var H=/^\s+/;function L(n){return n&&n.slice(0,G(n)+1).replace(H,"")}var o=NaN,W=/^[-+]0x[0-9a-f]+$/i,X=/^0b[01]+$/i,Y=/^0o[0-7]+$/i,q=parseInt;function z(n){if(typeof n=="number")return n;if(x(n))return o;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=L(n);var t=X.test(n);return t||Y.test(n)?q(n.slice(2),t?2:8):W.test(n)?o:+n}var v=1/0,C=17976931348623157e292;function K(n){if(!n)return n===0?n:0;if(n=z(n),n===v||n===-v){var r=n<0?-1:1;return r*C}return n===n?n:0}function U(n){var r=K(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?O(n):[]}var b=Object.prototype,Z=b.hasOwnProperty,dn=F(function(n,r){n=Object(n);var t=-1,i=r.length,a=i>2?r[2]:void 0;for(a&&M(r[0],r[1],a)&&(i=1);++t<i;)for(var f=r[t],e=T(f),s=-1,d=e.length;++s<d;){var u=e[s],h=n[u];(h===void 0||_(h,b[u])&&!Z.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function D(n){return function(r,t,i){var a=Object(r);if(!l(r)){var f=m(t);r=P(r),t=function(s){return f(a[s],s,a)}}var e=n(r,t,i);return e>-1?a[f?r[e]:e]:void 0}}var J=Math.max;function Q(n,r,t){var i=n==null?0:n.length;if(!i)return-1;var a=t==null?0:U(t);return a<0&&(a=J(i+a,0)),p(n,m(r),a)}var hn=D(Q);function V(n,r){var t=-1,i=l(n)?Array(n.length):[];return w(n,function(a,f,e){i[++t]=r(a,f,e)}),i}function gn(n,r){var t=$(n)?c:V;return t(n,m(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function mn(n,r){return n!=null&&A(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var i=-1,a=n.length;++i<a;){var f=n[i],e=r(f);if(e!=null&&(s===void 0?e===e&&!x(e):t(e,s)))var s=e,d=f}return d}function on(n){return n&&n.length?tn(n,B,rn):void 0}function an(n,r,t,i){if(!g(n))return n;r=I(r,n);for(var a=-1,f=r.length,e=f-1,s=n;s!=null&&++a<f;){var d=N(r[a]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(a!=e){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:S(r[a+1])?[]:{})}y(s,d,u),s=s[d]}return n}function vn(n,r,t){for(var i=-1,a=r.length,f={};++i<a;){var e=r[i],s=E(n,e);t(s,e)&&an(f,I(e,n),s)}return f}export{rn as a,tn as b,V as c,vn as d,on as e,fn as f,hn as g,mn as h,dn as i,U as j,un as l,gn as m,K as t};
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{N as ln,O as an,P as Z,Q as O,R as V,S as un,T as y,V as tn,W as z,X as _,Y as rn,Z as o,$ as on,a0 as sn,a1 as fn}from"./mermaid.core-v0oo9NRr.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,D,S,v,R,W,a){var E=D-l,i=S-h,n=W-v,d=a-R,u=d*E-n*i;if(!(u*u<y))return u=(n*(h-R)-d*(l-v))/u,[l+u*E,h+u*i]}function J(l,h,D,S,v,R,W){var a=l-D,E=h-S,i=(W?R:-R)/z(a*a+E*E),n=i*E,d=-i*a,u=l+n,s=h+d,f=D+n,c=S+d,X=(u+f)/2,t=(s+c)/2,m=f-u,g=c-s,A=m*m+g*g,T=v-R,P=u*c-f*s,I=(g<0?-1:1)*z(on(0,T*T*A-P*P)),N=(P*g-m*I)/A,Q=(-P*m-g*I)/A,w=(P*g+m*I)/A,p=(-P*m+g*I)/A,x=N-X,e=Q-t,r=w-X,Y=p-t;return x*x+e*e>r*r+Y*Y&&(N=w,Q=p),{cx:N,cy:Q,x01:-n,y01:-d,x11:N*(v/T-1),y11:Q*(v/T-1)}}function hn(){var l=cn,h=yn,D=V(0),S=null,v=gn,R=dn,W=mn,a=null,E=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-un,c=R.apply(this,arguments)-un,X=rn(c-f),t=c>f;if(a||(a=n=E()),s<u&&(d=s,s=u,u=d),!(s>y))a.moveTo(0,0);else if(X>tn-y)a.moveTo(s*Z(f),s*O(f)),a.arc(0,0,s,f,c,!t),u>y&&(a.moveTo(u*Z(c),u*O(c)),a.arc(0,0,u,c,f,t));else{var m=f,g=c,A=f,T=c,P=X,I=X,N=W.apply(this,arguments)/2,Q=N>y&&(S?+S.apply(this,arguments):z(u*u+s*s)),w=_(rn(s-u)/2,+D.apply(this,arguments)),p=w,x=w,e,r;if(Q>y){var Y=sn(Q/u*O(N)),B=sn(Q/s*O(N));(P-=Y*2)>y?(Y*=t?1:-1,A+=Y,T-=Y):(P=0,A=T=(f+c)/2),(I-=B*2)>y?(B*=t?1:-1,m+=B,g-=B):(I=0,m=g=(f+c)/2)}var $=s*Z(m),j=s*O(m),C=u*Z(T),F=u*O(T);if(w>y){var G=s*Z(g),H=s*O(g),K=u*Z(A),L=u*O(A),q;if(X<an)if(q=pn($,j,K,L,G,H,C,F)){var M=$-q[0],U=j-q[1],k=G-q[0],b=H-q[1],nn=1/O(fn((M*k+U*b)/(z(M*M+U*U)*z(k*k+b*b)))/2),en=z(q[0]*q[0]+q[1]*q[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}I>y?x>y?(e=J(K,L,$,j,s,x,t),r=J(G,H,C,F,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo($,j),a.arc(0,0,s,m,g,!t)):a.moveTo($,j),!(u>y)||!(P>y)?a.lineTo(C,F):p>y?(e=J(C,F,G,H,u,-p,t),r=J($,j,K,L,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,A,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +R.apply(this,arguments))/2-an/2;return[Z(d)*n,O(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:V(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:V(+n),i):h},i.cornerRadius=function(n){return arguments.length?(D=typeof n=="function"?n:V(+n),i):D},i.padRadius=function(n){return arguments.length?(S=n==null?null:typeof n=="function"?n:V(+n),i):S},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:V(+n),i):v},i.endAngle=function(n){return arguments.length?(R=typeof n=="function"?n:V(+n),i):R},i.padAngle=function(n){return arguments.length?(W=typeof n=="function"?n:V(+n),i):W},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};
|
import{N as ln,O as an,P as Z,Q as O,R as V,S as un,T as y,V as tn,W as z,X as _,Y as rn,Z as o,$ as on,a0 as sn,a1 as fn}from"./mermaid.core-CFUktQ8s.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,D,S,v,R,W,a){var E=D-l,i=S-h,n=W-v,d=a-R,u=d*E-n*i;if(!(u*u<y))return u=(n*(h-R)-d*(l-v))/u,[l+u*E,h+u*i]}function J(l,h,D,S,v,R,W){var a=l-D,E=h-S,i=(W?R:-R)/z(a*a+E*E),n=i*E,d=-i*a,u=l+n,s=h+d,f=D+n,c=S+d,X=(u+f)/2,t=(s+c)/2,m=f-u,g=c-s,A=m*m+g*g,T=v-R,P=u*c-f*s,I=(g<0?-1:1)*z(on(0,T*T*A-P*P)),N=(P*g-m*I)/A,Q=(-P*m-g*I)/A,w=(P*g+m*I)/A,p=(-P*m+g*I)/A,x=N-X,e=Q-t,r=w-X,Y=p-t;return x*x+e*e>r*r+Y*Y&&(N=w,Q=p),{cx:N,cy:Q,x01:-n,y01:-d,x11:N*(v/T-1),y11:Q*(v/T-1)}}function hn(){var l=cn,h=yn,D=V(0),S=null,v=gn,R=dn,W=mn,a=null,E=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-un,c=R.apply(this,arguments)-un,X=rn(c-f),t=c>f;if(a||(a=n=E()),s<u&&(d=s,s=u,u=d),!(s>y))a.moveTo(0,0);else if(X>tn-y)a.moveTo(s*Z(f),s*O(f)),a.arc(0,0,s,f,c,!t),u>y&&(a.moveTo(u*Z(c),u*O(c)),a.arc(0,0,u,c,f,t));else{var m=f,g=c,A=f,T=c,P=X,I=X,N=W.apply(this,arguments)/2,Q=N>y&&(S?+S.apply(this,arguments):z(u*u+s*s)),w=_(rn(s-u)/2,+D.apply(this,arguments)),p=w,x=w,e,r;if(Q>y){var Y=sn(Q/u*O(N)),B=sn(Q/s*O(N));(P-=Y*2)>y?(Y*=t?1:-1,A+=Y,T-=Y):(P=0,A=T=(f+c)/2),(I-=B*2)>y?(B*=t?1:-1,m+=B,g-=B):(I=0,m=g=(f+c)/2)}var $=s*Z(m),j=s*O(m),C=u*Z(T),F=u*O(T);if(w>y){var G=s*Z(g),H=s*O(g),K=u*Z(A),L=u*O(A),q;if(X<an)if(q=pn($,j,K,L,G,H,C,F)){var M=$-q[0],U=j-q[1],k=G-q[0],b=H-q[1],nn=1/O(fn((M*k+U*b)/(z(M*M+U*U)*z(k*k+b*b)))/2),en=z(q[0]*q[0]+q[1]*q[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}I>y?x>y?(e=J(K,L,$,j,s,x,t),r=J(G,H,C,F,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo($,j),a.arc(0,0,s,m,g,!t)):a.moveTo($,j),!(u>y)||!(P>y)?a.lineTo(C,F):p>y?(e=J(C,F,G,H,u,-p,t),r=J($,j,K,L,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,A,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +R.apply(this,arguments))/2-an/2;return[Z(d)*n,O(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:V(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:V(+n),i):h},i.cornerRadius=function(n){return arguments.length?(D=typeof n=="function"?n:V(+n),i):D},i.padRadius=function(n){return arguments.length?(S=n==null?null:typeof n=="function"?n:V(+n),i):S},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:V(+n),i):v},i.endAngle=function(n){return arguments.length?(R=typeof n=="function"?n:V(+n),i):R},i.padAngle=function(n){return arguments.length?(W=typeof n=="function"?n:V(+n),i):W},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{U as a,M as n}from"./mermaid.core-CFUktQ8s.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{U as a,M as n}from"./mermaid.core-v0oo9NRr.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
|
|
||||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{_ as i}from"./mermaid.core-v0oo9NRr.js";function t(c,e){c.accDescr&&e.setAccDescription?.(c.accDescr),c.accTitle&&e.setAccTitle?.(c.accTitle),c.title&&e.setDiagramTitle?.(c.title)}i(t,"populateCommonDb");export{t as p};
|
import{_ as i}from"./mermaid.core-CFUktQ8s.js";function t(c,e){c.accDescr&&e.setAccDescription?.(c.accDescr),c.accTitle&&e.setAccTitle?.(c.accTitle),c.title&&e.setDiagramTitle?.(c.title)}i(t,"populateCommonDb");export{t as p};
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{_ as a,d as o}from"./mermaid.core-v0oo9NRr.js";var d=a((t,e)=>{let n;return e==="sandbox"&&(n=o("#i"+t)),(e==="sandbox"?o(n.nodes()[0].contentDocument.body):o("body")).select(`[id="${t}"]`)},"getDiagramElement");export{d as g};
|
import{_ as a,d as o}from"./mermaid.core-CFUktQ8s.js";var d=a((t,e)=>{let n;return e==="sandbox"&&(n=o("#i"+t)),(e==="sandbox"?o(n.nodes()[0].contentDocument.body):o("body")).select(`[id="${t}"]`)},"getDiagramElement");export{d as g};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{_ as e}from"./mermaid.core-v0oo9NRr.js";var l=e(()=>`
|
import{_ as e}from"./mermaid.core-CFUktQ8s.js";var l=e(()=>`
|
||||||
/* Font Awesome icon styling - consolidated */
|
/* Font Awesome icon styling - consolidated */
|
||||||
.label-icon {
|
.label-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{_ as a,e as w,l as x}from"./mermaid.core-v0oo9NRr.js";var d=a((e,t,i,r)=>{e.attr("class",i);const{width:o,height:h,x:n,y:c}=u(e,t);w(e,h,o,r);const s=l(n,c,o,h,t);e.attr("viewBox",s),x.debug(`viewBox configured: ${s} with padding: ${t}`)},"setupViewPortForSVG"),u=a((e,t)=>{const i=e.node()?.getBBox()||{width:0,height:0,x:0,y:0};return{width:i.width+t*2,height:i.height+t*2,x:i.x,y:i.y}},"calculateDimensionsWithPadding"),l=a((e,t,i,r,o)=>`${e-o} ${t-o} ${i} ${r}`,"createViewBox");export{d as s};
|
import{_ as a,e as w,l as x}from"./mermaid.core-CFUktQ8s.js";var d=a((e,t,i,r)=>{e.attr("class",i);const{width:o,height:h,x:n,y:c}=u(e,t);w(e,h,o,r);const s=l(n,c,o,h,t);e.attr("viewBox",s),x.debug(`viewBox configured: ${s} with padding: ${t}`)},"setupViewPortForSVG"),u=a((e,t)=>{const i=e.node()?.getBBox()||{width:0,height:0,x:0,y:0};return{width:i.width+t*2,height:i.height+t*2,x:i.x,y:i.y}},"calculateDimensionsWithPadding"),l=a((e,t,i,r,o)=>`${e-o} ${t-o} ${i} ${r}`,"createViewBox");export{d as s};
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{_ as s}from"./mermaid.core-v0oo9NRr.js";var t,e=(t=class{constructor(i){this.init=i,this.records=this.init()}reset(){this.records=this.init()}},s(t,"ImperativeState"),t);export{e as I};
|
import{_ as s}from"./mermaid.core-CFUktQ8s.js";var t,e=(t=class{constructor(i){this.init=i,this.records=this.init()}reset(){this.records=this.init()}},s(t,"ImperativeState"),t);export{e as I};
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{_ as n,L as o,j as l}from"./mermaid.core-v0oo9NRr.js";var x=n((s,t)=>{const e=s.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const r in t.attrs)e.attr(r,t.attrs[r]);return t.class&&e.attr("class",t.class),e},"drawRect"),d=n((s,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};x(s,e).lower()},"drawBackgroundRect"),g=n((s,t)=>{const e=t.text.replace(o," "),r=s.append("text");r.attr("x",t.x),r.attr("y",t.y),r.attr("class","legend"),r.style("text-anchor",t.anchor),t.class&&r.attr("class",t.class);const a=r.append("tspan");return a.attr("x",t.x+t.textMargin*2),a.text(e),r},"drawText"),h=n((s,t,e,r)=>{const a=s.append("image");a.attr("x",t),a.attr("y",e);const i=l.sanitizeUrl(r);a.attr("xlink:href",i)},"drawImage"),m=n((s,t,e,r)=>{const a=s.append("use");a.attr("x",t),a.attr("y",e);const i=l.sanitizeUrl(r);a.attr("xlink:href",`#${i}`)},"drawEmbeddedImage"),y=n(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),p=n(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj");export{d as a,p as b,m as c,x as d,h as e,g as f,y as g};
|
import{_ as n,L as o,j as l}from"./mermaid.core-CFUktQ8s.js";var x=n((s,t)=>{const e=s.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const r in t.attrs)e.attr(r,t.attrs[r]);return t.class&&e.attr("class",t.class),e},"drawRect"),d=n((s,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};x(s,e).lower()},"drawBackgroundRect"),g=n((s,t)=>{const e=t.text.replace(o," "),r=s.append("text");r.attr("x",t.x),r.attr("y",t.y),r.attr("class","legend"),r.style("text-anchor",t.anchor),t.class&&r.attr("class",t.class);const a=r.append("tspan");return a.attr("x",t.x+t.textMargin*2),a.text(e),r},"drawText"),h=n((s,t,e,r)=>{const a=s.append("image");a.attr("x",t),a.attr("y",e);const i=l.sanitizeUrl(r);a.attr("xlink:href",i)},"drawImage"),m=n((s,t,e,r)=>{const a=s.append("use");a.attr("x",t),a.attr("y",e);const i=l.sanitizeUrl(r);a.attr("xlink:href",`#${i}`)},"drawEmbeddedImage"),y=n(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),p=n(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj");export{d as a,p as b,m as c,x as d,h as e,g as f,y as g};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-Cq2XT3QN.js";import{_ as i}from"./mermaid.core-CFUktQ8s.js";import"./chunk-FMBD7UC4-BALYpKmy.js";import"./chunk-55IACEB6-fbeP3Dtn.js";import"./chunk-QN33PNHL-B0_3_NGo.js";import"./index-DNCGxUDM.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-eem5VR5l.js";import{_ as i}from"./mermaid.core-v0oo9NRr.js";import"./chunk-FMBD7UC4-HblipWIM.js";import"./chunk-55IACEB6-CtULfmDo.js";import"./chunk-QN33PNHL-DSThOC6-.js";import"./index-8cIrvc8q.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-Cq2XT3QN.js";import{_ as i}from"./mermaid.core-CFUktQ8s.js";import"./chunk-FMBD7UC4-BALYpKmy.js";import"./chunk-55IACEB6-fbeP3Dtn.js";import"./chunk-QN33PNHL-B0_3_NGo.js";import"./index-DNCGxUDM.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-eem5VR5l.js";import{_ as i}from"./mermaid.core-v0oo9NRr.js";import"./chunk-FMBD7UC4-HblipWIM.js";import"./chunk-55IACEB6-CtULfmDo.js";import"./chunk-QN33PNHL-DSThOC6-.js";import"./index-8cIrvc8q.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{b as r}from"./_baseUniq-T1y-Xwdr.js";var e=4;function a(o){return r(o,e)}export{a as c};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{b as r}from"./_baseUniq-DAOs4kUj.js";var e=4;function a(o){return r(o,e)}export{a as c};
|
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{s as k,g as R,q as E,p as F,a as I,b as _,_ as l,H as D,y as G,D as f,E as P,F as C,l as z,K as H}from"./mermaid.core-v0oo9NRr.js";import{p as V}from"./chunk-4BX2VUAB-DWDvTYfd.js";import{p as W}from"./treemap-GDKQZRPO-DJjQsbt8.js";import"./index-8cIrvc8q.js";import"./_baseUniq-DAOs4kUj.js";import"./_basePickBy-CL4iQUG-.js";import"./clone-C1u3K6Fy.js";var h={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:h},m=structuredClone(w),B=P.radar,j=l(()=>f({...B,...C().radar}),"getConfig"),b=l(()=>m.axes,"getAxes"),q=l(()=>m.curves,"getCurves"),K=l(()=>m.options,"getOptions"),N=l(a=>{m.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),U=l(a=>{m.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:X(t.entries)}))},"setCurves"),X=l(a=>{if(a[0].axis==null)return a.map(e=>e.value);const t=b();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(e=>{const r=a.find(s=>s.axis?.$refText===e.name);if(r===void 0)throw new Error("Missing entry for axis "+e.label);return r.value})},"computeCurveEntries"),Y=l(a=>{const t=a.reduce((e,r)=>(e[r.name]=r,e),{});m.options={showLegend:t.showLegend?.value??h.showLegend,ticks:t.ticks?.value??h.ticks,max:t.max?.value??h.max,min:t.min?.value??h.min,graticule:t.graticule?.value??h.graticule}},"setOptions"),Z=l(()=>{G(),m=structuredClone(w)},"clear"),$={getAxes:b,getCurves:q,getOptions:K,setAxes:N,setCurves:U,setOptions:Y,getConfig:j,clear:Z,setAccTitle:_,getAccTitle:I,setDiagramTitle:F,getDiagramTitle:E,getAccDescription:R,setAccDescription:k},J=l(a=>{V(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),Q={parse:l(async a=>{const t=await W("radar",a);z.debug(t),J(t)},"parse")},tt=l((a,t,e,r)=>{const s=r.db,o=s.getAxes(),i=s.getCurves(),n=s.getOptions(),c=s.getConfig(),d=s.getDiagramTitle(),u=D(t),p=et(u,c),g=n.max??Math.max(...i.map(y=>Math.max(...y.entries))),x=n.min,v=Math.min(c.width,c.height)/2;at(p,o,v,n.ticks,n.graticule),rt(p,o,v,c),M(p,o,i,x,g,n.graticule,c),T(p,i,n.showLegend,c),p.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-c.height/2-c.marginTop)},"draw"),et=l((a,t)=>{const e=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return a.attr("viewbox",`0 0 ${e} ${r}`).attr("width",e).attr("height",r),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),at=l((a,t,e,r,s)=>{if(s==="circle")for(let o=0;o<r;o++){const i=e*(o+1)/r;a.append("circle").attr("r",i).attr("class","radarGraticule")}else if(s==="polygon"){const o=t.length;for(let i=0;i<r;i++){const n=e*(i+1)/r,c=t.map((d,u)=>{const p=2*u*Math.PI/o-Math.PI/2,g=n*Math.cos(p),x=n*Math.sin(p);return`${g},${x}`}).join(" ");a.append("polygon").attr("points",c).attr("class","radarGraticule")}}},"drawGraticule"),rt=l((a,t,e,r)=>{const s=t.length;for(let o=0;o<s;o++){const i=t[o].label,n=2*o*Math.PI/s-Math.PI/2;a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*Math.cos(n)).attr("y2",e*r.axisScaleFactor*Math.sin(n)).attr("class","radarAxisLine"),a.append("text").text(i).attr("x",e*r.axisLabelFactor*Math.cos(n)).attr("y",e*r.axisLabelFactor*Math.sin(n)).attr("class","radarAxisLabel")}},"drawAxes");function M(a,t,e,r,s,o,i){const n=t.length,c=Math.min(i.width,i.height)/2;e.forEach((d,u)=>{if(d.entries.length!==n)return;const p=d.entries.map((g,x)=>{const v=2*Math.PI*x/n-Math.PI/2,y=A(g,r,s,c),O=y*Math.cos(v),S=y*Math.sin(v);return{x:O,y:S}});o==="circle"?a.append("path").attr("d",L(p,i.curveTension)).attr("class",`radarCurve-${u}`):o==="polygon"&&a.append("polygon").attr("points",p.map(g=>`${g.x},${g.y}`).join(" ")).attr("class",`radarCurve-${u}`)})}l(M,"drawCurves");function A(a,t,e,r){const s=Math.min(Math.max(a,t),e);return r*(s-t)/(e-t)}l(A,"relativeRadius");function L(a,t){const e=a.length;let r=`M${a[0].x},${a[0].y}`;for(let s=0;s<e;s++){const o=a[(s-1+e)%e],i=a[s],n=a[(s+1)%e],c=a[(s+2)%e],d={x:i.x+(n.x-o.x)*t,y:i.y+(n.y-o.y)*t},u={x:n.x-(c.x-i.x)*t,y:n.y-(c.y-i.y)*t};r+=` C${d.x},${d.y} ${u.x},${u.y} ${n.x},${n.y}`}return`${r} Z`}l(L,"closedRoundCurve");function T(a,t,e,r){if(!e)return;const s=(r.width/2+r.marginRight)*3/4,o=-(r.height/2+r.marginTop)*3/4,i=20;t.forEach((n,c)=>{const d=a.append("g").attr("transform",`translate(${s}, ${o+c*i})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${c}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}l(T,"drawLegend");var st={draw:tt},nt=l((a,t)=>{let e="";for(let r=0;r<a.THEME_COLOR_LIMIT;r++){const s=a[`cScale${r}`];e+=`
|
import{s as k,g as R,q as E,p as F,a as I,b as _,_ as l,H as D,y as G,D as f,E as P,F as C,l as z,K as H}from"./mermaid.core-CFUktQ8s.js";import{p as V}from"./chunk-4BX2VUAB-DHPPu6Xd.js";import{p as W}from"./treemap-GDKQZRPO-DWvWdchV.js";import"./index-DNCGxUDM.js";import"./_baseUniq-T1y-Xwdr.js";import"./_basePickBy-8KTOl_Ov.js";import"./clone-4l7SFgdX.js";var h={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:h},m=structuredClone(w),B=P.radar,j=l(()=>f({...B,...C().radar}),"getConfig"),b=l(()=>m.axes,"getAxes"),q=l(()=>m.curves,"getCurves"),K=l(()=>m.options,"getOptions"),N=l(a=>{m.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),U=l(a=>{m.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:X(t.entries)}))},"setCurves"),X=l(a=>{if(a[0].axis==null)return a.map(e=>e.value);const t=b();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(e=>{const r=a.find(s=>s.axis?.$refText===e.name);if(r===void 0)throw new Error("Missing entry for axis "+e.label);return r.value})},"computeCurveEntries"),Y=l(a=>{const t=a.reduce((e,r)=>(e[r.name]=r,e),{});m.options={showLegend:t.showLegend?.value??h.showLegend,ticks:t.ticks?.value??h.ticks,max:t.max?.value??h.max,min:t.min?.value??h.min,graticule:t.graticule?.value??h.graticule}},"setOptions"),Z=l(()=>{G(),m=structuredClone(w)},"clear"),$={getAxes:b,getCurves:q,getOptions:K,setAxes:N,setCurves:U,setOptions:Y,getConfig:j,clear:Z,setAccTitle:_,getAccTitle:I,setDiagramTitle:F,getDiagramTitle:E,getAccDescription:R,setAccDescription:k},J=l(a=>{V(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),Q={parse:l(async a=>{const t=await W("radar",a);z.debug(t),J(t)},"parse")},tt=l((a,t,e,r)=>{const s=r.db,o=s.getAxes(),i=s.getCurves(),n=s.getOptions(),c=s.getConfig(),d=s.getDiagramTitle(),u=D(t),p=et(u,c),g=n.max??Math.max(...i.map(y=>Math.max(...y.entries))),x=n.min,v=Math.min(c.width,c.height)/2;at(p,o,v,n.ticks,n.graticule),rt(p,o,v,c),M(p,o,i,x,g,n.graticule,c),T(p,i,n.showLegend,c),p.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-c.height/2-c.marginTop)},"draw"),et=l((a,t)=>{const e=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return a.attr("viewbox",`0 0 ${e} ${r}`).attr("width",e).attr("height",r),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),at=l((a,t,e,r,s)=>{if(s==="circle")for(let o=0;o<r;o++){const i=e*(o+1)/r;a.append("circle").attr("r",i).attr("class","radarGraticule")}else if(s==="polygon"){const o=t.length;for(let i=0;i<r;i++){const n=e*(i+1)/r,c=t.map((d,u)=>{const p=2*u*Math.PI/o-Math.PI/2,g=n*Math.cos(p),x=n*Math.sin(p);return`${g},${x}`}).join(" ");a.append("polygon").attr("points",c).attr("class","radarGraticule")}}},"drawGraticule"),rt=l((a,t,e,r)=>{const s=t.length;for(let o=0;o<s;o++){const i=t[o].label,n=2*o*Math.PI/s-Math.PI/2;a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*Math.cos(n)).attr("y2",e*r.axisScaleFactor*Math.sin(n)).attr("class","radarAxisLine"),a.append("text").text(i).attr("x",e*r.axisLabelFactor*Math.cos(n)).attr("y",e*r.axisLabelFactor*Math.sin(n)).attr("class","radarAxisLabel")}},"drawAxes");function M(a,t,e,r,s,o,i){const n=t.length,c=Math.min(i.width,i.height)/2;e.forEach((d,u)=>{if(d.entries.length!==n)return;const p=d.entries.map((g,x)=>{const v=2*Math.PI*x/n-Math.PI/2,y=A(g,r,s,c),O=y*Math.cos(v),S=y*Math.sin(v);return{x:O,y:S}});o==="circle"?a.append("path").attr("d",L(p,i.curveTension)).attr("class",`radarCurve-${u}`):o==="polygon"&&a.append("polygon").attr("points",p.map(g=>`${g.x},${g.y}`).join(" ")).attr("class",`radarCurve-${u}`)})}l(M,"drawCurves");function A(a,t,e,r){const s=Math.min(Math.max(a,t),e);return r*(s-t)/(e-t)}l(A,"relativeRadius");function L(a,t){const e=a.length;let r=`M${a[0].x},${a[0].y}`;for(let s=0;s<e;s++){const o=a[(s-1+e)%e],i=a[s],n=a[(s+1)%e],c=a[(s+2)%e],d={x:i.x+(n.x-o.x)*t,y:i.y+(n.y-o.y)*t},u={x:n.x-(c.x-i.x)*t,y:n.y-(c.y-i.y)*t};r+=` C${d.x},${d.y} ${u.x},${u.y} ${n.x},${n.y}`}return`${r} Z`}l(L,"closedRoundCurve");function T(a,t,e,r){if(!e)return;const s=(r.width/2+r.marginRight)*3/4,o=-(r.height/2+r.marginTop)*3/4,i=20;t.forEach((n,c)=>{const d=a.append("g").attr("transform",`translate(${s}, ${o+c*i})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${c}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}l(T,"drawLegend");var st={draw:tt},nt=l((a,t)=>{let e="";for(let r=0;r<a.THEME_COLOR_LIMIT;r++){const s=a[`cScale${r}`];e+=`
|
||||||
.radarCurve-${r} {
|
.radarCurve-${r} {
|
||||||
color: ${s};
|
color: ${s};
|
||||||
fill: ${s};
|
fill: ${s};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{_ as b,D as m,H as B,e as C,l as w,b as S,a as D,p as T,q as E,g as F,s as P,E as z,F as A,y as W}from"./mermaid.core-v0oo9NRr.js";import{p as _}from"./chunk-4BX2VUAB-DWDvTYfd.js";import{p as N}from"./treemap-GDKQZRPO-DJjQsbt8.js";import"./index-8cIrvc8q.js";import"./_baseUniq-DAOs4kUj.js";import"./_basePickBy-CL4iQUG-.js";import"./clone-C1u3K6Fy.js";var L=z.packet,u,v=(u=class{constructor(){this.packet=[],this.setAccTitle=S,this.getAccTitle=D,this.setDiagramTitle=T,this.getDiagramTitle=E,this.getAccDescription=F,this.setAccDescription=P}getConfig(){const t=m({...L,...A().packet});return t.showBits&&(t.paddingY+=10),t}getPacket(){return this.packet}pushWord(t){t.length>0&&this.packet.push(t)}clear(){W(),this.packet=[]}},b(u,"PacketDB"),u),M=1e4,Y=b((e,t)=>{_(e,t);let r=-1,o=[],n=1;const{bitsPerRow:l}=t.getConfig();for(let{start:a,end:i,bits:d,label:c}of e.blocks){if(a!==void 0&&i!==void 0&&i<a)throw new Error(`Packet block ${a} - ${i} is invalid. End must be greater than start.`);if(a??=r+1,a!==r+1)throw new Error(`Packet block ${a} - ${i??a} is not contiguous. It should start from ${r+1}.`);if(d===0)throw new Error(`Packet block ${a} is invalid. Cannot have a zero bit field.`);for(i??=a+(d??1)-1,d??=i-a+1,r=i,w.debug(`Packet block ${a} - ${r} with label ${c}`);o.length<=l+1&&t.getPacket().length<M;){const[p,s]=H({start:a,end:i,bits:d,label:c},n,l);if(o.push(p),p.end+1===n*l&&(t.pushWord(o),o=[],n++),!s)break;({start:a,end:i,bits:d,label:c}=s)}}t.pushWord(o)},"populate"),H=b((e,t,r)=>{if(e.start===void 0)throw new Error("start should have been set during first phase");if(e.end===void 0)throw new Error("end should have been set during first phase");if(e.start>e.end)throw new Error(`Block start ${e.start} is greater than block end ${e.end}.`);if(e.end+1<=t*r)return[e,void 0];const o=t*r-1,n=t*r;return[{start:e.start,end:o,label:e.label,bits:o-e.start},{start:n,end:e.end,label:e.label,bits:e.end-n}]},"getNextFittingBlock"),x={parser:{yy:void 0},parse:b(async e=>{const t=await N("packet",e),r=x.parser?.yy;if(!(r instanceof v))throw new Error("parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");w.debug(t),Y(t,r)},"parse")},I=b((e,t,r,o)=>{const n=o.db,l=n.getConfig(),{rowHeight:a,paddingY:i,bitWidth:d,bitsPerRow:c}=l,p=n.getPacket(),s=n.getDiagramTitle(),h=a+i,g=h*(p.length+1)-(s?0:a),k=d*c+2,f=B(t);f.attr("viewbox",`0 0 ${k} ${g}`),C(f,g,k,l.useMaxWidth);for(const[y,$]of p.entries())O(f,$,y,l);f.append("text").text(s).attr("x",k/2).attr("y",g-h/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),O=b((e,t,r,{rowHeight:o,paddingX:n,paddingY:l,bitWidth:a,bitsPerRow:i,showBits:d})=>{const c=e.append("g"),p=r*(o+l)+l;for(const s of t){const h=s.start%i*a+1,g=(s.end-s.start+1)*a-n;if(c.append("rect").attr("x",h).attr("y",p).attr("width",g).attr("height",o).attr("class","packetBlock"),c.append("text").attr("x",h+g/2).attr("y",p+o/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(s.label),!d)continue;const k=s.end===s.start,f=p-2;c.append("text").attr("x",h+(k?g/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(s.start),k||c.append("text").attr("x",h+g).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(s.end)}},"drawWord"),j={draw:I},q={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},G=b(({packet:e}={})=>{const t=m(q,e);return`
|
import{_ as b,D as m,H as B,e as C,l as w,b as S,a as D,p as T,q as E,g as F,s as P,E as z,F as A,y as W}from"./mermaid.core-CFUktQ8s.js";import{p as _}from"./chunk-4BX2VUAB-DHPPu6Xd.js";import{p as N}from"./treemap-GDKQZRPO-DWvWdchV.js";import"./index-DNCGxUDM.js";import"./_baseUniq-T1y-Xwdr.js";import"./_basePickBy-8KTOl_Ov.js";import"./clone-4l7SFgdX.js";var L=z.packet,u,v=(u=class{constructor(){this.packet=[],this.setAccTitle=S,this.getAccTitle=D,this.setDiagramTitle=T,this.getDiagramTitle=E,this.getAccDescription=F,this.setAccDescription=P}getConfig(){const t=m({...L,...A().packet});return t.showBits&&(t.paddingY+=10),t}getPacket(){return this.packet}pushWord(t){t.length>0&&this.packet.push(t)}clear(){W(),this.packet=[]}},b(u,"PacketDB"),u),M=1e4,Y=b((e,t)=>{_(e,t);let r=-1,o=[],n=1;const{bitsPerRow:l}=t.getConfig();for(let{start:a,end:i,bits:d,label:c}of e.blocks){if(a!==void 0&&i!==void 0&&i<a)throw new Error(`Packet block ${a} - ${i} is invalid. End must be greater than start.`);if(a??=r+1,a!==r+1)throw new Error(`Packet block ${a} - ${i??a} is not contiguous. It should start from ${r+1}.`);if(d===0)throw new Error(`Packet block ${a} is invalid. Cannot have a zero bit field.`);for(i??=a+(d??1)-1,d??=i-a+1,r=i,w.debug(`Packet block ${a} - ${r} with label ${c}`);o.length<=l+1&&t.getPacket().length<M;){const[p,s]=H({start:a,end:i,bits:d,label:c},n,l);if(o.push(p),p.end+1===n*l&&(t.pushWord(o),o=[],n++),!s)break;({start:a,end:i,bits:d,label:c}=s)}}t.pushWord(o)},"populate"),H=b((e,t,r)=>{if(e.start===void 0)throw new Error("start should have been set during first phase");if(e.end===void 0)throw new Error("end should have been set during first phase");if(e.start>e.end)throw new Error(`Block start ${e.start} is greater than block end ${e.end}.`);if(e.end+1<=t*r)return[e,void 0];const o=t*r-1,n=t*r;return[{start:e.start,end:o,label:e.label,bits:o-e.start},{start:n,end:e.end,label:e.label,bits:e.end-n}]},"getNextFittingBlock"),x={parser:{yy:void 0},parse:b(async e=>{const t=await N("packet",e),r=x.parser?.yy;if(!(r instanceof v))throw new Error("parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");w.debug(t),Y(t,r)},"parse")},I=b((e,t,r,o)=>{const n=o.db,l=n.getConfig(),{rowHeight:a,paddingY:i,bitWidth:d,bitsPerRow:c}=l,p=n.getPacket(),s=n.getDiagramTitle(),h=a+i,g=h*(p.length+1)-(s?0:a),k=d*c+2,f=B(t);f.attr("viewbox",`0 0 ${k} ${g}`),C(f,g,k,l.useMaxWidth);for(const[y,$]of p.entries())O(f,$,y,l);f.append("text").text(s).attr("x",k/2).attr("y",g-h/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),O=b((e,t,r,{rowHeight:o,paddingX:n,paddingY:l,bitWidth:a,bitsPerRow:i,showBits:d})=>{const c=e.append("g"),p=r*(o+l)+l;for(const s of t){const h=s.start%i*a+1,g=(s.end-s.start+1)*a-n;if(c.append("rect").attr("x",h).attr("y",p).attr("width",g).attr("height",o).attr("class","packetBlock"),c.append("text").attr("x",h+g/2).attr("y",p+o/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(s.label),!d)continue;const k=s.end===s.start,f=p-2;c.append("text").attr("x",h+(k?g/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(s.start),k||c.append("text").attr("x",h+g).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(s.end)}},"drawWord"),j={draw:I},q={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},G=b(({packet:e}={})=>{const t=m(q,e);return`
|
||||||
.packetByte {
|
.packetByte {
|
||||||
font-size: ${t.byteFontSize};
|
font-size: ${t.byteFontSize};
|
||||||
}
|
}
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/FilmGrid-BM-3a1vS.js","assets/FilmGrid.vue_vue_type_script_setup_true_lang-Dj0SEfcW.js","assets/index-8cIrvc8q.js","assets/index-BJkaQ2c4.css","assets/useContentImages-7wLVntsF.js","assets/FilmDetail-0aNPT6Ze.js","assets/FilmDetail.vue_vue_type_script_setup_true_lang-BhKlPG3Y.js"])))=>i.map(i=>d[i]);
|
|
||||||
import{d as e,_ as r}from"./index-8cIrvc8q.js";const _={id:"film",name:"Film Renderer",contentType:"film",surfaces:["chat-preview","panel-preview","panel-play"],chatPreview:e(()=>r(()=>import("./FilmGrid-BM-3a1vS.js"),__vite__mapDeps([0,1,2,3,4]))),panelPreview:e(()=>r(()=>import("./FilmGrid-BM-3a1vS.js"),__vite__mapDeps([0,1,2,3,4]))),panelPlay:e(()=>r(()=>import("./FilmDetail-0aNPT6Ze.js"),__vite__mapDeps([5,6,2,3])))};export{_ as filmRenderer};
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/FilmGrid-Dz4qYSg3.js","assets/FilmGrid.vue_vue_type_script_setup_true_lang-CkIQ4bRp.js","assets/index-DNCGxUDM.js","assets/index-BJh-vGUe.css","assets/useContentImages-DdjyABL9.js","assets/FilmDetail-D9AyPH42.js","assets/FilmDetail.vue_vue_type_script_setup_true_lang-Cgf4f-Ng.js"])))=>i.map(i=>d[i]);
|
||||||
|
import{d as e,_ as r}from"./index-DNCGxUDM.js";const _={id:"film",name:"Film Renderer",contentType:"film",surfaces:["chat-preview","panel-preview","panel-play"],chatPreview:e(()=>r(()=>import("./FilmGrid-Dz4qYSg3.js"),__vite__mapDeps([0,1,2,3,4]))),panelPreview:e(()=>r(()=>import("./FilmGrid-Dz4qYSg3.js"),__vite__mapDeps([0,1,2,3,4]))),panelPlay:e(()=>r(()=>import("./FilmDetail-D9AyPH42.js"),__vite__mapDeps([5,6,2,3])))};export{_ as filmRenderer};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{g as qt}from"./chunk-FMBD7UC4-HblipWIM.js";import{_ as m,n as Ot,l as $,c as b1,d as E1,o as Ht,r as Xt,u as it,b as Qt,s as Jt,p as Zt,a as $t,g as te,q as ee,k as se,t as ie,J as re,v as ae,x as st,y as ne,z as ue,A as oe}from"./mermaid.core-v0oo9NRr.js";import{g as le}from"./chunk-55IACEB6-CtULfmDo.js";import{s as ce}from"./chunk-QN33PNHL-DSThOC6-.js";import{c as he}from"./channel-Dg2Em7BA.js";import"./index-8cIrvc8q.js";var de="flowchart-",G1,pe=(G1=class{constructor(){this.vertexCounter=0,this.config=b1(),this.vertices=new Map,this.edges=[],this.classes=new Map,this.subGraphs=[],this.subGraphLookup=new Map,this.tooltips=new Map,this.subCount=0,this.firstGraphFlag=!0,this.secCount=-1,this.posCrossRef=[],this.funs=[],this.setAccTitle=Qt,this.setAccDescription=Jt,this.setDiagramTitle=Zt,this.getAccTitle=$t,this.getAccDescription=te,this.getDiagramTitle=ee,this.funs.push(this.setupToolTips.bind(this)),this.addVertex=this.addVertex.bind(this),this.firstGraph=this.firstGraph.bind(this),this.setDirection=this.setDirection.bind(this),this.addSubGraph=this.addSubGraph.bind(this),this.addLink=this.addLink.bind(this),this.setLink=this.setLink.bind(this),this.updateLink=this.updateLink.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.destructLink=this.destructLink.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setTooltip=this.setTooltip.bind(this),this.updateLinkInterpolate=this.updateLinkInterpolate.bind(this),this.setClickFun=this.setClickFun.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.lex={firstGraph:this.firstGraph.bind(this)},this.clear(),this.setGen("gen-2")}sanitizeText(i){return se.sanitizeText(i,this.config)}lookUpDomId(i){for(const r of this.vertices.values())if(r.id===i)return r.domId;return i}addVertex(i,r,a,n,l,g,c={},b){if(!i||i.trim().length===0)return;let u;if(b!==void 0){let k;b.includes(`
|
import{g as qt}from"./chunk-FMBD7UC4-BALYpKmy.js";import{_ as m,n as Ot,l as $,c as b1,d as E1,o as Ht,r as Xt,u as it,b as Qt,s as Jt,p as Zt,a as $t,g as te,q as ee,k as se,t as ie,J as re,v as ae,x as st,y as ne,z as ue,A as oe}from"./mermaid.core-CFUktQ8s.js";import{g as le}from"./chunk-55IACEB6-fbeP3Dtn.js";import{s as ce}from"./chunk-QN33PNHL-B0_3_NGo.js";import{c as he}from"./channel-BXJ3-Z4Z.js";import"./index-DNCGxUDM.js";var de="flowchart-",G1,pe=(G1=class{constructor(){this.vertexCounter=0,this.config=b1(),this.vertices=new Map,this.edges=[],this.classes=new Map,this.subGraphs=[],this.subGraphLookup=new Map,this.tooltips=new Map,this.subCount=0,this.firstGraphFlag=!0,this.secCount=-1,this.posCrossRef=[],this.funs=[],this.setAccTitle=Qt,this.setAccDescription=Jt,this.setDiagramTitle=Zt,this.getAccTitle=$t,this.getAccDescription=te,this.getDiagramTitle=ee,this.funs.push(this.setupToolTips.bind(this)),this.addVertex=this.addVertex.bind(this),this.firstGraph=this.firstGraph.bind(this),this.setDirection=this.setDirection.bind(this),this.addSubGraph=this.addSubGraph.bind(this),this.addLink=this.addLink.bind(this),this.setLink=this.setLink.bind(this),this.updateLink=this.updateLink.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.destructLink=this.destructLink.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setTooltip=this.setTooltip.bind(this),this.updateLinkInterpolate=this.updateLinkInterpolate.bind(this),this.setClickFun=this.setClickFun.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.lex={firstGraph:this.firstGraph.bind(this)},this.clear(),this.setGen("gen-2")}sanitizeText(i){return se.sanitizeText(i,this.config)}lookUpDomId(i){for(const r of this.vertices.values())if(r.id===i)return r.domId;return i}addVertex(i,r,a,n,l,g,c={},b){if(!i||i.trim().length===0)return;let u;if(b!==void 0){let k;b.includes(`
|
||||||
`)?k=b+`
|
`)?k=b+`
|
||||||
`:k=`{
|
`:k=`{
|
||||||
`+b+`
|
`+b+`
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
import{_ as e,l as s,H as n,e as i,I as p}from"./mermaid.core-v0oo9NRr.js";import{p as g}from"./treemap-GDKQZRPO-DJjQsbt8.js";import"./index-8cIrvc8q.js";import"./_baseUniq-DAOs4kUj.js";import"./_basePickBy-CL4iQUG-.js";import"./clone-C1u3K6Fy.js";var v={parse:e(async r=>{const a=await g("info",r);s.debug(a)},"parse")},d={version:p.version+""},m=e(()=>d.version,"getVersion"),c={getVersion:m},l=e((r,a,o)=>{s.debug(`rendering info diagram
|
import{_ as e,l as s,H as n,e as i,I as p}from"./mermaid.core-CFUktQ8s.js";import{p as g}from"./treemap-GDKQZRPO-DWvWdchV.js";import"./index-DNCGxUDM.js";import"./_baseUniq-T1y-Xwdr.js";import"./_basePickBy-8KTOl_Ov.js";import"./clone-4l7SFgdX.js";var v={parse:e(async r=>{const a=await g("info",r);s.debug(a)},"parse")},d={version:p.version+""},m=e(()=>d.version,"getVersion"),c={getVersion:m},l=e((r,a,o)=>{s.debug(`rendering info diagram
|
||||||
`+r);const t=n(a);i(t,100,400,!0),t.append("g").append("text").attr("x",100).attr("y",40).attr("class","version").attr("font-size",32).style("text-anchor","middle").text(`v${o}`)},"draw"),f={draw:l},z={parser:v,db:c,renderer:f};export{z as diagram};
|
`+r);const t=n(a);i(t,100,400,!0),t.append("g").append("text").attr("x",100).attr("y",40).attr("class","version").attr("font-size",32).style("text-anchor","middle").text(`v${o}`)},"draw"),f={draw:l},z={parser:v,db:c,renderer:f};export{z as diagram};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{a as gt,g as lt,f as mt,d as xt}from"./chunk-TZMSLE5B-93PKdbpb.js";import{g as kt}from"./chunk-FMBD7UC4-HblipWIM.js";import{g as _t,s as vt,a as bt,b as wt,q as Tt,p as St,_ as n,c as R,d as X,e as $t,y as Mt}from"./mermaid.core-v0oo9NRr.js";import{d as et}from"./arc-UjuE1bPP.js";import"./index-8cIrvc8q.js";var U=(function(){var t=n(function(h,i,a,l){for(a=a||{},l=h.length;l--;a[h[l]]=i);return a},"o"),e=[6,8,10,11,12,14,16,17,18],s=[1,9],c=[1,10],r=[1,11],f=[1,12],u=[1,13],y=[1,14],g={trace:n(function(){},"trace"),yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,taskName:18,taskData:19,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",18:"taskName",19:"taskData"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,2]],performAction:n(function(i,a,l,d,p,o,b){var k=o.length-1;switch(p){case 1:return o[k-1];case 2:this.$=[];break;case 3:o[k-1].push(o[k]),this.$=o[k-1];break;case 4:case 5:this.$=o[k];break;case 6:case 7:this.$=[];break;case 8:d.setDiagramTitle(o[k].substr(6)),this.$=o[k].substr(6);break;case 9:this.$=o[k].trim(),d.setAccTitle(this.$);break;case 10:case 11:this.$=o[k].trim(),d.setAccDescription(this.$);break;case 12:d.addSection(o[k].substr(8)),this.$=o[k].substr(8);break;case 13:d.addTask(o[k-1],o[k]),this.$="task";break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},t(e,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:s,12:c,14:r,16:f,17:u,18:y},t(e,[2,7],{1:[2,1]}),t(e,[2,3]),{9:15,11:s,12:c,14:r,16:f,17:u,18:y},t(e,[2,5]),t(e,[2,6]),t(e,[2,8]),{13:[1,16]},{15:[1,17]},t(e,[2,11]),t(e,[2,12]),{19:[1,18]},t(e,[2,4]),t(e,[2,9]),t(e,[2,10]),t(e,[2,13])],defaultActions:{},parseError:n(function(i,a){if(a.recoverable)this.trace(i);else{var l=new Error(i);throw l.hash=a,l}},"parseError"),parse:n(function(i){var a=this,l=[0],d=[],p=[null],o=[],b=this.table,k="",C=0,K=0,dt=2,Q=1,yt=o.slice.call(arguments,1),_=Object.create(this.lexer),I={yy:{}};for(var O in this.yy)Object.prototype.hasOwnProperty.call(this.yy,O)&&(I.yy[O]=this.yy[O]);_.setInput(i,I.yy),I.yy.lexer=_,I.yy.parser=this,typeof _.yylloc>"u"&&(_.yylloc={});var Y=_.yylloc;o.push(Y);var ft=_.options&&_.options.ranges;typeof I.yy.parseError=="function"?this.parseError=I.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function pt(w){l.length=l.length-2*w,p.length=p.length-w,o.length=o.length-w}n(pt,"popStack");function D(){var w;return w=d.pop()||_.lex()||Q,typeof w!="number"&&(w instanceof Array&&(d=w,w=d.pop()),w=a.symbols_[w]||w),w}n(D,"lex");for(var v,A,T,q,F={},N,M,tt,z;;){if(A=l[l.length-1],this.defaultActions[A]?T=this.defaultActions[A]:((v===null||typeof v>"u")&&(v=D()),T=b[A]&&b[A][v]),typeof T>"u"||!T.length||!T[0]){var H="";z=[];for(N in b[A])this.terminals_[N]&&N>dt&&z.push("'"+this.terminals_[N]+"'");_.showPosition?H="Parse error on line "+(C+1)+`:
|
import{a as gt,g as lt,f as mt,d as xt}from"./chunk-TZMSLE5B-Cajwq1yU.js";import{g as kt}from"./chunk-FMBD7UC4-BALYpKmy.js";import{g as _t,s as vt,a as bt,b as wt,q as Tt,p as St,_ as n,c as R,d as X,e as $t,y as Mt}from"./mermaid.core-CFUktQ8s.js";import{d as et}from"./arc-P9DEh39j.js";import"./index-DNCGxUDM.js";var U=(function(){var t=n(function(h,i,a,l){for(a=a||{},l=h.length;l--;a[h[l]]=i);return a},"o"),e=[6,8,10,11,12,14,16,17,18],s=[1,9],c=[1,10],r=[1,11],f=[1,12],u=[1,13],y=[1,14],g={trace:n(function(){},"trace"),yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,taskName:18,taskData:19,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",18:"taskName",19:"taskData"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,2]],performAction:n(function(i,a,l,d,p,o,b){var k=o.length-1;switch(p){case 1:return o[k-1];case 2:this.$=[];break;case 3:o[k-1].push(o[k]),this.$=o[k-1];break;case 4:case 5:this.$=o[k];break;case 6:case 7:this.$=[];break;case 8:d.setDiagramTitle(o[k].substr(6)),this.$=o[k].substr(6);break;case 9:this.$=o[k].trim(),d.setAccTitle(this.$);break;case 10:case 11:this.$=o[k].trim(),d.setAccDescription(this.$);break;case 12:d.addSection(o[k].substr(8)),this.$=o[k].substr(8);break;case 13:d.addTask(o[k-1],o[k]),this.$="task";break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},t(e,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:s,12:c,14:r,16:f,17:u,18:y},t(e,[2,7],{1:[2,1]}),t(e,[2,3]),{9:15,11:s,12:c,14:r,16:f,17:u,18:y},t(e,[2,5]),t(e,[2,6]),t(e,[2,8]),{13:[1,16]},{15:[1,17]},t(e,[2,11]),t(e,[2,12]),{19:[1,18]},t(e,[2,4]),t(e,[2,9]),t(e,[2,10]),t(e,[2,13])],defaultActions:{},parseError:n(function(i,a){if(a.recoverable)this.trace(i);else{var l=new Error(i);throw l.hash=a,l}},"parseError"),parse:n(function(i){var a=this,l=[0],d=[],p=[null],o=[],b=this.table,k="",C=0,K=0,dt=2,Q=1,yt=o.slice.call(arguments,1),_=Object.create(this.lexer),I={yy:{}};for(var O in this.yy)Object.prototype.hasOwnProperty.call(this.yy,O)&&(I.yy[O]=this.yy[O]);_.setInput(i,I.yy),I.yy.lexer=_,I.yy.parser=this,typeof _.yylloc>"u"&&(_.yylloc={});var Y=_.yylloc;o.push(Y);var ft=_.options&&_.options.ranges;typeof I.yy.parseError=="function"?this.parseError=I.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function pt(w){l.length=l.length-2*w,p.length=p.length-w,o.length=o.length-w}n(pt,"popStack");function D(){var w;return w=d.pop()||_.lex()||Q,typeof w!="number"&&(w instanceof Array&&(d=w,w=d.pop()),w=a.symbols_[w]||w),w}n(D,"lex");for(var v,A,T,q,F={},N,M,tt,z;;){if(A=l[l.length-1],this.defaultActions[A]?T=this.defaultActions[A]:((v===null||typeof v>"u")&&(v=D()),T=b[A]&&b[A][v]),typeof T>"u"||!T.length||!T[0]){var H="";z=[];for(N in b[A])this.terminals_[N]&&N>dt&&z.push("'"+this.terminals_[N]+"'");_.showPosition?H="Parse error on line "+(C+1)+`:
|
||||||
`+_.showPosition()+`
|
`+_.showPosition()+`
|
||||||
Expecting `+z.join(", ")+", got '"+(this.terminals_[v]||v)+"'":H="Parse error on line "+(C+1)+": Unexpected "+(v==Q?"end of input":"'"+(this.terminals_[v]||v)+"'"),this.parseError(H,{text:_.match,token:this.terminals_[v]||v,line:_.yylineno,loc:Y,expected:z})}if(T[0]instanceof Array&&T.length>1)throw new Error("Parse Error: multiple actions possible at state: "+A+", token: "+v);switch(T[0]){case 1:l.push(v),p.push(_.yytext),o.push(_.yylloc),l.push(T[1]),v=null,K=_.yyleng,k=_.yytext,C=_.yylineno,Y=_.yylloc;break;case 2:if(M=this.productions_[T[1]][1],F.$=p[p.length-M],F._$={first_line:o[o.length-(M||1)].first_line,last_line:o[o.length-1].last_line,first_column:o[o.length-(M||1)].first_column,last_column:o[o.length-1].last_column},ft&&(F._$.range=[o[o.length-(M||1)].range[0],o[o.length-1].range[1]]),q=this.performAction.apply(F,[k,K,C,I.yy,T[1],p,o].concat(yt)),typeof q<"u")return q;M&&(l=l.slice(0,-1*M*2),p=p.slice(0,-1*M),o=o.slice(0,-1*M)),l.push(this.productions_[T[1]][0]),p.push(F.$),o.push(F._$),tt=b[l[l.length-2]][l[l.length-1]],l.push(tt);break;case 3:return!0}}return!0},"parse")},m=(function(){var h={EOF:1,parseError:n(function(a,l){if(this.yy.parser)this.yy.parser.parseError(a,l);else throw new Error(a)},"parseError"),setInput:n(function(i,a){return this.yy=a||this.yy||{},this._input=i,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:n(function(){var i=this._input[0];this.yytext+=i,this.yyleng++,this.offset++,this.match+=i,this.matched+=i;var a=i.match(/(?:\r\n?|\n).*/g);return a?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),i},"input"),unput:n(function(i){var a=i.length,l=i.split(/(?:\r\n?|\n)/g);this._input=i+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-a),this.offset-=a;var d=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),l.length-1&&(this.yylineno-=l.length-1);var p=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:l?(l.length===d.length?this.yylloc.first_column:0)+d[d.length-l.length].length-l[0].length:this.yylloc.first_column-a},this.options.ranges&&(this.yylloc.range=[p[0],p[0]+this.yyleng-a]),this.yyleng=this.yytext.length,this},"unput"),more:n(function(){return this._more=!0,this},"more"),reject:n(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
|
Expecting `+z.join(", ")+", got '"+(this.terminals_[v]||v)+"'":H="Parse error on line "+(C+1)+": Unexpected "+(v==Q?"end of input":"'"+(this.terminals_[v]||v)+"'"),this.parseError(H,{text:_.match,token:this.terminals_[v]||v,line:_.yylineno,loc:Y,expected:z})}if(T[0]instanceof Array&&T.length>1)throw new Error("Parse Error: multiple actions possible at state: "+A+", token: "+v);switch(T[0]){case 1:l.push(v),p.push(_.yytext),o.push(_.yylloc),l.push(T[1]),v=null,K=_.yyleng,k=_.yytext,C=_.yylineno,Y=_.yylloc;break;case 2:if(M=this.productions_[T[1]][1],F.$=p[p.length-M],F._$={first_line:o[o.length-(M||1)].first_line,last_line:o[o.length-1].last_line,first_column:o[o.length-(M||1)].first_column,last_column:o[o.length-1].last_column},ft&&(F._$.range=[o[o.length-(M||1)].range[0],o[o.length-1].range[1]]),q=this.performAction.apply(F,[k,K,C,I.yy,T[1],p,o].concat(yt)),typeof q<"u")return q;M&&(l=l.slice(0,-1*M*2),p=p.slice(0,-1*M),o=o.slice(0,-1*M)),l.push(this.productions_[T[1]][0]),p.push(F.$),o.push(F._$),tt=b[l[l.length-2]][l[l.length-1]],l.push(tt);break;case 3:return!0}}return!0},"parse")},m=(function(){var h={EOF:1,parseError:n(function(a,l){if(this.yy.parser)this.yy.parser.parseError(a,l);else throw new Error(a)},"parseError"),setInput:n(function(i,a){return this.yy=a||this.yy||{},this._input=i,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:n(function(){var i=this._input[0];this.yytext+=i,this.yyleng++,this.offset++,this.match+=i,this.matched+=i;var a=i.match(/(?:\r\n?|\n).*/g);return a?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),i},"input"),unput:n(function(i){var a=i.length,l=i.split(/(?:\r\n?|\n)/g);this._input=i+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-a),this.offset-=a;var d=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),l.length-1&&(this.yylineno-=l.length-1);var p=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:l?(l.length===d.length?this.yylloc.first_column:0)+d[d.length-l.length].length-l[0].length:this.yylloc.first_column-a},this.options.ranges&&(this.yylloc.range=[p[0],p[0]+this.yyleng-a]),this.yyleng=this.yytext.length,this},"unput"),more:n(function(){return this._more=!0,this},"more"),reject:n(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
|
||||||
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:n(function(i){this.unput(this.match.slice(i))},"less"),pastInput:n(function(){var i=this.matched.substr(0,this.matched.length-this.match.length);return(i.length>20?"...":"")+i.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:n(function(){var i=this.match;return i.length<20&&(i+=this._input.substr(0,20-i.length)),(i.substr(0,20)+(i.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:n(function(){var i=this.pastInput(),a=new Array(i.length+1).join("-");return i+this.upcomingInput()+`
|
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:n(function(i){this.unput(this.match.slice(i))},"less"),pastInput:n(function(){var i=this.matched.substr(0,this.matched.length-this.match.length);return(i.length>20?"...":"")+i.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:n(function(){var i=this.match;return i.length<20&&(i+=this._input.substr(0,20-i.length)),(i.substr(0,20)+(i.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:n(function(){var i=this.pastInput(),a=new Array(i.length+1).join("-");return i+this.upcomingInput()+`
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{_ as o,l as te,c as U,H as fe,ah as ye,ai as be,aj as me,ac as Ee,E as K,i as F,t as _e,J as ke,ad as Se,ae as ce,af as le}from"./mermaid.core-v0oo9NRr.js";import{g as Ne}from"./chunk-FMBD7UC4-HblipWIM.js";import"./index-8cIrvc8q.js";var $=(function(){var e=o(function(O,i,n,r){for(n=n||{},r=O.length;r--;n[O[r]]=i);return n},"o"),u=[1,4],p=[1,13],s=[1,12],d=[1,15],_=[1,16],b=[1,20],l=[1,19],D=[6,7,8],I=[1,26],g=[1,24],w=[1,25],E=[6,7,11],G=[1,31],N=[6,7,11,24],V=[1,6,13,16,17,20,23],m=[1,35],A=[1,36],L=[1,6,7,11,13,16,17,20,23],H=[1,38],T={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,KANBAN:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,shapeData:15,ICON:16,CLASS:17,nodeWithId:18,nodeWithoutId:19,NODE_DSTART:20,NODE_DESCR:21,NODE_DEND:22,NODE_ID:23,SHAPE_DATA:24,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"KANBAN",11:"EOF",13:"SPACELIST",16:"ICON",17:"CLASS",20:"NODE_DSTART",21:"NODE_DESCR",22:"NODE_DEND",23:"NODE_ID",24:"SHAPE_DATA"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,3],[12,2],[12,2],[12,2],[12,1],[12,2],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[19,3],[18,1],[18,4],[15,2],[15,1]],performAction:o(function(i,n,r,a,h,t,M){var c=t.length-1;switch(h){case 6:case 7:return a;case 8:a.getLogger().trace("Stop NL ");break;case 9:a.getLogger().trace("Stop EOF ");break;case 11:a.getLogger().trace("Stop NL2 ");break;case 12:a.getLogger().trace("Stop EOF2 ");break;case 15:a.getLogger().info("Node: ",t[c-1].id),a.addNode(t[c-2].length,t[c-1].id,t[c-1].descr,t[c-1].type,t[c]);break;case 16:a.getLogger().info("Node: ",t[c].id),a.addNode(t[c-1].length,t[c].id,t[c].descr,t[c].type);break;case 17:a.getLogger().trace("Icon: ",t[c]),a.decorateNode({icon:t[c]});break;case 18:case 23:a.decorateNode({class:t[c]});break;case 19:a.getLogger().trace("SPACELIST");break;case 20:a.getLogger().trace("Node: ",t[c-1].id),a.addNode(0,t[c-1].id,t[c-1].descr,t[c-1].type,t[c]);break;case 21:a.getLogger().trace("Node: ",t[c].id),a.addNode(0,t[c].id,t[c].descr,t[c].type);break;case 22:a.decorateNode({icon:t[c]});break;case 27:a.getLogger().trace("node found ..",t[c-2]),this.$={id:t[c-1],descr:t[c-1],type:a.getType(t[c-2],t[c])};break;case 28:this.$={id:t[c],descr:t[c],type:0};break;case 29:a.getLogger().trace("node found ..",t[c-3]),this.$={id:t[c-3],descr:t[c-1],type:a.getType(t[c-2],t[c])};break;case 30:this.$=t[c-1]+t[c];break;case 31:this.$=t[c];break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:u},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:u},{6:p,7:[1,10],9:9,12:11,13:s,14:14,16:d,17:_,18:17,19:18,20:b,23:l},e(D,[2,3]),{1:[2,2]},e(D,[2,4]),e(D,[2,5]),{1:[2,6],6:p,12:21,13:s,14:14,16:d,17:_,18:17,19:18,20:b,23:l},{6:p,9:22,12:11,13:s,14:14,16:d,17:_,18:17,19:18,20:b,23:l},{6:I,7:g,10:23,11:w},e(E,[2,24],{18:17,19:18,14:27,16:[1,28],17:[1,29],20:b,23:l}),e(E,[2,19]),e(E,[2,21],{15:30,24:G}),e(E,[2,22]),e(E,[2,23]),e(N,[2,25]),e(N,[2,26]),e(N,[2,28],{20:[1,32]}),{21:[1,33]},{6:I,7:g,10:34,11:w},{1:[2,7],6:p,12:21,13:s,14:14,16:d,17:_,18:17,19:18,20:b,23:l},e(V,[2,14],{7:m,11:A}),e(L,[2,8]),e(L,[2,9]),e(L,[2,10]),e(E,[2,16],{15:37,24:G}),e(E,[2,17]),e(E,[2,18]),e(E,[2,20],{24:H}),e(N,[2,31]),{21:[1,39]},{22:[1,40]},e(V,[2,13],{7:m,11:A}),e(L,[2,11]),e(L,[2,12]),e(E,[2,15],{24:H}),e(N,[2,30]),{22:[1,41]},e(N,[2,27]),e(N,[2,29])],defaultActions:{2:[2,1],6:[2,2]},parseError:o(function(i,n){if(n.recoverable)this.trace(i);else{var r=new Error(i);throw r.hash=n,r}},"parseError"),parse:o(function(i){var n=this,r=[0],a=[],h=[null],t=[],M=this.table,c="",W=0,se=0,ue=2,re=1,ge=t.slice.call(arguments,1),y=Object.create(this.lexer),R={yy:{}};for(var J in this.yy)Object.prototype.hasOwnProperty.call(this.yy,J)&&(R.yy[J]=this.yy[J]);y.setInput(i,R.yy),R.yy.lexer=y,R.yy.parser=this,typeof y.yylloc>"u"&&(y.yylloc={});var q=y.yylloc;t.push(q);var de=y.options&&y.options.ranges;typeof R.yy.parseError=="function"?this.parseError=R.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function pe(S){r.length=r.length-2*S,h.length=h.length-S,t.length=t.length-S}o(pe,"popStack");function ae(){var S;return S=a.pop()||y.lex()||re,typeof S!="number"&&(S instanceof Array&&(a=S,S=a.pop()),S=n.symbols_[S]||S),S}o(ae,"lex");for(var k,P,x,Q,j={},z,C,oe,X;;){if(P=r[r.length-1],this.defaultActions[P]?x=this.defaultActions[P]:((k===null||typeof k>"u")&&(k=ae()),x=M[P]&&M[P][k]),typeof x>"u"||!x.length||!x[0]){var Z="";X=[];for(z in M[P])this.terminals_[z]&&z>ue&&X.push("'"+this.terminals_[z]+"'");y.showPosition?Z="Parse error on line "+(W+1)+`:
|
import{_ as o,l as te,c as U,H as fe,ah as ye,ai as be,aj as me,ac as Ee,E as K,i as F,t as _e,J as ke,ad as Se,ae as ce,af as le}from"./mermaid.core-CFUktQ8s.js";import{g as Ne}from"./chunk-FMBD7UC4-BALYpKmy.js";import"./index-DNCGxUDM.js";var $=(function(){var e=o(function(O,i,n,r){for(n=n||{},r=O.length;r--;n[O[r]]=i);return n},"o"),u=[1,4],p=[1,13],s=[1,12],d=[1,15],_=[1,16],b=[1,20],l=[1,19],D=[6,7,8],I=[1,26],g=[1,24],w=[1,25],E=[6,7,11],G=[1,31],N=[6,7,11,24],V=[1,6,13,16,17,20,23],m=[1,35],A=[1,36],L=[1,6,7,11,13,16,17,20,23],H=[1,38],T={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,KANBAN:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,shapeData:15,ICON:16,CLASS:17,nodeWithId:18,nodeWithoutId:19,NODE_DSTART:20,NODE_DESCR:21,NODE_DEND:22,NODE_ID:23,SHAPE_DATA:24,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"KANBAN",11:"EOF",13:"SPACELIST",16:"ICON",17:"CLASS",20:"NODE_DSTART",21:"NODE_DESCR",22:"NODE_DEND",23:"NODE_ID",24:"SHAPE_DATA"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,3],[12,2],[12,2],[12,2],[12,1],[12,2],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[19,3],[18,1],[18,4],[15,2],[15,1]],performAction:o(function(i,n,r,a,h,t,M){var c=t.length-1;switch(h){case 6:case 7:return a;case 8:a.getLogger().trace("Stop NL ");break;case 9:a.getLogger().trace("Stop EOF ");break;case 11:a.getLogger().trace("Stop NL2 ");break;case 12:a.getLogger().trace("Stop EOF2 ");break;case 15:a.getLogger().info("Node: ",t[c-1].id),a.addNode(t[c-2].length,t[c-1].id,t[c-1].descr,t[c-1].type,t[c]);break;case 16:a.getLogger().info("Node: ",t[c].id),a.addNode(t[c-1].length,t[c].id,t[c].descr,t[c].type);break;case 17:a.getLogger().trace("Icon: ",t[c]),a.decorateNode({icon:t[c]});break;case 18:case 23:a.decorateNode({class:t[c]});break;case 19:a.getLogger().trace("SPACELIST");break;case 20:a.getLogger().trace("Node: ",t[c-1].id),a.addNode(0,t[c-1].id,t[c-1].descr,t[c-1].type,t[c]);break;case 21:a.getLogger().trace("Node: ",t[c].id),a.addNode(0,t[c].id,t[c].descr,t[c].type);break;case 22:a.decorateNode({icon:t[c]});break;case 27:a.getLogger().trace("node found ..",t[c-2]),this.$={id:t[c-1],descr:t[c-1],type:a.getType(t[c-2],t[c])};break;case 28:this.$={id:t[c],descr:t[c],type:0};break;case 29:a.getLogger().trace("node found ..",t[c-3]),this.$={id:t[c-3],descr:t[c-1],type:a.getType(t[c-2],t[c])};break;case 30:this.$=t[c-1]+t[c];break;case 31:this.$=t[c];break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:u},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:u},{6:p,7:[1,10],9:9,12:11,13:s,14:14,16:d,17:_,18:17,19:18,20:b,23:l},e(D,[2,3]),{1:[2,2]},e(D,[2,4]),e(D,[2,5]),{1:[2,6],6:p,12:21,13:s,14:14,16:d,17:_,18:17,19:18,20:b,23:l},{6:p,9:22,12:11,13:s,14:14,16:d,17:_,18:17,19:18,20:b,23:l},{6:I,7:g,10:23,11:w},e(E,[2,24],{18:17,19:18,14:27,16:[1,28],17:[1,29],20:b,23:l}),e(E,[2,19]),e(E,[2,21],{15:30,24:G}),e(E,[2,22]),e(E,[2,23]),e(N,[2,25]),e(N,[2,26]),e(N,[2,28],{20:[1,32]}),{21:[1,33]},{6:I,7:g,10:34,11:w},{1:[2,7],6:p,12:21,13:s,14:14,16:d,17:_,18:17,19:18,20:b,23:l},e(V,[2,14],{7:m,11:A}),e(L,[2,8]),e(L,[2,9]),e(L,[2,10]),e(E,[2,16],{15:37,24:G}),e(E,[2,17]),e(E,[2,18]),e(E,[2,20],{24:H}),e(N,[2,31]),{21:[1,39]},{22:[1,40]},e(V,[2,13],{7:m,11:A}),e(L,[2,11]),e(L,[2,12]),e(E,[2,15],{24:H}),e(N,[2,30]),{22:[1,41]},e(N,[2,27]),e(N,[2,29])],defaultActions:{2:[2,1],6:[2,2]},parseError:o(function(i,n){if(n.recoverable)this.trace(i);else{var r=new Error(i);throw r.hash=n,r}},"parseError"),parse:o(function(i){var n=this,r=[0],a=[],h=[null],t=[],M=this.table,c="",W=0,se=0,ue=2,re=1,ge=t.slice.call(arguments,1),y=Object.create(this.lexer),R={yy:{}};for(var J in this.yy)Object.prototype.hasOwnProperty.call(this.yy,J)&&(R.yy[J]=this.yy[J]);y.setInput(i,R.yy),R.yy.lexer=y,R.yy.parser=this,typeof y.yylloc>"u"&&(y.yylloc={});var q=y.yylloc;t.push(q);var de=y.options&&y.options.ranges;typeof R.yy.parseError=="function"?this.parseError=R.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function pe(S){r.length=r.length-2*S,h.length=h.length-S,t.length=t.length-S}o(pe,"popStack");function ae(){var S;return S=a.pop()||y.lex()||re,typeof S!="number"&&(S instanceof Array&&(a=S,S=a.pop()),S=n.symbols_[S]||S),S}o(ae,"lex");for(var k,P,x,Q,j={},z,C,oe,X;;){if(P=r[r.length-1],this.defaultActions[P]?x=this.defaultActions[P]:((k===null||typeof k>"u")&&(k=ae()),x=M[P]&&M[P][k]),typeof x>"u"||!x.length||!x[0]){var Z="";X=[];for(z in M[P])this.terminals_[z]&&z>ue&&X.push("'"+this.terminals_[z]+"'");y.showPosition?Z="Parse error on line "+(W+1)+`:
|
||||||
`+y.showPosition()+`
|
`+y.showPosition()+`
|
||||||
Expecting `+X.join(", ")+", got '"+(this.terminals_[k]||k)+"'":Z="Parse error on line "+(W+1)+": Unexpected "+(k==re?"end of input":"'"+(this.terminals_[k]||k)+"'"),this.parseError(Z,{text:y.match,token:this.terminals_[k]||k,line:y.yylineno,loc:q,expected:X})}if(x[0]instanceof Array&&x.length>1)throw new Error("Parse Error: multiple actions possible at state: "+P+", token: "+k);switch(x[0]){case 1:r.push(k),h.push(y.yytext),t.push(y.yylloc),r.push(x[1]),k=null,se=y.yyleng,c=y.yytext,W=y.yylineno,q=y.yylloc;break;case 2:if(C=this.productions_[x[1]][1],j.$=h[h.length-C],j._$={first_line:t[t.length-(C||1)].first_line,last_line:t[t.length-1].last_line,first_column:t[t.length-(C||1)].first_column,last_column:t[t.length-1].last_column},de&&(j._$.range=[t[t.length-(C||1)].range[0],t[t.length-1].range[1]]),Q=this.performAction.apply(j,[c,se,W,R.yy,x[1],h,t].concat(ge)),typeof Q<"u")return Q;C&&(r=r.slice(0,-1*C*2),h=h.slice(0,-1*C),t=t.slice(0,-1*C)),r.push(this.productions_[x[1]][0]),h.push(j.$),t.push(j._$),oe=M[r[r.length-2]][r[r.length-1]],r.push(oe);break;case 3:return!0}}return!0},"parse")},Y=(function(){var O={EOF:1,parseError:o(function(n,r){if(this.yy.parser)this.yy.parser.parseError(n,r);else throw new Error(n)},"parseError"),setInput:o(function(i,n){return this.yy=n||this.yy||{},this._input=i,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var i=this._input[0];this.yytext+=i,this.yyleng++,this.offset++,this.match+=i,this.matched+=i;var n=i.match(/(?:\r\n?|\n).*/g);return n?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),i},"input"),unput:o(function(i){var n=i.length,r=i.split(/(?:\r\n?|\n)/g);this._input=i+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-n),this.offset-=n;var a=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),r.length-1&&(this.yylineno-=r.length-1);var h=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:r?(r.length===a.length?this.yylloc.first_column:0)+a[a.length-r.length].length-r[0].length:this.yylloc.first_column-n},this.options.ranges&&(this.yylloc.range=[h[0],h[0]+this.yyleng-n]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
|
Expecting `+X.join(", ")+", got '"+(this.terminals_[k]||k)+"'":Z="Parse error on line "+(W+1)+": Unexpected "+(k==re?"end of input":"'"+(this.terminals_[k]||k)+"'"),this.parseError(Z,{text:y.match,token:this.terminals_[k]||k,line:y.yylineno,loc:q,expected:X})}if(x[0]instanceof Array&&x.length>1)throw new Error("Parse Error: multiple actions possible at state: "+P+", token: "+k);switch(x[0]){case 1:r.push(k),h.push(y.yytext),t.push(y.yylloc),r.push(x[1]),k=null,se=y.yyleng,c=y.yytext,W=y.yylineno,q=y.yylloc;break;case 2:if(C=this.productions_[x[1]][1],j.$=h[h.length-C],j._$={first_line:t[t.length-(C||1)].first_line,last_line:t[t.length-1].last_line,first_column:t[t.length-(C||1)].first_column,last_column:t[t.length-1].last_column},de&&(j._$.range=[t[t.length-(C||1)].range[0],t[t.length-1].range[1]]),Q=this.performAction.apply(j,[c,se,W,R.yy,x[1],h,t].concat(ge)),typeof Q<"u")return Q;C&&(r=r.slice(0,-1*C*2),h=h.slice(0,-1*C),t=t.slice(0,-1*C)),r.push(this.productions_[x[1]][0]),h.push(j.$),t.push(j._$),oe=M[r[r.length-2]][r[r.length-1]],r.push(oe);break;case 3:return!0}}return!0},"parse")},Y=(function(){var O={EOF:1,parseError:o(function(n,r){if(this.yy.parser)this.yy.parser.parseError(n,r);else throw new Error(n)},"parseError"),setInput:o(function(i,n){return this.yy=n||this.yy||{},this._input=i,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var i=this._input[0];this.yytext+=i,this.yyleng++,this.offset++,this.match+=i,this.matched+=i;var n=i.match(/(?:\r\n?|\n).*/g);return n?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),i},"input"),unput:o(function(i){var n=i.length,r=i.split(/(?:\r\n?|\n)/g);this._input=i+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-n),this.offset-=n;var a=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),r.length-1&&(this.yylineno-=r.length-1);var h=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:r?(r.length===a.length?this.yylloc.first_column:0)+a[a.length-r.length].length-r[0].length:this.yylloc.first_column-n},this.options.ranges&&(this.yylloc.range=[h[0],h[0]+this.yyleng-n]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
|
||||||
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(i){this.unput(this.match.slice(i))},"less"),pastInput:o(function(){var i=this.matched.substr(0,this.matched.length-this.match.length);return(i.length>20?"...":"")+i.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var i=this.match;return i.length<20&&(i+=this._input.substr(0,20-i.length)),(i.substr(0,20)+(i.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var i=this.pastInput(),n=new Array(i.length+1).join("-");return i+this.upcomingInput()+`
|
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(i){this.unput(this.match.slice(i))},"less"),pastInput:o(function(){var i=this.matched.substr(0,this.matched.length-this.match.length);return(i.length>20?"...":"")+i.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var i=this.match;return i.length<20&&(i+=this._input.substr(0,20-i.length)),(i.substr(0,20)+(i.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var i=this.pastInput(),n=new Array(i.length+1).join("-");return i+this.upcomingInput()+`
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{R as S,V as z,aG as j,g as q,s as H,a as Z,b as J,q as K,p as Q,_ as p,l as F,c as X,D as Y,H as ee,a4 as te,e as ae,y as re,E as ne}from"./mermaid.core-v0oo9NRr.js";import{p as ie}from"./chunk-4BX2VUAB-DWDvTYfd.js";import{p as se}from"./treemap-GDKQZRPO-DJjQsbt8.js";import{d as I}from"./arc-UjuE1bPP.js";import{o as le}from"./ordinal-Cboi1Yqb.js";import"./index-8cIrvc8q.js";import"./_baseUniq-DAOs4kUj.js";import"./_basePickBy-CL4iQUG-.js";import"./clone-C1u3K6Fy.js";import"./init-Gi6I4Gst.js";function oe(e,a){return a<e?-1:a>e?1:a>=e?0:NaN}function ce(e){return e}function ue(){var e=ce,a=oe,f=null,y=S(0),s=S(z),o=S(0);function l(t){var n,c=(t=j(t)).length,d,x,h=0,u=new Array(c),i=new Array(c),v=+y.apply(this,arguments),w=Math.min(z,Math.max(-z,s.apply(this,arguments)-v)),m,C=Math.min(Math.abs(w)/c,o.apply(this,arguments)),$=C*(w<0?-1:1),g;for(n=0;n<c;++n)(g=i[u[n]=n]=+e(t[n],n,t))>0&&(h+=g);for(a!=null?u.sort(function(A,D){return a(i[A],i[D])}):f!=null&&u.sort(function(A,D){return f(t[A],t[D])}),n=0,x=h?(w-c*$)/h:0;n<c;++n,v=m)d=u[n],g=i[d],m=v+(g>0?g*x:0)+$,i[d]={data:t[d],index:n,value:g,startAngle:v,endAngle:m,padAngle:C};return i}return l.value=function(t){return arguments.length?(e=typeof t=="function"?t:S(+t),l):e},l.sortValues=function(t){return arguments.length?(a=t,f=null,l):a},l.sort=function(t){return arguments.length?(f=t,a=null,l):f},l.startAngle=function(t){return arguments.length?(y=typeof t=="function"?t:S(+t),l):y},l.endAngle=function(t){return arguments.length?(s=typeof t=="function"?t:S(+t),l):s},l.padAngle=function(t){return arguments.length?(o=typeof t=="function"?t:S(+t),l):o},l}var pe=ne.pie,G={sections:new Map,showData:!1},T=G.sections,N=G.showData,de=structuredClone(pe),ge=p(()=>structuredClone(de),"getConfig"),fe=p(()=>{T=new Map,N=G.showData,re()},"clear"),me=p(({label:e,value:a})=>{if(a<0)throw new Error(`"${e}" has invalid value: ${a}. Negative values are not allowed in pie charts. All slice values must be >= 0.`);T.has(e)||(T.set(e,a),F.debug(`added new section: ${e}, with value: ${a}`))},"addSection"),he=p(()=>T,"getSections"),ve=p(e=>{N=e},"setShowData"),Se=p(()=>N,"getShowData"),L={getConfig:ge,clear:fe,setDiagramTitle:Q,getDiagramTitle:K,setAccTitle:J,getAccTitle:Z,setAccDescription:H,getAccDescription:q,addSection:me,getSections:he,setShowData:ve,getShowData:Se},ye=p((e,a)=>{ie(e,a),a.setShowData(e.showData),e.sections.map(a.addSection)},"populateDb"),xe={parse:p(async e=>{const a=await se("pie",e);F.debug(a),ye(a,L)},"parse")},we=p(e=>`
|
import{R as S,V as z,aG as j,g as q,s as H,a as Z,b as J,q as K,p as Q,_ as p,l as F,c as X,D as Y,H as ee,a4 as te,e as ae,y as re,E as ne}from"./mermaid.core-CFUktQ8s.js";import{p as ie}from"./chunk-4BX2VUAB-DHPPu6Xd.js";import{p as se}from"./treemap-GDKQZRPO-DWvWdchV.js";import{d as I}from"./arc-P9DEh39j.js";import{o as le}from"./ordinal-Cboi1Yqb.js";import"./index-DNCGxUDM.js";import"./_baseUniq-T1y-Xwdr.js";import"./_basePickBy-8KTOl_Ov.js";import"./clone-4l7SFgdX.js";import"./init-Gi6I4Gst.js";function oe(e,a){return a<e?-1:a>e?1:a>=e?0:NaN}function ce(e){return e}function ue(){var e=ce,a=oe,f=null,y=S(0),s=S(z),o=S(0);function l(t){var n,c=(t=j(t)).length,d,x,h=0,u=new Array(c),i=new Array(c),v=+y.apply(this,arguments),w=Math.min(z,Math.max(-z,s.apply(this,arguments)-v)),m,C=Math.min(Math.abs(w)/c,o.apply(this,arguments)),$=C*(w<0?-1:1),g;for(n=0;n<c;++n)(g=i[u[n]=n]=+e(t[n],n,t))>0&&(h+=g);for(a!=null?u.sort(function(A,D){return a(i[A],i[D])}):f!=null&&u.sort(function(A,D){return f(t[A],t[D])}),n=0,x=h?(w-c*$)/h:0;n<c;++n,v=m)d=u[n],g=i[d],m=v+(g>0?g*x:0)+$,i[d]={data:t[d],index:n,value:g,startAngle:v,endAngle:m,padAngle:C};return i}return l.value=function(t){return arguments.length?(e=typeof t=="function"?t:S(+t),l):e},l.sortValues=function(t){return arguments.length?(a=t,f=null,l):a},l.sort=function(t){return arguments.length?(f=t,a=null,l):f},l.startAngle=function(t){return arguments.length?(y=typeof t=="function"?t:S(+t),l):y},l.endAngle=function(t){return arguments.length?(s=typeof t=="function"?t:S(+t),l):s},l.padAngle=function(t){return arguments.length?(o=typeof t=="function"?t:S(+t),l):o},l}var pe=ne.pie,G={sections:new Map,showData:!1},T=G.sections,N=G.showData,de=structuredClone(pe),ge=p(()=>structuredClone(de),"getConfig"),fe=p(()=>{T=new Map,N=G.showData,re()},"clear"),me=p(({label:e,value:a})=>{if(a<0)throw new Error(`"${e}" has invalid value: ${a}. Negative values are not allowed in pie charts. All slice values must be >= 0.`);T.has(e)||(T.set(e,a),F.debug(`added new section: ${e}, with value: ${a}`))},"addSection"),he=p(()=>T,"getSections"),ve=p(e=>{N=e},"setShowData"),Se=p(()=>N,"getShowData"),L={getConfig:ge,clear:fe,setDiagramTitle:Q,getDiagramTitle:K,setAccTitle:J,getAccTitle:Z,setAccDescription:H,getAccDescription:q,addSection:me,getSections:he,setShowData:ve,getShowData:Se},ye=p((e,a)=>{ie(e,a),a.setShowData(e.showData),e.sections.map(a.addSection)},"populateDb"),xe={parse:p(async e=>{const a=await se("pie",e);F.debug(a),ye(a,L)},"parse")},we=p(e=>`
|
||||||
.pieCircle{
|
.pieCircle{
|
||||||
stroke: ${e.pieStrokeColor};
|
stroke: ${e.pieStrokeColor};
|
||||||
stroke-width : ${e.pieStrokeWidth};
|
stroke-width : ${e.pieStrokeWidth};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/SongGrid-C568K-c_.js","assets/SongGrid.vue_vue_type_script_setup_true_lang-IvAOIQYW.js","assets/index-8cIrvc8q.js","assets/index-BJkaQ2c4.css","assets/useContentImages-7wLVntsF.js","assets/SongDetail-BiVK4Yzb.js","assets/SongDetail.vue_vue_type_script_setup_true_lang-B3om3Z8v.js"])))=>i.map(i=>d[i]);
|
|
||||||
import{d as e,_ as r}from"./index-8cIrvc8q.js";const o={id:"song",name:"Song Renderer",contentType:"song",surfaces:["chat-preview","panel-preview","panel-play"],chatPreview:e(()=>r(()=>import("./SongGrid-C568K-c_.js"),__vite__mapDeps([0,1,2,3,4]))),panelPreview:e(()=>r(()=>import("./SongGrid-C568K-c_.js"),__vite__mapDeps([0,1,2,3,4]))),panelPlay:e(()=>r(()=>import("./SongDetail-BiVK4Yzb.js"),__vite__mapDeps([5,6,2,3])))};export{o as songRenderer};
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/SongGrid-BdJzZ_PP.js","assets/SongGrid.vue_vue_type_script_setup_true_lang-70SttNTZ.js","assets/index-DNCGxUDM.js","assets/index-BJh-vGUe.css","assets/useContentImages-DdjyABL9.js","assets/SongDetail-Cdt_oumO.js","assets/SongDetail.vue_vue_type_script_setup_true_lang-0mQhUBE8.js"])))=>i.map(i=>d[i]);
|
||||||
|
import{d as e,_ as r}from"./index-DNCGxUDM.js";const o={id:"song",name:"Song Renderer",contentType:"song",surfaces:["chat-preview","panel-preview","panel-play"],chatPreview:e(()=>r(()=>import("./SongGrid-BdJzZ_PP.js"),__vite__mapDeps([0,1,2,3,4]))),panelPreview:e(()=>r(()=>import("./SongGrid-BdJzZ_PP.js"),__vite__mapDeps([0,1,2,3,4]))),panelPlay:e(()=>r(()=>import("./SongDetail-Cdt_oumO.js"),__vite__mapDeps([5,6,2,3])))};export{o as songRenderer};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{s as e,b as r,a,S as s}from"./chunk-DI55MBZ5-C35X8NbN.js";import{_ as i}from"./mermaid.core-CFUktQ8s.js";import"./chunk-55IACEB6-fbeP3Dtn.js";import"./chunk-QN33PNHL-B0_3_NGo.js";import"./index-DNCGxUDM.js";var p={parser:a,get db(){return new s(2)},renderer:r,styles:e,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{p as diagram};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{s as e,b as r,a,S as s}from"./chunk-DI55MBZ5-BzH7fNN2.js";import{_ as i}from"./mermaid.core-v0oo9NRr.js";import"./chunk-55IACEB6-CtULfmDo.js";import"./chunk-QN33PNHL-DSThOC6-.js";import"./index-8cIrvc8q.js";var p={parser:a,get db(){return new s(2)},renderer:r,styles:e,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{p as diagram};
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{_ as s,c as xt,l as E,d as j,ac as kt,ad as vt,ae as _t,af as bt,B as wt,ag as St,y as Et}from"./mermaid.core-v0oo9NRr.js";import{d as nt}from"./arc-UjuE1bPP.js";import"./index-8cIrvc8q.js";var Q=(function(){var n=s(function(x,r,a,c){for(a=a||{},c=x.length;c--;a[x[c]]=r);return a},"o"),t=[6,8,10,11,12,14,16,17,20,21],e=[1,9],l=[1,10],i=[1,11],d=[1,12],h=[1,13],f=[1,16],m=[1,17],p={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,timeline:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,period_statement:18,event_statement:19,period:20,event:21,$accept:0,$end:1},terminals_:{2:"error",4:"timeline",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",20:"period",21:"event"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,1],[9,1],[18,1],[19,1]],performAction:s(function(r,a,c,u,y,o,w){var v=o.length-1;switch(y){case 1:return o[v-1];case 2:this.$=[];break;case 3:o[v-1].push(o[v]),this.$=o[v-1];break;case 4:case 5:this.$=o[v];break;case 6:case 7:this.$=[];break;case 8:u.getCommonDb().setDiagramTitle(o[v].substr(6)),this.$=o[v].substr(6);break;case 9:this.$=o[v].trim(),u.getCommonDb().setAccTitle(this.$);break;case 10:case 11:this.$=o[v].trim(),u.getCommonDb().setAccDescription(this.$);break;case 12:u.addSection(o[v].substr(8)),this.$=o[v].substr(8);break;case 15:u.addTask(o[v],0,""),this.$=o[v];break;case 16:u.addEvent(o[v].substr(2)),this.$=o[v];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},n(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:e,12:l,14:i,16:d,17:h,18:14,19:15,20:f,21:m},n(t,[2,7],{1:[2,1]}),n(t,[2,3]),{9:18,11:e,12:l,14:i,16:d,17:h,18:14,19:15,20:f,21:m},n(t,[2,5]),n(t,[2,6]),n(t,[2,8]),{13:[1,19]},{15:[1,20]},n(t,[2,11]),n(t,[2,12]),n(t,[2,13]),n(t,[2,14]),n(t,[2,15]),n(t,[2,16]),n(t,[2,4]),n(t,[2,9]),n(t,[2,10])],defaultActions:{},parseError:s(function(r,a){if(a.recoverable)this.trace(r);else{var c=new Error(r);throw c.hash=a,c}},"parseError"),parse:s(function(r){var a=this,c=[0],u=[],y=[null],o=[],w=this.table,v="",N=0,P=0,V=2,U=1,H=o.slice.call(arguments,1),g=Object.create(this.lexer),b={yy:{}};for(var L in this.yy)Object.prototype.hasOwnProperty.call(this.yy,L)&&(b.yy[L]=this.yy[L]);g.setInput(r,b.yy),b.yy.lexer=g,b.yy.parser=this,typeof g.yylloc>"u"&&(g.yylloc={});var M=g.yylloc;o.push(M);var W=g.options&&g.options.ranges;typeof b.yy.parseError=="function"?this.parseError=b.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Z(T){c.length=c.length-2*T,y.length=y.length-T,o.length=o.length-T}s(Z,"popStack");function tt(){var T;return T=u.pop()||g.lex()||U,typeof T!="number"&&(T instanceof Array&&(u=T,T=u.pop()),T=a.symbols_[T]||T),T}s(tt,"lex");for(var S,A,I,J,R={},B,$,et,O;;){if(A=c[c.length-1],this.defaultActions[A]?I=this.defaultActions[A]:((S===null||typeof S>"u")&&(S=tt()),I=w[A]&&w[A][S]),typeof I>"u"||!I.length||!I[0]){var K="";O=[];for(B in w[A])this.terminals_[B]&&B>V&&O.push("'"+this.terminals_[B]+"'");g.showPosition?K="Parse error on line "+(N+1)+`:
|
import{_ as s,c as xt,l as E,d as j,ac as kt,ad as vt,ae as _t,af as bt,B as wt,ag as St,y as Et}from"./mermaid.core-CFUktQ8s.js";import{d as nt}from"./arc-P9DEh39j.js";import"./index-DNCGxUDM.js";var Q=(function(){var n=s(function(x,r,a,c){for(a=a||{},c=x.length;c--;a[x[c]]=r);return a},"o"),t=[6,8,10,11,12,14,16,17,20,21],e=[1,9],l=[1,10],i=[1,11],d=[1,12],h=[1,13],f=[1,16],m=[1,17],p={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,timeline:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,period_statement:18,event_statement:19,period:20,event:21,$accept:0,$end:1},terminals_:{2:"error",4:"timeline",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",20:"period",21:"event"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,1],[9,1],[18,1],[19,1]],performAction:s(function(r,a,c,u,y,o,w){var v=o.length-1;switch(y){case 1:return o[v-1];case 2:this.$=[];break;case 3:o[v-1].push(o[v]),this.$=o[v-1];break;case 4:case 5:this.$=o[v];break;case 6:case 7:this.$=[];break;case 8:u.getCommonDb().setDiagramTitle(o[v].substr(6)),this.$=o[v].substr(6);break;case 9:this.$=o[v].trim(),u.getCommonDb().setAccTitle(this.$);break;case 10:case 11:this.$=o[v].trim(),u.getCommonDb().setAccDescription(this.$);break;case 12:u.addSection(o[v].substr(8)),this.$=o[v].substr(8);break;case 15:u.addTask(o[v],0,""),this.$=o[v];break;case 16:u.addEvent(o[v].substr(2)),this.$=o[v];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},n(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:e,12:l,14:i,16:d,17:h,18:14,19:15,20:f,21:m},n(t,[2,7],{1:[2,1]}),n(t,[2,3]),{9:18,11:e,12:l,14:i,16:d,17:h,18:14,19:15,20:f,21:m},n(t,[2,5]),n(t,[2,6]),n(t,[2,8]),{13:[1,19]},{15:[1,20]},n(t,[2,11]),n(t,[2,12]),n(t,[2,13]),n(t,[2,14]),n(t,[2,15]),n(t,[2,16]),n(t,[2,4]),n(t,[2,9]),n(t,[2,10])],defaultActions:{},parseError:s(function(r,a){if(a.recoverable)this.trace(r);else{var c=new Error(r);throw c.hash=a,c}},"parseError"),parse:s(function(r){var a=this,c=[0],u=[],y=[null],o=[],w=this.table,v="",N=0,P=0,V=2,U=1,H=o.slice.call(arguments,1),g=Object.create(this.lexer),b={yy:{}};for(var L in this.yy)Object.prototype.hasOwnProperty.call(this.yy,L)&&(b.yy[L]=this.yy[L]);g.setInput(r,b.yy),b.yy.lexer=g,b.yy.parser=this,typeof g.yylloc>"u"&&(g.yylloc={});var M=g.yylloc;o.push(M);var W=g.options&&g.options.ranges;typeof b.yy.parseError=="function"?this.parseError=b.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Z(T){c.length=c.length-2*T,y.length=y.length-T,o.length=o.length-T}s(Z,"popStack");function tt(){var T;return T=u.pop()||g.lex()||U,typeof T!="number"&&(T instanceof Array&&(u=T,T=u.pop()),T=a.symbols_[T]||T),T}s(tt,"lex");for(var S,A,I,J,R={},B,$,et,O;;){if(A=c[c.length-1],this.defaultActions[A]?I=this.defaultActions[A]:((S===null||typeof S>"u")&&(S=tt()),I=w[A]&&w[A][S]),typeof I>"u"||!I.length||!I[0]){var K="";O=[];for(B in w[A])this.terminals_[B]&&B>V&&O.push("'"+this.terminals_[B]+"'");g.showPosition?K="Parse error on line "+(N+1)+`:
|
||||||
`+g.showPosition()+`
|
`+g.showPosition()+`
|
||||||
Expecting `+O.join(", ")+", got '"+(this.terminals_[S]||S)+"'":K="Parse error on line "+(N+1)+": Unexpected "+(S==U?"end of input":"'"+(this.terminals_[S]||S)+"'"),this.parseError(K,{text:g.match,token:this.terminals_[S]||S,line:g.yylineno,loc:M,expected:O})}if(I[0]instanceof Array&&I.length>1)throw new Error("Parse Error: multiple actions possible at state: "+A+", token: "+S);switch(I[0]){case 1:c.push(S),y.push(g.yytext),o.push(g.yylloc),c.push(I[1]),S=null,P=g.yyleng,v=g.yytext,N=g.yylineno,M=g.yylloc;break;case 2:if($=this.productions_[I[1]][1],R.$=y[y.length-$],R._$={first_line:o[o.length-($||1)].first_line,last_line:o[o.length-1].last_line,first_column:o[o.length-($||1)].first_column,last_column:o[o.length-1].last_column},W&&(R._$.range=[o[o.length-($||1)].range[0],o[o.length-1].range[1]]),J=this.performAction.apply(R,[v,P,N,b.yy,I[1],y,o].concat(H)),typeof J<"u")return J;$&&(c=c.slice(0,-1*$*2),y=y.slice(0,-1*$),o=o.slice(0,-1*$)),c.push(this.productions_[I[1]][0]),y.push(R.$),o.push(R._$),et=w[c[c.length-2]][c[c.length-1]],c.push(et);break;case 3:return!0}}return!0},"parse")},k=(function(){var x={EOF:1,parseError:s(function(a,c){if(this.yy.parser)this.yy.parser.parseError(a,c);else throw new Error(a)},"parseError"),setInput:s(function(r,a){return this.yy=a||this.yy||{},this._input=r,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var r=this._input[0];this.yytext+=r,this.yyleng++,this.offset++,this.match+=r,this.matched+=r;var a=r.match(/(?:\r\n?|\n).*/g);return a?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),r},"input"),unput:s(function(r){var a=r.length,c=r.split(/(?:\r\n?|\n)/g);this._input=r+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-a),this.offset-=a;var u=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),c.length-1&&(this.yylineno-=c.length-1);var y=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:c?(c.length===u.length?this.yylloc.first_column:0)+u[u.length-c.length].length-c[0].length:this.yylloc.first_column-a},this.options.ranges&&(this.yylloc.range=[y[0],y[0]+this.yyleng-a]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
|
Expecting `+O.join(", ")+", got '"+(this.terminals_[S]||S)+"'":K="Parse error on line "+(N+1)+": Unexpected "+(S==U?"end of input":"'"+(this.terminals_[S]||S)+"'"),this.parseError(K,{text:g.match,token:this.terminals_[S]||S,line:g.yylineno,loc:M,expected:O})}if(I[0]instanceof Array&&I.length>1)throw new Error("Parse Error: multiple actions possible at state: "+A+", token: "+S);switch(I[0]){case 1:c.push(S),y.push(g.yytext),o.push(g.yylloc),c.push(I[1]),S=null,P=g.yyleng,v=g.yytext,N=g.yylineno,M=g.yylloc;break;case 2:if($=this.productions_[I[1]][1],R.$=y[y.length-$],R._$={first_line:o[o.length-($||1)].first_line,last_line:o[o.length-1].last_line,first_column:o[o.length-($||1)].first_column,last_column:o[o.length-1].last_column},W&&(R._$.range=[o[o.length-($||1)].range[0],o[o.length-1].range[1]]),J=this.performAction.apply(R,[v,P,N,b.yy,I[1],y,o].concat(H)),typeof J<"u")return J;$&&(c=c.slice(0,-1*$*2),y=y.slice(0,-1*$),o=o.slice(0,-1*$)),c.push(this.productions_[I[1]][0]),y.push(R.$),o.push(R._$),et=w[c[c.length-2]][c[c.length-1]],c.push(et);break;case 3:return!0}}return!0},"parse")},k=(function(){var x={EOF:1,parseError:s(function(a,c){if(this.yy.parser)this.yy.parser.parseError(a,c);else throw new Error(a)},"parseError"),setInput:s(function(r,a){return this.yy=a||this.yy||{},this._input=r,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var r=this._input[0];this.yytext+=r,this.yyleng++,this.offset++,this.match+=r,this.matched+=r;var a=r.match(/(?:\r\n?|\n).*/g);return a?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),r},"input"),unput:s(function(r){var a=r.length,c=r.split(/(?:\r\n?|\n)/g);this._input=r+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-a),this.offset-=a;var u=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),c.length-1&&(this.yylineno-=c.length-1);var y=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:c?(c.length===u.length?this.yylloc.first_column:0)+u[u.length-c.length].length-c[0].length:this.yylloc.first_column-a},this.options.ranges&&(this.yylloc.range=[y[0],y[0]+this.yyleng-a]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
|
||||||
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(r){this.unput(this.match.slice(r))},"less"),pastInput:s(function(){var r=this.matched.substr(0,this.matched.length-this.match.length);return(r.length>20?"...":"")+r.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var r=this.match;return r.length<20&&(r+=this._input.substr(0,20-r.length)),(r.substr(0,20)+(r.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var r=this.pastInput(),a=new Array(r.length+1).join("-");return r+this.upcomingInput()+`
|
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(r){this.unput(this.match.slice(r))},"less"),pastInput:s(function(){var r=this.matched.substr(0,this.matched.length-this.match.length);return(r.length>20?"...":"")+r.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var r=this.match;return r.length<20&&(r+=this._input.substr(0,20-r.length)),(r.substr(0,20)+(r.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var r=this.pastInput(),a=new Array(r.length+1).join("-");return r+this.upcomingInput()+`
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{A as v,r as g,B as w,C as I}from"./index-8cIrvc8q.js";function k(t){const a=g(new Set),r=w(new Map);function i(e){a.value=new Set([...a.value,e])}function f(e){const n=t.id(e);return a.value.has(n)?null:t.existingUrl(e)||r.get(n)||null}function l(e){return t.fallback(e)}function d(e){i(t.id(e))}function o(e){const n=t.id(e);return!f(e)&&!a.value.has(n)}function h(e){for(const n of e){const c=t.id(n);t.existingUrl(n)||r.has(c)||a.value.has(c)||t.fetch(n).then(s=>{s?r.set(c,s):i(c)}).catch(()=>{i(c)})}}const u=t.items,m=I(u)?()=>u.value:u;return v(m,e=>h(e),{immediate:!0}),{coverSrc:f,fallbackSrc:l,onError:d,isLoading:o}}export{k as u};
|
import{A as v,r as g,B as w,C as I}from"./index-DNCGxUDM.js";function k(t){const a=g(new Set),r=w(new Map);function i(e){a.value=new Set([...a.value,e])}function f(e){const n=t.id(e);return a.value.has(n)?null:t.existingUrl(e)||r.get(n)||null}function l(e){return t.fallback(e)}function d(e){i(t.id(e))}function o(e){const n=t.id(e);return!f(e)&&!a.value.has(n)}function h(e){for(const n of e){const c=t.id(n);t.existingUrl(n)||r.has(c)||a.value.has(c)||t.fetch(n).then(s=>{s?r.set(c,s):i(c)}).catch(()=>{i(c)})}}const u=t.items,m=I(u)?()=>u.value:u;return v(m,e=>h(e),{immediate:!0}),{coverSrc:f,fallbackSrc:l,onError:d,isLoading:o}}export{k as u};
|
||||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user