Integration/0.2.13 (#324)

* updates to 8.10.4, adjusts dependencies, adds license info feature

* add toJSON

* add licesne info to services

* remove mocks

* adds license info to available show

* prepare upgrade messaging

* better welcome message

* update backend versioning to 0.2.13

* add version migration file

* update ui build scripts

* update eos image

* update eos image with embassy

* add migration files

* update welcome page

* explicity add migration files

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
Matt Hill
2021-05-19 11:46:37 -06:00
committed by GitHub
parent 7509c3a91e
commit 8f9111ce3d
34 changed files with 1867 additions and 1433 deletions

View File

@@ -33,5 +33,5 @@ database:
database: "start9_agent.sqlite3" database: "start9_agent.sqlite3"
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10" poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
app-mgr-version-spec: "=0.2.12" app-mgr-version-spec: "=0.2.13"
#analytics: UA-YOURCODE #analytics: UA-YOURCODE

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -1,5 +1,5 @@
name: ambassador-agent name: ambassador-agent
version: 0.2.12 version: 0.2.13
default-extensions: default-extensions:
- NoImplicitPrelude - NoImplicitPrelude
@@ -182,3 +182,4 @@ executables:
condition: flag(library-only) condition: flag(library-only)
- condition: false - condition: false
other-modules: Paths_ambassador_agent other-modules: Paths_ambassador_agent
extra-source-files: ./migrations/*

View File

@@ -154,8 +154,7 @@ getAvailableAppsLogic :: ( Has (Reader AgentCtx) sig m
getAvailableAppsLogic = do getAvailableAppsLogic = do
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
let installCache = inspect SInstalling jobCache let installCache = inspect SInstalling jobCache
(Reg.AppManifestRes apps, serverApps) <- LAsync.concurrently Reg.getAppManifest (Reg.AppIndexRes apps, serverApps) <- LAsync.concurrently Reg.getAppIndex (AppMgr2.list [AppMgr2.flags|-s -d|])
(AppMgr2.list [AppMgr2.flags|-s -d|])
let remapped = remapAppMgrInfo jobCache serverApps let remapped = remapAppMgrInfo jobCache serverApps
pure $ foreach apps $ \app@StoreApp { storeAppId } -> pure $ foreach apps $ \app@StoreApp { storeAppId } ->
let installing = let installing =
@@ -183,8 +182,9 @@ getAvailableAppByIdLogic appId = do
let storeAppId' = storeAppId let storeAppId' = storeAppId
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
let installCache = inspect SInstalling jobCache let installCache = inspect SInstalling jobCache
(Reg.AppManifestRes storeApps, serverApps) <- LAsync.concurrently Reg.getAppManifest ((Reg.AppIndexRes storeApps, serverApps), AppManifest.AppManifest { appManifestLicenseName, appManifestLicenseLink }) <-
(AppMgr2.list [AppMgr2.flags|-s -d|]) LAsync.concurrently (LAsync.concurrently Reg.getAppIndex (AppMgr2.list [AppMgr2.flags|-s -d|]))
(Reg.getAppManifest appId)
StoreApp {..} <- pure (find ((== appId) . storeAppId) storeApps) `orThrowM` NotFoundE "appId" (show appId) StoreApp {..} <- pure (find ((== appId) . storeAppId) storeApps) `orThrowM` NotFoundE "appId" (show appId)
let remapped = remapAppMgrInfo jobCache serverApps let remapped = remapAppMgrInfo jobCache serverApps
let installingInfo = let installingInfo =
@@ -213,6 +213,8 @@ getAvailableAppByIdLogic appId = do
appId appId
storeAppTitle storeAppTitle
(storeIconUrl appId (storeAppVersionInfoVersion $ extract storeAppVersions)) (storeIconUrl appId (storeAppVersionInfoVersion $ extract storeAppVersions))
, appAvailableFullLicenseName = appManifestLicenseName
, appAvailableFullLicenseLink = appManifestLicenseLink
, appAvailableFullInstallInfo = installingInfo , appAvailableFullInstallInfo = installingInfo
, appAvailableFullVersionLatest = storeAppVersionInfoVersion latest , appAvailableFullVersionLatest = storeAppVersionInfoVersion latest
, appAvailableFullDescriptionShort = storeAppDescriptionShort , appAvailableFullDescriptionShort = storeAppDescriptionShort
@@ -303,6 +305,8 @@ getInstalledAppByIdLogic appId = do
backupTime <- lift $ LAsync.wait backupTime' backupTime <- lift $ LAsync.wait backupTime'
hoistMaybe $ HM.lookup appId installCache <&> \(StoreApp {..}, StoreAppVersionInfo {..}) -> AppInstalledFull hoistMaybe $ HM.lookup appId installCache <&> \(StoreApp {..}, StoreAppVersionInfo {..}) -> AppInstalledFull
{ appInstalledFullBase = AppBase appId storeAppTitle (iconUrl appId storeAppVersionInfoVersion) { appInstalledFullBase = AppBase appId storeAppTitle (iconUrl appId storeAppVersionInfoVersion)
, appInstalledFullLicenseName = Nothing
, appInstalledFullLicenseLink = Nothing
, appInstalledFullStatus = AppStatusTmp Installing , appInstalledFullStatus = AppStatusTmp Installing
, appInstalledFullVersionInstalled = storeAppVersionInfoVersion , appInstalledFullVersionInstalled = storeAppVersionInfoVersion
, appInstalledFullInstructions = Nothing , appInstalledFullInstructions = Nothing
@@ -319,7 +323,7 @@ getInstalledAppByIdLogic appId = do
} }
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|] serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
let remapped = remapAppMgrInfo jobCache serverApps let remapped = remapAppMgrInfo jobCache serverApps
appManifestFetchCached <- cached Reg.getAppManifest appManifestFetchCached <- cached Reg.getAppIndex
let let
installed = do installed = do
(status, version, AppMgr2.InfoRes {..}) <- hoistMaybe (HM.lookup appId remapped) (status, version, AppMgr2.InfoRes {..}) <- hoistMaybe (HM.lookup appId remapped)
@@ -333,7 +337,7 @@ getInstalledAppByIdLogic appId = do
fromInstalled = (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) fromInstalled = (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion)
<$> hoistMaybe (HM.lookup depId serverApps) <$> hoistMaybe (HM.lookup depId serverApps)
let fromStore = do let fromStore = do
Reg.AppManifestRes res <- lift appManifestFetchCached Reg.AppIndexRes res <- lift appManifestFetchCached
(storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions) (storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions)
<$> hoistMaybe (find ((== depId) . storeAppId) res) <$> hoistMaybe (find ((== depId) . storeAppId) res)
(title, v) <- fromInstalled <|> fromStore (title, v) <- fromInstalled <|> fromStore
@@ -354,6 +358,8 @@ getInstalledAppByIdLogic appId = do
guard (not . null $ lanConfs) guard (not . null $ lanConfs)
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version) pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version)
, appInstalledFullLicenseName = AppManifest.appManifestLicenseName manifest
, appInstalledFullLicenseLink = AppManifest.appManifestLicenseLink manifest
, appInstalledFullStatus = status , appInstalledFullStatus = status
, appInstalledFullVersionInstalled = version , appInstalledFullVersionInstalled = version
, appInstalledFullInstructions = instructions , appInstalledFullInstructions = instructions
@@ -674,8 +680,8 @@ getAvailableAppVersionInfoLogic :: ( Has (Reader AgentCtx) sig m
-> VersionRange -> VersionRange
-> m AppVersionInfo -> m AppVersionInfo
getAvailableAppVersionInfoLogic appId appVersionSpec = do getAvailableAppVersionInfoLogic appId appVersionSpec = do
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
Reg.AppManifestRes storeApps <- Reg.getAppManifest Reg.AppIndexRes storeApps <- Reg.getAppIndex
let titles = let titles =
(storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions) <$> indexBy storeAppId storeApps (storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions) <$> indexBy storeAppId storeApps
StoreApp {..} <- find ((== appId) . storeAppId) storeApps `orThrowPure` NotFoundE "appId" (show appId) StoreApp {..} <- find ((== appId) . storeAppId) storeApps `orThrowPure` NotFoundE "appId" (show appId)

View File

@@ -14,6 +14,13 @@ import Network.HTTP.Simple
import System.FilePath.Posix import System.FilePath.Posix
import Yesod.Core import Yesod.Core
import Control.Carrier.Reader hiding ( asks )
import Control.Concurrent.STM ( modifyTVar
, readTVarIO
)
import Control.Effect.Labelled ( runLabelled )
import Crypto.Hash.Conduit ( hashFile )
import qualified Data.HashMap.Strict as HM
import Foundation import Foundation
import Lib.Algebra.State.RegistryUrl import Lib.Algebra.State.RegistryUrl
import Lib.Error import Lib.Error
@@ -21,16 +28,9 @@ import qualified Lib.External.Registry as Reg
import Lib.IconCache import Lib.IconCache
import Lib.SystemPaths hiding ( (</>) ) import Lib.SystemPaths hiding ( (</>) )
import Lib.Types.Core import Lib.Types.Core
import Lib.Types.Emver
import Lib.Types.ServerApp import Lib.Types.ServerApp
import Settings import Settings
import Control.Carrier.Reader hiding ( asks )
import Control.Effect.Labelled ( runLabelled )
import qualified Data.HashMap.Strict as HM
import Control.Concurrent.STM ( modifyTVar
, readTVarIO
)
import Crypto.Hash.Conduit ( hashFile )
import Lib.Types.Emver
iconUrl :: AppId -> Version -> Text iconUrl :: AppId -> Version -> Text
iconUrl appId version = (foldMap (T.cons '/') . fst . renderRoute . AppIconR $ appId) <> "?" <> show version iconUrl appId version = (foldMap (T.cons '/') . fst . renderRoute . AppIconR $ appId) <> "?" <> show version
@@ -63,7 +63,7 @@ getAppIconR appId = handleS9ErrT $ do
lift $ respondSource (parseContentType path) $ CB.sourceFile path .| awaitForever sendChunkBS lift $ respondSource (parseContentType path) $ CB.sourceFile path .| awaitForever sendChunkBS
where where
fetchIcon = do fetchIcon = do
url <- find ((== appId) . storeAppId) . Reg.storeApps <$> Reg.getAppManifest >>= \case url <- find ((== appId) . storeAppId) . Reg.storeApps <$> Reg.getAppIndex >>= \case
Nothing -> throwError $ NotFoundE "icon" (show appId) Nothing -> throwError $ NotFoundE "icon" (show appId)
Just x -> pure . toS $ storeAppIconUrl x Just x -> pure . toS $ storeAppIconUrl x
bp <- getAbsoluteLocationFor iconBasePath bp <- getAbsoluteLocationFor iconBasePath
@@ -84,7 +84,7 @@ getAvailableAppIconR :: AppId -> Handler TypedContent
getAvailableAppIconR appId = handleS9ErrT $ do getAvailableAppIconR appId = handleS9ErrT $ do
s <- getsYesod appSettings s <- getsYesod appSettings
url <- do url <- do
find ((== appId) . storeAppId) . Reg.storeApps <$> interp s Reg.getAppManifest >>= \case find ((== appId) . storeAppId) . Reg.storeApps <$> interp s Reg.getAppIndex >>= \case
Nothing -> throwE $ NotFoundE "icon" (show appId) Nothing -> throwE $ NotFoundE "icon" (show appId)
Just x -> pure . toS $ storeAppIconUrl x Just x -> pure . toS $ storeAppIconUrl x
req <- case parseRequest url of req <- case parseRequest url of

View File

@@ -74,6 +74,8 @@ instance FromJSON InstallNewAppReq where
data AppAvailableFull = AppAvailableFull data AppAvailableFull = AppAvailableFull
{ appAvailableFullBase :: AppBase { appAvailableFullBase :: AppBase
, appAvailableFullLicenseName :: Maybe Text
, appAvailableFullLicenseLink :: Maybe Text
, appAvailableFullInstallInfo :: Maybe (Version, AppStatus) , appAvailableFullInstallInfo :: Maybe (Version, AppStatus)
, appAvailableFullVersionLatest :: Version , appAvailableFullVersionLatest :: Version
, appAvailableFullDescriptionShort :: Text , appAvailableFullDescriptionShort :: Text
@@ -88,7 +90,9 @@ instance ToJSON AppAvailableFull where
toJSON AppAvailableFull {..} = mergeTo toJSON AppAvailableFull {..} = mergeTo
(toJSON appAvailableFullBase) (toJSON appAvailableFullBase)
(object (object
[ "versionInstalled" .= fmap fst appAvailableFullInstallInfo [ "licenseName" .= appAvailableFullLicenseName
, "licenseLink" .= appAvailableFullLicenseLink
, "versionInstalled" .= fmap fst appAvailableFullInstallInfo
, "status" .= fmap snd appAvailableFullInstallInfo , "status" .= fmap snd appAvailableFullInstallInfo
, "versionLatest" .= appAvailableFullVersionLatest , "versionLatest" .= appAvailableFullVersionLatest
, "descriptionShort" .= appAvailableFullDescriptionShort , "descriptionShort" .= appAvailableFullDescriptionShort
@@ -131,6 +135,8 @@ instance ToJSON (AppDependencyRequirement Keep) where
-- mute violations downstream of version for installing apps -- mute violations downstream of version for installing apps
data AppInstalledFull = AppInstalledFull data AppInstalledFull = AppInstalledFull
{ appInstalledFullBase :: AppBase { appInstalledFullBase :: AppBase
, appInstalledFullLicenseName :: Maybe Text
, appInstalledFullLicenseLink :: Maybe Text
, appInstalledFullStatus :: AppStatus , appInstalledFullStatus :: AppStatus
, appInstalledFullVersionInstalled :: Version , appInstalledFullVersionInstalled :: Version
, appInstalledFullTorAddress :: Maybe TorAddress , appInstalledFullTorAddress :: Maybe TorAddress
@@ -156,6 +162,8 @@ instance ToJSON AppInstalledFull where
, "lanUi" .= appInstalledFullLanUi , "lanUi" .= appInstalledFullLanUi
, "id" .= appBaseId appInstalledFullBase , "id" .= appBaseId appInstalledFullBase
, "title" .= appBaseTitle appInstalledFullBase , "title" .= appBaseTitle appInstalledFullBase
, "licenseName" .= appInstalledFullLicenseName
, "licenseLink" .= appInstalledFullLicenseLink
, "iconURL" .= appBaseIconUrl appInstalledFullBase , "iconURL" .= appBaseIconUrl appInstalledFullBase
, "versionInstalled" .= appInstalledFullVersionInstalled , "versionInstalled" .= appInstalledFullVersionInstalled
, "status" .= appInstalledFullStatus , "status" .= appInstalledFullStatus

View File

@@ -78,6 +78,8 @@ data AppManifest where
AppManifest ::{ appManifestId :: AppId AppManifest ::{ appManifestId :: AppId
, appManifestVersion :: Version , appManifestVersion :: Version
, appManifestTitle :: Text , appManifestTitle :: Text
, appManifestLicenseName :: Maybe Text
, appManifestLicenseLink :: Maybe Text
, appManifestDescShort :: Text , appManifestDescShort :: Text
, appManifestDescLong :: Text , appManifestDescLong :: Text
, appManifestReleaseNotes :: Text , appManifestReleaseNotes :: Text
@@ -109,6 +111,8 @@ instance FromJSON AppManifest where
appManifestId <- o .: "id" appManifestId <- o .: "id"
appManifestVersion <- o .: "version" appManifestVersion <- o .: "version"
appManifestTitle <- o .: "title" appManifestTitle <- o .: "title"
appManifestLicenseName <- o .:? "license-info" >>= traverse (.: "license")
appManifestLicenseLink <- o .:? "license-info" >>= traverse (.: "url")
appManifestDescShort <- o .: "description" >>= (.: "short") appManifestDescShort <- o .: "description" >>= (.: "short")
appManifestDescLong <- o .: "description" >>= (.: "long") appManifestDescLong <- o .: "description" >>= (.: "long")
appManifestReleaseNotes <- o .: "release-notes" appManifestReleaseNotes <- o .: "release-notes"

View File

@@ -13,8 +13,8 @@ import Startlude.ByteStream hiding ( count )
import Conduit import Conduit
import Control.Algebra import Control.Algebra
import Control.Effect.Lift
import Control.Effect.Error import Control.Effect.Error
import Control.Effect.Lift
import Control.Effect.Reader.Labelled import Control.Effect.Reader.Labelled
import Control.Monad.Fail ( fail ) import Control.Monad.Fail ( fail )
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
@@ -30,15 +30,17 @@ import System.Directory
import System.Process import System.Process
import Constants import Constants
import qualified Data.Aeson.Types ( parseEither )
import Data.Time.ISO8601 ( parseISO8601 )
import Lib.Algebra.State.RegistryUrl import Lib.Algebra.State.RegistryUrl
import Lib.Error import Lib.Error
import Lib.External.AppManifest
import Lib.SystemPaths import Lib.SystemPaths
import Lib.Types.Core import Lib.Types.Core
import Lib.Types.Emver import Lib.Types.Emver
import Lib.Types.ServerApp import Lib.Types.ServerApp
import Data.Time.ISO8601 ( parseISO8601 )
newtype AppManifestRes = AppManifestRes newtype AppIndexRes = AppIndexRes
{ storeApps :: [StoreApp] } deriving (Eq, Show) { storeApps :: [StoreApp] } deriving (Eq, Show)
newtype RegistryVersionForSpecRes = RegistryVersionForSpecRes newtype RegistryVersionForSpecRes = RegistryVersionForSpecRes
@@ -85,8 +87,8 @@ getLifelineBinary avs = do
liftIO $ runConduitRes $ httpSource request getResponseBody .| sinkFile (toS lifelineTarget) liftIO $ runConduitRes $ httpSource request getResponseBody .| sinkFile (toS lifelineTarget)
liftIO $ void $ readProcessWithExitCode "chmod" ["700", toS lifelineTarget] "" liftIO $ void $ readProcessWithExitCode "chmod" ["700", toS lifelineTarget] ""
getAppManifest :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => m AppManifestRes getAppIndex :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => m AppIndexRes
getAppManifest = do getAppIndex = do
manifestPath <- registryManifestUrl manifestPath <- registryManifestUrl
req <- liftIO $ fmap setUserAgent . parseRequestThrow $ toS manifestPath req <- liftIO $ fmap setUserAgent . parseRequestThrow $ toS manifestPath
val <- (liftIO . try @SomeException) (httpBS req) >>= \case val <- (liftIO . try @SomeException) (httpBS req) >>= \case
@@ -96,22 +98,29 @@ getAppManifest = do
Left e -> throwError $ RegistryParseE manifestPath . toS $ e Left e -> throwError $ RegistryParseE manifestPath . toS $ e
Right a -> pure a Right a -> pure a
getAppManifest :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => AppId -> m AppManifest
getAppManifest appId = do
let path = "/apps/manifest/" <> unAppId appId
v <- registryRequest path
case Data.Aeson.Types.parseEither parseJSON v of
Left e -> throwError $ RegistryParseE path . toS $ e
Right a -> pure a
getStoreAppInfo :: (MonadIO m, Has RegistryUrl sig m, Has (Error S9Error) sig m) => AppId -> m (Maybe StoreApp) getStoreAppInfo :: (MonadIO m, Has RegistryUrl sig m, Has (Error S9Error) sig m) => AppId -> m (Maybe StoreApp)
getStoreAppInfo name = find ((== name) . storeAppId) . storeApps <$> getAppManifest getStoreAppInfo name = find ((== name) . storeAppId) . storeApps <$> getAppIndex
parseBsManifest :: Has RegistryUrl sig m => ByteString -> m (Either String AppManifestRes) parseBsManifest :: Has RegistryUrl sig m => ByteString -> m (Either String AppIndexRes)
parseBsManifest bs = do parseBsManifest bs = do
parseRegistryRes' <- parseRegistryRes parseRegistryRes' <- parseRegistryRes
pure $ parseEither parseRegistryRes' . fromJust . decodeThrow $ bs pure $ parseEither parseRegistryRes' . fromJust . decodeThrow $ bs
parseRegistryRes :: Has RegistryUrl sig m => m (Value -> Parser AppManifestRes) parseRegistryRes :: Has RegistryUrl sig m => m (Value -> Parser AppIndexRes)
parseRegistryRes = do parseRegistryRes = do
parseAppData' <- parseAppData parseAppData' <- parseAppData
pure $ withObject "app registry response" $ \obj -> do pure $ withObject "app registry response" $ \obj -> do
let keyVals = HM.toList obj let keyVals = HM.toList obj
let mManifestApps = fmap (\(k, v) -> parseMaybe (parseAppData' (AppId k)) v) keyVals let mManifestApps = fmap (\(k, v) -> parseMaybe (parseAppData' (AppId k)) v) keyVals
pure . AppManifestRes . catMaybes $ mManifestApps pure . AppIndexRes . catMaybes $ mManifestApps
registryUrl :: (Has RegistryUrl sig m) => m Text registryUrl :: (Has RegistryUrl sig m) => m Text
registryUrl = maybe "https://registry.start9labs.com:443" show <$> getRegistryUrl registryUrl = maybe "https://registry.start9labs.com:443" show <$> getRegistryUrl

View File

@@ -98,12 +98,12 @@ parseKernelVersion = do
pure $ KernelVersion (Version (major', minor', patch', 0)) arch pure $ KernelVersion (Version (major', minor', patch', 0)) arch
synchronizer :: Synchronizer synchronizer :: Synchronizer
synchronizer = sync_0_2_12 synchronizer = sync_0_2_13
{-# INLINE synchronizer #-} {-# INLINE synchronizer #-}
sync_0_2_12 :: Synchronizer sync_0_2_13 :: Synchronizer
sync_0_2_12 = Synchronizer sync_0_2_13 = Synchronizer
"0.2.12" "0.2.13"
[ syncCreateAgentTmp [ syncCreateAgentTmp
, syncCreateSshDir , syncCreateSshDir
, syncRemoveAvahiSystemdDependency , syncRemoveAvahiSystemdDependency

View File

@@ -1,13 +1,7 @@
-- {-# OPTIONS_GHC -fno-warn-unused-imports #-} -- {-# OPTIONS_GHC -fno-warn-unused-imports #-}
module Startlude.ByteStream module Startlude.ByteStream
( module Startlude.ByteStream ( module BS
, module BS ) where
)
where
import Data.ByteString.Streaming as BS import Streaming.ByteString as BS
hiding ( ByteString ) hiding ( ByteString )
import Data.ByteString.Streaming as X
( ByteString )
type ByteStream m = X.ByteString m

View File

@@ -1,7 +1,5 @@
module Startlude.ByteStream.Char8 module Startlude.ByteStream.Char8
( module X ( module X
) ) where
where
import Data.ByteString.Streaming.Char8 import Streaming.ByteString.Char8 as X
as X

View File

@@ -1,10 +1,10 @@
resolver: nightly-2020-09-29 resolver: lts-17.10
packages: packages:
- . - .
extra-deps: extra-deps:
- aeson-1.4.7.1 # - aeson-1.4.7.1
- aeson-flatten-0.1.0.2 - aeson-flatten-0.1.0.2
- exinst-0.8 - exinst-0.8
- fused-effects-1.1.0.0 - fused-effects-1.1.0.0
@@ -12,13 +12,14 @@ extra-deps:
- git-embed-0.1.0 - git-embed-0.1.0
- json-stream-0.4.2.4 - json-stream-0.4.2.4
- protolude-0.3.0 - protolude-0.3.0
- streaming-bytestring-0.1.7
- streaming-conduit-0.1.2.2 - streaming-conduit-0.1.2.2
- streaming-utils-0.2.0.0 - streaming-utils-0.2.0.0
# to avoid the ridiculous bug where stat64 is not found (only affects development) # to avoid the ridiculous bug where stat64 is not found (only affects development)
- git: https://github.com/ProofOfKeags/persistent.git # - git: https://github.com/ProofOfKeags/persistent.git
commit: 3b52b13d9ce79cdef14bb1c37cc527657a529462 # commit: 3b52b13d9ce79cdef14bb1c37cc527657a529462
subdirs: # subdirs:
- persistent-sqlite # - persistent-sqlite
ghc-options: ghc-options:
"$locals": -fwrite-ide-info "$locals": -fwrite-ide-info

2
appmgr/Cargo.lock generated
View File

@@ -41,7 +41,7 @@ checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
[[package]] [[package]]
name = "appmgr" name = "appmgr"
version = "0.2.12" version = "0.2.13"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"avahi-sys", "avahi-sys",

View File

@@ -2,7 +2,7 @@
authors = ["Aiden McClelland <me@drbonez.dev>"] authors = ["Aiden McClelland <me@drbonez.dev>"]
edition = "2018" edition = "2018"
name = "appmgr" name = "appmgr"
version = "0.2.12" version = "0.2.13"
[lib] [lib]
name = "appmgrlib" name = "appmgrlib"

View File

@@ -29,8 +29,9 @@ mod v0_2_9;
mod v0_2_10; mod v0_2_10;
mod v0_2_11; mod v0_2_11;
mod v0_2_12; mod v0_2_12;
mod v0_2_13;
pub use v0_2_12::Version as Current; pub use v0_2_13::Version as Current;
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)] #[serde(untagged)]
@@ -55,6 +56,7 @@ enum Version {
V0_2_10(Wrapper<v0_2_10::Version>), V0_2_10(Wrapper<v0_2_10::Version>),
V0_2_11(Wrapper<v0_2_11::Version>), V0_2_11(Wrapper<v0_2_11::Version>),
V0_2_12(Wrapper<v0_2_12::Version>), V0_2_12(Wrapper<v0_2_12::Version>),
V0_2_13(Wrapper<v0_2_13::Version>),
Other(emver::Version), Other(emver::Version),
} }
@@ -169,6 +171,7 @@ pub async fn init() -> Result<(), failure::Error> {
Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?, Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_11(v) => v.0.migrate_to(&Current::new()).await?, Version::V0_2_11(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_12(v) => v.0.migrate_to(&Current::new()).await?, Version::V0_2_12(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_13(v) => v.0.migrate_to(&Current::new()).await?,
Version::Other(_) => (), Version::Other(_) => (),
// TODO find some way to automate this? // TODO find some way to automate this?
} }
@@ -262,6 +265,7 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?, Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_11(v) => Current::new().migrate_to(&v.0).await?, Version::V0_2_11(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_12(v) => Current::new().migrate_to(&v.0).await?, Version::V0_2_12(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_13(v) => Current::new().migrate_to(&v.0).await?,
Version::Other(_) => (), Version::Other(_) => (),
// TODO find some way to automate this? // TODO find some way to automate this?
}; };

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_13: emver::Version = emver::Version::new(0, 2, 13, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_12::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_13
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 KiB

After

Width:  |  Height:  |  Size: 3.0 MiB

41
ui/build-send-alpha.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -e
echo "turn off mocks"
echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json
echo "$( jq '.skipStartupAlerts = false' use-mocks.json )" > use-mocks.json
echo "FILTER: rm -rf www"
rm -rf www
echo "FILTER: ionic build"
npm run build-prod
echo "FILTER: cp client-manifest.yaml www"
cp client-manifest.yaml www
echo "FILTER: git hash"
touch git-hash.txt
git log | head -n1 > git-hash.txt
mv git-hash.txt www
echo "FILTER: removing mock icons"
rm -rf www/assets/img/service-icons
echo "FILTER: tar -zcvf ambassador-ui.tar.gz www"
tar -zcvf ambassador-ui.tar.gz www
SHA_SUM=$(sha1sum ambassador-ui.tar.gz)
echo "${SHA_SUM}"
echo "Set version"
VERSION=$(jq ".version" package.json)
echo "${VERSION}"
echo "FILTER: mkdir alpha-reg"
ssh root@alpha-registry.start9labs.com "mkdir -p /var/www/html/resources/sys/ambassador-ui.tar.gz/${VERSION}"
echo "FILTER: scp ambassador-ui.tar.gz"
scp ambassador-ui.tar.gz root@alpha-registry.start9labs.com:/var/www/html/resources/sys/ambassador-ui.tar.gz/${VERSION}/ambassador-ui.tar.gz
echo "FILTER: fin"

View File

@@ -1,6 +1,6 @@
manifest-version: 0 manifest-version: 0
app-id: start9-ambassador app-id: start9-ambassador
app-version: 0.2.12 app-version: 0.2.13
uri-rewrites: uri-rewrites:
- =/api -> http://{{start9-ambassador}}:5959/authenticate - =/api -> http://{{start9-ambassador}}:5959/authenticate
- /api/ -> http://{{start9-ambassador}}:5959/ - /api/ -> http://{{start9-ambassador}}:5959/

2823
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "embassy-ui", "name": "embassy-ui",
"version": "0.2.12", "version": "0.2.13",
"description": "GUI for EmbassyOS", "description": "GUI for EmbassyOS",
"author": "Start9 Labs", "author": "Start9 Labs",
"homepage": "https://github.com/Start9Labs/embassy-ui", "homepage": "https://github.com/Start9Labs/embassy-ui",

View File

@@ -1,27 +1,17 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title > <ion-title >
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.12!</ion-label> <ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.13!</ion-label>
</ion-title> </ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%"> <div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
<h2>Highlights</h2> <h2>Highlights: 0.2.13</h2>
<div class="main-content"> <div class="main-content">
<p>This release includes several bugfixes to resolve:</p> <p>At long last, Matrix has arrived!</p>
<ol> <p>This release also enables displaying Service license information and contains utilities to facilitate the next major release of EmbassyOS.</p>
<li>Faster file upload/download ability</li>
<li>More robust support for .local addresses</li>
<li>Refreshing error messages during configuration changes</li>
<li>Starting services with uninstalled optional dependencies</li>
<li>Uninstalling services with optional dependencies</li>
<li>Redirecting to HTTPS when navigating to LAN address</li>
<li>Displaying warning messages during concurrent upgrades of dependent services</li>
<li>Allowing larger file uploads</li>
<li>Patching a security fix for Tor</li>
</ol>
</div> </div>
<div class="close-button"> <div class="close-button">

View File

@@ -18,9 +18,11 @@ export interface AppAvailablePreview extends BaseApp {
} }
export type AppAvailableFull = export type AppAvailableFull =
AppAvailablePreview & AppAvailablePreview & {
{ descriptionLong: string descriptionLong: string
versions: string[] versions: string[]
licenseName?: string // @TODO required for 0.3.0
licenseLink?: string // @TODO required for 0.3.0
} & } &
AppAvailableVersionSpecificInfo AppAvailableVersionSpecificInfo
@@ -45,6 +47,8 @@ export interface AppInstalledPreview extends BaseApp {
} }
export interface AppInstalledFull extends AppInstalledPreview { export interface AppInstalledFull extends AppInstalledPreview {
licenseName?: string // @TODO required for 0.3.0
licenseLink?: string // @TODO required for 0.3.0
instructions: string | null instructions: string | null
lastBackup: string | null lastBackup: string | null
configuredRequirements: AppDependency[] | null // null if not yet configured configuredRequirements: AppDependency[] | null // null if not yet configured

View File

@@ -21,6 +21,26 @@
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text> <ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item> </ion-item>
<ion-card *ngIf="v1Status.status === 'instructions'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="instructions-card">
<ion-card-header>
<ion-card-subtitle>Coming Soon...</ion-card-subtitle>
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<b>Get ready. View the update instructions.</b>
</ion-card-content>
</ion-card>
<ion-card *ngIf="v1Status.status === 'available'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="available-card">
<ion-card-header>
<ion-card-subtitle>Now Available...</ion-card-subtitle>
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<b>View the update instructions.</b>
</ion-card-content>
</ion-card>
<ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]"> <ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]">
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l"> <ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l">
<ion-avatar style="margin-top: 8px;" slot="start"> <ion-avatar style="margin-top: 8px;" slot="start">

View File

@@ -3,4 +3,14 @@
font-style: italic; font-style: italic;
font-family: 'Open Sans'; font-family: 'Open Sans';
padding: 1px 0px 1.5px 0px; padding: 1px 0px 1.5px 0px;
}
.instructions-card {
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-medium) 150%);
margin: 16px 10px;
}
.available-card {
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-danger) 150%);
margin: 16px 10px;
} }

View File

@@ -8,6 +8,7 @@ import { Subscription, BehaviorSubject, combineLatest } from 'rxjs'
import { take } from 'rxjs/operators' import { take } from 'rxjs/operators'
import { markAsLoadingDuringP } from 'src/app/services/loader.service' import { markAsLoadingDuringP } from 'src/app/services/loader.service'
import { OsUpdateService } from 'src/app/services/os-update.service' import { OsUpdateService } from 'src/app/services/os-update.service'
import { V1Status } from 'src/app/services/api/api-types'
@Component({ @Component({
selector: 'app-available-list', selector: 'app-available-list',
@@ -20,6 +21,7 @@ export class AppAvailableListPage {
installedAppDeltaSubscription: Subscription installedAppDeltaSubscription: Subscription
apps: PropertySubjectId<AppAvailablePreview>[] = [] apps: PropertySubjectId<AppAvailablePreview>[] = []
appsInstalled: PropertySubjectId<AppInstalledPreview>[] = [] appsInstalled: PropertySubjectId<AppInstalledPreview>[] = []
v1Status: V1Status = { status: 'nothing', version: '' }
constructor ( constructor (
private readonly apiService: ApiService, private readonly apiService: ApiService,
@@ -35,6 +37,7 @@ export class AppAvailableListPage {
markAsLoadingDuringP(this.$loading$, Promise.all([ markAsLoadingDuringP(this.$loading$, Promise.all([
this.getApps(), this.getApps(),
this.checkV1Status(),
this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally
pauseFor(600), pauseFor(600),
])) ]))
@@ -44,6 +47,14 @@ export class AppAvailableListPage {
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id)) this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
} }
async checkV1Status () {
try {
this.v1Status = await this.apiService.checkV1Status()
} catch (e) {
console.error(e)
}
}
mergeInstalledProps (appInstalledId: string) { mergeInstalledProps (appInstalledId: string) {
const appAvailable = this.apps.find(app => app.id === appInstalledId) const appAvailable = this.apps.find(app => app.id === appInstalledId)
if (!appAvailable) return if (!appAvailable) return

View File

@@ -17,6 +17,8 @@
versionInstalled: $app$.versionInstalled | async, versionInstalled: $app$.versionInstalled | async,
versionViewing: $app$.versionViewing | async, versionViewing: $app$.versionViewing | async,
descriptionLong: $app$.descriptionLong | async, descriptionLong: $app$.descriptionLong | async,
licenseName: $app$.licenseName | async,
licenseLink: $app$.licenseLink | async,
serviceRequirements: $app$.serviceRequirements | async, serviceRequirements: $app$.serviceRequirements | async,
iconURL: $app$.iconURL | async, iconURL: $app$.iconURL | async,
releaseNotes: $app$.releaseNotes | async releaseNotes: $app$.releaseNotes | async
@@ -112,9 +114,14 @@
<dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list> <dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
</ng-container> </ng-container>
<ion-item-divider></ion-item-divider> <ion-item-divider></ion-item-divider>
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
<ion-icon slot="start" name="newspaper-outline"></ion-icon>
<ion-label>License</ion-label>
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
</ion-item>
<ion-item lines="none" button (click)="presentAlertVersions()"> <ion-item lines="none" button (click)="presentAlertVersions()">
<ion-icon color="medium" slot="start" name="file-tray-stacked-outline"></ion-icon> <ion-icon slot="start" name="file-tray-stacked-outline"></ion-icon>
<ion-label color="medium">Other versions</ion-label> <ion-label>Other versions</ion-label>
</ion-item> </ion-item>
</ion-item-group> </ion-item-group>
</ng-container> </ng-container>

View File

@@ -15,6 +15,8 @@
torAddress: app.torAddress | async, torAddress: app.torAddress | async,
status: app.status | async, status: app.status | async,
versionInstalled: app.versionInstalled | async, versionInstalled: app.versionInstalled | async,
licenseName: app.licenseName | async,
licenseLink: app.licenseLink | async,
configuredRequirements: app.configuredRequirements | async, configuredRequirements: app.configuredRequirements | async,
lastBackup: app.lastBackup | async, lastBackup: app.lastBackup | async,
hasFetchedFull: app.hasFetchedFull | async, hasFetchedFull: app.hasFetchedFull | async,
@@ -157,6 +159,12 @@
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon> <ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label> <ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
</ion-item> </ion-item>
<!-- license -->
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">License</ion-text></ion-label>
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
</ion-item>
<!-- dependencies --> <!-- dependencies -->
<ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length"> <ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length">

View File

@@ -37,3 +37,7 @@ export interface ApiAppConfig {
export type Unit = { never?: never; } // hack for the unit typ export type Unit = { never?: never; } // hack for the unit typ
export type V1Status = {
status: 'nothing' | 'instructions' | 'available'
version: string
}

View File

@@ -2,7 +2,7 @@ import { Rules } from '../../models/app-model'
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types' import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model' import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview } from './api-types' import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview, V1Status } from './api-types'
import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util' import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util'
import { ConfigSpec } from 'src/app/app-config/config-types' import { ConfigSpec } from 'src/app/app-config/config-types'
@@ -64,6 +64,7 @@ export abstract class ApiService {
abstract ejectExternalDisk (logicalName: string): Promise<Unit> abstract ejectExternalDisk (logicalName: string): Promise<Unit>
abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse> abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse>
abstract refreshLAN (): Promise<Unit> abstract refreshLAN (): Promise<Unit>
abstract checkV1Status (): Promise<V1Status>
} }
export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } { export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } {

View File

@@ -4,7 +4,7 @@ import { AppModel, AppStatus } from '../../models/app-model'
import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types' import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model' import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
import { ApiService, ReqRes } from './api.service' import { ApiService, ReqRes } from './api.service'
import { ApiAppInstalledPreview, ApiServer, Unit } from './api-types' import { ApiAppInstalledPreview, ApiServer, Unit, V1Status } from './api-types'
import { HttpErrorResponse } from '@angular/common/http' import { HttpErrorResponse } from '@angular/common/http'
import { isUnauthorized } from 'src/app/util/web.util' import { isUnauthorized } from 'src/app/util/web.util'
import { Replace } from 'src/app/util/types.util' import { Replace } from 'src/app/util/types.util'
@@ -17,7 +17,7 @@ import { ConfigService } from '../config.service'
@Injectable() @Injectable()
export class LiveApiService extends ApiService { export class LiveApiService extends ApiService {
constructor( constructor (
private readonly http: HttpService, private readonly http: HttpService,
// TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel // TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel
private readonly appModel: AppModel, private readonly appModel: AppModel,
@@ -25,40 +25,40 @@ export class LiveApiService extends ApiService {
private readonly config: ConfigService, private readonly config: ConfigService,
) { super() } ) { super() }
testConnection(url: string): Promise<true> { testConnection (url: string): Promise<true> {
return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise() return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise()
} }
// Used to check whether password or key is valid. If so, it will be used implicitly by all other calls. // Used to check whether password or key is valid. If so, it will be used implicitly by all other calls.
async getCheckAuth(): Promise<Unit> { async getCheckAuth (): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' }) return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' })
} }
async postLogin(password: string): Promise<Unit> { async postLogin (password: string): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' }) return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' })
} }
async postLogout(): Promise<Unit> { async postLogout (): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return {} }) return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return { } })
} }
async getServer(timeout?: number): Promise<ApiServer> { async getServer (timeout?: number): Promise<ApiServer> {
return this.authRequest<ReqRes.GetServerRes>({ method: Method.GET, url: '/', readTimeout: timeout }) return this.authRequest<ReqRes.GetServerRes>({ method: Method.GET, url: '/', readTimeout: timeout })
} }
async acknowledgeOSWelcome(version: string): Promise<Unit> { async acknowledgeOSWelcome (version: string): Promise<Unit> {
return this.authRequest<Unit>({ method: Method.POST, url: `/welcome/${version}` }) return this.authRequest<Unit>({ method: Method.POST, url: `/welcome/${version}` })
} }
async getVersionLatest(): Promise<ReqRes.GetVersionLatestRes> { async getVersionLatest (): Promise<ReqRes.GetVersionLatestRes> {
return this.authRequest<ReqRes.GetVersionLatestRes>({ method: Method.GET, url: '/versionLatest' }, { version: '' }) return this.authRequest<ReqRes.GetVersionLatestRes>({ method: Method.GET, url: '/versionLatest' }, { version: '' })
} }
async getServerMetrics(): Promise<ReqRes.GetServerMetricsRes> { async getServerMetrics (): Promise<ReqRes.GetServerMetricsRes> {
return this.authRequest<ReqRes.GetServerMetricsRes>({ method: Method.GET, url: `/metrics` }) return this.authRequest<ReqRes.GetServerMetricsRes>({ method: Method.GET, url: `/metrics` })
} }
async getNotifications(page: number, perPage: number): Promise<S9Notification[]> { async getNotifications (page: number, perPage: number): Promise<S9Notification[]> {
const params: ReqRes.GetNotificationsReq = { const params: ReqRes.GetNotificationsReq = {
page: String(page), page: String(page),
perPage: String(perPage), perPage: String(perPage),
@@ -66,27 +66,27 @@ export class LiveApiService extends ApiService {
return this.authRequest<ReqRes.GetNotificationsRes>({ method: Method.GET, url: `/notifications`, params }) return this.authRequest<ReqRes.GetNotificationsRes>({ method: Method.GET, url: `/notifications`, params })
} }
async deleteNotification(id: string): Promise<Unit> { async deleteNotification (id: string): Promise<Unit> {
return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` }) return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` })
} }
async getExternalDisks(): Promise<DiskInfo[]> { async getExternalDisks (): Promise<DiskInfo[]> {
return this.authRequest<ReqRes.GetExternalDisksRes>({ method: Method.GET, url: `/disks` }) return this.authRequest<ReqRes.GetExternalDisksRes>({ method: Method.GET, url: `/disks` })
} }
// TODO: EJECT-DISKS // TODO: EJECT-DISKS
async ejectExternalDisk(logicalName: string): Promise<Unit> { async ejectExternalDisk (logicalName: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/disks/eject`, data: { logicalName } }) return this.authRequest({ method: Method.POST, url: `/disks/eject`, data: { logicalName } })
} }
async updateAgent(version: string): Promise<Unit> { async updateAgent (version: string): Promise<Unit> {
const data: ReqRes.PostUpdateAgentReq = { const data: ReqRes.PostUpdateAgentReq = {
version: `=${version}`, version: `=${version}`,
} }
return this.authRequest({ method: Method.POST, url: '/update', data }) return this.authRequest({ method: Method.POST, url: '/update', data })
} }
async getAvailableAppVersionSpecificInfo(appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> { async getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> {
return this return this
.authRequest<Replace<ReqRes.GetAppAvailableVersionInfoRes, 'versionViewing', 'version'>>({ method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` }) .authRequest<Replace<ReqRes.GetAppAvailableVersionInfoRes, 'versionViewing', 'version'>>({ method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` })
.then(res => ({ ...res, versionViewing: res.version })) .then(res => ({ ...res, versionViewing: res.version }))
@@ -96,7 +96,7 @@ export class LiveApiService extends ApiService {
}) })
} }
async getAvailableApps(): Promise<AppAvailablePreview[]> { async getAvailableApps (): Promise<AppAvailablePreview[]> {
const res = await this.authRequest<ReqRes.GetAppsAvailableRes>({ method: Method.GET, url: '/apps/store' }) const res = await this.authRequest<ReqRes.GetAppsAvailableRes>({ method: Method.GET, url: '/apps/store' })
return res.map(a => { return res.map(a => {
const latestVersionTimestamp = new Date(a.latestVersionTimestamp) const latestVersionTimestamp = new Date(a.latestVersionTimestamp)
@@ -105,7 +105,7 @@ export class LiveApiService extends ApiService {
}) })
} }
async getAvailableApp(appId: string): Promise<AppAvailableFull> { async getAvailableApp (appId: string): Promise<AppAvailableFull> {
return this.authRequest<ReqRes.GetAppAvailableRes>({ method: Method.GET, url: `/apps/${appId}/store` }) return this.authRequest<ReqRes.GetAppAvailableRes>({ method: Method.GET, url: `/apps/${appId}/store` })
.then(res => { .then(res => {
return { return {
@@ -115,7 +115,7 @@ export class LiveApiService extends ApiService {
}) })
} }
async getInstalledApp(appId: string): Promise<AppInstalledFull> { async getInstalledApp (appId: string): Promise<AppInstalledFull> {
return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` }) return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` })
.then(app => { .then(app => {
return { return {
@@ -127,7 +127,7 @@ export class LiveApiService extends ApiService {
}) })
} }
async getInstalledApps(): Promise<AppInstalledPreview[]> { async getInstalledApps (): Promise<AppInstalledPreview[]> {
return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` }) return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` })
.then(apps => { .then(apps => {
return apps.map(app => { return apps.map(app => {
@@ -140,24 +140,24 @@ export class LiveApiService extends ApiService {
}) })
} }
async getAppConfig(appId: string): Promise<ReqRes.GetAppConfigRes> { async getAppConfig (appId: string): Promise<ReqRes.GetAppConfigRes> {
return this.authRequest<ReqRes.GetAppConfigRes>({ method: Method.GET, url: `/apps/${appId}/config` }) return this.authRequest<ReqRes.GetAppConfigRes>({ method: Method.GET, url: `/apps/${appId}/config` })
} }
async getAppLogs(appId: string, params: ReqRes.GetAppLogsReq = {}): Promise<string[]> { async getAppLogs (appId: string, params: ReqRes.GetAppLogsReq = { }): Promise<string[]> {
return this.authRequest<ReqRes.GetAppLogsRes>({ method: Method.GET, url: `/apps/${appId}/logs`, params: params as any }) return this.authRequest<ReqRes.GetAppLogsRes>({ method: Method.GET, url: `/apps/${appId}/logs`, params: params as any })
} }
async getServerLogs(): Promise<string[]> { async getServerLogs (): Promise<string[]> {
return this.authRequest<ReqRes.GetServerLogsRes>({ method: Method.GET, url: `/logs` }) return this.authRequest<ReqRes.GetServerLogsRes>({ method: Method.GET, url: `/logs` })
} }
async getAppMetrics(appId: string): Promise<AppMetrics> { async getAppMetrics (appId: string): Promise<AppMetrics> {
return this.authRequest<ReqRes.GetAppMetricsRes | string>({ method: Method.GET, url: `/apps/${appId}/metrics` }) return this.authRequest<ReqRes.GetAppMetricsRes | string>({ method: Method.GET, url: `/apps/${appId}/metrics` })
.then(parseMetricsPermissive) .then(parseMetricsPermissive)
} }
async installApp(appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> { async installApp (appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
const data: ReqRes.PostInstallAppReq = { const data: ReqRes.PostInstallAppReq = {
version, version,
} }
@@ -172,94 +172,94 @@ export class LiveApiService extends ApiService {
}) })
} }
async uninstallApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> { async uninstallApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
} }
async startApp(appId: string): Promise<Unit> { async startApp (appId: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING })) .then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING }))
.then(() => ({})) .then(() => ({ }))
} }
async stopApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> { async stopApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 60000 }) const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }, modulateTime(new Date(), 5, 'seconds')) if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }, modulateTime(new Date(), 5, 'seconds'))
return res return res
} }
async restartApp(appId: string): Promise<Unit> { async restartApp (appId: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 60000 })
.then(() => ({} as any)) .then(() => ({ } as any))
} }
async createAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> { async createAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
const data: ReqRes.PostAppBackupCreateReq = { const data: ReqRes.PostAppBackupCreateReq = {
password: password || undefined, password: password || undefined,
logicalname, logicalname,
} }
return this.authRequest<ReqRes.PostAppBackupCreateRes>({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 60000 }) return this.authRequest<ReqRes.PostAppBackupCreateRes>({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP })) .then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP }))
.then(() => ({})) .then(() => ({ }))
} }
async stopAppBackup(appId: string): Promise<Unit> { async stopAppBackup (appId: string): Promise<Unit> {
return this.authRequest<ReqRes.PostAppBackupStopRes>({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 60000 }) return this.authRequest<ReqRes.PostAppBackupStopRes>({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED })) .then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED }))
.then(() => ({})) .then(() => ({ }))
} }
async restoreAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> { async restoreAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
const data: ReqRes.PostAppBackupRestoreReq = { const data: ReqRes.PostAppBackupRestoreReq = {
password: password || undefined, password: password || undefined,
logicalname, logicalname,
} }
return this.authRequest<ReqRes.PostAppBackupRestoreRes>({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 60000 }) return this.authRequest<ReqRes.PostAppBackupRestoreRes>({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP })) .then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP }))
.then(() => ({})) .then(() => ({ }))
} }
async patchAppConfig(app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> { async patchAppConfig (app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> {
const data: ReqRes.PatchAppConfigReq = { const data: ReqRes.PatchAppConfigReq = {
config, config,
} }
return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 60000 }) return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 60000 })
} }
async postConfigureDependency(dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> { async postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
} }
async patchServerConfig(attr: string, value: any): Promise<Unit> { async patchServerConfig (attr: string, value: any): Promise<Unit> {
const data: ReqRes.PatchServerConfigReq = { const data: ReqRes.PatchServerConfigReq = {
value, value,
} }
return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 60000 }) return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 60000 })
.then(() => this.serverModel.update({ [attr]: value })) .then(() => this.serverModel.update({ [attr]: value }))
.then(() => ({})) .then(() => ({ }))
} }
async wipeAppData(app: AppInstalledPreview): Promise<Unit> { async wipeAppData (app: AppInstalledPreview): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 60000 }).then((res) => { return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 60000 }).then((res) => {
this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG }) this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG })
return res return res
}) })
} }
async toggleAppLAN(appId: string, toggle: 'enable' | 'disable'): Promise<Unit> { async toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` })
} }
async addSSHKey(sshKey: string): Promise<Unit> { async addSSHKey (sshKey: string): Promise<Unit> {
const data: ReqRes.PostAddSSHKeyReq = { const data: ReqRes.PostAddSSHKeyReq = {
sshKey, sshKey,
} }
const fingerprint = await this.authRequest<ReqRes.PostAddSSHKeyRes>({ method: Method.POST, url: `/sshKeys`, data }) const fingerprint = await this.authRequest<ReqRes.PostAddSSHKeyRes>({ method: Method.POST, url: `/sshKeys`, data })
this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] }) this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] })
return {} return { }
} }
async addWifi(ssid: string, password: string, country: string, connect: boolean): Promise<Unit> { async addWifi (ssid: string, password: string, country: string, connect: boolean): Promise<Unit> {
const data: ReqRes.PostAddWifiReq = { const data: ReqRes.PostAddWifiReq = {
ssid, ssid,
password, password,
@@ -269,30 +269,30 @@ export class LiveApiService extends ApiService {
return this.authRequest({ method: Method.POST, url: `/wifi`, data }) return this.authRequest({ method: Method.POST, url: `/wifi`, data })
} }
async connectWifi(ssid: string): Promise<Unit> { async connectWifi (ssid: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) }) return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) })
} }
async deleteWifi(ssid: string): Promise<Unit> { async deleteWifi (ssid: string): Promise<Unit> {
return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) }) return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) })
} }
async deleteSSHKey(fingerprint: SSHFingerprint): Promise<Unit> { async deleteSSHKey (fingerprint: SSHFingerprint): Promise<Unit> {
await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` }) await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` })
const ssh = this.serverModel.peek().ssh const ssh = this.serverModel.peek().ssh
this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) }) this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) })
return {} return { }
} }
async restartServer(): Promise<Unit> { async restartServer (): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 60000 })
} }
async shutdownServer(): Promise<Unit> { async shutdownServer (): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 })
} }
async serviceAction(appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> { async serviceAction (appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
const data: ReqRes.ServiceActionRequest = { const data: ReqRes.ServiceActionRequest = {
jsonrpc: '2.0', jsonrpc: '2.0',
id: uuid.v4(), id: uuid.v4(),
@@ -301,11 +301,15 @@ export class LiveApiService extends ApiService {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/actions`, data, readTimeout: 300000 }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/actions`, data, readTimeout: 300000 })
} }
async refreshLAN(): Promise<Unit> { async refreshLAN (): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: '/network/lan/reset' }) return this.authRequest({ method: Method.POST, url: '/network/lan/reset' })
} }
private async authRequest<T>(opts: HttpOptions, overrides: Partial<{ version: string }> = {}): Promise<T> { async checkV1Status (): Promise<V1Status> {
return this.http.request({ method: Method.GET, url: 'https://registry.start9labs.com/sys/status' })
}
private async authRequest<T> (opts: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`) if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`)
opts.withCredentials = true opts.withCredentials = true
@@ -324,7 +328,7 @@ const dryRunParam = (dryRun: boolean, first: boolean) => {
return first ? `?dryrun` : `&dryrun` return first ? `?dryrun` : `&dryrun`
} }
function catchHttpStatusError(error: HttpErrorResponse): Observable<true> { function catchHttpStatusError (error: HttpErrorResponse): Observable<true> {
if (error.error instanceof ErrorEvent) { if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly. // A client-side or network error occurred. Handle it accordingly.
return throwError('Not Connected') return throwError('Not Connected')

View File

@@ -4,7 +4,7 @@ import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalle
import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model' import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model'
import { pauseFor } from '../../util/misc.util' import { pauseFor } from '../../util/misc.util'
import { ApiService, ReqRes } from './api.service' import { ApiService, ReqRes } from './api.service'
import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit } from './api-types' import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit, V1Status } from './api-types'
import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util' import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util'
import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures' import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures'
import { ConfigService } from '../config.service' import { ConfigService } from '../config.service'
@@ -273,12 +273,19 @@ export class MockApiService extends ApiService {
return mockRefreshLAN() return mockRefreshLAN()
} }
async checkV1Status (): Promise<V1Status> {
return {
status: 'instructions',
version: '1.0.0',
}
}
private hasUI (app: ApiAppInstalledPreview): boolean { private hasUI (app: ApiAppInstalledPreview): boolean {
return app.lanUi || app.torUi return app.lanUi || app.torUi
} }
private isLaunchable (app: ApiAppInstalledPreview): boolean { private isLaunchable (app: ApiAppInstalledPreview): boolean {
return !this.config.isConsulate && return !this.config.isConsulate &&
app.status === AppStatus.RUNNING && app.status === AppStatus.RUNNING &&
( (
(app.torAddress && app.torUi && this.config.isTor()) || (app.torAddress && app.torUi && this.config.isTor()) ||
@@ -355,7 +362,7 @@ async function mockGetServerLogs (): Promise<ReqRes.GetServerLogsRes> {
async function mockGetAppMetrics (): Promise<ReqRes.GetAppMetricsRes> { async function mockGetAppMetrics (): Promise<ReqRes.GetAppMetricsRes> {
await pauseFor(1000) await pauseFor(1000)
return mockApiAppMetricsV1 return mockApiAppMetricsV1 as ReqRes.GetAppMetricsRes
} }
async function mockGetAvailableAppVersionInfo (): Promise<ReqRes.GetAppAvailableVersionInfoRes> { async function mockGetAvailableAppVersionInfo (): Promise<ReqRes.GetAppAvailableVersionInfoRes> {
@@ -492,7 +499,7 @@ const mockApiNotifications: ReqRes.GetNotificationsRes = [
const mockApiServer: () => ReqRes.GetServerRes = () => ({ const mockApiServer: () => ReqRes.GetServerRes = () => ({
serverId: 'start9-mockxyzab', serverId: 'start9-mockxyzab',
name: 'Embassy:12345678', name: 'Embassy:12345678',
versionInstalled: '0.2.12', versionInstalled: '0.2.13',
versionLatest: '0.2.13', versionLatest: '0.2.13',
status: ServerStatus.RUNNING, status: ServerStatus.RUNNING,
alternativeRegistryUrl: 'beta-registry.start9labs.com', alternativeRegistryUrl: 'beta-registry.start9labs.com',

View File

@@ -54,6 +54,8 @@ export const bitcoinI: ApiAppInstalledFull = {
versionInstalled: '0.18.1', versionInstalled: '0.18.1',
lanAddress: undefined, lanAddress: undefined,
title: 'Bitcoin Core', title: 'Bitcoin Core',
licenseName: 'MIT',
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion', torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion',
startAlert: 'Bitcoind could take a loooooong time to start. Please be patient.', startAlert: 'Bitcoind could take a loooooong time to start. Please be patient.',
status: AppStatus.STOPPED, status: AppStatus.STOPPED,
@@ -147,6 +149,8 @@ export const bitcoinA: AppAvailableFull = {
id: 'bitcoind', id: 'bitcoind',
versionLatest: '0.19.1.1', versionLatest: '0.19.1.1',
versionInstalled: '0.19.0', versionInstalled: '0.19.0',
licenseName: 'MIT',
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
status: AppStatus.UNKNOWN, status: AppStatus.UNKNOWN,
title: 'Bitcoin Core', title: 'Bitcoin Core',
descriptionShort: 'Bitcoin is an innovative payment network and new kind of money.', descriptionShort: 'Bitcoin is an innovative payment network and new kind of money.',

View File

@@ -5,6 +5,7 @@ import { WizardBaker } from '../components/install-wizard/prebaked-wizards'
import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page' import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
import { S9Server } from '../models/server-model' import { S9Server } from '../models/server-model'
import { displayEmver } from '../pipes/emver.pipe' import { displayEmver } from '../pipes/emver.pipe'
import { V1Status } from './api/api-types'
import { ApiService, ReqRes } from './api/api.service' import { ApiService, ReqRes } from './api/api.service'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { Emver } from './emver.service' import { Emver } from './emver.service'
@@ -36,6 +37,13 @@ export class StartupAlertsNotifier {
display: vl => this.displayOsUpdateCheck(vl), display: vl => this.displayOsUpdateCheck(vl),
hasRun: this.config.skipStartupAlerts, hasRun: this.config.skipStartupAlerts,
} }
const v1StatusUpdate: Check<V1Status> = {
name: 'v1Status',
shouldRun: s => this.shouldRunOsUpdateCheck(s),
check: () => this.v1StatusCheck(),
display: s => this.displayV1Check(s),
hasRun: this.config.skipStartupAlerts,
}
const apps: Check<boolean> = { const apps: Check<boolean> = {
name: 'apps', name: 'apps',
shouldRun: s => this.shouldRunAppsCheck(s), shouldRun: s => this.shouldRunAppsCheck(s),
@@ -43,7 +51,7 @@ export class StartupAlertsNotifier {
display: () => this.displayAppsCheck(), display: () => this.displayAppsCheck(),
hasRun: this.config.skipStartupAlerts, hasRun: this.config.skipStartupAlerts,
} }
this.checks = [welcome, osUpdate, apps] this.checks = [welcome, osUpdate, v1StatusUpdate, apps]
} }
// This takes our three checks and filters down to those that should run. // This takes our three checks and filters down to those that should run.
@@ -85,6 +93,10 @@ export class StartupAlertsNotifier {
return server.autoCheckUpdates return server.autoCheckUpdates
} }
private async v1StatusCheck (): Promise<V1Status> {
return this.apiService.checkV1Status()
}
private async osUpdateCheck (s: Readonly<S9Server>): Promise<ReqRes.GetVersionLatestRes | undefined> { private async osUpdateCheck (s: Readonly<S9Server>): Promise<ReqRes.GetVersionLatestRes | undefined> {
const res = await this.apiService.getVersionLatest() const res = await this.apiService.getVersionLatest()
return this.osUpdateService.updateIsAvailable(s.versionInstalled, res) ? res : undefined return this.osUpdateService.updateIsAvailable(s.versionInstalled, res) ? res : undefined
@@ -131,6 +143,33 @@ export class StartupAlertsNotifier {
return true return true
} }
private async displayV1Check (s: V1Status): Promise<boolean> {
return new Promise(async resolve => {
if (s.status !== 'available') return resolve(true)
const alert = await this.alertCtrl.create({
backdropDismiss: true,
header: `EmbassyOS ${s.version} Now Available!`,
message: `Version ${s.version} introduces SSD support and a whole lot more.`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
handler: () => resolve(true),
},
{
text: 'View Instructions',
handler: () => {
window.open(`https://start9.com/eos-${s.version}`, '_blank')
resolve(false)
},
},
],
})
await alert.present()
})
}
private async displayAppsCheck (): Promise<boolean> { private async displayAppsCheck (): Promise<boolean> {
return new Promise(async resolve => { return new Promise(async resolve => {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({