diff --git a/apps/cuprate-ui/manifest.yml b/apps/cuprate-ui/manifest.yml new file mode 100644 index 00000000..05260c95 --- /dev/null +++ b/apps/cuprate-ui/manifest.yml @@ -0,0 +1,67 @@ +app: + id: cuprate-ui + name: Cuprate UI + version: 1.0.0 + # Built by this project — there is no upstream release feed to watch. + upstream: + kind: internal + description: | + Archipelago-native HTTP frontend for the Cuprate Monero node. Runs nginx + inside a container, serves a static status dashboard, and proxies + /cuprate-rpc/ to the cuprate restricted RPC on 127.0.0.1:18090 (the + published host port for the container's 18089). No credentials are + injected — the restricted RPC is Monero's own safe-for-public subset — so + the nginx.conf is baked into the image and there is no rendered-config + bind-mount like bitcoin-ui's. + + container: + build: + context: /opt/archipelago/docker/cuprate-ui + dockerfile: Dockerfile + tag: localhost/cuprate-ui:local + + dependencies: + - app_id: cuprate + + resources: + memory_limit: 64Mi + + security: + readonly_root: false + network_policy: host + + # Host networking: nginx listens on 18091 directly on the host IP. + # Declared so the APP GATE can see this port. Host networking means Podman + # publishes nothing (quadlet skips PublishPort in host mode), so `bind:` here + # is a statement of where the container's own nginx listens — 127.0.0.1 — + # not a publish instruction. Without this declaration the gate would have no + # idea the port existed: neither protected nor listed as unprotected. + ports: + - host: 18091 + container: 18091 + protocol: tcp + bind: 127.0.0.1 + auth: gated + # First-party companion UI: its nginx forwards the node session cookie + # to the daemon's authenticated endpoints; without passthrough the gate + # strips it and every data call 401s while the page shell renders. + session_passthrough: true + + volumes: [] + + environment: [] + + health_check: + type: http + endpoint: http://127.0.0.1:18091 + path: / + interval: 30s + timeout: 5s + retries: 3 + + metadata: + icon: /assets/img/app-icons/cuprate.svg + category: money + tier: optional + author: Archipelago + repo: https://github.com/Cuprate/cuprate diff --git a/core/archipelago/src/container/companion.rs b/core/archipelago/src/container/companion.rs index 934137c3..9752454f 100644 --- a/core/archipelago/src/container/companion.rs +++ b/core/archipelago/src/container/companion.rs @@ -10,6 +10,7 @@ //! | lnd | archy-lnd-ui | wallet/channel UI | //! | electrumx | archy-electrs-ui | indexer status UI | //! | fedimint | archy-fedimint-ui | wait/proxy Guardian UI | +//! | cuprate | archy-cuprate-ui | Monero node status UI | //! //! Lifecycle: `install` writes a Quadlet `.container` unit to //! `~/.config/containers/systemd/`, daemon-reloads, then starts the @@ -97,6 +98,7 @@ pub fn companions_for(package_id: &str) -> &'static [CompanionSpec] { "lnd" => LND_UI, "electrumx" | "electrs" | "mempool-electrs" => ELECTRS_UI, "fedimint" | "fedimintd" => FEDIMINT_UI, + "cuprate" => CUPRATE_UI, _ => &[], } } @@ -104,7 +106,8 @@ pub fn companions_for(package_id: &str) -> &'static [CompanionSpec] { /// Every companion this build knows how to provision. Kept beside /// `companions_for` — a new companion must be added to both, or the reaper /// will not recognise it as one of ours and will leave it running forever. -const ALL_COMPANIONS: &[&[CompanionSpec]] = &[BITCOIN_UI, LND_UI, ELECTRS_UI, FEDIMINT_UI]; +const ALL_COMPANIONS: &[&[CompanionSpec]] = + &[BITCOIN_UI, LND_UI, ELECTRS_UI, FEDIMINT_UI, CUPRATE_UI]; const BITCOIN_UI: &[CompanionSpec] = &[CompanionSpec { name: "archy-bitcoin-ui", @@ -172,6 +175,24 @@ const FEDIMINT_UI: &[CompanionSpec] = &[CompanionSpec { host_network: true, }]; +const CUPRATE_UI: &[CompanionSpec] = &[CompanionSpec { + name: "archy-cuprate-ui", + image_base: "cuprate-ui", + build_dir_candidates: &[ + "/opt/archipelago/docker/cuprate-ui", + "/home/archipelago/archy/docker/cuprate-ui", + "/home/archipelago/Projects/archy/docker/cuprate-ui", + ], + // No pre-start hook and no bind mounts: unlike bitcoin-ui there is no + // secret to inject. Cuprate's restricted RPC (the only thing this UI + // proxies) is unauthenticated by design — Monero's safe-for-public + // subset — so the nginx.conf is baked into the image. + pre_start: None, + bind_mounts: &[], + ports: &[], + host_network: true, +}]; + fn render_bitcoin_ui() -> futures_util::future::BoxFuture<'static, Result<()>> { Box::pin(async { let paths = crate::container::bitcoin_ui::RenderPaths::default(); @@ -869,6 +890,7 @@ mod tests { "mempool-electrs", "fedimint", "fedimintd", + "cuprate", ]; let known: std::collections::HashSet<&str> = ALL_COMPANIONS .iter() @@ -893,6 +915,7 @@ mod tests { names(&orphan_companions(&[])), vec![ "archy-bitcoin-ui", + "archy-cuprate-ui", "archy-electrs-ui", "archy-fedimint-ui", "archy-lnd-ui" @@ -906,7 +929,10 @@ mod tests { // electrumx installed, fedimint and lnd not — yet all four companions // were running because the reconciler was fed the manifest list. let orphans = orphan_companions(&ids(&["bitcoin-knots", "electrumx"])); - assert_eq!(names(&orphans), vec!["archy-fedimint-ui", "archy-lnd-ui"]); + assert_eq!( + names(&orphans), + vec!["archy-cuprate-ui", "archy-fedimint-ui", "archy-lnd-ui"] + ); } #[test] @@ -926,12 +952,18 @@ mod tests { #[test] fn apps_without_companions_orphan_everything_and_panic_nothing() { let orphans = orphan_companions(&ids(&["nextcloud", "not-a-real-app"])); - assert_eq!(orphans.len(), 4); + assert_eq!(orphans.len(), 5); } #[test] fn every_backend_installed_leaves_no_orphans() { - let orphans = orphan_companions(&ids(&["bitcoin-knots", "lnd", "electrumx", "fedimint"])); + let orphans = orphan_companions(&ids(&[ + "bitcoin-knots", + "lnd", + "electrumx", + "fedimint", + "cuprate", + ])); assert!( names(&orphans).is_empty(), "unexpected orphans: {:?}", @@ -970,7 +1002,12 @@ mod tests { 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"] + vec![ + "archy-cuprate-ui", + "archy-electrs-ui", + "archy-fedimint-ui", + "archy-lnd-ui" + ] ); } @@ -1024,6 +1061,7 @@ mod tests { assert_eq!(companions_for("mempool-electrs").len(), 1); assert_eq!(companions_for("fedimint").len(), 1); assert_eq!(companions_for("fedimintd").len(), 1); + assert_eq!(companions_for("cuprate").len(), 1); assert_eq!(companions_for("nextcloud").len(), 0); assert_eq!(companions_for("not-a-real-app").len(), 0); } diff --git a/core/archipelago/src/container/image_versions.rs b/core/archipelago/src/container/image_versions.rs index a82ba467..0fd991c9 100644 --- a/core/archipelago/src/container/image_versions.rs +++ b/core/archipelago/src/container/image_versions.rs @@ -146,6 +146,7 @@ fn image_var_for_app(app_id: &str) -> Option<&'static str> { "bitcoin-ui" | "archy-bitcoin-ui" => Some("BITCOIN_UI_IMAGE"), "lnd-ui" | "archy-lnd-ui" => Some("LND_UI_IMAGE"), "electrs-ui" | "archy-electrs-ui" => Some("ELECTRS_UI_IMAGE"), + "cuprate-ui" | "archy-cuprate-ui" => Some("CUPRATE_UI_IMAGE"), // Mempool stack (primary = web) "mempool" | "mempool-web" | "archy-mempool-web" => Some("MEMPOOL_WEB_IMAGE"), diff --git a/core/archipelago/src/fips/app_ports.rs b/core/archipelago/src/fips/app_ports.rs index 85c0ba53..54bbd536 100644 --- a/core/archipelago/src/fips/app_ports.rs +++ b/core/archipelago/src/fips/app_ports.rs @@ -6,7 +6,42 @@ //! no listener, so allowing them is inert. pub const APP_LAUNCH_PORTS: &[u16] = &[ - 2283, 2342, 3000, 3001, 3002, 4080, 5180, 7778, 8080, 8081, 8082, 8083, 8084, 8085, 8087, 8090, - 8096, 8123, 8175, 8176, 8187, 8240, 8334, 8336, 8337, 8888, 8999, 9000, 9100, 10380, 11434, - 18081, 18083, 23000, 32838, 50002, + 2283, + 2342, + 3000, + 3001, + 3002, + 4080, + 5180, + 7778, + 8080, + 8081, + 8082, + 8083, + 8084, + 8085, + 8087, + 8090, + 8096, + 8123, + 8175, + 8176, + 8187, + 8240, + 8334, + 8336, + 8337, + 8888, + 8999, + 9000, + 9100, + 10380, + 11434, + 18081, + 18083, + 18091, + 18091, + 23000, + 32838, + 50002, ]; diff --git a/core/archipelago/src/health_monitor.rs b/core/archipelago/src/health_monitor.rs index fbcdc2cd..a203e138 100644 --- a/core/archipelago/src/health_monitor.rs +++ b/core/archipelago/src/health_monitor.rs @@ -53,8 +53,8 @@ fn container_tier(name: &str) -> StartupTier { | "indeedhub-api" => StartupTier::DependentService, // Tier 4: Frontend/UI - "mempool-web" | "bitcoin-ui" | "lnd-ui" | "electrs-ui" | "penpot-frontend" - | "penpot-exporter" | "indeedhub" => StartupTier::Frontend, + "mempool-web" | "bitcoin-ui" | "lnd-ui" | "electrs-ui" | "cuprate-ui" + | "penpot-frontend" | "penpot-exporter" | "indeedhub" => StartupTier::Frontend, // Tier 3: Application layer (everything else) _ => StartupTier::Application, diff --git a/docker/cuprate-ui/50x.html b/docker/cuprate-ui/50x.html new file mode 100644 index 00000000..a57c2f93 --- /dev/null +++ b/docker/cuprate-ui/50x.html @@ -0,0 +1,19 @@ + + +
+Sorry, the page you are looking for is currently unavailable.
+Please try again later.
If you are the system administrator of this resource then you should check +the error log for details.
+Faithfully yours, nginx.
+ + diff --git a/docker/cuprate-ui/Dockerfile b/docker/cuprate-ui/Dockerfile new file mode 100644 index 00000000..1ede64e3 --- /dev/null +++ b/docker/cuprate-ui/Dockerfile @@ -0,0 +1,20 @@ +FROM git.tx1138.com/lfg2025/nginx:1.27.4-alpine +# Static site content. +COPY index.html /usr/share/nginx/html/ +COPY 50x.html /usr/share/nginx/html/ +# Unlike bitcoin-ui, the nginx.conf is baked into the image, not +# bind-mounted: there is no secret to render in. Cuprate's restricted RPC +# (the only upstream this UI proxies) is unauthenticated by design — +# Monero's safe-for-public subset — so there is nothing to substitute at +# start time and no rotation to follow. +COPY nginx.conf /etc/nginx/conf.d/default.conf +# +# Run nginx as root to avoid chown failures in rootless Podman user +# namespaces. The rest of the nginx image is unchanged. +RUN sed -i 's/^user nginx;/user root;/' /etc/nginx/nginx.conf && \ + mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp \ + /var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp \ + /var/cache/nginx/scgi_temp +EXPOSE 18091 +ENTRYPOINT [] +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/cuprate-ui/index.html b/docker/cuprate-ui/index.html new file mode 100644 index 00000000..4b110574 --- /dev/null +++ b/docker/cuprate-ui/index.html @@ -0,0 +1,339 @@ + + + + + + + + +