From 35db5c57e6870eb0ea9cb2672a7ce2148ec05197 Mon Sep 17 00:00:00 2001
From: Lucas Berger
Date: Mon, 15 Jun 2026 21:31:26 -0400
Subject: [PATCH] feat(12-05): drop DB-vs-env aside, add read-only DB-name
field (gaps 1, 3)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Remove the 'written to the database — not your environment file' aside from the Instance step intro
- Render a read-only, disabled DB-name field under App URL, populated from GET /api/setup/status dbName
- Helper text explains DB is configured via Docker env; only dbName is surfaced (T-12-3DB)
- Tests: assert aside absent, DB field readOnly/disabled with mocked dbName, existing DB validation row intact
---
apps/pwa/src/routes/SetupPage.test.tsx | 70 +++++++++++++++++++++++++-
apps/pwa/src/routes/SetupPage.tsx | 43 ++++++++++++++--
2 files changed, 109 insertions(+), 4 deletions(-)
diff --git a/apps/pwa/src/routes/SetupPage.test.tsx b/apps/pwa/src/routes/SetupPage.test.tsx
index 32ca942..80caf21 100644
--- a/apps/pwa/src/routes/SetupPage.test.tsx
+++ b/apps/pwa/src/routes/SetupPage.test.tsx
@@ -210,9 +210,14 @@ describe('SetupPage — Already Locked screen', () => {
describe('SetupPage — Step 2 VAPID validation (CR-01 gap)', () => {
let queryClient: QueryClient;
- beforeEach(() => {
+ beforeEach(async () => {
queryClient = makeQueryClient();
vi.resetAllMocks();
+ const { fetchSetupStatus } = await import('../api/client.js');
+ (fetchSetupStatus as ReturnType).mockResolvedValue({
+ setupComplete: false,
+ dbName: 'familysync',
+ });
});
/**
@@ -305,3 +310,66 @@ describe('SetupPage — Step 2 VAPID validation (CR-01 gap)', () => {
});
});
});
+
+// ── Tests: Step 2 Instance copy + read-only DB-name field (gaps 1, 3-frontend) ──
+
+describe('SetupPage — Step 2 Instance copy + DB-name field (gaps 1, 3)', () => {
+ let queryClient: QueryClient;
+
+ beforeEach(async () => {
+ queryClient = makeQueryClient();
+ vi.resetAllMocks();
+ const { fetchSetupStatus } = await import('../api/client.js');
+ (fetchSetupStatus as ReturnType).mockResolvedValue({
+ setupComplete: false,
+ dbName: 'familysync',
+ });
+ });
+
+ async function advanceToStep2() {
+ renderSetupPage(queryClient);
+ const continueBtn = await screen.findByRole('button', { name: 'Continue' });
+ fireEvent.click(continueBtn);
+ await screen.findByText('Instance Configuration');
+ }
+
+ it('Instance step intro no longer contains the DB-vs-env-file aside (gap 1)', async () => {
+ await advanceToStep2();
+ expect(screen.queryByText(/not your environment file/i)).toBeNull();
+ // First sentence is preserved
+ expect(screen.getByText(/Enter your instance/i)).toBeInTheDocument();
+ });
+
+ it('renders a read-only, disabled DB-name field populated from status dbName (gap 3)', async () => {
+ await advanceToStep2();
+ const dbField = await screen.findByLabelText('Database');
+ await waitFor(() => {
+ expect(dbField).toHaveValue('familysync');
+ });
+ expect(dbField).toHaveAttribute('readonly');
+ expect(dbField).toBeDisabled();
+ expect(dbField).toHaveAttribute('aria-readonly', 'true');
+ });
+
+ it('keeps the existing "Database connection verified" validation row available', async () => {
+ const { postSetupConfig, validateSetupDb, validateSetupOidc, validateSetupVapid } =
+ await import('../api/client.js');
+ (postSetupConfig as ReturnType).mockResolvedValue(undefined);
+ (validateSetupDb as ReturnType).mockResolvedValue(undefined);
+ (validateSetupOidc as ReturnType).mockResolvedValue(undefined);
+ (validateSetupVapid as ReturnType).mockResolvedValue(undefined);
+
+ await advanceToStep2();
+ fireEvent.change(screen.getByLabelText('App URL'), { target: { value: 'https://app.example.com' } });
+ fireEvent.change(screen.getByLabelText('OIDC issuer URL'), { target: { value: 'https://auth.example.com' } });
+ fireEvent.change(screen.getByLabelText('OIDC client ID'), { target: { value: 'familysync' } });
+ fireEvent.change(screen.getByLabelText('VAPID public key'), { target: { value: 'BHtest123' } });
+ fireEvent.click(screen.getByRole('button', { name: 'Save & Validate' }));
+
+ await waitFor(() => {
+ expect(screen.getByText('Database connection verified.')).toBeInTheDocument();
+ });
+ });
+});
+
+// (gap-4 Back-navigation tests added in Task 2)
diff --git a/apps/pwa/src/routes/SetupPage.tsx b/apps/pwa/src/routes/SetupPage.tsx
index 92f7757..23da0fa 100644
--- a/apps/pwa/src/routes/SetupPage.tsx
+++ b/apps/pwa/src/routes/SetupPage.tsx
@@ -21,9 +21,10 @@
*/
import { useState, useRef, useEffect } from 'react';
-import { useMutation } from '@tanstack/react-query';
+import { useMutation, useQuery } from '@tanstack/react-query';
import { ShieldCheck, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
import {
+ fetchSetupStatus,
postSetupConfig,
validateSetupDb,
validateSetupOidc,
@@ -442,6 +443,17 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) {
const [vapidPublicKey, setVapidPublicKey] = useState('');
const [fieldError, setFieldError] = useState(null);
+ // Gap 3 (frontend): fetch the env-derived, non-secret DB name so the
+ // "database connection verified" row below has an on-screen referent.
+ // Only dbName is surfaced — DB_HOST/DB_USER/DB_PASSWORD are never fetched (T-12-3DB).
+ const { data: setupStatus } = useQuery({
+ queryKey: ['setupStatus'],
+ queryFn: fetchSetupStatus,
+ staleTime: 0,
+ retry: false,
+ });
+ const dbName = setupStatus?.dbName ?? '';
+
const [validationRows, setValidationRows] = useState>({
db: 'idle',
oidc: 'idle',
@@ -553,8 +565,7 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) {
lineHeight: 1.5,
}}
>
- Enter your instance's connection details. These are written to the database — not
- your environment file.
+ Enter your instance's connection details.
{/* App URL */}
@@ -574,6 +585,32 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) {
The public URL where FamilySync is reachable.
+ {/* Database (read-only, env-derived) — gives the DB validation row below a referent */}
+
+
+
+
+ Configured via the server's Docker environment (DB_HOST,{' '}
+ DB_PORT, DB_USER, DB_PASSWORD) — not entered here.
+
+
+
{/* OIDC Issuer */}