272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { useSearchParams } from 'next/navigation';
|
|
|
|
type EnvStatus = {
|
|
required: Record<string, boolean>;
|
|
optional: Record<string, boolean>;
|
|
};
|
|
|
|
type InstallStatus = {
|
|
env: EnvStatus;
|
|
db: {
|
|
connected: boolean;
|
|
schemaReady: boolean;
|
|
usersCount: number;
|
|
};
|
|
};
|
|
|
|
const requiredLabels: Record<string, string> = {
|
|
DATABASE_URL: 'Database connection string',
|
|
AUTH_SECRET: 'Auth cookie secret',
|
|
NEXT_PUBLIC_NODE_DOMAIN: 'Public node domain',
|
|
NEXT_PUBLIC_NODE_NAME: 'Node display name',
|
|
ADMIN_EMAILS: 'Admin emails list',
|
|
};
|
|
|
|
const optionalLabels: Record<string, string> = {};
|
|
|
|
const StepCard = ({
|
|
title,
|
|
description,
|
|
status,
|
|
children,
|
|
}: {
|
|
title: string;
|
|
description?: string;
|
|
status?: { label: string; tone: 'ok' | 'warn' };
|
|
children: React.ReactNode;
|
|
}) => (
|
|
<div className="install-card">
|
|
<div className="install-card-header">
|
|
<div>
|
|
<div className="install-card-title">{title}</div>
|
|
{description && <div className="install-card-desc">{description}</div>}
|
|
</div>
|
|
{status && (
|
|
<div className={`install-step-status ${status.tone}`}>
|
|
{status.label}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
export default function InstallPage() {
|
|
const searchParams = useSearchParams();
|
|
const force = searchParams.get('force') === '1';
|
|
const [status, setStatus] = useState<InstallStatus | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const loadStatus = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch('/api/install/status');
|
|
const data = await res.json();
|
|
setStatus(data);
|
|
} catch {
|
|
setStatus(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadStatus();
|
|
}, []);
|
|
|
|
const isInstalled = status?.db.connected && status?.db.schemaReady && status?.db.usersCount > 0;
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="install-shell">
|
|
<div className="install-card">Checking install status...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!status) {
|
|
return (
|
|
<div className="install-shell">
|
|
<div className="install-card">
|
|
<h1>Setup Wizard</h1>
|
|
<p>We could not load the install status.</p>
|
|
<button className="btn btn-primary" onClick={loadStatus}>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isInstalled && !force) {
|
|
return (
|
|
<div className="install-shell">
|
|
<div className="install-card">
|
|
<h1>Synapsis is already set up</h1>
|
|
<p>Your database is connected and at least one user exists.</p>
|
|
<Link href="/" className="btn btn-primary" style={{ marginTop: '12px' }}>
|
|
Go to home
|
|
</Link>
|
|
<Link href="/install?force=1" className="btn btn-ghost" style={{ marginTop: '12px' }}>
|
|
Re-run setup
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const missingRequired = Object.entries(status.env.required).filter(([, ok]) => !ok);
|
|
const missingOptional = Object.entries(status.env.optional).filter(([, ok]) => !ok);
|
|
const envComplete = missingRequired.length === 0;
|
|
const dbComplete = status.db.connected && status.db.schemaReady;
|
|
const adminComplete = status.db.usersCount > 0;
|
|
const completedSteps = [envComplete, dbComplete, adminComplete].filter(Boolean).length;
|
|
const progressPercent = Math.round((completedSteps / 3) * 100);
|
|
|
|
return (
|
|
<div className="install-shell">
|
|
<div className="install-hero">
|
|
<div className="install-header">
|
|
<div>
|
|
<h1>Synapsis Setup</h1>
|
|
<p>Follow these steps to complete your installation.</p>
|
|
</div>
|
|
<button className="btn btn-ghost" onClick={loadStatus}>
|
|
Recheck
|
|
</button>
|
|
</div>
|
|
<div className="install-progress">
|
|
<div className="install-progress-bar">
|
|
<div className="install-progress-fill" style={{ width: `${progressPercent}%` }} />
|
|
</div>
|
|
<div className="install-progress-meta">
|
|
<span>{completedSteps} / 3 steps complete</span>
|
|
<span>{progressPercent}%</span>
|
|
</div>
|
|
</div>
|
|
<div className="install-summary">
|
|
<div>
|
|
<div className="install-summary-title">Environment</div>
|
|
<div className="install-summary-value">{envComplete ? 'Ready' : 'Needs values'}</div>
|
|
</div>
|
|
<div>
|
|
<div className="install-summary-title">Database</div>
|
|
<div className="install-summary-value">{dbComplete ? 'Ready' : 'Not ready'}</div>
|
|
</div>
|
|
<div>
|
|
<div className="install-summary-title">Admin</div>
|
|
<div className="install-summary-value">{adminComplete ? 'Ready' : 'Not created'}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<StepCard
|
|
title="1. Environment variables"
|
|
description="Set required values in your deployment environment (.env or platform settings)."
|
|
status={{
|
|
label: envComplete ? 'Complete' : 'Needs attention',
|
|
tone: envComplete ? 'ok' : 'warn',
|
|
}}
|
|
>
|
|
<div className="install-grid">
|
|
{Object.entries(status.env.required).map(([key, ok]) => (
|
|
<div key={key} className={`install-item ${ok ? 'ok' : 'missing'}`}>
|
|
<div className="install-item-title">{key}</div>
|
|
<div className="install-item-desc">{requiredLabels[key]}</div>
|
|
<div className="install-item-status">{ok ? 'Set' : 'Missing'}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{missingRequired.length > 0 && (
|
|
<div className="install-hint">
|
|
Missing required values: {missingRequired.map(([key]) => key).join(', ')}
|
|
</div>
|
|
)}
|
|
{missingRequired.some(([key]) => key === 'AUTH_SECRET') && (
|
|
<div className="install-hint" style={{ marginTop: '8px' }}>
|
|
To generate an AUTH_SECRET, run:
|
|
<code style={{ display: 'block', padding: '8px', background: 'rgba(0,0,0,0.05)', marginTop: '4px', borderRadius: '4px', fontFamily: 'monospace' }}>
|
|
openssl rand -base64 33
|
|
</code>
|
|
</div>
|
|
)}
|
|
<div className="install-subtitle">Optional</div>
|
|
<div className="install-grid">
|
|
{Object.entries(status.env.optional).map(([key, ok]) => (
|
|
<div key={key} className={`install-item ${ok ? 'ok' : 'missing'}`}>
|
|
<div className="install-item-title">{key}</div>
|
|
<div className="install-item-desc">{optionalLabels[key]}</div>
|
|
<div className="install-item-status">{ok ? 'Set' : 'Not set'}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{missingOptional.length > 0 && (
|
|
<div className="install-hint">
|
|
Optional values missing: {missingOptional.map(([key]) => key).join(', ')}
|
|
</div>
|
|
)}
|
|
</StepCard>
|
|
|
|
<StepCard
|
|
title="2. Database"
|
|
description="Ensure the database is reachable and the schema is pushed."
|
|
status={{
|
|
label: dbComplete ? 'Complete' : 'Needs attention',
|
|
tone: dbComplete ? 'ok' : 'warn',
|
|
}}
|
|
>
|
|
<div className="install-status-row">
|
|
<span>Database connection</span>
|
|
<strong>{status.db.connected ? 'Connected' : 'Not connected'}</strong>
|
|
</div>
|
|
<div className="install-status-row">
|
|
<span>Schema</span>
|
|
<strong>{status.db.schemaReady ? 'Ready' : 'Missing tables'}</strong>
|
|
</div>
|
|
{!status.db.schemaReady && (
|
|
<div className="install-hint">
|
|
Run <code>npm run db:push</code> to create tables.
|
|
</div>
|
|
)}
|
|
</StepCard>
|
|
|
|
<StepCard
|
|
title="3. Create admin account"
|
|
description="Register your first account, then grant admin access."
|
|
status={{
|
|
label: adminComplete ? 'Complete' : 'Needs attention',
|
|
tone: adminComplete ? 'ok' : 'warn',
|
|
}}
|
|
>
|
|
<div className="install-status-row">
|
|
<span>Existing users</span>
|
|
<strong>{status.db.usersCount}</strong>
|
|
</div>
|
|
<p className="install-body">
|
|
Register a user via the login page. Then add their email to
|
|
<code>ADMIN_EMAILS</code> and redeploy.
|
|
</p>
|
|
<Link href="/login" className="btn btn-primary">
|
|
Go to login / register
|
|
</Link>
|
|
</StepCard>
|
|
|
|
<StepCard
|
|
title="4. Launch"
|
|
description="Once you have at least one user and admin access, you are ready."
|
|
>
|
|
<Link href="/" className="btn btn-primary">
|
|
Go to home
|
|
</Link>
|
|
<Link href="/admin" className="btn btn-ghost">
|
|
Open moderation dashboard
|
|
</Link>
|
|
</StepCard>
|
|
</div>
|
|
);
|
|
}
|