diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index c993b05..4284205 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -28,6 +28,7 @@ mod spaces; mod sync; mod tab_context; mod task_manager; +mod updates; use ely_browser_core::BrowserSnapshot; use ely_design_system::{colors, spacing}; @@ -88,6 +89,7 @@ impl ElyShell { "ely://settings/plugins" => self.render_plugins_page(snapshot, cx), "ely://settings/profiles" => self.render_profiles_page(snapshot, cx), "ely://settings/sync" => self.render_sync_page(snapshot, cx), + "ely://settings/updates" => self.render_updates_page(snapshot), "ely://sync/status" => self.render_sync_page(snapshot, cx), url if super::web_surface::is_external_web_url(url) => { self.render_external_web_canvas(tab, cx) diff --git a/crates/ely_app/src/shell/internal_pages/settings.rs b/crates/ely_app/src/shell/internal_pages/settings.rs index 98216ec..e65f09f 100644 --- a/crates/ely_app/src/shell/internal_pages/settings.rs +++ b/crates/ely_app/src/shell/internal_pages/settings.rs @@ -84,6 +84,12 @@ const SETTINGS_ROUTES: &[SettingsRoute] = &[ detail: "Local sync state and object scope.", route: "ely://settings/sync", }, + SettingsRoute { + icon: IconName::LoaderCircle, + title: "Updates", + detail: "Build identity and Elydora release manifest contract.", + route: "ely://settings/updates", + }, SettingsRoute { icon: IconName::Asterisk, title: "Plugins", diff --git a/crates/ely_app/src/shell/internal_pages/updates.rs b/crates/ely_app/src/shell/internal_pages/updates.rs new file mode 100644 index 0000000..366c339 --- /dev/null +++ b/crates/ely_app/src/shell/internal_pages/updates.rs @@ -0,0 +1,215 @@ +use std::env; + +use ely_browser_core::BrowserSnapshot; +use ely_design_system::colors; +use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb}; +use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; + +use super::{ElyShell, render_canvas_surface}; + +const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); +const BUILD_REVISION: &str = env!("ELY_BUILD_REVISION"); +const RELEASE_MANIFEST_PATH: &str = "/api/releases/manifest"; +const RELEASE_SIGNATURE_PATH: &str = "/api/releases/signature"; +const RELEASE_MANIFEST_CACHE: &str = "release_manifest_cache"; +const RELEASE_INTEGRITY: &str = "SHA-256 + Ed25519"; + +impl ElyShell { + pub(super) fn render_updates_page(&mut self, snapshot: &BrowserSnapshot) -> AnyElement { + render_canvas_surface( + div() + .size_full() + .p_8() + .flex() + .flex_col() + .gap_5() + .child(render_updates_header(snapshot)) + .child(render_updates_summary()) + .child(render_update_contract_rows()), + ) + } +} + +fn render_updates_header(snapshot: &BrowserSnapshot) -> AnyElement { + div() + .flex() + .items_end() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_2() + .child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Updates")) + .child( + div() + .text_sm() + .truncate() + .text_color(rgb(colors::MUTED)) + .child(format!("Profile: {}", snapshot.active_profile_name)), + ), + ) + .child( + div() + .flex() + .items_center() + .gap_2() + .text_xs() + .font_semibold() + .text_color(rgb(colors::MUTED)) + .child(IconName::LoaderCircle) + .child(format!("Build {BUILD_REVISION}")), + ) + .into_any_element() +} + +fn render_updates_summary() -> AnyElement { + div() + .rounded_md() + .border_1() + .border_color(rgb(colors::HAIRLINE)) + .bg(rgb(colors::CANVAS_SOFT)) + .px_4() + .py_3() + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .items_center() + .gap_3() + .child(div().text_color(rgb(colors::PRIMARY)).child(IconName::LoaderCircle)) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .text_color(rgb(colors::INK)) + .child("Release Manifest Contract"), + ) + .child(div().text_xs().truncate().text_color(rgb(colors::MUTED)).child( + format!( + "{} target through Elydora Cloud release APIs", + release_target() + ), + )), + ), + ) + .child(div().text_xs().font_semibold().text_color(rgb(colors::SUCCESS)).child(APP_VERSION)) + .into_any_element() +} + +fn render_update_contract_rows() -> AnyElement { + div() + .flex_1() + .min_h_0() + .flex() + .flex_col() + .overflow_y_scrollbar() + .border_t_1() + .border_color(rgb(colors::HAIRLINE)) + .child(update_row(IconName::Info, "Current Version", APP_VERSION, "Cargo package version")) + .child(update_row(IconName::GitHub, "Build Revision", BUILD_REVISION, "Git revision")) + .child(update_row( + IconName::Globe, + "Release Target", + release_target(), + "Platform and architecture", + )) + .child(update_row( + IconName::File, + "Manifest API", + RELEASE_MANIFEST_PATH, + format!("KV namespace: {RELEASE_MANIFEST_CACHE}"), + )) + .child(update_row( + IconName::File, + "Signature API", + signature_query_path(), + "Targeted release signature document", + )) + .child(update_row( + IconName::CircleCheck, + "Artifact Integrity", + RELEASE_INTEGRITY, + "Release manifest requires package hash and signature", + )) + .into_any_element() +} + +fn update_row( + icon: IconName, + label: &'static str, + value: impl Into, + detail: impl Into, +) -> AnyElement { + let value = value.into(); + let detail = detail.into(); + + div() + .py_3() + .border_b_1() + .border_color(rgb(colors::HAIRLINE)) + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .items_center() + .gap_3() + .child(div().text_color(rgb(colors::MUTED_SOFT)).child(icon)) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .truncate() + .text_color(rgb(colors::INK)) + .child(label), + ) + .child( + div().text_xs().truncate().text_color(rgb(colors::MUTED)).child(detail), + ), + ), + ) + .child( + div() + .max_w(px(360.0)) + .truncate() + .text_sm() + .font_semibold() + .text_color(rgb(colors::INK)) + .child(value), + ) + .into_any_element() +} + +fn release_target() -> String { + format!("{} / {}", env::consts::OS, env::consts::ARCH) +} + +fn signature_query_path() -> String { + format!( + "{RELEASE_SIGNATURE_PATH}?platform={}&architecture={}&version={APP_VERSION}", + env::consts::OS, + env::consts::ARCH + ) +} diff --git a/crates/ely_browser_core/src/navigation.rs b/crates/ely_browser_core/src/navigation.rs index 675dbc3..70aa81b 100644 --- a/crates/ely_browser_core/src/navigation.rs +++ b/crates/ely_browser_core/src/navigation.rs @@ -41,6 +41,7 @@ fn internal_page_title(url: &str) -> Option<&'static str> { "ely://settings/plugins" => Some("Plugin Settings"), "ely://settings/profiles" => Some("Profile Settings"), "ely://settings/sync" => Some("Sync Settings"), + "ely://settings/updates" => Some("Update Settings"), "ely://sync/status" => Some("Sync Status"), _ => None, } @@ -259,6 +260,9 @@ fn settings_page_route(query: &str) -> Option<&'static str> { Some("ely://settings/shortcuts") } "sync" | "sync settings" => Some("ely://settings/sync"), + "update" | "updates" | "auto update" | "auto updates" | "release" | "releases" => { + Some("ely://settings/updates") + } "profile" | "profiles" | "profile settings" | "profiles settings" => { Some("ely://settings/profiles") } diff --git a/crates/ely_browser_core/tests/settings_routes.rs b/crates/ely_browser_core/tests/settings_routes.rs index 5935911..0d5d237 100644 --- a/crates/ely_browser_core/tests/settings_routes.rs +++ b/crates/ely_browser_core/tests/settings_routes.rs @@ -191,3 +191,24 @@ fn settings_scoped_search_opens_site_permissions_page() -> Result<(), Box Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + + core.set_command_query("@settings updates"); + let intent = core.submit_command()?; + let active_tab = core.active_tab()?; + + assert_eq!( + intent, + Some(CommandIntent::ScopedSearch { + scope: CommandScope::Settings, + query: "updates".to_string(), + }) + ); + assert_eq!(active_tab.title(), "Update Settings"); + assert_eq!(active_tab.url().as_str(), "ely://settings/updates"); + assert_eq!(core.snapshot()?.command_query, ""); + Ok(()) +}