@mostajs/setup 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,6 +61,7 @@ npm install @mostajs/setup @mostajs/orm
61
61
  │ ├── reconfig.route.ts # Factory GET+POST /api/setup/reconfig
62
62
  │ └── upload-jar.route.ts # Factory GET+POST+DELETE /api/setup/upload-jar
63
63
  ├── components/
64
+ │ ├── SetupWizard.tsx # Wizard d'installation complet (6 etapes)
64
65
  │ └── ReconfigPanel.tsx # UI reconfiguration (modules + DB)
65
66
  ├── types/
66
67
  │ └── index.ts # Tous les types TypeScript
@@ -348,242 +349,68 @@ Ce handler :
348
349
 
349
350
  ### Etape 4 — Creer la page Setup (frontend)
350
351
 
351
- Voici un exemple **minimal** de page setup. Adaptez le style a votre design system.
352
+ Le module fournit un composant **`SetupWizard`** pret a l'emploi avec inline styles (aucune dependance Tailwind/shadcn).
353
+ Votre page Next.js n'est qu'un **wrapper** d'une vingtaine de lignes :
352
354
 
353
355
  ```tsx
354
356
  // src/app/setup/page.tsx
355
357
  'use client'
356
358
 
357
- import { useState, useEffect } from 'react'
358
359
  import { useRouter } from 'next/navigation'
359
- import { resolveModuleDependencies } from '@mostajs/setup/data/module-definitions'
360
- import type { ModuleDefinition } from '@mostajs/setup'
361
-
362
- type Dialect = 'mongodb' | 'sqlite' | 'postgres' | 'mysql' | 'mariadb'
363
- | 'oracle' | 'mssql' | 'cockroachdb' | 'db2' | 'hana'
364
- | 'hsqldb' | 'spanner' | 'sybase'
360
+ import SetupWizard from '@mostajs/setup/components/SetupWizard'
361
+ import { t } from '@/i18n' // ou toute fonction de traduction
365
362
 
366
363
  export default function SetupPage() {
367
364
  const router = useRouter()
368
365
 
369
- // --- Etat du wizard ---
370
- const [step, setStep] = useState(1) // 1=modules, 2=db, 3=admin, 4=install
371
-
372
- // Modules
373
- const [availableModules, setAvailableModules] = useState<ModuleDefinition[]>([])
374
- const [selectedModules, setSelectedModules] = useState<string[]>([])
375
- const [installedModules, setInstalledModules] = useState<string[]>([])
376
-
377
- // DB
378
- const [dialect, setDialect] = useState<Dialect>('mongodb')
379
- const [dbConfig, setDbConfig] = useState({ host: 'localhost', port: 27017, name: 'mydb', user: '', password: '' })
380
- const [dbOk, setDbOk] = useState(false)
381
-
382
- // Admin
383
- const [admin, setAdmin] = useState({ email: '', password: '', firstName: '', lastName: '' })
384
-
385
- // Install
386
- const [installing, setInstalling] = useState(false)
387
- const [result, setResult] = useState<{ ok: boolean; error?: string } | null>(null)
388
-
389
- // --- Charger les modules depuis l'API au montage ---
390
- useEffect(() => {
391
- fetch('/api/setup/detect-modules')
392
- .then(r => r.json())
393
- .then((data: { modules: ModuleDefinition[]; installed: string[] }) => {
394
- setAvailableModules(data.modules || [])
395
- setInstalledModules(data.installed || [])
396
- // Pre-cocher les modules required + default + deja installes
397
- const preChecked = new Set([
398
- ...(data.modules || []).filter(m => m.required || m.default).map(m => m.key),
399
- ...(data.installed || []),
400
- ])
401
- setSelectedModules(Array.from(preChecked))
402
- })
403
- .catch(() => {})
404
- }, [])
405
-
406
- // --- Toggle un module (avec resolution des dependances) ---
407
- function toggleModule(key: string) {
408
- const mod = availableModules.find(m => m.key === key)
409
- if (!mod || mod.required) return
410
-
411
- setSelectedModules(prev => {
412
- if (prev.includes(key)) {
413
- // Decochage : retirer aussi les modules qui dependent de celui-ci
414
- const toRemove = new Set([key])
415
- let changed = true
416
- while (changed) {
417
- changed = false
418
- for (const m of availableModules) {
419
- if (toRemove.has(m.key) || m.required) continue
420
- if (m.dependsOn?.some(dep => toRemove.has(dep)) && prev.includes(m.key)) {
421
- toRemove.add(m.key)
422
- changed = true
423
- }
424
- }
425
- }
426
- return prev.filter(k => !toRemove.has(k))
427
- } else {
428
- // Cochage : ajouter les dependances transitives
429
- return resolveModuleDependencies([...prev, key], availableModules)
430
- }
431
- })
432
- }
366
+ return (
367
+ <SetupWizard
368
+ t={t}
369
+ onComplete={() => router.push('/login')}
370
+ dbNamePrefix="secuaccessdb"
371
+ endpoints={{
372
+ detectModules: '/api/setup/detect-modules',
373
+ testDb: '/api/setup/test-db',
374
+ installModules: '/api/setup/install-modules',
375
+ install: '/api/setup/install',
376
+ uploadJar: '/api/setup/upload-jar',
377
+ }}
378
+ />
379
+ )
380
+ }
381
+ ```
433
382
 
434
- // --- Tester la connexion DB ---
435
- async function testDb() {
436
- const res = await fetch('/api/setup/test-db', {
437
- method: 'POST',
438
- headers: { 'Content-Type': 'application/json' },
439
- body: JSON.stringify({ dialect, ...dbConfig }),
440
- })
441
- const data = await res.json()
442
- setDbOk(data.ok === true)
443
- }
383
+ #### Props de SetupWizard
444
384
 
445
- // --- Lancer l'installation ---
446
- async function install() {
447
- setInstalling(true)
448
- setResult(null)
449
-
450
- // 1. Installer les modules npm
451
- const modRes = await fetch('/api/setup/install-modules', {
452
- method: 'POST',
453
- headers: { 'Content-Type': 'application/json' },
454
- body: JSON.stringify({ modules: selectedModules }),
455
- })
456
- if (!modRes.ok) {
457
- setResult({ ok: false, error: 'Erreur installation modules' })
458
- setInstalling(false)
459
- return
460
- }
385
+ | Prop | Type | Default | Description |
386
+ |------|------|---------|-------------|
387
+ | `t` | `(key: string) => string` | `(k) => k` | Fonction de traduction (recoit des cles `setup.xxx`) |
388
+ | `onComplete` | `() => void` | — | Callback apres installation reussie (ex: `router.push('/login')`) |
389
+ | `dbNamePrefix` | `string` | `'mydb'` | Prefixe pour le nom de base par defaut |
390
+ | `persistState` | `boolean` | `true` | Persister l'etat du wizard dans `sessionStorage` |
391
+ | `endpoints` | `object` | voir ci-dessous | URLs des routes API |
461
392
 
462
- // 2. Configurer la DB + seeder
463
- const res = await fetch('/api/setup/install', {
464
- method: 'POST',
465
- headers: { 'Content-Type': 'application/json' },
466
- body: JSON.stringify({ dialect, db: dbConfig, admin, modules: selectedModules }),
467
- })
468
- const data = await res.json()
469
- setResult(data.data || { ok: false, error: data.error?.message })
470
- setInstalling(false)
471
- }
472
-
473
- return (
474
- <div style={{ maxWidth: 600, margin: '50px auto', padding: 20 }}>
475
- <h1>Setup — Mon Application</h1>
476
-
477
- {/* ── Etape 1 : Modules ── */}
478
- {step === 1 && (
479
- <div>
480
- <h2>1. Modules</h2>
481
- {availableModules.map(mod => (
482
- <label key={mod.key} style={{ display: 'block', margin: '8px 0' }}>
483
- <input
484
- type="checkbox"
485
- checked={selectedModules.includes(mod.key)}
486
- disabled={mod.required}
487
- onChange={() => toggleModule(mod.key)}
488
- />
489
- {' '}{mod.icon} {mod.label}
490
- {mod.required && <em> (requis)</em>}
491
- {mod.discovered && <strong> NOUVEAU</strong>}
492
- {installedModules.includes(mod.key) && <span> ✓</span>}
493
- </label>
494
- ))}
495
- <button onClick={() => setStep(2)}>Suivant →</button>
496
- </div>
497
- )}
498
-
499
- {/* ── Etape 2 : Base de donnees ── */}
500
- {step === 2 && (
501
- <div>
502
- <h2>2. Base de donnees</h2>
503
- <select value={dialect} onChange={e => setDialect(e.target.value as Dialect)}>
504
- <option value="mongodb">MongoDB</option>
505
- <option value="sqlite">SQLite</option>
506
- <option value="postgres">PostgreSQL</option>
507
- <option value="mysql">MySQL</option>
508
- {/* ... autres dialectes ... */}
509
- </select>
510
-
511
- {dialect !== 'sqlite' && (
512
- <>
513
- <input placeholder="Host" value={dbConfig.host}
514
- onChange={e => setDbConfig({ ...dbConfig, host: e.target.value })} />
515
- <input placeholder="Port" type="number" value={dbConfig.port}
516
- onChange={e => setDbConfig({ ...dbConfig, port: +e.target.value })} />
517
- <input placeholder="User" value={dbConfig.user}
518
- onChange={e => setDbConfig({ ...dbConfig, user: e.target.value })} />
519
- <input placeholder="Password" type="password" value={dbConfig.password}
520
- onChange={e => setDbConfig({ ...dbConfig, password: e.target.value })} />
521
- </>
522
- )}
523
- <input placeholder="Nom de la base" value={dbConfig.name}
524
- onChange={e => setDbConfig({ ...dbConfig, name: e.target.value })} />
525
-
526
- <button onClick={testDb}>Tester la connexion</button>
527
- {dbOk && <span style={{ color: 'green' }}> ✓ Connexion OK</span>}
528
-
529
- <br />
530
- <button onClick={() => setStep(1)}>← Retour</button>
531
- <button onClick={() => setStep(3)} disabled={dialect !== 'sqlite' && !dbOk}>
532
- Suivant →
533
- </button>
534
- </div>
535
- )}
536
-
537
- {/* ── Etape 3 : Administrateur ── */}
538
- {step === 3 && (
539
- <div>
540
- <h2>3. Administrateur</h2>
541
- <input placeholder="Prenom" value={admin.firstName}
542
- onChange={e => setAdmin({ ...admin, firstName: e.target.value })} />
543
- <input placeholder="Nom" value={admin.lastName}
544
- onChange={e => setAdmin({ ...admin, lastName: e.target.value })} />
545
- <input placeholder="Email" type="email" value={admin.email}
546
- onChange={e => setAdmin({ ...admin, email: e.target.value })} />
547
- <input placeholder="Mot de passe (min 6)" type="password" value={admin.password}
548
- onChange={e => setAdmin({ ...admin, password: e.target.value })} />
549
-
550
- <br />
551
- <button onClick={() => setStep(2)}>← Retour</button>
552
- <button onClick={() => setStep(4)}>Suivant →</button>
553
- </div>
554
- )}
555
-
556
- {/* ── Etape 4 : Installation ── */}
557
- {step === 4 && (
558
- <div>
559
- <h2>4. Recapitulatif</h2>
560
- <p><strong>DB :</strong> {dialect} — {dbConfig.name}</p>
561
- <p><strong>Admin :</strong> {admin.email}</p>
562
- <p><strong>Modules :</strong> {selectedModules.join(', ')}</p>
563
-
564
- <button onClick={install} disabled={installing}>
565
- {installing ? 'Installation en cours...' : 'Installer'}
566
- </button>
567
-
568
- {result?.ok && (
569
- <div style={{ color: 'green', marginTop: 16 }}>
570
- ✓ Installation terminee !
571
- <br />
572
- <button onClick={() => router.push('/login')}>Aller au login →</button>
573
- </div>
574
- )}
575
- {result && !result.ok && (
576
- <div style={{ color: 'red', marginTop: 16 }}>
577
- ✗ Erreur : {result.error}
578
- </div>
579
- )}
580
- </div>
581
- )}
582
- </div>
583
- )
393
+ **Endpoints par defaut :**
394
+ ```json
395
+ {
396
+ "detectModules": "/api/setup/detect-modules",
397
+ "testDb": "/api/setup/test-db",
398
+ "installModules": "/api/setup/install-modules",
399
+ "install": "/api/setup/install",
400
+ "uploadJar": "/api/setup/upload-jar"
584
401
  }
585
402
  ```
586
403
 
404
+ **Cles i18n attendues :** `setup.steps.*`, `setup.welcome.*`, `setup.modules.*`, `setup.dialect.*`, `setup.database.*`, `setup.admin.*`, `setup.summary.*`, `setup.back`, `setup.next`.
405
+
406
+ #### Le wizard inclut :
407
+ - **6 etapes** : Accueil → Modules → Dialecte → Base de donnees → Admin → Recapitulatif
408
+ - **13 dialectes** avec distinction Premium (grises, non cliquables) et badges JDBC
409
+ - **Upload JAR** integre pour les dialectes JDBC (hsqldb, oracle, db2, hana, sybase)
410
+ - **Persistence sessionStorage** pour survivre aux hot-reloads Next.js
411
+ - **Resolution des dependances** entre modules
412
+ - **Retry automatique** apres npm install (le serveur peut redemarrer)
413
+
587
414
  ### Etape 5 — Middleware : rediriger vers /setup
588
415
 
589
416
  ```typescript
@@ -9,4 +9,5 @@ export declare function createUploadJarHandlers(): {
9
9
  GET: () => Promise<Response>;
10
10
  POST: (req: Request) => Promise<Response>;
11
11
  DELETE: (req: Request) => Promise<Response>;
12
+ PATCH: (req: Request) => Promise<Response>;
12
13
  };
@@ -69,5 +69,30 @@ export function createUploadJarHandlers() {
69
69
  return Response.json({ ok: false, error: message }, { status: 500 });
70
70
  }
71
71
  }
72
- return { GET, POST, DELETE };
72
+ /**
73
+ * PATCH — Start/test the JDBC bridge for a given dialect.
74
+ * Body: { dialect: string, uri: string }
75
+ * Returns: { ok: true, port: number } or { ok: false, error: string }
76
+ */
77
+ async function PATCH(req) {
78
+ try {
79
+ const { dialect, uri } = await req.json();
80
+ if (!dialect || !uri) {
81
+ return Response.json({ ok: false, error: 'dialect et uri requis' }, { status: 400 });
82
+ }
83
+ const { testConnection } = await import('@mostajs/orm');
84
+ const result = await testConnection({ dialect, uri, schemaStrategy: 'none' });
85
+ if (result.ok) {
86
+ return Response.json({ ok: true });
87
+ }
88
+ else {
89
+ return Response.json({ ok: false, error: result.error || 'Echec lancement du bridge' });
90
+ }
91
+ }
92
+ catch (err) {
93
+ const message = err instanceof Error ? err.message : 'Erreur serveur';
94
+ return Response.json({ ok: false, error: message }, { status: 500 });
95
+ }
96
+ }
97
+ return { GET, POST, DELETE, PATCH };
73
98
  }
@@ -0,0 +1,19 @@
1
+ export interface SetupWizardProps {
2
+ /** Translation function — receives i18n key, returns translated string */
3
+ t?: (key: string) => string;
4
+ /** Called when setup is complete and user clicks "Go to Login" */
5
+ onComplete?: () => void;
6
+ /** API endpoints */
7
+ endpoints?: {
8
+ detectModules?: string;
9
+ testDb?: string;
10
+ installModules?: string;
11
+ install?: string;
12
+ uploadJar?: string;
13
+ };
14
+ /** Default database name prefix (e.g. 'secuaccessdb') */
15
+ dbNamePrefix?: string;
16
+ /** Whether to persist wizard state in sessionStorage (default: true) */
17
+ persistState?: boolean;
18
+ }
19
+ export default function SetupWizard({ t: tProp, onComplete, endpoints, dbNamePrefix, persistState, }: SetupWizardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,543 @@
1
+ // @mostajs/setup — Setup Wizard (reusable, inline-styles, no Tailwind/shadcn dependency)
2
+ // Author: Dr Hamid MADANI drmdh@msn.com
3
+ 'use client';
4
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
5
+ import { useState, useEffect, useCallback } from 'react';
6
+ // ── Constants ────────────────────────────────────────────────
7
+ const STEPS = ['welcome', 'modules', 'dialect', 'database', 'admin', 'summary'];
8
+ const DIALECT_DEFAULTS = {
9
+ mongodb: { host: 'localhost', port: 27017, name: 'mydb_prod', user: '', password: '' },
10
+ sqlite: { host: '', port: 0, name: 'mydb', user: '', password: '' },
11
+ postgres: { host: 'localhost', port: 5432, name: 'mydb', user: 'postgres', password: '' },
12
+ mysql: { host: 'localhost', port: 3306, name: 'mydb', user: 'root', password: '' },
13
+ mariadb: { host: 'localhost', port: 3306, name: 'mydb', user: 'root', password: '' },
14
+ oracle: { host: 'localhost', port: 1521, name: 'mydb', user: 'system', password: '' },
15
+ mssql: { host: 'localhost', port: 1433, name: 'mydb', user: 'sa', password: '' },
16
+ cockroachdb: { host: 'localhost', port: 26257, name: 'mydb', user: 'root', password: '' },
17
+ db2: { host: 'localhost', port: 50000, name: 'mydb', user: 'db2inst1', password: '' },
18
+ hana: { host: 'localhost', port: 30015, name: 'mydb', user: 'SYSTEM', password: '' },
19
+ hsqldb: { host: 'localhost', port: 9001, name: 'mydb', user: 'SA', password: '' },
20
+ spanner: { host: '', port: 0, name: 'my-project/my-instance/mydb', user: '', password: '' },
21
+ sybase: { host: 'localhost', port: 5000, name: 'mydb', user: 'sa', password: '' },
22
+ };
23
+ const DIALECT_INFO = [
24
+ { key: 'mongodb', name: 'MongoDB', icon: '🍃' },
25
+ { key: 'sqlite', name: 'SQLite', icon: '📁' },
26
+ { key: 'postgres', name: 'PostgreSQL', icon: '🐘' },
27
+ { key: 'mysql', name: 'MySQL', icon: '🐬' },
28
+ { key: 'mariadb', name: 'MariaDB', icon: '🦭' },
29
+ { key: 'mssql', name: 'SQL Server', icon: '🟦' },
30
+ { key: 'oracle', name: 'Oracle', icon: '🔴', premium: true, jdbc: true },
31
+ { key: 'cockroachdb', name: 'CockroachDB', icon: '🪳' },
32
+ { key: 'db2', name: 'IBM DB2', icon: '🏢', premium: true, jdbc: true },
33
+ { key: 'hana', name: 'SAP HANA', icon: '💎', premium: true, jdbc: true },
34
+ { key: 'hsqldb', name: 'HyperSQL', icon: '⚡', jdbc: true },
35
+ { key: 'spanner', name: 'Cloud Spanner', icon: '☁️', premium: true },
36
+ { key: 'sybase', name: 'Sybase ASE', icon: '🔷', premium: true, jdbc: true },
37
+ ];
38
+ const JDBC_DIALECTS = ['hsqldb', 'oracle', 'db2', 'hana', 'sybase'];
39
+ const DRIVER_HINTS = {
40
+ mongodb: 'npm install mongoose',
41
+ sqlite: 'npm install better-sqlite3',
42
+ postgres: 'npm install pg',
43
+ mysql: 'npm install mysql2',
44
+ mariadb: 'npm install mariadb',
45
+ oracle: 'npm install oracledb',
46
+ mssql: 'npm install mssql',
47
+ cockroachdb: 'npm install pg',
48
+ db2: 'npm install ibm_db',
49
+ hana: 'npm install @sap/hana-client',
50
+ hsqldb: 'Uploader hsqldb*.jar dans jar_files/',
51
+ spanner: 'npm install @google-cloud/spanner',
52
+ sybase: 'npm install sybase',
53
+ };
54
+ const STORAGE_KEY = 'setup-wizard-state';
55
+ // ── Inline Styles ────────────────────────────────────────────
56
+ const S = {
57
+ wrapper: { display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', padding: 16, fontFamily: 'system-ui, sans-serif' },
58
+ container: { width: '100%', maxWidth: 672 },
59
+ // Stepper
60
+ stepperRow: { display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 32, gap: 8, flexWrap: 'wrap' },
61
+ stepCircle: (state) => ({
62
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
63
+ width: 32, height: 32, borderRadius: '50%', fontSize: 13, fontWeight: 500,
64
+ backgroundColor: state === 'future' ? '#e5e7eb' : '#0284c7',
65
+ color: state === 'future' ? '#6b7280' : '#fff',
66
+ ...(state === 'current' ? { boxShadow: '0 0 0 4px #bae6fd' } : {}),
67
+ }),
68
+ stepLabel: (active) => ({
69
+ fontSize: 13,
70
+ fontWeight: active ? 600 : 400,
71
+ color: active ? '#0369a1' : '#6b7280',
72
+ }),
73
+ stepLine: (done) => ({
74
+ width: 32, height: 2,
75
+ backgroundColor: done ? '#0284c7' : '#e5e7eb',
76
+ }),
77
+ // Card
78
+ card: { backgroundColor: '#fff', border: '1px solid #e5e7eb', borderRadius: 16, boxShadow: '0 10px 25px -5px rgba(0,0,0,0.1)', padding: 32 },
79
+ // Section header
80
+ sectionHeader: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 },
81
+ sectionIcon: { fontSize: 24, color: '#0284c7', lineHeight: 1 },
82
+ sectionTitle: { fontSize: 20, fontWeight: 700, color: '#111827' },
83
+ sectionDesc: { fontSize: 13, color: '#6b7280', marginTop: 4 },
84
+ // Buttons
85
+ btn: (variant = 'primary', disabled = false) => ({
86
+ display: 'inline-flex', alignItems: 'center', gap: 8,
87
+ padding: variant === 'lg' ? '12px 24px' : '8px 16px',
88
+ border: variant === 'outline' ? '1px solid #d1d5db' : 'none',
89
+ borderRadius: 8,
90
+ fontSize: variant === 'lg' ? 15 : 13,
91
+ fontWeight: 600,
92
+ cursor: disabled ? 'not-allowed' : 'pointer',
93
+ opacity: disabled ? 0.5 : 1,
94
+ backgroundColor: variant === 'outline' ? '#fff' : '#0284c7',
95
+ color: variant === 'outline' ? '#374151' : '#fff',
96
+ transition: 'all 0.15s',
97
+ }),
98
+ // Form
99
+ formGroup: { marginBottom: 12 },
100
+ label: { display: 'block', marginBottom: 4, fontWeight: 500, fontSize: 13, color: '#374151' },
101
+ input: { width: '100%', padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', outline: 'none' },
102
+ formRow: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 },
103
+ // Grid
104
+ grid2: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 12 },
105
+ grid3: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 12 },
106
+ // Module card
107
+ moduleCard: (selected, disabled) => ({
108
+ padding: 16, borderRadius: 8, textAlign: 'left',
109
+ border: `2px solid ${selected ? '#0284c7' : '#e5e7eb'}`,
110
+ backgroundColor: selected ? '#f0f9ff' : '#fff',
111
+ cursor: disabled ? 'not-allowed' : 'pointer',
112
+ opacity: disabled ? 0.5 : 1,
113
+ transition: 'all 0.2s',
114
+ }),
115
+ moduleHeader: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 },
116
+ moduleLeft: { display: 'flex', alignItems: 'center', gap: 8 },
117
+ moduleName: { fontWeight: 600, fontSize: 14, color: '#111827' },
118
+ moduleDesc: { fontSize: 12, color: '#6b7280' },
119
+ badge: (type) => ({
120
+ display: 'inline-block', padding: '1px 6px', borderRadius: 4, fontSize: 10, fontWeight: 600, marginLeft: 4,
121
+ backgroundColor: type === 'required' ? '#dbeafe' : type === 'installed' ? '#d1fae5' :
122
+ type === 'new' ? '#f3e8ff' : '#fef3c7',
123
+ color: type === 'required' ? '#1e40af' : type === 'installed' ? '#065f46' :
124
+ type === 'new' ? '#6b21a8' : '#92400e',
125
+ }),
126
+ // Dialect card
127
+ dialectCard: (selected, premium) => ({
128
+ padding: 16, borderRadius: 8, textAlign: 'center',
129
+ border: `2px solid ${!premium && selected ? '#0284c7' : '#e5e7eb'}`,
130
+ backgroundColor: !premium && selected ? '#f0f9ff' : '#fff',
131
+ cursor: premium ? 'not-allowed' : 'pointer',
132
+ opacity: premium ? 0.4 : 1,
133
+ filter: premium ? 'grayscale(0.5)' : 'none',
134
+ transition: 'all 0.2s',
135
+ }),
136
+ dialectIcon: { fontSize: 24, marginBottom: 4 },
137
+ dialectName: { fontSize: 12, fontWeight: 600, color: '#111827' },
138
+ // Alert
139
+ alert: (type) => ({
140
+ padding: 12, borderRadius: 8, marginBottom: 16, fontSize: 13,
141
+ backgroundColor: type === 'success' ? '#d1fae5' : type === 'error' ? '#fee2e2' :
142
+ type === 'warning' ? '#fef3c7' : '#dbeafe',
143
+ color: type === 'success' ? '#065f46' : type === 'error' ? '#991b1b' :
144
+ type === 'warning' ? '#92400e' : '#1e40af',
145
+ border: `1px solid ${type === 'success' ? '#a7f3d0' : type === 'error' ? '#fecaca' :
146
+ type === 'warning' ? '#fde68a' : '#bfdbfe'}`,
147
+ }),
148
+ // Summary card
149
+ summaryCard: { border: '1px solid #e5e7eb', borderRadius: 8, padding: 16, marginBottom: 12 },
150
+ summaryTitle: { fontWeight: 600, fontSize: 14, color: '#374151', marginBottom: 8 },
151
+ summaryText: { fontSize: 13, color: '#6b7280' },
152
+ // Checkbox row
153
+ checkRow: { display: 'flex', alignItems: 'flex-start', gap: 12, padding: '8px 0' },
154
+ checkbox: { width: 16, height: 16, marginTop: 2, cursor: 'pointer', accentColor: '#0284c7' },
155
+ // Nav row
156
+ navRow: { display: 'flex', justifyContent: 'space-between', paddingTop: 16 },
157
+ // JAR section
158
+ jarBox: { padding: 16, borderRadius: 8, backgroundColor: '#f0f9ff', border: '1px solid #bae6fd', marginTop: 12 },
159
+ jarTitle: { fontSize: 13, fontWeight: 600, color: '#0369a1', marginBottom: 8 },
160
+ // Module badges
161
+ moduleBadges: { display: 'flex', alignItems: 'center', gap: 4 },
162
+ // Center
163
+ center: { textAlign: 'center' },
164
+ // Spacer
165
+ spacer: (px) => ({ height: px }),
166
+ // Flex
167
+ flex: (gap = 8) => ({ display: 'flex', alignItems: 'center', gap }),
168
+ flexBetween: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
169
+ flexWrap: { display: 'flex', flexWrap: 'wrap', gap: 8 },
170
+ };
171
+ // ── Helpers ──────────────────────────────────────────────────
172
+ function resolveModuleDeps(selected, all) {
173
+ const set = new Set(selected);
174
+ let changed = true;
175
+ while (changed) {
176
+ changed = false;
177
+ for (const m of all) {
178
+ if (set.has(m.key) && m.dependsOn) {
179
+ for (const dep of m.dependsOn) {
180
+ if (!set.has(dep)) {
181
+ set.add(dep);
182
+ changed = true;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ return Array.from(set);
189
+ }
190
+ function simpleDbUri(dialect, cfg) {
191
+ if (dialect === 'sqlite')
192
+ return `./data/${cfg.name}.db`;
193
+ if (dialect === 'mongodb')
194
+ return `mongodb://${cfg.user ? `${cfg.user}:${cfg.password}@` : ''}${cfg.host}:${cfg.port}/${cfg.name}`;
195
+ const scheme = {
196
+ postgres: 'postgresql', mysql: 'mysql', mariadb: 'mysql', mssql: 'mssql',
197
+ oracle: 'oracle:thin', cockroachdb: 'postgresql', db2: 'db2', hana: 'sap',
198
+ hsqldb: 'hsqldb:hsql', spanner: 'spanner', sybase: 'sybase:Tds',
199
+ };
200
+ const s = scheme[dialect] || dialect;
201
+ return `${s}://${cfg.user ? `${cfg.user}:${cfg.password}@` : ''}${cfg.host}:${cfg.port}/${cfg.name}`;
202
+ }
203
+ async function safeJson(res) {
204
+ try {
205
+ return JSON.parse(await res.text());
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
211
+ async function fetchRetry(url, init, retries = 3, delay = 2000) {
212
+ for (let i = 0; i < retries; i++) {
213
+ try {
214
+ const res = await fetch(url, init);
215
+ if (res.status === 404 && i < retries - 1) {
216
+ await new Promise(r => setTimeout(r, delay));
217
+ continue;
218
+ }
219
+ return res;
220
+ }
221
+ catch (err) {
222
+ if (i < retries - 1) {
223
+ await new Promise(r => setTimeout(r, delay));
224
+ continue;
225
+ }
226
+ throw err;
227
+ }
228
+ }
229
+ return fetch(url, init);
230
+ }
231
+ // ── JAR Upload Sub-component ─────────────────────────────────
232
+ function JarUploadInline({ dialect, jarEndpoint, dbUri }) {
233
+ const [uploading, setUploading] = useState(false);
234
+ const [bridgeTesting, setBridgeTesting] = useState(false);
235
+ const [message, setMessage] = useState(null);
236
+ const [jarStatus, setJarStatus] = useState(null);
237
+ useEffect(() => {
238
+ fetch(jarEndpoint)
239
+ .then(r => r.json())
240
+ .then(data => {
241
+ if (data.ok) {
242
+ const s = data.dialects?.find((d) => d.dialect === dialect);
243
+ setJarStatus(s || { hasJar: false, jarFile: null });
244
+ }
245
+ })
246
+ .catch(() => { });
247
+ }, [dialect, jarEndpoint]);
248
+ const handleUpload = async (e) => {
249
+ const file = e.target.files?.[0];
250
+ if (!file || !file.name.endsWith('.jar'))
251
+ return;
252
+ setUploading(true);
253
+ setMessage(null);
254
+ try {
255
+ const fd = new FormData();
256
+ fd.append('jar', file);
257
+ const res = await fetch(jarEndpoint, { method: 'POST', body: fd });
258
+ const result = await res.json();
259
+ if (result.ok) {
260
+ setMessage({ ok: true, text: `${result.fileName} uploade` });
261
+ setJarStatus({ hasJar: true, jarFile: result.fileName });
262
+ }
263
+ else {
264
+ setMessage({ ok: false, text: result.error || 'Erreur' });
265
+ }
266
+ }
267
+ catch {
268
+ setMessage({ ok: false, text: 'Erreur reseau' });
269
+ }
270
+ finally {
271
+ setUploading(false);
272
+ e.target.value = '';
273
+ }
274
+ };
275
+ const handleStartBridge = async () => {
276
+ if (!dbUri) {
277
+ setMessage({ ok: false, text: 'Renseignez les parametres de connexion avant de lancer le bridge' });
278
+ return;
279
+ }
280
+ setBridgeTesting(true);
281
+ setMessage(null);
282
+ try {
283
+ const res = await fetch(jarEndpoint, {
284
+ method: 'PATCH',
285
+ headers: { 'Content-Type': 'application/json' },
286
+ body: JSON.stringify({ dialect, uri: dbUri }),
287
+ });
288
+ const result = await res.json();
289
+ if (result.ok) {
290
+ setMessage({ ok: true, text: 'Bridge JDBC lance avec succes' });
291
+ }
292
+ else {
293
+ setMessage({ ok: false, text: result.error || 'Echec lancement du bridge' });
294
+ }
295
+ }
296
+ catch {
297
+ setMessage({ ok: false, text: 'Erreur reseau' });
298
+ }
299
+ finally {
300
+ setBridgeTesting(false);
301
+ }
302
+ };
303
+ return (_jsxs("div", { style: S.jarBox, children: [_jsxs("div", { style: S.flex(8), children: [_jsx("span", { style: S.jarTitle, children: "Driver JDBC" }), jarStatus?.hasJar ? (_jsx("span", { style: { ...S.badge('installed'), marginLeft: 0 }, children: jarStatus.jarFile })) : (_jsx("span", { style: { fontSize: 12, color: '#6b7280' }, children: "Aucun JAR installe" }))] }), _jsxs("div", { style: { ...S.flex(12), marginTop: 8, flexWrap: 'wrap' }, children: [_jsxs("label", { style: { ...S.btn('primary', uploading), cursor: uploading ? 'wait' : 'pointer', fontSize: 12, padding: '6px 12px' }, children: [uploading ? 'Upload...' : 'Uploader un .jar', _jsx("input", { type: "file", accept: ".jar", onChange: handleUpload, disabled: uploading, style: { display: 'none' } })] }), jarStatus?.hasJar && (_jsx("button", { style: {
304
+ ...S.btn('primary', bridgeTesting),
305
+ fontSize: 12, padding: '6px 12px',
306
+ backgroundColor: bridgeTesting ? '#6b7280' : '#059669',
307
+ }, onClick: handleStartBridge, disabled: bridgeTesting, children: bridgeTesting ? 'Lancement...' : 'Lancer le bridge' })), _jsx("span", { style: { fontSize: 11, color: '#9ca3af' }, children: "Ex: hsqldb*.jar, ojdbc*.jar, db2jcc*.jar" })] }), message && (_jsx("p", { style: { fontSize: 12, color: message.ok ? '#059669' : '#dc2626', marginTop: 8 }, children: message.text }))] }));
308
+ }
309
+ // ── Main Component ───────────────────────────────────────────
310
+ export default function SetupWizard({ t: tProp, onComplete, endpoints = {}, dbNamePrefix = 'mydb', persistState = true, }) {
311
+ const t = tProp || ((k) => k);
312
+ const ep = {
313
+ detectModules: endpoints.detectModules || '/api/setup/detect-modules',
314
+ testDb: endpoints.testDb || '/api/setup/test-db',
315
+ installModules: endpoints.installModules || '/api/setup/install-modules',
316
+ install: endpoints.install || '/api/setup/install',
317
+ uploadJar: endpoints.uploadJar || '/api/setup/upload-jar',
318
+ };
319
+ // --- State ---
320
+ const [currentStep, setCurrentStep] = useState(0);
321
+ const [dialect, setDialect] = useState('mongodb');
322
+ const [dbConfig, setDbConfig] = useState({ ...DIALECT_DEFAULTS.mongodb, name: `${dbNamePrefix}_prod` });
323
+ const [dbTestResult, setDbTestResult] = useState(null);
324
+ const [dbTesting, setDbTesting] = useState(false);
325
+ const [adminConfig, setAdminConfig] = useState({ firstName: '', lastName: '', email: '', password: '', confirmPassword: '' });
326
+ const [seedOptions, setSeedOptions] = useState({ activities: true, demoUsers: false, demoData: false });
327
+ const [availableModules, setAvailableModules] = useState([]);
328
+ const [selectedModules, setSelectedModules] = useState([]);
329
+ const [detectedModules, setDetectedModules] = useState([]);
330
+ const [modulesDetected, setModulesDetected] = useState(false);
331
+ const [installing, setInstalling] = useState(false);
332
+ const [installResult, setInstallResult] = useState(null);
333
+ const [hydrated, setHydrated] = useState(false);
334
+ const step = STEPS[currentStep];
335
+ // --- Persist / Restore ---
336
+ useEffect(() => {
337
+ if (!persistState) {
338
+ setHydrated(true);
339
+ return;
340
+ }
341
+ try {
342
+ const raw = sessionStorage.getItem(STORAGE_KEY);
343
+ if (raw) {
344
+ const saved = JSON.parse(raw);
345
+ if (saved.currentStep != null)
346
+ setCurrentStep(saved.currentStep);
347
+ if (saved.dialect)
348
+ setDialect(saved.dialect);
349
+ if (saved.dbConfig)
350
+ setDbConfig(saved.dbConfig);
351
+ if (saved.adminConfig)
352
+ setAdminConfig(saved.adminConfig);
353
+ if (saved.seedOptions)
354
+ setSeedOptions(saved.seedOptions);
355
+ if (saved.selectedModules?.length)
356
+ setSelectedModules(saved.selectedModules);
357
+ }
358
+ }
359
+ catch { }
360
+ setHydrated(true);
361
+ }, [persistState]);
362
+ useEffect(() => {
363
+ if (!hydrated || !persistState)
364
+ return;
365
+ try {
366
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ currentStep, dialect, dbConfig, adminConfig, seedOptions, selectedModules }));
367
+ }
368
+ catch { }
369
+ }, [hydrated, persistState, currentStep, dialect, dbConfig, adminConfig, seedOptions, selectedModules]);
370
+ // --- Detect modules ---
371
+ useEffect(() => {
372
+ fetch(ep.detectModules)
373
+ .then(r => r.json())
374
+ .then((data) => {
375
+ if (data.modules)
376
+ setAvailableModules(data.modules);
377
+ if (data.installed)
378
+ setDetectedModules(data.installed);
379
+ if (selectedModules.length === 0) {
380
+ const mods = data.modules || [];
381
+ const pre = new Set([
382
+ ...mods.filter(m => m.required || m.default).map(m => m.key),
383
+ ...(data.installed || []),
384
+ ]);
385
+ setSelectedModules(Array.from(pre));
386
+ }
387
+ setModulesDetected(true);
388
+ })
389
+ .catch(() => setModulesDetected(true));
390
+ // eslint-disable-next-line react-hooks/exhaustive-deps
391
+ }, []);
392
+ // --- Module toggle ---
393
+ const toggleModule = useCallback((key) => {
394
+ const mod = availableModules.find(m => m.key === key);
395
+ if (!mod || mod.required)
396
+ return;
397
+ setSelectedModules(prev => {
398
+ if (prev.includes(key)) {
399
+ const toRemove = new Set([key]);
400
+ let changed = true;
401
+ while (changed) {
402
+ changed = false;
403
+ for (const m of availableModules) {
404
+ if (toRemove.has(m.key) || m.required)
405
+ continue;
406
+ if (m.dependsOn?.some(dep => toRemove.has(dep)) && prev.includes(m.key)) {
407
+ toRemove.add(m.key);
408
+ changed = true;
409
+ }
410
+ }
411
+ }
412
+ return prev.filter(k => !toRemove.has(k));
413
+ }
414
+ else {
415
+ return resolveModuleDeps([...prev, key], availableModules);
416
+ }
417
+ });
418
+ }, [availableModules]);
419
+ // --- Dialect select ---
420
+ function selectDialect(d) {
421
+ setDialect(d);
422
+ const defaults = DIALECT_DEFAULTS[d];
423
+ setDbConfig({ ...defaults, name: defaults.name === 'mydb' ? dbNamePrefix : defaults.name === 'mydb_prod' ? `${dbNamePrefix}_prod` : defaults.name.replace('mydb', dbNamePrefix) });
424
+ setDbTestResult(null);
425
+ }
426
+ // --- Test DB ---
427
+ async function testDb() {
428
+ setDbTesting(true);
429
+ setDbTestResult(null);
430
+ try {
431
+ const res = await fetch(ep.testDb, {
432
+ method: 'POST',
433
+ headers: { 'Content-Type': 'application/json' },
434
+ body: JSON.stringify({ dialect, ...dbConfig }),
435
+ });
436
+ const data = await res.json();
437
+ if (data.ok) {
438
+ setDbTestResult(data);
439
+ }
440
+ else {
441
+ const err = typeof data.error === 'string' ? data.error : data.error?.message || 'Connexion echouee';
442
+ setDbTestResult({ ok: false, error: err });
443
+ }
444
+ }
445
+ catch (err) {
446
+ setDbTestResult({ ok: false, error: err instanceof Error ? err.message : 'Erreur' });
447
+ }
448
+ setDbTesting(false);
449
+ }
450
+ // --- Install ---
451
+ async function runInstallation() {
452
+ setInstalling(true);
453
+ setInstallResult(null);
454
+ try {
455
+ const modRes = await fetch(ep.installModules, {
456
+ method: 'POST',
457
+ headers: { 'Content-Type': 'application/json' },
458
+ body: JSON.stringify({ modules: selectedModules }),
459
+ });
460
+ if (!modRes.ok) {
461
+ const d = await safeJson(modRes);
462
+ setInstallResult({ ok: false, error: d?.error?.message || 'Erreur installation modules' });
463
+ setInstalling(false);
464
+ return;
465
+ }
466
+ const res = await fetchRetry(ep.install, {
467
+ method: 'POST',
468
+ headers: { 'Content-Type': 'application/json' },
469
+ body: JSON.stringify({
470
+ dialect,
471
+ db: dbConfig,
472
+ admin: { email: adminConfig.email, password: adminConfig.password, firstName: adminConfig.firstName, lastName: adminConfig.lastName },
473
+ seed: seedOptions,
474
+ modules: selectedModules,
475
+ }),
476
+ });
477
+ const data = await safeJson(res);
478
+ if (res.ok && data) {
479
+ setInstallResult({ ok: true, needsRestart: data.data?.needsRestart });
480
+ }
481
+ else {
482
+ setInstallResult({ ok: false, error: data?.error?.message || `Erreur installation (HTTP ${res.status})` });
483
+ }
484
+ }
485
+ catch (err) {
486
+ setInstallResult({ ok: false, error: err instanceof Error ? err.message : 'Erreur' });
487
+ }
488
+ setInstalling(false);
489
+ }
490
+ // --- Navigation ---
491
+ function canGoNext() {
492
+ switch (step) {
493
+ case 'welcome': return true;
494
+ case 'modules': return selectedModules.length > 0;
495
+ case 'dialect': return true;
496
+ case 'database':
497
+ if (dialect === 'sqlite' || dialect === 'spanner')
498
+ return dbConfig.name.trim() !== '';
499
+ return dbTestResult?.ok === true;
500
+ case 'admin':
501
+ return adminConfig.firstName.trim() !== '' && adminConfig.lastName.trim() !== '' &&
502
+ adminConfig.email.trim() !== '' && adminConfig.password.length >= 6 &&
503
+ adminConfig.password === adminConfig.confirmPassword;
504
+ case 'summary': return false;
505
+ }
506
+ }
507
+ function goNext() { if (currentStep < STEPS.length - 1)
508
+ setCurrentStep(currentStep + 1); }
509
+ function goBack() { if (currentStep > 0)
510
+ setCurrentStep(currentStep - 1); }
511
+ function dbSummaryLabel() {
512
+ if (dialect === 'sqlite')
513
+ return `SQLite — ./data/${dbConfig.name}.db`;
514
+ if (dialect === 'spanner')
515
+ return `Cloud Spanner — ${dbConfig.name}`;
516
+ const info = DIALECT_INFO.find(d => d.key === dialect);
517
+ return `${info?.name || dialect} — ${dbConfig.host}:${dbConfig.port}/${dbConfig.name}`;
518
+ }
519
+ function handleComplete() {
520
+ if (persistState) {
521
+ try {
522
+ sessionStorage.removeItem(STORAGE_KEY);
523
+ }
524
+ catch { }
525
+ }
526
+ onComplete?.();
527
+ }
528
+ // ── Render ─────────────────────────────────────────────────
529
+ return (_jsx("div", { style: S.wrapper, children: _jsxs("div", { style: S.container, children: [_jsx("div", { style: S.stepperRow, children: STEPS.map((s, i) => (_jsxs("div", { style: S.flex(8), children: [_jsx("div", { style: S.stepCircle(i < currentStep ? 'done' : i === currentStep ? 'current' : 'future'), children: i < currentStep ? '✓' : i + 1 }), _jsx("span", { style: S.stepLabel(i === currentStep), children: t(`setup.steps.${s}`) }), i < STEPS.length - 1 && _jsx("div", { style: S.stepLine(i < currentStep) })] }, s))) }), _jsxs("div", { style: S.card, children: [step === 'welcome' && (_jsxs("div", { style: S.center, children: [_jsx("div", { style: { width: 80, height: 80, borderRadius: '50%', backgroundColor: '#e0f2fe', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px', fontSize: 40 }, children: "\uD83D\uDEE1\uFE0F" }), _jsx("h2", { style: { fontSize: 24, fontWeight: 700, color: '#111827', marginBottom: 8 }, children: t('setup.welcome.title') }), _jsx("p", { style: { color: '#6b7280', marginBottom: 24 }, children: t('setup.welcome.description') }), _jsxs("button", { style: S.btn('lg'), onClick: goNext, children: [t('setup.welcome.start'), " \u2192"] })] })), step === 'modules' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDCE6" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.modules.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.modules.description') })] })] }), _jsx("div", { style: S.grid2, children: availableModules.map(mod => {
530
+ const isSelected = selectedModules.includes(mod.key);
531
+ const isDetected = detectedModules.includes(mod.key);
532
+ return (_jsxs("div", { style: S.moduleCard(isSelected, !!mod.required), onClick: () => toggleModule(mod.key), children: [_jsxs("div", { style: S.moduleHeader, children: [_jsxs("div", { style: S.moduleLeft, children: [_jsx("span", { style: { fontSize: 20 }, children: mod.icon }), _jsx("span", { style: S.moduleName, children: mod.label })] }), _jsxs("div", { style: S.moduleBadges, children: [mod.discovered && _jsx("span", { style: S.badge('new'), children: "Nouveau" }), isDetected && _jsx("span", { style: S.badge('installed'), children: t('setup.modules.installed') }), mod.required && _jsx("span", { style: S.badge('required'), children: t('setup.modules.required') }), _jsx("input", { type: "checkbox", checked: isSelected, disabled: mod.required, readOnly: true, style: S.checkbox })] })] }), _jsx("div", { style: S.moduleDesc, children: mod.description }), mod.dependsOn?.length ? (_jsxs("div", { style: { fontSize: 11, color: '#9ca3af', marginTop: 4 }, children: ["Depend de : ", mod.dependsOn.join(', ')] })) : null] }, mod.key));
533
+ }) }), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', !canGoNext()), onClick: goNext, disabled: !canGoNext(), children: [t('setup.next'), " \u2192"] })] })] })), step === 'dialect' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDCBE" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.dialect.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.dialect.description') })] })] }), _jsx("div", { style: S.grid3, children: DIALECT_INFO.map(d => (_jsxs("div", { style: S.dialectCard(dialect === d.key, !!d.premium), onClick: () => !d.premium && selectDialect(d.key), title: d.premium ? `${d.name} — disponible en version Premium` : d.name, children: [_jsx("div", { style: S.dialectIcon, children: d.icon }), _jsx("div", { style: S.dialectName, children: d.name }), d.premium && (_jsx("span", { style: { ...S.badge('premium'), marginLeft: 0, marginTop: 4 }, children: "Premium" }))] }, d.key))) }), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary'), onClick: goNext, children: [t('setup.next'), " \u2192"] })] })] })), step === 'database' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDDC4\uFE0F" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.database.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.database.description') })] })] }), dialect === 'sqlite' ? (_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.name') }), _jsx("input", { style: S.input, value: dbConfig.name, onChange: e => { setDbConfig({ ...dbConfig, name: e.target.value }); setDbTestResult(null); }, placeholder: dbNamePrefix }), _jsxs("p", { style: { fontSize: 11, color: '#9ca3af', marginTop: 4 }, children: [t('setup.database.sqliteInfo'), " ", _jsxs("code", { style: { fontFamily: 'monospace', backgroundColor: '#f3f4f6', padding: '1px 4px', borderRadius: 3 }, children: ["./data/", dbConfig.name, ".db"] })] })] })) : dialect === 'spanner' ? (_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.spannerPath') }), _jsx("input", { style: S.input, value: dbConfig.name, onChange: e => { setDbConfig({ ...dbConfig, name: e.target.value }); setDbTestResult(null); }, placeholder: "my-project/my-instance/mydb" }), _jsx("p", { style: { fontSize: 11, color: '#9ca3af', marginTop: 4 }, children: t('setup.database.spannerInfo') })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.host') }), _jsx("input", { style: S.input, value: dbConfig.host, onChange: e => { setDbConfig({ ...dbConfig, host: e.target.value }); setDbTestResult(null); } })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.port') }), _jsx("input", { style: S.input, type: "number", value: dbConfig.port, onChange: e => { setDbConfig({ ...dbConfig, port: parseInt(e.target.value) || 0 }); setDbTestResult(null); } })] })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.name') }), _jsx("input", { style: S.input, value: dbConfig.name, onChange: e => { setDbConfig({ ...dbConfig, name: e.target.value }); setDbTestResult(null); } })] }), dialect !== 'hsqldb' && (_jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.user') }), _jsx("input", { style: S.input, value: dbConfig.user, onChange: e => { setDbConfig({ ...dbConfig, user: e.target.value }); setDbTestResult(null); } })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.password') }), _jsx("input", { style: S.input, type: "password", value: dbConfig.password, onChange: e => { setDbConfig({ ...dbConfig, password: e.target.value }); setDbTestResult(null); } })] })] }))] })), dialect !== 'mongodb' && dialect !== 'sqlite' && (_jsxs("div", { style: { ...S.alert('warning'), marginTop: 12 }, children: [t('setup.database.driverHint'), ' ', _jsx("code", { style: { fontFamily: 'monospace', backgroundColor: '#fef3c7', padding: '1px 4px', borderRadius: 3 }, children: DRIVER_HINTS[dialect] })] })), JDBC_DIALECTS.includes(dialect) && (_jsx(JarUploadInline, { dialect: dialect, jarEndpoint: ep.uploadJar, dbUri: simpleDbUri(dialect, dbConfig) })), dialect !== 'sqlite' && dialect !== 'spanner' && (_jsxs("div", { style: { ...S.flex(16), marginTop: 16 }, children: [_jsxs("button", { style: S.btn('outline', dbTesting), onClick: testDb, disabled: dbTesting, children: [dbTesting ? '⏳ ' : '🗄️ ', dbTesting ? t('setup.database.testing') : t('setup.database.test')] }), dbTestResult && (_jsx("span", { style: { fontSize: 13, color: dbTestResult.ok ? '#059669' : '#dc2626' }, children: dbTestResult.ok
534
+ ? `✅ ${t('setup.database.success')}${dbTestResult.dbVersion ? ` (v${dbTestResult.dbVersion})` : ''}`
535
+ : `❌ ${t('setup.database.error')}: ${dbTestResult.error}` }))] })), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', !canGoNext()), onClick: goNext, disabled: !canGoNext(), children: [t('setup.next'), " \u2192"] })] })] })), step === 'admin' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDC64" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.admin.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.admin.description') })] })] }), _jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.firstName') }), _jsx("input", { style: S.input, value: adminConfig.firstName, onChange: e => setAdminConfig({ ...adminConfig, firstName: e.target.value }) })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.lastName') }), _jsx("input", { style: S.input, value: adminConfig.lastName, onChange: e => setAdminConfig({ ...adminConfig, lastName: e.target.value }) })] })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.email') }), _jsx("input", { style: S.input, type: "email", value: adminConfig.email, onChange: e => setAdminConfig({ ...adminConfig, email: e.target.value }), placeholder: "admin@example.com" })] }), _jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.password') }), _jsx("input", { style: S.input, type: "password", value: adminConfig.password, onChange: e => setAdminConfig({ ...adminConfig, password: e.target.value }) })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.confirmPassword') }), _jsx("input", { style: S.input, type: "password", value: adminConfig.confirmPassword, onChange: e => setAdminConfig({ ...adminConfig, confirmPassword: e.target.value }) })] })] }), adminConfig.password && adminConfig.confirmPassword && adminConfig.password !== adminConfig.confirmPassword && (_jsx("p", { style: { fontSize: 13, color: '#dc2626' }, children: t('setup.admin.passwordMismatch') })), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', !canGoNext()), onClick: goNext, disabled: !canGoNext(), children: [t('setup.next'), " \u2192"] })] })] })), step === 'summary' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\u2699\uFE0F" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.summary.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.summary.description') })] })] }), _jsxs("div", { style: S.summaryCard, children: [_jsx("div", { style: S.summaryTitle, children: t('setup.summary.dbConfig') }), _jsxs("div", { style: S.summaryText, children: [_jsx("span", { style: { fontFamily: 'monospace' }, children: dbSummaryLabel() }), dialect !== 'sqlite' && dbConfig.user && _jsxs("span", { style: { display: 'block', marginTop: 4 }, children: ["Utilisateur: ", dbConfig.user] })] })] }), _jsxs("div", { style: S.summaryCard, children: [_jsx("div", { style: S.summaryTitle, children: t('setup.summary.adminConfig') }), _jsxs("div", { style: S.summaryText, children: [_jsxs("div", { children: [adminConfig.firstName, " ", adminConfig.lastName] }), _jsx("div", { children: adminConfig.email })] })] }), _jsxs("div", { style: S.summaryCard, children: [_jsx("div", { style: S.summaryTitle, children: t('setup.modules.title') }), _jsx("div", { style: S.flexWrap, children: selectedModules.map(key => {
536
+ const mod = availableModules.find(m => m.key === key);
537
+ return mod ? (_jsxs("span", { style: {
538
+ display: 'inline-flex', alignItems: 'center', gap: 4,
539
+ fontSize: 12, backgroundColor: '#f0f9ff', color: '#0369a1',
540
+ border: '1px solid #bae6fd', borderRadius: 16, padding: '4px 10px',
541
+ }, children: [mod.icon, " ", mod.label] }, key)) : null;
542
+ }) })] }), _jsxs("div", { style: S.summaryCard, children: [_jsx("div", { style: S.summaryTitle, children: t('setup.summary.seedTitle') }), _jsx("p", { style: { ...S.summaryText, marginBottom: 12 }, children: t('setup.summary.seedInfo') }), _jsxs("div", { style: S.checkRow, children: [_jsx("input", { type: "checkbox", style: S.checkbox, checked: seedOptions.activities, onChange: e => setSeedOptions({ ...seedOptions, activities: e.target.checked, demoData: e.target.checked ? seedOptions.demoData : false }), disabled: installing || !!installResult?.ok }), _jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 500 }, children: t('setup.summary.seedActivities') }), _jsx("div", { style: { fontSize: 12, color: '#9ca3af' }, children: t('setup.summary.seedActivitiesDesc') })] })] }), _jsxs("div", { style: S.checkRow, children: [_jsx("input", { type: "checkbox", style: S.checkbox, checked: seedOptions.demoUsers, onChange: e => setSeedOptions({ ...seedOptions, demoUsers: e.target.checked }), disabled: installing || !!installResult?.ok }), _jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 500 }, children: t('setup.summary.seedDemoUsers') }), _jsx("div", { style: { fontSize: 12, color: '#9ca3af' }, children: t('setup.summary.seedDemoUsersDesc') })] })] }), _jsxs("div", { style: S.checkRow, children: [_jsx("input", { type: "checkbox", style: S.checkbox, checked: seedOptions.demoData, onChange: e => setSeedOptions({ ...seedOptions, demoData: e.target.checked, activities: e.target.checked ? true : seedOptions.activities }), disabled: installing || !!installResult?.ok }), _jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 500 }, children: t('setup.summary.seedDemoData') }), _jsx("div", { style: { fontSize: 12, color: '#9ca3af' }, children: t('setup.summary.seedDemoDataDesc') })] })] })] }), installResult && (_jsx("div", { style: S.alert(installResult.ok ? 'success' : 'error'), children: installResult.ok ? (_jsxs(_Fragment, { children: [_jsxs("div", { style: { fontWeight: 600, marginBottom: 4 }, children: ["\u2705 ", t('setup.summary.success')] }), _jsx("div", { children: t('setup.summary.successDesc') }), installResult.needsRestart && (_jsx("div", { style: { color: '#92400e', fontWeight: 500, marginTop: 8 }, children: t('setup.summary.needsRestart') }))] })) : (_jsxs("div", { children: ["\u274C ", installResult.error] })) })), _jsx("div", { style: S.navRow, children: !installResult?.ok ? (_jsxs(_Fragment, { children: [_jsxs("button", { style: S.btn('outline', installing), onClick: goBack, disabled: installing, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', installing), onClick: runInstallation, disabled: installing, children: [installing ? '⏳ ' : '⚙️ ', installing ? t('setup.summary.installing') : t('setup.summary.install')] })] })) : (_jsx("div", { style: { width: '100%', textAlign: 'center' }, children: _jsxs("button", { style: S.btn('lg'), onClick: handleComplete, children: [t('setup.summary.goToLogin'), " \u2192"] }) })) })] }))] })] }) }));
543
+ }
package/dist/index.d.ts CHANGED
@@ -13,5 +13,6 @@ export { createInstallModulesHandler } from './api/install-modules.route';
13
13
  export { createReconfigHandlers } from './api/reconfig.route';
14
14
  export { createUploadJarHandlers } from './api/upload-jar.route';
15
15
  export { default as ReconfigPanel } from './components/ReconfigPanel';
16
+ export { default as SetupWizard } from './components/SetupWizard';
16
17
  export { setupMenuContribution } from './lib/menu';
17
18
  export type { DialectType, DialectInfo, DbConfig, InstallConfig, SeedOptions, SeedDefinition, MostaSetupConfig, ModuleDefinition, } from './types/index';
package/dist/index.js CHANGED
@@ -20,5 +20,6 @@ export { createReconfigHandlers } from './api/reconfig.route';
20
20
  export { createUploadJarHandlers } from './api/upload-jar.route';
21
21
  // Components
22
22
  export { default as ReconfigPanel } from './components/ReconfigPanel';
23
+ export { default as SetupWizard } from './components/SetupWizard';
23
24
  // Menu contribution
24
25
  export { setupMenuContribution } from './lib/menu';
@@ -26,8 +26,8 @@ export async function testDbConnection(params) {
26
26
  default: {
27
27
  const uri = composeDbUri(dialect, dbConfig);
28
28
  const { testConnection } = await import('@mostajs/orm');
29
- const ok = await testConnection({ dialect, uri, schemaStrategy: 'none' });
30
- return { ok };
29
+ const result = await testConnection({ dialect, uri, schemaStrategy: 'none' });
30
+ return result;
31
31
  }
32
32
  }
33
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/setup",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "Reusable setup wizard module — multi-dialect DB configuration, .env.local writer, seed runner",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "MIT",
@@ -43,6 +43,11 @@
43
43
  "import": "./dist/components/ReconfigPanel.js",
44
44
  "default": "./dist/components/ReconfigPanel.js"
45
45
  },
46
+ "./components/SetupWizard": {
47
+ "types": "./dist/components/SetupWizard.d.ts",
48
+ "import": "./dist/components/SetupWizard.js",
49
+ "default": "./dist/components/SetupWizard.js"
50
+ },
46
51
  "./api/reconfig": {
47
52
  "types": "./dist/api/reconfig.route.d.ts",
48
53
  "import": "./dist/api/reconfig.route.js",
@@ -81,7 +86,7 @@
81
86
  "prepublishOnly": "npm run build"
82
87
  },
83
88
  "dependencies": {
84
- "@mostajs/orm": "^1.3.1",
89
+ "@mostajs/orm": "^1.4.0",
85
90
  "bcryptjs": "^2.4.3"
86
91
  },
87
92
  "devDependencies": {