Setup Wizard — Algorithms & Architecture
The Setup Wizard guides first-time administrators through initial configuration. Accessible at /setup.
Request Flow Algorithm
Request ──► SetupMiddleware ──► MaintenanceMiddleware ──► Controller
│ │
┌────────────┘ │
▼ │
isSetup() == false? │
├─ Yes → redirect /setup │
└─ No → continue │
┌──────────┘
▼
isMaintenance() == true?
├─ Yes → UnderConstruction page
└─ No → continue
│
▼
isSeeder() == true?
├─ Yes → redirect /portal (admin)
└─ No → render landing pageKey Algorithm: IdentifierAppService::isSetup()
This is the master check that determines if setup is complete. Located in app/Services/Common/IdentifierAppService.php:
public static function isSetup(): bool
{
// 1. Super user must exist
if (! $setup['superUser']) return false;
// 2. All seeders must have run
if (! $seeder['geographic']) return false;
if (! $seeder['gender'] || ! $seeder['ethnicity'] || ! $seeder['religion']) return false;
if (! $seeder['localization']) return false;
// 3. Core settings must exist
if (! Setting::query()->where('module', 'core')->where('group', 'general')->exists()) return false;
// 4. About config must be filled
if (! Setting::query()->where('module', 'core')->where('group', 'general')->where('key', 'yayasan_name')->exists()) return false;
// 5. School + education level must exist
if (! $initial['school']) return false;
// 6. At least one article/news must exist
if (! $initial['news']) return false;
// 7. Contact + social media must be configured
if (! Setting::query()->where('module', 'contact')->exists()) return false;
if (! Setting::query()->where('module', 'social')->exists()) return false;
return true;
}Why this matters: If the frontend ever sends the user away from /setup when not ready, this method is the single source of truth. All conditions must pass before the app leaves setup mode.
Auto-Navigation Algorithm
When a returning user visits /setup, the wizard calculates the first incomplete step. This avoids forcing users to redo completed steps.
Process Flow
Backend returns 8 checks: Frontend computes step:
[0] Super User ──── completed ──► Step 1 done?
[1] Geographic ──── completed ──┐
[2] Core Settings ─ completed ┤ Step 2 done? (all 4)
[3] Gender/Ethnic ─ completed ┤
[4] Localization ── completed ─┘
[5] Schools ─────── pending ──┐
[6] Articles ───── pending ┤ Step 3 done? (all 3)
[7] About Config ── pending ─┘
│
┌──────────┴──────────┐
▼ ▼
All done? Some pending?
return 5 return step #Implementation (computeInitialStep)
// resources/js/modules/landing/setup/form/shared/services/step-control-actions.ts
export function computeInitialStep(checks: CheckItem[]): number {
if (checks.length === 0) return 1;
if (checks[0]?.status !== 'completed') return 1; // Super User
const seedChecks = checks.slice(1, 5); // Geographic, Core, Gender, Localization
if (seedChecks.some((c) => c.status !== 'completed')) return 2; // Seed
const aboutChecks = checks.slice(5, 8); // Schools, Articles, About
if (aboutChecks.some((c) => c.status !== 'completed')) return 3; // About
return 5; // Done — step 4 (Settings) is skipped in auto-nav
}Critical detail: The $state is initialized with the computed value immediately — not in a $effect:
// Index.svelte — DO NOT use $effect for initial step
let currentStep = $state(computeInitialStep(initialChecks));Using $state(1) and then updating in $effect causes a render flash and SSR hydration mismatch.
Middleware Algorithm
SetupMiddleware
Registered in both bootstrap/app.php (web group) and Filament PanelProviders. Located at app/Http/Middleware/Setup/SetupMiddleware.php:
public function handle(Request $request, Closure $next): Response
{
// Always allow /setup* and /api/setup* routes through
if ($request->is('setup*') || $request->is('api/setup*')) {
return $next($request);
}
if (! IdentifierAppService::isSetup()) {
return redirect('/setup');
}
return $next($request);
}MaintenanceMiddleware
Runs AFTER SetupMiddleware. Located at app/Http/Middleware/Setup/MaintenanceMiddleware.php:
public function handle(Request $request, Closure $next): Response
{
// Bypass for setup and portal routes
if ($request->is('setup*') || $request->is('api/setup*') || $request->is('portal/*')) {
return $next($request);
}
if (IdentifierAppService::isMaintenance()) {
return Inertia::render('landing/UnderConstruction', ['maintenanceMode' => true]);
}
if (IdentifierAppService::isSeeder()) {
return redirect('/portal');
}
return $next($request);
}Order matters: SetupMiddleware must run BEFORE MaintenanceMiddleware. If setup is incomplete, the user goes to /setup before maintenance checks run.
SSE Seed Algorithm
Step 2 (Seed) uses Server-Sent Events for real-time progress:
Architecture
StepSeed (Index.svelte)
│
▼
seedStore.startSeed() ──► stores/seed.stores.ts
│
▼
seedSseStore.stream() ──► stores/seed-sse.stores.ts (SSE connection)
│
▼
seed.service.ts ──► services/seed.service.ts (connect + parse)
│
▼
seed.api.ts ──► api/seed.api.ts (HTTP POST)
│
▼
POST /api/setup/seed ──► SSE stream responseSSE Event Types
| Event | Payload | Description |
|---|---|---|
start | { total: 7 } | Seeding begins |
progress | { label, current, total, percent } | Each seeder starts |
seederComplete | { label, current, total } | Each seeder finishes |
checks | { checks: CheckItem[] } | Updated system status |
complete | { message, checks } | All seeders done |
error | { message } | Seeder failed |
State Flow
IDLE → CONNECTING → STREAMING (progress updates) → COMPLETE
│ │
└── error ──► IDLE (retry) │
│
Done → auto-navigate
to next stepStore Pattern (StateContainer<TData>)
Every store follows this from @/types/state:
interface StateContainer<TData> {
meta: {
loading: boolean;
initialized: boolean;
updatedAt: string | null;
errorAt: string | null;
};
data: TData;
errors: Record<string, string>;
}Available Helpers
const initialMeta: StateMeta = { loading: false, initialized: false, updatedAt: null, errorAt: null };
const loadingMeta: StateMeta = { loading: true, initialized: true, updatedAt: null, errorAt: null };
function hasErrors(errors: StateErrors): boolean;
function metaFrom(data?: Partial<StateMeta>): StateMeta;
function createState<TData>(initial: TData): StateContainer<TData>;Store Architecture Layers
Component (presentational only)
│
▼ subscribe + action calls
Store (state management)
│
▼ calls
Service (business logic)
│
▼ calls
API (HTTP layer)Rules:
- Components: NO raw
fetch(), NO SSE parsing, NO business logic - Stores: manage state only, delegate to services for logic
- Services: orchestrate API calls, transform data, handle errors
- API: pure HTTP calls, no business logic
Auto-Navigation Store
setup-auto-navigation.stores.ts handles the algorithm for determining the correct step:
class SetupAutoNavigationStore {
// Computes target step from backend checks
computeStep(checks: CheckItem[]): number {
if (checks.length === 0) return 1;
if (checks[0]?.status !== 'completed') return 1;
const seedChecks = checks.slice(1, 5);
if (seedChecks.some((c) => c.status !== 'completed')) return 2;
const aboutChecks = checks.slice(5, 8);
if (aboutChecks.some((c) => c.status !== 'completed')) return 3;
return 5;
}
// Resolve and emit to subscribers
resolve(checks: CheckItem[]): void { ... }
}Wizard Steps Overview
| Step | Tab | Backend Check | API Endpoint |
|---|---|---|---|
| 1 | Super User | Super user exists | POST /api/setup/register |
| 2 | Seed | Geographic, Core, Gender, Localization | POST /api/setup/seed (SSE) |
| 3 | About | School, Articles, About Config | POST /api/setup/about |
| 4 | Settings | Contact, Social Media | POST /api/setup/setting |
| 5 | Done | All checks pass | — |