@ramme-io/create-app 1.2.1 → 1.2.5

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 (102) hide show
  1. package/package.json +9 -15
  2. package/template/package.json +41 -0
  3. package/template/pkg.json +9 -7
  4. package/template/src/App.tsx +72 -31
  5. package/template/src/components/AIChatWidget.tsx +2 -2
  6. package/template/src/components/AppHeader.tsx +12 -12
  7. package/template/src/components/AutoForm.tsx +13 -0
  8. package/template/src/{pages/styleguide → components}/NotFound.tsx +1 -1
  9. package/template/src/components/PageTitleUpdater.tsx +2 -2
  10. package/template/src/components/ProtectedRoute.tsx +18 -1
  11. package/template/src/components/ScrollToTop.tsx +19 -0
  12. package/template/src/components/dashboard/ChartLine.tsx +28 -0
  13. package/template/src/components/dashboard/StatCard.tsx +61 -0
  14. package/template/src/config/app.manifest.ts +3 -1
  15. package/template/src/config/component-registry.tsx +69 -0
  16. package/template/src/config/navigation.ts +1 -1
  17. package/template/src/data/mock-charts.ts +32 -28
  18. package/template/src/{components → engine/renderers}/DynamicBlock.tsx +28 -8
  19. package/template/src/engine/renderers/DynamicPage.tsx +150 -0
  20. package/template/src/engine/runtime/ManifestContext.tsx +79 -0
  21. package/template/src/{contexts → engine/runtime}/MqttContext.tsx +23 -14
  22. package/template/src/{contexts → engine/runtime}/SitemapContext.tsx +1 -1
  23. package/template/src/engine/runtime/data-seeder.ts +47 -0
  24. package/template/src/{hooks → engine/runtime}/useAction.ts +11 -14
  25. package/template/src/{hooks → engine/runtime}/useCrudLocalStorage.ts +27 -8
  26. package/template/src/{hooks → engine/runtime}/useDataQuery.ts +15 -1
  27. package/template/src/engine/runtime/useDynamicSitemap.tsx +43 -0
  28. package/template/src/engine/runtime/useJustInTimeSeeder.ts +76 -0
  29. package/template/src/engine/runtime/useLiveBridge.ts +44 -0
  30. package/template/src/engine/runtime/useSignal.ts +40 -0
  31. package/template/src/{generated/hooks.ts → engine/runtime/useSignalStore.ts} +35 -8
  32. package/template/src/engine/runtime/useWorkflowEngine.ts +89 -0
  33. package/template/src/{core → engine/types}/manifest-types.ts +35 -3
  34. package/template/src/{types → engine/validation}/schema.ts +17 -0
  35. package/template/src/{pages → features/ai/pages}/AiChat.tsx +1 -1
  36. package/template/src/features/auth/AuthContext.tsx +118 -0
  37. package/template/src/features/auth/pages/AuthLayout.tsx +55 -0
  38. package/template/src/features/auth/pages/LoginPage.tsx +106 -0
  39. package/template/src/features/auth/pages/SignupPage.tsx +96 -0
  40. package/template/src/{blocks → features/datagrid}/SmartTable.tsx +41 -25
  41. package/template/src/features/developer/GhostOverlay.tsx +114 -0
  42. package/template/src/{pages → features/onboarding/pages}/Welcome.tsx +0 -1
  43. package/template/src/features/overview/index.ts +1 -0
  44. package/template/src/features/overview/pages/OverviewPage.tsx +127 -0
  45. package/template/src/{pages → features/playground/pages}/AccountingLedgerPage.tsx +1 -1
  46. package/template/src/{pages/prototypes → features/playground/pages}/ItemSelectorPage.tsx +1 -1
  47. package/template/src/{pages/settings → features/settings/pages}/BillingPage.tsx +1 -1
  48. package/template/src/features/settings/pages/ProfilePage.tsx +153 -0
  49. package/template/src/{pages/settings → features/settings/pages}/TeamPage.tsx +1 -1
  50. package/template/src/features/styleguide/Styleguide.tsx +75 -0
  51. package/template/src/features/users/components/UserDrawer.tsx +138 -0
  52. package/template/src/features/users/index.ts +2 -0
  53. package/template/src/features/users/pages/UsersPage.tsx +151 -0
  54. package/template/src/features/visualizations/SmartChart.tsx +178 -0
  55. package/template/src/index.css +1 -1
  56. package/template/src/main.tsx +27 -15
  57. package/template/src/templates/dashboard/DashboardLayout.tsx +77 -107
  58. package/template/src/templates/dashboard/dashboard.sitemap.ts +19 -22
  59. package/template/src/templates/docs/DocsLayout.tsx +49 -38
  60. package/template/src/templates/docs/docs.sitemap.ts +22 -34
  61. package/template/src/templates/settings/SettingsLayout.tsx +83 -143
  62. package/template/src/templates/settings/settings.sitemap.ts +6 -6
  63. package/template/tailwind.config.cjs +10 -9
  64. package/template/vite.config.ts +0 -11
  65. package/template/src/adaptors/.gitkeep +0 -0
  66. package/template/src/components/LocalSideNav.tsx +0 -120
  67. package/template/src/components/PageWithSideNav.tsx +0 -69
  68. package/template/src/components/dev/GhostOverlay.tsx +0 -68
  69. package/template/src/config/dashboard.layout.ts +0 -110
  70. package/template/src/contexts/AuthContext.tsx +0 -64
  71. package/template/src/core/component-registry.tsx +0 -56
  72. package/template/src/core/data-seeder.ts +0 -35
  73. package/template/src/data/mockUsers.ts +0 -18
  74. package/template/src/hooks/useSignal.ts +0 -83
  75. package/template/src/hooks/useWorkflowEngine.ts +0 -123
  76. package/template/src/layouts/DataLayout.tsx +0 -37
  77. package/template/src/layouts/SideNavLayout.tsx +0 -28
  78. package/template/src/pages/Dashboard.tsx +0 -60
  79. package/template/src/pages/DataGridPage.tsx +0 -184
  80. package/template/src/pages/DynamicPage.tsx +0 -95
  81. package/template/src/pages/LoginPage.tsx +0 -58
  82. package/template/src/pages/settings/ProfilePage.tsx +0 -10
  83. package/template/src/pages/styleguide/Styleguide.tsx +0 -40
  84. package/template/src/templates/docs/pages/Introduction.tsx +0 -13
  85. package/template/src/types/signal.ts +0 -23
  86. /package/template/src/{core → engine/renderers}/route-generator.tsx +0 -0
  87. /package/template/src/{core → engine/types}/sitemap-entry.ts +0 -0
  88. /package/template/src/{pages → features}/GenericContentPage.tsx +0 -0
  89. /package/template/src/{hooks → features/assistant}/useMockChat.ts +0 -0
  90. /package/template/src/{hooks → features/developer}/useDevTools.ts +0 -0
  91. /package/template/src/{pages → features}/styleguide/sections/charts/ChartsSection.tsx +0 -0
  92. /package/template/src/{pages → features}/styleguide/sections/colors/ColorsSection.tsx +0 -0
  93. /package/template/src/{pages → features}/styleguide/sections/elements/ElementsSection.tsx +0 -0
  94. /package/template/src/{pages → features}/styleguide/sections/feedback/FeedbackSection.tsx +0 -0
  95. /package/template/src/{pages → features}/styleguide/sections/forms/FormsSection.tsx +0 -0
  96. /package/template/src/{pages → features}/styleguide/sections/icons/IconsSection.tsx +0 -0
  97. /package/template/src/{pages → features}/styleguide/sections/layout/LayoutSection.tsx +0 -0
  98. /package/template/src/{pages → features}/styleguide/sections/navigation/NavigationSection.tsx +0 -0
  99. /package/template/src/{pages → features}/styleguide/sections/tables/TablesSection.tsx +0 -0
  100. /package/template/src/{pages → features}/styleguide/sections/templates/TemplatesSection.tsx +0 -0
  101. /package/template/src/{pages → features}/styleguide/sections/theming/ThemingSection.tsx +0 -0
  102. /package/template/src/{pages → features}/styleguide/sections/utilities/UtilitiesSection.tsx +0 -0
@@ -1,37 +1,41 @@
1
- // src/data/mock-charts.ts
2
- import type { ChartData } from 'chart.js';
1
+ /**
2
+ * @file mock-charts.ts
3
+ * @description The "Chart Data Lake".
4
+ *
5
+ * ARCHITECTURAL ROLE:
6
+ * This file stores the structured data required by Recharts components.
7
+ * Separating this from the tabular `mockData.ts` prevents bloat and allows
8
+ * for specific optimization of visualization data.
9
+ */
3
10
 
4
- // The centralized registry of "Heavy Data"
5
- export const MOCK_CHART_DATA: Record<string, ChartData<any>> = {
11
+ // 1. Define a generic shape for Recharts data
12
+ // Recharts expects an array of objects: [{ name: 'A', value: 10 }, ...]
13
+ export type RechartsData = Record<string, any>[];
14
+
15
+ export const MOCK_CHART_DATA: Record<string, RechartsData> = {
6
16
 
7
17
  // ID: energy_history
8
- energy_history: {
9
- labels: ["12am", "4am", "8am", "12pm", "4pm", "8pm"],
10
- datasets: [
11
- {
12
- label: "Energy (kWh)",
13
- data: [12, 19, 3, 5, 2, 3],
14
- borderColor: "#3b82f6",
15
- backgroundColor: "rgba(59, 130, 246, 0.5)",
16
- tension: 0.4
17
- }
18
- ]
19
- },
18
+ energy_history: [
19
+ { time: "12am", value: 12 },
20
+ { time: "4am", value: 19 },
21
+ { time: "8am", value: 3 },
22
+ { time: "12pm", value: 5 },
23
+ { time: "4pm", value: 2 },
24
+ { time: "8pm", value: 3 }
25
+ ],
20
26
 
21
27
  // ID: server_load
22
- server_load: {
23
- labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
24
- datasets: [
25
- {
26
- label: "CPU Load %",
27
- data: [45, 52, 38, 70, 65, 30, 40],
28
- borderColor: "#10b981",
29
- backgroundColor: "rgba(16, 185, 129, 0.5)"
30
- }
31
- ]
32
- }
28
+ server_load: [
29
+ { day: "Mon", load: 45 },
30
+ { day: "Tue", load: 52 },
31
+ { day: "Wed", load: 38 },
32
+ { day: "Thu", load: 70 },
33
+ { day: "Fri", load: 65 },
34
+ { day: "Sat", load: 30 },
35
+ { day: "Sun", load: 40 }
36
+ ]
33
37
  };
34
38
 
35
39
  export const getChartData = (id: string) => {
36
- return MOCK_CHART_DATA[id] || null;
40
+ return MOCK_CHART_DATA[id] || [];
37
41
  };
@@ -1,8 +1,23 @@
1
1
  import React from 'react';
2
- import { getComponent } from '../core/component-registry';
2
+ import { getComponent } from '../../config/component-registry';
3
3
  // @ts-ignore
4
- import { useGeneratedSignals } from '../generated/hooks';
5
- import { getMockData } from '../data/mockData';
4
+ import { useGeneratedSignals } from '../runtime/useSignalStore';
5
+ import { getMockData } from '../../data/mockData';
6
+
7
+ /**
8
+ * @file DynamicBlock.tsx
9
+ * @description The "Runtime Hydrator" for the application.
10
+ *
11
+ * ARCHITECTURAL ROLE:
12
+ * This component acts as the bridge between the Abstract Syntax Tree (JSON Manifest)
13
+ * and the concrete React UI.
14
+ *
15
+ * KEY RESPONSIBILITIES:
16
+ * 1. **Component Lookup:** Resolves string types ('StatCard') to actual React components.
17
+ * 2. **Signal Injection:** Subscribes to the real-time Signal Engine and feeds live values to props.
18
+ * 3. **Data Hydration:** Fetches static or async data (users, logs) based on `dataId`.
19
+ * 4. **Status Normalization:** Translates system-level signal states into UI-friendly status colors.
20
+ */
6
21
 
7
22
  const mapSignalStatus = (status: string): string => {
8
23
  switch (status) {
@@ -39,18 +54,23 @@ export const DynamicBlock: React.FC<any> = ({ block }) => {
39
54
  const signalState = signals[signalId];
40
55
 
41
56
  if (signalState) {
42
- // ✅ FIX: Handle both Raw Values (Legacy) and Signal Objects (New)
43
- // If it's an object with a 'value' property, use that. Otherwise use it directly.
44
- const rawValue = (typeof signalState === 'object' && 'value' in signalState)
57
+ // ✅ FIX: Check for null and handle objects vs raw values
58
+ const isSignalObject = typeof signalState === 'object' && signalState !== null;
59
+
60
+ // Extract Value
61
+ const rawValue = (isSignalObject && 'value' in signalState)
45
62
  ? signalState.value
46
63
  : signalState;
47
64
 
48
- const status = (typeof signalState === 'object' && 'status' in signalState)
65
+ // Extract Status
66
+ const rawStatus = (isSignalObject && 'status' in signalState)
49
67
  ? signalState.status
50
68
  : 'fresh'; // Default for raw values
51
69
 
52
70
  dynamicProps.value = typeof rawValue === 'number' ? rawValue : String(rawValue);
53
- dynamicProps.status = mapSignalStatus(status);
71
+
72
+ // ✅ FIX: Explicitly cast to String to satisfy TypeScript
73
+ dynamicProps.status = mapSignalStatus(String(rawStatus));
54
74
  } else {
55
75
  dynamicProps.status = mapSignalStatus('disconnected');
56
76
  }
@@ -0,0 +1,150 @@
1
+ import React, { Component, type ErrorInfo, useMemo } from 'react';
2
+ import { PageHeader, Alert, Badge } from '@ramme-io/ui';
3
+ import { useManifest, useBridgeStatus } from '../runtime/ManifestContext';
4
+ import { getComponent } from '../../config/component-registry';
5
+ import { getMockData } from '../../data/mockData'; // Keep as fallback
6
+ import { Wifi, WifiOff, AlertTriangle, Loader2, Database, Wand2 } from 'lucide-react';
7
+
8
+ // --- 🛠️ JIT DATA GENERATOR ---
9
+ // This ensures the preview ALWAYS has data, even for new resources not yet in the DB.
10
+ const generateJitData = (resourceDef: any, count = 10) => {
11
+ if (!resourceDef) return [];
12
+
13
+ return Array.from({ length: count }).map((_, i) => {
14
+ const row: any = { id: i + 1 };
15
+ resourceDef.fields.forEach((f: any) => {
16
+ // Intelligent Mocking based on field type
17
+ if (f.type === 'status') {
18
+ row[f.key] = ['Active', 'Pending', 'Closed', 'Archived'][Math.floor(Math.random() * 4)];
19
+ } else if (f.type === 'boolean') {
20
+ row[f.key] = Math.random() > 0.3; // 70% true
21
+ } else if (f.type === 'number' || f.type === 'currency') {
22
+ row[f.key] = Math.floor(Math.random() * 1000) + 100;
23
+ } else if (f.type === 'date') {
24
+ const d = new Date();
25
+ d.setDate(d.getDate() - Math.floor(Math.random() * 30));
26
+ row[f.key] = d.toISOString().split('T')[0];
27
+ } else {
28
+ row[f.key] = `${f.label} ${i + 1}`;
29
+ }
30
+ });
31
+ return row;
32
+ });
33
+ };
34
+
35
+ // --- ERROR BOUNDARY ---
36
+ class BlockErrorBoundary extends Component<{ children: React.ReactNode }, { hasError: boolean, error: string }> {
37
+ constructor(props: any) { super(props); this.state = { hasError: false, error: '' }; }
38
+ static getDerivedStateFromError(error: any) { return { hasError: true, error: error.message }; }
39
+ render() {
40
+ if (this.state.hasError) return (
41
+ <div className="h-full min-h-[100px] p-4 border-2 border-dashed border-red-300 bg-red-50/50 rounded-lg flex flex-col items-center justify-center text-red-600 text-xs">
42
+ <AlertTriangle size={16} className="mb-2 opacity-80" />
43
+ <span className="font-bold">Render Error</span>
44
+ <span className="opacity-75 text-center truncate max-w-[200px]">{this.state.error}</span>
45
+ </div>
46
+ );
47
+ return this.props.children;
48
+ }
49
+ }
50
+
51
+ // --- MAIN RENDERER ---
52
+ export const DynamicPage = ({ pageId }: { pageId: string }) => {
53
+ const manifest = useManifest();
54
+ const status = useBridgeStatus();
55
+ const isLive = status === 'live';
56
+
57
+ const page = useMemo(() => manifest.pages?.find((p: any) => p.id === pageId), [manifest, pageId]);
58
+
59
+ if (!page) {
60
+ return (
61
+ <div className="p-8 space-y-4">
62
+ <Alert variant="info" title="Connecting..."><Loader2 className="animate-spin mr-2" size={16} /> Loading Page...</Alert>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ return (
68
+ <div className="space-y-8 animate-in fade-in duration-300 pb-20 p-6 md:p-8">
69
+ {/* Header */}
70
+ <div className="flex flex-col md:flex-row md:items-start justify-between gap-4 border-b border-border pb-6">
71
+ <div>
72
+ <h1 className="text-3xl font-bold tracking-tight text-foreground">{page.title}</h1>
73
+ {page.description && <p className="text-muted-foreground text-lg">{page.description}</p>}
74
+ </div>
75
+ <div className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-bold border ${isLive ? 'bg-green-100 text-green-700 border-green-200' : 'bg-slate-100 text-slate-500'}`}>
76
+ {isLive ? <Wifi size={14} className="text-green-600 animate-pulse"/> : <WifiOff size={14} />}
77
+ {isLive ? 'LIVE PREVIEW' : 'STATIC MODE'}
78
+ </div>
79
+ </div>
80
+
81
+ {/* Grid Layout */}
82
+ <div className="grid gap-8">
83
+ {page.sections?.map((section: any) => (
84
+ <section key={section.id} className="space-y-4">
85
+ {section.title && (
86
+ <div className="flex items-center gap-2 pb-2 border-b border-dashed border-gray-200">
87
+ <h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground">{section.title}</h3>
88
+ </div>
89
+ )}
90
+ <div className="grid gap-6" style={{ gridTemplateColumns: `repeat(${section.layout?.columns || 1}, minmax(0, 1fr))` }}>
91
+ {section.blocks.map((block: any) => {
92
+ const Component = getComponent(block.type);
93
+ const safeDataId = block.props.dataId?.toLowerCase();
94
+
95
+ // --- 🛡️ HYBRID DATA STRATEGY ---
96
+ let resolvedData: any[] = [];
97
+ let isGenerated = false;
98
+ let autoColumns = undefined;
99
+
100
+ if (safeDataId) {
101
+ // 1. Try Local Storage first
102
+ resolvedData = getMockData(safeDataId);
103
+
104
+ // 2. If empty, Generate JIT Data from Manifest
105
+ if (!resolvedData || resolvedData.length === 0) {
106
+ const resourceDef = manifest.resources?.find((r: any) => r.id.toLowerCase() === safeDataId);
107
+ if (resourceDef) {
108
+ resolvedData = generateJitData(resourceDef);
109
+ isGenerated = true; // Flag for debug overlay
110
+ }
111
+ }
112
+
113
+ // 3. Auto-Generate Table Columns
114
+ const resourceDef = manifest.resources?.find((r: any) => r.id.toLowerCase() === safeDataId);
115
+ if (resourceDef && !block.props.columnDefs) {
116
+ autoColumns = resourceDef.fields.map((f: any) => ({
117
+ field: f.key, headerName: f.label, filter: true, flex: 1,
118
+ cellRenderer: f.type === 'status' ? (p: any) => <Badge variant="secondary">{p.value}</Badge> : undefined
119
+ }));
120
+ }
121
+ }
122
+
123
+ return (
124
+ <div key={block.id} style={{ gridColumn: `span ${block.layout?.colSpan || 1}`, gridRow: `span ${block.layout?.rowSpan || 1}` }} className="relative group">
125
+ <BlockErrorBoundary>
126
+ {/* Debug Overlay */}
127
+ <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-20 flex gap-1 pointer-events-none">
128
+ <div className={`text-white text-[10px] px-2 py-1 rounded backdrop-blur-md flex items-center gap-1 ${isGenerated ? 'bg-amber-600/90' : 'bg-black/80'}`}>
129
+ {block.type}
130
+ {safeDataId && <> <span className="opacity-50">|</span> {isGenerated ? <Wand2 size={10}/> : <Database size={10}/>} {safeDataId} ({resolvedData.length})</>}
131
+ </div>
132
+ </div>
133
+
134
+ <Component
135
+ {...block.props}
136
+ rowData={resolvedData}
137
+ columnDefs={block.props.columnDefs || autoColumns}
138
+ className="w-full h-full shadow-sm bg-card rounded-xl border border-border"
139
+ />
140
+ </BlockErrorBoundary>
141
+ </div>
142
+ );
143
+ })}
144
+ </div>
145
+ </section>
146
+ ))}
147
+ </div>
148
+ </div>
149
+ );
150
+ };
@@ -0,0 +1,79 @@
1
+ import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
2
+ // ✅ FIX 1: Correct Path to Static Manifest
3
+ import { appManifest as staticManifest } from '../../config/app.manifest';
4
+ // ✅ FIX 2: Correct Path to Types
5
+ import type { AppSpecification } from '../validation/schema';
6
+
7
+ interface ManifestContextType {
8
+ manifest: AppSpecification;
9
+ isLive: boolean;
10
+ }
11
+
12
+ const ManifestContext = createContext<ManifestContextType>({
13
+ manifest: staticManifest,
14
+ isLive: false
15
+ });
16
+
17
+ export const ManifestProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
18
+ // 1. Initialize State
19
+ const [manifest, setManifest] = useState<AppSpecification>(staticManifest);
20
+ const [isLive, setIsLive] = useState(false);
21
+
22
+ // 2. Global Listener Guard (Refs persist across re-renders)
23
+ const isListening = useRef(false);
24
+
25
+ useEffect(() => {
26
+ // Prevent double-attaching listeners in StrictMode
27
+ if (isListening.current) return;
28
+ isListening.current = true;
29
+
30
+ // A. Define the Listener
31
+ const handleMessage = (event: MessageEvent) => {
32
+ // Security: Filter out noise, listen only for our specific event
33
+ if (event.data?.type === 'RAMME_SYNC_MANIFEST') {
34
+ const payload = event.data.payload;
35
+ // console.log("⚡️ [Context] Sync Received");
36
+ setManifest(payload);
37
+ setIsLive(true);
38
+ }
39
+ };
40
+
41
+ // B. Attach Listener
42
+ window.addEventListener('message', handleMessage);
43
+
44
+ // C. Send Handshake (ONCE per session)
45
+ // We check a global window property to ensure we never spam the parent,
46
+ // even if this Provider is unmounted and remounted by React Router.
47
+ if (window.parent !== window && !(window as any).__RAMME_HANDSHAKE_SENT) {
48
+ console.log("🔌 [Context] Handshake Sent 🤝");
49
+ window.parent.postMessage({ type: 'RAMME_CLIENT_READY' }, '*');
50
+ (window as any).__RAMME_HANDSHAKE_SENT = true;
51
+ }
52
+
53
+ // Cleanup
54
+ return () => {
55
+ window.removeEventListener('message', handleMessage);
56
+ isListening.current = false;
57
+ };
58
+ }, []);
59
+
60
+ return (
61
+ <ManifestContext.Provider value={{ manifest, isLive }}>
62
+ {children}
63
+ </ManifestContext.Provider>
64
+ );
65
+ };
66
+
67
+ // --- CONSUMER HOOKS ---
68
+
69
+ export const useManifest = () => {
70
+ const context = useContext(ManifestContext);
71
+ if (!context) return staticManifest;
72
+ return context.manifest;
73
+ };
74
+
75
+ // Returns 'live' | 'static' string to match your UI components
76
+ export const useBridgeStatus = () => {
77
+ const context = useContext(ManifestContext);
78
+ return context?.isLive ? 'live' : 'static';
79
+ };
@@ -1,5 +1,8 @@
1
1
  import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
2
2
  import mqtt, { type MqttClient } from 'mqtt';
3
+ // ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
4
+ // ✅ ADDED: Live Context
5
+ import { useManifest } from './ManifestContext';
3
6
 
4
7
  interface MqttContextType {
5
8
  isConnected: boolean;
@@ -11,35 +14,42 @@ interface MqttContextType {
11
14
 
12
15
  const MqttContext = createContext<MqttContextType | null>(null);
13
16
 
14
- // Public Test Broker for "Out of the Box" functionality
15
- // In a real app, this comes from config.ts
16
- const DEFAULT_BROKER = 'wss://test.mosquitto.org:8081';
17
-
18
17
  export const MqttProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
19
18
  const [isConnected, setIsConnected] = useState(false);
20
19
  const [lastMessage, setLastMessage] = useState<Record<string, string>>({});
21
20
  const clientRef = useRef<MqttClient | null>(null);
22
-
23
- // Track active subscriptions to avoid double-subscribing
24
21
  const subscriptions = useRef<Set<string>>(new Set());
25
22
 
23
+ // ✅ 1. Consume Live Manifest
24
+ const appManifest = useManifest();
25
+
26
26
  useEffect(() => {
27
- console.log(`[MQTT] Connecting to ${DEFAULT_BROKER}...`);
27
+ // ✅ 2. Hot-Swap Broker Connection
28
+ // If you change the Broker URL in the Builder, this effect will re-run!
29
+ const brokerUrl = appManifest.config.brokerUrl || 'wss://test.mosquitto.org:8081';
30
+ console.log(`[MQTT] Connecting to ${brokerUrl}...`);
28
31
 
29
- const client = mqtt.connect(DEFAULT_BROKER);
32
+ // Disconnect previous if exists
33
+ if (clientRef.current) {
34
+ clientRef.current.end();
35
+ }
36
+
37
+ const client = mqtt.connect(brokerUrl);
30
38
  clientRef.current = client;
31
39
 
32
40
  client.on('connect', () => {
33
41
  console.log('[MQTT] Connected ✅');
34
42
  setIsConnected(true);
43
+ // Re-subscribe to previous topics if needed
44
+ subscriptions.current.forEach(t => client.subscribe(t));
35
45
  });
36
46
 
37
- client.on('message', (topic: any, message: { toString: () => any; }) => {
38
- const payload = message.toString();
39
- setLastMessage((prev) => ({ ...prev, [topic]: payload }));
47
+ client.on('message', (topic: string, payload: Buffer) => {
48
+ const messageStr = payload.toString();
49
+ setLastMessage((prev) => ({ ...prev, [topic]: messageStr }));
40
50
  });
41
51
 
42
- client.on('error', (err: any) => {
52
+ client.on('error', (err) => {
43
53
  console.error('[MQTT] Connection error: ', err);
44
54
  client.end();
45
55
  });
@@ -48,11 +58,10 @@ export const MqttProvider: React.FC<{ children: React.ReactNode }> = ({ children
48
58
  console.log('[MQTT] Disconnecting...');
49
59
  client.end();
50
60
  };
51
- }, []);
61
+ }, [appManifest.config.brokerUrl]); // Only re-connect if URL changes
52
62
 
53
63
  const subscribe = (topic: string) => {
54
64
  if (clientRef.current && !subscriptions.current.has(topic)) {
55
- console.log(`[MQTT] Subscribing to: ${topic}`);
56
65
  clientRef.current.subscribe(topic);
57
66
  subscriptions.current.add(topic);
58
67
  }
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { createContext, useContext } from 'react';
22
22
  // Import the canonical "schema" for a sitemap entry
23
- import type { SitemapEntry } from '../core/sitemap-entry';
23
+ import type { SitemapEntry } from '../types/sitemap-entry';
24
24
 
25
25
  // 1. Create the context
26
26
  // We initialize it with an empty array as a safe default.
@@ -0,0 +1,47 @@
1
+ // ✅ Match the export from your new mockData.ts
2
+ import { DATA_REGISTRY } from '../../data/mockData';
3
+
4
+ /**
5
+ * @file data-seeder.ts
6
+ * @description The "Big Bang" for the client-side database.
7
+ * * ARCHITECTURAL ROLE:
8
+ * This utility ensures that the LocalStorage "Data Lake" is never empty.
9
+ * On app launch, it checks if data exists. If not, it writes the seed data
10
+ * from `mockData.ts` into the browser's storage.
11
+ */
12
+
13
+ // 🔒 SHARED CONSTANT: Ensure everyone uses the same key format
14
+ const DB_PREFIX = 'ramme_mock_';
15
+
16
+ export const initializeDataLake = () => {
17
+ if (typeof window === 'undefined') return;
18
+
19
+ console.groupCollapsed('🌊 [Data Lake] Initialization');
20
+
21
+ Object.entries(DATA_REGISTRY).forEach(([key, seedData]) => {
22
+ // ✅ FIX: Use the prefix so getMockData() can find it
23
+ const storageKey = `${DB_PREFIX}${key}`;
24
+ const existing = localStorage.getItem(storageKey);
25
+
26
+ if (!existing) {
27
+ console.log(`✨ Seeding collection: ${key} (${(seedData as any[]).length} records)`);
28
+ localStorage.setItem(storageKey, JSON.stringify(seedData));
29
+ } else {
30
+ console.log(`✅ Collection exists: ${key}`);
31
+ }
32
+ });
33
+
34
+ console.groupEnd();
35
+ };
36
+
37
+ /**
38
+ * Utility to clear the lake (useful for a "Reset Data" button)
39
+ */
40
+ export const resetDataLake = () => {
41
+ Object.keys(DATA_REGISTRY).forEach((key) => {
42
+ const storageKey = `${DB_PREFIX}${key}`;
43
+ localStorage.removeItem(storageKey);
44
+ });
45
+ console.log("🔥 Data Lake Evaporated (Cleared)");
46
+ window.location.reload();
47
+ };
@@ -1,21 +1,25 @@
1
- // src/hooks/useAction.ts
2
1
  import { useCallback } from 'react';
3
- import { useMqtt } from '../contexts/MqttContext';
4
- import { appManifest } from '../config/app.manifest';
2
+ import { useMqtt } from './MqttContext';
3
+ // ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
4
+ // ✅ ADDED: Live Context
5
+ import { useManifest } from './ManifestContext';
5
6
 
6
7
  export const useAction = () => {
7
8
  const { publish, isConnected } = useMqtt();
9
+
10
+ // ✅ 1. Consume Live Manifest
11
+ const appManifest = useManifest();
8
12
  const { config, domain } = appManifest;
9
13
 
10
14
  const sendAction = useCallback(async (entityId: string, value: any) => {
11
- // 1. Find the Entity definition
15
+ // 2. Find the Entity definition (Live)
12
16
  const entity = domain.entities.find(e => e.id === entityId);
17
+
13
18
  if (!entity) {
14
- console.warn(`[Action] Entity not found: ${entityId}`);
19
+ console.warn(`[Action] Entity ID '${entityId}' not found in manifest.`);
15
20
  return;
16
21
  }
17
22
 
18
- // 2. Find the Primary Signal (The target of the action)
19
23
  const signalId = entity.signals[0];
20
24
  const signal = domain.signals.find(s => s.id === signalId);
21
25
 
@@ -24,12 +28,9 @@ export const useAction = () => {
24
28
  return;
25
29
  }
26
30
 
27
- // 3. EXECUTE based on Mode & Source
28
-
29
31
  // --- Mock Mode ---
30
32
  if (config.mockMode) {
31
33
  console.log(`%c[Mock Action] Setting ${entity.name} to:`, 'color: #10b981; font-weight: bold;', value);
32
- // In a real app, we would update the local cache optimistically here
33
34
  return;
34
35
  }
35
36
 
@@ -46,9 +47,6 @@ export const useAction = () => {
46
47
 
47
48
  // --- Live Mode (HTTP) ---
48
49
  if (signal.source === 'http' && signal.endpoint) {
49
- console.log(`[HTTP] POST to ${signal.endpoint}:`, value);
50
- // Note: This will likely fail (404/405) against a static .json file,
51
- // but this is the correct code for a real API.
52
50
  try {
53
51
  await fetch(signal.endpoint, {
54
52
  method: 'POST',
@@ -56,8 +54,7 @@ export const useAction = () => {
56
54
  body: JSON.stringify({ id: signal.id, value })
57
55
  });
58
56
  } catch (err) {
59
- // We expect this to fail on the static demo, so we suppress the error alert
60
- console.log('[HTTP] (Simulation) Request sent.');
57
+ console.log('[HTTP] (Simulation) Request sent.');
61
58
  }
62
59
  }
63
60
 
@@ -1,10 +1,12 @@
1
1
  import { useState, useCallback } from 'react';
2
2
 
3
3
  /**
4
- * A generic hook for performing CRUD (Create, Read, Update, Delete)
4
+ * @hook useCrudLocalStorage
5
+ * @description A generic hook for performing CRUD (Create, Read, Update, Delete)
5
6
  * operations on an array of items stored in the browser's localStorage.
7
+ * * It serves as the "Database Engine" for the mock runtime.
6
8
  *
7
- * @param storageKey The unique key for this data in localStorage.
9
+ * @param storageKey The unique key for this data in localStorage (e.g., 'ramme_db_users').
8
10
  * @param initialData The default data to seed localStorage with if it's empty.
9
11
  * @returns An object with the current data and functions to manipulate it.
10
12
  */
@@ -12,28 +14,43 @@ export const useCrudLocalStorage = <T extends { id: any }>(
12
14
  storageKey: string,
13
15
  initialData: T[]
14
16
  ) => {
17
+ // 1. Initialize State from LocalStorage
15
18
  const [data, setData] = useState<T[]>(() => {
19
+ // Safety check for Server-Side Rendering
20
+ if (typeof window === 'undefined') return initialData;
21
+
16
22
  try {
17
23
  const item = window.localStorage.getItem(storageKey);
18
24
  if (item) {
19
- // If data exists in localStorage, parse and return it.
20
25
  return JSON.parse(item);
21
26
  } else {
22
- // Otherwise, seed localStorage with the initial data.
27
+ // Seed the "DB" if empty
23
28
  window.localStorage.setItem(storageKey, JSON.stringify(initialData));
24
29
  return initialData;
25
30
  }
26
31
  } catch (error) {
27
- console.error("Error reading from localStorage", error);
32
+ console.error(`[Data Lake] Error reading key "${storageKey}":`, error);
28
33
  return initialData;
29
34
  }
30
35
  });
31
36
 
37
+ // 2. CREATE
32
38
  const createItem = useCallback((newItem: Omit<T, 'id'>) => {
33
39
  setData(prevData => {
34
- // Find the highest existing ID to safely create a new one.
35
- const maxId = prevData.reduce((max, item) => (item.id > max ? item.id : max), 0);
36
- const fullNewItem = { ...newItem, id: maxId + 1 } as T;
40
+ // ROBUST ID GENERATION (Zero Jank)
41
+ // Detects if existing IDs are Numbers or Strings to prevent type conflicts.
42
+ let newId: any;
43
+ const isNumeric = prevData.length > 0 && typeof prevData[0].id === 'number';
44
+
45
+ if (isNumeric) {
46
+ const maxId = prevData.reduce((max, item) => (typeof item.id === 'number' && item.id > max ? item.id : max), 0);
47
+ newId = maxId + 1;
48
+ } else {
49
+ // Fallback for string IDs (e.g. 'usr_17354...')
50
+ newId = `id_${Date.now()}`;
51
+ }
52
+
53
+ const fullNewItem = { ...newItem, id: newId } as T;
37
54
 
38
55
  const updatedData = [...prevData, fullNewItem];
39
56
  window.localStorage.setItem(storageKey, JSON.stringify(updatedData));
@@ -41,6 +58,7 @@ export const useCrudLocalStorage = <T extends { id: any }>(
41
58
  });
42
59
  }, [storageKey]);
43
60
 
61
+ // 3. UPDATE
44
62
  const updateItem = useCallback((updatedItem: T) => {
45
63
  setData(prevData => {
46
64
  const updatedData = prevData.map(item =>
@@ -51,6 +69,7 @@ export const useCrudLocalStorage = <T extends { id: any }>(
51
69
  });
52
70
  }, [storageKey]);
53
71
 
72
+ // 4. DELETE
54
73
  const deleteItem = useCallback((id: T['id']) => {
55
74
  setData(prevData => {
56
75
  const updatedData = prevData.filter(item => item.id !== id);
@@ -1,6 +1,20 @@
1
1
  import { useMemo } from 'react';
2
2
 
3
- // --- 1. Export the Missing Types (Fixes ts(2305)) ---
3
+ /**
4
+ * @file useDataQuery.ts
5
+ * @description The "In-Memory Database Engine".
6
+ *
7
+ * ARCHITECTURAL ROLE:
8
+ * Since this application runs without a real backend, this hook acts as the
9
+ * SQL Query Engine. It takes raw arrays from the Data Lake and performs
10
+ * real-time filtering, sorting, and pagination before passing the result
11
+ * to the UI.
12
+ *
13
+ * CAPABILITIES:
14
+ * 1. WHERE: Supports complex filtering (equals, contains, gt, lt).
15
+ * 2. ORDER BY: Handles ascending/descending sorts on any field.
16
+ * 3. LIMIT/OFFSET: Calculates pagination slices automatically.
17
+ */
4
18
 
5
19
  export type SortDirection = 'asc' | 'desc';
6
20