feat: implement update backend API and client logic

Add complete update mechanism for client-side updates:
- backend/update.py: Core update logic (check, apply, rollback, status/logs)
- backend/app.py: REST API endpoints (GET /update/status, POST /update/check, POST /update/apply, POST /update/rollback, GET /update/logs)
- backend/models.py: Pydantic models for update API responses
- backend/settings.py: Update config (status/log file paths)
- scripts/rollback_client.sh: Rollback script for failed updates
- scripts/update_client.sh: Enhanced update client script
- CLAUDE.md: Documentation for future Claude Code instances

Complete US_000026-028 and TASK_000027-029:
- US_000026: Client pulls updates from remote service
- US_000027: Client verifies and applies updates atomically
- US_000028: Client reports update status to backend

All endpoints require authentication. Updates run asynchronously.
Documentation updated per SOP (CHANGELOG, PROJECT_STATUS, stories/tasks).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 11:18:34 +01:00
parent b64cc5981c
commit 107cdabe8d
15 changed files with 530 additions and 19 deletions

76
scripts/rollback_client.sh Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
SERVICE_NAME="${SERVICE_NAME:-skd}"
INSTALL_DIR="${INSTALL_DIR:-/opt/sk}"
STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}"
LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
write_status() {
local status="$1"
local error="${2:-}"
local version="$3"
SKD_STATUS="${status}" SKD_ERROR="${error}" SKD_VERSION="${version}" \
SKD_STATUS_FILE="${STATUS_FILE}" SKD_LOG_FILE="${LOG_FILE}" python3 - <<'PY'
import json
import os
from datetime import datetime, timezone
from pathlib import Path
status = os.environ["SKD_STATUS"]
error = os.environ.get("SKD_ERROR", "")
version = os.environ.get("SKD_VERSION", "unknown")
status_file = Path(os.environ["SKD_STATUS_FILE"])
log_file = Path(os.environ["SKD_LOG_FILE"])
status_file.parent.mkdir(parents=True, exist_ok=True)
log_file.parent.mkdir(parents=True, exist_ok=True)
payload = {
"device_id": os.uname().nodename,
"version": version,
"status": status,
"error": error,
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
status_file.write_text(json.dumps(payload), encoding="utf-8")
with log_file.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(payload) + "\n")
PY
}
LATEST_BACKUP="$(ls -dt /opt/sk_backup_* 2>/dev/null | head -1 || true)"
if [[ -z "${LATEST_BACKUP}" ]]; then
log "No backup found; rollback aborted."
write_status "failed" "no backup found" "unknown"
exit 1
fi
VERSION="unknown"
if [[ -f "${LATEST_BACKUP}/VERSION" ]]; then
VERSION="$(cat "${LATEST_BACKUP}/VERSION" | tr -d '\n')"
fi
log "Stopping service ${SERVICE_NAME}..."
sudo systemctl stop "${SERVICE_NAME}.service"
FAILED_DIR="${INSTALL_DIR}_failed_$(date +%s)"
log "Swapping ${INSTALL_DIR} -> ${FAILED_DIR}..."
if [[ -d "${INSTALL_DIR}" ]]; then
sudo mv "${INSTALL_DIR}" "${FAILED_DIR}"
fi
sudo mv "${LATEST_BACKUP}" "${INSTALL_DIR}"
log "Starting service ${SERVICE_NAME}..."
if sudo systemctl start "${SERVICE_NAME}.service"; then
log "Rollback completed."
write_status "success" "" "${VERSION}"
else
log "Rollback failed."
write_status "failed" "service start failed" "${VERSION}"
exit 1
fi