diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..0d53136 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"aed37e34-4be8-4788-b03a-7145d9b4b2ce","pid":3544538,"procStart":"34480817","acquiredAt":1779373227101} \ No newline at end of file diff --git a/.env.example b/.env.example index ea68c15..46c4a86 100644 --- a/.env.example +++ b/.env.example @@ -32,7 +32,7 @@ POSTGRES_PASSWORD= # host (outside Docker). Must match POSTGRES_PASSWORD above. Inside the app # container this variable is overridden by docker-compose.yml (which routes # to the postgres service name on the internal network). -DATABASE_URL=postgresql://capakraken:capakraken_dev@localhost:5433/capakraken +DATABASE_URL=postgresql://nexus:nexus_dev@localhost:5433/nexus # ─── Redis ─────────────────────────────────────────────────────────────────── @@ -104,7 +104,7 @@ PGADMIN_PASSWORD= # that any resolved path remains inside this directory; this prevents an # admin (or compromised admin token) from pointing the parser at arbitrary # files on disk and reaching ExcelJS CVEs. Defaults to ./imports if unset. -# DISPO_IMPORT_DIR=/var/lib/capakraken/imports +# DISPO_IMPORT_DIR=/var/lib/nexus/imports # ─── Testing (never enable in production) ──────────────────────────────────── diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c6fd33..a523692 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,11 +159,11 @@ jobs: postgres: image: postgres:16 env: - POSTGRES_DB: capakraken_test - POSTGRES_USER: capakraken - POSTGRES_PASSWORD: capakraken_test + POSTGRES_DB: nexus_test + POSTGRES_USER: nexus + POSTGRES_PASSWORD: nexus_test options: >- - --health-cmd="pg_isready -U capakraken -d capakraken_test" + --health-cmd="pg_isready -U nexus -d nexus_test" --health-interval=10s --health-timeout=5s --health-retries=5 @@ -175,7 +175,7 @@ jobs: --health-timeout=5s --health-retries=5 env: - DATABASE_URL: postgresql://capakraken:capakraken_test@postgres:5432/capakraken_test + DATABASE_URL: postgresql://nexus:nexus_test@postgres:5432/nexus_test REDIS_URL: redis://redis:6379 # Force in-memory rate limiter to avoid cross-test state when Redis drops. # Redis fallback downgrades to max/10 limits which rate-limits unit tests. @@ -291,11 +291,11 @@ jobs: e2epg: image: postgres:16 env: - POSTGRES_DB: capakraken_test - POSTGRES_USER: capakraken - POSTGRES_PASSWORD: capakraken_test + POSTGRES_DB: nexus_test + POSTGRES_USER: nexus + POSTGRES_PASSWORD: nexus_test options: >- - --health-cmd="pg_isready -U capakraken -d capakraken_test" + --health-cmd="pg_isready -U nexus -d nexus_test" --health-interval=10s --health-timeout=5s --health-retries=5 @@ -307,14 +307,14 @@ jobs: --health-timeout=5s --health-retries=5 env: - DATABASE_URL: postgresql://capakraken:capakraken_test@e2epg:5432/capakraken_test + DATABASE_URL: postgresql://nexus:nexus_test@e2epg:5432/nexus_test # Playwright test-server.mjs requires an explicit test DB URL. - PLAYWRIGHT_DATABASE_URL: postgresql://capakraken:capakraken_test@e2epg:5432/capakraken_test + PLAYWRIGHT_DATABASE_URL: postgresql://nexus:nexus_test@e2epg:5432/nexus_test # prisma-with-env.mjs refuses to run unless DATABASE_URL's db name matches - # the expected target; default is "capakraken", CI uses capakraken_test. - CAPAKRAKEN_EXPECTED_DB_NAME: capakraken_test + # the expected target; default is "nexus", CI uses nexus_test. + NEXUS_EXPECTED_DB_NAME: nexus_test ALLOW_DESTRUCTIVE_DB_TOOLS: "true" - CONFIRM_DESTRUCTIVE_DB_NAME: capakraken_test + CONFIRM_DESTRUCTIVE_DB_NAME: nexus_test REDIS_URL: redis://e2eredis:6379 PORT: 3100 # test-server.mjs spawns `docker compose --profile test up postgres-test`; @@ -375,7 +375,7 @@ jobs: - name: Push DB schema & seed env: - PGPASSWORD: capakraken_test + PGPASSWORD: nexus_test run: | # Nuke any leftover schema state from a previous job that shared the # postgres service container (act_runner reuses service volumes). @@ -397,7 +397,7 @@ jobs: IPS=$(getent hosts e2epg | awk '{print $1}') PG_IP="" for ip in $IPS; do - if PGPASSWORD=capakraken_test psql -h "$ip" -U capakraken -d capakraken_test -v ON_ERROR_STOP=1 -Atc "SELECT 1" >/dev/null 2>&1; then + if PGPASSWORD=nexus_test psql -h "$ip" -U nexus -d nexus_test -v ON_ERROR_STOP=1 -Atc "SELECT 1" >/dev/null 2>&1; then PG_IP="$ip" echo "Locked onto postgres at $PG_IP" break @@ -406,19 +406,19 @@ jobs: fi done if [ -z "$PG_IP" ]; then - echo "ERROR: no resolved e2epg IP accepted capakraken_test credentials" + echo "ERROR: no resolved e2epg IP accepted nexus_test credentials" exit 1 fi - PINNED_URL="postgresql://capakraken:capakraken_test@$PG_IP:5432/capakraken_test" + PINNED_URL="postgresql://nexus:nexus_test@$PG_IP:5432/nexus_test" echo "DATABASE_URL=$PINNED_URL" >> "$GITHUB_ENV" echo "PLAYWRIGHT_DATABASE_URL=$PINNED_URL" >> "$GITHUB_ENV" echo "--- DROP SCHEMA ---" - psql -h "$PG_IP" -U capakraken -d capakraken_test -v ON_ERROR_STOP=1 \ - -c "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO capakraken; GRANT ALL ON SCHEMA public TO public;" + psql -h "$PG_IP" -U nexus -d nexus_test -v ON_ERROR_STOP=1 \ + -c "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO nexus; GRANT ALL ON SCHEMA public TO public;" echo "--- prisma db push ---" DATABASE_URL="$PINNED_URL" pnpm --filter @nexus/db exec prisma db push --schema ./prisma/schema.prisma --accept-data-loss --skip-generate echo "--- tables in public after push ---" - psql -h "$PG_IP" -U capakraken -d capakraken_test -v ON_ERROR_STOP=1 -At \ + psql -h "$PG_IP" -U nexus -d nexus_test -v ON_ERROR_STOP=1 -At \ -c "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename" \ | tee /tmp/tables.txt if ! grep -qx 'audit_logs' /tmp/tables.txt; then @@ -468,8 +468,8 @@ jobs: NEXTAUTH_SECRET=ci-test-secret-minimum-32-chars-xx PGADMIN_PASSWORD=ci-pgadmin # Must match the password baked into docker-compose.ci.yml's - # DATABASE_URL override (capakraken_dev). - POSTGRES_PASSWORD=capakraken_dev + # DATABASE_URL override (nexus_dev). + POSTGRES_PASSWORD=nexus_dev EOF - name: Tear down any stale stack & volumes @@ -477,7 +477,11 @@ jobs: # runs. A previous run's failed migration entry in _prisma_migrations # causes P3009 on the next migrate deploy; wipe volumes for a truly # fresh deploy test every time. - run: docker compose -f docker-compose.yml -f docker-compose.ci.yml down -v --remove-orphans || true + # Also tear down the legacy "capakraken" project (pre-Phase-3 rename) + # in case old containers are still holding host ports 5433/6380. + run: | + docker compose -p capakraken --profile full -f docker-compose.yml -f docker-compose.ci.yml down -v --remove-orphans || true + docker compose --profile full -f docker-compose.yml -f docker-compose.ci.yml down -v --remove-orphans || true - name: Start infrastructure (postgres + redis) run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d postgres redis @@ -485,7 +489,7 @@ jobs: - name: Wait for postgres run: | for i in $(seq 1 20); do - docker compose -f docker-compose.yml -f docker-compose.ci.yml exec -T postgres pg_isready -U capakraken -d capakraken && break + docker compose -f docker-compose.yml -f docker-compose.ci.yml exec -T postgres pg_isready -U nexus -d nexus && break sleep 3 done diff --git a/apps/web/e2e/assistant-approvals.spec.ts b/apps/web/e2e/assistant-approvals.spec.ts index 5a8263c..08257a1 100644 --- a/apps/web/e2e/assistant-approvals.spec.ts +++ b/apps/web/e2e/assistant-approvals.spec.ts @@ -101,7 +101,7 @@ test.describe("Assistant approvals", () => { test.beforeEach(async ({ page }) => { await page.addInitScript((conversationId) => { - window.sessionStorage.setItem("capakraken-chat-conversation-id", conversationId); + window.sessionStorage.setItem("nexus-chat-conversation-id", conversationId); }, CURRENT_CONVERSATION_ID); runDb(` diff --git a/apps/web/e2e/dev-system/auth-session.spec.ts b/apps/web/e2e/dev-system/auth-session.spec.ts index cf37edb..8a1453d 100644 --- a/apps/web/e2e/dev-system/auth-session.spec.ts +++ b/apps/web/e2e/dev-system/auth-session.spec.ts @@ -42,9 +42,9 @@ test.describe("Auth — login / logout", () => { await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/auth\/signin/, { timeout: 5000 }); // Error message visible - await expect( - page.locator("text=/invalid|incorrect|wrong|credentials/i"), - ).toBeVisible({ timeout: 5000 }); + await expect(page.locator("text=/invalid|incorrect|wrong|credentials/i")).toBeVisible({ + timeout: 5000, + }); }); test("after logout, protected routes redirect to sign-in", async ({ page }) => { @@ -75,7 +75,7 @@ test.describe("Session registry — no tRPC 401s after login", () => { // At least one user row should be visible await expect(page.locator("table")).toBeVisible({ timeout: 10000 }); - await expect(page.locator("text=/planarchy\\.dev|capakraken\\.dev/").first()).toBeVisible({ + await expect(page.locator("text=/planarchy\\.dev|nexus\\.dev/").first()).toBeVisible({ timeout: 10000, }); await expect(page.locator("text=No users found")).toHaveCount(0); diff --git a/apps/web/e2e/dev-system/dashboard-widgets.spec.ts b/apps/web/e2e/dev-system/dashboard-widgets.spec.ts index 541a438..8308bb6 100644 --- a/apps/web/e2e/dev-system/dashboard-widgets.spec.ts +++ b/apps/web/e2e/dev-system/dashboard-widgets.spec.ts @@ -12,7 +12,7 @@ * - Creates a temporary test user via tRPC (admin session) for isolation. * - Cleans up the test user in afterAll. * - Uses an empty storageState to ensure no cross-user localStorage bleed. - * - localStorage key is user-scoped: "capakraken_dashboard_v1_{userId}". + * - localStorage key is user-scoped: "nexus_dashboard_v1_{userId}". */ import { expect, test, type Browser, type Page } from "@playwright/test"; @@ -20,9 +20,16 @@ import { STORAGE_STATE } from "../../playwright.dev.config.js"; // ─── tRPC helpers ───────────────────────────────────────────────────────────── -type TrpcResult = { result?: { data?: unknown }; error?: { data?: { code?: string }; message?: string } }; +type TrpcResult = { + result?: { data?: unknown }; + error?: { data?: { code?: string }; message?: string }; +}; -async function trpcMutation(page: Page, procedure: string, input: unknown = null): Promise { +async function trpcMutation( + page: Page, + procedure: string, + input: unknown = null, +): Promise { return page.evaluate( async ({ procedure, input }) => { const res = await fetch(`/api/trpc/${procedure}?batch=1`, { @@ -38,7 +45,11 @@ async function trpcMutation(page: Page, procedure: string, input: unknown = null ); } -async function trpcQuery(page: Page, procedure: string, input: unknown = null): Promise { +async function trpcQuery( + page: Page, + procedure: string, + input: unknown = null, +): Promise { return page.evaluate( async ({ procedure, input }) => { const encodedInput = encodeURIComponent(JSON.stringify({ "0": { json: input } })); @@ -128,7 +139,9 @@ test.describe("Dashboard — widget management", () => { // Default layout should show at least the stat-cards widget // (from createDefaultDashboardLayout in useDashboardLayout) - await expect(page.locator('[data-testid="widget-stat-cards"], .react-grid-item').first()).toBeVisible({ + await expect( + page.locator('[data-testid="widget-stat-cards"], .react-grid-item').first(), + ).toBeVisible({ timeout: 8000, }); }); @@ -138,16 +151,21 @@ test.describe("Dashboard — widget management", () => { await navigateToDashboard(page); // Open modal - await page.getByRole("button", { name: /add widget/i }).first().click(); + await page + .getByRole("button", { name: /add widget/i }) + .first() + .click(); // Verify modal is open - await expect(page.getByRole("heading", { name: /add widget/i })).toBeVisible({ timeout: 5000 }); + await expect(page.getByRole("heading", { name: /add widget/i })).toBeVisible({ + timeout: 5000, + }); // Verify widget entries are visible in the modal // The catalog has 11 widgets; check for at least 5 visible buttons inside the modal - const widgetButtons = page.locator( - '[role="dialog"] button, .fixed button[type="button"]', - ).filter({ hasText: /./ }); + const widgetButtons = page + .locator('[role="dialog"] button, .fixed button[type="button"]') + .filter({ hasText: /./ }); // Count items in the grid (the ×-close button is excluded by checking for icon content) const modalContent = page.locator(".fixed.inset-0 .grid"); @@ -166,10 +184,16 @@ test.describe("Dashboard — widget management", () => { const initialCount = await page.locator(".react-grid-item").count(); // Open modal and add "Resource Table" widget - await page.getByRole("button", { name: /add widget/i }).first().click(); + await page + .getByRole("button", { name: /add widget/i }) + .first() + .click(); await expect(page.locator(".fixed.inset-0")).toBeVisible({ timeout: 5000 }); - await page.locator(".fixed.inset-0 button").filter({ hasText: /resource table/i }).click(); + await page + .locator(".fixed.inset-0 button") + .filter({ hasText: /resource table/i }) + .click(); // Modal should close after adding await expect(page.locator(".fixed.inset-0")).not.toBeVisible({ timeout: 5000 }); @@ -184,9 +208,15 @@ test.describe("Dashboard — widget management", () => { await navigateToDashboard(page); // Add a recognizable widget - await page.getByRole("button", { name: /add widget/i }).first().click(); + await page + .getByRole("button", { name: /add widget/i }) + .first() + .click(); await expect(page.locator(".fixed.inset-0")).toBeVisible({ timeout: 5000 }); - await page.locator(".fixed.inset-0 button").filter({ hasText: /project overview/i }).click(); + await page + .locator(".fixed.inset-0 button") + .filter({ hasText: /project overview/i }) + .click(); await expect(page.locator(".fixed.inset-0")).not.toBeVisible({ timeout: 5000 }); const countAfterAdd = await page.locator(".react-grid-item").count(); @@ -214,19 +244,23 @@ test.describe("Dashboard — widget management", () => { // Read the admin's localStorage key to verify it is user-scoped const adminUserId = await adminPage.evaluate(async () => { - const res = await fetch("/api/trpc/user.me?batch=1&input=" + encodeURIComponent(JSON.stringify({ "0": { json: null } })), { - credentials: "include", - }); - const body = await res.json() as [{ result?: { data?: { json?: { id?: string } } } }]; + const res = await fetch( + "/api/trpc/user.me?batch=1&input=" + + encodeURIComponent(JSON.stringify({ "0": { json: null } })), + { + credentials: "include", + }, + ); + const body = (await res.json()) as [{ result?: { data?: { json?: { id?: string } } } }]; return body[0]?.result?.data?.json?.id ?? null; }); - // Verify admin has a user-scoped storage key (not shared "capakraken_dashboard_v1") + // Verify admin has a user-scoped storage key (not shared "nexus_dashboard_v1") if (adminUserId) { const storageKey = await adminPage.evaluate((userId) => { // Check both old (unscoped) and new (user-scoped) key formats - const oldKey = "capakraken_dashboard_v1"; - const newKey = `capakraken_dashboard_v1_${userId}`; + const oldKey = "nexus_dashboard_v1"; + const newKey = `nexus_dashboard_v1_${userId}`; const oldValue = localStorage.getItem(oldKey); const newValue = localStorage.getItem(newKey); return { oldKey: oldValue !== null, newKey: newValue !== null }; @@ -244,8 +278,13 @@ test.describe("Dashboard — widget management", () => { // Inject the admin's storage key to simulate same browser await newUserPage.evaluate( - ({ key, value }) => { localStorage.setItem(key, value ?? ""); }, - { key: `capakraken_dashboard_v1_${adminUserId}`, value: JSON.stringify({ version: 2, gridCols: 12, widgets: [] }) }, + ({ key, value }) => { + localStorage.setItem(key, value ?? ""); + }, + { + key: `nexus_dashboard_v1_${adminUserId}`, + value: JSON.stringify({ version: 2, gridCols: 12, widgets: [] }), + }, ); // Log in as test user @@ -262,7 +301,10 @@ test.describe("Dashboard — widget management", () => { const gridItems = await newUserPage.locator(".react-grid-item").count(); // Either show default layout (≥1 widget) OR the properly-scoped empty state with Add Widget CTA // The key check: the test user's Add Widget button should still work - await newUserPage.getByRole("button", { name: /add widget/i }).first().click(); + await newUserPage + .getByRole("button", { name: /add widget/i }) + .first() + .click(); // Modal must show widgets to choose from const modalContent = newUserPage.locator(".fixed.inset-0 .grid"); diff --git a/apps/web/e2e/dev-system/global-setup.ts b/apps/web/e2e/dev-system/global-setup.ts index dfe70e6..edaf5c7 100644 --- a/apps/web/e2e/dev-system/global-setup.ts +++ b/apps/web/e2e/dev-system/global-setup.ts @@ -25,9 +25,9 @@ const RESET_TEST_USER = { password: "Dev123456!", }; -const DB_CONTAINER = "capakraken-postgres-1"; -const DB_USER = "capakraken"; -const DB_NAME = "capakraken"; +const DB_CONTAINER = "nexus-postgres-1"; +const DB_USER = "nexus"; +const DB_NAME = "nexus"; function psqlExec(sql: string): string { return execSync( diff --git a/apps/web/e2e/dev-system/helpers.ts b/apps/web/e2e/dev-system/helpers.ts index 11e4f1b..e856702 100644 --- a/apps/web/e2e/dev-system/helpers.ts +++ b/apps/web/e2e/dev-system/helpers.ts @@ -26,7 +26,7 @@ export async function signOut(page: Page) { await page.goto("/dashboard"); // land on any authenticated page for cookie context await page.evaluate(async () => { const csrfRes = await fetch("/api/auth/csrf"); - const { csrfToken } = await csrfRes.json() as { csrfToken: string }; + const { csrfToken } = (await csrfRes.json()) as { csrfToken: string }; await fetch("/api/auth/signout", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, @@ -62,11 +62,9 @@ function decodeMimeBody(body: string, encoding: string | undefined): string { const enc = (encoding ?? "").toLowerCase().trim(); if (enc === "quoted-printable") { return body - .replace(/=\r\n/g, "") // soft line break (CRLF) - .replace(/=\n/g, "") // soft line break (LF) - .replace(/=([0-9A-Fa-f]{2})/g, (_, hex: string) => - String.fromCharCode(parseInt(hex, 16)), - ); + .replace(/=\r\n/g, "") // soft line break (CRLF) + .replace(/=\n/g, "") // soft line break (LF) + .replace(/=([0-9A-Fa-f]{2})/g, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))); } if (enc === "base64") { return Buffer.from(body.replace(/\s/g, ""), "base64").toString("utf8"); @@ -90,7 +88,10 @@ export async function clearMailhog(): Promise { */ export async function getLatestEmailTo( address: string, - { timeoutMs = 10_000, pollIntervalMs = 500 }: { timeoutMs?: number; pollIntervalMs?: number } = {}, + { + timeoutMs = 10_000, + pollIntervalMs = 500, + }: { timeoutMs?: number; pollIntervalMs?: number } = {}, ): Promise<{ subject: string; body: string; html: string }> { const deadline = Date.now() + timeoutMs; @@ -144,7 +145,9 @@ export function extractUrlFromEmail( pathPrefix: string, ): string { const text = email.html || email.body; - const match = text.match(new RegExp(`https?://[^\\s"'<>]*${pathPrefix.replace("/", "\\/")}[^\\s"'<>]*`)); + const match = text.match( + new RegExp(`https?://[^\\s"'<>]*${pathPrefix.replace("/", "\\/")}[^\\s"'<>]*`), + ); if (!match?.[0]) { throw new Error(`No URL with prefix "${pathPrefix}" found in email`); } @@ -166,10 +169,10 @@ export async function resetPasswordViaApi( // argon2id hashes use base64 chars only — safe inside a SQL single-quoted string // Column name is camelCase (Prisma default) — must be double-quoted in SQL const sql = `UPDATE users SET "passwordHash" = '${passwordHash}' WHERE email = '${email}';`; - execSync( - `docker exec -i capakraken-postgres-1 psql -U capakraken -d capakraken`, - { input: sql, encoding: "utf8" }, - ); + execSync(`docker exec -i nexus-postgres-1 psql -U nexus -d nexus`, { + input: sql, + encoding: "utf8", + }); } // ── tRPC helpers ─────────────────────────────────────────────────────────────── diff --git a/apps/web/e2e/dev-system/invite-flow.spec.ts b/apps/web/e2e/dev-system/invite-flow.spec.ts index a8f683c..e3bc646 100644 --- a/apps/web/e2e/dev-system/invite-flow.spec.ts +++ b/apps/web/e2e/dev-system/invite-flow.spec.ts @@ -27,7 +27,7 @@ test.describe("invite flow", () => { }); test("admin invites a new user and invited user can sign in", async ({ page, browser }) => { - const testEmail = `invite-e2e-${Date.now()}@capakraken.test`; + const testEmail = `invite-e2e-${Date.now()}@nexus.test`; // Step 1: Navigate to admin users page await page.goto("/admin/users"); @@ -36,7 +36,7 @@ test.describe("invite flow", () => { // Step 2: Open invite modal await page.click('button:has-text("Invite User")'); // Wait for the modal heading — AnimatedModal does not use role="dialog" - await page.waitForSelector('text=Invite User', { state: "visible" }); + await page.waitForSelector("text=Invite User", { state: "visible" }); // Step 3: Fill in invite form await page.fill('input[type="email"]', testEmail); @@ -45,7 +45,9 @@ test.describe("invite flow", () => { await page.click('button:has-text("Send Invite")'); // Step 5: Wait for success message (exact text from InviteUserModal.tsx) - await expect(page.locator("text=Invitation sent successfully.")).toBeVisible({ timeout: 10_000 }); + await expect(page.locator("text=Invitation sent successfully.")).toBeVisible({ + timeout: 10_000, + }); // Step 6: Read invite email from Mailhog const email = await getLatestEmailTo(testEmail, { timeoutMs: 15_000 }); diff --git a/apps/web/e2e/dev-system/rbac-permissions.spec.ts b/apps/web/e2e/dev-system/rbac-permissions.spec.ts index 4146a77..11f5c87 100644 --- a/apps/web/e2e/dev-system/rbac-permissions.spec.ts +++ b/apps/web/e2e/dev-system/rbac-permissions.spec.ts @@ -28,7 +28,7 @@ test.describe("RBAC — admin routes (admin session)", () => { await expect(page.locator("table")).toBeVisible({ timeout: 10000 }); // Seed users have planarchy.dev or nexus.dev email domains - await expect(page.locator("text=/planarchy\\.dev|capakraken\\.dev/").first()).toBeVisible({ + await expect(page.locator("text=/planarchy\\.dev|nexus\\.dev/").first()).toBeVisible({ timeout: 10000, }); }); diff --git a/apps/web/e2e/test-server.mjs b/apps/web/e2e/test-server.mjs index b1fc9d9..04a233f 100644 --- a/apps/web/e2e/test-server.mjs +++ b/apps/web/e2e/test-server.mjs @@ -16,9 +16,9 @@ const webDistDirPath = resolve(webRoot, webDistDir); const managedEnvBanner = "# Managed by apps/web/e2e/test-server.mjs"; const e2ePort = process.env.PLAYWRIGHT_TEST_PORT ?? "3110"; const e2eBaseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL ?? `http://localhost:${e2ePort}`; -const e2eAuthSecret = process.env.PLAYWRIGHT_AUTH_SECRET ?? `capakraken-e2e-${randomBytes(24).toString("hex")}`; +const e2eAuthSecret = process.env.PLAYWRIGHT_AUTH_SECRET ?? `nexus-e2e-${randomBytes(24).toString("hex")}`; const manageWebEnvFile = process.env.PLAYWRIGHT_MANAGE_WEB_ENV_FILE === "true"; -const composeProjectName = `capakraken-e2e-${process.pid}`; +const composeProjectName = `nexus-e2e-${process.pid}`; const managedEnvKeys = [ "DATABASE_URL", "REDIS_URL", @@ -29,7 +29,7 @@ const managedEnvKeys = [ "NODE_ENV", "PORT", ]; -const e2eComposePrefix = "capakraken-e2e-"; +const e2eComposePrefix = "nexus-e2e-"; function dockerComposeArgs(...args) { return ["compose", "-p", composeProjectName, ...args]; @@ -256,7 +256,7 @@ async function ensureE2eDatabaseContainer() { try { await runQuiet( "docker", - dockerComposeArgs("exec", "-T", "postgres-test", "pg_isready", "-U", "capakraken", "-d", "capakraken_test", "-q"), + dockerComposeArgs("exec", "-T", "postgres-test", "pg_isready", "-U", "nexus", "-d", "nexus_test", "-q"), workspaceRoot, ); return; @@ -360,7 +360,7 @@ process.env.PLAYWRIGHT_DATABASE_URL = playwrightDatabaseUrl; if (selectedTestDbPort !== undefined) { process.env.POSTGRES_TEST_PORT = String(selectedTestDbPort); } -process.env.CAPAKRAKEN_EXPECTED_DB_NAME = playwrightDatabaseName; +process.env.NEXUS_EXPECTED_DB_NAME = playwrightDatabaseName; process.env.ALLOW_DESTRUCTIVE_DB_TOOLS = "true"; process.env.CONFIRM_DESTRUCTIVE_DB_NAME = playwrightDatabaseName; process.env.NODE_ENV = process.env.NODE_ENV ?? "development"; diff --git a/apps/web/e2e/timeline.spec.ts b/apps/web/e2e/timeline.spec.ts index 7da2f5c..198fd41 100644 --- a/apps/web/e2e/timeline.spec.ts +++ b/apps/web/e2e/timeline.spec.ts @@ -856,10 +856,10 @@ async function switchToResourceView(page: Page, readySelector?: string) { async function ensureOpenDemandVisibilityEnabled(page: Page) { await page.evaluate(() => { - const raw = window.localStorage.getItem("capakraken_prefs"); + const raw = window.localStorage.getItem("nexus_prefs"); const parsed = raw ? (JSON.parse(raw) as Record) : {}; window.localStorage.setItem( - "capakraken_prefs", + "nexus_prefs", JSON.stringify({ ...parsed, showDemandProjects: true, @@ -874,9 +874,9 @@ test.describe("Timeline", () => { test.beforeEach(async ({ page }) => { await page.addInitScript(() => { - localStorage.setItem("capakraken_theme", JSON.stringify({ mode: "dark" })); + localStorage.setItem("nexus_theme", JSON.stringify({ mode: "dark" })); localStorage.setItem( - "capakraken_prefs", + "nexus_prefs", JSON.stringify({ hideCompletedProjects: true, timelineDisplayMode: "strip", diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index b5e6165..5345d43 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -34,8 +34,8 @@ services: # REDIS_URL to the unique compose container names so resolution is # unambiguous regardless of attached networks. environment: - DATABASE_URL: postgresql://capakraken:capakraken_dev@capakraken-postgres-1:5432/capakraken - REDIS_URL: redis://capakraken-redis-1:6379 + DATABASE_URL: postgresql://nexus:nexus_dev@nexus-postgres-1:5432/nexus + REDIS_URL: redis://nexus-redis-1:6379 networks: gitea_gitea: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 4743f43..5e94967 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,4 +1,4 @@ -name: capakraken-prod +name: nexus-prod services: postgres: @@ -7,8 +7,8 @@ services: ports: - "127.0.0.1:${POSTGRES_PORT:-5432}:5432" environment: - POSTGRES_DB: capakraken - POSTGRES_USER: capakraken + POSTGRES_DB: nexus + POSTGRES_USER: nexus POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} command: > postgres @@ -18,9 +18,9 @@ services: -c log_line_prefix='%t [%p] %u@%d ' -c log_min_duration_statement=1000 volumes: - - capakraken_prod_pgdata:/var/lib/postgresql/data + - nexus_prod_pgdata:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U capakraken -d capakraken"] + test: ["CMD-SHELL", "pg_isready -U nexus -d nexus"] interval: 10s timeout: 5s retries: 5 @@ -34,7 +34,7 @@ services: - "127.0.0.1:${REDIS_PORT:-6379}:6379" command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru --requirepass ${REDIS_PASSWORD} volumes: - - capakraken_prod_redis:/data + - nexus_prod_redis:/data healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "--no-auth-warning", "ping"] interval: 10s @@ -49,7 +49,7 @@ services: env_file: - .env.production environment: - DATABASE_URL: postgresql://capakraken:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/capakraken + DATABASE_URL: postgresql://nexus:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/nexus REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 RATE_LIMIT_BACKEND: ${RATE_LIMIT_BACKEND:-redis} depends_on: @@ -67,7 +67,7 @@ services: env_file: - .env.production environment: - DATABASE_URL: postgresql://capakraken:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/capakraken + DATABASE_URL: postgresql://nexus:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/nexus REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 RATE_LIMIT_BACKEND: ${RATE_LIMIT_BACKEND:-redis} NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN:-} @@ -84,7 +84,7 @@ services: start_period: 30s volumes: - capakraken_prod_pgdata: - name: capakraken_prod_pgdata - capakraken_prod_redis: - name: capakraken_prod_redis + nexus_prod_pgdata: + name: nexus_prod_pgdata + nexus_prod_redis: + name: nexus_prod_redis diff --git a/docker-compose.yml b/docker-compose.yml index effce02..8f54879 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -name: capakraken +name: nexus services: postgres: @@ -6,8 +6,8 @@ services: ports: - "5433:5432" environment: - POSTGRES_DB: capakraken - POSTGRES_USER: capakraken + POSTGRES_DB: nexus + POSTGRES_USER: nexus POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env (any non-empty value for local dev)} command: > postgres @@ -17,9 +17,9 @@ services: -c log_line_prefix='%t [%p] %u@%d ' -c log_min_duration_statement=1000 volumes: - - capakraken_pgdata:/var/lib/postgresql/data + - nexus_pgdata:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U capakraken -d capakraken"] + test: ["CMD-SHELL", "pg_isready -U nexus -d nexus"] interval: 5s timeout: 3s retries: 5 @@ -61,7 +61,7 @@ services: # Always use the Docker-internal service name. The host-level DATABASE_URL # (localhost:5433) must not bleed into the container where "localhost" is # the container itself, not the host. - DATABASE_URL: postgresql://capakraken:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/capakraken + DATABASE_URL: postgresql://nexus:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/nexus REDIS_URL: redis://redis:6379 NEXTAUTH_URL: ${NEXTAUTH_URL:?NEXTAUTH_URL must be set (e.g. https://your-domain.com)} NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?set NEXTAUTH_SECRET} @@ -90,9 +90,9 @@ services: volumes: - .:/app # Named volumes mask the bind-mount for generated/installed artefacts. - # Named (not anonymous) so they can be selectively removed: docker volume rm capakraken_node_modules - - capakraken_node_modules:/app/node_modules - - capakraken_next:/app/apps/web/.next + # Named (not anonymous) so they can be selectively removed: docker volume rm nexus_node_modules + - nexus_node_modules:/app/node_modules + - nexus_next:/app/apps/web/.next profiles: - full @@ -101,18 +101,18 @@ services: ports: - "${POSTGRES_TEST_PORT:-5434}:5432" environment: - POSTGRES_DB: capakraken_test - POSTGRES_USER: capakraken - POSTGRES_PASSWORD: capakraken_test + POSTGRES_DB: nexus_test + POSTGRES_USER: nexus + POSTGRES_PASSWORD: nexus_test tmpfs: - /var/lib/postgresql/data profiles: - test volumes: - capakraken_pgdata: - name: capakraken_pgdata - capakraken_node_modules: - name: capakraken_node_modules - capakraken_next: - name: capakraken_next + nexus_pgdata: + name: nexus_pgdata + nexus_node_modules: + name: nexus_node_modules + nexus_next: + name: nexus_next diff --git a/package.json b/package.json index 4c465ed..0f37343 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "check:imports": "node ./scripts/check-workspace-imports.mjs", "worktree:hygiene": "node ./scripts/worktree-hygiene.mjs", "clean:next": "node ./scripts/clean-next-artifacts.mjs", - "db:doctor": "node ./scripts/db-doctor.mjs capakraken", + "db:doctor": "node ./scripts/db-doctor.mjs nexus", "db:prisma": "node ./scripts/prisma-with-env.mjs", "db:push": "node ./scripts/prisma-with-env.mjs db push", "db:migrate": "node ./scripts/prisma-with-env.mjs migrate dev", diff --git a/packages/api/src/__tests__/rbac-cache-redis-pubsub.test.ts b/packages/api/src/__tests__/rbac-cache-redis-pubsub.test.ts index b6f4094..92f177c 100644 --- a/packages/api/src/__tests__/rbac-cache-redis-pubsub.test.ts +++ b/packages/api/src/__tests__/rbac-cache-redis-pubsub.test.ts @@ -105,10 +105,10 @@ describe("RBAC cache Redis pub/sub (#57)", () => { // Simulate a peer instance publishing an invalidation: grab any // subscriber on the channel and fire the event as if Redis delivered it. - const subs = channelSubscribers.get("capakraken:rbac-invalidate"); + const subs = channelSubscribers.get("nexus:rbac-invalidate"); expect(subs).toBeDefined(); expect(subs!.size).toBeGreaterThanOrEqual(1); - for (const sub of subs!) sub.emit("message", "capakraken:rbac-invalidate", "1"); + for (const sub of subs!) sub.emit("message", "nexus:rbac-invalidate", "1"); // Next load must hit the DB again. await loadRoleDefaults(); @@ -126,6 +126,6 @@ describe("RBAC cache Redis pub/sub (#57)", () => { const newPublishes = publishCalls.slice(countBefore); expect(newPublishes.length).toBe(1); - expect(newPublishes[0]!.channel).toBe("capakraken:rbac-invalidate"); + expect(newPublishes[0]!.channel).toBe("nexus:rbac-invalidate"); }); }); diff --git a/packages/api/src/__tests__/ssrf-guard.test.ts b/packages/api/src/__tests__/ssrf-guard.test.ts index e24fcfa..b9db7b4 100644 --- a/packages/api/src/__tests__/ssrf-guard.test.ts +++ b/packages/api/src/__tests__/ssrf-guard.test.ts @@ -24,7 +24,7 @@ describe("assertWebhookUrlAllowed — SSRF guard", () => { it("allows an HTTPS URL with a path and query string", async () => { await expect( - assertWebhookUrlAllowed("https://hooks.external.io/events?source=capakraken"), + assertWebhookUrlAllowed("https://hooks.external.io/events?source=nexus"), ).resolves.toBeUndefined(); }); diff --git a/packages/api/src/lib/app-base-url.ts b/packages/api/src/lib/app-base-url.ts index b0f766e..3668d0b 100644 --- a/packages/api/src/lib/app-base-url.ts +++ b/packages/api/src/lib/app-base-url.ts @@ -22,15 +22,15 @@ export function getAppBaseUrl(): string { if (process.env["NODE_ENV"] === "production") { throw new Error( "NEXTAUTH_URL must be set in production — email links will contain localhost otherwise. " + - "Set it to the public URL of this app (e.g. https://capakraken.example.com).", + "Set it to the public URL of this app (e.g. https://nexus.example.com).", ); } if (!warned) { warned = true; console.warn( - "[capakraken] NEXTAUTH_URL is not set — falling back to http://localhost:3100 for email links. " + - "Set NEXTAUTH_URL in your .env to suppress this warning.", + "[nexus] NEXTAUTH_URL is not set — falling back to http://localhost:3100 for email links. " + + "Set NEXTAUTH_URL in your .env to suppress this warning.", ); } diff --git a/packages/api/src/lib/logger.ts b/packages/api/src/lib/logger.ts index 2bd360f..8793320 100644 --- a/packages/api/src/lib/logger.ts +++ b/packages/api/src/lib/logger.ts @@ -44,13 +44,13 @@ const redactConfig = { paths: REDACT_PATHS, censor: "[REDACTED]" }; export const logger = isProduction ? pino({ level: LOG_LEVEL, - base: { service: "capakraken-api" }, + base: { service: "nexus-api" }, redact: redactConfig, }) : pino( { level: LOG_LEVEL, - base: { service: "capakraken-api" }, + base: { service: "nexus-api" }, redact: redactConfig, formatters: { level(label: string) { diff --git a/packages/api/src/middleware/rate-limit.ts b/packages/api/src/middleware/rate-limit.ts index 5a6c18b..579a0e1 100644 --- a/packages/api/src/middleware/rate-limit.ts +++ b/packages/api/src/middleware/rate-limit.ts @@ -31,7 +31,7 @@ type RateLimiterBackend = { reset: () => Promise; }; -const DEFAULT_REDIS_KEY_PREFIX = "capakraken:ratelimit"; +const DEFAULT_REDIS_KEY_PREFIX = "nexus:ratelimit"; const DEFAULT_REDIS_BACKEND = process.env["RATE_LIMIT_BACKEND"] as RateLimitBackendMode | undefined; const DEFAULT_REDIS_URL = process.env["REDIS_URL"]?.trim(); const warnedRedisFailures = new Set(); diff --git a/packages/api/src/sse/event-bus.ts b/packages/api/src/sse/event-bus.ts index c9e4ecd..8a34a86 100644 --- a/packages/api/src/sse/event-bus.ts +++ b/packages/api/src/sse/event-bus.ts @@ -201,7 +201,7 @@ const REDIS_URL = : (() => { throw new Error("REDIS_URL required in production"); })()); -const CHANNEL = "capakraken:sse"; +const CHANNEL = "nexus:sse"; let publisher: Redis | null = null; let subscriber: Redis | null = null; diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 8dd2b5f..0f7f8a3 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -42,7 +42,7 @@ const ROLE_DEFAULTS_TTL = 10_000; // We publish a single invalidate message per change; every node subscribes and // clears its local cache on receipt. Failure to publish/subscribe is logged // but never thrown — the TTL above is the fall-back. -const RBAC_INVALIDATE_CHANNEL = "capakraken:rbac-invalidate"; +const RBAC_INVALIDATE_CHANNEL = "nexus:rbac-invalidate"; let _rbacPublisher: Redis | null = null; let _rbacSubscriber: Redis | null = null; diff --git a/packages/db/package.json b/packages/db/package.json index 88b788c..63913fa 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -8,7 +8,7 @@ "./client": "./src/client.ts" }, "scripts": { - "db:doctor": "node ../../scripts/db-doctor.mjs capakraken", + "db:doctor": "node ../../scripts/db-doctor.mjs nexus", "db:push": "node ../../scripts/prisma-with-env.mjs db push --schema ./prisma/schema.prisma", "db:migrate": "node ../../scripts/prisma-with-env.mjs migrate dev --schema ./prisma/schema.prisma", "db:migrate:deploy": "node ../../scripts/prisma-with-env.mjs migrate deploy --schema ./prisma/schema.prisma", diff --git a/packages/db/src/destructive-db-guard.test.ts b/packages/db/src/destructive-db-guard.test.ts index fbd6e9c..a79c87f 100644 --- a/packages/db/src/destructive-db-guard.test.ts +++ b/packages/db/src/destructive-db-guard.test.ts @@ -22,34 +22,34 @@ test.afterEach(() => { process.env = { ...ORIGINAL_ENV }; }); -test("assertDestructiveDbAllowed allows an explicitly confirmed disposable capakraken test database", () => { +test("assertDestructiveDbAllowed allows an explicitly confirmed disposable nexus test database", () => { setEnv({ - DATABASE_URL: "postgresql://tester:secret@localhost:5432/capakraken_test", + DATABASE_URL: "postgresql://tester:secret@localhost:5432/nexus_test", ALLOW_DESTRUCTIVE_DB_TOOLS: "true", - CONFIRM_DESTRUCTIVE_DB_NAME: "capakraken_test", + CONFIRM_DESTRUCTIVE_DB_NAME: "nexus_test", }); const target = assertDestructiveDbAllowed({ commandName: "db:test", - allowedDatabaseNames: ["capakraken_test"], + allowedDatabaseNames: ["nexus_test"], }); - assert.equal(target.databaseName, "capakraken_test"); + assert.equal(target.databaseName, "nexus_test"); assert.equal(target.hostname, "localhost"); }); test("assertDestructiveDbAllowed rejects protected live database names even if allowlisted", () => { setEnv({ - DATABASE_URL: "postgresql://tester:secret@localhost:5432/capakraken", + DATABASE_URL: "postgresql://tester:secret@localhost:5432/nexus", ALLOW_DESTRUCTIVE_DB_TOOLS: "true", - CONFIRM_DESTRUCTIVE_DB_NAME: "capakraken", + CONFIRM_DESTRUCTIVE_DB_NAME: "nexus", }); assert.throws( () => assertDestructiveDbAllowed({ commandName: "db:test", - allowedDatabaseNames: ["capakraken"], + allowedDatabaseNames: ["nexus"], }), /explicitly protected/u, ); @@ -57,7 +57,7 @@ test("assertDestructiveDbAllowed rejects protected live database names even if a test("assertDestructiveDbAllowed rejects missing confirmation", () => { setEnv({ - DATABASE_URL: "postgresql://tester:secret@localhost:5432/capakraken_e2e", + DATABASE_URL: "postgresql://tester:secret@localhost:5432/nexus_e2e", ALLOW_DESTRUCTIVE_DB_TOOLS: "true", CONFIRM_DESTRUCTIVE_DB_NAME: "wrong_db", }); @@ -66,24 +66,24 @@ test("assertDestructiveDbAllowed rejects missing confirmation", () => { () => assertDestructiveDbAllowed({ commandName: "db:test", - allowedDatabaseNames: ["capakraken_e2e"], + allowedDatabaseNames: ["nexus_e2e"], }), - /CONFIRM_DESTRUCTIVE_DB_NAME=capakraken_e2e/u, + /CONFIRM_DESTRUCTIVE_DB_NAME=nexus_e2e/u, ); }); test("assertDestructiveDbAllowed rejects missing destructive allow flag", () => { setEnv({ - DATABASE_URL: "postgresql://tester:secret@localhost:5432/capakraken_ci", + DATABASE_URL: "postgresql://tester:secret@localhost:5432/nexus_ci", ALLOW_DESTRUCTIVE_DB_TOOLS: undefined, - CONFIRM_DESTRUCTIVE_DB_NAME: "capakraken_ci", + CONFIRM_DESTRUCTIVE_DB_NAME: "nexus_ci", }); assert.throws( () => assertDestructiveDbAllowed({ commandName: "db:test", - allowedDatabaseNames: ["capakraken_ci"], + allowedDatabaseNames: ["nexus_ci"], }), /ALLOW_DESTRUCTIVE_DB_TOOLS=true/u, ); @@ -99,19 +99,19 @@ test("assertSafeSeedTarget rejects unexpected legacy disposable databases", () = assert.throws(() => assertSafeSeedTarget("db:seed"), /not in the destructive-tool allowlist/u); }); -test("assertNexusDbTarget accepts non-destructive capakraken targets", () => { +test("assertNexusDbTarget accepts non-destructive nexus targets", () => { setEnv({ - DATABASE_URL: "postgresql://tester:secret@localhost:5432/capakraken_dev", + DATABASE_URL: "postgresql://tester:secret@localhost:5432/nexus_dev", }); const target = assertNexusDbTarget("db:seed:holidays"); - assert.equal(target.databaseName, "capakraken_dev"); + assert.equal(target.databaseName, "nexus_dev"); }); -test("assertNexusDbTarget rejects legacy non-capakraken targets", () => { +test("assertNexusDbTarget rejects legacy non-nexus targets", () => { setEnv({ - DATABASE_URL: "postgresql://tester:secret@localhost:5432/legacy_non_capakraken", + DATABASE_URL: "postgresql://tester:secret@localhost:5432/legacy_non_nexus", }); assert.throws(() => assertNexusDbTarget("db:seed:holidays"), /not a valid Nexus target/u); diff --git a/packages/db/src/destructive-db-guard.ts b/packages/db/src/destructive-db-guard.ts index bc0f923..1f59824 100644 --- a/packages/db/src/destructive-db-guard.ts +++ b/packages/db/src/destructive-db-guard.ts @@ -6,7 +6,7 @@ interface DestructiveGuardOptions { requireConfirmation?: boolean; } -const PROTECTED_DATABASE_NAMES = new Set(["capakraken"]); +const PROTECTED_DATABASE_NAMES = new Set(["nexus"]); export function parseDatabaseUrl(rawUrl: string) { const parsed = new URL(rawUrl); diff --git a/packages/db/src/reset-dispo-import.ts b/packages/db/src/reset-dispo-import.ts index 08a92ee..a5cda10 100644 --- a/packages/db/src/reset-dispo-import.ts +++ b/packages/db/src/reset-dispo-import.ts @@ -157,7 +157,7 @@ async function main() { const options = parseArgs(process.argv.slice(2)); const target = assertDestructiveDbAllowed({ commandName: "db:reset:dispo", - allowedDatabaseNames: ["capakraken_test", "capakraken_e2e", "capakraken_ci"], + allowedDatabaseNames: ["nexus_test", "nexus_e2e", "nexus_ci"], }); const databaseUrl = process.env.DATABASE_URL; diff --git a/packages/db/src/safe-destructive-env.ts b/packages/db/src/safe-destructive-env.ts index a52882e..f145e0c 100644 --- a/packages/db/src/safe-destructive-env.ts +++ b/packages/db/src/safe-destructive-env.ts @@ -4,7 +4,7 @@ import { parseDatabaseUrl, } from "./destructive-db-guard.js"; -const TEST_DATABASE_NAMES = ["capakraken_test", "capakraken_e2e", "capakraken_ci"]; +const TEST_DATABASE_NAMES = ["nexus_test", "nexus_e2e", "nexus_ci"]; export function assertSafeSeedTarget(commandName: string) { return assertDestructiveDbAllowed({ @@ -24,7 +24,7 @@ export function assertNexusDbTarget(commandName: string) { const target = parseDatabaseUrl(rawUrl); - if (!target.databaseName.startsWith("capakraken")) { + if (!target.databaseName.startsWith("nexus")) { throw new Error( `${commandName} aborted: database '${target.databaseName}' is not a valid Nexus target. Target=${formatTarget(target)}`, ); diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index ac9a7d8..dfde5c3 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -2372,7 +2372,7 @@ async function main() { .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); - const email = `${eid}@capakraken.example`; + const email = `${eid}@nexus.example`; const lcrCents = Math.round(lcr * 100); const ucrCents = Math.round(ucr * 100); const availability = computeAvailability(fraction, availDays); diff --git a/packages/db/src/update-excel.mjs b/packages/db/src/update-excel.mjs index 733e0ba..136cb67 100644 --- a/packages/db/src/update-excel.mjs +++ b/packages/db/src/update-excel.mjs @@ -23,7 +23,7 @@ function toDisplayName(eid) { } function toEmail(eid) { - return `${eid}@capakraken.example`; + return `${eid}@nexus.example`; } function computeSkillLabel(chapter, typeOfWork) { @@ -150,7 +150,7 @@ async function main() { { col: 15, // O header: "Email\n(generated)", - doc: "Generated email: firstname.lastname@capakraken.example. Required unique field in Nexus. Replace with real email in production.", + doc: "Generated email: firstname.lastname@nexus.example. Required unique field in Nexus. Replace with real email in production.", }, { col: 16, // P diff --git a/restart.sh b/restart.sh index 531efe7..bdfa411 100755 --- a/restart.sh +++ b/restart.sh @@ -40,7 +40,7 @@ docker compose --profile "$PROFILE" stop app 2>/dev/null || true if $CLEAN; then echo "==> Removing stale node_modules and .next volumes..." - docker volume rm capakraken_node_modules capakraken_next 2>/dev/null || true + docker volume rm nexus_node_modules nexus_next 2>/dev/null || true fi echo "==> Rebuilding and starting ($( [[ -z "$SERVICES" ]] && echo "all services" || echo "$SERVICES" ))..." diff --git a/samples/CDP/ToDecrypt/Archive.zip b/samples/CDP/ToDecrypt/Archive.zip new file mode 100644 index 0000000..022e1ec Binary files /dev/null and b/samples/CDP/ToDecrypt/Archive.zip differ diff --git a/samples/generate_skillmatrix.mjs b/samples/generate_skillmatrix.mjs index bc132ea..cef721c 100644 --- a/samples/generate_skillmatrix.mjs +++ b/samples/generate_skillmatrix.mjs @@ -5,9 +5,9 @@ import { createRequire } from "module"; import { writeFileSync, mkdirSync } from "fs"; const require = createRequire(import.meta.url); -const XLSX = require("/home/hartmut/Documents/Copilot/capakraken/node_modules/.pnpm/xlsx@0.18.5/node_modules/xlsx/xlsx.js"); +const XLSX = require("/home/hartmut/Documents/Copilot/nexus/node_modules/.pnpm/xlsx@0.18.5/node_modules/xlsx/xlsx.js"); -const OUT_DIR = "/home/hartmut/Documents/Copilot/capakraken/samples/skillmatrix_dummydata"; +const OUT_DIR = "/home/hartmut/Documents/Copilot/nexus/samples/skillmatrix_dummydata"; mkdirSync(OUT_DIR, { recursive: true }); // ─── Skill Definitions ───────────────────────────────────────────────────── diff --git a/scripts/check-architecture-guardrails.mjs b/scripts/check-architecture-guardrails.mjs index b6e6a33..b43aca9 100644 --- a/scripts/check-architecture-guardrails.mjs +++ b/scripts/check-architecture-guardrails.mjs @@ -719,7 +719,7 @@ export const rules = [ ], forbidden: [ { - pattern: /pnpm --filter @capakraken\/db exec prisma generate/, + pattern: /pnpm --filter @nexus\/db exec prisma generate/, message: "CI must not call prisma generate directly outside the workspace wrapper", }, ], diff --git a/scripts/db-target-guard.mjs b/scripts/db-target-guard.mjs index 8cbda75..55f06c2 100644 --- a/scripts/db-target-guard.mjs +++ b/scripts/db-target-guard.mjs @@ -4,7 +4,7 @@ export function formatDatabaseTarget(parsedUrl, databaseName) { return `${parsedUrl.protocol}//${decodeURIComponent(parsedUrl.username)}@${parsedUrl.hostname}${parsedUrl.port ? `:${parsedUrl.port}` : ""}/${databaseName}`; } -export function inspectDatabaseUrl(rawUrl, expectedDatabase = "capakraken") { +export function inspectDatabaseUrl(rawUrl, expectedDatabase = "nexus") { if (!rawUrl) { throw new Error("DATABASE_URL is not configured."); } @@ -82,5 +82,5 @@ export function shouldGuardPrismaCommand(args) { } export function getExpectedDatabaseName() { - return process.env.CAPAKRAKEN_EXPECTED_DB_NAME?.trim() || "capakraken"; + return process.env.NEXUS_EXPECTED_DB_NAME?.trim() || "nexus"; } diff --git a/scripts/db-target-guard.test.mjs b/scripts/db-target-guard.test.mjs index 17413b7..1ef5a75 100644 --- a/scripts/db-target-guard.test.mjs +++ b/scripts/db-target-guard.test.mjs @@ -6,21 +6,21 @@ import { } from "./db-target-guard.mjs"; describe("db target guard", () => { - it("accepts the expected capakraken database target", () => { + it("accepts the expected nexus database target", () => { const result = inspectDatabaseUrl( - "postgresql://capakraken:secret@localhost:5432/capakraken", - "capakraken", + "postgresql://nexus:secret@localhost:5432/nexus", + "nexus", ); - assert.equal(result.databaseName, "capakraken"); - assert.equal(result.expectedDatabase, "capakraken"); - assert.equal(result.target, "postgresql://capakraken@localhost:5432/capakraken"); + assert.equal(result.databaseName, "nexus"); + assert.equal(result.expectedDatabase, "nexus"); + assert.equal(result.target, "postgresql://nexus@localhost:5432/nexus"); }); it("rejects a mismatched database target", () => { assert.throws( - () => inspectDatabaseUrl("postgresql://capakraken:secret@localhost:5432/planarchy", "capakraken"), - /Unexpected database target 'planarchy'\. Expected 'capakraken'\./, + () => inspectDatabaseUrl("postgresql://nexus:secret@localhost:5432/planarchy", "nexus"), + /Unexpected database target 'planarchy'\. Expected 'nexus'\./, ); }); diff --git a/scripts/export-dev-seed.mjs b/scripts/export-dev-seed.mjs index 8e0442e..9c32b2f 100644 --- a/scripts/export-dev-seed.mjs +++ b/scripts/export-dev-seed.mjs @@ -10,8 +10,8 @@ * node scripts/export-dev-seed.mjs * * Requirements: - * - The capakraken-postgres-1 Docker container must be running - * - DATABASE_URL must point to a local capakraken database + * - The nexus-postgres-1 Docker container must be running + * - DATABASE_URL must point to a local nexus database */ import { execSync, spawnSync } from "node:child_process"; @@ -48,7 +48,7 @@ if (!["localhost", "127.0.0.1", "::1"].includes(host)) { // ── Docker container check ──────────────────────────────────────────────────── -const CONTAINER = "capakraken-postgres-1"; +const CONTAINER = "nexus-postgres-1"; const containerCheck = spawnSync("docker", ["inspect", "--format={{.State.Running}}", CONTAINER], { encoding: "utf8", }); @@ -83,8 +83,8 @@ const excludeFlags = EXCLUDE_TABLES.flatMap((t) => ["--exclude-table-data", `pub // ── Run pg_dump inside the Docker container ─────────────────────────────────── -const DB_USER = decodeURIComponent(parsedUrl.username) || "capakraken"; -const DB_NAME = parsedUrl.pathname.replace(/^\/+/, "") || "capakraken"; +const DB_USER = decodeURIComponent(parsedUrl.username) || "nexus"; +const DB_NAME = parsedUrl.pathname.replace(/^\/+/, "") || "nexus"; const DB_PORT = parsedUrl.port || "5432"; console.log(`🔍 Exporting ${DB_USER}@${host}:${DB_PORT}/${DB_NAME} …`); diff --git a/scripts/harden-postgres.sh b/scripts/harden-postgres.sh index bb7bf20..eabb7dc 100755 --- a/scripts/harden-postgres.sh +++ b/scripts/harden-postgres.sh @@ -2,8 +2,8 @@ # Remove SUPERUSER from the application database user # Run after initial setup: bash scripts/harden-postgres.sh -DB_USER="${DB_USER:-capakraken}" -DB_NAME="${DB_NAME:-capakraken}" +DB_USER="${DB_USER:-nexus}" +DB_NAME="${DB_NAME:-nexus}" echo "Hardening PostgreSQL for $DB_USER..." diff --git a/scripts/import-dev-seed.mjs b/scripts/import-dev-seed.mjs index 3f46c2b..04d8a27 100644 --- a/scripts/import-dev-seed.mjs +++ b/scripts/import-dev-seed.mjs @@ -10,8 +10,8 @@ * node scripts/import-dev-seed.mjs * * Requirements: - * - The capakraken-postgres-1 Docker container must be running - * - DATABASE_URL must point to a local capakraken database + * - The nexus-postgres-1 Docker container must be running + * - DATABASE_URL must point to a local nexus database * - dev-seed.sql must exist (run export-dev-seed.mjs first) */ @@ -46,13 +46,13 @@ if (!["localhost", "127.0.0.1", "::1"].includes(host)) { process.exit(1); } -const DB_USER = decodeURIComponent(parsedUrl.username) || "capakraken"; -const DB_NAME = parsedUrl.pathname.replace(/^\/+/, "") || "capakraken"; +const DB_USER = decodeURIComponent(parsedUrl.username) || "nexus"; +const DB_NAME = parsedUrl.pathname.replace(/^\/+/, "") || "nexus"; const DB_PORT = parsedUrl.port || "5432"; // ── Docker container check ──────────────────────────────────────────────────── -const CONTAINER = "capakraken-postgres-1"; +const CONTAINER = "nexus-postgres-1"; const containerCheck = spawnSync("docker", ["inspect", "--format={{.State.Running}}", CONTAINER], { encoding: "utf8", }); diff --git a/scripts/prisma-with-env.mjs b/scripts/prisma-with-env.mjs index 7e41bf9..3341c48 100644 --- a/scripts/prisma-with-env.mjs +++ b/scripts/prisma-with-env.mjs @@ -27,7 +27,7 @@ if (shouldGuardPrismaCommand(prismaArgs)) { } catch (error) { console.error(error instanceof Error ? error.message : String(error)); console.error("Refusing to run Prisma against an unexpected database target."); - console.error("Use the repo env files for Nexus, or set CAPAKRAKEN_EXPECTED_DB_NAME explicitly if you intentionally target another database."); + console.error("Use the repo env files for Nexus, or set NEXUS_EXPECTED_DB_NAME explicitly if you intentionally target another database."); process.exit(1); } } diff --git a/scripts/start.sh b/scripts/start.sh index 974f646..03d66af 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -15,7 +15,7 @@ sleep 2 # 2. Wait for PostgreSQL to be healthy echo " Waiting for PostgreSQL..." for i in {1..30}; do - if docker compose exec -T postgres pg_isready -U capakraken -d capakraken -q 2>/dev/null; then + if docker compose exec -T postgres pg_isready -U nexus -d nexus -q 2>/dev/null; then break fi sleep 1 diff --git a/scripts/worktree-hygiene.test.mjs b/scripts/worktree-hygiene.test.mjs index 4419091..7abc1d4 100644 --- a/scripts/worktree-hygiene.test.mjs +++ b/scripts/worktree-hygiene.test.mjs @@ -9,7 +9,7 @@ import { function createGitStub(statusOutput) { return (args) => { if (args[0] === "rev-parse" && args[1] === "--show-toplevel") { - return "/tmp/capakraken\n"; + return "/tmp/nexus\n"; } if (args[0] === "rev-parse" && args[1] === "--abbrev-ref") { return "main\n"; diff --git a/tooling/deploy/deploy.env.example b/tooling/deploy/deploy.env.example index 2804f91..159bac2 100644 --- a/tooling/deploy/deploy.env.example +++ b/tooling/deploy/deploy.env.example @@ -1,5 +1,5 @@ -APP_IMAGE=ghcr.io/example/capakraken-app:sha-abc123 -MIGRATOR_IMAGE=ghcr.io/example/capakraken-migrator:sha-abc123 +APP_IMAGE=ghcr.io/example/nexus-app:sha-abc123 +MIGRATOR_IMAGE=ghcr.io/example/nexus-migrator:sha-abc123 APP_HOST_PORT=3000 GHCR_USERNAME= GHCR_TOKEN= diff --git a/tooling/migrate/rename-to-nexus.sh b/tooling/migrate/rename-to-nexus.sh new file mode 100755 index 0000000..3e08ab9 --- /dev/null +++ b/tooling/migrate/rename-to-nexus.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# +# Phase 3 cutover: migrate the running stack from `capakraken` (DB, role, +# volumes, compose project) to `nexus`. Intended to be run inside a brief +# maintenance window — the app is stopped for the duration. +# +# Idempotency: each step checks the current state. Re-running after a +# partial run only does the missing pieces. The script never DROPS the +# legacy `capakraken` DB or role — that's a manual decision after a +# stability window. +# +# Usage: +# ./tooling/migrate/rename-to-nexus.sh dev # against docker-compose.yml +# ./tooling/migrate/rename-to-nexus.sh prod # against docker-compose.prod.yml +# +# Requires: +# - POSTGRES_PASSWORD set in env (or .env file picked up by docker compose) +# - docker compose CLI v2+ +# - the `nexus` branch of code already checked out (compose files reference nexus_*) + +set -euo pipefail + +MODE="${1:-dev}" +case "$MODE" in + dev) + COMPOSE_FILE=docker-compose.yml + OLD_PROJECT=capakraken + NEW_PROJECT=nexus + VOLUMES=(pgdata node_modules next) + ;; + prod) + COMPOSE_FILE=docker-compose.prod.yml + OLD_PROJECT=capakraken-prod + NEW_PROJECT=nexus-prod + VOLUMES=(prod_pgdata prod_redis) + ;; + *) + echo "Usage: $0 [dev|prod]" >&2 + exit 2 + ;; +esac + +if [ -z "${POSTGRES_PASSWORD:-}" ]; then + echo "POSTGRES_PASSWORD must be set in the environment." >&2 + exit 2 +fi + +DUMP_FILE="/tmp/capakraken-pre-rename-$(date +%Y%m%d-%H%M%S).sql" + +echo "=== Phase 3 cutover ($MODE) ===" +echo "compose: $COMPOSE_FILE project: $OLD_PROJECT → $NEW_PROJECT dump: $DUMP_FILE" +echo + +#─────────────────────────────────────────────────────────────────────────────── +# 1. Stop the app (DB stays up so we can dump it). +#─────────────────────────────────────────────────────────────────────────────── +echo "[1/7] Stopping app container under old project name..." +docker compose -p "$OLD_PROJECT" -f "$COMPOSE_FILE" stop app 2>/dev/null || true + +#─────────────────────────────────────────────────────────────────────────────── +# 2. Capture row counts for verification. +#─────────────────────────────────────────────────────────────────────────────── +echo "[2/7] Capturing pre-rename row counts..." +PRE_COUNTS=$(docker compose -p "$OLD_PROJECT" -f "$COMPOSE_FILE" exec -T postgres \ + psql -U capakraken -d capakraken -t -c \ + "SELECT table_name, n_live_tup FROM pg_stat_user_tables ORDER BY table_name;") +echo "$PRE_COUNTS" | head -20 +echo "..." + +#─────────────────────────────────────────────────────────────────────────────── +# 3. Dump existing DB. +#─────────────────────────────────────────────────────────────────────────────── +echo "[3/7] pg_dump capakraken → $DUMP_FILE..." +docker compose -p "$OLD_PROJECT" -f "$COMPOSE_FILE" exec -T postgres \ + pg_dump -U capakraken -d capakraken --clean --if-exists > "$DUMP_FILE" +echo "Dump size: $(du -h "$DUMP_FILE" | cut -f1)" + +#─────────────────────────────────────────────────────────────────────────────── +# 4. Create new role + DB inside the running postgres container. +#─────────────────────────────────────────────────────────────────────────────── +echo "[4/7] Creating nexus role + database..." +docker compose -p "$OLD_PROJECT" -f "$COMPOSE_FILE" exec -T postgres \ + psql -U capakraken -d postgres </dev/null 2>&1; then + echo " Source volume $src missing — skip" + continue + fi + if docker volume inspect "$dst" >/dev/null 2>&1; then + echo " Destination volume $dst already exists — skip" + continue + fi + echo " $src → $dst" + docker volume create "$dst" >/dev/null + docker run --rm \ + -v "${src}:/from:ro" \ + -v "${dst}:/to" \ + alpine sh -c "cd /from && cp -a . /to/" +done + +#─────────────────────────────────────────────────────────────────────────────── +# 7. Bring up under new compose project name. +#─────────────────────────────────────────────────────────────────────────────── +echo "[7/7] Starting stack under new project name '$NEW_PROJECT'..." +PROFILE="" +[ "$MODE" = "dev" ] && PROFILE="--profile full" +# shellcheck disable=SC2086 +docker compose -p "$NEW_PROJECT" -f "$COMPOSE_FILE" $PROFILE up -d + +echo +echo "Waiting 15s for postgres to be ready..." +sleep 15 + +echo "=== Verification ===" +POST_COUNTS=$(docker compose -p "$NEW_PROJECT" -f "$COMPOSE_FILE" exec -T postgres \ + psql -U nexus -d nexus -t -c \ + "SELECT table_name, n_live_tup FROM pg_stat_user_tables ORDER BY table_name;") +echo "Post-rename row counts (sample):" +echo "$POST_COUNTS" | head -20 + +if diff <(echo "$PRE_COUNTS") <(echo "$POST_COUNTS") >/dev/null; then + echo "✓ Row counts match — migration verified." +else + echo "⚠ Row counts differ — review diff:" + diff <(echo "$PRE_COUNTS") <(echo "$POST_COUNTS") | head +fi + +echo +echo "Done. Old DB+role retained for rollback. Dump kept at $DUMP_FILE." +echo "After a stability window, drop with:" +echo " docker compose -p $NEW_PROJECT -f $COMPOSE_FILE exec postgres psql -U nexus -d postgres \\" +echo " -c 'DROP DATABASE capakraken; DROP ROLE capakraken;'"