@lastbrain/app 0.1.30 → 0.1.32

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.
Files changed (53) hide show
  1. package/dist/auth/useAuthSession.d.ts.map +1 -1
  2. package/dist/auth/useAuthSession.js +8 -2
  3. package/dist/components/NotificationContainer.d.ts +2 -0
  4. package/dist/components/NotificationContainer.d.ts.map +1 -0
  5. package/dist/components/NotificationContainer.js +8 -0
  6. package/dist/hooks/useNotifications.d.ts +29 -0
  7. package/dist/hooks/useNotifications.d.ts.map +1 -0
  8. package/dist/hooks/useNotifications.js +165 -0
  9. package/dist/hooks/useNotificationsSimple.d.ts +20 -0
  10. package/dist/hooks/useNotificationsSimple.d.ts.map +1 -0
  11. package/dist/hooks/useNotificationsSimple.js +37 -0
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +4 -1
  15. package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -1
  16. package/dist/layouts/AdminLayoutWithSidebar.js +13 -4
  17. package/dist/layouts/AppProviders.d.ts +15 -2
  18. package/dist/layouts/AppProviders.d.ts.map +1 -1
  19. package/dist/layouts/AppProviders.js +21 -8
  20. package/dist/layouts/AuthLayoutWithSidebar.d.ts.map +1 -1
  21. package/dist/layouts/AuthLayoutWithSidebar.js +10 -2
  22. package/dist/layouts/PublicLayout.js +1 -1
  23. package/dist/layouts/PublicLayoutWithSidebar.d.ts +8 -0
  24. package/dist/layouts/PublicLayoutWithSidebar.d.ts.map +1 -0
  25. package/dist/layouts/PublicLayoutWithSidebar.js +19 -0
  26. package/dist/layouts/RootLayout.d.ts +3 -1
  27. package/dist/layouts/RootLayout.d.ts.map +1 -1
  28. package/dist/layouts/RootLayout.js +3 -2
  29. package/dist/scripts/init-app.d.ts.map +1 -1
  30. package/dist/scripts/init-app.js +209 -23
  31. package/dist/scripts/module-build.d.ts.map +1 -1
  32. package/dist/scripts/module-build.js +52 -0
  33. package/dist/styles.css +1 -1
  34. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  35. package/dist/templates/DefaultDoc.js +64 -1
  36. package/dist/templates/DocPage.d.ts.map +1 -1
  37. package/dist/templates/DocPage.js +7 -0
  38. package/package.json +1 -2
  39. package/src/auth/useAuthSession.ts +6 -2
  40. package/src/components/NotificationContainer.tsx +20 -0
  41. package/src/hooks/useNotifications.ts +216 -0
  42. package/src/index.ts +4 -1
  43. package/src/layouts/AdminLayoutWithSidebar.tsx +18 -6
  44. package/src/layouts/AppProviders.tsx +50 -13
  45. package/src/layouts/AuthLayoutWithSidebar.tsx +16 -4
  46. package/src/layouts/PublicLayout.tsx +1 -1
  47. package/src/layouts/PublicLayoutWithSidebar.tsx +56 -0
  48. package/src/layouts/RootLayout.tsx +10 -2
  49. package/src/scripts/generate-realtime-config.mjs +183 -0
  50. package/src/scripts/init-app.ts +237 -23
  51. package/src/scripts/module-build.ts +63 -0
  52. package/src/templates/DefaultDoc.tsx +265 -0
  53. package/src/templates/DocPage.tsx +7 -0
@@ -2,9 +2,17 @@
2
2
 
3
3
  import { ThemeProvider } from "next-themes";
4
4
  import { AppProviders } from "./AppProviders.js";
5
+ import type { ModuleRealtimeConfig } from "@lastbrain/core";
5
6
 
6
7
  // Note: L'app Next.js doit importer son propre globals.css dans son layout
7
- export function RootLayout({ children }: { children: React.ReactNode }) {
8
+ // Note: La configuration realtime doit être fournie par l'app qui utilise ce layout
9
+ export function RootLayout({
10
+ children,
11
+ realtimeConfig = [],
12
+ }: {
13
+ children: React.ReactNode;
14
+ realtimeConfig?: ModuleRealtimeConfig[];
15
+ }) {
8
16
  return (
9
17
  <html lang="fr" suppressHydrationWarning>
10
18
  <body className="min-h-screen">
@@ -14,7 +22,7 @@ export function RootLayout({ children }: { children: React.ReactNode }) {
14
22
  enableSystem={false}
15
23
  storageKey="lastbrain-theme"
16
24
  >
17
- <AppProviders>
25
+ <AppProviders realtimeConfig={realtimeConfig}>
18
26
  <div className=" min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-white">
19
27
  {children}
20
28
  </div>
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /* global console, process */
3
+
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ /**
12
+ * Script de génération de configuration realtime globale
13
+ * Lit les build.config de tous les modules et génère un fichier TypeScript de configuration
14
+ * pour le RealtimeProvider
15
+ */
16
+
17
+ async function loadModuleBuildConfig(modulePath) {
18
+ try {
19
+ // Charger directement le build.config depuis la version compilée
20
+ const configPath = path.join(modulePath, 'dist');
21
+ if (!fs.existsSync(configPath)) {
22
+ console.warn(`⚠️ Module non compilé: ${modulePath}/dist`);
23
+ return null;
24
+ }
25
+
26
+ // Chercher le fichier principal exporté
27
+ const packageJsonPath = path.join(modulePath, 'package.json');
28
+ if (!fs.existsSync(packageJsonPath)) {
29
+ return null;
30
+ }
31
+
32
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
33
+ const main = packageJson.main || 'dist/index.js';
34
+ const mainPath = path.resolve(modulePath, main);
35
+
36
+ if (!fs.existsSync(mainPath)) {
37
+ console.warn(`⚠️ Fichier principal non trouvé: ${mainPath}`);
38
+ return null;
39
+ }
40
+
41
+ // Charger le module
42
+ const moduleUrl = `file://${mainPath}`;
43
+ const moduleExports = await import(moduleUrl);
44
+ const buildConfig = moduleExports.default || moduleExports;
45
+
46
+ if (buildConfig && typeof buildConfig === 'object' && buildConfig.realtime) {
47
+ console.log(`✅ Configuration realtime trouvée pour ${buildConfig.moduleName}`);
48
+ return buildConfig;
49
+ }
50
+
51
+ return null;
52
+ } catch (error) {
53
+ console.warn(`⚠️ Erreur lors du chargement de ${modulePath}:`, error.message);
54
+ return null;
55
+ }
56
+ }
57
+
58
+ async function findModules(rootPath) {
59
+ const packagesPath = path.join(rootPath, 'packages');
60
+ const modules = [];
61
+
62
+ if (!fs.existsSync(packagesPath)) {
63
+ console.error('❌ Dossier packages non trouvé');
64
+ return modules;
65
+ }
66
+
67
+ const entries = fs.readdirSync(packagesPath);
68
+
69
+ for (const entry of entries) {
70
+ const entryPath = path.join(packagesPath, entry);
71
+ const stat = fs.statSync(entryPath);
72
+
73
+ if (stat.isDirectory() && entry.startsWith('module-')) {
74
+ const packageJsonPath = path.join(entryPath, 'package.json');
75
+
76
+ if (fs.existsSync(packageJsonPath)) {
77
+ modules.push(entryPath);
78
+ }
79
+ }
80
+ }
81
+
82
+ return modules;
83
+ }
84
+
85
+ function generateTypeScriptConfig(realtimeConfigs, targetPath) {
86
+ const imports = `import type { ModuleRealtimeConfig } from "@lastbrain/core";`;
87
+
88
+ const configData = {
89
+ modules: realtimeConfigs,
90
+ generatedAt: new Date().toISOString(),
91
+ version: "1.0.0"
92
+ };
93
+
94
+ const content = `${imports}
95
+
96
+ // GENERATED FILE - DO NOT EDIT MANUALLY
97
+ // Generated at: ${configData.generatedAt}
98
+
99
+ export const realtimeConfig: ModuleRealtimeConfig[] = ${JSON.stringify(configData.modules, null, 2)};
100
+
101
+ export default realtimeConfig;
102
+ `;
103
+
104
+ // Créer le dossier config s'il n'existe pas
105
+ const configDir = path.dirname(targetPath);
106
+ if (!fs.existsSync(configDir)) {
107
+ fs.mkdirSync(configDir, { recursive: true });
108
+ }
109
+
110
+ // Écrire le fichier TypeScript
111
+ fs.writeFileSync(targetPath, content);
112
+
113
+ return configData;
114
+ }
115
+
116
+ async function generateRealtimeConfig(targetAppPath) {
117
+ console.log('🔄 Génération de la configuration realtime...');
118
+
119
+ // Trouver le dossier racine du projet
120
+ const rootPath = path.resolve(__dirname, '../../../..');
121
+ console.log(`📁 Répertoire racine: ${rootPath}`);
122
+
123
+ // Trouver tous les modules
124
+ const modulePaths = await findModules(rootPath);
125
+ console.log(`📦 Modules trouvés: ${modulePaths.length}`);
126
+
127
+ const realtimeConfigs = [];
128
+
129
+ // Charger les configurations de chaque module
130
+ for (const modulePath of modulePaths) {
131
+ console.log(`🔍 Analyse du module: ${path.basename(modulePath)}`);
132
+
133
+ const buildConfig = await loadModuleBuildConfig(modulePath);
134
+
135
+ if (buildConfig?.realtime) {
136
+ console.log(`✅ Configuration realtime trouvée pour ${buildConfig.moduleName}`);
137
+ realtimeConfigs.push(buildConfig.realtime);
138
+ } else {
139
+ console.log(`ℹ️ Pas de configuration realtime pour ${path.basename(modulePath)}`);
140
+ }
141
+ }
142
+
143
+ // Générer le fichier TypeScript de configuration
144
+ const outputPath = path.join(targetAppPath, 'config', 'realtime.ts');
145
+ const configData = generateTypeScriptConfig(realtimeConfigs, outputPath);
146
+
147
+ console.log('✅ Configuration realtime générée avec succès!');
148
+ console.log(`📄 Fichier: ${outputPath}`);
149
+ console.log(`📊 Modules avec realtime: ${realtimeConfigs.length}`);
150
+
151
+ // Afficher un résumé
152
+ realtimeConfigs.forEach(module => {
153
+ console.log(` - ${module.moduleId}: ${module.tables.length} table(s)`);
154
+ module.tables.forEach(table => {
155
+ console.log(` • ${table.schema}.${table.table} (${table.event})`);
156
+ });
157
+ });
158
+
159
+ return configData;
160
+ }
161
+
162
+ // Exécution si le script est appelé directement
163
+ if (import.meta.url === `file://${process.argv[1]}`) {
164
+ const targetAppPath = process.argv[2];
165
+
166
+ if (!targetAppPath) {
167
+ console.error('❌ Chemin de l\'app cible requis');
168
+ console.log('Usage: node generate-realtime-config.mjs <target-app-path>');
169
+ process.exit(1);
170
+ }
171
+
172
+ generateRealtimeConfig(targetAppPath)
173
+ .then(() => {
174
+ console.log('🎉 Génération terminée!');
175
+ process.exit(0);
176
+ })
177
+ .catch((error) => {
178
+ console.error('❌ Erreur lors de la génération:', error);
179
+ process.exit(1);
180
+ });
181
+ }
182
+
183
+ export { generateRealtimeConfig };
@@ -90,6 +90,7 @@ export async function initApp(options: InitAppOptions) {
90
90
  // 6. Créer .gitignore et .env.local.example
91
91
  await createGitIgnore(targetDir, force);
92
92
  await createEnvExample(targetDir, force);
93
+ await createEnvLocal(targetDir, force);
93
94
 
94
95
  // 7. Créer la structure Supabase avec migrations
95
96
  await createSupabaseStructure(targetDir, force);
@@ -251,6 +252,7 @@ function getLastBrainVersions(targetDir: string): {
251
252
  core: string;
252
253
  ui: string;
253
254
  moduleAuth: string;
255
+ moduleAi: string;
254
256
  } {
255
257
  const targetIsInMonorepo =
256
258
  fs.existsSync(
@@ -265,34 +267,74 @@ function getLastBrainVersions(targetDir: string): {
265
267
  core: "workspace:*",
266
268
  ui: "workspace:*",
267
269
  moduleAuth: "workspace:*",
270
+ moduleAi: "workspace:*",
268
271
  };
269
272
  }
270
273
 
271
- // Hors monorepo, lire les versions depuis les package.json locaux (plus fiable que "latest")
274
+ // Hors monorepo, essayer de lire les versions depuis le monorepo source
272
275
  try {
273
- // Essayer de lire depuis node_modules/@lastbrain (si init-app a été installé)
274
- const appPkgPath = path.join(__dirname, "../../package.json");
276
+ // Détecter si on est dans le contexte du package CLI en build
277
+ const cliPkgPath = path.join(__dirname, "../../package.json");
278
+
279
+ if (fs.existsSync(cliPkgPath)) {
280
+ const cliPkg = JSON.parse(fs.readFileSync(cliPkgPath, "utf-8"));
281
+ const cliVersion = `^${cliPkg.version}`;
282
+
283
+ // Essayer de lire les versions depuis le monorepo (si disponible)
284
+ const monorepoRoot = path.join(__dirname, "../../../../");
285
+ const moduleAuthPkgPath = path.join(
286
+ monorepoRoot,
287
+ "packages/module-auth/package.json",
288
+ );
289
+ const moduleAiPkgPath = path.join(
290
+ monorepoRoot,
291
+ "packages/module-ai/package.json",
292
+ );
293
+ const corePkgPath = path.join(monorepoRoot, "packages/core/package.json");
294
+ const uiPkgPath = path.join(monorepoRoot, "packages/ui/package.json");
295
+
296
+ let moduleAuthVersion = cliVersion;
297
+ let moduleAiVersion = cliVersion;
298
+ let coreVersion = cliPkg.dependencies?.["@lastbrain/core"] || cliVersion;
299
+ let uiVersion = cliPkg.dependencies?.["@lastbrain/ui"] || cliVersion;
300
+
301
+ // Lire les vraies versions depuis les package.json du monorepo
302
+ if (fs.existsSync(moduleAuthPkgPath)) {
303
+ const moduleAuthPkg = JSON.parse(
304
+ fs.readFileSync(moduleAuthPkgPath, "utf-8"),
305
+ );
306
+ moduleAuthVersion = `^${moduleAuthPkg.version}`;
307
+ }
275
308
 
276
- if (fs.existsSync(appPkgPath)) {
277
- const appPkg = JSON.parse(fs.readFileSync(appPkgPath, "utf-8"));
278
- const appVersion = `^${appPkg.version}`;
309
+ if (fs.existsSync(moduleAiPkgPath)) {
310
+ const moduleAiPkg = JSON.parse(
311
+ fs.readFileSync(moduleAiPkgPath, "utf-8"),
312
+ );
313
+ moduleAiVersion = `^${moduleAiPkg.version}`;
314
+ }
315
+
316
+ if (fs.existsSync(corePkgPath)) {
317
+ const corePkg = JSON.parse(fs.readFileSync(corePkgPath, "utf-8"));
318
+ coreVersion = `^${corePkg.version}`;
319
+ }
279
320
 
280
- // Lire les versions des dépendances de @lastbrain/app
281
- const coreDep = appPkg.dependencies?.["@lastbrain/core"];
282
- const uiDep = appPkg.dependencies?.["@lastbrain/ui"];
283
- const authDep = appPkg.dependencies?.["@lastbrain/module-auth"];
321
+ if (fs.existsSync(uiPkgPath)) {
322
+ const uiPkg = JSON.parse(fs.readFileSync(uiPkgPath, "utf-8"));
323
+ uiVersion = `^${uiPkg.version}`;
324
+ }
284
325
 
285
326
  return {
286
- app: appVersion,
287
- core: coreDep || appVersion,
288
- ui: uiDep || appVersion,
289
- moduleAuth: authDep || appVersion,
327
+ app: cliVersion,
328
+ core: coreVersion,
329
+ ui: uiVersion,
330
+ moduleAuth: moduleAuthVersion,
331
+ moduleAi: moduleAiVersion,
290
332
  };
291
333
  }
292
- } catch {
334
+ } catch (error) {
293
335
  console.warn(
294
336
  chalk.yellow(
295
- "⚠️ Impossible de lire les versions locales, utilisation de 'latest'",
337
+ `⚠️ Impossible de lire les versions locales (${error}), utilisation de 'latest'`,
296
338
  ),
297
339
  );
298
340
  }
@@ -303,6 +345,7 @@ function getLastBrainVersions(targetDir: string): {
303
345
  core: "latest",
304
346
  ui: "latest",
305
347
  moduleAuth: "latest",
348
+ moduleAi: "latest",
306
349
  };
307
350
  }
308
351
 
@@ -328,7 +371,7 @@ async function addDependencies(
328
371
  "@lastbrain/core": versions.core,
329
372
  "@lastbrain/ui": versions.ui,
330
373
  "next-themes": "^0.4.6",
331
- "pino": "^10.1.0",
374
+ pino: "^10.1.0",
332
375
  "pino-pretty": "^13.1.2",
333
376
  };
334
377
 
@@ -341,8 +384,13 @@ async function addDependencies(
341
384
  for (const moduleName of selectedModules) {
342
385
  const moduleInfo = AVAILABLE_MODULES.find((m) => m.name === moduleName);
343
386
  if (moduleInfo && moduleInfo.package !== "@lastbrain/module-auth") {
344
- // Pour les autres modules, utiliser "latest" pour éviter les problèmes de versions
345
- requiredDeps[moduleInfo.package] = "latest";
387
+ // Utiliser les vraies versions au lieu de "latest"
388
+ if (moduleInfo.package === "@lastbrain/module-ai") {
389
+ requiredDeps[moduleInfo.package] = versions.moduleAi;
390
+ } else {
391
+ // Pour les futurs modules, utiliser "latest" en attendant
392
+ requiredDeps[moduleInfo.package] = "latest";
393
+ }
346
394
  }
347
395
  }
348
396
 
@@ -443,7 +491,7 @@ import { HeroUIProvider } from "@heroui/system";
443
491
 
444
492
  import { ThemeProvider } from "next-themes";
445
493
  import { useRouter } from "next/navigation";
446
- import { AppProviders } from "@lastbrain/app";
494
+ import { AppProviders } from "../components/AppProviders";
447
495
  import type { PropsWithChildren } from "react";
448
496
  import { AppHeader } from "../components/AppHeader";
449
497
 
@@ -481,7 +529,7 @@ export default function RootLayout({ children }: PropsWithChildren<{}>) {
481
529
 
482
530
  import "../styles/globals.css";
483
531
  import { ThemeProvider } from "next-themes";
484
- import { AppProviders } from "@lastbrain/app";
532
+ import { AppProviders } from "../components/AppProviders";
485
533
  import type { PropsWithChildren } from "react";
486
534
  import { AppHeader } from "../components/AppHeader";
487
535
 
@@ -591,6 +639,9 @@ export default function NotFound() {
591
639
 
592
640
  // Créer le composant AppAside
593
641
  await createAppAside(targetDir, force);
642
+
643
+ // Créer le wrapper AppProviders avec configuration realtime
644
+ await createAppProvidersWrapper(targetDir, force);
594
645
  }
595
646
 
596
647
  async function createRoute(
@@ -723,11 +774,13 @@ import { Header } from "@lastbrain/ui";
723
774
  import { menuConfig } from "../config/menu";
724
775
  import { supabaseBrowserClient } from "@lastbrain/core";
725
776
  import { useRouter } from "next/navigation";
726
- import { useAuthSession } from "@lastbrain/app";
777
+ import { useAuthSession, useNotificationsContext } from "@lastbrain/app";
727
778
 
728
779
  export function AppHeader() {
729
780
  const router = useRouter();
730
781
  const { user, isSuperAdmin } = useAuthSession();
782
+ const { data, loading, markAsRead, markAllAsRead, deleteNotification } =
783
+ useNotificationsContext();
731
784
 
732
785
  const handleLogout = async () => {
733
786
  await supabaseBrowserClient.auth.signOut();
@@ -743,6 +796,12 @@ export function AppHeader() {
743
796
  brandName="LastBrain App"
744
797
  brandHref="/"
745
798
  isSuperAdmin={isSuperAdmin}
799
+ notifications={data.notifications}
800
+ unreadCount={data.unreadCount}
801
+ notificationsLoading={loading}
802
+ onMarkAsRead={markAsRead}
803
+ onMarkAllAsRead={markAllAsRead}
804
+ onDeleteNotification={deleteNotification}
746
805
  />
747
806
  );
748
807
  }
@@ -798,6 +857,36 @@ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
798
857
  }
799
858
  }
800
859
 
860
+ async function createAppProvidersWrapper(targetDir: string, force: boolean) {
861
+ const componentsDir = path.join(targetDir, "components");
862
+ await fs.ensureDir(componentsDir);
863
+
864
+ const providersPath = path.join(componentsDir, "AppProviders.tsx");
865
+ if (!fs.existsSync(providersPath) || force) {
866
+ const providersContent = `"use client";
867
+
868
+ import { AppProviders as BaseAppProviders } from "@lastbrain/app";
869
+ import { realtimeConfig } from "../config/realtime";
870
+ import type { ReactNode } from "react";
871
+
872
+ export function AppProviders({ children }: { children: ReactNode }) {
873
+ return (
874
+ <BaseAppProviders realtimeConfig={realtimeConfig}>
875
+ {children}
876
+ </BaseAppProviders>
877
+ );
878
+ }`;
879
+ await fs.writeFile(providersPath, providersContent);
880
+ console.log(chalk.green("✓ components/AppProviders.tsx créé"));
881
+ } else {
882
+ console.log(
883
+ chalk.gray(
884
+ " components/AppProviders.tsx existe déjà (utilisez --force pour écraser)",
885
+ ),
886
+ );
887
+ }
888
+ }
889
+
801
890
  async function createConfigFiles(
802
891
  targetDir: string,
803
892
  force: boolean,
@@ -984,6 +1073,7 @@ export default config;
984
1073
  const tsconfigPath = path.join(targetDir, "tsconfig.json");
985
1074
  if (!fs.existsSync(tsconfigPath) || force) {
986
1075
  const tsconfig = {
1076
+ extends: "../../tsconfig.base.json",
987
1077
  compilerOptions: {
988
1078
  target: "ES2020",
989
1079
  lib: ["ES2020", "DOM", "DOM.Iterable"],
@@ -1003,9 +1093,10 @@ export default config;
1003
1093
  paths: {
1004
1094
  "@/*": ["./*"],
1005
1095
  },
1096
+ types: [],
1006
1097
  },
1007
1098
  include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
1008
- exclude: ["node_modules"],
1099
+ exclude: ["node_modules", "dist", ".next", "out"],
1009
1100
  };
1010
1101
  await fs.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
1011
1102
  console.log(chalk.green("✓ tsconfig.json créé"));
@@ -1036,6 +1127,107 @@ export const menuConfig: MenuConfig = {
1036
1127
  await fs.writeFile(menuConfigPath, menuConfig);
1037
1128
  console.log(chalk.green("✓ config/menu.ts créé"));
1038
1129
  }
1130
+
1131
+ // Créer les hooks
1132
+ await createHooksDirectory(targetDir, force);
1133
+ }
1134
+
1135
+ async function createHooksDirectory(targetDir: string, force: boolean) {
1136
+ const hooksDir = path.join(targetDir, "hooks");
1137
+ await fs.ensureDir(hooksDir);
1138
+
1139
+ // Créer le hook useNotifications
1140
+ const useNotificationsPath = path.join(hooksDir, "useNotifications.ts");
1141
+ if (!fs.existsSync(useNotificationsPath) || force) {
1142
+ const hookContent = `"use client";
1143
+
1144
+ import { useEffect, useState } from "react";
1145
+ import { useNotificationRealtime } from "@lastbrain/core";
1146
+ import { supabaseBrowserClient } from "@lastbrain/core";
1147
+ import { useAuth } from "@lastbrain/app";
1148
+
1149
+ export interface Notification {
1150
+ id: string;
1151
+ title: string;
1152
+ message: string;
1153
+ read: boolean;
1154
+ created_at: string;
1155
+ owner_id: string;
1156
+ }
1157
+
1158
+ export function useNotifications() {
1159
+ const { user } = useAuth();
1160
+ const [notifications, setNotifications] = useState<Notification[]>([]);
1161
+ const [loading, setLoading] = useState(true);
1162
+ const [error, setError] = useState<Error | null>(null);
1163
+
1164
+ // Écouter les changements en temps réel
1165
+ const realtimeSignal = useNotificationRealtime();
1166
+
1167
+ // Charger les notifications initiales
1168
+ const fetchNotifications = async () => {
1169
+ if (!user?.id) return;
1170
+
1171
+ try {
1172
+ setLoading(true);
1173
+ const { data, error } = await supabaseBrowserClient
1174
+ .from("user_notifications")
1175
+ .select("*")
1176
+ .eq("owner_id", user.id)
1177
+ .order("created_at", { ascending: false });
1178
+
1179
+ if (error) throw error;
1180
+ setNotifications(data || []);
1181
+ setError(null);
1182
+ } catch (err) {
1183
+ setError(err as Error);
1184
+ } finally {
1185
+ setLoading(false);
1186
+ }
1187
+ };
1188
+
1189
+ // Charger au montage et quand l'utilisateur change
1190
+ useEffect(() => {
1191
+ fetchNotifications();
1192
+ }, [user?.id]);
1193
+
1194
+ // Recharger quand les données changent en temps réel
1195
+ useEffect(() => {
1196
+ if (realtimeSignal.hasUpdates && user?.id) {
1197
+ console.log("🔔 Rechargement des notifications suite à un changement");
1198
+ fetchNotifications();
1199
+ }
1200
+ }, [realtimeSignal.tick, user?.id]);
1201
+
1202
+ return {
1203
+ notifications,
1204
+ loading,
1205
+ error,
1206
+ refetch: fetchNotifications,
1207
+ hasUpdates: realtimeSignal.hasUpdates,
1208
+ };
1209
+ }
1210
+ `;
1211
+ await fs.writeFile(useNotificationsPath, hookContent);
1212
+ console.log(chalk.green("✓ hooks/useNotifications.ts créé"));
1213
+ }
1214
+
1215
+ // Créer un placeholder pour realtime.ts qui sera généré par build:modules
1216
+ const realtimePath = path.join(targetDir, "config", "realtime.ts");
1217
+ if (!fs.existsSync(realtimePath) || force) {
1218
+ const realtimeContent = `import type { ModuleRealtimeConfig } from "@lastbrain/core";
1219
+
1220
+ // GENERATED FILE - DO NOT EDIT MANUALLY
1221
+ // Ce fichier sera automatiquement généré lors du build:modules
1222
+ // Exécutez "pnpm build:modules" pour créer la configuration realtime
1223
+
1224
+ export const realtimeConfig: ModuleRealtimeConfig[] = [];
1225
+
1226
+ export default realtimeConfig;
1227
+ `;
1228
+ await fs.writeFile(realtimePath, realtimeContent);
1229
+ console.log(chalk.green("✓ config/realtime.ts placeholder créé"));
1230
+ }
1039
1231
  }
1040
1232
 
1041
1233
  async function createGitIgnore(targetDir: string, force: boolean) {
@@ -1145,6 +1337,28 @@ SUPABASE_SERVICE_ROLE_KEY=YOUR_LOCAL_SERVICE_ROLE_KEY
1145
1337
  }
1146
1338
  }
1147
1339
 
1340
+ async function createEnvLocal(targetDir: string, force: boolean) {
1341
+ const envLocalPath = path.join(targetDir, ".env.local");
1342
+
1343
+ if (!fs.existsSync(envLocalPath) || force) {
1344
+ console.log(chalk.yellow("\n🔐 Création de .env.local..."));
1345
+
1346
+ const envContent = `# Supabase Configuration
1347
+ # Valeurs par défaut pour le développement local
1348
+
1349
+ # Supabase Local (par défaut)
1350
+ NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
1351
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
1352
+ SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...
1353
+
1354
+ # OpenAI Configuration (clé factice pour éviter les erreurs de build)
1355
+ OPENAI_API_KEY=sk-fake-openai-key-for-development-replace-with-real-key
1356
+ `;
1357
+ await fs.writeFile(envLocalPath, envContent);
1358
+ console.log(chalk.green("✓ .env.local créé"));
1359
+ }
1360
+ }
1361
+
1148
1362
  async function createSupabaseStructure(targetDir: string, force: boolean) {
1149
1363
  console.log(chalk.yellow("\n🗄️ Création de la structure Supabase..."));
1150
1364
 
@@ -8,6 +8,7 @@ import type {
8
8
  ModuleMenuItemConfig,
9
9
  ModulePageConfig,
10
10
  ModuleSection,
11
+ ModuleRealtimeConfig,
11
12
  } from "@lastbrain/core";
12
13
 
13
14
  // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
@@ -779,6 +780,64 @@ export default function AdminLayout({
779
780
  }
780
781
  }
781
782
 
783
+ async function generateRealtimeConfig(moduleConfigs: ModuleBuildConfig[]) {
784
+ try {
785
+ // Extraire les configurations realtime des modules
786
+ const realtimeConfigs: ModuleRealtimeConfig[] = moduleConfigs
787
+ .filter((config) => config.realtime)
788
+ .map((config) => config.realtime!);
789
+
790
+ if (realtimeConfigs.length === 0) {
791
+ console.log("⏭️ No realtime configuration found in modules");
792
+ return;
793
+ }
794
+
795
+ // Générer le contenu TypeScript
796
+ const imports = `import type { ModuleRealtimeConfig } from "@lastbrain/core";`;
797
+
798
+ const configData = {
799
+ modules: realtimeConfigs,
800
+ generatedAt: new Date().toISOString(),
801
+ version: "1.0.0",
802
+ };
803
+
804
+ const content = `${imports}
805
+
806
+ // GENERATED FILE - DO NOT EDIT MANUALLY
807
+ // Generated at: ${configData.generatedAt}
808
+
809
+ export const realtimeConfig: ModuleRealtimeConfig[] = ${JSON.stringify(configData.modules, null, 2)};
810
+
811
+ export default realtimeConfig;
812
+ `;
813
+
814
+ // Créer le fichier de configuration
815
+ const outputPath = path.join(projectRoot, "config", "realtime.ts");
816
+ const configDir = path.dirname(outputPath);
817
+
818
+ // Créer le dossier config s'il n'existe pas
819
+ if (!fs.existsSync(configDir)) {
820
+ fs.mkdirSync(configDir, { recursive: true });
821
+ }
822
+
823
+ // Écrire le fichier TypeScript
824
+ fs.writeFileSync(outputPath, content);
825
+
826
+ console.log(`✅ Generated realtime configuration: ${outputPath}`);
827
+ console.log(`📊 Modules with realtime: ${realtimeConfigs.length}`);
828
+
829
+ // Afficher un résumé
830
+ realtimeConfigs.forEach((module) => {
831
+ console.log(` - ${module.moduleId}: ${module.tables.length} table(s)`);
832
+ module.tables.forEach((table) => {
833
+ console.log(` • ${table.schema}.${table.table} (${table.event})`);
834
+ });
835
+ });
836
+ } catch (error) {
837
+ console.error("❌ Error generating realtime configuration:", error);
838
+ }
839
+ }
840
+
782
841
  export async function runModuleBuild() {
783
842
  ensureDirectory(appDirectory);
784
843
 
@@ -823,4 +882,8 @@ export async function runModuleBuild() {
823
882
  generateAppAside();
824
883
  generateLayouts();
825
884
  copyModuleMigrations(moduleConfigs);
885
+
886
+ // Générer la configuration realtime
887
+ console.log("🔄 Generating realtime configuration...");
888
+ await generateRealtimeConfig(moduleConfigs);
826
889
  }