Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .concepts/backup-restore.md
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion dash/src/components/deployments/deployment-monitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +35,10 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P
},
onError: (err) => {
console.error('Deployment error:', err);
toast.error(err);
},
onClose: () => {
handleClose();
},
});

Expand Down Expand Up @@ -111,7 +116,7 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P
)}
/>
<span className="text-muted-foreground">
{isLive
{isLive
? (isConnected ? 'Live' : 'Disconnected')
: 'Completed'
}
Expand Down
19 changes: 18 additions & 1 deletion dash/src/hooks/use-deployment-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ interface UseDeploymentMonitorOptions {
enabled: boolean;
onComplete?: () => void;
onError?: (error: string) => void;
onClose?: () => void;
}

export const useDeploymentMonitor = ({
deploymentId,
enabled,
onComplete,
onError,
onClose,
}: UseDeploymentMonitorOptions) => {
const [logs, setLogs] = useState<string[]>([]);
const [status, setStatus] = useState<StatusUpdate | null>(null);
Expand Down Expand Up @@ -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');
}

Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions server/api/handlers/deployments/getCompletedLogs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
59 changes: 50 additions & 9 deletions server/api/handlers/deployments/logsHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
},
}:
}
}
}
}()
Expand Down
3 changes: 3 additions & 0 deletions server/websockets/logWatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down