Skip to content

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 page

Key Algorithm: IdentifierAppService::isSetup()

This is the master check that determines if setup is complete. Located in app/Services/Common/IdentifierAppService.php:

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)

typescript
// 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:

svelte
// 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:

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:

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 response

SSE Event Types

EventPayloadDescription
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 step

Store Pattern (StateContainer<TData>)

Every store follows this from @/types/state:

typescript
interface StateContainer<TData> {
    meta: {
        loading: boolean;
        initialized: boolean;
        updatedAt: string | null;
        errorAt: string | null;
    };
    data: TData;
    errors: Record<string, string>;
}

Available Helpers

typescript
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:

typescript
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

StepTabBackend CheckAPI Endpoint
1Super UserSuper user existsPOST /api/setup/register
2SeedGeographic, Core, Gender, LocalizationPOST /api/setup/seed (SSE)
3AboutSchool, Articles, About ConfigPOST /api/setup/about
4SettingsContact, Social MediaPOST /api/setup/setting
5DoneAll checks pass