diff --git a/backend/src/routes/updates.rs b/backend/src/routes/updates.rs index b287db7d..bdf8e004 100644 --- a/backend/src/routes/updates.rs +++ b/backend/src/routes/updates.rs @@ -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, + pub timestamp: Option, +} + #[derive(Debug, Serialize, Deserialize)] pub struct VersionInfo { pub current_version: String, @@ -106,6 +116,43 @@ pub async fn check_updates(State(_state): State) -> Result, +) -> Result, 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::(&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, @@ -312,6 +359,7 @@ impl IntoResponse for AppError { pub fn router() -> Router { 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)) } diff --git a/frontend/src/lib/components/UpdateScreen.svelte b/frontend/src/lib/components/UpdateScreen.svelte new file mode 100644 index 00000000..0cff8f9c --- /dev/null +++ b/frontend/src/lib/components/UpdateScreen.svelte @@ -0,0 +1,202 @@ + + +
+
+ +
+ CSF Logo +
+ + +

+ {getStatusIcon()} System Update in Progress +

+ + + {#if updateStatus.version} +

+ Updating to version {updateStatus.version} +

+ {/if} + + +
+
+ + {updateStatus.message} + + + {updateStatus.progress}% + +
+ +
+ + +
+

Current Status:

+

+ {updateStatus.message} +

+
+ + +
+
+

Update Log

+
+
+ {#if logs.length === 0} +

Waiting for update logs...

+ {:else} + {#each logs as log} +
+ {log} +
+ {/each} + {/if} +
+
+ + +
+

⚠️ Please do not close this window or refresh the page.

+

The system will automatically reload when the update is complete.

+
+ + + {#if updateStatus.status === 'error'} +
+

Update Failed

+

+ An error occurred during the update process. The system may have been rolled back to the + previous version. +

+ +
+ {/if} +
+
+ + diff --git a/frontend/src/lib/components/settings/UpdateSettings.svelte b/frontend/src/lib/components/settings/UpdateSettings.svelte index ddc69054..f3bcd0d2 100644 --- a/frontend/src/lib/components/settings/UpdateSettings.svelte +++ b/frontend/src/lib/components/settings/UpdateSettings.svelte @@ -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, @@ -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; } } diff --git a/frontend/src/lib/stores/update.ts b/frontend/src/lib/stores/update.ts new file mode 100644 index 00000000..65f16c9a --- /dev/null +++ b/frontend/src/lib/stores/update.ts @@ -0,0 +1,4 @@ +import { writable } from 'svelte/store'; + +export const updateInProgress = writable(false); +export const updateVersion = writable(null); diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 2cd76ecb..2fef25f7 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -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'; @@ -62,6 +64,11 @@ + +{#if $updateInProgress} + +{/if} + {#if showSidebar} diff --git a/scripts/update.sh b/scripts/update.sh index 0973087c..f377ec7f 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -9,114 +9,168 @@ VERSION="${1}" REPO="CS-Foundry/CSF-Core" INSTALL_DIR="/opt/csf-core" BACKUP_DIR="/tmp/csf-core-backup-$(date +%s)" +STATUS_FILE="/tmp/csf-core-update-status.json" log() { - echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" + local message="[$(date +'%Y-%m-%d %H:%M:%S')] $1" + echo "$message" + update_status "in_progress" "$1" } error() { - echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1" >&2 + local message="[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1" + echo "$message" >&2 + update_status "error" "$1" exit 1 } +update_status() { + local status="$1" + local message="$2" + local progress="${3:-0}" + + cat > "$STATUS_FILE" <" fi -log "Starting CSF-Core update to version ${VERSION}..." +log "πŸš€ Starting CSF-Core update to version ${VERSION}..." 5 # Check if running as root or with sudo if [ "$EUID" -eq 0 ]; then SUDO="" + log "βœ“ Running with root privileges" 10 else SUDO="sudo" + log "βœ“ Running with sudo" 10 fi # Create backup directory -log "Creating backup at ${BACKUP_DIR}..." +log "πŸ“¦ Creating backup directory at ${BACKUP_DIR}..." 15 $SUDO mkdir -p "${BACKUP_DIR}" # Backup current installation if [ -d "${INSTALL_DIR}" ]; then - log "Backing up current installation..." + log "πŸ’Ύ Backing up current installation (this may take a moment)..." 20 $SUDO cp -r "${INSTALL_DIR}" "${BACKUP_DIR}/" + log "βœ“ Backup completed successfully" 25 +else + log "⚠ No existing installation found to backup" 25 fi # Stop the service if running if systemctl is-active --quiet csf-core.service 2>/dev/null; then - log "Stopping CSF-Core service..." - $SUDO systemctl stop csf-core.service || log "Service stop failed or not installed" + log "⏸️ Stopping CSF-Core service..." 30 + $SUDO systemctl stop csf-core.service || log "⚠ Service stop failed or not installed" + log "βœ“ Service stopped" 35 +else + log "ℹ️ Service not running" 35 fi -# Download the new version -DOWNLOAD_URL="https://github.com/${REPO}/archive/refs/tags/v${VERSION}.tar.gz" +# Detect architecture +log "πŸ” Detecting system architecture..." 40 +ARCH=$(uname -m) +case $ARCH in + x86_64) + ARCH_NAME="amd64" + ;; + aarch64|arm64) + ARCH_NAME="arm64" + ;; + *) + error "Unsupported architecture: $ARCH" + ;; +esac +log "βœ“ Detected architecture: $ARCH_NAME" 42 + +# Detect OS +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +log "βœ“ Detected OS: $OS" 45 + +# Download the new binaries +log "πŸ“₯ Downloading backend binary for version ${VERSION}..." 50 +BACKEND_URL="https://github.com/${REPO}/releases/download/v${VERSION}/csf-backend-${OS}-${ARCH_NAME}" TMP_DIR=$(mktemp -d) -log "Downloading version ${VERSION} from ${DOWNLOAD_URL}..." -if ! curl -L -o "${TMP_DIR}/csf-core.tar.gz" "${DOWNLOAD_URL}"; then - error "Failed to download update from ${DOWNLOAD_URL}" +if ! curl -L -f -o "${TMP_DIR}/backend" "${BACKEND_URL}" 2>&1 | grep -v "^[0-9 ]*$" || true; then + error "Failed to download backend from ${BACKEND_URL}" fi +log "βœ“ Backend binary downloaded" 60 -# Extract the archive -log "Extracting archive..." -cd "${TMP_DIR}" -tar -xzf csf-core.tar.gz || error "Failed to extract archive" +# Download frontend package +log "πŸ“₯ Downloading frontend package..." 65 +FRONTEND_URL="https://github.com/${REPO}/releases/download/v${VERSION}/csf-frontend-${VERSION}.tar.gz" -# Find the extracted directory (should be CSF-Core-) -EXTRACTED_DIR=$(find "${TMP_DIR}" -maxdepth 1 -type d -name "CSF-Core-*" | head -1) -if [ -z "$EXTRACTED_DIR" ]; then - error "Could not find extracted directory" +if ! curl -L -f -o "${TMP_DIR}/frontend.tar.gz" "${FRONTEND_URL}" 2>&1 | grep -v "^[0-9 ]*$" || true; then + log "⚠ Frontend package not found, will try to keep existing frontend" fi -log "Extracted to ${EXTRACTED_DIR}" +if [ -f "${TMP_DIR}/frontend.tar.gz" ]; then + log "βœ“ Frontend package downloaded" 70 +fi -# Install the update -log "Installing update..." -cd "${EXTRACTED_DIR}" +# Install the binaries +log "πŸ“¦ Installing backend binary..." 75 +$SUDO mkdir -p "${INSTALL_DIR}/backend" +$SUDO cp "${TMP_DIR}/backend" "${INSTALL_DIR}/backend/" +$SUDO chmod +x "${INSTALL_DIR}/backend/backend" +log "βœ“ Backend installed" 80 + +# Install frontend if downloaded +if [ -f "${TMP_DIR}/frontend.tar.gz" ]; then + log "πŸ“¦ Installing frontend..." 85 + $SUDO mkdir -p "${INSTALL_DIR}/frontend" + $SUDO tar -xzf "${TMP_DIR}/frontend.tar.gz" -C "${INSTALL_DIR}/frontend/" + log "βœ“ Frontend installed" 90 +fi -# Run installation script if it exists -if [ -f "scripts/install.sh" ]; then - log "Running installation script..." - $SUDO bash scripts/install.sh || error "Installation script failed" -else - # Manual installation fallback - log "No installation script found, performing manual update..." - - # Copy files to installation directory - $SUDO mkdir -p "${INSTALL_DIR}" - $SUDO cp -r ./* "${INSTALL_DIR}/" +# Set ownership +log "πŸ” Setting correct permissions..." 92 +$SUDO chown -R csf-core:csf-core "${INSTALL_DIR}" 2>/dev/null || $SUDO chown -R root:root "${INSTALL_DIR}" +log "βœ“ Permissions set" 95 + +# Restart service +log "πŸ”„ Restarting CSF-Core service..." 97 +if systemctl list-unit-files | grep -q csf-core.service; then + $SUDO systemctl daemon-reload + $SUDO systemctl start csf-core.service + sleep 2 - # Set permissions - $SUDO chown -R root:root "${INSTALL_DIR}" - $SUDO chmod +x "${INSTALL_DIR}/scripts/"*.sh 2>/dev/null || true - - # Restart service if systemd service exists - if [ -f "${INSTALL_DIR}/csf-core.service" ]; then - log "Reloading systemd and starting service..." - $SUDO systemctl daemon-reload - $SUDO systemctl start csf-core.service - $SUDO systemctl enable csf-core.service + if systemctl is-active --quiet csf-core.service; then + log "βœ… CSF-Core service is running" 100 + update_status "completed" "Update completed successfully!" 100 + else + log "❌ Service failed to start" 100 + update_status "error" "Service failed to start after update" 100 + error "Service failed to start" fi +else + log "⚠ No systemd service found" 100 + update_status "completed" "Binaries updated, but no service configured" 100 fi # Cleanup -log "Cleaning up temporary files..." +log "🧹 Cleaning up temporary files..." rm -rf "${TMP_DIR}" -# Verify installation -if systemctl is-active --quiet csf-core.service 2>/dev/null; then - log "βœ“ CSF-Core service is running" -else - log "⚠ CSF-Core service is not running. Manual start may be required." -fi - -log "Update to version ${VERSION} completed successfully!" -log "Backup saved at: ${BACKUP_DIR}" +log "βœ… Update to version ${VERSION} completed successfully!" +log "πŸ“¦ Backup saved at: ${BACKUP_DIR}" log "" -log "If you encounter any issues, you can restore from the backup:" -log " sudo rm -rf ${INSTALL_DIR}" -log " sudo mv ${BACKUP_DIR}/csf-core ${INSTALL_DIR}" -log " sudo systemctl restart csf-core.service" +log "ℹ️ If you encounter any issues, you can restore from the backup:" +log " sudo systemctl stop csf-core.service" +log " sudo rm -rf ${INSTALL_DIR}" +log " sudo mv ${BACKUP_DIR}/csf-core ${INSTALL_DIR}" +log " sudo systemctl start csf-core.service" exit 0