diff --git a/.concepts/backup-restore.md b/.concepts/backup-restore.md new file mode 100644 index 0000000..320c4cd --- /dev/null +++ b/.concepts/backup-restore.md @@ -0,0 +1,20 @@ +# feature + +we'll support backup and restore, on the same system and also on different systems, you can backup your +current state of mist into something like `bak.mist` or something, will be decided later, using the cli as well as ui (you should be able to download it through the ui), and on the same system or let's say a different system, assuming mist is already installed using the install script, `mist-cli` is also installed with it user should be able to do something like `mist-cli restore /path/to/bak.mist`, and this command should restore the backed up state, exactly as it was, with same database (almost), all the config files, logs, and all the apps up and running (atleast in the queue, and ready to be build and deployed). + + +## considerations + +1. we can't just replace the database file `mist.db`: + +let's say the backup was made at version `v1.0.2` and the restoration was done with the mist of version `v1.0.8` installed it will fuck up: + - the versioning + - new migrations (if any) + +so we need to iterate through the old db from `bak.mist` and push the data into the new db present at `/var/lib/mist/mist.db` + +2. deployments can't be replayed normally + +in current implementation we deploy from the latest commit, but during the restoration we can't do that, +becuase let's say in between backup and restore i pushed 4 new commits to the project, then if we deploy the latest commit it won't truly restore the original state diff --git a/dash/src/components/deployments/deployment-monitor.tsx b/dash/src/components/deployments/deployment-monitor.tsx index ff318a6..47a6101 100644 --- a/dash/src/components/deployments/deployment-monitor.tsx +++ b/dash/src/components/deployments/deployment-monitor.tsx @@ -10,6 +10,7 @@ import { Badge } from '@/components/ui/badge'; import { Terminal, X, CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react'; import { useDeploymentMonitor } from '@/hooks'; import { cn } from '@/lib/utils'; +import { toast } from 'sonner'; interface Props { deploymentId: number; @@ -34,6 +35,10 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P }, onError: (err) => { console.error('Deployment error:', err); + toast.error(err); + }, + onClose: () => { + handleClose(); }, }); @@ -111,7 +116,7 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P )} /> - {isLive + {isLive ? (isConnected ? 'Live' : 'Disconnected') : 'Completed' } diff --git a/dash/src/hooks/use-deployment-monitor.ts b/dash/src/hooks/use-deployment-monitor.ts index 3bdd623..e04797f 100644 --- a/dash/src/hooks/use-deployment-monitor.ts +++ b/dash/src/hooks/use-deployment-monitor.ts @@ -6,6 +6,7 @@ interface UseDeploymentMonitorOptions { enabled: boolean; onComplete?: () => void; onError?: (error: string) => void; + onClose?: () => void; } export const useDeploymentMonitor = ({ @@ -13,6 +14,7 @@ export const useDeploymentMonitor = ({ enabled, onComplete, onError, + onClose, }: UseDeploymentMonitorOptions) => { const [logs, setLogs] = useState([]); const [status, setStatus] = useState(null); @@ -43,6 +45,16 @@ export const useDeploymentMonitor = ({ hasFetchedRef.current = true; return; } + if (response.status === 404) { + const result = await response.json(); + const errorMsg = result.message || 'Deployment log file not found'; + setError(errorMsg); + onError?.(errorMsg); + onClose?.(); + setIsLoading(false); + hasFetchedRef.current = true; + return; + } throw new Error('Failed to fetch deployment logs'); } @@ -146,6 +158,11 @@ export const useDeploymentMonitor = ({ console.error('[DeploymentMonitor] Error event:', errorMsg); setError(errorMsg); onError?.(errorMsg); + + // If it's a log file not found error, close the viewer + if (errorMsg.includes('log file not found') || errorMsg.includes('Failed to read deployment logs')) { + onClose?.(); + } break; } } @@ -194,7 +211,7 @@ export const useDeploymentMonitor = ({ setError('Failed to establish connection'); setIsLoading(false); } - }, [deploymentId, enabled, isLive, onComplete, onError]); + }, [deploymentId, enabled, isLive, onComplete, onError, onClose]); useEffect(() => { if (enabled) { diff --git a/server/api/handlers/deployments/getCompletedLogs.go b/server/api/handlers/deployments/getCompletedLogs.go index 2d719ab..e843acf 100644 --- a/server/api/handlers/deployments/getCompletedLogs.go +++ b/server/api/handlers/deployments/getCompletedLogs.go @@ -89,6 +89,10 @@ func GetCompletedDeploymentLogsHandler(w http.ResponseWriter, r *http.Request) { logContent = string(content) } } + } else { + log.Warn().Int64("deployment_id", depId).Str("log_path", logPath).Msg("Deployment log file not found") + handlers.SendResponse(w, http.StatusNotFound, false, nil, "Deployment log file not found", "") + return } response := GetDeploymentLogsResponse{ diff --git a/server/api/handlers/deployments/logsHandler.go b/server/api/handlers/deployments/logsHandler.go index 1c62227..58d0e19 100644 --- a/server/api/handlers/deployments/logsHandler.go +++ b/server/api/handlers/deployments/logsHandler.go @@ -79,8 +79,11 @@ func LogsHandler(w http.ResponseWriter, r *http.Request) { }() go func() { + // Wait up to 10 seconds for log file to appear + fileFound := false for i := 0; i < 20; i++ { if _, err := os.Stat(logPath); err == nil { + fileFound = true break } select { @@ -90,24 +93,62 @@ func LogsHandler(w http.ResponseWriter, r *http.Request) { } } + if !fileFound { + select { + case <-ctx.Done(): + return + case events <- websockets.DeploymentEvent{ + Type: "error", + Timestamp: time.Now(), + Data: map[string]string{ + "message": "Deployment log file not found", + }, + }: + } + return + } + send := make(chan string, 100) + errChan := make(chan error, 1) go func() { - _ = websockets.WatcherLogs(ctx, logPath, send) + err := websockets.WatcherLogs(ctx, logPath, send) + if err != nil { + errChan <- err + } close(send) }() - for line := range send { + for { select { case <-ctx.Done(): return - case events <- websockets.DeploymentEvent{ - Type: "log", - Timestamp: time.Now(), - Data: websockets.LogUpdate{ - Line: line, + case err := <-errChan: + if err != nil { + events <- websockets.DeploymentEvent{ + Type: "error", + Timestamp: time.Now(), + Data: map[string]string{ + "message": "Failed to read deployment logs: " + err.Error(), + }, + } + return + } + case line, ok := <-send: + if !ok { + return + } + select { + case <-ctx.Done(): + return + case events <- websockets.DeploymentEvent{ + Type: "log", Timestamp: time.Now(), - }, - }: + Data: websockets.LogUpdate{ + Line: line, + Timestamp: time.Now(), + }, + }: + } } } }() diff --git a/server/websockets/logWatcher.go b/server/websockets/logWatcher.go index 29f6956..a116631 100644 --- a/server/websockets/logWatcher.go +++ b/server/websockets/logWatcher.go @@ -11,6 +11,9 @@ import ( func WatcherLogs(ctx context.Context, filePath string, send chan<- string) error { file, err := os.Open(filePath) if err != nil { + if os.IsNotExist(err) { + return err + } return err } defer file.Close()