@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.
- package/package.json +9 -15
- package/template/package.json +41 -0
- package/template/pkg.json +9 -7
- package/template/src/App.tsx +72 -31
- package/template/src/components/AIChatWidget.tsx +2 -2
- package/template/src/components/AppHeader.tsx +12 -12
- package/template/src/components/AutoForm.tsx +13 -0
- package/template/src/{pages/styleguide → components}/NotFound.tsx +1 -1
- package/template/src/components/PageTitleUpdater.tsx +2 -2
- package/template/src/components/ProtectedRoute.tsx +18 -1
- package/template/src/components/ScrollToTop.tsx +19 -0
- package/template/src/components/dashboard/ChartLine.tsx +28 -0
- package/template/src/components/dashboard/StatCard.tsx +61 -0
- package/template/src/config/app.manifest.ts +3 -1
- package/template/src/config/component-registry.tsx +69 -0
- package/template/src/config/navigation.ts +1 -1
- package/template/src/data/mock-charts.ts +32 -28
- package/template/src/{components → engine/renderers}/DynamicBlock.tsx +28 -8
- package/template/src/engine/renderers/DynamicPage.tsx +150 -0
- package/template/src/engine/runtime/ManifestContext.tsx +79 -0
- package/template/src/{contexts → engine/runtime}/MqttContext.tsx +23 -14
- package/template/src/{contexts → engine/runtime}/SitemapContext.tsx +1 -1
- package/template/src/engine/runtime/data-seeder.ts +47 -0
- package/template/src/{hooks → engine/runtime}/useAction.ts +11 -14
- package/template/src/{hooks → engine/runtime}/useCrudLocalStorage.ts +27 -8
- package/template/src/{hooks → engine/runtime}/useDataQuery.ts +15 -1
- package/template/src/engine/runtime/useDynamicSitemap.tsx +43 -0
- package/template/src/engine/runtime/useJustInTimeSeeder.ts +76 -0
- package/template/src/engine/runtime/useLiveBridge.ts +44 -0
- package/template/src/engine/runtime/useSignal.ts +40 -0
- package/template/src/{generated/hooks.ts → engine/runtime/useSignalStore.ts} +35 -8
- package/template/src/engine/runtime/useWorkflowEngine.ts +89 -0
- package/template/src/{core → engine/types}/manifest-types.ts +35 -3
- package/template/src/{types → engine/validation}/schema.ts +17 -0
- package/template/src/{pages → features/ai/pages}/AiChat.tsx +1 -1
- package/template/src/features/auth/AuthContext.tsx +118 -0
- package/template/src/features/auth/pages/AuthLayout.tsx +55 -0
- package/template/src/features/auth/pages/LoginPage.tsx +106 -0
- package/template/src/features/auth/pages/SignupPage.tsx +96 -0
- package/template/src/{blocks → features/datagrid}/SmartTable.tsx +41 -25
- package/template/src/features/developer/GhostOverlay.tsx +114 -0
- package/template/src/{pages → features/onboarding/pages}/Welcome.tsx +0 -1
- package/template/src/features/overview/index.ts +1 -0
- package/template/src/features/overview/pages/OverviewPage.tsx +127 -0
- package/template/src/{pages → features/playground/pages}/AccountingLedgerPage.tsx +1 -1
- package/template/src/{pages/prototypes → features/playground/pages}/ItemSelectorPage.tsx +1 -1
- package/template/src/{pages/settings → features/settings/pages}/BillingPage.tsx +1 -1
- package/template/src/features/settings/pages/ProfilePage.tsx +153 -0
- package/template/src/{pages/settings → features/settings/pages}/TeamPage.tsx +1 -1
- package/template/src/features/styleguide/Styleguide.tsx +75 -0
- package/template/src/features/users/components/UserDrawer.tsx +138 -0
- package/template/src/features/users/index.ts +2 -0
- package/template/src/features/users/pages/UsersPage.tsx +151 -0
- package/template/src/features/visualizations/SmartChart.tsx +178 -0
- package/template/src/index.css +1 -1
- package/template/src/main.tsx +27 -15
- package/template/src/templates/dashboard/DashboardLayout.tsx +77 -107
- package/template/src/templates/dashboard/dashboard.sitemap.ts +19 -22
- package/template/src/templates/docs/DocsLayout.tsx +49 -38
- package/template/src/templates/docs/docs.sitemap.ts +22 -34
- package/template/src/templates/settings/SettingsLayout.tsx +83 -143
- package/template/src/templates/settings/settings.sitemap.ts +6 -6
- package/template/tailwind.config.cjs +10 -9
- package/template/vite.config.ts +0 -11
- package/template/src/adaptors/.gitkeep +0 -0
- package/template/src/components/LocalSideNav.tsx +0 -120
- package/template/src/components/PageWithSideNav.tsx +0 -69
- package/template/src/components/dev/GhostOverlay.tsx +0 -68
- package/template/src/config/dashboard.layout.ts +0 -110
- package/template/src/contexts/AuthContext.tsx +0 -64
- package/template/src/core/component-registry.tsx +0 -56
- package/template/src/core/data-seeder.ts +0 -35
- package/template/src/data/mockUsers.ts +0 -18
- package/template/src/hooks/useSignal.ts +0 -83
- package/template/src/hooks/useWorkflowEngine.ts +0 -123
- package/template/src/layouts/DataLayout.tsx +0 -37
- package/template/src/layouts/SideNavLayout.tsx +0 -28
- package/template/src/pages/Dashboard.tsx +0 -60
- package/template/src/pages/DataGridPage.tsx +0 -184
- package/template/src/pages/DynamicPage.tsx +0 -95
- package/template/src/pages/LoginPage.tsx +0 -58
- package/template/src/pages/settings/ProfilePage.tsx +0 -10
- package/template/src/pages/styleguide/Styleguide.tsx +0 -40
- package/template/src/templates/docs/pages/Introduction.tsx +0 -13
- package/template/src/types/signal.ts +0 -23
- /package/template/src/{core → engine/renderers}/route-generator.tsx +0 -0
- /package/template/src/{core → engine/types}/sitemap-entry.ts +0 -0
- /package/template/src/{pages → features}/GenericContentPage.tsx +0 -0
- /package/template/src/{hooks → features/assistant}/useMockChat.ts +0 -0
- /package/template/src/{hooks → features/developer}/useDevTools.ts +0 -0
- /package/template/src/{pages → features}/styleguide/sections/charts/ChartsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/colors/ColorsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/elements/ElementsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/feedback/FeedbackSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/forms/FormsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/icons/IconsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/layout/LayoutSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/navigation/NavigationSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/tables/TablesSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/templates/TemplatesSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/theming/ThemingSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/utilities/UtilitiesSection.tsx +0 -0
|
@@ -1,37 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
//
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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] ||
|
|
40
|
+
return MOCK_CHART_DATA[id] || [];
|
|
37
41
|
};
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { getComponent } from '
|
|
2
|
+
import { getComponent } from '../../config/component-registry';
|
|
3
3
|
// @ts-ignore
|
|
4
|
-
import { useGeneratedSignals } from '../
|
|
5
|
-
import { getMockData } from '
|
|
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:
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
38
|
-
const
|
|
39
|
-
setLastMessage((prev) => ({ ...prev, [topic]:
|
|
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
|
|
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 '../
|
|
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 '
|
|
4
|
-
import { appManifest } from '
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|