Setup Wizard — Algoritma & Arsitektur
Setup Wizard memandu administrator pertama melalui konfigurasi awal. Akses di /setup.
Alur Request (Middleware)
Request ──► SetupMiddleware ──► MaintenanceMiddleware ──► Controller
│ │
┌────────────┘ │
▼ ▼
isSetup() == false? isMaintenance() == true?
├─ Ya → redirect /setup ├─ Ya → Halaman UnderConstruction
└─ Tidak → continue └─ Tidak → continue
│
▼
isSeeder() == true?
├─ Ya → redirect /portal (admin)
└─ Tidak → render landingAlgoritma Utama: IdentifierAppService::isSetup()
Ini pengecekan master yang menentukan apakah setup selesai. File: app/Services/Common/IdentifierAppService.php:
php
public static function isSetup(): bool
{
if (! $setup['superUser']) return false; // 1. Super user
if (! $seeder['geographic']) return false; // 2. Seeder
if (! $seeder['gender'] || ! $seeder['ethnicity'] || ! $seeder['religion']) return false;
if (! $seeder['localization']) return false;
if (! coreSettingsExist()) return false; // 3. Core settings
if (! yayasanNameExists()) return false; // 4. About
if (! $initial['school']) return false; // 5. School
if (! $initial['news']) return false; // 6. Article
if (! contactExists()) return false; // 7. Contact
if (! socialExists()) return false; // 8. Social media
return true;
}Algoritma Auto-Navigasi
Saat user kembali ke /setup, wizard otomatis menghitung langkah pertama yang belum selesai.
Cara Kerja
Backend kirim 8 pengecekan: Frontend hitung step:
[0] Super User ──── selesai ──► Step 1 selesai?
[1] Geographic ──── selesai ─┐
[2] Core Settings ─ selesai ─┤ Step 2 selesai? (semua 4)
[3] Gender/Ethnic ─ selesai ─┤
[4] Localization ── selesai ─┘
[5] Schools ─────── pending ─┐
[6] Articles ───── pending ──┤ Step 3 selesai? (semua 3)
[7] About Config ── pending ─┘
│
┌──────────┴──────────┐
▼ ▼
Semua selesai? Ada yg pending?
return 5 return step #Implementasi (computeInitialStep)
typescript
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);
if (seedChecks.some((c) => c.status !== 'completed')) return 2; // Seed
const aboutChecks = checks.slice(5, 8);
if (aboutChecks.some((c) => c.status !== 'completed')) return 3; // About
return 5; // Done
}Penting!
$state harus diinisialisasi LANGSUNG dengan nilai yang sudah dihitung:
svelte
// ✅ BENAR
let currentStep = $state(computeInitialStep(initialChecks));
// ❌ SALAH — menyebabkan flash render
let currentStep = $state(1);
$effect(() => { stepControl.configureFromChecks(initialChecks); });Middleware
SetupMiddleware
File: app/Http/Middleware/Setup/SetupMiddleware.php
php
// /setup* dan /api/setup* selalu diizinkan
if ($request->is('setup*') || $request->is('api/setup*')) {
return $next($request);
}
if (! IdentifierAppService::isSetup()) {
return redirect('/setup');
}MaintenanceMiddleware
File: app/Http/Middleware/Setup/MaintenanceMiddleware.php
php
// /setup*, /api/setup*, /portal/* bypass
if ($request->is('setup*') || $request->is('api/setup*') || $request->is('portal/*')) {
return $next($request);
}
if (IdentifierAppService::isMaintenance()) {
return Inertia::render('landing/UnderConstruction');
}
if (IdentifierAppService::isSeeder()) {
return redirect('/portal');
}Algoritma SSE Seed (Step 2)
Menggunakan Server-Sent Events untuk progress real-time.
Arsitektur
StepSeed (Index.svelte)
▼
seedStore.startSeed() ──► stores/seed.stores.ts
▼
seedSseStore.stream() ──► stores/seed-sse.stores.ts
▼
seed.service.ts ──► services/seed.service.ts
▼
seed.api.ts ──► api/seed.api.ts
▼
POST /api/setup/seed ──► SSE streamEvent SSE
| Event | Data | Keterangan |
|---|---|---|
start | { total: 7 } | Seeding dimulai |
progress | { label, current, total, percent } | Per-seeder |
seederComplete | { label, current, total } | Seeder selesai |
checks | { checks: [...] } | Status sistem terbaru |
complete | { message, checks } | Semua selesai |
error | { message } | Gagal |
State Machine
IDLE → CONNECTING → STREAMING → COMPLETE
│ │
└── error └── auto-next step
│
IDLE (retry)Store Pattern (StateContainer<TData>)
Semua store menggunakan format dari @/types/state:
typescript
interface StateContainer<TData> {
meta: { loading, initialized, updatedAt, errorAt };
data: TData;
errors: Record<string, string>;
}Arsitektur Layer
Component (hanya presentasi)
▼ subscribe + panggil action
Store (manajemen state)
▼ delegasi ke
Service (logika bisnis)
▼ panggil
API (HTTP fetch)Aturan Ketat
- Component: DILARANG
fetch()langsung, DILARANG parse SSE, DILARANG logika bisnis - Store: kelola state saja, delegasi logika ke service
- Service: orchestrasi API calls, transform data
- API: pure HTTP, tanpa logika bisnis
Struktur Modul
module/
├── Index.svelte # Entry point — minimal, komposisi saja
├── api/ # Fungsi fetch
├── parts/ # Sub-komponen (atomic design)
├── services/ # Logika bisnis
├── stores/ # State management
│ ├── aksi-modul.stores.ts
│ └── subdir/
│ ├── store-a.stores.ts
│ └── index.ts
├── types/ # TypeScript interfaces
└── __tests__/ # Vitest testsNaming Conventions
| Layer | Format | Contoh |
|---|---|---|
| Store file | <aksi>-<modul>.stores.ts | fetch-bulletin.stores.ts |
| Service file | <modul>.service.ts | bulletin.service.ts |
| API file | <modul>.api.ts | bulletin.api.ts |
| Type file | <modul>.types.ts | bulletin.types.ts |
| Komponen | PascalCase | BulletinCard.svelte |
| Subfolder store | <aksi>-<modul>/ | super-user-entry/ |
Frontend: camelCase. Komunikasi ke backend: snake_case.
Migration Runner
File: database/migrations/0000_runner_migration.php
- Baca env
MIGRATION_RUNNER_FOLDERS(default:v1_0_0,...) - Jalankan migrasi per subfolder versi
- Support custom sort via
sortMigrations()overrides - Output nama folder:
echo "--- Migrating: v1_0_0 ---"
Multi-Bahasa (i18n)
- File:
lang/en.jsondanlang/id.json - Import:
import { t } from '@/lib/i18n.svelte' - Pattern key:
<modul>.<bagian>.<key>(e.g.,setup.seed.title) - DILARANG hardcode string display di component