4ad0f9e493
## Backend — Service Layer & Repository Refactoring ### Neue Services (21 neue Dateien) **Interfaces & Implementierungen:** - `IOpenClawGatewayClient` — Interface für OpenClawGatewayClient (DIP-Fix: DashboardController hing an konkreter Klasse) - `IAgentConfigService` / `AgentConfigService` — Agent-Config-File-I/O aus AgentsController extrahiert - `IProjectService` / `ProjectService` — Projekt-CRUD + Activity-Logging (SRP) - `ITaskService` / `TaskService` — Task-State-Machine, Approve/Reject, Dashboard-Operationen (eliminiert Duplikation zwischen TasksController und DashboardController) - `IDashboardService` / `DashboardService` — Queue-Aggregation, Priority-Normalisierung, Gateway-Delegation - `IOperationsService` / `OperationsService` — Metriken-Berechnung aus OperationsController - `ITeamService` / `TeamService` — IDENTITY.md-Lesen aus TeamController - `IMemoryService` / `MemoryService` — File-I/O aus MemoryController - `IIncidentService` / `IncidentService` — File-Parsing (Regex-Source-Generatoren) aus IncidentsController - `IDocService` / `DocService` — Directory-Scan aus DocsController - `ICalendarService` / `CalendarService` — Gateway-HTTP-Calls + Fallback-Daten aus CalendarController ### Repository-Fixes **IUserRepository / UserRepository:** - `SaveChangesAsync` entfernt (leaky abstraction — Caller sollten nie SaveChanges steuern) - `RevokeTokenAsync(tokenHash)` — atomares Token-Revoke inkl. SaveChanges - `RevokeFamilyAsync(familyId)` — Batch-Revoke einer Token-Familie inkl. SaveChanges - `RemoveExpiredTokensAsync` speichert jetzt selbst (war vorher dependent auf nachfolgenden Save) ### AuthService-Fixes - `GetUserAsync`: unnötiges `Task.Run` entfernt → direkt `_users.GetByIdAsync().AsTask()` - `RevokeAsync`: delegiert jetzt an `IUserRepository.RevokeTokenAsync` - `RefreshAsync`: Token-Reuse-Detection delegiert an `IUserRepository.RevokeFamilyAsync` ### Bug-Fix - `OpenClawGatewayClient.ReadAgentGoalAsync`: pre-existing `CS1656` behoben (`reader` war `using`-Variable und wurde neu zugewiesen — in `reader2` umbenannt) ### Controller (16 Stück — alle slim) Alle Controller reduziert auf: Input validieren → Service aufrufen → HTTP-Result zurückgeben. Kein Business-Logic, kein File-I/O, keine direkte Repository-Nutzung (außer AgentsController für Activity-Log). **Program.cs — neue Registrierungen:** - `AddHttpClient<IOpenClawGatewayClient, OpenClawGatewayClient>` (war vorher konkrete Klasse) - Scoped: IDashboardService, IProjectService, ITaskService, IOperationsService, ITeamService, ICalendarService - Singleton: IAgentConfigService, IMemoryService, IIncidentService, IDocService --- ## Frontend — Dashboard V2 Components **AgentDetailModal.vue, IrisChat.vue, TaskStrip.vue:** - V2 Design-System: Dark Space Theme, Glass-Panels, Gradient-Akzente - Stores (agents, chat, tasks) nutzen Service + Mapper-Pattern - NexusLayout, FlowBoard, Topbar — Layoutfixes für fullHeight-Route-Meta Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
157 lines
7.6 KiB
YAML
157 lines
7.6 KiB
YAML
name: Database Backup
|
|
run-name: 💾 DB Backup triggered by @${{ gitea.actor }}
|
|
|
|
# ───────────────────────────────────────────────────────
|
|
# Owner: DevOps (Architekt)
|
|
# Trigger: Manual (workflow_dispatch) + optional schedule.
|
|
#
|
|
# Strategy:
|
|
# 1. Connects to the live PostgreSQL container via docker exec.
|
|
# 2. Runs pg_dumpall (full cluster dump, single file).
|
|
# 3. Compresses with gzip.
|
|
# 4. Uploads as a Gitea Action artifact (or writes to host path).
|
|
# 5. Artifacts are retained per Gitea repo settings (default 90 days).
|
|
#
|
|
# Rotation: Gitea artifact expiration handles old backups automatically.
|
|
# For longer retention, configure an external cron job or use the
|
|
# host_path output to copy the backup elsewhere.
|
|
#
|
|
# Restoration: See phases/deployment.md for step-by-step instructions.
|
|
# ───────────────────────────────────────────────────────
|
|
concurrency:
|
|
group: db-backup
|
|
cancel-in-progress: false
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
keep_on_host:
|
|
description: 'Also copy backup to host path?'
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
host_backup_path:
|
|
description: 'Host path for backup (only if keep_on_host is true)'
|
|
required: false
|
|
default: '/opt/openclaw/backups'
|
|
type: string
|
|
|
|
# Optional: uncomment to enable nightly automatic backups
|
|
# schedule:
|
|
# - cron: '0 3 * * *' # Every night at 03:00 UTC
|
|
|
|
jobs:
|
|
backup:
|
|
name: Backup PostgreSQL
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
ENV_TMPFILE: /tmp/nexus-backup-env
|
|
ENV_POSTGRES_PASSWORD: ${{ secrets.ENV_POSTGRES_PASSWORD }}
|
|
DEPLOY_PATH: /opt/openclaw/data/openclaw/workspace/nexus
|
|
BACKUP_CONTAINER_NAME: nexus-postgres-1
|
|
|
|
steps:
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 1: Generate backup filename
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Generate backup identifier
|
|
id: meta
|
|
run: |
|
|
TIMESTAMP=$(date -u +'%Y-%m-%dT%H%M%SZ')
|
|
echo "timestamp=${TIMESTAMP}" >> "$GITEA_OUTPUT"
|
|
echo "filename=nexus-backup-${TIMESTAMP}.sql.gz" >> "$GITEA_OUTPUT"
|
|
echo "📅 Backup ID: ${TIMESTAMP}"
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 2: Dump PostgreSQL via docker exec
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Dump database
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
echo "🗄️ Dumping PostgreSQL cluster..."
|
|
|
|
docker exec "${BACKUP_CONTAINER_NAME}" \
|
|
sh -c "PGPASSWORD='${ENV_POSTGRES_PASSWORD}' pg_dumpall -U nexus -h localhost" \
|
|
| gzip > "${{ steps.meta.outputs.filename }}"
|
|
|
|
SIZE=$(du -h "${{ steps.meta.outputs.filename }}" | cut -f1)
|
|
echo "✅ Backup written: ${{ steps.meta.outputs.filename }} (${SIZE})"
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 3: Upload backup as Gitea artifact
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Upload backup artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: nexus-backup-${{ steps.meta.outputs.timestamp }}
|
|
path: ${{ steps.meta.outputs.filename }}
|
|
retention-days: 90
|
|
compression-level: 0 # already gzipped
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 4: Optional — copy to host filesystem
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Copy backup to host (optional)
|
|
if: inputs.keep_on_host == true
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
HOST_PATH="${{ inputs.host_backup_path }}"
|
|
|
|
# Create host dir if it doesn't exist
|
|
docker run --rm \
|
|
-v "${HOST_PATH}:/backup-target" \
|
|
-v "${{ gitea.workspace }}:/src:ro" \
|
|
alpine:latest \
|
|
sh -c "
|
|
mkdir -p /backup-target && \
|
|
cp /src/${{ steps.meta.outputs.filename }} /backup-target/ && \
|
|
echo '✅ Backup copied to host: ${HOST_PATH}/${{ steps.meta.outputs.filename }}'
|
|
"
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 5: Verify backup integrity
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Verify backup integrity
|
|
run: |
|
|
echo "🔍 Verifying backup integrity..."
|
|
if gzip -t "${{ steps.meta.outputs.filename }}"; then
|
|
echo "✅ Backup gzip integrity check passed"
|
|
else
|
|
echo "❌ Backup file is corrupted!"
|
|
exit 1
|
|
fi
|
|
|
|
# Quick content check: should start with PostgreSQL dump header
|
|
HEADER=$(zcat "${{ steps.meta.outputs.filename }}" | head -1)
|
|
if echo "$HEADER" | grep -qE '^(-- PostgreSQL database cluster dump|-- Dumped|--)'; then
|
|
echo "✅ Backup content header check passed"
|
|
else
|
|
echo "⚠️ Unexpected backup header (may still be valid): $HEADER"
|
|
fi
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 6: Backup Summary
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Backup Summary
|
|
if: always()
|
|
run: |
|
|
STATUS="${{ job.status }}"
|
|
echo ""
|
|
echo "═══════════════════════════════════════"
|
|
echo " 💾 Database Backup Summary"
|
|
echo "═══════════════════════════════════════"
|
|
echo " File: ${{ steps.meta.outputs.filename }}"
|
|
echo " Timestamp: ${{ steps.meta.outputs.timestamp }}"
|
|
echo " Triggered: @${{ gitea.actor }}"
|
|
echo " On host: ${{ inputs.keep_on_host == 'true' && inputs.host_backup_path || 'No (artifact only)' }}"
|
|
echo " Status: ${STATUS}"
|
|
echo "═══════════════════════════════════════"
|
|
|
|
if [ "${STATUS}" = "success" ]; then
|
|
echo ""
|
|
echo "💡 Restore command (manual, on host):"
|
|
echo " zcat ${{ steps.meta.outputs.filename }} | docker exec -i nexus-postgres-1 psql -U nexus -d postgres"
|
|
fi
|