refactor: SOLID architecture — backend service layer + frontend V2 components
## 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>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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
|
||||
Reference in New Issue
Block a user