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
48 changes: 48 additions & 0 deletions backend/src/routes/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use std::process::Command;
use tokio::fs;

use crate::AppState;

#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateStatus {
pub status: String,
pub message: String,
pub progress: u8,
pub version: Option<String>,
pub timestamp: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VersionInfo {
pub current_version: String,
Expand Down Expand Up @@ -106,6 +116,43 @@ pub async fn check_updates(State(_state): State<AppState>) -> Result<Json<Versio
}))
}

/// Get update status
#[utoipa::path(
get,
path = "/api/updates/status",
responses(
(status = 200, description = "Update status retrieved successfully", body = UpdateStatus),
(status = 404, description = "No update in progress"),
(status = 500, description = "Failed to read status")
),
tag = "Updates"
)]
pub async fn get_update_status(
State(_state): State<AppState>,
) -> Result<Json<UpdateStatus>, AppError> {
let status_file = "/tmp/csf-core-update-status.json";

match tokio::fs::read_to_string(status_file).await {
Ok(content) => match serde_json::from_str::<UpdateStatus>(&content) {
Ok(status) => Ok(Json(status)),
Err(e) => Err(AppError::InternalError(format!(
"Failed to parse status file: {}",
e
))),
},
Err(_) => {
// No status file means no update in progress
Ok(Json(UpdateStatus {
status: "idle".to_string(),
message: "No update in progress".to_string(),
progress: 0,
version: None,
timestamp: None,
}))
}
}
}

/// Trigger update installation
#[utoipa::path(
post,
Expand Down Expand Up @@ -312,6 +359,7 @@ impl IntoResponse for AppError {
pub fn router() -> Router<AppState> {
Router::new()
.route("/updates/check", get(check_updates))
.route("/updates/status", get(get_update_status))
.route("/updates/install", post(install_update))
.route("/updates/changelog/:version", get(get_changelog))
}
202 changes: 202 additions & 0 deletions frontend/src/lib/components/UpdateScreen.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { fade } from 'svelte/transition';
import { Progress } from '$lib/components/ui/progress';
import Logo from '$lib/assets/logo.svg';

let updateStatus = $state({
status: 'idle',
message: 'Initializing update...',
progress: 0,
version: '',
timestamp: '',
});

let logs: string[] = $state([]);
let logsContainer: HTMLDivElement;
let pollInterval: number;

async function fetchUpdateStatus() {
try {
const response = await fetch('/api/updates/status');
if (response.ok) {
const data = await response.json();

// Only add to logs if message changed
if (data.message !== updateStatus.message) {
logs = [...logs, `${new Date().toLocaleTimeString()}: ${data.message}`];

// Auto-scroll to bottom
setTimeout(() => {
if (logsContainer) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
}, 10);
}

updateStatus = data;

// If update is completed or error, stop polling
if (data.status === 'completed') {
setTimeout(() => {
// Reload the page after 2 seconds
window.location.reload();
}, 2000);
} else if (data.status === 'error') {
// Stop polling on error
if (pollInterval) {
clearInterval(pollInterval);
}
}
}
} catch (error) {
console.error('Failed to fetch update status:', error);
}
}

onMount(() => {
// Initial fetch
fetchUpdateStatus();

// Poll every second
pollInterval = setInterval(fetchUpdateStatus, 1000);
});

onDestroy(() => {
if (pollInterval) {
clearInterval(pollInterval);
}
});

function getStatusColor() {
switch (updateStatus.status) {
case 'completed':
return 'text-green-500';
case 'error':
return 'text-red-500';
case 'in_progress':
return 'text-blue-500';
default:
return 'text-gray-500';
}
}

function getStatusIcon() {
switch (updateStatus.status) {
case 'completed':
return '✅';
case 'error':
return '❌';
case 'in_progress':
return '⏳';
default:
return '🔄';
}
}
</script>

<div
class="fixed inset-0 z-[9999] flex items-center justify-center bg-background"
transition:fade={{ duration: 300 }}
>
<div class="w-full max-w-2xl px-8">
<!-- Logo -->
<div class="mb-12 flex justify-center">
<img src={Logo} alt="CSF Logo" class="h-24 w-24" />
</div>

<!-- Title -->
<h1 class="mb-2 text-center text-3xl font-bold">
{getStatusIcon()} System Update in Progress
</h1>

<!-- Version Info -->
{#if updateStatus.version}
<p class="mb-8 text-center text-muted-foreground">
Updating to version <span class="font-semibold">{updateStatus.version}</span>
</p>
{/if}

<!-- Progress Bar -->
<div class="mb-8">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium {getStatusColor()}">
{updateStatus.message}
</span>
<span class="text-sm text-muted-foreground">
{updateStatus.progress}%
</span>
</div>
<Progress value={updateStatus.progress} class="h-3" />
</div>

<!-- Status Message -->
<div class="mb-6 rounded-lg border bg-card p-4">
<h3 class="mb-2 text-sm font-semibold">Current Status:</h3>
<p class="text-sm text-muted-foreground {getStatusColor()}">
{updateStatus.message}
</p>
</div>

<!-- Logs -->
<div class="rounded-lg border bg-card">
<div class="border-b p-3">
<h3 class="text-sm font-semibold">Update Log</h3>
</div>
<div bind:this={logsContainer} class="max-h-64 overflow-y-auto p-3 font-mono text-xs">
{#if logs.length === 0}
<p class="text-muted-foreground">Waiting for update logs...</p>
{:else}
{#each logs as log}
<div class="mb-1 text-muted-foreground">
{log}
</div>
{/each}
{/if}
</div>
</div>

<!-- Warning -->
<div class="mt-6 text-center text-sm text-muted-foreground">
<p>⚠️ Please do not close this window or refresh the page.</p>
<p>The system will automatically reload when the update is complete.</p>
</div>

<!-- Error Recovery -->
{#if updateStatus.status === 'error'}
<div class="mt-6 rounded-lg border border-destructive bg-destructive/10 p-4">
<p class="mb-2 text-sm font-semibold text-destructive">Update Failed</p>
<p class="mb-4 text-sm text-muted-foreground">
An error occurred during the update process. The system may have been rolled back to the
previous version.
</p>
<button
onclick={() => window.location.reload()}
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Reload Application
</button>
</div>
{/if}
</div>
</div>

<style>
/* Smooth scrolling for logs */
div::-webkit-scrollbar {
width: 8px;
}

div::-webkit-scrollbar-track {
background: transparent;
}

div::-webkit-scrollbar-thumb {
background: hsl(var(--muted));
border-radius: 4px;
}

div::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground));
}
</style>
6 changes: 5 additions & 1 deletion frontend/src/lib/components/settings/UpdateSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Switch } from '$lib/components/ui/switch';
import { Label } from '$lib/components/ui/label';
import { updateStore } from '$lib/stores/updates';
import { updateInProgress, updateVersion } from '$lib/stores/update';
import {
Download,
RefreshCw,
Expand Down Expand Up @@ -60,12 +61,15 @@
try {
const response = await updateStore.installUpdate(version);

// Trigger update screen
updateVersion.set(version);
updateInProgress.set(true);

message = response.message;
messageType = 'success';
} catch (error) {
message = error instanceof Error ? error.message : 'Unbekannter Fehler';
messageType = 'error';
} finally {
isInstalling = false;
}
}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/lib/stores/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { writable } from 'svelte/store';

export const updateInProgress = writable(false);
export const updateVersion = writable<string | null>(null);
7 changes: 7 additions & 0 deletions frontend/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import favicon from '$lib/assets/favicon.svg';
import { onMount, onDestroy } from 'svelte';
import AppSidebar from '$lib/components/navbar/app-sidebar.svelte';
import UpdateScreen from '$lib/components/UpdateScreen.svelte';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import { Separator } from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { theme, effectiveTheme, initThemeFromStorage } from '$lib/stores/theme';
import { updateInProgress } from '$lib/stores/update';
import { page } from '$app/stores';
import { authStore } from '$lib/stores/auth';
import { ApiClient } from '$lib/services/api-client';
Expand Down Expand Up @@ -62,6 +64,11 @@
<link rel="icon" href={favicon} />
</svelte:head>

<!-- Update Screen Overlay -->
{#if $updateInProgress}
<UpdateScreen />
{/if}

{#if showSidebar}
<Sidebar.Provider>
<AppSidebar />
Expand Down
Loading
Loading