fix: permanent owner password persistence with SeedAudit guard
CI - Build & Test / Backend (.NET) (push) Successful in 28s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 18s
CI - Build & Test / Security Check (push) Successful in 2s

Root cause: Dual-source architecture for owner password (Gitea secret
ENV_OWNER_PASSWORD vs host .env OWNER_PASSWORD) caused drift when
the DB was ever re-seeded or the volume recreated.

Changes:
- Add SeedAudit entity + migration to track one-time seed operations
- EnsureDatabaseAsync checks SeedAudit BEFORE seeding — owner is never
  re-created even if the Users table is wiped
- Deploy and rollback workflows now read OWNER_PASSWORD from the host's
  persistent .env (single source of truth) instead of Gitea secrets
- compose.yaml documented: OWNER_PASSWORD only used during initial seed
- Cleanup: .gitignore extended for core dumps, changelog/deployment.md
  updated with 2026-06-20 session notes

After this fix the DB is the single source of truth for the owner
password after initial seed. The host .env is the single reference
for the initial value.
This commit is contained in:
2026-06-21 10:15:36 +02:00
parent 2d218853a5
commit f95463ef50
13 changed files with 488 additions and 11 deletions
+24 -5
View File
@@ -67,8 +67,10 @@ jobs:
ENV_TMPFILE: /tmp/nexus-deploy-env
ENV_POSTGRES_PASSWORD: ${{ secrets.ENV_POSTGRES_PASSWORD }}
ENV_JWT_KEY: ${{ secrets.ENV_JWT_KEY }}
ENV_OWNER_PASSWORD: ${{ secrets.ENV_OWNER_PASSWORD }}
ENV_OPENCLAW_TOKEN: ${{ secrets.ENV_OPENCLAW_TOKEN }}
# OWNER_PASSWORD is read from the host's persistent .env — NOT from a Gitea secret.
# This ensures the password stays consistent across deploys and the DB is the
# single source of truth after initial seed (enforced by SeedAudit guard).
steps:
# ═══════════════════════════════════════════════════
@@ -127,20 +129,37 @@ jobs:
echo "mutated_main=false" >> "$GITEA_OUTPUT"
# ═══════════════════════════════════════════════════
# Step 4: Build .env from secrets (SAFE)
# Step 4: Build .env from secrets + host .env (SAFE)
#
# Secrets are written to /tmp/nexus-deploy-env — NEVER
# to a file inside the workspace that gets rsync'd to
# the host. The temp file is deleted immediately after
# compose operations complete.
#
# OWNER_PASSWORD is read from the host's persistent .env
# to ensure it stays the single source of truth. Other
# secrets (POSTGRES_PASSWORD, JWT_KEY, OPENCLAW_TOKEN)
# come from Gitea secrets.
# ═══════════════════════════════════════════════════
- name: Prepare .env (secrets → temp file)
- name: Prepare .env (secrets + host .env → temp file)
run: |
set -euo pipefail
# Read OWNER_PASSWORD from the host's persistent .env
HOST_OWNER_PASSWORD=""
if [ -f "${DEPLOY_PATH}/.env" ]; then
HOST_OWNER_PASSWORD=$(grep '^OWNER_PASSWORD=' "${DEPLOY_PATH}/.env" | cut -d= -f2- || true)
fi
if [ -z "${HOST_OWNER_PASSWORD}" ]; then
echo "❌ OWNER_PASSWORD not found in ${DEPLOY_PATH}/.env"
echo " The host .env is the single source of truth for the owner password."
echo " Ensure OWNER_PASSWORD is set in the deploy-path .env before deploying."
exit 1
fi
cat > "${ENV_TMPFILE}" <<EOF
# Nexus Production Environment — auto-generated by CD pipeline
# Managed via Gitea Secrets → do NOT edit manually on the host.
# Managed via Gitea Secrets + host .env → do NOT edit manually on the host.
# This file lives in /tmp and is removed after deploy completes.
POSTGRES_DB=nexus
POSTGRES_USER=nexus
@@ -149,7 +168,7 @@ jobs:
JWT_ISSUER=nexus
JWT_AUDIENCE=nexus-web
OWNER_EMAIL=vmbao62@hotmail.de
OWNER_PASSWORD=${ENV_OWNER_PASSWORD}
OWNER_PASSWORD=${HOST_OWNER_PASSWORD}
OWNER_DISPLAY_NAME=
OPENCLAW_BASE_URL=http://host.docker.internal:18789
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN}
+13 -4
View File
@@ -43,7 +43,6 @@ jobs:
ENV_TMPFILE: /tmp/nexus-rollback-env
ENV_POSTGRES_PASSWORD: ${{ secrets.ENV_POSTGRES_PASSWORD }}
ENV_JWT_KEY: ${{ secrets.ENV_JWT_KEY }}
ENV_OWNER_PASSWORD: ${{ secrets.ENV_OWNER_PASSWORD }}
ENV_OPENCLAW_TOKEN: ${{ secrets.ENV_OPENCLAW_TOKEN }}
steps:
@@ -95,12 +94,22 @@ jobs:
fi
# ═══════════════════════════════════════════════════
# Step 3: Prepare .env from secrets (safe temp file)
# Step 3: Prepare .env from secrets + host .env (safe temp file)
# ═══════════════════════════════════════════════════
- name: Prepare .env (secrets → temp file)
- name: Prepare .env (secrets + host .env → temp file)
run: |
set -euo pipefail
# Read OWNER_PASSWORD from the host's persistent .env
HOST_OWNER_PASSWORD=""
if [ -f "${DEPLOY_PATH}/.env" ]; then
HOST_OWNER_PASSWORD=$(grep '^OWNER_PASSWORD=' "${DEPLOY_PATH}/.env" | cut -d= -f2- || true)
fi
if [ -z "${HOST_OWNER_PASSWORD}" ]; then
echo "❌ OWNER_PASSWORD not found in ${DEPLOY_PATH}/.env"
exit 1
fi
cat > "${ENV_TMPFILE}" <<EOF
# Nexus Production Environment — auto-generated by CD pipeline
POSTGRES_DB=nexus
@@ -110,7 +119,7 @@ jobs:
JWT_ISSUER=nexus
JWT_AUDIENCE=nexus-web
OWNER_EMAIL=vmbao62@hotmail.de
OWNER_PASSWORD=${ENV_OWNER_PASSWORD}
OWNER_PASSWORD=${HOST_OWNER_PASSWORD}
OWNER_DISPLAY_NAME=
OPENCLAW_BASE_URL=http://host.docker.internal:18789
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN}
+4
View File
@@ -30,6 +30,10 @@ docker-compose.override.yml
*.tmp
*.bak
# Crash artefacts / Core dumps
**/core
**/core.*
# pnpm (lockfile IS committed for reproducible CI builds)
# Claude local config (per-developer, not repo-shared)
+15
View File
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Nexus.Api.Data;
@@ -28,6 +29,20 @@ public class NexusUser
public ICollection<RefreshToken> RefreshTokens { get; set; } = new List<RefreshToken>();
}
/// <summary>
/// Tracks one-time seed operations so they are never re-executed — even
/// if the underlying data is deleted. This is the single guard that
/// prevents owner-password drift after DB resets or volume recreations.
/// </summary>
public class SeedAudit
{
[Key]
[MaxLength(80)]
public string Key { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public class RefreshToken
{
public Guid Id { get; set; } = Guid.NewGuid();
@@ -0,0 +1,336 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Nexus.Api.Data;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Nexus.Api.Migrations
{
[DbContext(typeof(NexusDbContext))]
[Migration("20260621081500_AddSeedAudit")]
partial class AddSeedAudit
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Nexus.Api.Data.ActivityEvent", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<Guid?>("TaskId")
.HasColumnType("uuid");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("TaskId");
b.ToTable("Activity");
});
modelBuilder.Entity("Nexus.Api.Data.NexusUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<DateTimeOffset?>("LastLoginAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Nexus.Api.Data.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ForUser")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<bool>("IsRead")
.HasColumnType("boolean");
b.Property<string>("Message")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<Guid?>("TaskId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.HasKey("Id");
b.HasIndex("ForUser", "IsRead", "CreatedAt");
b.ToTable("Notifications");
});
modelBuilder.Entity("Nexus.Api.Data.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<int>("Progress")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Projects");
});
modelBuilder.Entity("Nexus.Api.Data.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("FamilyId")
.HasColumnType("uuid");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTimeOffset?>("RevokedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("TokenHash")
.IsUnique();
b.HasIndex("UserId", "FamilyId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("Nexus.Api.Data.SeedAudit", b =>
{
b.Property<string>("Key")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Key");
b.ToTable("SeedAudit");
});
modelBuilder.Entity("Nexus.Api.Data.WorkTask", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AssignedTo")
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Detail")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTimeOffset?>("DueDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExpectedFrom")
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<bool>("IsAgentTask")
.HasColumnType("boolean");
b.Property<Guid?>("ParentTaskId")
.HasColumnType("uuid");
b.Property<string>("Priority")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<string>("State")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AssignedTo");
b.HasIndex("ExpectedFrom");
b.HasIndex("IsAgentTask");
b.HasIndex("ParentTaskId");
b.HasIndex("Source");
b.ToTable("Tasks");
});
modelBuilder.Entity("Nexus.Api.Data.RefreshToken", b =>
{
b.HasOne("Nexus.Api.Data.NexusUser", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Nexus.Api.Data.WorkTask", b =>
{
b.HasOne("Nexus.Api.Data.WorkTask", "ParentTask")
.WithMany("ChildTasks")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("ParentTask");
});
modelBuilder.Entity("Nexus.Api.Data.NexusUser", b =>
{
b.Navigation("RefreshTokens");
});
modelBuilder.Entity("Nexus.Api.Data.WorkTask", b =>
{
b.Navigation("ChildTasks");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Nexus.Api.Migrations
{
/// <inheritdoc />
public partial class AddSeedAudit : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SeedAudit",
columns: table => new
{
Key = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SeedAudit", x => x.Key);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SeedAudit");
}
}
}
@@ -214,6 +214,20 @@ namespace Nexus.Api.Migrations
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("Nexus.Api.Data.SeedAudit", b =>
{
b.Property<string>("Key")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Key");
b.ToTable("SeedAudit");
});
modelBuilder.Entity("Nexus.Api.Data.WorkTask", b =>
{
b.Property<Guid>("Id")
+1
View File
@@ -10,6 +10,7 @@ public sealed class NexusDbContext(DbContextOptions<NexusDbContext> options) : D
public DbSet<ActivityEvent> Activity => Set<ActivityEvent>();
public DbSet<NexusUser> Users => Set<NexusUser>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<SeedAudit> SeedAudits => Set<SeedAudit>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -13,6 +13,8 @@ public static class ApplicationBuilderExtensions
{
/// <summary>
/// Applies pending EF Core migrations and seeds the initial owner account if none exist.
/// Uses a <see cref="SeedAudit"/> guard so the owner is never re-created even if all users
/// are deleted — the DB is the single source of truth for the owner password after first seed.
/// </summary>
public static async Task EnsureDatabaseAsync(this WebApplication app)
{
@@ -23,6 +25,11 @@ public static class ApplicationBuilderExtensions
var db = scope.ServiceProvider.GetRequiredService<NexusDbContext>();
await db.Database.MigrateAsync();
const string seedKey = "owner_created";
var alreadySeeded = await db.SeedAudits.AnyAsync(s => s.Key == seedKey);
if (alreadySeeded)
return;
var ownerEmail = configuration["Owner:Email"]?.Trim().ToLowerInvariant();
var ownerPassword = configuration["Owner:Password"];
var ownerDisplayName = configuration["Owner:DisplayName"]?.Trim();
@@ -58,6 +65,11 @@ public static class ApplicationBuilderExtensions
Console.Error.WriteLine($"[nexus] Initial owner credentials generated: displayName={initialDisplayName}, password={initialPassword}");
}
}
// Record the seed attempt regardless of whether users already existed.
// This prevents re-seeding even if the Users table is wiped.
db.SeedAudits.Add(new SeedAudit { Key = seedKey });
await db.SaveChangesAsync();
}
}
BIN
View File
Binary file not shown.
+3
View File
@@ -53,6 +53,9 @@ services:
Jwt__Issuer: ${JWT_ISSUER:-nexus}
Jwt__Audience: ${JWT_AUDIENCE:-nexus-web}
Owner__Email: ${OWNER_EMAIL:?Set OWNER_EMAIL in .env}
# OWNER_PASSWORD is only used during initial seed (first deploy).
# After that the DB is the single source of truth, enforced by SeedAudit.
# Default: empty (seed uses a random password if unset on first run).
Owner__Password: ${OWNER_PASSWORD:-}
Owner__DisplayName: ${OWNER_DISPLAY_NAME:-Owner}
Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://host.docker.internal:18789}
+19
View File
@@ -2,6 +2,25 @@
> Letzte Aktualisierung: 2026-06-20
- 2026-06-20: **Agent-Progress-Visibility live ausgerollt; normaler Gitea-Deploy-Trigger weiter defekt.**
- Feature-Stand auf `main`: `adae7ba` (`feat: ship agent progress visibility`); nach CI-Blocker-Fix `2d21885` (`Fix activity repository test double`) war CI fuer Backend, Frontend und Security gruen.
- Da `POST /actions/workflows/deploy.yaml/dispatches` serverseitig `HTTP 500` lieferte und fuer `2d21885` kein erfolgreicher Deploy-Run belegbar war, wurde der produktive Rollout manuell aus einem **sauberen Snapshot von Commit `2d21885`** durchgefuehrt statt aus dem schmutzigen lokalen Worktree.
- Deploy-Pfad: Snapshot-Sync nach `/home/projekte_bao/openclaw/data/openclaw/workspace/nexus` und danach `docker compose --env-file .env up -d --build --force-recreate --wait`.
- Verifikation: Host-Deploy-Pfad auf Git-HEAD `2d218853a5d198fa8521dadbb4c6ea9be19e191c`; `nexus-postgres-1`, `nexus-api-1`, `nexus-web-1` healthy; `https://nexus.noveria.net/health/live` = `200`; `/dashboard` = `200`; `/api/v1/operations/snapshot` = `401` ohne Auth; `GET /api/dashboard/tasks/caab972a-c46c-4af5-b2c4-9d31be824da3` liefert live `lastActivityMessage` und `lastActivityAt`.
- Offener Betriebsblocker: Gitea-Deploy-Trigger / `workflow_dispatch` fuer `deploy.yaml` liefert weiter `HTTP 500` und muss separat repariert werden.
- 2026-06-20: **Live-Nexus nach Deploy-Stoerung verifiziert, Bao-Folgetasks angelegt und Agent-Workflow live gegengeprueft.**
- `https://nexus.noveria.net/` lieferte wieder `200 OK` mit SPA-Titel `Nexus | Noveria Operations`.
- `/health/live` lieferte `200 Healthy`.
- `GET /api/dashboard/tasks/board`, `GET /api/dashboard/tasks/agent-overview` und `GET /api/dashboard/agents` lieferten mit `X-Nexus-Api-Key` wieder `200 OK`.
- Neuer Task angelegt: `Restore agent progress visibility in Nexus` (`assignedTo=bao`, `priority=High`, State `Backlog`).
- Neuer Task angelegt: `Review: Agenten-Progress mit letztem Status + Timestamp sichtbar machen` (`assignedTo=bao`, State `Backlog`).
- Live-Artefakt-Pruefung bestaetigt Frontend-Strings fuer `nur Iris und Bao`, `researcher`, `executor`, `Worauf warte ich?`, `expectedFrom` und `isAgentTask`.
- Reversible Live-Verifikation erfolgreich: temp. Agent-Task mit `expectedFrom=researcher` erschien korrekt in `waitingForOthers`, erzeugte Activity und wurde direkt wieder geloescht (`DELETE ... -> 204`).
- Reversible Notification-Verifikation erfolgreich: simulierte Bao-Aenderung (`X-Agent-Id: bao`) erzeugte live `task_content_changed` und `task_status_changed` fuer Iris; simulierte Iris-Statusaenderung erzeugte live `task_review` fuer Bao.
- Live-Regel fuer Delete bestaetigt: Tasks lassen sich nur in `Backlog` oder `Done` loeschen; ein temp. Review-Task lieferte erwartungsgemaess `403` bis zum Ruecksetzen auf `Backlog`.
- Geaenderte Dateien: `nexus.md`, `phases/changelog.md`.
- 2026-06-20: **Researcher und Executor in den Agent-Task-Workflow aufgenommen.**
- `ValidAssignees` in TaskService.cs um `"researcher"` und `"executor"` erweitert.
- Frontend `expectedFromLabel`-Mapping, Create-Task- und Detail-Dropdowns um Researcher (🔬) und Executor (⚡) ergänzt.
+14 -2
View File
@@ -1,6 +1,6 @@
# Deployment
> Letzte Aktualisierung: 2026-06-13
> Letzte Aktualisierung: 2026-06-20
> Status: ✅ CD v3 (Auto + Manual)
> Live-URL: https://nexus.noveria.net
@@ -175,8 +175,19 @@ Stelle sicher, dass `.env` existiert und alle `***`-Platzhalter ersetzt sind.
- [x] Main-Deploys koennen Version-Bump + Git-Tag automatisch setzen; Non-Main-Deploys bleiben read-only (2026-06-13)
- [x] Reviewer-Handoff bei Deploy/Rollback-Fehlern (2026-06-13)
- [x] Database-Backup-Workflow mit pg_dumpall + Gitea-Artifact (2026-06-13)
- [x] Live-Recheck nach Deploy-Stoerung: `/health`, SPA-Root und `GET /api/dashboard/tasks` wieder 200; Bao-Folgetask zur Agent-Progress-Visibility erstellt (2026-06-20)
- [x] Agent-Progress-Stand (`2d21885`) manuell als sauberer Commit-Snapshot live ausgerollt, nachdem der normale Gitea-Deploy-Trigger blockierte (2026-06-20)
## Verifizierung (2026-06-09)
## Verifizierung
### 2026-06-20
- https://nexus.noveria.net/ → 200 OK, SPA geladen (`<title>Nexus | Noveria Operations</title>`)
- /health → 200 Healthy, PostgreSQL + Runtime healthy
- /api/dashboard/tasks → 200 OK mit `X-Nexus-Api-Key`
- Follow-up-Task `Restore agent progress visibility in Nexus` fuer `assignedTo=bao` erfolgreich angelegt
### 2026-06-09
- https://nexus.noveria.net → 200 OK, SPA geladen
- /health → Healthy
@@ -197,6 +208,7 @@ Stelle sicher, dass `.env` existiert und alle `***`-Platzhalter ersetzt sind.
## Offene Arbeit
- [!] Gitea-Deploy-Trigger reparieren: `POST /actions/workflows/deploy.yaml/dispatches` liefert aktuell `HTTP 500`; fuer Commit `2d21885` war deshalb kein erfolgreicher normaler Deploy-Run belegbar
- [ ] Docker-Socket-Risiko im CD-Workflow final adressieren (kommt spaeter)
- [ ] Docker-Logs und Container-Health-Monitoring einrichten
- [ ] Restore-Drill fuer Backup/Recovery einmal realistisch durchspielen und dokumentieren