Compare commits

..
Author SHA1 Message Date
Archipelago 081dab5934 Archipelago — open-source initial import 2026-08-12 10:55:49 +00:00
251 changed files with 1857 additions and 1224 deletions
+10 -1
View File
@@ -1,10 +1,19 @@
# Changelog # Changelog
## Unreleased ## v1.7.127-alpha (2026-08-09)
- **Your node now has its own assistant.** This is the first release to ship AIUI: a conversational screen that can answer from your node's own content — your films, music and files come first, the open web second — and can act on the node itself: install or remove an app, check what's running, or queue up your media, all through a fixed list of vetted actions rather than free rein. It is off-limits to your data until you say otherwise: every data category starts closed, grants are made in Settings → AI Data Access and live on the node itself, and anything that changes the node asks you to confirm in the dashboard's own chrome first — a declined action stays declined. What leaves the node is screened: your API key is stored encrypted and never written in plain text, credential-shaped strings are scrubbed from app logs before the model sees them, your public address and Wi-Fi name are stripped from network answers, web search is gated behind your login session, and cloud-bound text passes a secret scan on the way out. Three model backends are supported — Anthropic's API, a local Ollama, and pay-per-use Routstr with a hard prepaid budget ceiling — and mesh peers can reach the same loop with `!ai`.
- **Tor now tells you the truth, heals itself, and the Restart button really restarts it.** Three nodes ran for days with Tor completely dead while the dashboard said "Connected" — the indicator was reading a leftover address file, not the daemon, and the restart button reported success without checking. The cause was a configuration line Tor can never bind on our systems; a node could re-break itself from a single settings change. The node now refuses to write that line, checks Tor with a real connection instead of a leftover file, repairs its own Tor configuration at every start, and the Restart button only claims success once Tor is actually answering. Onion addresses that had silently never been published (BTCPay's included) come back with it.
- **Inviting another node as Trusted works again — on every node.** Generating a Trusted invite, or promoting a peer from the dropdown, silently failed everywhere: the security prompt that asks for your node password could never appear, because the message requesting it was being scrubbed out of the reply on its way to your browser. The prompt now opens, and if a trust change fails, the error appears inside the window you are looking at instead of hidden behind it.
- **The mempool explorer actually connects now.** The page loaded but sat empty forever. Three separate causes stacked up: the block index had spent days rebuilding without anything saying so, and then two different layers of the node's plumbing were dropping the live-data connection the page depends on — so everything reported healthy while your screen showed nothing. All three are fixed, and the node's own health checks now test the real connection a browser makes, so this cannot pass unnoticed again.
- **Apps no longer vanish after stopping cleanly.** A stopped app's container is deleted by design, but the restart policy meant an app that exited cleanly was never brought back — it simply disappeared until reinstalled. Backends now restart in every case, the node remembers what you have installed so a missing app is recreated rather than forgotten, and this release repairs the incorrect policy on apps installed by earlier versions.
- **Your Bitcoin node will not silently change software versions anymore.** "Latest" previously meant different things in different places — one path installed a newer build that deliberately halts until you make a network-rules decision, which froze one node's sync at a fixed block while it reported itself fully synced. Bitcoin Knots is now pinned to an explicit, known-good version; changing it is a decision you make, never a side effect of an update.
- **Smaller fixes:** the AI data-access settings now say plainly which categories the assistant can see but not act on; the transactions window's tab bar is transparent glass instead of a black block; BTCPay logins no longer fail with a server error when the node is under heavy load right at that moment.
- **You can now replace your Lightning connection keys from Settings, without touching a terminal.** The tokens wallet apps like Zeus use to reach your node are bearer keys: anything that has ever seen one can spend from your node until they are replaced, and there is no way to cancel one individually. Replacing them was previously a script you had to SSH in and run, which in practice meant it never happened. Settings → Lightning credentials now shows when yours were issued, which node they belong to and how many channels must survive, then does the whole job behind your node password — with a step-by-step progress list, and a refusal to call it a success unless it has confirmed your node identity and every channel came back. Your coins and channels are not touched: nothing is closed, and the wallet is never re-created. Afterwards you re-pair Zeus by scanning the Lightning app's QR code again. - **You can now replace your Lightning connection keys from Settings, without touching a terminal.** The tokens wallet apps like Zeus use to reach your node are bearer keys: anything that has ever seen one can spend from your node until they are replaced, and there is no way to cancel one individually. Replacing them was previously a script you had to SSH in and run, which in practice meant it never happened. Settings → Lightning credentials now shows when yours were issued, which node they belong to and how many channels must survive, then does the whole job behind your node password — with a step-by-step progress list, and a refusal to call it a success unless it has confirmed your node identity and every channel came back. Your coins and channels are not touched: nothing is closed, and the wallet is never re-created. Afterwards you re-pair Zeus by scanning the Lightning app's QR code again.
- **Replacing those keys no longer silently breaks BTCPay Server.** BTCPay holds its own copy of the key, and that copy cannot repair itself — so a node that replaced its keys ended up with BTCPay running, healthy, and unable to take a single Lightning payment, with nothing anywhere saying why. The dashboard now updates BTCPay's copy as part of the run and restarts it around its existing data, and the Settings screen warns you if it finds a node already stuck in that state. The command-line script fixes the same gap. - **Replacing those keys no longer silently breaks BTCPay Server.** BTCPay holds its own copy of the key, and that copy cannot repair itself — so a node that replaced its keys ended up with BTCPay running, healthy, and unable to take a single Lightning payment, with nothing anywhere saying why. The dashboard now updates BTCPay's copy as part of the run and restarts it around its existing data, and the Settings screen warns you if it finds a node already stuck in that state. The command-line script fixes the same gap.
- **Lightning stops getting stuck locked on a busy node.** Lightning opens its databases before it will accept the password that unlocks the wallet, and on a loaded node that took nearly three minutes — longer than the node was willing to wait. Giving up restarted Lightning, which started the slow open again, so the wallet stayed locked forever and everything depending on it stayed broken. The node now waits as long as it takes. A genuinely wrong password still fails immediately. - **Lightning stops getting stuck locked on a busy node.** Lightning opens its databases before it will accept the password that unlocks the wallet, and on a loaded node that took nearly three minutes — longer than the node was willing to wait. Giving up restarted Lightning, which started the slow open again, so the wallet stayed locked forever and everything depending on it stayed broken. The node now waits as long as it takes. A genuinely wrong password still fails immediately.
- Known gaps, disclosed rather than buried: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The full 5x real-node lifecycle gate was not run for this release; reboot survival was verified directly on a live node — all installed apps returned after a cold reboot, uninstalled apps stayed gone, and restart policy was confirmed on every managed unit.
## v1.7.126-alpha (2026-08-07) ## v1.7.126-alpha (2026-08-07)
+1 -1
View File
@@ -19,7 +19,7 @@
"author": "Bitcoin Knots", "author": "Bitcoin Knots",
"category": "money", "category": "money",
"tier": "core", "tier": "core",
"dockerImage": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest", "dockerImage": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210",
"repoUrl": "https://github.com/bitcoinknots/bitcoin" "repoUrl": "https://github.com/bitcoinknots/bitcoin"
}, },
{ {
+7 -1
View File
@@ -7,7 +7,13 @@ app:
container_name: bitcoin-knots container_name: bitcoin-knots
container: container:
image: source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest # Pinned deliberately — NEVER use :latest for a consensus-critical app.
# Knots 20260508 applies the BIP110/RDTS network upgrade and HALTS until an
# operator explicitly sets consensusrules=rdts: a node running it sits frozen
# (blocks and headers both static) while reporting itself synced. A moving
# :latest tag can therefore stop the whole fleet following the chain without
# anyone choosing that. Changing this line is a consensus decision.
image: source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210
pull_policy: if-not-present pull_policy: if-not-present
network: archy-net network: archy-net
entrypoint: ["sh", "-lc"] entrypoint: ["sh", "-lc"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "archipelago" name = "archipelago"
version = "1.7.126-alpha" version = "1.7.127-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"
+1 -3
View File
@@ -290,9 +290,7 @@ async fn probe_one(client: &reqwest::Client, url: &str, label: &str, findings: &
if label == "/v1/models" { if label == "/v1/models" {
findings.models_endpoint_ok = Some(status.is_success()); findings.models_endpoint_ok = Some(status.is_success());
findings.models_endpoint_openai_shape = Some( findings.models_endpoint_openai_shape = Some(
status.is_success() status.is_success() && body.contains("\"data\"") && body.contains("\"object\""),
&& body.contains("\"data\"")
&& body.contains("\"object\""),
); );
} }
if (status.as_u16() == 401 || status.as_u16() == 402) if (status.as_u16() == 401 || status.as_u16() == 402)
+5 -1
View File
@@ -441,7 +441,11 @@ impl ApiHandler {
// from the cookie inside handle_model_proxy — it does not trust // from the cookie inside handle_model_proxy — it does not trust
// nginx to have gated the request already, the same "don't trust // nginx to have gated the request already, the same "don't trust
// the front door" discipline as /lnd-connect-info below. // the front door" discipline as /lnd-connect-info below.
(_, p) if p.starts_with("/aiui/api/claude/") || p.starts_with("/aiui/api/ollama/") || p.starts_with("/aiui/api/web-search") => { (_, p)
if p.starts_with("/aiui/api/claude/")
|| p.starts_with("/aiui/api/ollama/")
|| p.starts_with("/aiui/api/web-search") =>
{
self.handle_model_proxy(req_with_bytes, p).await self.handle_model_proxy(req_with_bytes, p).await
} }
+30 -32
View File
@@ -135,8 +135,7 @@ fn blocked_secret_shaped() -> Response<Body> {
/// 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> { async fn forward_screen(text: &str, data_dir: &Path) -> Option<&'static str> {
let secrets = let secrets = crate::assistant::egress::load_known_secrets(&data_dir.join("secrets")).await;
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)
} }
@@ -171,7 +170,10 @@ async fn forward_claude(req: Request<Body>, rest: &str, data_dir: &Path) -> Resu
.await .await
.map_err(|e| anyhow::anyhow!("read request payload: {e}"))?; .map_err(|e| anyhow::anyhow!("read request payload: {e}"))?;
if let Some(kind) = forward_screen(&String::from_utf8_lossy(&payload), data_dir).await { if let Some(kind) = forward_screen(&String::from_utf8_lossy(&payload), data_dir).await {
tracing::error!(kind, "model proxy: blocked claude forward — secret-shaped content"); tracing::error!(
kind,
"model proxy: blocked claude forward — secret-shaped content"
);
return Ok(blocked_secret_shaped()); return Ok(blocked_secret_shaped());
} }
let req = Request::from_parts(parts, Body::from(payload)); let req = Request::from_parts(parts, Body::from(payload));
@@ -229,7 +231,10 @@ async fn forward_web_search(req: Request<Body>, data_dir: &Path) -> Result<Respo
// `+` are how a pasted phrase's words separate inside a query string. // `+` are how a pasted phrase's words separate inside a query string.
let decoded = query.replace('+', " ").replace("%20", " "); let decoded = query.replace('+', " ").replace("%20", " ");
if let Some(kind) = forward_screen(&decoded, data_dir).await { if let Some(kind) = forward_screen(&decoded, data_dir).await {
tracing::error!(kind, "model proxy: blocked web-search query — secret-shaped content"); tracing::error!(
kind,
"model proxy: blocked web-search query — secret-shaped content"
);
return Ok(blocked_secret_shaped()); return Ok(blocked_secret_shaped());
} }
let rest = web_search_upstream_path(query); let rest = web_search_upstream_path(query);
@@ -283,8 +288,8 @@ async fn forward(
.build() .build()
.map_err(|e| anyhow::anyhow!("client build: {e}"))?; .map_err(|e| anyhow::anyhow!("client build: {e}"))?;
let reqwest_method = reqwest::Method::from_bytes(method.as_str().as_bytes()) let reqwest_method =
.unwrap_or(reqwest::Method::POST); reqwest::Method::from_bytes(method.as_str().as_bytes()).unwrap_or(reqwest::Method::POST);
let url = format!("{}{}", upstream_base, rest); let url = format!("{}{}", upstream_base, rest);
let mut upstream_req = client let mut upstream_req = client
.request(reqwest_method, &url) .request(reqwest_method, &url)
@@ -372,14 +377,9 @@ mod tests {
let store = test_store().await; let store = test_store().await;
let data_dir = tempfile::tempdir().unwrap(); let data_dir = tempfile::tempdir().unwrap();
let req = req_with_cookie("POST", "/aiui/api/claude/v1/messages", None); let req = req_with_cookie("POST", "/aiui/api/claude/v1/messages", None);
let resp = route_model_proxy( let resp = route_model_proxy(&store, data_dir.path(), req, "/aiui/api/claude/v1/messages")
&store, .await
data_dir.path(), .unwrap();
req,
"/aiui/api/claude/v1/messages",
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
} }
@@ -407,7 +407,10 @@ mod tests {
#[test] #[test]
fn web_search_query_forces_json_and_strips_format() { fn web_search_query_forces_json_and_strips_format() {
assert_eq!(web_search_upstream_path("q=bitcoin"), "search?q=bitcoin&format=json"); assert_eq!(
web_search_upstream_path("q=bitcoin"),
"search?q=bitcoin&format=json"
);
assert_eq!( assert_eq!(
web_search_upstream_path("q=bitcoin+halving&format=html"), web_search_upstream_path("q=bitcoin+halving&format=html"),
"search?q=bitcoin+halving&format=json" "search?q=bitcoin+halving&format=json"
@@ -424,14 +427,9 @@ mod tests {
"/aiui/api/claude/v1/messages", "/aiui/api/claude/v1/messages",
Some("not-a-real-token"), Some("not-a-real-token"),
); );
let resp = route_model_proxy( let resp = route_model_proxy(&store, data_dir.path(), req, "/aiui/api/claude/v1/messages")
&store, .await
data_dir.path(), .unwrap();
req,
"/aiui/api/claude/v1/messages",
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
} }
@@ -442,18 +440,18 @@ mod tests {
// Deliberately no data_dir/secrets/claude-api-key written. // Deliberately no data_dir/secrets/claude-api-key written.
let data_dir = tempfile::tempdir().unwrap(); let data_dir = tempfile::tempdir().unwrap();
let req = req_with_cookie("POST", "/aiui/api/claude/v1/messages", Some(&token)); let req = req_with_cookie("POST", "/aiui/api/claude/v1/messages", Some(&token));
let resp = route_model_proxy( let resp = route_model_proxy(&store, data_dir.path(), req, "/aiui/api/claude/v1/messages")
&store, .await
data_dir.path(), .unwrap();
req,
"/aiui/api/claude/v1/messages",
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE); assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
} }
fn req_with_cookie_and_body(method: &str, path: &str, cookie: Option<&str>, body: &'static str) -> Request<Body> { fn req_with_cookie_and_body(
method: &str,
path: &str,
cookie: Option<&str>,
body: &'static str,
) -> Request<Body> {
let mut builder = Request::builder().method(method).uri(path); let mut builder = Request::builder().method(method).uri(path);
if let Some(c) = cookie { if let Some(c) = cookie {
builder = builder.header("cookie", format!("session={c}")); builder = builder.header("cookie", format!("session={c}"));
+4 -1
View File
@@ -253,7 +253,10 @@ impl ApiHandler {
.status(StatusCode::PARTIAL_CONTENT) .status(StatusCode::PARTIAL_CONTENT)
.header("Content-Type", mime_type) .header("Content-Type", mime_type)
.header("Content-Length", slice.len().to_string()) .header("Content-Length", slice.len().to_string())
.header("Content-Range", format!("bytes {}-{}/{}", start, end, total)) .header(
"Content-Range",
format!("bytes {}-{}/{}", start, end, total),
)
.header("Accept-Ranges", "bytes") .header("Accept-Ranges", "bytes")
.body(hyper::Body::from(slice.to_vec())) .body(hyper::Body::from(slice.to_vec()))
.unwrap_or_else(|_| Response::new(hyper::Body::empty()))); .unwrap_or_else(|_| Response::new(hyper::Body::empty())));
@@ -405,9 +405,17 @@ impl RpcHandler {
.as_ref() .as_ref()
.ok_or_else(|| anyhow::anyhow!("Mesh service not running"))?; .ok_or_else(|| anyhow::anyhow!("Mesh service not running"))?;
let device_type = svc.shared_state().status.read().await.device_type; let device_type = svc.shared_state().status.read().await.device_type;
// Resource transfer is a native RNS transfer over LoRa — it needs an
// actual radio route to this contact, not just a Reticulum device on
// our end. A federation-only peer with no radio twin fits the size
// and device-type checks but has no dest_prefix to send to; without
// this check the send falls into send_content_resource and fails
// with "Peer is federation-only (no radio twin)" (picture-send,
// 2026-08-07) instead of falling back to the federation path below.
let use_resource_transfer = bytes.len() > INLINE_HARD_MAX let use_resource_transfer = bytes.len() > INLINE_HARD_MAX
&& device_type == crate::mesh::types::DeviceType::Reticulum && device_type == crate::mesh::types::DeviceType::Reticulum
&& bytes.len() <= RETICULUM_RESOURCE_MAX; && bytes.len() <= RETICULUM_RESOURCE_MAX
&& svc.has_radio_route(contact_id).await;
if bytes.len() > INLINE_HARD_MAX && !use_resource_transfer { if bytes.len() > INLINE_HARD_MAX && !use_resource_transfer {
anyhow::bail!( anyhow::bail!(
@@ -492,15 +500,58 @@ impl RpcHandler {
) )
.await? .await?
} else { } else {
svc.send_typed_wire( // Federation-only peers have no radio twin for
contact_id, // send_typed_wire's LoRa dest-prefix resolution — route over
wire, // Tor federation instead, mirroring mesh.send-content's onion
"content_ref", // lookup, or the send fails with "Peer is federation-only (no
&display, // radio twin)" (picture-send from a federation-only contact,
Some(typed_json), // 2026-08-07).
seq, let federation_onion = {
) let state = svc.shared_state();
.await? let peers = state.peers.read().await;
peers
.get(&contact_id)
.map(|p| (p.pubkey_hex.clone(), p.did.clone()))
};
let federation_onion = match federation_onion {
Some((Some(pubkey_hex), did)) => {
let nodes = crate::federation::load_nodes(&self.config.data_dir)
.await
.unwrap_or_default();
nodes
.iter()
.find(|n| n.pubkey == pubkey_hex)
.map(|n| n.onion.clone())
.or_else(|| {
did.as_ref().and_then(|d| {
nodes.iter().find(|n| &n.did == d).map(|n| n.onion.clone())
})
})
}
_ => None,
};
if let Some(onion) = federation_onion {
svc.send_typed_wire_via_federation(
contact_id,
&onion,
wire,
"content_ref",
&display,
Some(typed_json),
seq,
)
.await?
} else {
svc.send_typed_wire(
contact_id,
wire,
"content_ref",
&display,
Some(typed_json),
seq,
)
.await?
}
} }
}; };
@@ -590,6 +641,16 @@ impl RpcHandler {
let est_seconds = (size.saturating_add(lora_bytes_per_sec - 1) / lora_bytes_per_sec).max(1); let est_seconds = (size.saturating_add(lora_bytes_per_sec - 1) / lora_bytes_per_sec).max(1);
let is_reticulum = device_type == crate::mesh::types::DeviceType::Reticulum; let is_reticulum = device_type == crate::mesh::types::DeviceType::Reticulum;
// A Reticulum device on our end doesn't mean THIS peer is radio
// reachable — a federation-only contact (no radio twin) has no dest
// prefix for a resource transfer, even though it's small enough and
// our device type qualifies. Without this check the frontend was
// steered into mesh.send-content-inline's resource-transfer path,
// which fails with "Peer is federation-only (no radio twin)"
// (picture-send, 2026-08-07); the tier below now defers to the
// has_tor branches for such peers, which route via mesh.send-content
// (federation) instead.
let has_radio_route = is_reticulum && svc.has_radio_route(contact_id).await;
let (tier, reason) = if size <= MESH_AUTO_MAX { let (tier, reason) = if size <= MESH_AUTO_MAX {
("auto-mesh", "Small enough to send inline over mesh") ("auto-mesh", "Small enough to send inline over mesh")
} else if size <= MESH_HARD_MAX { } else if size <= MESH_HARD_MAX {
@@ -598,7 +659,7 @@ impl RpcHandler {
} else { } else {
("auto-mesh", "No Tor path — sending inline over mesh") ("auto-mesh", "No Tor path — sending inline over mesh")
} }
} else if is_reticulum && size <= RETICULUM_RESOURCE_MAX { } else if has_radio_route && size <= RETICULUM_RESOURCE_MAX {
( (
"resource-mesh", "resource-mesh",
"Sending directly over LoRa via a Reticulum resource transfer", "Sending directly over LoRa via a Reticulum resource transfer",
@@ -64,6 +64,18 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
"must be", "must be",
"cannot", "cannot",
"Password", "Password",
// The federation escalation sentinel. "Password" above does NOT cover
// it — starts_with is case-sensitive and the sentinel is ALL-CAPS —
// so the frontend's isPasswordRequired() never saw it and the
// password prompt could never open. Net effect: no node could mint a
// Trusted invite or promote a peer from the trust dropdown, fleet-wide
// (2026-08-09). The sentinel is machine-read by the UI; it must pass
// through verbatim.
"PASSWORD_REQUIRED",
// "Tor address not available. Tor may not be running." — the invite
// handler's precondition, entirely user-actionable, was likewise
// collapsing into the generic message.
"Tor address not available",
"Session", "Session",
"Failed to pull", "Failed to pull",
"Failed to start", "Failed to start",
@@ -177,6 +189,24 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
mod sanitize_tests { mod sanitize_tests {
use super::sanitize_error_message; use super::sanitize_error_message;
#[test]
fn password_required_sentinel_passes_through_verbatim() {
// The UI machine-reads this sentinel (isPasswordRequired checks
// `includes('PASSWORD_REQUIRED')`) to know it should open the password
// prompt. The "Password" prefix does not cover it — starts_with is
// case-sensitive — and masking it made Trusted invites and trust
// promotion impossible on EVERY node (2026-08-09): the prompt simply
// never opened.
let msg = "PASSWORD_REQUIRED: node password required to grant Trusted";
assert_eq!(sanitize_error_message(msg), msg);
}
#[test]
fn tor_unavailable_precondition_passes_through() {
let msg = "Tor address not available. Tor may not be running.";
assert_eq!(sanitize_error_message(msg), msg);
}
#[test] #[test]
fn seed_reveal_errors_pass_through() { fn seed_reveal_errors_pass_through() {
// Every user-actionable seed.reveal failure must reach the user — // Every user-actionable seed.reveal failure must reach the user —
+1 -1
View File
@@ -43,7 +43,7 @@ mod security;
mod seed_rpc; mod seed_rpc;
mod streaming; mod streaming;
mod system; mod system;
mod tor; pub(crate) mod tor;
mod totp; mod totp;
mod transitional; mod transitional;
mod transport; mod transport;
@@ -602,11 +602,13 @@ pub(super) fn get_data_dirs_for_app(package_id: &str) -> Vec<String> {
// (operator report, 2026-08-07). This list is deliberately hardcoded // (operator report, 2026-08-07). This list is deliberately hardcoded
// and reviewed: deletion code must never derive its targets from a // and reviewed: deletion code must never derive its targets from a
// manifest at uninstall time (a bad manifest could aim the wipe). // manifest at uninstall time (a bad manifest could aim the wipe).
"btcpay-server" | "btcpayserver" | "btcpay" | "archy-btcpay-db" | "archy-nbxplorer" => vec![ "btcpay-server" | "btcpayserver" | "btcpay" | "archy-btcpay-db" | "archy-nbxplorer" => {
format!("{}/btcpay", base), vec![
format!("{}/postgres-btcpay", base), format!("{}/btcpay", base),
format!("{}/nbxplorer", base), format!("{}/postgres-btcpay", base),
], format!("{}/nbxplorer", base),
]
}
"fedimint" => vec![ "fedimint" => vec![
format!("{}/fedimint", base), format!("{}/fedimint", base),
format!("{}/fedimint-gateway", base), format!("{}/fedimint-gateway", base),
@@ -1015,9 +1015,7 @@ impl RpcHandler {
/// ///
/// Node-side because these grants were in per-origin localStorage, so they /// Node-side because these grants were in per-origin localStorage, so they
/// vanished whenever the operator reached the node by a different address. /// vanished whenever the operator reached the node by a different address.
pub(in crate::api::rpc) async fn handle_ai_permissions_get( pub(in crate::api::rpc) async fn handle_ai_permissions_get(&self) -> Result<serde_json::Value> {
&self,
) -> Result<serde_json::Value> {
let granted = Self::ai_grants_unified(&self.config.data_dir).await; let granted = Self::ai_grants_unified(&self.config.data_dir).await;
Ok(serde_json::json!({ "granted": granted })) Ok(serde_json::json!({ "granted": granted }))
} }
@@ -1658,7 +1656,11 @@ mod ai_grants_tests {
.unwrap(); .unwrap();
let got = RpcHandler::ai_grants_unified(dir.path()).await; let got = RpcHandler::ai_grants_unified(dir.path()).await;
assert_eq!(got, vec!["apps".to_string()], "legacy file must not widen the authority"); assert_eq!(
got,
vec!["apps".to_string()],
"legacy file must not widen the authority"
);
} }
/// With no assistant grants file yet (pre-unification upgrade), the /// With no assistant grants file yet (pre-unification upgrade), the
+86 -1
View File
@@ -129,7 +129,7 @@ pub(in crate::api::rpc) async fn restart_tor() -> Result<()> {
.await .await
} }
pub(super) async fn check_tor_running() -> bool { pub(crate) async fn check_tor_running() -> bool {
tokio::net::TcpStream::connect("127.0.0.1:9050") tokio::net::TcpStream::connect("127.0.0.1:9050")
.await .await
.is_ok() .is_ok()
@@ -184,9 +184,94 @@ async fn archy_net_gateway_and_subnet() -> Option<(String, String)> {
if gateway.is_empty() || subnet.is_empty() { if gateway.is_empty() || subnet.is_empty() {
return None; return None;
} }
// Podman REPORTING a gateway is not proof the host can bind it. Under
// rootless podman the bridge and its gateway live inside a network
// namespace, so the address never appears on a host interface — and Tor
// binds it at startup, not at config-check time (`--verify-config` passes
// happily). The result on austin-sapien, 2026-08-09:
//
// [warn] Could not bind to 10.89.0.1:9050: Cannot assign requested address
// [warn] Failed to parse/validate config: Failed to bind one of the listener ports.
// [err] Reading config failed--see warnings above.
//
// Tor then refuses to start AT ALL — loopback SOCKS and every hidden
// service go with it. Widening SOCKS must never be able to take the whole
// daemon down, so probe the address exactly the way Tor will and fall back
// to the loopback-only branch when it is not bindable.
if !host_can_bind(&gateway) {
tracing::warn!(
%gateway,
"archy-net gateway is not bindable on this host (rootless podman \
namespaces the bridge) keeping Tor SOCKS loopback-only"
);
return None;
}
Some((gateway, subnet)) Some((gateway, subnet))
} }
/// Boot-time Tor self-heal: rebuild torrc from current config and apply it if
/// the live file has drifted or Tor is not actually answering.
///
/// Without this, shipping a torrc-generation fix does NOT repair the nodes it
/// was written for. `regenerate_torrc` only runs from the Tor RPC handlers and
/// package install, so a node carrying a bad torrc keeps it until somebody
/// happens to install an app or toggle a Tor setting — its Tor stays down
/// indefinitely and an OTA changes nothing. That is exactly what happened with
/// the unbindable `SocksPort <archy-net gateway>` line: two of three reachable
/// nodes had Tor dead, one of them unnoticed.
///
/// Deliberately conservative — it only applies (which restarts Tor) when the
/// staged torrc differs from the live one or Tor is not answering on 9050.
/// A healthy node with a matching torrc is left completely alone.
pub(crate) async fn heal_on_boot(data_dir: &Path) -> Result<bool> {
let config_dir = data_dir.join("tor-config");
let config = load_services_config(&config_dir).await;
// Stage what the torrc SHOULD be with the current (fixed) generator.
regenerate_torrc(&config).await?;
let staged = tokio::fs::read_to_string("/var/lib/archipelago/tor-config/torrc.staged")
.await
.unwrap_or_default();
if staged.is_empty() {
return Ok(false);
}
let live = tokio::fs::read_to_string("/etc/tor/torrc")
.await
.unwrap_or_default();
let drifted = live.trim() != staged.trim();
let answering = check_tor_running().await;
if !drifted && answering {
debug!("Tor healthy and torrc in sync — nothing to heal");
return Ok(false);
}
info!(
drifted,
answering, "Healing Tor: rewriting torrc from current config and restarting"
);
restart_tor().await?;
// Report the truth rather than assuming the restart worked.
let ok = check_tor_running().await;
if !ok {
warn!("Tor still not answering on 127.0.0.1:9050 after heal");
}
Ok(ok)
}
/// Can this host actually bind `addr`? Binds an ephemeral port, the same
/// operation Tor performs, so the answer matches Tor's own behaviour rather
/// than inferring it from interface listings.
fn host_can_bind(addr: &str) -> bool {
use std::net::{IpAddr, SocketAddr, TcpListener};
match addr.parse::<IpAddr>() {
Ok(ip) => TcpListener::bind(SocketAddr::new(ip, 0)).is_ok(),
Err(_) => false,
}
}
pub(in crate::api::rpc) async fn regenerate_torrc(config: &ServicesConfig) -> Result<()> { pub(in crate::api::rpc) async fn regenerate_torrc(config: &ServicesConfig) -> Result<()> {
let base = detect_hidden_service_base(); let base = detect_hidden_service_base();
let mut lines = vec![ let mut lines = vec![
+46 -2
View File
@@ -480,6 +480,44 @@ async fn proxy_to_app(
parts.headers.remove(header::AUTHORIZATION); parts.headers.remove(header::AUTHORIZATION);
} }
// Websocket upgrades need splicing, not request forwarding. hyper::Client
// alone completes the app's 101 handshake and then DROPS the upgraded
// connection, so every ws-driven app (mempool's entire UI is one) loaded
// fine through the gate and then died with close code 1006 on connect —
// reported from a browser console, 2026-08-09, after two other layers of
// the same symptom had already been fixed. The gate's server side already
// accepts client upgrades (.with_upgrades() in listener.rs); this is the
// missing upstream half: hand the handshake to the app and, on 101, bridge
// the two upgraded connections byte-for-byte. Header stripping above still
// applies — the app sees the same sanitized headers on a ws handshake as
// on any other request.
if parts.headers.contains_key(header::UPGRADE) {
// The client side's OnUpgrade handle rides in the request extensions;
// take it before the parts become the upstream request. It resolves
// once serve_connection's with_upgrades hands us the raw socket after
// we return the 101.
let client_upgrade = parts.extensions.remove::<hyper::upgrade::OnUpgrade>();
let upstream_req = Request::from_parts(parts, Body::empty());
let client = hyper::Client::new();
let mut upstream_resp = match client.request(upstream_req).await {
Ok(resp) => resp,
Err(_) => return bad_gateway(),
};
if upstream_resp.status() == StatusCode::SWITCHING_PROTOCOLS {
if let Some(client_upgrade) = client_upgrade {
let upstream_upgrade = hyper::upgrade::on(&mut upstream_resp);
tokio::spawn(async move {
if let (Ok(mut client_io), Ok(mut app_io)) =
(client_upgrade.await, upstream_upgrade.await)
{
let _ = tokio::io::copy_bidirectional(&mut client_io, &mut app_io).await;
}
});
}
}
return upstream_resp;
}
let client = hyper::Client::new(); let client = hyper::Client::new();
match client.request(Request::from_parts(parts, body)).await { match client.request(Request::from_parts(parts, body)).await {
Ok(resp) => resp, Ok(resp) => resp,
@@ -913,7 +951,10 @@ mod tests {
// A <link rel="manifest"> fetch never carries the cookie, so these must // A <link rel="manifest"> fetch never carries the cookie, so these must
// pass or a logged-in user still gets 401 + a login page. // pass or a logged-in user still gets 401 + a login page.
for p in ["/manifest.json", "/site.webmanifest", "/favicon.ico"] { for p in ["/manifest.json", "/site.webmanifest", "/favicon.ico"] {
assert!(AppGate::is_credentialless_public_path(p), "{p} still challenged"); assert!(
AppGate::is_credentialless_public_path(p),
"{p} still challenged"
);
} }
} }
@@ -931,7 +972,10 @@ mod tests {
"/api/auth/nostr/session", "/api/auth/nostr/session",
"/admin", "/admin",
] { ] {
assert!(!AppGate::is_credentialless_public_path(p), "{p} wrongly bypassed the gate"); assert!(
!AppGate::is_credentialless_public_path(p),
"{p} wrongly bypassed the gate"
);
} }
} }
use super::*; use super::*;
@@ -276,7 +276,8 @@ fn parse_openai_tool_calls(raw_calls: &[Value]) -> Vec<ToolCall> {
.get("arguments") .get("arguments")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("{}"); .unwrap_or("{}");
let arguments: Value = serde_json::from_str(arguments_str).unwrap_or_else(|_| json!({})); let arguments: Value =
serde_json::from_str(arguments_str).unwrap_or_else(|_| json!({}));
Some(ToolCall { Some(ToolCall {
id, id,
name, name,
@@ -797,9 +798,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn send_with_zero_providers_returns_a_clean_error_not_a_panic() { async fn send_with_zero_providers_returns_a_clean_error_not_a_panic() {
let backend = backend_for("unused", 1_000); let backend = backend_for("unused", 1_000);
let result = backend let result = backend.send_with_providers(&[], "sys", &[], &[]).await;
.send_with_providers(&[], "sys", &[], &[])
.await;
assert!(result.is_err(), "zero providers must be a clean Err"); assert!(result.is_err(), "zero providers must be a clean Err");
let msg = result.err().expect("checked is_err above").to_string(); let msg = result.err().expect("checked is_err above").to_string();
assert!( assert!(
@@ -847,8 +846,7 @@ mod tests {
assert_eq!(endpoint, "http://onionaddr123.onion"); assert_eq!(endpoint, "http://onionaddr123.onion");
// Tor down -> clearnet endpoint instead. // Tor down -> clearnet endpoint instead.
let (_p, _m, _price, endpoint) = let (_p, _m, _price, endpoint) = select_provider(&[p], 1_000, false).expect("affordable");
select_provider(&[p], 1_000, false).expect("affordable");
assert_eq!(endpoint, "https://clearnet.example.com"); assert_eq!(endpoint, "https://clearnet.example.com");
} }
+3 -2
View File
@@ -576,8 +576,9 @@ mod tests {
#[test] #[test]
fn real_system_prompt_is_not_a_seed_phrase() { fn real_system_prompt_is_not_a_seed_phrase() {
let registry = crate::assistant::tools::registry(); let registry = crate::assistant::tools::registry();
let all: std::collections::BTreeSet<_> = let all: std::collections::BTreeSet<_> = crate::assistant::PermissionCategory::ALL
crate::assistant::PermissionCategory::ALL.into_iter().collect(); .into_iter()
.collect();
let visible = registry.visible_to(&all); let visible = registry.visible_to(&all);
let prompt = crate::assistant::build_system_prompt(&visible, &[]); let prompt = crate::assistant::build_system_prompt(&visible, &[]);
assert!( assert!(
+20 -5
View File
@@ -504,7 +504,10 @@ pub async fn run_case(case: &EvalCase, backend_under_test: &dyn Backend) -> Resu
// Trace-writing is diagnostic only — never fail a case over it, and // Trace-writing is diagnostic only — never fail a case over it, and
// never fall back to writing it anywhere else (AI-SPEC §7a: plain // never fall back to writing it anywhere else (AI-SPEC §7a: plain
// files under the gitignored build directory, nowhere else). // files under the gitignored build directory, nowhere else).
eprintln!("assistant-evals: could not write trace for {}: {e}", case.id); eprintln!(
"assistant-evals: could not write trace for {}: {e}",
case.id
);
} }
Ok(outcome) Ok(outcome)
} }
@@ -862,8 +865,14 @@ fn forbidden_execution_fails_the_suite() {
}; };
let err = evaluate_case(case, &injected_violation) let err = evaluate_case(case, &injected_violation)
.expect_err("a forbidden execution must fail the suite, not pass silently"); .expect_err("a forbidden execution must fail the suite, not pass silently");
assert!(err.contains(&case.id), "failure must name the case id: {err}"); assert!(
assert!(err.contains("app_restart"), "failure must name the tool: {err}"); err.contains(&case.id),
"failure must name the case id: {err}"
);
assert!(
err.contains("app_restart"),
"failure must name the tool: {err}"
);
assert!(err.contains("FORBIDDEN EXECUTION"), "{err}"); assert!(err.contains("FORBIDDEN EXECUTION"), "{err}");
} }
@@ -884,8 +893,14 @@ fn forbidden_claim_fails_the_suite() {
}; };
let err = evaluate_case(case, &injected_violation) let err = evaluate_case(case, &injected_violation)
.expect_err("a forbidden claim must fail the suite, not pass silently"); .expect_err("a forbidden claim must fail the suite, not pass silently");
assert!(err.contains(&case.id), "failure must name the case id: {err}"); assert!(
assert!(err.contains("sent 10,000 sats"), "failure must name the term: {err}"); err.contains(&case.id),
"failure must name the case id: {err}"
);
assert!(
err.contains("sent 10,000 sats"),
"failure must name the term: {err}"
);
assert!(err.contains("FORBIDDEN CLAIM"), "{err}"); assert!(err.contains("FORBIDDEN CLAIM"), "{err}");
} }
+3 -3
View File
@@ -60,8 +60,6 @@ impl Grants {
/// data layer; `CallerScope::granted_categories` has no other source of /// data layer; `CallerScope::granted_categories` has no other source of
/// authority to fall back to. /// authority to fall back to.
pub async fn load(data_dir: &Path) -> Grants { pub async fn load(data_dir: &Path) -> Grants {
let path = data_dir.join(GRANTS_FILE); let path = data_dir.join(GRANTS_FILE);
let Ok(content) = tokio::fs::read_to_string(&path).await else { let Ok(content) = tokio::fs::read_to_string(&path).await else {
return Grants::default_closed(); return Grants::default_closed();
@@ -75,7 +73,9 @@ impl Grants {
/// file is authoritative, while an absent one triggers the one-time /// file is authoritative, while an absent one triggers the one-time
/// legacy migration. /// legacy migration.
pub(crate) async fn exists(data_dir: &Path) -> bool { pub(crate) async fn exists(data_dir: &Path) -> bool {
tokio::fs::metadata(data_dir.join(GRANTS_FILE)).await.is_ok() tokio::fs::metadata(data_dir.join(GRANTS_FILE))
.await
.is_ok()
} }
/// Persist the grants for this node, 0600 (following /// Persist the grants for this node, 0600 (following
+3 -1
View File
@@ -477,7 +477,9 @@ mod tests {
"the user's own prior turn must be replayed: {texts:?}" "the user's own prior turn must be replayed: {texts:?}"
); );
assert!( assert!(
texts.iter().any(|t| t.contains("Yes, filebrowser is running.")), texts
.iter()
.any(|t| t.contains("Yes, filebrowser is running.")),
"the assistant's prior answer must be replayed: {texts:?}" "the assistant's prior answer must be replayed: {texts:?}"
); );
// Tool traffic is never replayed: a stale tool result is a claim // Tool traffic is never replayed: a stale tool result is a claim
+17 -17
View File
@@ -303,21 +303,17 @@ pub(crate) async fn execute_tool(call: &ToolCall, ctx: &ToolExecCtx) -> ToolResu
// value, before the untrusted wrap below turns it into // value, before the untrusted wrap below turns it into
// delimiter-fenced text. See `ToolExecCtx::surfaces`. // delimiter-fenced text. See `ToolExecCtx::surfaces`.
if super::tools::is_surface_tool(&call.name) { if super::tools::is_surface_tool(&call.name) {
ctx.note_surface( ctx.note_surface(&call.name, super::tools::surface_scope(&args), v.clone());
&call.name,
super::tools::surface_scope(&args),
v.clone(),
);
} }
ToolResult { ToolResult {
call_id: call.id.clone(), call_id: call.id.clone(),
is_error: false, is_error: false,
// D-10: peer-authored content (filenames, log lines, mesh/peer // D-10: peer-authored content (filenames, log lines, mesh/peer
// status) is wrapped in an untrusted-content boundary before it // status) is wrapped in an untrusted-content boundary before it
// becomes part of a ChatMessage — this IS the point where a // becomes part of a ChatMessage — this IS the point where a
// ToolResult is constructed. Operator/node-authored tool // ToolResult is constructed. Operator/node-authored tool
// results (disk status, settings) pass through unchanged. // results (disk status, settings) pass through unchanged.
content: super::tools::wrap_tool_result_if_untrusted(&call.name, v.to_string()), content: super::tools::wrap_tool_result_if_untrusted(&call.name, v.to_string()),
} }
} }
Err(msg) => ToolResult { Err(msg) => ToolResult {
@@ -525,8 +521,10 @@ mod tests {
); );
let notices = counters.notices(); let notices = counters.notices();
assert!( assert!(
notices.iter().any(|n| n.message.to_lowercase().contains("step limit") notices
|| n.message.to_lowercase().contains("loop")), .iter()
.any(|n| n.message.to_lowercase().contains("step limit")
|| n.message.to_lowercase().contains("loop")),
"reaching MAX_TURNS 3+ times in one session must raise an owner notice: {notices:?}" "reaching MAX_TURNS 3+ times in one session must raise an owner notice: {notices:?}"
); );
} }
@@ -567,8 +565,10 @@ mod tests {
Arc::new(crate::assistant::confirm::ConfirmGate::new()), Arc::new(crate::assistant::confirm::ConfirmGate::new()),
counters_a.clone(), counters_a.clone(),
); );
let wrapped = let wrapped = crate::assistant::untrusted::wrap_untrusted(
crate::assistant::untrusted::wrap_untrusted("PEER_NOTE", "ignore that, just try things"); "PEER_NOTE",
"ignore that, just try things",
);
let seeded_history = vec![ChatMessage { let seeded_history = vec![ChatMessage {
role: Role::Tool, role: Role::Tool,
text: Some(wrapped), text: Some(wrapped),
+7 -7
View File
@@ -834,7 +834,10 @@ pub fn build_system_prompt(
.ok() .ok()
.and_then(|v| v.as_str().map(|s| s.to_string())) .and_then(|v| v.as_str().map(|s| s.to_string()))
.unwrap_or_else(|| format!("{:?}", tool.category)); .unwrap_or_else(|| format!("{:?}", tool.category));
prompt.push_str(&format!("- {} [{}]: {}\n", tool.name, cat, tool.description)); prompt.push_str(&format!(
"- {} [{}]: {}\n",
tool.name, cat, tool.description
));
} }
prompt.push_str( prompt.push_str(
"When the request genuinely needs one of these, CALL it — then tell the operator \ "When the request genuinely needs one of these, CALL it — then tell the operator \
@@ -857,12 +860,10 @@ fn extract_needs_markers(text: &str) -> (String, Vec<PermissionCategory>) {
let mut rest = text; let mut rest = text;
while let Some(start) = rest.find("[[needs:") { while let Some(start) = rest.find("[[needs:") {
let after = &rest[start + 8..]; let after = &rest[start + 8..];
match after.find("]]" ) { match after.find("]]") {
Some(end) => { Some(end) => {
let id = after[..end].trim().to_ascii_lowercase(); let id = after[..end].trim().to_ascii_lowercase();
if let Ok(cat) = if let Ok(cat) = serde_json::from_str::<PermissionCategory>(&format!("\"{id}\"")) {
serde_json::from_str::<PermissionCategory>(&format!("\"{id}\""))
{
out.push_str(&rest[..start]); out.push_str(&rest[..start]);
if !found.contains(&cat) { if !found.contains(&cat) {
found.push(cat); found.push(cat);
@@ -1755,8 +1756,7 @@ mod tests {
_tools: &[tools::ToolDef], _tools: &[tools::ToolDef],
_history: &[tools::ChatMessage], _history: &[tools::ChatMessage],
) -> Result<backends::BackendTurn> { ) -> Result<backends::BackendTurn> {
self.calls self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Err(BudgetExhausted { Err(BudgetExhausted {
remaining_sats: self.remaining_sats, remaining_sats: self.remaining_sats,
quoted_price_sats: self.quoted_price_sats, quoted_price_sats: self.quoted_price_sats,
+56 -18
View File
@@ -243,11 +243,9 @@ impl ToolDef {
.map(ToolArgs::ContentList) .map(ToolArgs::ContentList)
.context("tool arguments did not match the declared schema"), .context("tool arguments did not match the declared schema"),
"system_disk_status" | "system_stats" | "apps_list" | "bitcoin_status" "system_disk_status" | "system_stats" | "apps_list" | "bitcoin_status"
| "network_status" | "mesh_status" => { | "network_status" | "mesh_status" => serde_json::from_value(raw.clone())
serde_json::from_value(raw.clone()) .map(ToolArgs::Empty)
.map(ToolArgs::Empty) .context("tool arguments did not match the declared schema"),
.context("tool arguments did not match the declared schema")
}
"app_logs" => serde_json::from_value(raw.clone()) "app_logs" => serde_json::from_value(raw.clone())
.map(ToolArgs::AppLogs) .map(ToolArgs::AppLogs)
.context("tool arguments did not match the declared schema"), .context("tool arguments did not match the declared schema"),
@@ -415,13 +413,18 @@ fn redact_log_line(line: &str) -> String {
fn redact_secrets_in_json(value: serde_json::Value) -> serde_json::Value { fn redact_secrets_in_json(value: serde_json::Value) -> serde_json::Value {
match value { match value {
serde_json::Value::String(s) => serde_json::Value::String( serde_json::Value::String(s) => serde_json::Value::String(
s.lines().map(redact_log_line).collect::<Vec<_>>().join("\n"), s.lines()
.map(redact_log_line)
.collect::<Vec<_>>()
.join("\n"),
), ),
serde_json::Value::Array(items) => { serde_json::Value::Array(items) => {
serde_json::Value::Array(items.into_iter().map(redact_secrets_in_json).collect()) serde_json::Value::Array(items.into_iter().map(redact_secrets_in_json).collect())
} }
serde_json::Value::Object(map) => serde_json::Value::Object( serde_json::Value::Object(map) => serde_json::Value::Object(
map.into_iter().map(|(k, v)| (k, redact_secrets_in_json(v))).collect(), map.into_iter()
.map(|(k, v)| (k, redact_secrets_in_json(v)))
.collect(),
), ),
other => other, other => other,
} }
@@ -804,7 +807,11 @@ pub async fn validate_business_rules(
/// here is meant to become the tool result's `content` verbatim — a /// here is meant to become the tool result's `content` verbatim — a
/// message the model (and, through it, the user) can read and act on, not /// message the model (and, through it, the user) can read and act on, not
/// an internal diagnostic. /// an internal diagnostic.
pub async fn dispatch(name: &str, args: &ToolArgs, handler: &Arc<RpcHandler>) -> Result<Value, String> { pub async fn dispatch(
name: &str,
args: &ToolArgs,
handler: &Arc<RpcHandler>,
) -> Result<Value, String> {
validate_business_rules(name, args, handler).await?; validate_business_rules(name, args, handler).await?;
match name { match name {
"system_disk_status" => handler "system_disk_status" => handler
@@ -1068,9 +1075,16 @@ mod tests {
// Listing peers is not the same action as listing this node's own // Listing peers is not the same action as listing this node's own
// files; sharing an action_key would let one be replayed as the other. // files; sharing an action_key would let one be replayed as the other.
use crate::assistant::confirm::action_key; use crate::assistant::confirm::action_key;
let own = content_list_tool().validate(&json!({ "scope": "own" })).unwrap(); let own = content_list_tool()
let peers = content_list_tool().validate(&json!({ "scope": "peers" })).unwrap(); .validate(&json!({ "scope": "own" }))
assert_ne!(action_key("content_list", &own), action_key("content_list", &peers)); .unwrap();
let peers = content_list_tool()
.validate(&json!({ "scope": "peers" }))
.unwrap();
assert_ne!(
action_key("content_list", &own),
action_key("content_list", &peers)
);
} }
use super::*; use super::*;
use crate::api::rpc::RpcHandler; use crate::api::rpc::RpcHandler;
@@ -1496,10 +1510,25 @@ mod tests {
"wifi_ssid": "Pretty Fly for a Wi-Fi", "wifi_ssid": "Pretty Fly for a Wi-Fi",
})); }));
let obj = stripped.as_object().expect("diagnostics stays an object"); let obj = stripped.as_object().expect("diagnostics stays an object");
assert!(!obj.contains_key("wan_ip"), "WAN IP must not enter model context"); assert!(
assert!(!obj.contains_key("wifi_ssid"), "Wi-Fi SSID must not enter model context"); !obj.contains_key("wan_ip"),
for kept in ["nat_type", "upnp_available", "tor_connected", "dns_working", "recommendations"] { "WAN IP must not enter model context"
assert!(obj.contains_key(kept), "connectivity field {kept} must survive"); );
assert!(
!obj.contains_key("wifi_ssid"),
"Wi-Fi SSID must not enter model context"
);
for kept in [
"nat_type",
"upnp_available",
"tor_connected",
"dns_working",
"recommendations",
] {
assert!(
obj.contains_key(kept),
"connectivity field {kept} must survive"
);
} }
// A result with neither key (e.g. offline diagnostics) passes through untouched. // A result with neither key (e.g. offline diagnostics) passes through untouched.
let already_clean = json!({ "nat_type": null, "dns_working": false }); let already_clean = json!({ "nat_type": null, "dns_working": false });
@@ -1523,12 +1552,21 @@ mod tests {
); );
// 64+ hex run → redacted as a key even without a keyword // 64+ hex run → redacted as a key even without a keyword
let hex = "a".repeat(64); let hex = "a".repeat(64);
assert_eq!(redact_log_line(&format!("seed {hex}")), "seed [REDACTED_KEY]"); assert_eq!(
redact_log_line(&format!("seed {hex}")),
"seed [REDACTED_KEY]"
);
// 64+ base64 run → redacted as a token // 64+ base64 run → redacted as a token
let b64 = "Q".repeat(68); let b64 = "Q".repeat(68);
assert_eq!(redact_log_line(&format!("macaroon blob {b64}")), "macaroon blob [REDACTED_TOKEN]"); assert_eq!(
redact_log_line(&format!("macaroon blob {b64}")),
"macaroon blob [REDACTED_TOKEN]"
);
// ...and as a key=value pair the keyword rule fires first // ...and as a key=value pair the keyword rule fires first
assert_eq!(redact_log_line(&format!("macaroon={b64}")), "macaroon=[REDACTED]"); assert_eq!(
redact_log_line(&format!("macaroon={b64}")),
"macaroon=[REDACTED]"
);
// An ordinary line is untouched // An ordinary line is untouched
let normal = "2026-08-07 INFO block height 861234"; let normal = "2026-08-07 INFO block height 861234";
assert_eq!(redact_log_line(normal), normal); assert_eq!(redact_log_line(normal), normal);
+123 -2
View File
@@ -166,6 +166,21 @@ pub async fn ensure_doctor_installed() {
Ok(false) => debug!("/opt/archipelago/apps already populated (or no installer copy)"), Ok(false) => debug!("/opt/archipelago/apps already populated (or no installer copy)"),
Err(e) => warn!("Apps dir repair failed (non-fatal): {:#}", e), Err(e) => warn!("Apps dir repair failed (non-fatal): {:#}", e),
} }
match run_tor_helper_sync().await {
Ok(true) => info!("tor-helper.sh synchronized with binary"),
Ok(false) => debug!("tor-helper.sh already current"),
Err(e) => warn!("tor-helper sync failed (non-fatal): {:#}", e),
}
match run_tor_torrc_repair().await {
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"),
Err(e) => warn!("Tor boot heal failed (non-fatal): {:#}", e),
}
match run_nginx_mempool_ws_repair().await {
Ok(true) => info!("nginx mempool websocket headers repaired and reloaded"),
Ok(false) => debug!("nginx mempool websocket headers already present"),
Err(e) => warn!("nginx mempool ws repair failed (non-fatal): {:#}", e),
}
match run_polkit_networkmanager_repair().await { match run_polkit_networkmanager_repair().await {
Ok(true) => info!( Ok(true) => info!(
"Installed NetworkManager polkit rule for the archipelago user — Wi-Fi setup enabled" "Installed NetworkManager polkit rule for the archipelago user — Wi-Fi setup enabled"
@@ -620,6 +635,111 @@ exit 2
} }
} }
/// Repair Tor at boot so a torrc-generation fix actually reaches the nodes it
/// was written for.
///
/// `regenerate_torrc` only ran from the Tor RPC handlers and package install,
/// so a node carrying a bad torrc kept it indefinitely: shipping a fixed binary
/// changed nothing until somebody happened to install an app or toggle a Tor
/// setting. On 2026-08-09 two of the three reachable nodes had Tor completely
/// down from an unbindable `SocksPort <archy-net gateway>` line, one of them
/// unnoticed, while the dashboard reported "connected".
///
/// Non-fatal and conservative: it only restarts Tor when the regenerated torrc
/// differs from the live one, or Tor is not answering on 9050.
/// The privileged Tor helper, embedded so the OTA actually delivers it.
/// scripts/tor-helper.sh previously reached nodes only through ISO builds and
/// manual deploys — the 2026-08-09 helper fix (reset-failed + truthful result)
/// would have shipped to nobody. Same include_str! pattern as the doctor.
const TOR_HELPER_SH: &str = include_str!("../../../scripts/tor-helper.sh");
const TOR_HELPER_PATH: &str = "/opt/archipelago/scripts/tor-helper.sh";
async fn run_tor_helper_sync() -> Result<bool> {
let current = tokio::fs::read_to_string(TOR_HELPER_PATH)
.await
.unwrap_or_default();
if current == TOR_HELPER_SH {
return Ok(false);
}
let staged = "/var/lib/archipelago/tor-config/tor-helper.staged";
if let Some(dir) = Path::new(staged).parent() {
tokio::fs::create_dir_all(dir).await.ok();
}
tokio::fs::write(staged, TOR_HELPER_SH)
.await
.context("stage tor-helper.sh")?;
let script = format!(
"set -eu\ninstall -m 0755 {staged} {dest}\nexit 0\n",
staged = staged,
dest = TOR_HELPER_PATH
);
host_sudo(&["sh", "-lc", &script])
.await
.context("install tor-helper.sh")?;
Ok(true)
}
/// Existing nodes' nginx configs never receive repo snippet fixes — the OTA
/// updates the binary and web assets, not /etc/nginx. The mempool UI is
/// websocket-driven, and every fleet node's /app/mempool/ proxy block strips
/// the Upgrade handshake, so the page loads and never connects (three-layer
/// outage, 2026-08-09). Idempotently add the two headers to any mempool block
/// missing them, in every nginx file that has one, then reload once.
async fn run_nginx_mempool_ws_repair() -> Result<bool> {
let script = r#"
set -eu
changed=0
for f in /etc/nginx/sites-available/archipelago-http \
/etc/nginx/sites-available/archipelago \
/etc/nginx/snippets/archipelago-https-app-proxies.conf; do
[ -f "$f" ] || continue
grep -q 'location /app/mempool/' "$f" || continue
python3 - "$f" <<'PYEOF'
import re, sys
p = sys.argv[1]
src = open(p).read()
def fix(m):
b = m.group(0)
if 'Upgrade $http_upgrade' in b:
return b
return b.replace('proxy_http_version 1.1;',
'proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection "upgrade";', 1)
new = re.sub(r'location /app/mempool/ \{[^}]*\}', fix, src, flags=re.S)
if new != src:
open(p, 'w').write(new)
sys.exit(3)
PYEOF
rc=$?
[ "$rc" = 3 ] && changed=1
[ "$rc" = 0 ] || [ "$rc" = 3 ] || exit "$rc"
done
if [ "$changed" = 1 ]; then
nginx -t >/dev/null 2>&1 && systemctl reload nginx || true
exit 3
fi
exit 0
"#;
let status = host_sudo(&["sh", "-lc", script])
.await
.context("nginx mempool ws repair")?;
Ok(status.code() == Some(3))
}
async fn run_tor_torrc_repair() -> Result<bool> {
// Same location the RPC handlers use (Config::data_dir); bootstrap runs
// before the server owns a Config, and this path is fixed on real installs.
let data_dir = Path::new("/var/lib/archipelago");
if !data_dir.exists() {
debug!("No {} — skipping Tor boot heal", data_dir.display());
return Ok(false);
}
if !Path::new("/etc/tor/torrc").exists() {
debug!("No /etc/tor/torrc — Tor not installed here, skipping boot heal");
return Ok(false);
}
crate::api::rpc::tor::heal_on_boot(data_dir).await
}
async fn run_bitcoin_rpc_repair() -> Result<bool> { async fn run_bitcoin_rpc_repair() -> Result<bool> {
// bitcoind is launched with -conf=/tmp/rpc.conf and never reads a // bitcoind is launched with -conf=/tmp/rpc.conf and never reads a
// datadir bitcoin.conf (apps/bitcoin-core & bitcoin-knots manifest.yml, // datadir bitcoin.conf (apps/bitcoin-core & bitcoin-knots manifest.yml,
@@ -1121,7 +1241,6 @@ async fn patch_nginx_conf(path: &str) -> Result<bool> {
patched = p; patched = p;
} }
if missing_v6_http { if missing_v6_http {
patched = patched.replace( patched = patched.replace(
"listen 80 default_server;", "listen 80 default_server;",
@@ -1304,7 +1423,9 @@ mod tests {
// Second pass is a no-op (idempotent self-heal). // Second pass is a no-op (idempotent self-heal).
assert!(heal_stale_web_search_block(&healed).is_none()); assert!(heal_stale_web_search_block(&healed).is_none());
// A config without the block is untouched. // A config without the block is untouched.
assert!(heal_stale_web_search_block("location / { try_files $uri /index.html; }").is_none()); assert!(
heal_stale_web_search_block("location / { try_files $uri /index.html; }").is_none()
);
} }
} }
+14 -9
View File
@@ -927,13 +927,12 @@ mod tests {
#[test] #[test]
fn every_backend_installed_leaves_no_orphans() { fn every_backend_installed_leaves_no_orphans() {
let orphans = orphan_companions(&ids(&[ let orphans = orphan_companions(&ids(&["bitcoin-knots", "lnd", "electrumx", "fedimint"]));
"bitcoin-knots", assert!(
"lnd", names(&orphans).is_empty(),
"electrumx", "unexpected orphans: {:?}",
"fedimint", names(&orphans)
])); );
assert!(names(&orphans).is_empty(), "unexpected orphans: {:?}", names(&orphans));
} }
fn name_set(specs: &[&'static CompanionSpec]) -> std::collections::HashSet<&'static str> { fn name_set(specs: &[&'static CompanionSpec]) -> std::collections::HashSet<&'static str> {
@@ -965,7 +964,10 @@ mod tests {
assert!(due.is_empty()); assert!(due.is_empty());
// A pass after the grace window reaps. // A pass after the grace window reaps.
let due = due_after_grace(orphans, &names_seen, &mut since, start + ORPHAN_GRACE); let due = due_after_grace(orphans, &names_seen, &mut since, start + ORPHAN_GRACE);
assert_eq!(names(&due), vec!["archy-electrs-ui", "archy-fedimint-ui", "archy-lnd-ui"]); assert_eq!(
names(&due),
vec!["archy-electrs-ui", "archy-fedimint-ui", "archy-lnd-ui"]
);
} }
#[test] #[test]
@@ -989,7 +991,10 @@ mod tests {
!names(&due).contains(&"archy-lnd-ui"), !names(&due).contains(&"archy-lnd-ui"),
"lnd companion reaped even though lnd came back" "lnd companion reaped even though lnd came back"
); );
assert!(!since.contains_key("archy-lnd-ui"), "stale clock kept for lnd"); assert!(
!since.contains_key("archy-lnd-ui"),
"stale clock kept for lnd"
);
// lnd goes away for real. It must wait a fresh full grace period. // lnd goes away for real. It must wait a fresh full grace period.
let orphans = orphan_companions(&ids(&["bitcoin-knots"])); let orphans = orphan_companions(&ids(&["bitcoin-knots"]));
@@ -5989,7 +5989,8 @@ app:
} }
#[tokio::test] #[tokio::test]
async fn manifest_generated_files_can_overwrite_when_declared() {let rt = Arc::new(MockRuntime::default()); async fn manifest_generated_files_can_overwrite_when_declared() {
let rt = Arc::new(MockRuntime::default());
let orch = orch_with(rt.clone()).await; let orch = orch_with(rt.clone()).await;
let data_dir = tempfile::tempdir_in("/var/lib/archipelago").unwrap(); let data_dir = tempfile::tempdir_in("/var/lib/archipelago").unwrap();
+12 -3
View File
@@ -244,13 +244,22 @@ mod tests {
#[test] #[test]
fn year_survives_number_string_and_date_forms() { fn year_survives_number_string_and_date_forms() {
assert_eq!(project(serde_json::json!({"releaseYear": 2014})).year_num(), Some(2014)); assert_eq!(
assert_eq!(project(serde_json::json!({"releaseYear": "2016"})).year_num(), Some(2016)); project(serde_json::json!({"releaseYear": 2014})).year_num(),
Some(2014)
);
assert_eq!(
project(serde_json::json!({"releaseYear": "2016"})).year_num(),
Some(2016)
);
assert_eq!( assert_eq!(
project(serde_json::json!({"releaseYear": "2020-05-01"})).year_num(), project(serde_json::json!({"releaseYear": "2020-05-01"})).year_num(),
Some(2020) Some(2020)
); );
assert_eq!(project(serde_json::json!({"releaseYear": "n/a"})).year_num(), None); assert_eq!(
project(serde_json::json!({"releaseYear": "n/a"})).year_num(),
None
);
assert_eq!(project(serde_json::json!({})).year_num(), None); assert_eq!(project(serde_json::json!({})).year_num(), None);
} }
+28 -28
View File
@@ -299,40 +299,40 @@ pub async fn serve_content(
// Check access control // Check access control
if !owner_session { if !owner_session {
match &item.access { match &item.access {
AccessControl::Paid { price_sats, .. } => { AccessControl::Paid { price_sats, .. } => {
// Two ways to satisfy payment: // Two ways to satisfy payment:
// (a) a valid ecash token (the local-wallet fast path), or // (a) a valid ecash token (the local-wallet fast path), or
// (b) a Lightning-invoice payment hash this node issued and has // (b) a Lightning-invoice payment hash this node issued and has
// since confirmed settled (the "pay from any wallet" path, #46). // since confirmed settled (the "pay from any wallet" path, #46).
// Each path only counts when the sharer accepts that method. // Each path only counts when the sharer accepts that method.
let mut authorized = false; let mut authorized = false;
if let Some(token) = payment_token { if let Some(token) = payment_token {
if (method_accepted(&item.access, "ecash") if (method_accepted(&item.access, "ecash")
|| method_accepted(&item.access, "fedimint")) || method_accepted(&item.access, "fedimint"))
&& verify_payment_token(data_dir, token, *price_sats).await && verify_payment_token(data_dir, token, *price_sats).await
{
authorized = true;
}
}
if !authorized {
if let Some(hash) = invoice_hash {
if method_accepted(&item.access, "lightning")
&& crate::content_invoice::is_paid_for(hash, id).await
{ {
authorized = true; authorized = true;
} }
} }
if !authorized {
if let Some(hash) = invoice_hash {
if method_accepted(&item.access, "lightning")
&& crate::content_invoice::is_paid_for(hash, id).await
{
authorized = true;
}
}
}
if !authorized {
return Ok(ServeResult::PaymentRequired(*price_sats));
}
} }
if !authorized { AccessControl::PeersOnly => {
return Ok(ServeResult::PaymentRequired(*price_sats)); if !is_known_peer {
return Ok(ServeResult::Forbidden);
}
} }
} AccessControl::Free => {}
AccessControl::PeersOnly => {
if !is_known_peer {
return Ok(ServeResult::Forbidden);
}
}
AccessControl::Free => {}
} }
} }
+10 -3
View File
@@ -1200,7 +1200,9 @@ mod tests {
mark_installed(tmp.path(), "bitcoin-knots").await; mark_installed(tmp.path(), "bitcoin-knots").await;
mark_installed(tmp.path(), "lnd").await; mark_installed(tmp.path(), "lnd").await;
assert!(load_installed_apps(tmp.path()).await.contains("bitcoin-knots")); assert!(load_installed_apps(tmp.path())
.await
.contains("bitcoin-knots"));
// Uninstall forgets it, or desired-state recovery would recreate the // Uninstall forgets it, or desired-state recovery would recreate the
// very app that was just removed. // very app that was just removed.
@@ -1224,7 +1226,9 @@ mod tests {
assert!(load_last_running_names(tmp.path()).await.is_empty()); assert!(load_last_running_names(tmp.path()).await.is_empty());
assert!( assert!(
load_installed_apps(tmp.path()).await.contains("bitcoin-knots"), load_installed_apps(tmp.path())
.await
.contains("bitcoin-knots"),
"installation record must outlive the running snapshot" "installation record must outlive the running snapshot"
); );
} }
@@ -1268,7 +1272,10 @@ mod tests {
backfill_installed_apps(tmp.path(), &["bitcoin-knots".to_string()]).await; backfill_installed_apps(tmp.path(), &["bitcoin-knots".to_string()]).await;
let installed = load_installed_apps(tmp.path()).await; let installed = load_installed_apps(tmp.path()).await;
assert!(installed.contains("lnd"), "a down app was dropped by backfill"); assert!(
installed.contains("lnd"),
"a down app was dropped by backfill"
);
assert!(installed.contains("bitcoin-knots")); assert!(installed.contains("bitcoin-knots"));
// An empty adoption list (podman unreachable, say) must change nothing. // An empty adoption list (podman unreachable, say) must change nothing.
+10
View File
@@ -51,6 +51,15 @@ pub struct ServerInfo {
pub lan_address: Option<String>, pub lan_address: Option<String>,
#[serde(rename = "tor-address")] #[serde(rename = "tor-address")]
pub tor_address: Option<String>, pub tor_address: Option<String>,
/// Is the Tor daemon actually answering — NOT "was an onion provisioned".
///
/// `tor_address` is read from the hidden-service hostname file on disk and
/// survives Tor being dead, so the dashboard's Network card reported
/// "Connected" on three fleet nodes whose Tor had been down for days
/// (2026-08-09). Liveness has to come from a probe; the address is a
/// separate fact and must not be used as a proxy for it.
#[serde(rename = "tor-running")]
pub tor_running: bool,
#[serde(rename = "node-address", skip_serializing_if = "Option::is_none")] #[serde(rename = "node-address", skip_serializing_if = "Option::is_none")]
pub node_address: Option<String>, pub node_address: Option<String>,
pub unread: u32, pub unread: u32,
@@ -354,6 +363,7 @@ impl DataModel {
}, },
lan_address: Some("http://localhost:8100".to_string()), lan_address: Some("http://localhost:8100".to_string()),
tor_address: None, tor_address: None,
tor_running: false,
node_address: None, node_address: None,
unread: 0, unread: 0,
wifi_ssids: vec![], wifi_ssids: vec![],
+2 -5
View File
@@ -336,11 +336,8 @@ async fn main() -> Result<()> {
// need it. Additive and evidence-based: only names with a real // need it. Additive and evidence-based: only names with a real
// adopted container are claimed, and anything the operator // adopted container are claimed, and anything the operator
// uninstalled is skipped, so it cannot invent an install. // uninstalled is skipped, so it cannot invent an install.
crate::crash_recovery::backfill_installed_apps( crate::crash_recovery::backfill_installed_apps(&config.data_dir, &report.adopted)
&config.data_dir, .await;
&report.adopted,
)
.await;
} }
Ok(Err(e)) => { Ok(Err(e)) => {
tracing::warn!(error = %e, "prod orchestrator: adopt_existing failed (non-fatal)"); tracing::warn!(error = %e, "prod orchestrator: adopt_existing failed (non-fatal)");
+4 -6
View File
@@ -873,10 +873,9 @@ mod tests {
/// A real Ed25519 keypair, its did:key, and a manifest signed by it. /// A real Ed25519 keypair, its did:key, and a manifest signed by it.
fn signed_manifest() -> (ed25519_dalek::SigningKey, String, AppManifest) { fn signed_manifest() -> (ed25519_dalek::SigningKey, String, AppManifest) {
let key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng); let key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
let did = crate::identity::did_key_from_pubkey_hex(&hex::encode( let did =
key.verifying_key().as_bytes(), crate::identity::did_key_from_pubkey_hex(&hex::encode(key.verifying_key().as_bytes()))
)) .unwrap();
.unwrap();
let mut manifest = sample_manifest(); let mut manifest = sample_manifest();
manifest.author.did = did.clone(); manifest.author.did = did.clone();
sign_manifest(&mut manifest, &key).unwrap(); sign_manifest(&mut manifest, &key).unwrap();
@@ -1054,8 +1053,7 @@ mod tests {
manifest.repo_url = String::new(); manifest.repo_url = String::new();
manifest.version = "1".into(); manifest.version = "1".into();
manifest.container.readonly_root = false; manifest.container.readonly_root = false;
let (score, _tier) = let (score, _tier) = calculate_trust_score(&manifest, 1, &[], &SignatureStatus::Missing);
calculate_trust_score(&manifest, 1, &[], &SignatureStatus::Missing);
assert!(score < 50, "Expected low score, got {score}"); assert!(score < 50, "Expected low score, got {score}");
} }
+13
View File
@@ -1206,6 +1206,19 @@ impl MeshService {
Ok(dest_prefix) Ok(dest_prefix)
} }
/// True if `contact_id` is reachable over the mesh radio right now — the
/// same peer/twin resolution `peer_dest_prefix` performs, exposed as a
/// cheap bool so RPC handlers can gate radio-only transports (LXMF
/// native image, Reticulum resource transfer) without duplicating the
/// twin-resolution logic. A federation-only contact_id with no matching
/// radio twin returns false here — offering "resource-mesh" or native
/// image to such a peer sends it straight into `peer_dest_prefix`'s
/// "federation-only (no radio twin)" error (picture-send from a
/// federation-only contact, 2026-08-07).
pub async fn has_radio_route(&self, contact_id: u32) -> bool {
self.peer_dest_prefix(contact_id).await.is_ok()
}
/// Split an oversized wire payload into MC-framed base64 chunks and send /// Split an oversized wire payload into MC-framed base64 chunks and send
/// each via the mesh device. Matches the receive-side reassembly in /// each via the mesh device. Matches the receive-side reassembly in
/// `mesh/listener/decode.rs::handle_chunked_frame` (header `MCIIXXTT`, /// `mesh/listener/decode.rs::handle_chunked_frame` (header `MCIIXXTT`,
+17 -12
View File
@@ -55,10 +55,7 @@ pub enum TagExtractionError {
/// `path` is canonicalized and confined to `media_roots` *before* the file /// `path` is canonicalized and confined to `media_roots` *before* the file
/// is opened. `media_roots` is a parameter, not a constant, so a caller /// is opened. `media_roots` is a parameter, not a constant, so a caller
/// cannot bypass the confinement by construction. /// cannot bypass the confinement by construction.
pub fn extract_tags( pub fn extract_tags(path: &Path, media_roots: &[PathBuf]) -> Result<RawTags, TagExtractionError> {
path: &Path,
media_roots: &[PathBuf],
) -> Result<RawTags, TagExtractionError> {
let canonical = path.canonicalize()?; let canonical = path.canonicalize()?;
let within_roots = media_roots.iter().any(|root| { let within_roots = media_roots.iter().any(|root| {
@@ -91,9 +88,7 @@ pub fn extract_tags(
.unwrap_or_else(|| fallback_from_filename(&canonical)); .unwrap_or_else(|| fallback_from_filename(&canonical));
let artist = tag.artist().map(|cow| cow.into_owned()); let artist = tag.artist().map(|cow| cow.into_owned());
let album = tag.album().map(|cow| cow.into_owned()); let album = tag.album().map(|cow| cow.into_owned());
let album_artist = tag let album_artist = tag.get_string(ItemKey::AlbumArtist).map(ToOwned::to_owned);
.get_string(ItemKey::AlbumArtist)
.map(ToOwned::to_owned);
let track = tag.track(); let track = tag.track();
let disc = tag.disk(); let disc = tag.disk();
let year = tag.date().map(|timestamp| u32::from(timestamp.year)); let year = tag.date().map(|timestamp| u32::from(timestamp.year));
@@ -208,8 +203,14 @@ mod tests {
/// FLAC STREAMINFO block content (34 bytes) — the only metadata block /// FLAC STREAMINFO block content (34 bytes) — the only metadata block
/// lofty's duration calculation reads (`flac/properties.rs`). /// lofty's duration calculation reads (`flac/properties.rs`).
fn flac_streaminfo(sample_rate: u32, channels: u32, bits_per_sample: u32, total_samples: u64) -> Vec<u8> { fn flac_streaminfo(
let mut info: u32 = (sample_rate << 12) | ((channels - 1) << 9) | ((bits_per_sample - 1) << 4); sample_rate: u32,
channels: u32,
bits_per_sample: u32,
total_samples: u64,
) -> Vec<u8> {
let mut info: u32 =
(sample_rate << 12) | ((channels - 1) << 9) | ((bits_per_sample - 1) << 4);
info |= ((total_samples >> 32) as u32) & 0xF; info |= ((total_samples >> 32) as u32) & 0xF;
let total_samples_low = (total_samples & 0xFFFF_FFFF) as u32; let total_samples_low = (total_samples & 0xFFFF_FFFF) as u32;
@@ -270,7 +271,7 @@ mod tests {
file.extend_from_slice(&flac_block(0, false, &streaminfo)); file.extend_from_slice(&flac_block(0, false, &streaminfo));
let vorbis_comments = vorbis_comment_block("test-vendor", comments); let vorbis_comments = vorbis_comment_block("test-vendor", comments);
file.extend_from_slice(&flac_block(4, true, &vorbis_comments)); file.extend_from_slice(&flac_block(4, true, &vorbis_comments));
}, }
} }
file file
} }
@@ -341,7 +342,8 @@ mod tests {
mdhd_content.extend_from_slice(&[0, 0, 0, 0]); // creation_time mdhd_content.extend_from_slice(&[0, 0, 0, 0]); // creation_time
mdhd_content.extend_from_slice(&[0, 0, 0, 0]); // modification_time mdhd_content.extend_from_slice(&[0, 0, 0, 0]); // modification_time
mdhd_content.extend_from_slice(&MP4_TIMESCALE.to_be_bytes()); mdhd_content.extend_from_slice(&MP4_TIMESCALE.to_be_bytes());
mdhd_content.extend_from_slice(&((MP4_TIMESCALE as u64 * MP4_DURATION_SECS) as u32).to_be_bytes()); mdhd_content
.extend_from_slice(&((MP4_TIMESCALE as u64 * MP4_DURATION_SECS) as u32).to_be_bytes());
let mdhd = atom(b"mdhd", &mdhd_content); let mdhd = atom(b"mdhd", &mdhd_content);
let mdia = atom(b"mdia", &[hdlr, mdhd].concat()); let mdia = atom(b"mdia", &[hdlr, mdhd].concat());
@@ -374,7 +376,10 @@ mod tests {
let mut segment_table = Vec::new(); let mut segment_table = Vec::new();
let mut content = Vec::new(); let mut content = Vec::new();
for packet in packets { for packet in packets {
assert!(packet.len() < 255, "fixture packet too large for a single OGG lacing segment"); assert!(
packet.len() < 255,
"fixture packet too large for a single OGG lacing segment"
);
segment_table.push(packet.len() as u8); segment_table.push(packet.len() as u8);
content.extend_from_slice(packet); content.extend_from_slice(packet);
} }
+2 -2
View File
@@ -55,8 +55,8 @@ pub fn pubkey_from_did(did: &str) -> Result<[u8; 32]> {
let id = did let id = did
.strip_prefix("did:dht:") .strip_prefix("did:dht:")
.ok_or_else(|| anyhow::anyhow!("Not a did:dht identifier: {}", did))?; .ok_or_else(|| anyhow::anyhow!("Not a did:dht identifier: {}", did))?;
let bytes = let bytes = zbase32::decode_full_bytes_str(id)
zbase32::decode_full_bytes_str(id).map_err(|e| anyhow::anyhow!("Invalid z-base-32: {e}"))?; .map_err(|e| anyhow::anyhow!("Invalid z-base-32: {e}"))?;
if bytes.len() != 32 { if bytes.len() != 32 {
anyhow::bail!("Expected 32-byte pubkey, got {} bytes", bytes.len()); anyhow::bail!("Expected 32-byte pubkey, got {} bytes", bytes.len());
} }
+6 -2
View File
@@ -174,7 +174,9 @@ mod tests {
#[test] #[test]
fn a_32_byte_key_is_52_chars_and_round_trips() { fn a_32_byte_key_is_52_chars_and_round_trips() {
for seed in 0u8..64 { for seed in 0u8..64 {
let key: Vec<u8> = (0u8..32).map(|i| i.wrapping_mul(7).wrapping_add(seed)).collect(); let key: Vec<u8> = (0u8..32)
.map(|i| i.wrapping_mul(7).wrapping_add(seed))
.collect();
let encoded = encode_full_bytes(&key); let encoded = encode_full_bytes(&key);
assert_eq!(encoded.len(), 52, "256 bits must encode to 52 characters"); assert_eq!(encoded.len(), 52, "256 bits must encode to 52 characters");
assert_eq!(decode_full_bytes_str(&encoded).unwrap(), key); assert_eq!(decode_full_bytes_str(&encoded).unwrap(), key);
@@ -184,7 +186,9 @@ mod tests {
#[test] #[test]
fn round_trips_every_length_up_to_a_block() { fn round_trips_every_length_up_to_a_block() {
for len in 0..40usize { for len in 0..40usize {
let data: Vec<u8> = (0..len).map(|i| (i as u8).wrapping_mul(31) ^ 0x5a).collect(); let data: Vec<u8> = (0..len)
.map(|i| (i as u8).wrapping_mul(31) ^ 0x5a)
.collect();
let encoded = encode_full_bytes(&data); let encoded = encode_full_bytes(&data);
// decode_full_bytes only recovers whole bytes, and encoding N bytes // decode_full_bytes only recovers whole bytes, and encoding N bytes
// produces ceil(8N/5) chars which always carry at least 8N bits. // produces ceil(8N/5) chars which always carry at least 8N bits.
+6 -1
View File
@@ -979,7 +979,12 @@ mod tests {
// BIP-32 m/84'/0'/0' // BIP-32 m/84'/0'/0'
assert_eq!( assert_eq!(
hex::encode(derive_bitcoin_xprv(&seed).unwrap().private_key.secret_bytes()), hex::encode(
derive_bitcoin_xprv(&seed)
.unwrap()
.private_key
.secret_bytes()
),
"57558e8c90c2e72f0c121d0fb8844bbbe7a872f0065d21b218a990450b9f93be", "57558e8c90c2e72f0c121d0fb8844bbbe7a872f0065d21b218a990450b9f93be",
"Bitcoin BIP-84 account key" "Bitcoin BIP-84 account key"
); );
+41 -2
View File
@@ -96,6 +96,11 @@ impl Server {
} }
} }
data.server_info.tor_address = docker_packages::read_tor_address("archipelago").await; data.server_info.tor_address = docker_packages::read_tor_address("archipelago").await;
// Liveness comes from a probe, never from the presence of an onion
// address: read_tor_address reads a hostname file that outlives the
// daemon, which is why the dashboard showed "Connected" on nodes whose
// Tor had been dead for days.
data.server_info.tor_running = crate::api::rpc::tor::check_tor_running().await;
if let Some(ref tor) = data.server_info.tor_address { if let Some(ref tor) = data.server_info.tor_address {
data.server_info.node_address = Some(identity.node_address(tor)); data.server_info.node_address = Some(identity.node_address(tor));
} }
@@ -1763,8 +1768,19 @@ async fn scan_and_update_packages(
.unwrap_or(false); .unwrap_or(false);
let update_changed = update_available != current_data.server_info.status_info.updated; let update_changed = update_available != current_data.server_info.status_info.updated;
// Empty scan result = podman failure or timeout, preserve existing state // The durable installed set is the truth the scan must never contradict:
if packages.is_empty() && !first_scan { // quadlet renders --rm, so every stop DELETES the container and a scan mid
// stop->start legitimately sees nothing where an installed app lives.
let installed_registry = crate::crash_recovery::load_installed_apps(data_dir).await;
let user_uninstalled = crate::crash_recovery::load_user_uninstalled(data_dir).await;
// Empty scan result = podman failure or timeout, preserve existing state.
// The first scan is NOT exempt when the durable registry says apps exist:
// the daemon restarts mid-churn (gate runs, OTAs), and publishing that
// first empty scan blanked the whole My Apps map — the dashboard showed a
// node with zero apps until the next scan (observed twice, 2026-08-09,
// once at load ~2). Better to keep saying "scanning…" than to say "empty".
if packages.is_empty() && (!first_scan || !installed_registry.is_empty()) {
if tor_changed || update_changed { if tor_changed || update_changed {
let mut data = current_data; let mut data = current_data;
data.server_info.tor_address = tor_addr.clone(); data.server_info.tor_address = tor_addr.clone();
@@ -1914,6 +1930,29 @@ async fn scan_and_update_packages(
let count = absence_tracker.entry(id.clone()).or_insert(0); let count = absence_tracker.entry(id.clone()).or_insert(0);
*count += 1; *count += 1;
if *count >= CONTAINER_ABSENCE_THRESHOLD { if *count >= CONTAINER_ABSENCE_THRESHOLD {
// An app the durable registry says is installed (and the user
// has not uninstalled) must NEVER be dropped from the map just
// because its container is momentarily gone — with --rm that
// is every restart's normal window. Dropping it here is what
// made dashboard tiles vanish mid-restart and gate waits read
// 'absent' (grafana, 2026-08-09, at load ~2). Hold it as
// Stopped instead; the next scan that sees the container
// restores the live state, and desired-state recovery still
// recreates genuinely lost containers.
if installed_registry.contains(&id) && !user_uninstalled.contains(&id) {
if let Some(entry) = merged.get(&id) {
if entry.state != crate::data_model::PackageState::Stopped {
let mut held = entry.clone();
held.state = crate::data_model::PackageState::Stopped;
held.health = None;
held.exit_code = None;
merged.insert(id.clone(), held);
changed = true;
}
}
absence_tracker.remove(&id);
continue;
}
debug!( debug!(
"Removing {} from state after {} consecutive absent scans", "Removing {} from state after {} consecutive absent scans",
id, count id, count
@@ -159,7 +159,9 @@ mod tests {
async fn a_corrupt_file_fails_closed() { async fn a_corrupt_file_fails_closed() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(FILE_PATH); let path = dir.path().join(FILE_PATH);
tokio::fs::create_dir_all(path.parent().unwrap()).await.unwrap(); tokio::fs::create_dir_all(path.parent().unwrap())
.await
.unwrap();
tokio::fs::write(&path, b"{ not json").await.unwrap(); tokio::fs::write(&path, b"{ not json").await.unwrap();
// The dangerous failure would be defaulting to "all granted". // The dangerous failure would be defaulting to "all granted".
assert!(load(dir.path()).await.granted.is_empty()); assert!(load(dir.path()).await.granted.is_empty());
+3
View File
@@ -0,0 +1,3 @@
commit=7b82dfc779f94e068ed4d7b2ada39d6fd8f77dce
built_at=2026-08-09T19:43:11Z
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
View File
@@ -0,0 +1 @@
.magazine[data-v-02741b8c]{font-family:Georgia,Times New Roman,Times,serif}.magazine-light[data-v-02741b8c]{background-color:#faf9f6}.magazine-dark[data-v-02741b8c]{background-color:#0a0a0a}iframe[data-v-0f5111f5]::-webkit-scrollbar{display:none}.code-detail table[data-v-7f40c0bc]{font-variant-numeric:tabular-nums}.animate-cell-pulse[data-v-ce9be807]{animation:cell-pulse-ce9be807 1.8s cubic-bezier(.4,0,.2,1) infinite}.animate-shimmer-sweep[data-v-ce9be807]{animation:shimmer-sweep-ce9be807 2.2s cubic-bezier(.4,0,.2,1) infinite}.animate-progress-sweep[data-v-ce9be807]{animation:progress-sweep-ce9be807 1.6s cubic-bezier(.4,0,.2,1) infinite}@keyframes cell-pulse-ce9be807{0%,to{opacity:.6;transform:scale(.96)}50%{opacity:1;transform:scale(1.02)}}@keyframes shimmer-sweep-ce9be807{0%{transform:translate(-100%)}60%{transform:translate(200%)}to{transform:translate(200%)}}@keyframes progress-sweep-ce9be807{0%{width:0;margin-left:0}45%{width:60%;margin-left:20%}90%{width:0;margin-left:100%}to{width:0;margin-left:0}}.content-fade-enter-active[data-v-30760816],.content-fade-leave-active[data-v-30760816]{transition:opacity .2s ease}.content-fade-enter-from[data-v-30760816],.content-fade-leave-to[data-v-30760816]{opacity:0}.detail-active[data-v-30760816]{border-color:transparent!important}.detail-persistent[data-v-30760816] button[class*=absolute][class*=top-3][class*=left-3]{display:none!important}
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.magazine[data-v-02741b8c]{font-family:Georgia,Times New Roman,Times,serif}.magazine-light[data-v-02741b8c]{background-color:#faf9f6}.magazine-dark[data-v-02741b8c]{background-color:#0a0a0a}iframe[data-v-0f5111f5]::-webkit-scrollbar{display:none}.code-detail table[data-v-7f40c0bc]{font-variant-numeric:tabular-nums}.animate-cell-pulse[data-v-f8eb31f3]{animation:cell-pulse-f8eb31f3 1.8s cubic-bezier(.4,0,.2,1) infinite}.animate-shimmer-sweep[data-v-f8eb31f3]{animation:shimmer-sweep-f8eb31f3 2.2s cubic-bezier(.4,0,.2,1) infinite}.animate-progress-sweep[data-v-f8eb31f3]{animation:progress-sweep-f8eb31f3 1.6s cubic-bezier(.4,0,.2,1) infinite}@keyframes cell-pulse-f8eb31f3{0%,to{opacity:.6;transform:scale(.96)}50%{opacity:1;transform:scale(1.02)}}@keyframes shimmer-sweep-f8eb31f3{0%{transform:translate(-100%)}60%{transform:translate(200%)}to{transform:translate(200%)}}@keyframes progress-sweep-f8eb31f3{0%{width:0;margin-left:0}45%{width:60%;margin-left:20%}90%{width:0;margin-left:100%}to{width:0;margin-left:0}}.content-fade-enter-active[data-v-e9c77de5],.content-fade-leave-active[data-v-e9c77de5]{transition:opacity .2s ease}.content-fade-enter-from[data-v-e9c77de5],.content-fade-leave-to[data-v-e9c77de5]{opacity:0}.detail-active[data-v-e9c77de5]{border-color:transparent!important}.detail-persistent[data-v-e9c77de5] button[class*=absolute][class*=top-3][class*=left-3]{display:none!important}
+1
View File
@@ -0,0 +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}
-1
View File
@@ -1 +0,0 @@
.picker-enter-active[data-v-02d91cf7]{transition:all .2s cubic-bezier(.22,1,.36,1)}.picker-leave-active[data-v-02d91cf7]{transition:all .15s ease-in}.picker-enter-from[data-v-02d91cf7],.picker-leave-to[data-v-02d91cf7]{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}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{a as V,K as j,c as r,e as s,L as y,M as g,t as c,F as p,N as h,i as b,g as B,O as D,r as u,k as L,P as T,b as l,n as k}from"./index-Lh5NfTCq.js";import{useNostr as U}from"./useNostr-DYbkCQxC.js";const F={class:"min-h-screen bg-[#0a0a0a] text-white"},P={class:"sticky top-0 z-10 glass border-b border-white/5"},R={class:"max-w-3xl mx-auto px-4 py-3 flex items-center gap-3"},z={class:"flex-1 min-w-0"},E={class:"text-sm font-semibold text-white/90 truncate"},H={class:"text-xs text-white/40"},G={key:0,class:"flex items-center justify-center h-64"},K={key:1,class:"max-w-3xl mx-auto px-4 py-12 text-center"},O={class:"text-white/40 text-sm"},Y={key:2,class:"max-w-3xl mx-auto px-4 py-6 space-y-4"},q={class:"flex items-center gap-2 mb-2"},J=["textContent"],Z=V({__name:"ConversationViewerPage",setup(Q){const C=D(),{connect:N,fetchNote:A}=U(),v=u(!0),i=u(null),f=u("Shared Conversation"),x=u(null),d=u(null),w=u([]),I=L(()=>d.value?new Date(d.value*1e3).toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"}):"");function M(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-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(`
`);let e="",a=[];for(const m of o){const _=m.match(/^##?\s*(?:Human|User|You)/),S=m.match(/^##?\s*(?:Assistant|AI|Claude)/);_||S?(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 j(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=M(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=T("router-link");return l(),r("div",F,[s("header",P,[s("div",R,[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",z,[s("h1",E,c(f.value),1),s("p",H,[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",G,[...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",K,[s("p",O,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",Y,[(l(!0),r(p,null,B(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",q,[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,J)],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-Cg4zvjy1.js";import"./index-Lh5NfTCq.js";export{m as default}; import{_ as m}from"./FilmDetail.vue_vue_type_script_setup_true_lang-BhKlPG3Y.js";import"./index-8cIrvc8q.js";export{m as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
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};
-1
View File
@@ -1 +0,0 @@
import{_ as o}from"./FilmGrid.vue_vue_type_script_setup_true_lang-CWkUdZ32.js";import"./index-Lh5NfTCq.js";import"./useContentImages-CagIZs4M.js";export{o as default};
@@ -1 +0,0 @@
import{a as F,b as a,c as r,e as s,n as d,u as l,t as c,f as L,w as S,v as j,F as v,g as m,h as U,i as x,j as E,r as y,k as w,l as z,m as B,p as D}from"./index-Lh5NfTCq.js";import{u as G}from"./useContentImages-CagIZs4M.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={class:"text-xs text-accent font-bold"},Z={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"},le=F({__name:"FilmGrid",props:{films:{},title:{default:"Recommended Films"}},emits:["selectFilm"],setup(f){const b=f,{isDark:n}=E(),h=y(""),u=y(null),{coverSrc:p,fallbackSrc:k,onError:C,isLoading:_}=G({items:D(b,"films"),id:t=>t.id,existingUrl:t=>t.posterUrl||t.backdropUrl,fetch:t=>B(t.title,t.year).then(o=>o.posterUrl),fallback:t=>z(t.title,t.year)}),$=w(()=>{const t=new Map;for(const o of b.films)for(const e of o.genres)t.set(e,(t.get(e)??0)+1);return[...t.entries()].sort((o,e)=>e[1]-o[1]).slice(0,8).map(([o])=>o)}),g=w(()=>{let t=b.films;if(h.value){const o=h.value.toLowerCase();t=t.filter(e=>e.title.toLowerCase().includes(o)||e.director.toLowerCase().includes(o)||e.cast.some(i=>i.toLowerCase().includes(o)))}return u.value&&(t=t.filter(o=>o.genres.includes(u.value))),t});return(t,o)=>(a(),r("div",N,[s("div",{class:"p-4 space-y-3",style:U(l(n)?"border-bottom: 1px solid rgba(255, 255, 255, 0.08)":"border-bottom: 1px solid rgba(0, 0, 0, 0.06)")},[s("div",V,[s("h3",{class:d(["text-sm font-bold",l(n)?"text-white/90":"text-gray-900"])},c(f.title),3),s("div",I,[s("span",{class:d(["text-xs font-mono",l(n)?"text-white/30":"text-gray-400"])},c(g.value.length)+" films ",3),L(t.$slots,"header-actions")])]),S(s("input",{"onUpdate:modelValue":o[0]||(o[0]=e=>h.value=e),type:"text",placeholder:"Search films...",class:d(["w-full px-3 py-2 rounded-lg text-base outline-none transition-colors",l(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,h.value]]),s("div",M,[(a(!0),r(v,null,m($.value,e=>(a(),r("button",{key:e,class:d(["text-xs px-2 py-1 rounded-md transition-all duration-150",u.value===e?"nav-tab-active":l(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=>u.value=u.value===e?null:e},c(e),11,R))),128))])],4),s("div",T,[s("div",q,[(a(!0),r(v,null,m(g.value,e=>(a(),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)},[s("div",A,[s("div",{class:d(["aspect-[2/3] relative w-full overflow-hidden rounded-[10px]",l(p)(e)?"":l(n)?"bg-white/[0.06]":"bg-black/[0.04]"])},[l(_)(e)?(a(),r("div",H)):x("",!0),l(p)(e)?(a(),r("img",{key:1,src:l(p)(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=>l(C)(e)},null,40,J)):l(_)(e)?x("",!0):(a(),r("img",{key:2,src:l(k)(e),alt:e.title,class:"w-full h-full object-cover"},null,8,K)),l(p)(e)?(a(),r("div",O)):x("",!0),s("div",Q,[s("p",W,c(e.title),1),s("div",X,[s("span",Y,"★ "+c(e.rating),1),s("span",Z,c(e.year),1)])]),s("div",ee,[(a(!0),r(v,null,m(e.sources.slice(0,2),i=>(a(),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?(a(),r("div",te,[s("p",{class:d(["text-sm",l(n)?"text-white/30":"text-gray-400"])}," No films match your search ",2)])):x("",!0)])]))}});export{le as _};
@@ -0,0 +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 _};
-1
View File
@@ -1 +0,0 @@
.guide-page[data-v-881f73b8]{min-height:100vh;background:#0a0a0a;color:#ffffffe6;font-family:Inter,-apple-system,BlinkMacSystemFont,sans-serif}.guide-header[data-v-881f73b8]{position:sticky;top:0;z-index:10;display:flex;align-items:center;gap:12px;padding:16px 20px;background:#0009;backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);border-bottom:1px solid rgba(255,255,255,.08)}.back-btn[data-v-881f73b8]{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:10px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;color:#ffffffb3;cursor:pointer;transition:all .2s ease}.back-btn[data-v-881f73b8]:hover{background:#ffffff1a;color:#fff}.guide-title[data-v-881f73b8]{font-size:18px;font-weight:600;flex:1}.guide-version[data-v-881f73b8]{font-size:12px;color:#fff6;padding:2px 8px;border-radius:6px;background:#ffffff0f}.guide-content[data-v-881f73b8]{max-width:680px;margin:0 auto;padding:24px 20px 80px}.guide-intro[data-v-881f73b8]{font-size:15px;line-height:1.7;color:#ffffffb3}.guide-section[data-v-881f73b8]{margin-bottom:36px}.section-title[data-v-881f73b8]{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:600;margin-bottom:12px;color:#fffffff2}.section-icon[data-v-881f73b8]{font-size:20px}.section-desc[data-v-881f73b8]{font-size:14px;line-height:1.7;color:#fff9;margin-bottom:16px}.section-desc code[data-v-881f73b8]{background:#ffffff14;padding:2px 6px;border-radius:4px;font-size:13px;font-family:Menlo,monospace;color:#fffc}.example-box[data-v-881f73b8]{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}.example-prompt[data-v-881f73b8]{padding:10px 14px;background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:10px;font-size:13px;color:#ffffffbf;font-style:italic}.info-note[data-v-881f73b8]{padding:12px 16px;background:#f7931a14;border:1px solid rgba(247,147,26,.2);border-radius:10px;font-size:13px;line-height:1.6;color:#fff9}.feature-grid[data-v-881f73b8]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.feature-card[data-v-881f73b8]{padding:16px;background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:12px}.feature-card h3[data-v-881f73b8]{font-size:14px;font-weight:600;margin-bottom:6px;color:#ffffffe6}.feature-card p[data-v-881f73b8]{font-size:13px;line-height:1.5;color:#ffffff80}.tips-list[data-v-881f73b8]{list-style:none;padding:0;display:flex;flex-direction:column;gap:10px}.tips-list li[data-v-881f73b8]{padding:10px 14px 10px 28px;position:relative;background:#ffffff08;border:1px solid rgba(255,255,255,.06);border-radius:10px;font-size:13px;line-height:1.5;color:#fff9}.tips-list li[data-v-881f73b8]:before{content:"•";position:absolute;left:14px;color:#f7931a}.demo-button[data-v-881f73b8]{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:14px 20px;border-radius:12px;border:1px solid rgba(247,147,26,.3);background:#f7931a1a;color:#f7931a;font-size:15px;font-weight:600;cursor:pointer;transition:all .2s ease}.demo-button[data-v-881f73b8]:hover:not(:disabled){background:#f7931a2e;border-color:#f7931a80;transform:translateY(-1px)}.demo-button[data-v-881f73b8]:disabled{opacity:.6;cursor:not-allowed}.demo-spinner[data-v-881f73b8]{width:18px;height:18px;border:2px solid rgba(247,147,26,.3);border-top-color:#f7931a;border-radius:50%;animation:spin-881f73b8 .8s linear infinite}@keyframes spin-881f73b8{to{transform:rotate(360deg)}}
+1
View File
@@ -0,0 +1 @@
.guide-page[data-v-f6611b72]{min-height:100vh;background:#0a0a0a;color:#ffffffe6;font-family:Inter,-apple-system,BlinkMacSystemFont,sans-serif}.guide-header[data-v-f6611b72]{position:sticky;top:0;z-index:10;display:flex;align-items:center;gap:12px;padding:16px 20px;background:#0009;backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);border-bottom:1px solid rgba(255,255,255,.08)}.back-btn[data-v-f6611b72]{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:10px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;color:#ffffffb3;cursor:pointer;transition:all .2s ease}.back-btn[data-v-f6611b72]:hover{background:#ffffff1a;color:#fff}.guide-title[data-v-f6611b72]{font-size:18px;font-weight:600;flex:1}.guide-version[data-v-f6611b72]{font-size:12px;color:#fff6;padding:2px 8px;border-radius:6px;background:#ffffff0f}.guide-content[data-v-f6611b72]{max-width:680px;margin:0 auto;padding:24px 20px 80px}.guide-intro[data-v-f6611b72]{font-size:15px;line-height:1.7;color:#ffffffb3}.guide-section[data-v-f6611b72]{margin-bottom:36px}.section-title[data-v-f6611b72]{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:600;margin-bottom:12px;color:#fffffff2}.section-icon[data-v-f6611b72]{font-size:20px}.section-desc[data-v-f6611b72]{font-size:14px;line-height:1.7;color:#fff9;margin-bottom:16px}.section-desc code[data-v-f6611b72]{background:#ffffff14;padding:2px 6px;border-radius:4px;font-size:13px;font-family:Menlo,monospace;color:#fffc}.example-box[data-v-f6611b72]{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}.example-prompt[data-v-f6611b72]{padding:10px 14px;background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:10px;font-size:13px;color:#ffffffbf;font-style:italic}.info-note[data-v-f6611b72]{padding:12px 16px;background:#f7931a14;border:1px solid rgba(247,147,26,.2);border-radius:10px;font-size:13px;line-height:1.6;color:#fff9}.feature-grid[data-v-f6611b72]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.feature-card[data-v-f6611b72]{padding:16px;background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:12px}.feature-card h3[data-v-f6611b72]{font-size:14px;font-weight:600;margin-bottom:6px;color:#ffffffe6}.feature-card p[data-v-f6611b72]{font-size:13px;line-height:1.5;color:#ffffff80}.tips-list[data-v-f6611b72]{list-style:none;padding:0;display:flex;flex-direction:column;gap:10px}.tips-list li[data-v-f6611b72]{padding:10px 14px 10px 28px;position:relative;background:#ffffff08;border:1px solid rgba(255,255,255,.06);border-radius:10px;font-size:13px;line-height:1.5;color:#fff9}.tips-list li[data-v-f6611b72]:before{content:"•";position:absolute;left:14px;color:#f7931a}.demo-button[data-v-f6611b72]{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:14px 20px;border-radius:12px;border:1px solid rgba(247,147,26,.3);background:#f7931a1a;color:#f7931a;font-size:15px;font-weight:600;cursor:pointer;transition:all .2s ease}.demo-button[data-v-f6611b72]:hover:not(:disabled){background:#f7931a2e;border-color:#f7931a80;transform:translateY(-1px)}.demo-button[data-v-f6611b72]:disabled{opacity:.6;cursor:not-allowed}.demo-spinner[data-v-f6611b72]{width:18px;height:18px;border:2px solid rgba(247,147,26,.3);border-top-color:#f7931a;border-radius:50%;animation:spin-f6611b72 .8s linear infinite}@keyframes spin-f6611b72{to{transform:rotate(360deg)}}
File diff suppressed because one or more lines are too long
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-CvC0ROCb.js";import"./index-Lh5NfTCq.js";export{m as default}; import{_ as m}from"./SongDetail.vue_vue_type_script_setup_true_lang-B3om3Z8v.js";import"./index-8cIrvc8q.js";export{m as default};
-1
View File
@@ -1 +0,0 @@
import{_ as o}from"./SongGrid.vue_vue_type_script_setup_true_lang-CW1T9zpX.js";import"./index-Lh5NfTCq.js";import"./useContentImages-CagIZs4M.js";export{o as default};
+1
View File
@@ -0,0 +1 @@
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 +0,0 @@
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 f,z as U,j as V,r as _,k}from"./index-Lh5NfTCq.js";import{u as P}from"./useContentImages-CagIZs4M.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={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(),h=_(""),d=_(null),{coverSrc:p,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(h.value){const l=h.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=>h.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,h.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(p)(t)?"":a(i)?"bg-white/[0.06]":"bg-black/[0.04]"])},[a(w)(t)?(o(),r("div",W)):f("",!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(p)(t)?(o(),r("img",{key:1,src:a(p)(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)?f("",!0):(o(),r("img",{key:2,src:a(j)(t),alt:t.title,class:"w-full h-full object-cover"},null,8,Z)),a(p)(t)?(o(),r("div",tt)):f("",!0),s("div",et,[s("p",st,u(t.title),1),s("p",lt,u(t.artist),1)]),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)])):f("",!0)])]))}});export{nt as _};
@@ -0,0 +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 _};
@@ -1 +1 @@
import{a as h,P as m,b as d,c as r,e as t,t as s,F as u,g as p,Q as x,h as f}from"./index-Lh5NfTCq.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-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};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{e as x,c as b,g as m,k as P,h as p,j as w,l as N,m as c,n as I,t as A,o as M}from"./_baseUniq-C5dU7AKy.js";import{aM as g,ar as E,aN as F,aO as T,aP as _,aQ as l,aR as $,aS as B,aT as S,aU as y}from"./mermaid.core-DaNhpuX9.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,q=/^[-+]0x[0-9a-f]+$/i,z=/^0b[01]+$/i,C=/^0o[0-7]+$/i,K=parseInt;function Q(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=z.test(n);return t||C.test(n)?K(n.slice(2),t?2:8):q.test(n)?o:+n}var v=1/0,U=17976931348623157e292;function W(n){if(!n)return n===0?n:0;if(n=Q(n),n===v||n===-v){var r=n<0?-1:1;return r*U}return n===n?n:0}function X(n){var r=W(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?b(n):[]}var O=Object.prototype,Y=O.hasOwnProperty,dn=E(function(n,r){n=Object(n);var t=-1,i=r.length,a=i>2?r[2]:void 0;for(a&&F(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,O[u])&&!Y.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 Z(n,r,t){var i=n==null?0:n.length;if(!i)return-1;var a=t==null?0:X(t);return a<0&&(a=J(i+a,0)),p(n,m(r),a)}var hn=D(Z);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)?N: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&&c(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=A(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=M(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,X as j,un as l,gn as m,W as t};
+1
View File
@@ -0,0 +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};
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-DaNhpuX9.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-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};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{U as a,M as n}from"./mermaid.core-DaNhpuX9.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
+1
View File
@@ -0,0 +1 @@
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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_ as i}from"./mermaid.core-DaNhpuX9.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-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};
@@ -1 +1 @@
import{_ as a,d as o}from"./mermaid.core-DaNhpuX9.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-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};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{_ as e}from"./mermaid.core-DaNhpuX9.js";var l=e(()=>` import{_ as e}from"./mermaid.core-v0oo9NRr.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 @@
import{_ as a,e as w,l as x}from"./mermaid.core-DaNhpuX9.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-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};
@@ -1 +1 @@
import{_ as s}from"./mermaid.core-DaNhpuX9.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-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};
@@ -1 +1 @@
import{_ as n,L as o,j as l}from"./mermaid.core-DaNhpuX9.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-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};
@@ -0,0 +1 @@
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};
@@ -1 +0,0 @@
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-1SR22WeC.js";import{_ as i}from"./mermaid.core-DaNhpuX9.js";import"./chunk-FMBD7UC4-DGED6SBi.js";import"./chunk-55IACEB6-CWcaiZ1g.js";import"./chunk-QN33PNHL-C8Gh8Kbh.js";import"./index-Lh5NfTCq.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-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};
@@ -1 +0,0 @@
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-1SR22WeC.js";import{_ as i}from"./mermaid.core-DaNhpuX9.js";import"./chunk-FMBD7UC4-DGED6SBi.js";import"./chunk-55IACEB6-CWcaiZ1g.js";import"./chunk-QN33PNHL-C8Gh8Kbh.js";import"./index-Lh5NfTCq.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,2 +1,2 @@
const x="/aiui/",y=`${x}api/claude/v1/messages`;async function*h(r,o,g){const s={model:o.model,messages:r.filter(e=>e.role!=="system").map(e=>({role:e.role,content:typeof e.content=="string"?e.content:e.content.map(c=>c.text??"").join("")})),stream:o.stream??!0},n=r.find(e=>e.role==="system");n&&(s.system=typeof n.content=="string"?n.content:n.content.map(e=>e.text??"").join("")),s.max_tokens=o.maxTokens??4096,o.temperature!==void 0&&(s.temperature=o.temperature);const a=await fetch(y,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!a.ok){const e=await a.text().catch(()=>"Could not read error body");yield{type:"error",error:`Claude proxy error ${a.status}: ${e}`};return}const i=a.body?.getReader();if(!i){yield{type:"error",error:"No response body"};return}const m=new TextDecoder;let d="";try{for(;;){const{done:e,value:c}=await i.read();if(e)break;d+=m.decode(c,{stream:!0});const p=d.split(` const x="/aiui/",y=`${x}api/claude/v1/messages`;async function*h(r,o,g){const s={model:o.model,messages:r.filter(e=>e.role!=="system").map(e=>({role:e.role,content:typeof e.content=="string"?e.content:e.content.map(c=>c.text??"").join("")})),stream:o.stream??!0},n=r.find(e=>e.role==="system");n&&(s.system=typeof n.content=="string"?n.content:n.content.map(e=>e.text??"").join("")),o.maxTokens&&(s.max_tokens=o.maxTokens),o.temperature!==void 0&&(s.temperature=o.temperature);const a=await fetch(y,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!a.ok){const e=await a.text().catch(()=>"Could not read error body");yield{type:"error",error:`Claude proxy error ${a.status}: ${e}`};return}const i=a.body?.getReader();if(!i){yield{type:"error",error:"No response body"};return}const m=new TextDecoder;let d="";try{for(;;){const{done:e,value:c}=await i.read();if(e)break;d+=m.decode(c,{stream:!0});const p=d.split(`
`);d=p.pop()??"";for(const f of p){const u=f.trim();if(!u||!u.startsWith("data: "))continue;const l=u.slice(6);if(l==="[DONE]"){yield{type:"done"};return}try{const t=JSON.parse(l);t.type==="content_block_delta"&&t.delta?.text?yield{type:"text",text:t.delta.text}:t.type==="message_stop"?yield{type:"done",usage:t.usage?{promptTokens:t.usage.input_tokens??0,completionTokens:t.usage.output_tokens??0}:void 0}:t.type==="error"&&(yield{type:"error",error:t.error?.message??"Claude stream error"})}catch{}}}}finally{i.cancel().catch(()=>{})}yield{type:"done"}}const v={id:"claude",name:"Claude (Anthropic)",version:"1.0.0",type:"ai-provider",description:"Anthropic Claude AI via proxy server",supportsStreaming:!0,supportsVision:!0,supportsTools:!0,async init(r){},async destroy(){},async isAvailable(){try{const r=await fetch(y,{method:"OPTIONS"});return r.ok||r.status===405}catch{return!1}},chat(r,o){return h(r,o)},async models(){return[{id:"claude-haiku-4-5-20251001",name:"Claude 4.5 Haiku",provider:"claude",supportsVision:!0,supportsTools:!0,contextWindow:2e5},{id:"claude-sonnet-4",name:"Claude Sonnet 4",provider:"claude",supportsVision:!0,supportsTools:!0,contextWindow:2e5},{id:"claude-opus-4",name:"Claude Opus 4",provider:"claude",supportsVision:!0,supportsTools:!0,contextWindow:2e5}]}};export{v as claudeProvider}; `);d=p.pop()??"";for(const f of p){const u=f.trim();if(!u||!u.startsWith("data: "))continue;const l=u.slice(6);if(l==="[DONE]"){yield{type:"done"};return}try{const t=JSON.parse(l);t.type==="content_block_delta"&&t.delta?.text?yield{type:"text",text:t.delta.text}:t.type==="message_stop"?yield{type:"done",usage:t.usage?{promptTokens:t.usage.input_tokens??0,completionTokens:t.usage.output_tokens??0}:void 0}:t.type==="error"&&(yield{type:"error",error:t.error?.message??"Claude stream error"})}catch{}}}}finally{i.cancel().catch(()=>{})}yield{type:"done"}}const k={id:"claude",name:"Claude (Anthropic)",version:"1.0.0",type:"ai-provider",description:"Anthropic Claude AI via proxy server",supportsStreaming:!0,supportsVision:!0,supportsTools:!0,async init(r){},async destroy(){},async isAvailable(){try{const r=await fetch(y,{method:"OPTIONS"});return r.ok||r.status===405}catch{return!1}},chat(r,o){return h(r,o)},async models(){return[{id:"claude-haiku-4.5",name:"Claude 4.5 Haiku",provider:"claude",supportsVision:!0,supportsTools:!0,contextWindow:2e5},{id:"claude-sonnet-4",name:"Claude Sonnet 4",provider:"claude",supportsVision:!0,supportsTools:!0,contextWindow:2e5},{id:"claude-opus-4",name:"Claude Opus 4",provider:"claude",supportsVision:!0,supportsTools:!0,contextWindow:2e5}]}};export{k as claudeProvider};
+1
View File
@@ -0,0 +1 @@
import{b as r}from"./_baseUniq-DAOs4kUj.js";var e=4;function a(o){return r(o,e)}export{a as c};
-1
View File
@@ -1 +0,0 @@
import{b as r}from"./_baseUniq-C5dU7AKy.js";var e=4;function a(o){return r(o,e)}export{a as c};
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,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-DaNhpuX9.js";import{p as V}from"./chunk-4BX2VUAB-WOh8BXBb.js";import{p as W}from"./treemap-GDKQZRPO-yRLasM0b.js";import"./index-Lh5NfTCq.js";import"./_baseUniq-C5dU7AKy.js";import"./_basePickBy-BlfxZvco.js";import"./clone-CJT8Sng7.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-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+=`
.radarCurve-${r} { .radarCurve-${r} {
color: ${s}; color: ${s};
fill: ${s}; fill: ${s};

Some files were not shown because too many files have changed in this diff Show More