diff --git a/apps/home-assistant/Dockerfile b/apps/home-assistant/Dockerfile index 2c5d3ec7..877cbfac 100644 --- a/apps/home-assistant/Dockerfile +++ b/apps/home-assistant/Dockerfile @@ -1,5 +1,5 @@ # Home Assistant - uses official image -FROM homeassistant/home-assistant:2024.1 +FROM homeassistant/home-assistant:2026.7.3 # Default configuration is in the image # No additional setup needed diff --git a/apps/home-assistant/manifest.yml b/apps/home-assistant/manifest.yml index f939e48f..135db17b 100644 --- a/apps/home-assistant/manifest.yml +++ b/apps/home-assistant/manifest.yml @@ -1,11 +1,11 @@ app: id: homeassistant name: Home Assistant - version: 2024.1.0 + version: 2026.7.3 description: Open source home automation platform. Control and monitor your smart home devices. container: - image: 146.59.87.168:3000/lfg2025/home-assistant:2024.1 + image: 146.59.87.168:3000/lfg2025/home-assistant:2026.7.3 pull_policy: if-not-present network: pasta diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index bb860f2c..984e0132 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -1563,6 +1563,15 @@ autopilot.active=false\n", /// Run post-install hooks (Nextcloud trusted domains, Bitcoin UI container). /// Critical hooks (credential setup, config) are awaited; UI container builds are background. async fn run_post_install_hooks(&self, package_id: &str) { + if matches!(package_id, "homeassistant" | "home-assistant") + && super::pine_ha::pine_engines_installed().await + { + // Pine was installed first: wire HA to its Wyoming engines now, + // then restart so the seeded storage is what HA loads. + if super::pine_ha::seed_home_assistant_pine_defaults().await { + super::pine_ha::restart_home_assistant_if_running().await; + } + } if package_id == "filebrowser" { // Generate a random password (32 bytes, hex-encoded) let mut buf = [0u8; 32]; diff --git a/core/archipelago/src/api/rpc/package/mod.rs b/core/archipelago/src/api/rpc/package/mod.rs index 4c165026..cda27ea9 100644 --- a/core/archipelago/src/api/rpc/package/mod.rs +++ b/core/archipelago/src/api/rpc/package/mod.rs @@ -3,6 +3,7 @@ mod config; mod dependencies; mod install; mod lifecycle; +mod pine_ha; mod progress; mod runtime; mod set_config; diff --git a/core/archipelago/src/api/rpc/package/pine_ha.rs b/core/archipelago/src/api/rpc/package/pine_ha.rs new file mode 100644 index 00000000..3ee6ffe5 --- /dev/null +++ b/core/archipelago/src/api/rpc/package/pine_ha.rs @@ -0,0 +1,370 @@ +//! Home Assistant <-> Pine voice-stack provisioning. +//! +//! The Pine app ships the two Wyoming engines (pine-whisper :10300, +//! pine-piper :10200) as plain containers. Home Assistant only uses them once +//! a `wyoming` config entry exists for each, and once an Assist pipeline +//! points at the resulting `stt.faster_whisper` / `tts.piper` entities. Out of +//! the box that meant clicking through HA's integration UI twice and building +//! a pipeline by hand — so seed all of it directly into HA's `.storage` when +//! both apps are present. +//! +//! Everything here is best-effort (warn, never fail an install): HA migrates +//! the minimal store shapes we write to its current schema on boot, and skips +//! entries that already exist, so re-running is idempotent. + +use serde_json::{json, Value}; +use tracing::{info, warn}; + +const HA_STORAGE_DIR: &str = "/var/lib/archipelago/home-assistant/.storage"; + +/// The two Wyoming engines the Pine stack publishes on the host. +/// HA runs under pasta, so the host is reachable as host.containers.internal. +const PINE_ENGINES: [(&str, &str, u16); 2] = [ + ("faster-whisper", "host.containers.internal", 10300), + ("piper", "host.containers.internal", 10200), +]; + +/// Seed Home Assistant with the Pine voice defaults: a `wyoming` config entry +/// per engine and an Assist pipeline wired to them. Called after the Pine +/// stack installs and after Home Assistant installs (each side no-ops when +/// the other is missing). HA picks the new entries up on its next start; the +/// caller restarts the container when it is already running. +/// +/// Returns true when anything was written (caller should restart HA). +pub(super) async fn seed_home_assistant_pine_defaults() -> bool { + let storage = std::path::Path::new(HA_STORAGE_DIR); + if let Err(e) = tokio::fs::create_dir_all(storage).await { + warn!("pine/HA seed: cannot create {}: {}", HA_STORAGE_DIR, e); + return false; + } + + let entries_changed = seed_wyoming_config_entries(storage).await; + let pipeline_changed = seed_assist_pipeline(storage).await; + let intents_changed = seed_voice_intents().await; + entries_changed || pipeline_changed || intents_changed +} + +/// Marker guarding the seeded block in configuration.yaml (idempotence). +const VOICE_MARKER: &str = "# --- archipelago pine voice (seeded) ---"; + +/// Seed "ask Archy" node-info voice intents: custom sentences + intent_script +/// + a REST sensor for the block height (via the mempool backend when that +/// app is installed). Skips cleanly when the user already defines +/// intent_script themselves. +async fn seed_voice_intents() -> bool { + let config_dir = std::path::Path::new("/var/lib/archipelago/home-assistant"); + let config_yaml = config_dir.join("configuration.yaml"); + + let existing = tokio::fs::read_to_string(&config_yaml) + .await + .unwrap_or_default(); + if existing.contains(VOICE_MARKER) { + return false; + } + if existing.contains("intent_script:") || existing.contains("\nrest:") { + warn!("pine/HA seed: configuration.yaml already defines intent_script/rest — skipping voice intents"); + return false; + } + + // Sentences file (its own dir, no key-collision risk). + let sentences_dir = config_dir.join("custom_sentences/en"); + if let Err(e) = tokio::fs::create_dir_all(&sentences_dir).await { + warn!( + "pine/HA seed: cannot create {}: {}", + sentences_dir.display(), + e + ); + return false; + } + let sentences = r#"language: "en" +intents: + ArchyBlockHeight: + data: + - sentences: + - "what's the [current] block height" + - "what is the [current] block height" + - "[current] block height" + - "how many blocks [are there]" +"#; + if let Err(e) = tokio::fs::write(sentences_dir.join("archy.yaml"), sentences).await { + warn!("pine/HA seed: writing custom sentences failed: {e}"); + return false; + } + + let mempool_installed = tokio::fs::metadata("/var/lib/archipelago/mempool") + .await + .is_ok() + || tokio::fs::metadata("/var/lib/archipelago/data/mempool") + .await + .is_ok(); + let mut block = format!("\n{VOICE_MARKER}\n"); + if mempool_installed { + block.push_str( + r#"rest: + - resource: http://host.containers.internal:8999/api/blocks/tip/height + scan_interval: 60 + sensor: + - name: "Archy Block Height" + unique_id: archy_block_height + value_template: "{{ value }}" +"#, + ); + } + block.push_str( + r#"intent_script: + ArchyBlockHeight: + speech: + text: >- + {% if states('sensor.archy_block_height') not in ['unknown', 'unavailable'] %} + The block height is {{ states('sensor.archy_block_height') }}. + {% else %} + I can't read the block height right now. + {% endif %} +"#, + ); + + let mut merged = existing; + merged.push_str(&block); + let tmp = config_yaml.with_extension("yaml.tmp-seed"); + if let Err(e) = tokio::fs::write(&tmp, &merged).await { + warn!("pine/HA seed: writing configuration.yaml failed: {e}"); + return false; + } + if let Err(e) = tokio::fs::rename(&tmp, &config_yaml).await { + warn!("pine/HA seed: replacing configuration.yaml failed: {e}"); + return false; + } + info!( + "pine/HA seed: voice intents seeded (block height{})", + if mempool_installed { + " + mempool REST sensor" + } else { + ", sensor pending mempool install" + } + ); + true +} + +/// Ensure `core.config_entries` has a `wyoming` entry per Pine engine. +async fn seed_wyoming_config_entries(storage: &std::path::Path) -> bool { + let path = storage.join("core.config_entries"); + let mut store = if tokio::fs::metadata(&path).await.is_ok() { + match read_store(&path).await { + Some(v) => v, + // Present but unreadable/corrupt: never overwrite it with a + // fresh store — that would throw away every integration. + None => return false, + } + } else { + json!({ + "version": 1, + "minor_version": 1, + "key": "core.config_entries", + "data": { "entries": [] } + }) + }; + + let Some(entries) = store + .get_mut("data") + .and_then(|d| d.get_mut("entries")) + .and_then(|e| e.as_array_mut()) + else { + warn!("pine/HA seed: core.config_entries has unexpected shape, leaving untouched"); + return false; + }; + + let mut added = false; + for (title, host, port) in PINE_ENGINES { + let exists = entries.iter().any(|e| { + e.get("domain").and_then(Value::as_str) == Some("wyoming") + && e.get("data") + .and_then(|d| d.get("host")) + .and_then(Value::as_str) + == Some(host) + && e.get("data") + .and_then(|d| d.get("port")) + .and_then(Value::as_u64) + == Some(port as u64) + }); + if exists { + continue; + } + let entry_id: [u8; 16] = rand::random(); + entries.push(json!({ + "entry_id": hex::encode(entry_id), + "version": 1, + "minor_version": 1, + "domain": "wyoming", + "title": title, + "data": { "host": host, "port": port }, + "options": {}, + "pref_disable_new_entities": false, + "pref_disable_polling": false, + "source": "user", + "unique_id": null, + "disabled_by": null + })); + info!("pine/HA seed: added wyoming config entry {title} ({host}:{port})"); + added = true; + } + + if added && !write_store(&path, &store).await { + return false; + } + added +} + +/// Ensure an Assist pipeline uses the Pine engines. Creates the store when +/// missing; when it exists, only repairs the pre-2024.6 conversation-agent id +/// (`homeassistant` -> `conversation.home_assistant`), which modern HA no +/// longer resolves (pipelines error with `intent-not-supported` otherwise). +async fn seed_assist_pipeline(storage: &std::path::Path) -> bool { + let path = storage.join("assist_pipeline.pipelines"); + + if let Some(mut store) = read_store(&path).await { + let Some(items) = store + .get_mut("data") + .and_then(|d| d.get_mut("items")) + .and_then(|i| i.as_array_mut()) + else { + return false; + }; + let mut repaired = false; + for item in items.iter_mut() { + if item.get("conversation_engine").and_then(Value::as_str) == Some("homeassistant") { + item["conversation_engine"] = json!("conversation.home_assistant"); + repaired = true; + } + } + if repaired { + info!("pine/HA seed: repaired legacy conversation agent id in Assist pipelines"); + return write_store(&path, &store).await; + } + return false; + } + + // ULID-shaped id (26 chars, Crockford base32) — HA only needs uniqueness. + let id: String = { + const ALPHABET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz"; + let raw: [u8; 26] = rand::random(); + raw.iter() + .map(|b| ALPHABET[(*b % 32) as usize] as char) + .collect() + }; + let store = json!({ + "version": 1, + "minor_version": 1, + "key": "assist_pipeline.pipelines", + "data": { + "items": [{ + "conversation_engine": "conversation.home_assistant", + "conversation_language": "en", + "id": id, + "language": "en", + "name": "Pine (local)", + "stt_engine": "stt.faster_whisper", + "stt_language": "en", + "tts_engine": "tts.piper", + "tts_language": "en_GB", + "tts_voice": null, + "wake_word_entity": null, + "wake_word_id": null + }], + "preferred_item": id + } + }); + if write_store(&path, &store).await { + info!("pine/HA seed: created default Assist pipeline (whisper + piper)"); + return true; + } + false +} + +/// True when the Pine Wyoming engines are installed on this node (their shared +/// data dir is created by the stack install). +pub(super) async fn pine_engines_installed() -> bool { + tokio::fs::metadata("/var/lib/archipelago/pine") + .await + .is_ok() +} + +/// True when Home Assistant has a config dir on this node (installed at some +/// point; .storage may not exist until first boot, which seeding handles). +pub(super) async fn home_assistant_installed() -> bool { + tokio::fs::metadata("/var/lib/archipelago/home-assistant") + .await + .is_ok() +} + +/// Restart the HA container so it loads the seeded storage. Best-effort: when +/// the container doesn't exist (not yet created) the next start picks the +/// seeds up anyway. +pub(super) async fn restart_home_assistant_if_running() { + let running = tokio::process::Command::new("podman") + .args([ + "ps", + "--format", + "{{.Names}}", + "--filter", + "name=homeassistant", + ]) + .output() + .await + .map(|o| { + String::from_utf8_lossy(&o.stdout) + .lines() + .any(|l| l.trim() == "homeassistant") + }) + .unwrap_or(false); + if !running { + return; + } + match tokio::process::Command::new("podman") + .args(["restart", "homeassistant"]) + .output() + .await + { + Ok(o) if o.status.success() => info!("pine/HA seed: restarted homeassistant"), + Ok(o) => warn!( + "pine/HA seed: podman restart homeassistant failed: {}", + String::from_utf8_lossy(&o.stderr) + ), + Err(e) => warn!("pine/HA seed: podman restart homeassistant failed: {e}"), + } +} + +async fn read_store(path: &std::path::Path) -> Option { + let raw = tokio::fs::read_to_string(path).await.ok()?; + match serde_json::from_str(&raw) { + Ok(v) => Some(v), + Err(e) => { + warn!( + "pine/HA seed: {} is not valid JSON ({}), leaving untouched", + path.display(), + e + ); + None + } + } +} + +async fn write_store(path: &std::path::Path, store: &Value) -> bool { + let pretty = match serde_json::to_string_pretty(store) { + Ok(s) => s, + Err(e) => { + warn!("pine/HA seed: serialize {} failed: {}", path.display(), e); + return false; + } + }; + // Write via temp + rename so HA never reads a half-written store. + let tmp = path.with_extension("tmp-seed"); + if let Err(e) = tokio::fs::write(&tmp, pretty).await { + warn!("pine/HA seed: write {} failed: {}", tmp.display(), e); + return false; + } + if let Err(e) = tokio::fs::rename(&tmp, path).await { + warn!("pine/HA seed: rename into {} failed: {}", path.display(), e); + return false; + } + true +} diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index f3ef4e8a..034b96ca 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -1934,12 +1934,14 @@ impl RpcHandler { if let Some(orchestrated) = install_stack_via_orchestrator(self, "pine", pine_stack_app_ids()).await? { + Self::seed_pine_ha_defaults().await; return Ok(orchestrated); } if let Some(adopted) = adopt_stack_if_exists("pine", "pine", &["pine-whisper", "pine-piper", "pine"]).await? { + Self::seed_pine_ha_defaults().await; return Ok(adopted); } @@ -1947,6 +1949,17 @@ impl RpcHandler { "pine manifests not available on this node — the signed catalog must provide apps/pine-*/manifest.yml" ) } + + /// After a Pine install, wire Home Assistant to the new engines when HA is + /// on this node (the HA post-install hook covers the reverse order). + async fn seed_pine_ha_defaults() { + if !super::pine_ha::home_assistant_installed().await { + return; + } + if super::pine_ha::seed_home_assistant_pine_defaults().await { + super::pine_ha::restart_home_assistant_if_running().await; + } + } } #[cfg(test)] diff --git a/neode-ui/src/views/AppSession.vue b/neode-ui/src/views/AppSession.vue index e1ee9078..e96d6709 100644 --- a/neode-ui/src/views/AppSession.vue +++ b/neode-ui/src/views/AppSession.vue @@ -546,35 +546,14 @@ onBeforeUnmount(() => { opacity: 0.5; } -/* Mode dropdown */ -.mode-option { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - padding: 10px 14px; - font-size: 13px; - color: rgba(255, 255, 255, 0.7); - transition: all 0.15s ease; - text-align: left; -} -.mode-option:hover { - background: rgba(255, 255, 255, 0.08); - color: white; -} -.mode-option-active { +/* Active display-mode button in the header bar */ +.app-session-btn-active { color: #fb923c; - background: rgba(251, 146, 60, 0.08); + background: rgba(251, 146, 60, 0.12); } - -.menu-fade-enter-active, -.menu-fade-leave-active { - transition: opacity 0.15s ease, transform 0.15s ease; -} -.menu-fade-enter-from, -.menu-fade-leave-to { - opacity: 0; - transform: translateY(-4px); +.app-session-btn-active:hover { + color: #fb923c; + background: rgba(251, 146, 60, 0.18); } .content-fade-enter-active, diff --git a/neode-ui/src/views/appSession/AppSessionHeader.vue b/neode-ui/src/views/appSession/AppSessionHeader.vue index 478af545..e050f1d3 100644 --- a/neode-ui/src/views/appSession/AppSessionHeader.vue +++ b/neode-ui/src/views/appSession/AppSessionHeader.vue @@ -22,63 +22,41 @@ - -
+ +
+ + - - - -
- - - -
-
\n
Ready. Click the button, then pick “PineVoice” in the Bluetooth popup.
\n
\n\n

After WiFi joins, add the speaker to Home Assistant:\n Settings → Devices & services → Add Wyoming Protocol, host =\n the speaker’s IP, port 10700, then pick an Assist pipeline using\n Whisper + Piper. Wake word: “Hey Jarvis.”

\n \n\n \n\n\n", - "overwrite": true, - "path": "/var/lib/archipelago/pine/index.html" - } - ], - "health_check": { - "endpoint": "localhost:443", - "interval": "30s", - "retries": 5, - "start_period": "10s", - "timeout": "5s", - "type": "tcp" - }, - "id": "pine", - "interfaces": { - "main": { - "description": "Connect your speaker to WiFi and check the voice assistant", - "name": "Pine", - "path": "/", - "port": 10380, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "author": "Archipelago", - "category": "home", - "icon": "/assets/img/app-icons/pine.svg", - "launch": { - "open_in_new_tab": true - }, - "license": "MIT", - "repo": "https://github.com/rhasspy/wyoming", - "tags": [ - "home", - "voice", - "assistant", - "privacy" - ], - "website": "https://github.com/rhasspy/wyoming" - }, - "name": "Pine", - "ports": [ - { - "container": 80, - "host": 10380, - "protocol": "tcp" - }, - { - "container": 443, - "host": 10381, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "64Mi" }, @@ -3944,258 +3888,288 @@ "SETUID", "NET_BIND_SERVICE" ], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "version": "1.1.1", + "ports": [ + { + "host": 10380, + "container": 80, + "protocol": "tcp" + }, + { + "host": 10381, + "container": 443, + "protocol": "tcp" + } + ], "volumes": [ { - "options": [ - "ro" - ], + "type": "bind", "source": "/var/lib/archipelago/pine/nginx.conf", "target": "/etc/nginx/conf.d/default.conf", - "type": "bind" - }, - { "options": [ "ro" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/pine/tls.crt", "target": "/etc/nginx/tls.crt", - "type": "bind" - }, - { "options": [ "ro" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/pine/tls.key", "target": "/etc/nginx/tls.key", - "type": "bind" - }, - { "options": [ "ro" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/pine/index.html", "target": "/usr/share/nginx/html/index.html", - "type": "bind" + "options": [ + "ro" + ] } - ] + ], + "environment": [], + "files": [ + { + "path": "/var/lib/archipelago/pine/nginx.conf", + "overwrite": true, + "content": "server {\n listen 80;\n server_name _;\n return 301 https://$host:10381$request_uri;\n}\nserver {\n listen 443 ssl;\n server_name _;\n ssl_certificate /etc/nginx/tls.crt;\n ssl_certificate_key /etc/nginx/tls.key;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ /index.html; }\n}\n" + }, + { + "path": "/var/lib/archipelago/pine/index.html", + "overwrite": true, + "content": "\n\n\n \n \n Pine \u2014 connect your speaker\n \n\n\n
\n
\n \n \n \n \n \n \n

Pine

\n

Connect your speaker \u2014 everything stays on your node.

\n
\n\n
\n
Whisperspeech-to-text ready on :10300
\n
Pipertext-to-speech ready on :10200
\n
Speakerput it in pairing mode \u2014 ring LED blinking yellow
\n
\n\n
\n This page isn\u2019t running over HTTPS, so the browser blocks Bluetooth.\n Open it via its https://\u2026:10380 address (accept the self-signed\n certificate) and the button below will work.\n
\n\n
\n \n \n \n \n \n
Ready. Click the button, then pick \u201cPineVoice\u201d in the Bluetooth popup.
\n
\n\n

After WiFi joins, add the speaker to Home Assistant:\n Settings \u2192 Devices & services \u2192 Add Wyoming Protocol, host =\n the speaker\u2019s IP, port 10700, then pick an Assist pipeline using\n Whisper + Piper. Wake word: \u201cHey Jarvis.\u201d

\n
\n\n \n\n\n" + } + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:443", + "interval": "30s", + "timeout": "5s", + "retries": 5, + "start_period": "10s" + }, + "interfaces": { + "main": { + "name": "Pine", + "description": "Connect your speaker to WiFi and check the voice assistant", + "type": "ui", + "port": 10380, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "author": "Archipelago", + "icon": "/assets/img/app-icons/pine.svg", + "website": "https://github.com/rhasspy/wyoming", + "repo": "https://github.com/rhasspy/wyoming", + "license": "MIT", + "category": "home", + "launch": { + "open_in_new_tab": true + }, + "tags": [ + "home", + "voice", + "assistant", + "privacy" + ] + } } - }, - "version": "1.1.1" + } }, "pine-piper": { + "version": "2.2.2", "manifest": { "app": { + "id": "pine-piper", + "name": "Pine Piper (TTS)", + "version": "2.2.2", + "description": "Wyoming-protocol Piper text-to-speech engine. Internal Pine voice-assistant stack member \u2014 gives Home Assistant Assist a natural voice for spoken responses on the PineVoice satellite.", "category": "home", + "container_name": "pine-piper", "container": { - "custom_args": [ - "--voice", - "en_GB-alba-medium" - ], "image": "docker.io/rhasspy/wyoming-piper:2.2.2", + "pull_policy": "if-not-present", "network": "archy-net", "network_aliases": [ "pine-piper" ], - "pull_policy": "if-not-present" + "custom_args": [ + "--voice", + "en_GB-alba-medium" + ] }, - "container_name": "pine-piper", "dependencies": [ { "storage": "1Gi" } ], - "description": "Wyoming-protocol Piper text-to-speech engine. Internal Pine voice-assistant stack member — gives Home Assistant Assist a natural voice for spoken responses on the PineVoice satellite.", - "environment": [], - "health_check": { - "endpoint": "localhost:10200", - "interval": "30s", - "retries": 5, - "start_period": "60s", - "timeout": "5s", - "type": "tcp" - }, - "id": "pine-piper", - "metadata": { - "author": "Rhasspy / Home Assistant", - "icon": "/assets/img/app-icons/pine.svg", - "license": "MIT", - "repo": "https://github.com/rhasspy/wyoming-piper", - "tags": [ - "home", - "voice", - "text-to-speech", - "wyoming" - ], - "website": "https://github.com/rhasspy/wyoming-piper" - }, - "name": "Pine Piper (TTS)", - "ports": [ - { - "container": 10200, - "host": 10200, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "512Mi" }, "security": { "capabilities": [], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "version": "2.2.2", + "ports": [ + { + "host": 10200, + "container": 10200, + "protocol": "tcp" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/pine-piper", "target": "/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:10200", + "interval": "30s", + "timeout": "5s", + "retries": 5, + "start_period": "60s" + }, + "metadata": { + "author": "Rhasspy / Home Assistant", + "icon": "/assets/img/app-icons/pine.svg", + "website": "https://github.com/rhasspy/wyoming-piper", + "repo": "https://github.com/rhasspy/wyoming-piper", + "license": "MIT", + "tags": [ + "home", + "voice", + "text-to-speech", + "wyoming" + ] + } } - }, - "version": "2.2.2" + } }, "pine-whisper": { + "version": "3.4.1", "manifest": { "app": { + "id": "pine-whisper", + "name": "Pine Whisper (STT)", + "version": "3.4.1", + "description": "Wyoming-protocol faster-whisper speech-to-text engine. Internal Pine voice-assistant stack member \u2014 turns speech captured by a PineVoice satellite into text for Home Assistant Assist.", "category": "home", + "container_name": "pine-whisper", "container": { + "image": "docker.io/rhasspy/wyoming-whisper:3.4.1", + "pull_policy": "if-not-present", + "network": "archy-net", + "network_aliases": [ + "pine-whisper" + ], "custom_args": [ "--model", "base-int8", "--language", "en" - ], - "image": "docker.io/rhasspy/wyoming-whisper:3.4.1", - "network": "archy-net", - "network_aliases": [ - "pine-whisper" - ], - "pull_policy": "if-not-present" + ] }, - "container_name": "pine-whisper", "dependencies": [ { "storage": "2Gi" } ], - "description": "Wyoming-protocol faster-whisper speech-to-text engine. Internal Pine voice-assistant stack member — turns speech captured by a PineVoice satellite into text for Home Assistant Assist.", - "environment": [], - "health_check": { - "endpoint": "localhost:10300", - "interval": "30s", - "retries": 5, - "start_period": "60s", - "timeout": "5s", - "type": "tcp" - }, - "id": "pine-whisper", - "metadata": { - "author": "Rhasspy / Home Assistant", - "icon": "/assets/img/app-icons/pine.svg", - "license": "MIT", - "repo": "https://github.com/rhasspy/wyoming-faster-whisper", - "tags": [ - "home", - "voice", - "speech-to-text", - "wyoming" - ], - "website": "https://github.com/rhasspy/wyoming-faster-whisper" - }, - "name": "Pine Whisper (STT)", - "ports": [ - { - "container": 10300, - "host": 10300, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "2Gi" }, "security": { "capabilities": [], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "version": "3.4.1", + "ports": [ + { + "host": 10300, + "container": 10300, + "protocol": "tcp" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/pine-whisper", "target": "/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:10300", + "interval": "30s", + "timeout": "5s", + "retries": 5, + "start_period": "60s" + }, + "metadata": { + "author": "Rhasspy / Home Assistant", + "icon": "/assets/img/app-icons/pine.svg", + "website": "https://github.com/rhasspy/wyoming-faster-whisper", + "repo": "https://github.com/rhasspy/wyoming-faster-whisper", + "license": "MIT", + "tags": [ + "home", + "voice", + "speech-to-text", + "wyoming" + ] + } } - }, - "version": "3.4.1" + } }, "portainer": { + "version": "2.19.4", "image": "146.59.87.168:3000/lfg2025/portainer:2.19.4", "manifest": { "app": { + "id": "portainer", + "name": "Portainer", + "version": "2.19.4", + "description": "Container management web UI for the local Podman socket.", "category": "development", "container": { - "data_uid": "1000:1000", "image": "146.59.87.168:3000/lfg2025/portainer:2.19.4", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "data_uid": "1000:1000" }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Container management web UI for the local Podman socket.", - "environment": [], - "id": "portainer", - "interfaces": { - "main": { - "description": "Portainer web interface", - "name": "Web UI", - "path": "/", - "port": 9000, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "features": [ - "Container management dashboard", - "Local Podman socket access", - "Compose stack storage" - ], - "icon": "/assets/img/app-icons/portainer.webp", - "launch": { - "open_in_new_tab": true - }, - "tier": "optional" - }, - "name": "Portainer", - "ports": [ - { - "container": 9000, - "host": 9000, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "1Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "1Gi" }, "security": { "capabilities": [ @@ -4204,44 +4178,77 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "version": "2.19.4", + "ports": [ + { + "host": 9000, + "container": 9000, + "protocol": "tcp" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/portainer", "target": "/data", - "type": "bind" - }, - { "options": [ "rw" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/portainer/compose", "target": "/data/compose", - "type": "bind" - }, - { "options": [ "rw" - ], + ] + }, + { + "type": "bind", "source": "/run/user/1000/podman/podman.sock", "target": "/var/run/docker.sock", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "interfaces": { + "main": { + "name": "Web UI", + "description": "Portainer web interface", + "type": "ui", + "port": 9000, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/portainer.webp", + "tier": "optional", + "launch": { + "open_in_new_tab": true + }, + "features": [ + "Container management dashboard", + "Local Podman socket access", + "Compose stack storage" + ] + } } - }, - "version": "2.19.4" + } }, "router": { + "version": "1.0.0", "manifest": { "app": { + "id": "router", + "name": "Mesh Router", + "version": "1.0.0", + "description": "Mesh routing and local network management. Provides device discovery, routing, and network topology visualization.", "container": { "image": "archipelago/router:1.0.0", "image_signature": "cosign://...", @@ -4252,96 +4259,96 @@ "storage": "500Mi" } ], - "description": "Mesh routing and local network management. Provides device discovery, routing, and network topology visualization.", + "resources": { + "cpu_limit": 2, + "memory_limit": "512Mi", + "disk_limit": "500Mi" + }, + "security": { + "capabilities": [ + "NET_ADMIN", + "NET_RAW" + ], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "host", + "apparmor_profile": "router" + }, + "ports": [ + { + "host": 8084, + "container": 8080, + "protocol": "tcp" + }, + { + "host": 5353, + "container": 5353, + "protocol": "udp" + }, + { + "host": 1900, + "container": 1900, + "protocol": "udp" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/router", + "target": "/app/data", + "options": [ + "rw" + ] + }, + { + "type": "bind", + "source": "/var/run/dbus", + "target": "/var/run/dbus", + "options": [ + "ro" + ] + } + ], "environment": [ "NETWORK_INTERFACE=eth0", "MESH_ENABLED=true", "DEVICE_DISCOVERY=true" ], "health_check": { + "type": "http", "endpoint": "http://localhost:8084", - "interval": "30s", "path": "/health", - "retries": 3, + "interval": "30s", "timeout": "5s", - "type": "http" + "retries": 3 }, - "id": "router", - "name": "Mesh Router", "networking": { - "device_discovery": true, - "local_network_access": true, "mesh_enabled": true, + "local_network_access": true, + "device_discovery": true, "routing_protocols": [ "olsr", "babel" ] - }, - "ports": [ - { - "container": 8080, - "host": 8084, - "protocol": "tcp" - }, - { - "container": 5353, - "host": 5353, - "protocol": "udp" - }, - { - "container": 1900, - "host": 1900, - "protocol": "udp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "500Mi", - "memory_limit": "512Mi" - }, - "security": { - "apparmor_profile": "router", - "capabilities": [ - "NET_ADMIN", - "NET_RAW" - ], - "network_policy": "host", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "version": "1.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/router", - "target": "/app/data", - "type": "bind" - }, - { - "options": [ - "ro" - ], - "source": "/var/run/dbus", - "target": "/var/run/dbus", - "type": "bind" - } - ] + } } - }, - "version": "1.0.0" + } }, "routstr": { - "image": "146.59.87.168:3000/lfg2025/routstr:v0.4.3", - "version": "v0.4.3" + "version": "v0.4.3", + "image": "146.59.87.168:3000/lfg2025/routstr:v0.4.3" }, "searxng": { + "version": "latest", "image": "146.59.87.168:3000/lfg2025/searxng:latest", "manifest": { "app": { + "id": "searxng", + "name": "SearXNG", + "version": "1.0.0", + "description": "Privacy-respecting metasearch engine. Search the web without tracking.", "container": { "image": "146.59.87.168:3000/lfg2025/searxng:latest", "pull_policy": "if-not-present" @@ -4351,60 +4358,60 @@ "storage": "2Gi" } ], - "description": "Privacy-respecting metasearch engine. Search the web without tracking.", + "resources": { + "cpu_limit": 2, + "memory_limit": "1Gi", + "disk_limit": "2Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "searxng" + }, + "ports": [ + { + "host": 8888, + "container": 8080, + "protocol": "tcp" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/searxng", + "target": "/etc/searxng", + "options": [ + "rw" + ] + } + ], "environment": [ "SEARXNG_HOSTNAME=localhost", "SEARXNG_BIND_ADDRESS=0.0.0.0:8080" ], "health_check": { + "type": "http", "endpoint": "http://localhost:8080", - "interval": "30s", "path": "/", - "retries": 5, + "interval": "30s", "timeout": "30s", - "type": "http" - }, - "id": "searxng", - "name": "SearXNG", - "ports": [ - { - "container": 8080, - "host": 8888, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "2Gi", - "memory_limit": "1Gi" - }, - "security": { - "apparmor_profile": "searxng", - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "version": "1.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/searxng", - "target": "/etc/searxng", - "type": "bind" - } - ] + "retries": 5 + } } - }, - "version": "latest" + } }, "strfry": { + "version": "0.9.0", "manifest": { "app": { + "id": "strfry", + "name": "Strfry Nostr Relay", + "version": "0.9.0", + "description": "Lightweight Nostr relay written in C++. Alternative to nostr-rs-relay with lower resource usage.", "container": { "image": "dockurr/strfry:1.0.4", "image_signature": "cosign://...", @@ -4415,128 +4422,97 @@ "storage": "5Gi" } ], - "description": "Lightweight Nostr relay written in C++. Alternative to nostr-rs-relay with lower resource usage.", - "files": [ - { - "content": "##\n## Default strfry config\n##\n\n# Directory that contains the strfry LMDB database (restart required)\ndb = \"./strfry-db/\"\n\ndbParams {\n # Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required)\n maxreaders = 256\n\n # Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required)\n mapsize = 10995116277760\n\n # Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required)\n noReadAhead = false\n}\n\nevents {\n # Maximum size of normalised JSON, in bytes\n maxEventSize = 65536\n\n # Events newer than this will be rejected\n rejectEventsNewerThanSeconds = 900\n\n # Events older than this will be rejected\n rejectEventsOlderThanSeconds = 94608000\n\n # Ephemeral events older than this will be rejected\n rejectEphemeralEventsOlderThanSeconds = 60\n\n # Ephemeral events will be deleted from the DB when older than this\n ephemeralEventsLifetimeSeconds = 300\n\n # Maximum number of tags allowed\n maxNumTags = 2000\n\n # Maximum size for tag values, in bytes\n maxTagValSize = 1024\n}\n\nrelay {\n # Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)\n bind = \"0.0.0.0\"\n\n # Port to open for the nostr websocket protocol (restart required)\n port = 7777\n\n # Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required)\n nofiles = 0\n\n # HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case)\n realIpHeader = \"\"\n\n info {\n # NIP-11: Name of this server. Short/descriptive (< 30 characters)\n name = \"Archipelago Strfry Relay\"\n\n # NIP-11: Detailed information about relay, free-form\n description = \"Self-hosted strfry Nostr relay on Archipelago.\"\n\n # NIP-11: Administrative nostr pubkey, for contact purposes\n pubkey = \"\"\n\n # NIP-11: Alternative administrative contact (email, website, etc)\n contact = \"\"\n\n # NIP-11: URL pointing to an image to be used as an icon for the relay\n icon = \"\"\n\n # List of supported lists as JSON array, or empty string to use default. Example: \"[1,2]\"\n nips = \"\"\n }\n\n # Maximum accepted incoming websocket frame size (should be larger than max event) (restart required)\n maxWebsocketPayloadSize = 131072\n\n # Maximum number of filters allowed in a REQ\n maxReqFilterSize = 200\n\n # Websocket-level PING message frequency (should be less than any reverse proxy idle timeouts) (restart required)\n autoPingSeconds = 55\n\n # If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy)\n enableTcpKeepalive = false\n\n # How much uninterrupted CPU time a REQ query should get during its DB scan\n queryTimesliceBudgetMicroseconds = 10000\n\n # Maximum records that can be returned per filter\n maxFilterLimit = 500\n\n # Maximum number of subscriptions (concurrent REQs) a connection can have open at any time\n maxSubsPerConnection = 20\n\n writePolicy {\n # If non-empty, path to an executable script that implements the writePolicy plugin logic\n plugin = \"/app/write-policy.py\"\n }\n\n compression {\n # Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required)\n enabled = true\n\n # Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required)\n slidingWindow = true\n }\n\n logging {\n # Dump all incoming messages\n dumpInAll = false\n\n # Dump all incoming EVENT messages\n dumpInEvents = false\n\n # Dump all incoming REQ/CLOSE messages\n dumpInReqs = false\n\n # Log performance metrics for initial REQ database scans\n dbScanPerf = false\n\n # Log reason for invalid event rejection? Can be disabled to silence excessive logging\n invalidEvents = true\n }\n\n numThreads {\n # Ingester threads: route incoming requests, validate events/sigs (restart required)\n ingester = 3\n\n # reqWorker threads: Handle initial DB scan for events (restart required)\n reqWorker = 3\n\n # reqMonitor threads: Handle filtering of new events (restart required)\n reqMonitor = 3\n\n # negentropy threads: Handle negentropy protocol messages (restart required)\n negentropy = 2\n }\n\n negentropy {\n # Support negentropy protocol messages\n enabled = true\n\n # Maximum records that sync will process before returning an error\n maxSyncEvents = 1000000\n }\n}\n", - "overwrite": true, - "path": "/var/lib/archipelago/strfry-config/strfry.conf" - } - ], - "health_check": { - "endpoint": "http://127.0.0.1:7777", - "interval": "30s", - "path": "/health", - "retries": 3, - "timeout": "5s", - "type": "http" + "resources": { + "cpu_limit": 1, + "memory_limit": "512Mi", + "disk_limit": "5Gi" }, - "id": "strfry", - "name": "Strfry Nostr Relay", - "nostr_integration": { - "monetization_enabled": true, - "relay_type": "public" + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "nostr-relay" }, "ports": [ { - "container": 7777, "host": 8090, + "container": 7777, "protocol": "tcp" } ], - "resources": { - "cpu_limit": 1, - "disk_limit": "5Gi", - "memory_limit": "512Mi" - }, - "security": { - "apparmor_profile": "nostr-relay", - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default" - }, - "version": "0.9.0", "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/strfry", "target": "/app/strfry-db", - "type": "bind" + "options": [ + "rw" + ] }, { - "options": [ - "ro" - ], + "type": "bind", "source": "/var/lib/archipelago/strfry-config/strfry.conf", "target": "/etc/strfry.conf", - "type": "bind" + "options": [ + "ro" + ] } - ] + ], + "files": [ + { + "path": "/var/lib/archipelago/strfry-config/strfry.conf", + "overwrite": true, + "content": "##\n## Default strfry config\n##\n\n# Directory that contains the strfry LMDB database (restart required)\ndb = \"./strfry-db/\"\n\ndbParams {\n # Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required)\n maxreaders = 256\n\n # Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required)\n mapsize = 10995116277760\n\n # Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required)\n noReadAhead = false\n}\n\nevents {\n # Maximum size of normalised JSON, in bytes\n maxEventSize = 65536\n\n # Events newer than this will be rejected\n rejectEventsNewerThanSeconds = 900\n\n # Events older than this will be rejected\n rejectEventsOlderThanSeconds = 94608000\n\n # Ephemeral events older than this will be rejected\n rejectEphemeralEventsOlderThanSeconds = 60\n\n # Ephemeral events will be deleted from the DB when older than this\n ephemeralEventsLifetimeSeconds = 300\n\n # Maximum number of tags allowed\n maxNumTags = 2000\n\n # Maximum size for tag values, in bytes\n maxTagValSize = 1024\n}\n\nrelay {\n # Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)\n bind = \"0.0.0.0\"\n\n # Port to open for the nostr websocket protocol (restart required)\n port = 7777\n\n # Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required)\n nofiles = 0\n\n # HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case)\n realIpHeader = \"\"\n\n info {\n # NIP-11: Name of this server. Short/descriptive (< 30 characters)\n name = \"Archipelago Strfry Relay\"\n\n # NIP-11: Detailed information about relay, free-form\n description = \"Self-hosted strfry Nostr relay on Archipelago.\"\n\n # NIP-11: Administrative nostr pubkey, for contact purposes\n pubkey = \"\"\n\n # NIP-11: Alternative administrative contact (email, website, etc)\n contact = \"\"\n\n # NIP-11: URL pointing to an image to be used as an icon for the relay\n icon = \"\"\n\n # List of supported lists as JSON array, or empty string to use default. Example: \"[1,2]\"\n nips = \"\"\n }\n\n # Maximum accepted incoming websocket frame size (should be larger than max event) (restart required)\n maxWebsocketPayloadSize = 131072\n\n # Maximum number of filters allowed in a REQ\n maxReqFilterSize = 200\n\n # Websocket-level PING message frequency (should be less than any reverse proxy idle timeouts) (restart required)\n autoPingSeconds = 55\n\n # If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy)\n enableTcpKeepalive = false\n\n # How much uninterrupted CPU time a REQ query should get during its DB scan\n queryTimesliceBudgetMicroseconds = 10000\n\n # Maximum records that can be returned per filter\n maxFilterLimit = 500\n\n # Maximum number of subscriptions (concurrent REQs) a connection can have open at any time\n maxSubsPerConnection = 20\n\n writePolicy {\n # If non-empty, path to an executable script that implements the writePolicy plugin logic\n plugin = \"/app/write-policy.py\"\n }\n\n compression {\n # Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required)\n enabled = true\n\n # Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required)\n slidingWindow = true\n }\n\n logging {\n # Dump all incoming messages\n dumpInAll = false\n\n # Dump all incoming EVENT messages\n dumpInEvents = false\n\n # Dump all incoming REQ/CLOSE messages\n dumpInReqs = false\n\n # Log performance metrics for initial REQ database scans\n dbScanPerf = false\n\n # Log reason for invalid event rejection? Can be disabled to silence excessive logging\n invalidEvents = true\n }\n\n numThreads {\n # Ingester threads: route incoming requests, validate events/sigs (restart required)\n ingester = 3\n\n # reqWorker threads: Handle initial DB scan for events (restart required)\n reqWorker = 3\n\n # reqMonitor threads: Handle filtering of new events (restart required)\n reqMonitor = 3\n\n # negentropy threads: Handle negentropy protocol messages (restart required)\n negentropy = 2\n }\n\n negentropy {\n # Support negentropy protocol messages\n enabled = true\n\n # Maximum records that sync will process before returning an error\n maxSyncEvents = 1000000\n }\n}\n" + } + ], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:7777", + "path": "/health", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "nostr_integration": { + "relay_type": "public", + "monetization_enabled": true + } } - }, - "version": "0.9.0" + } }, "tailscale": { - "image": "146.59.87.168:3000/lfg2025/tailscale:stable", - "version": "stable" + "version": "stable", + "image": "146.59.87.168:3000/lfg2025/tailscale:stable" }, "uptime-kuma": { + "version": "1", "image": "146.59.87.168:3000/lfg2025/uptime-kuma:1", "manifest": { "app": { + "id": "uptime-kuma", + "name": "Uptime Kuma", + "version": "1.23.0", + "description": "Self-hosted uptime monitoring.", "container": { + "image": "146.59.87.168:3000/lfg2025/uptime-kuma:1", + "pull_policy": "if-not-present", + "network": "pasta", "custom_args": [ "--", "node", "server/server.js" - ], - "image": "146.59.87.168:3000/lfg2025/uptime-kuma:1", - "network": "pasta", - "pull_policy": "if-not-present" + ] }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Self-hosted uptime monitoring.", - "environment": [ - "TZ=UTC" - ], - "health_check": { - "endpoint": "localhost:3001", - "interval": "30s", - "path": "/", - "retries": 3, - "timeout": "5s", - "type": "http" - }, - "id": "uptime-kuma", - "metadata": { - "author": "Uptime Kuma", - "category": "data", - "icon": "/assets/img/app-icons/uptime-kuma.webp", - "launch": { - "open_in_new_tab": true - }, - "repo": "https://github.com/louislam/uptime-kuma", - "tier": "recommended" - }, - "name": "Uptime Kuma", - "ports": [ - { - "container": 3001, - "host": 3002, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "1Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "1Gi" }, "security": { "capabilities": [ @@ -4545,79 +4521,72 @@ "SETUID", "SETGID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "1.23.0", + "ports": [ + { + "host": 3002, + "container": 3001, + "protocol": "tcp" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/uptime-kuma", "target": "/app/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [ + "TZ=UTC" + ], + "health_check": { + "type": "http", + "endpoint": "localhost:3001", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "metadata": { + "icon": "/assets/img/app-icons/uptime-kuma.webp", + "category": "data", + "tier": "recommended", + "author": "Uptime Kuma", + "repo": "https://github.com/louislam/uptime-kuma", + "launch": { + "open_in_new_tab": true + } + } } - }, - "version": "1" + } }, "vaultwarden": { + "version": "1.30.0-alpine", "image": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine", "manifest": { "app": { + "id": "vaultwarden", + "name": "Vaultwarden", + "version": "1.30.0", + "description": "Self-hosted password vault with zero-knowledge encryption.", "container": { "image": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine", - "network": "pasta", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "pasta" }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Self-hosted password vault with zero-knowledge encryption.", - "environment": [], - "health_check": { - "endpoint": "localhost:80", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "vaultwarden", - "interfaces": { - "main": { - "description": "Vaultwarden web vault", - "name": "Web UI", - "path": "/", - "port": 8082, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "author": "Vaultwarden", - "category": "data", - "icon": "/assets/img/app-icons/vaultwarden.webp", - "launch": { - "open_in_new_tab": true - }, - "repo": "https://github.com/dani-garcia/vaultwarden", - "tier": "recommended" - }, - "name": "Vaultwarden", - "ports": [ - { - "container": 80, - "host": 8082, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "1Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "1Gi" }, "security": { "capabilities": [ @@ -4626,27 +4595,56 @@ "SETGID", "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "1.30.0", + "ports": [ + { + "host": 8082, + "container": 80, + "protocol": "tcp" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/vaultwarden", "target": "/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:80", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "interfaces": { + "main": { + "name": "Web UI", + "description": "Vaultwarden web vault", + "type": "ui", + "port": 8082, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/vaultwarden.webp", + "category": "data", + "tier": "recommended", + "author": "Vaultwarden", + "repo": "https://github.com/dani-garcia/vaultwarden", + "launch": { + "open_in_new_tab": true + } + } } - }, - "version": "1.30.0-alpine" + } } - }, - "schema": 1, - "signature": "aa471d39066c27f305aeefde39f239357d38af4bc6fe50aa2f6006690bb7a0cd7f4b0ecd6e866330c7055303ce433f27d94820964eff23efaa5a3a49883c9e08", - "signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur", - "updated": "2026-07-21" + } } diff --git a/scripts/image-versions.sh b/scripts/image-versions.sh index 3b6640cf..aa5e91a7 100644 --- a/scripts/image-versions.sh +++ b/scripts/image-versions.sh @@ -31,7 +31,7 @@ POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15.17" BTCPAY_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15.17" # Apps -HOMEASSISTANT_IMAGE="$ARCHY_REGISTRY/home-assistant:2024.1" +HOMEASSISTANT_IMAGE="$ARCHY_REGISTRY/home-assistant:2026.7.3" GRAFANA_IMAGE="$ARCHY_REGISTRY/grafana:10.2.0" UPTIME_KUMA_IMAGE="$ARCHY_REGISTRY/uptime-kuma:1" JELLYFIN_IMAGE="$ARCHY_REGISTRY/jellyfin:10.8.13"