diff --git a/backend/src/context/rpc.rs b/backend/src/context/rpc.rs index 2a015cb52..3f983ef86 100644 --- a/backend/src/context/rpc.rs +++ b/backend/src/context/rpc.rs @@ -308,14 +308,12 @@ impl RpcContext { let main = match status.main { MainStatus::BackingUp { started, .. } => { if let Some(_) = started { - MainStatus::Starting { restarting: false } + MainStatus::Starting } else { MainStatus::Stopped } } - MainStatus::Running { .. } => { - MainStatus::Starting { restarting: false } - } + MainStatus::Running { .. } => MainStatus::Starting, a => a.clone(), }; let new_package = PackageDataEntry::Installed { diff --git a/backend/src/control.rs b/backend/src/control.rs index 1922fdc81..794afc64a 100644 --- a/backend/src/control.rs +++ b/backend/src/control.rs @@ -67,10 +67,7 @@ pub async fn start(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<( let mut tx = db.begin().await?; let receipts = StartReceipts::new(&mut tx, &id).await?; let version = receipts.version.get(&mut tx).await?; - receipts - .status - .set(&mut tx, MainStatus::Starting { restarting: false }) - .await?; + receipts.status.set(&mut tx, MainStatus::Starting).await?; heal_all_dependents_transitive(&ctx, &mut tx, &id, &receipts.dependency_receipt).await?; tx.commit().await?; diff --git a/backend/src/db/model.rs b/backend/src/db/model.rs index 7a6fe1f7f..013fae2f2 100644 --- a/backend/src/db/model.rs +++ b/backend/src/db/model.rs @@ -44,7 +44,7 @@ impl Database { server_info: ServerInfo { id: account.server_id.clone(), version: Current::new().semver().into(), - hostname: Some(account.hostname.no_dot_host_name()), + hostname: account.hostname.no_dot_host_name(), last_backup: None, last_wifi_region: None, eos_version_compat: Current::new().compat().clone(), @@ -98,7 +98,7 @@ impl DatabaseModel { #[serde(rename_all = "kebab-case")] pub struct ServerInfo { pub id: String, - pub hostname: Option, + pub hostname: String, pub version: Version, pub last_backup: Option>, /// Used in the wifi to determine the region to set the system to diff --git a/backend/src/manager/manager_container.rs b/backend/src/manager/manager_container.rs index afce929f0..0578a4fd6 100644 --- a/backend/src/manager/manager_container.rs +++ b/backend/src/manager/manager_container.rs @@ -161,12 +161,7 @@ async fn save_state( set_status(&mut db, &seed.manifest, &MainStatus::Stopping).await } (None, StartStop::Stop, StartStop::Start) => { - set_status( - &mut db, - &seed.manifest, - &MainStatus::Starting { restarting: false }, - ) - .await + set_status(&mut db, &seed.manifest, &MainStatus::Starting).await } (None, StartStop::Stop, StartStop::Stop) => { set_status(&mut db, &seed.manifest, &MainStatus::Stopped).await diff --git a/backend/src/manager/sync.rs b/backend/src/manager/sync.rs deleted file mode 100644 index 41a6445c5..000000000 --- a/backend/src/manager/sync.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::collections::BTreeMap; -use std::time::Duration; - -use chrono::Utc; - -use super::{pause, resume, start, stop, ManagerSharedState, Status}; -use crate::status::MainStatus; -use crate::Error; - -/// Allocates a db handle. DO NOT CALL with a db handle already in scope -async fn synchronize_once(shared: &ManagerSharedState) -> Result { - let mut db = shared.seed.ctx.db.handle(); - let mut status = crate::db::DatabaseModel::new() - .package_data() - .idx_model(&shared.seed.manifest.id) - .expect(&mut db) - .await? - .installed() - .expect(&mut db) - .await? - .status() - .main() - .get_mut(&mut db) - .await?; - let manager_status = *shared.status.1.borrow(); - match manager_status { - Status::Stopped => match &mut *status { - MainStatus::Stopped => (), - MainStatus::Stopping => { - *status = MainStatus::Stopped; - } - MainStatus::Restarting => { - *status = MainStatus::Starting { restarting: true }; - } - MainStatus::Starting { .. } => { - start(shared).await?; - } - MainStatus::Running { started, .. } => { - *started = Utc::now(); - start(shared).await?; - } - MainStatus::BackingUp { .. } => (), - }, - Status::Starting => match *status { - MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => { - stop(shared).await?; - } - MainStatus::Starting { .. } | MainStatus::Running { .. } => (), - MainStatus::BackingUp { .. } => { - pause(shared).await?; - } - }, - Status::Running => match *status { - MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => { - stop(shared).await?; - } - MainStatus::Starting { .. } => { - *status = MainStatus::Running { - started: Utc::now(), - health: BTreeMap::new(), - }; - } - MainStatus::Running { .. } => (), - MainStatus::BackingUp { .. } => { - pause(shared).await?; - } - }, - Status::Paused => match *status { - MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => { - stop(shared).await?; - } - MainStatus::Starting { .. } | MainStatus::Running { .. } => { - resume(shared).await?; - } - MainStatus::BackingUp { .. } => (), - }, - Status::Shutdown => (), - } - status.save(&mut db).await?; - Ok(manager_status) -} - -pub async fn synchronizer(shared: &ManagerSharedState) { - let mut status_recv = shared.status.0.subscribe(); - loop { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(5)) => (), - _ = shared.synchronize_now.notified() => (), - _ = status_recv.changed() => (), - } - let status = match synchronize_once(shared).await { - Err(e) => { - tracing::error!( - "Synchronizer for {}@{} failed: {}", - shared.seed.manifest.id, - shared.seed.manifest.version, - e - ); - tracing::debug!("{:?}", e); - continue; - } - Ok(status) => status, - }; - tracing::trace!("{} status synchronized", shared.seed.manifest.id); - shared.synchronized.notify_waiters(); - match status { - Status::Shutdown => { - break; - } - _ => (), - } - } -} diff --git a/backend/src/procedure/js_scripts.rs b/backend/src/procedure/js_scripts.rs index 05d1c9bf1..82f51a781 100644 --- a/backend/src/procedure/js_scripts.rs +++ b/backend/src/procedure/js_scripts.rs @@ -554,181 +554,182 @@ mod tests { } })) .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-deep-dir".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - None, - ) - .await - .unwrap() + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-deep-dir".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(serde_json::json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); -} -#[tokio::test] -async fn js_action_test_deep_dir_escape() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + None, + ) + .await + .unwrap() + .unwrap(); + } + #[tokio::test] + async fn js_action_test_deep_dir_escape() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-deep-dir-escape".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(serde_json::json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-deep-dir-escape".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - None, - ) - .await - .unwrap() + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + None, + ) + .await + .unwrap() + .unwrap(); + } + #[tokio::test] + async fn js_action_test_zero_dir() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-zero-dir".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(serde_json::json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); -} -#[tokio::test] -async fn js_action_test_zero_dir() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + None, + ) + .await + .unwrap() + .unwrap(); + } + #[tokio::test] + async fn js_action_test_read_dir() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-read-dir".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(serde_json::json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-zero-dir".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - None, - ) - .await - .unwrap() - .unwrap(); -} -#[tokio::test] -async fn js_action_test_read_dir() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-read-dir".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ProcessGroupId(0), + None, + None, + ) + .await + .unwrap() + .unwrap(); + } #[tokio::test] async fn js_action_test_deep_dir() { diff --git a/backend/src/status/mod.rs b/backend/src/status/mod.rs index a55d674b6..073ea4e88 100644 --- a/backend/src/status/mod.rs +++ b/backend/src/status/mod.rs @@ -26,9 +26,7 @@ pub enum MainStatus { Stopped, Restarting, Stopping, - Starting { - restarting: bool, - }, + Starting, Running { started: DateTime, health: BTreeMap, diff --git a/backend/src/version/mod.rs b/backend/src/version/mod.rs index 7425e522f..07bb05459 100644 --- a/backend/src/version/mod.rs +++ b/backend/src/version/mod.rs @@ -9,40 +9,17 @@ use sqlx::PgPool; use crate::init::InitReceipts; use crate::Error; -mod v0_3_0; -mod v0_3_0_1; -mod v0_3_0_2; -mod v0_3_0_3; -mod v0_3_1; -mod v0_3_1_1; -mod v0_3_1_2; -mod v0_3_2; -mod v0_3_2_1; -mod v0_3_3; -mod v0_3_4; -mod v0_3_4_1; -mod v0_3_4_2; mod v0_3_4_3; +mod v0_4_0; -pub type Current = v0_3_4_3::Version; +pub type Current = v0_4_0::Version; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] enum Version { - V0_3_0(Wrapper), - V0_3_0_1(Wrapper), - V0_3_0_2(Wrapper), - V0_3_0_3(Wrapper), - V0_3_1(Wrapper), - V0_3_1_1(Wrapper), - V0_3_1_2(Wrapper), - V0_3_2(Wrapper), - V0_3_2_1(Wrapper), - V0_3_3(Wrapper), - V0_3_4(Wrapper), - V0_3_4_1(Wrapper), - V0_3_4_2(Wrapper), + LT0_3_4_3(LTWrapper), V0_3_4_3(Wrapper), + V0_4_0(Wrapper), Other(emver::Version), } @@ -58,20 +35,9 @@ impl Version { #[cfg(test)] fn as_sem_ver(&self) -> emver::Version { match self { - Version::V0_3_0(Wrapper(x)) => x.semver(), - Version::V0_3_0_1(Wrapper(x)) => x.semver(), - Version::V0_3_0_2(Wrapper(x)) => x.semver(), - Version::V0_3_0_3(Wrapper(x)) => x.semver(), - Version::V0_3_1(Wrapper(x)) => x.semver(), - Version::V0_3_1_1(Wrapper(x)) => x.semver(), - Version::V0_3_1_2(Wrapper(x)) => x.semver(), - Version::V0_3_2(Wrapper(x)) => x.semver(), - Version::V0_3_2_1(Wrapper(x)) => x.semver(), - Version::V0_3_3(Wrapper(x)) => x.semver(), - Version::V0_3_4(Wrapper(x)) => x.semver(), - Version::V0_3_4_1(Wrapper(x)) => x.semver(), - Version::V0_3_4_2(Wrapper(x)) => x.semver(), + Version::LT0_3_4_3(LTWrapper(x)) => x.semver(), Version::V0_3_4_3(Wrapper(x)) => x.semver(), + Version::V0_4_0(Wrapper(x)) => x.semver(), Version::Other(x) => x.clone(), } } @@ -177,6 +143,32 @@ where Ok(()) } } + +#[derive(Debug, Clone)] +struct LTWrapper(T, emver::Version); +impl serde::Serialize for LTWrapper +where + T: VersionT, +{ + fn serialize(&self, serializer: S) -> Result { + self.0.semver().serialize(serializer) + } +} +impl<'de, T> serde::Deserialize<'de> for LTWrapper +where + T: VersionT, +{ + fn deserialize>(deserializer: D) -> Result { + let v = crate::util::Version::deserialize(deserializer)?; + let version = T::new(); + if *v < version.semver() { + Ok(Self(version, v.into_version())) + } else { + Err(serde::de::Error::custom("Mismatched Version")) + } + } +} + #[derive(Debug, Clone)] struct Wrapper(T); impl serde::Serialize for Wrapper @@ -209,62 +201,20 @@ pub async fn init( ) -> Result<(), Error> { let version = Version::from_util_version(receipts.server_version.get(db).await?); match version { - Version::V0_3_0(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_0_1(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_0_2(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_0_3(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_1(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_1_1(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_1_2(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_2(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_2_1(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_3(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_4(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_4_1(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? - } - Version::V0_3_4_2(v) => { - v.0.migrate_to(&Current::new(), db, secrets, receipts) - .await? + Version::LT0_3_4_3(_) => { + return Err(Error::new( + eyre!("Cannot migrate from pre-0.3.4. Please update to v0.3.4 first."), + crate::ErrorKind::MigrationFailed, + )); } Version::V0_3_4_3(v) => { v.0.migrate_to(&Current::new(), db, secrets, receipts) .await? } + Version::V0_4_0(v) => { + v.0.migrate_to(&Current::new(), db, secrets, receipts) + .await? + } Version::Other(_) => { return Err(Error::new( eyre!("Cannot downgrade"), @@ -297,19 +247,6 @@ mod tests { fn versions() -> impl Strategy { prop_oneof![ - Just(Version::V0_3_0(Wrapper(v0_3_0::Version::new()))), - Just(Version::V0_3_0_1(Wrapper(v0_3_0_1::Version::new()))), - Just(Version::V0_3_0_2(Wrapper(v0_3_0_2::Version::new()))), - Just(Version::V0_3_0_3(Wrapper(v0_3_0_3::Version::new()))), - Just(Version::V0_3_1(Wrapper(v0_3_1::Version::new()))), - Just(Version::V0_3_1_1(Wrapper(v0_3_1_1::Version::new()))), - Just(Version::V0_3_1_2(Wrapper(v0_3_1_2::Version::new()))), - Just(Version::V0_3_2(Wrapper(v0_3_2::Version::new()))), - Just(Version::V0_3_2_1(Wrapper(v0_3_2_1::Version::new()))), - Just(Version::V0_3_3(Wrapper(v0_3_3::Version::new()))), - Just(Version::V0_3_4(Wrapper(v0_3_4::Version::new()))), - Just(Version::V0_3_4_1(Wrapper(v0_3_4_1::Version::new()))), - Just(Version::V0_3_4_2(Wrapper(v0_3_4_2::Version::new()))), Just(Version::V0_3_4_3(Wrapper(v0_3_4_3::Version::new()))), em_version().prop_map(Version::Other), ] diff --git a/backend/src/version/v0_4_0.rs b/backend/src/version/v0_4_0.rs new file mode 100644 index 000000000..dcacef822 --- /dev/null +++ b/backend/src/version/v0_4_0.rs @@ -0,0 +1,36 @@ +use async_trait::async_trait; +use emver::VersionRange; +use lazy_static::lazy_static; + +use super::*; + +const V0_4_0: emver::Version = emver::Version::new(0, 4, 0, 0); +lazy_static! { + pub static ref V0_4_0_COMPAT: VersionRange = VersionRange::Conj( + Box::new(VersionRange::Anchor(emver::GTE, V0_4_0)), + Box::new(VersionRange::Anchor(emver::LTE, Current::new().semver())), + ); +} + +#[derive(Clone, Debug)] +pub struct Version; + +#[async_trait] +impl VersionT for Version { + type Previous = v0_3_4::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> emver::Version { + V0_4_0 + } + fn compat(&self) -> &'static VersionRange { + &*V0_4_0_COMPAT + } + async fn up(&self, db: &mut Db, secrets: &PgPool) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, db: &mut Db, secrets: &PgPool) -> Result<(), Error> { + Ok(()) + } +} diff --git a/frontend/patchdb-ui-seed.json b/frontend/patchdb-ui-seed.json index bf6ca2c91..04cdea674 100644 --- a/frontend/patchdb-ui-seed.json +++ b/frontend/patchdb-ui-seed.json @@ -1,6 +1,6 @@ { "name": null, - "ack-welcome": "0.3.4.3", + "ack-welcome": "0.4.0", "marketplace": { "selected-url": "https://registry.start9.com/", "known-hosts": { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index f9288494c..8cd19f502 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { AppShowPage } from './app-show.page' -import { EmverPipesModule, ResponsiveColModule } from '@start9labs/shared' +import { EmverPipesModule, ResponsiveColModule, SharedPipesModule } from '@start9labs/shared' import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module' import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module' @@ -16,7 +16,6 @@ import { AppShowMenuComponent } from './components/app-show-menu/app-show-menu.c import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component' import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component' import { HealthColorPipe } from './pipes/health-color.pipe' -import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe' import { ToButtonsPipe } from './pipes/to-buttons.pipe' import { ToDependenciesPipe } from './pipes/to-dependencies.pipe' import { ToStatusPipe } from './pipes/to-status.pipe' @@ -34,7 +33,6 @@ const routes: Routes = [ AppShowPage, HealthColorPipe, ProgressDataPipe, - ToHealthChecksPipe, ToButtonsPipe, ToDependenciesPipe, ToStatusPipe, @@ -56,6 +54,7 @@ const routes: Routes = [ LaunchablePipeModule, UiPipeModule, ResponsiveColModule, + SharedPipesModule, ], }) export class AppShowPageModule {} diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index bc57dd27b..4f825ccb6 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -30,7 +30,7 @@ { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html index 9b4585728..1bc50c0fa 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html @@ -1,92 +1,82 @@ - - - Health Checks - - - - - - - - - - -

- {{ pkg.manifest['health-checks'][health.key].name }} -

- -

- {{ result | titlecase }} - ... - - {{ $any(health.value).error }} - - - {{ $any(health.value).message }} - - : - {{ - pkg.manifest['health-checks'][health.key]['success-message'] - }} - -

-
-
-
- - - - -

- {{ pkg.manifest['health-checks'][health.key].name }} -

-

Awaiting result...

-
-
-
-
- - - - - - + + Health Checks + + + + + + + + + - - +

+ {{ check.name }} +

+ +

+ {{ result | titlecase }} + ... + + {{ $any(check).error }} + + + {{ $any(check).message }} + + : + {{ $any(check).message }} + +

+
-
-
+
+ + + + +

+ {{ check.name }} +

+

Awaiting result...

+
+
+
+ + + + + + + + + + + + diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts index 5db1ab1ca..e6cb90951 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { PatchDB } from 'patch-db-client' +import { map } from 'rxjs' import { ConnectionService } from 'src/app/services/connection.service' -import { - HealthResult, - PackageDataEntry, -} from 'src/app/services/patch-db/data-model' +import { DataModel, HealthResult } from 'src/app/services/patch-db/data-model' +import { isEmptyObject } from '@start9labs/shared' @Component({ selector: 'app-show-health-checks', @@ -12,14 +12,26 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppShowHealthChecksComponent { - @Input() - pkg!: PackageDataEntry - - HealthResult = HealthResult + @Input() pkgId!: string readonly connected$ = this.connectionService.connected$ - constructor(private readonly connectionService: ConnectionService) {} + get healthChecks$() { + return this.patch + .watch$('package-data', this.pkgId, 'installed', 'status', 'main') + .pipe( + map(main => { + if (main.status !== 'running' || isEmptyObject(main.health)) + return null + return Object.values(main.health) + }), + ) + } + + constructor( + private readonly connectionService: ConnectionService, + private readonly patch: PatchDB, + ) {} isLoading(result: HealthResult): boolean { return result === HealthResult.Starting || result === HealthResult.Loading diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts deleted file mode 100644 index 8ba9bd4f3..000000000 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { - DataModel, - HealthCheckResult, - PackageDataEntry, - PackageMainStatus, -} from 'src/app/services/patch-db/data-model' -import { isEmptyObject } from '@start9labs/shared' -import { map, startWith } from 'rxjs/operators' -import { PatchDB } from 'patch-db-client' -import { Observable } from 'rxjs' - -@Pipe({ - name: 'toHealthChecks', -}) -export class ToHealthChecksPipe implements PipeTransform { - constructor(private readonly patch: PatchDB) {} - - transform( - pkg: PackageDataEntry, - ): Observable> | null { - const healthChecks = Object.keys(pkg.manifest['health-checks']).reduce( - (obj, key) => ({ ...obj, [key]: null }), - {}, - ) - - const healthChecks$ = this.patch - .watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main') - .pipe( - map(main => { - // Question: is this ok or do we have to use Object.keys - // to maintain order and the keys initially present in pkg? - return main.status === PackageMainStatus.Running && - !isEmptyObject(main.health) - ? main.health - : healthChecks - }), - startWith(healthChecks), - ) - - return isEmptyObject(healthChecks) ? null : healthChecks$ - } -} diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts index 0ab90d335..2bd2039a8 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -87,7 +87,6 @@ export module Mock { 'shm-size': '', 'sigterm-timeout': '1ms', }, - 'health-checks': {}, config: { get: null, set: null, @@ -382,7 +381,6 @@ export module Mock { 'shm-size': '', 'sigterm-timeout': '10000µs', }, - 'health-checks': {}, config: { get: null, set: null, @@ -535,7 +533,6 @@ export module Mock { 'shm-size': '', 'sigterm-timeout': '1m', }, - 'health-checks': {}, config: { get: {} as any, set: {} as any }, volumes: {}, 'min-os-version': '0.2.12', diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index db7810d28..cbe07ed0a 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -727,7 +727,18 @@ export class MockApiService extends ApiService { await pauseFor(2000) setTimeout(async () => { - const patch2 = [ + if (params.id !== 'bitcoind') { + const patch2 = [ + { + op: PatchOp.REPLACE, + path: path + '/health', + value: {}, + }, + ] + this.mockRevision(patch2) + } + + const patch3 = [ { op: PatchOp.REPLACE, path: path + '/status', @@ -739,52 +750,7 @@ export class MockApiService extends ApiService { value: new Date().toISOString(), }, ] - this.mockRevision(patch2) - - const patch3 = [ - { - op: PatchOp.REPLACE, - path: path + '/health', - value: { - 'ephemeral-health-check': { - result: 'starting', - }, - 'unnecessary-health-check': { - result: 'disabled', - }, - }, - }, - ] this.mockRevision(patch3) - - await pauseFor(2000) - - const patch4 = [ - { - op: PatchOp.REPLACE, - path: path + '/health', - value: { - 'ephemeral-health-check': { - result: 'starting', - }, - 'unnecessary-health-check': { - result: 'disabled', - }, - 'chain-state': { - result: 'loading', - message: 'Bitcoin is syncing from genesis', - }, - 'p2p-interface': { - result: 'success', - }, - 'rpc-interface': { - result: 'failure', - error: 'RPC interface unreachable.', - }, - }, - }, - ] - this.mockRevision(patch4) }, 2000) const originalPatch = [ @@ -896,11 +862,6 @@ export class MockApiService extends ApiService { path: path + '/status', value: PackageMainStatus.Stopping, }, - { - op: PatchOp.REPLACE, - path: path + '/health', - value: {}, - }, ] return this.withRevision(patch) diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 497fc7344..5f76884e4 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -127,24 +127,6 @@ export const mockPatchData: DataModel = { 'shm-size': '', 'sigterm-timeout': '.49m', }, - 'health-checks': { - 'chain-state': { - name: 'Chain State', - }, - 'ephemeral-health-check': { - name: 'Ephemeral Health Check', - }, - 'p2p-interface': { - name: 'P2P Interface', - 'success-message': 'the health check ran succesfully', - }, - 'rpc-interface': { - name: 'RPC Interface', - }, - 'unnecessary-health-check': { - name: 'Unneccessary Health Check', - }, - } as any, config: { get: {}, set: {}, @@ -243,9 +225,12 @@ export const mockPatchData: DataModel = { 'Your reason for re-syncing. Why are you doing this?', nullable: false, masked: false, - copyable: false, pattern: '^[a-zA-Z]+$', 'pattern-description': 'Must contain only letters.', + placeholder: null, + textarea: false, + warning: null, + default: null, }, name: { type: 'string', @@ -253,14 +238,19 @@ export const mockPatchData: DataModel = { description: 'Tell the class your name.', nullable: true, masked: false, - copyable: false, warning: 'You may loose all your money by providing your name.', + placeholder: null, + pattern: null, + 'pattern-description': null, + textarea: false, + default: null, }, notifications: { name: 'Notification Preferences', type: 'list', subtype: 'enum', description: 'how you want to be notified', + warning: null, range: '[1,3]', default: ['email'], spec: { @@ -282,6 +272,9 @@ export const mockPatchData: DataModel = { default: 100, range: '[0, 9999]', integral: true, + units: null, + placeholder: null, + warning: null, }, 'top-speed': { type: 'number', @@ -291,6 +284,9 @@ export const mockPatchData: DataModel = { range: '[-1000, 1000]', integral: false, units: 'm/s', + placeholder: null, + warning: null, + default: null, }, testnet: { name: 'Testnet', @@ -318,22 +314,33 @@ export const mockPatchData: DataModel = { name: 'Emergency Contact', type: 'object', description: 'The person to contact in case of emergency.', + warning: null, spec: { name: { type: 'string', name: 'Name', + description: null, nullable: false, masked: false, - copyable: false, pattern: '^[a-zA-Z]+$', 'pattern-description': 'Must contain only letters.', + placeholder: null, + textarea: false, + warning: null, + default: null, }, email: { type: 'string', name: 'Email', + description: null, nullable: false, masked: false, - copyable: true, + placeholder: null, + pattern: null, + 'pattern-description': null, + textarea: false, + warning: null, + default: null, }, }, }, @@ -351,7 +358,7 @@ export const mockPatchData: DataModel = { pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$', 'pattern-description': 'Must be a valid IP address', masked: false, - copyable: false, + placeholder: null, }, }, bitcoinNode: { @@ -375,7 +382,12 @@ export const mockPatchData: DataModel = { description: 'the lan address', nullable: true, masked: false, - copyable: false, + placeholder: null, + pattern: null, + 'pattern-description': null, + textarea: false, + warning: null, + default: null, }, }, external: { @@ -388,7 +400,9 @@ export const mockPatchData: DataModel = { pattern: '.*', 'pattern-description': 'anything', masked: false, - copyable: true, + placeholder: null, + textarea: false, + warning: null, }, }, }, @@ -411,20 +425,26 @@ export const mockPatchData: DataModel = { started: '2021-06-14T20:49:17.774Z', health: { 'ephemeral-health-check': { + name: 'Ephemeral Health Check', result: HealthResult.Starting, }, 'chain-state': { + name: 'Chain State', result: HealthResult.Loading, message: 'Bitcoin is syncing from genesis', }, 'p2p-interface': { + name: 'P2P Interface', result: HealthResult.Success, + message: 'the health check ran successfully', }, 'rpc-interface': { + name: 'RPC Interface', result: HealthResult.Failure, error: 'RPC interface unreachable.', }, 'unnecessary-health-check': { + name: 'Totally Unnecessary', result: HealthResult.Disabled, }, }, @@ -461,7 +481,7 @@ export const mockPatchData: DataModel = { manifest: { id: 'lnd', title: 'Lightning Network Daemon', - version: '0.11.0', + version: '0.11.1', description: { short: 'A bolt spec compliant client.', long: 'More info about LND. More info about LND. More info about LND.', @@ -501,7 +521,6 @@ export const mockPatchData: DataModel = { 'shm-size': '', 'sigterm-timeout': '0.5s', }, - 'health-checks': {}, config: { get: null, set: null, diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index 2d45dee27..05e6c1a93 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -155,10 +155,6 @@ export interface Manifest extends MarketplaceManifest { scripts: string // path to scripts folder } main: ActionImpl - 'health-checks': Record< - string, - ActionImpl & { name: string; 'success-message': string | null } - > config: ConfigActions | null volumes: Record 'min-os-version': string @@ -295,7 +291,6 @@ export interface MainStatusStopping { export interface MainStatusStarting { status: PackageMainStatus.Starting - restarting: boolean } export interface MainStatusRunning { @@ -322,12 +317,13 @@ export enum PackageMainStatus { Restarting = 'restarting', } -export type HealthCheckResult = +export type HealthCheckResult = { name: string } & ( | HealthCheckResultStarting | HealthCheckResultLoading | HealthCheckResultDisabled | HealthCheckResultSuccess | HealthCheckResultFailure +) export enum HealthResult { Starting = 'starting', @@ -347,6 +343,7 @@ export interface HealthCheckResultDisabled { export interface HealthCheckResultSuccess { result: HealthResult.Success + message: string } export interface HealthCheckResultLoading { diff --git a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts index 58fab27bf..f979d6a2c 100644 --- a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -1,5 +1,6 @@ import { isEmptyObject } from '@start9labs/shared' import { + InstalledPackageDataEntry, MainStatusStarting, PackageDataEntry, PackageMainStatus, @@ -8,42 +9,39 @@ import { } from 'src/app/services/patch-db/data-model' export interface PackageStatus { - primary: PrimaryStatus + primary: PrimaryStatus | PackageState | PackageMainStatus dependency: DependencyStatus | null health: HealthStatus | null } export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus { - let primary: PrimaryStatus + let primary: PrimaryStatus | PackageState | PackageMainStatus let dependency: DependencyStatus | null = null let health: HealthStatus | null = null - const hasHealthChecks = !isEmptyObject(pkg.manifest['health-checks']) if (pkg.state === PackageState.Installed && pkg.installed) { primary = getPrimaryStatus(pkg.installed.status) - dependency = getDependencyStatus(pkg) - health = getHealthStatus(pkg.installed.status, hasHealthChecks) + dependency = getDependencyStatus(pkg.installed) + health = getHealthStatus(pkg.installed.status) } else { - primary = pkg.state as string as PrimaryStatus + primary = pkg.state } return { primary, dependency, health } } -function getPrimaryStatus(status: Status): PrimaryStatus { +function getPrimaryStatus(status: Status): PrimaryStatus | PackageMainStatus { if (!status.configured) { return PrimaryStatus.NeedsConfig - } else if ((status.main as MainStatusStarting).restarting) { - return PrimaryStatus.Restarting } else { - return status.main.status as any as PrimaryStatus + return status.main.status } } -function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null { - const installed = pkg.installed - if (!installed || isEmptyObject(installed['current-dependencies'])) - return null +function getDependencyStatus( + installed: InstalledPackageDataEntry, +): DependencyStatus | null { + if (isEmptyObject(installed['current-dependencies'])) return null const depErrors = installed.status['dependency-errors'] const depIds = Object.keys(depErrors).filter(key => !!depErrors[key]) @@ -51,11 +49,8 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null { return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied } -function getHealthStatus( - status: Status, - hasHealthChecks: boolean, -): HealthStatus | null { - if (status.main.status !== PackageMainStatus.Running || !status.main.health) { +function getHealthStatus(status: Status): HealthStatus | null { + if (status.main.status !== PackageMainStatus.Running) { return null } @@ -65,10 +60,6 @@ function getHealthStatus( return HealthStatus.Failure } - if (!values.length && hasHealthChecks) { - return HealthStatus.Waiting - } - if (values.some(h => h.result === 'loading')) { return HealthStatus.Loading }