From da27ed250e59b1d44cf25caa85c27e47789c36dc Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 4 Sep 2024 10:46:12 +0530 Subject: [PATCH] feat: show startup error (#571) * feat: show startup error * chore: move startup error var to api * chore: simplify start error display * fix: startup error on setup page --------- Co-authored-by: Roland Bewick --- api/api.go | 32 ++++++++++++++++------ api/models.go | 28 ++++++++++--------- frontend/src/screens/Start.tsx | 21 ++++++++++++-- frontend/src/screens/setup/SetupFinish.tsx | 25 ++++++++++++++++- frontend/src/types.ts | 2 ++ http/http_service.go | 13 +-------- wails/wails_handlers.go | 12 ++------ 7 files changed, 88 insertions(+), 45 deletions(-) diff --git a/api/api.go b/api/api.go index e66b09fe..7aabfbed 100644 --- a/api/api.go +++ b/api/api.go @@ -32,13 +32,15 @@ import ( ) type api struct { - db *gorm.DB - dbSvc db.DBService - cfg config.Config - svc service.Service - permissionsSvc permissions.PermissionsService - keys keys.Keys - albyOAuthSvc alby.AlbyOAuthService + db *gorm.DB + dbSvc db.DBService + cfg config.Config + svc service.Service + permissionsSvc permissions.PermissionsService + keys keys.Keys + albyOAuthSvc alby.AlbyOAuthService + startupError error + startupErrorTime time.Time } func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albyOAuthSvc alby.AlbyOAuthService, eventPublisher events.EventPublisher) *api { @@ -648,6 +650,10 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info := InfoResponse{} backendType, _ := api.cfg.Get("LNBackendType", "") info.SetupCompleted = api.cfg.SetupCompleted() + if api.startupError != nil { + info.StartupError = api.startupError.Error() + info.StartupErrorTime = api.startupErrorTime + } info.Running = api.svc.GetLNClient() != nil info.BackendType = backendType info.AlbyAuthUrl = api.albyOAuthSvc.GetAuthUrl() @@ -700,7 +706,17 @@ func (api *api) SetNextBackupReminder(backupReminderRequest *BackupReminderReque var startMutex sync.Mutex -func (api *api) Start(startRequest *StartRequest) error { +func (api *api) Start(startRequest *StartRequest) { + api.startupError = nil + err := api.StartInternal(startRequest) + if err != nil { + logger.Logger.WithError(err).Error("Failed to start node") + api.startupError = err + api.startupErrorTime = time.Now() + } +} + +func (api *api) StartInternal(startRequest *StartRequest) (err error) { if !startMutex.TryLock() { // do not allow to start twice in case this is somehow called twice return errors.New("app is already starting") diff --git a/api/models.go b/api/models.go index b7279305..1f58ba22 100644 --- a/api/models.go +++ b/api/models.go @@ -42,7 +42,7 @@ type API interface { GetInfo(ctx context.Context) (*InfoResponse, error) GetMnemonic(unlockPassword string) (*MnemonicResponse, error) SetNextBackupReminder(backupReminderRequest *BackupReminderRequest) error - Start(startRequest *StartRequest) error + Start(startRequest *StartRequest) Setup(ctx context.Context, setupRequest *SetupRequest) error SendPaymentProbes(ctx context.Context, sendPaymentProbesRequest *SendPaymentProbesRequest) (*SendPaymentProbesResponse, error) SendSpontaneousPaymentProbes(ctx context.Context, sendSpontaneousPaymentProbesRequest *SendSpontaneousPaymentProbesRequest) (*SendSpontaneousPaymentProbesResponse, error) @@ -152,18 +152,20 @@ type User struct { } type InfoResponse struct { - BackendType string `json:"backendType"` - SetupCompleted bool `json:"setupCompleted"` - OAuthRedirect bool `json:"oauthRedirect"` - Running bool `json:"running"` - Unlocked bool `json:"unlocked"` - AlbyAuthUrl string `json:"albyAuthUrl"` - NextBackupReminder string `json:"nextBackupReminder"` - AlbyUserIdentifier string `json:"albyUserIdentifier"` - AlbyAccountConnected bool `json:"albyAccountConnected"` - Version string `json:"version"` - Network string `json:"network"` - EnableAdvancedSetup bool `json:"enableAdvancedSetup"` + BackendType string `json:"backendType"` + SetupCompleted bool `json:"setupCompleted"` + OAuthRedirect bool `json:"oauthRedirect"` + Running bool `json:"running"` + Unlocked bool `json:"unlocked"` + AlbyAuthUrl string `json:"albyAuthUrl"` + NextBackupReminder string `json:"nextBackupReminder"` + AlbyUserIdentifier string `json:"albyUserIdentifier"` + AlbyAccountConnected bool `json:"albyAccountConnected"` + Version string `json:"version"` + Network string `json:"network"` + EnableAdvancedSetup bool `json:"enableAdvancedSetup"` + StartupError string `json:"startupError"` + StartupErrorTime time.Time `json:"startupErrorTime"` } type MnemonicRequest struct { diff --git a/frontend/src/screens/Start.tsx b/frontend/src/screens/Start.tsx index 2a9f2b51..529d2e55 100644 --- a/frontend/src/screens/Start.tsx +++ b/frontend/src/screens/Start.tsx @@ -24,10 +24,27 @@ export default function Start() { const [unlockPassword, setUnlockPassword] = React.useState(""); const [loading, setLoading] = React.useState(false); const [buttonText, setButtonText] = React.useState("Login"); - useInfo(true); // poll the info endpoint to auto-redirect when app is running + + const { data: info } = useInfo(true); // poll the info endpoint to auto-redirect when app is running const { toast } = useToast(); + const startupError = info?.startupError; + const startupErrorTime = info?.startupErrorTime; + + React.useEffect(() => { + if (startupError && startupErrorTime) { + toast({ + title: "Failed to start", + description: startupError, + variant: "destructive", + }); + setLoading(false); + setButtonText("Login"); + setUnlockPassword(""); + } + }, [startupError, toast, startupErrorTime]); + React.useEffect(() => { if (!loading) { return; @@ -54,7 +71,7 @@ export default function Start() { setButtonText("Login"); setUnlockPassword(""); return; - }, 180000); // wait for 3 minutes + }, 30000); // wait for 30 seconds return () => { clearInterval(intervalId); diff --git a/frontend/src/screens/setup/SetupFinish.tsx b/frontend/src/screens/setup/SetupFinish.tsx index a8e4a66b..61b713f4 100644 --- a/frontend/src/screens/setup/SetupFinish.tsx +++ b/frontend/src/screens/setup/SetupFinish.tsx @@ -13,11 +13,12 @@ import { AuthTokenResponse, SetupNodeInfo } from "src/types"; import { handleRequestError } from "src/utils/handleRequestError"; import { request } from "src/utils/request"; +let lastStartupErrorTime: string; export function SetupFinish() { const navigate = useNavigate(); const { nodeInfo, unlockPassword } = useSetupStore(); const { toast } = useToast(); - useInfo(true); // poll the info endpoint to auto-redirect when app is running + const { data: info } = useInfo(true); // poll the info endpoint to auto-redirect when app is running const [loading, setLoading] = React.useState(false); const [connectionError, setConnectionError] = React.useState(false); @@ -32,6 +33,28 @@ export function SetupFinish() { }, }; + const startupError = info?.startupError; + const startupErrorTime = info?.startupErrorTime; + + React.useEffect(() => { + // lastStartupErrorTime check is required because user may leave page and come back + // after re-configuring settings + if ( + startupError && + startupErrorTime && + startupErrorTime !== lastStartupErrorTime + ) { + lastStartupErrorTime = startupErrorTime; + toast({ + title: "Failed to start", + description: startupError, + variant: "destructive", + }); + setLoading(false); + setConnectionError(true); + } + }, [startupError, toast, startupErrorTime]); + useEffect(() => { if (!loading) { return; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a853946a..2864e493 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -148,6 +148,8 @@ export interface InfoResponse { version: string; unlocked: boolean; enableAdvancedSetup: boolean; + startupError: string; + startupErrorTime: string; } export type Network = "bitcoin" | "testnet" | "signet"; diff --git a/http/http_service.go b/http/http_service.go index 427284e6..32e61baf 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -223,12 +223,6 @@ func (httpSvc *HttpService) startHandler(c echo.Context) error { }) } - if !httpSvc.cfg.CheckUnlockPassword(startRequest.UnlockPassword) { - return c.JSON(http.StatusUnauthorized, ErrorResponse{ - Message: "Invalid password", - }) - } - token, err := httpSvc.createJWT(nil) if err != nil { @@ -237,12 +231,7 @@ func (httpSvc *HttpService) startHandler(c echo.Context) error { }) } - go func() { - err := httpSvc.api.Start(&startRequest) - if err != nil { - logger.Logger.WithError(err).Error("Failed to start node") - } - }() + go httpSvc.api.Start(&startRequest) return c.JSON(http.StatusOK, &authTokenResponse{ Token: token, diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index 48a44a58..8cf2af77 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -641,15 +641,9 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } - err = app.api.Start(startRequest) - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "route": route, - "method": method, - "body": body, - }).WithError(err).Error("Failed to setup node") - return WailsRequestRouterResponse{Body: nil, Error: err.Error()} - } + + go app.api.Start(startRequest) + return WailsRequestRouterResponse{Body: nil, Error: ""} case "/api/setup": setupRequest := &api.SetupRequest{}