@ramme-io/create-app 1.2.2 → 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 -14
- package/template/package.json +3 -3
- package/template/pkg.json +9 -7
- package/template/src/App.tsx +11 -1
- package/template/src/components/AppHeader.tsx +10 -10
- package/template/src/components/dashboard/ChartLine.tsx +28 -0
- package/template/src/components/dashboard/StatCard.tsx +61 -0
- package/template/src/config/component-registry.tsx +57 -44
- package/template/src/engine/renderers/DynamicBlock.tsx +1 -1
- package/template/src/engine/renderers/DynamicPage.tsx +134 -98
- package/template/src/engine/runtime/ManifestContext.tsx +79 -0
- package/template/src/engine/runtime/MqttContext.tsx +16 -21
- package/template/src/engine/runtime/data-seeder.ts +9 -7
- package/template/src/engine/runtime/useAction.ts +7 -21
- 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 +10 -21
- package/template/src/engine/runtime/useWorkflowEngine.ts +23 -78
- package/template/src/features/datagrid/SmartTable.tsx +38 -20
- package/template/src/features/developer/GhostOverlay.tsx +67 -21
- package/template/src/features/visualizations/SmartChart.tsx +178 -0
- package/template/src/main.tsx +25 -13
- package/template/src/templates/dashboard/DashboardLayout.tsx +19 -18
- package/template/src/templates/dashboard/dashboard.sitemap.ts +1 -8
- package/template/tailwind.config.cjs +10 -9
- package/template/vite.config.ts +0 -14
|
@@ -1,114 +1,150 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { PageHeader, Alert,
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import { DynamicBlock } from './DynamicBlock';
|
|
8
|
-
// Import the Hook to control the overlay
|
|
9
|
-
import { useDevTools } from '../../features/developer/useDevTools';
|
|
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';
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
};
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
51
|
+
// --- MAIN RENDERER ---
|
|
52
|
+
export const DynamicPage = ({ pageId }: { pageId: string }) => {
|
|
53
|
+
const manifest = useManifest();
|
|
54
|
+
const status = useBridgeStatus();
|
|
55
|
+
const isLive = status === 'live';
|
|
37
56
|
|
|
38
|
-
|
|
39
|
-
const pageConfig = (appManifest as any).pages?.find((p: any) => p.id === pageId);
|
|
57
|
+
const page = useMemo(() => manifest.pages?.find((p: any) => p.id === pageId), [manifest, pageId]);
|
|
40
58
|
|
|
41
|
-
if (!
|
|
59
|
+
if (!page) {
|
|
42
60
|
return (
|
|
43
|
-
<div className="p-8">
|
|
44
|
-
|
|
45
|
-
The manifest does not contain a page with ID: <code>{pageId}</code>.
|
|
46
|
-
</Alert>
|
|
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>
|
|
47
63
|
</div>
|
|
48
64
|
);
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
return (
|
|
52
|
-
<div className="space-y-8 fade-in p-6">
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
|
68
99
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
>
|
|
82
|
-
{section.blocks.map((block: any) => {
|
|
83
|
-
// ⚡️ IMPROVEMENT: Calculate Spanning
|
|
84
|
-
// Allows a block to stretch across multiple columns if needed.
|
|
85
|
-
const colSpan = block.layout?.colSpan || 1;
|
|
86
|
-
const rowSpan = block.layout?.rowSpan || 1;
|
|
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
|
+
}
|
|
87
112
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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>
|
|
110
148
|
</div>
|
|
111
149
|
);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export default DynamicPage;
|
|
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,6 +1,8 @@
|
|
|
1
1
|
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import mqtt, { type MqttClient } from 'mqtt';
|
|
3
|
-
import { appManifest } from '../../config/app.manifest';
|
|
3
|
+
// ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
|
|
4
|
+
// ✅ ADDED: Live Context
|
|
5
|
+
import { useManifest } from './ManifestContext';
|
|
4
6
|
|
|
5
7
|
interface MqttContextType {
|
|
6
8
|
isConnected: boolean;
|
|
@@ -12,44 +14,38 @@ interface MqttContextType {
|
|
|
12
14
|
|
|
13
15
|
const MqttContext = createContext<MqttContextType | null>(null);
|
|
14
16
|
|
|
15
|
-
/**
|
|
16
|
-
* @file MqttContext.tsx
|
|
17
|
-
* @description The Real-Time Connectivity Layer.
|
|
18
|
-
*
|
|
19
|
-
* ARCHITECTURAL ROLE:
|
|
20
|
-
* This provider establishes a persistent WebSocket connection to the MQTT Broker
|
|
21
|
-
* defined in `app.manifest.ts`.
|
|
22
|
-
*
|
|
23
|
-
* KEY FEATURES:
|
|
24
|
-
* 1. **Global Connection:** Maintains one connection for the whole app (Singleton pattern).
|
|
25
|
-
* 2. **Topic Management:** specific components (like DeviceCard) can subscribe to
|
|
26
|
-
* specific topics on demand using `subscribe()`.
|
|
27
|
-
* 3. **State Distribution:** Broadcasts the latest messages to any component using `useMqtt()`.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
17
|
export const MqttProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
31
18
|
const [isConnected, setIsConnected] = useState(false);
|
|
32
19
|
const [lastMessage, setLastMessage] = useState<Record<string, string>>({});
|
|
33
20
|
const clientRef = useRef<MqttClient | null>(null);
|
|
34
21
|
const subscriptions = useRef<Set<string>>(new Set());
|
|
35
22
|
|
|
23
|
+
// ✅ 1. Consume Live Manifest
|
|
24
|
+
const appManifest = useManifest();
|
|
25
|
+
|
|
36
26
|
useEffect(() => {
|
|
37
|
-
// ✅
|
|
27
|
+
// ✅ 2. Hot-Swap Broker Connection
|
|
28
|
+
// If you change the Broker URL in the Builder, this effect will re-run!
|
|
38
29
|
const brokerUrl = appManifest.config.brokerUrl || 'wss://test.mosquitto.org:8081';
|
|
39
30
|
console.log(`[MQTT] Connecting to ${brokerUrl}...`);
|
|
40
31
|
|
|
32
|
+
// Disconnect previous if exists
|
|
33
|
+
if (clientRef.current) {
|
|
34
|
+
clientRef.current.end();
|
|
35
|
+
}
|
|
36
|
+
|
|
41
37
|
const client = mqtt.connect(brokerUrl);
|
|
42
38
|
clientRef.current = client;
|
|
43
39
|
|
|
44
40
|
client.on('connect', () => {
|
|
45
41
|
console.log('[MQTT] Connected ✅');
|
|
46
42
|
setIsConnected(true);
|
|
43
|
+
// Re-subscribe to previous topics if needed
|
|
44
|
+
subscriptions.current.forEach(t => client.subscribe(t));
|
|
47
45
|
});
|
|
48
46
|
|
|
49
|
-
// ✅ FIX: Proper typing for MQTT payload (Buffer)
|
|
50
47
|
client.on('message', (topic: string, payload: Buffer) => {
|
|
51
48
|
const messageStr = payload.toString();
|
|
52
|
-
// console.log(`[MQTT] Msg: ${topic} -> ${messageStr}`); // Optional debug
|
|
53
49
|
setLastMessage((prev) => ({ ...prev, [topic]: messageStr }));
|
|
54
50
|
});
|
|
55
51
|
|
|
@@ -62,11 +58,10 @@ export const MqttProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|
|
62
58
|
console.log('[MQTT] Disconnecting...');
|
|
63
59
|
client.end();
|
|
64
60
|
};
|
|
65
|
-
}, []);
|
|
61
|
+
}, [appManifest.config.brokerUrl]); // Only re-connect if URL changes
|
|
66
62
|
|
|
67
63
|
const subscribe = (topic: string) => {
|
|
68
64
|
if (clientRef.current && !subscriptions.current.has(topic)) {
|
|
69
|
-
console.log(`[MQTT] Subscribing to: ${topic}`);
|
|
70
65
|
clientRef.current.subscribe(topic);
|
|
71
66
|
subscriptions.current.add(topic);
|
|
72
67
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// src/core/data-seeder.ts
|
|
2
|
-
|
|
3
1
|
// ✅ Match the export from your new mockData.ts
|
|
4
2
|
import { DATA_REGISTRY } from '../../data/mockData';
|
|
5
3
|
|
|
@@ -10,21 +8,23 @@ import { DATA_REGISTRY } from '../../data/mockData';
|
|
|
10
8
|
* This utility ensures that the LocalStorage "Data Lake" is never empty.
|
|
11
9
|
* On app launch, it checks if data exists. If not, it writes the seed data
|
|
12
10
|
* from `mockData.ts` into the browser's storage.
|
|
13
|
-
* * This allows the app to feel "alive" with data immediately after installation.
|
|
14
11
|
*/
|
|
15
12
|
|
|
13
|
+
// 🔒 SHARED CONSTANT: Ensure everyone uses the same key format
|
|
14
|
+
const DB_PREFIX = 'ramme_mock_';
|
|
15
|
+
|
|
16
16
|
export const initializeDataLake = () => {
|
|
17
17
|
if (typeof window === 'undefined') return;
|
|
18
18
|
|
|
19
19
|
console.groupCollapsed('🌊 [Data Lake] Initialization');
|
|
20
20
|
|
|
21
21
|
Object.entries(DATA_REGISTRY).forEach(([key, seedData]) => {
|
|
22
|
-
//
|
|
23
|
-
const storageKey = key
|
|
22
|
+
// ✅ FIX: Use the prefix so getMockData() can find it
|
|
23
|
+
const storageKey = `${DB_PREFIX}${key}`;
|
|
24
24
|
const existing = localStorage.getItem(storageKey);
|
|
25
25
|
|
|
26
26
|
if (!existing) {
|
|
27
|
-
console.log(`✨ Seeding collection: ${key} (${seedData.length} records)`);
|
|
27
|
+
console.log(`✨ Seeding collection: ${key} (${(seedData as any[]).length} records)`);
|
|
28
28
|
localStorage.setItem(storageKey, JSON.stringify(seedData));
|
|
29
29
|
} else {
|
|
30
30
|
console.log(`✅ Collection exists: ${key}`);
|
|
@@ -39,7 +39,9 @@ export const initializeDataLake = () => {
|
|
|
39
39
|
*/
|
|
40
40
|
export const resetDataLake = () => {
|
|
41
41
|
Object.keys(DATA_REGISTRY).forEach((key) => {
|
|
42
|
-
|
|
42
|
+
const storageKey = `${DB_PREFIX}${key}`;
|
|
43
|
+
localStorage.removeItem(storageKey);
|
|
43
44
|
});
|
|
45
|
+
console.log("🔥 Data Lake Evaporated (Cleared)");
|
|
44
46
|
window.location.reload();
|
|
45
47
|
};
|
|
@@ -1,36 +1,25 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { useMqtt } from './MqttContext';
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @file useAction.ts
|
|
8
|
-
* @description The "Effectuator" hook.
|
|
9
|
-
*
|
|
10
|
-
* ARCHITECTURAL ROLE:
|
|
11
|
-
* This hook handles outgoing commands from the UI. It abstracts the transport layer,
|
|
12
|
-
* automatically routing actions to the correct destination (MQTT, HTTP, or Mock Console)
|
|
13
|
-
* based on the entity's configuration in the manifest.
|
|
14
|
-
*/
|
|
3
|
+
// ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
|
|
4
|
+
// ✅ ADDED: Live Context
|
|
5
|
+
import { useManifest } from './ManifestContext';
|
|
15
6
|
|
|
16
7
|
export const useAction = () => {
|
|
17
8
|
const { publish, isConnected } = useMqtt();
|
|
18
9
|
|
|
19
|
-
//
|
|
10
|
+
// ✅ 1. Consume Live Manifest
|
|
11
|
+
const appManifest = useManifest();
|
|
20
12
|
const { config, domain } = appManifest;
|
|
21
13
|
|
|
22
14
|
const sendAction = useCallback(async (entityId: string, value: any) => {
|
|
23
|
-
//
|
|
24
|
-
// Note: If domain.entities is empty (early stage), this is where we'd add fallback logic
|
|
15
|
+
// 2. Find the Entity definition (Live)
|
|
25
16
|
const entity = domain.entities.find(e => e.id === entityId);
|
|
26
17
|
|
|
27
18
|
if (!entity) {
|
|
28
|
-
// For development/debugging, we log this even if the entity is missing
|
|
29
19
|
console.warn(`[Action] Entity ID '${entityId}' not found in manifest.`);
|
|
30
20
|
return;
|
|
31
21
|
}
|
|
32
22
|
|
|
33
|
-
// 2. Find the Primary Signal (The target of the action)
|
|
34
23
|
const signalId = entity.signals[0];
|
|
35
24
|
const signal = domain.signals.find(s => s.id === signalId);
|
|
36
25
|
|
|
@@ -39,8 +28,6 @@ export const useAction = () => {
|
|
|
39
28
|
return;
|
|
40
29
|
}
|
|
41
30
|
|
|
42
|
-
// 3. EXECUTE based on Mode & Source
|
|
43
|
-
|
|
44
31
|
// --- Mock Mode ---
|
|
45
32
|
if (config.mockMode) {
|
|
46
33
|
console.log(`%c[Mock Action] Setting ${entity.name} to:`, 'color: #10b981; font-weight: bold;', value);
|
|
@@ -60,7 +47,6 @@ export const useAction = () => {
|
|
|
60
47
|
|
|
61
48
|
// --- Live Mode (HTTP) ---
|
|
62
49
|
if (signal.source === 'http' && signal.endpoint) {
|
|
63
|
-
console.log(`[HTTP] POST to ${signal.endpoint}:`, value);
|
|
64
50
|
try {
|
|
65
51
|
await fetch(signal.endpoint, {
|
|
66
52
|
method: 'POST',
|
|
@@ -68,7 +54,7 @@ export const useAction = () => {
|
|
|
68
54
|
body: JSON.stringify({ id: signal.id, value })
|
|
69
55
|
});
|
|
70
56
|
} catch (err) {
|
|
71
|
-
console.log('[HTTP] (Simulation) Request sent.');
|
|
57
|
+
console.log('[HTTP] (Simulation) Request sent.');
|
|
72
58
|
}
|
|
73
59
|
}
|
|
74
60
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useManifest, useBridgeStatus } from './ManifestContext';
|
|
3
|
+
import { DynamicPage } from '../renderers/DynamicPage';
|
|
4
|
+
import { type SitemapEntry } from '../types/sitemap-entry';
|
|
5
|
+
import { type IconName } from '@ramme-io/ui';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @hook useDynamicSitemap
|
|
9
|
+
* @description
|
|
10
|
+
* - In 'Static Mode' (npm run dev): Merges dynamic pages with the hardcoded sitemap.
|
|
11
|
+
* - In 'Live Mode' (Builder): REPLACES the sitemap entirely to show only the user's design.
|
|
12
|
+
*/
|
|
13
|
+
export const useDynamicSitemap = (staticSitemap: SitemapEntry[]) => {
|
|
14
|
+
const manifest = useManifest();
|
|
15
|
+
const status = useBridgeStatus();
|
|
16
|
+
const isLive = status === 'live'; // ✅ Detect Live Mode
|
|
17
|
+
|
|
18
|
+
return useMemo(() => {
|
|
19
|
+
// 1. Convert Manifest Pages -> Sitemap Entries
|
|
20
|
+
const dynamicEntries: SitemapEntry[] = (manifest.pages || []).map((page) => ({
|
|
21
|
+
id: page.id,
|
|
22
|
+
path: page.slug,
|
|
23
|
+
title: page.title,
|
|
24
|
+
icon: (page.icon as IconName) || 'layout',
|
|
25
|
+
component: () => <DynamicPage pageId={page.id} />
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// 2. ✅ OPTIMIZATION: Exclusive Mode
|
|
29
|
+
// If we are connected to the Builder (Live) AND have dynamic content,
|
|
30
|
+
// we drop the static "Showcase" pages.
|
|
31
|
+
// This makes the Kernel feel like a blank slate to the user.
|
|
32
|
+
if (isLive && dynamicEntries.length > 0) {
|
|
33
|
+
// console.log("🚀 [Sitemap] Live Mode Active: Hiding static pages.");
|
|
34
|
+
return dynamicEntries;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. Static/Hybrid Mode (Keep existing logic for local dev)
|
|
38
|
+
const staticIds = new Set(staticSitemap.map(s => s.id));
|
|
39
|
+
const uniqueDynamic = dynamicEntries.filter(d => !staticIds.has(d.id));
|
|
40
|
+
|
|
41
|
+
return [...staticSitemap, ...uniqueDynamic];
|
|
42
|
+
}, [manifest, staticSitemap, isLive]);
|
|
43
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useJustInTimeSeeder.ts
|
|
3
|
+
* @description Runtime Data Generator for the Preview Engine.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { getMockData } from '../../data/mockData';
|
|
8
|
+
import type { ResourceDefinition, FieldDefinition } from '../validation/schema';
|
|
9
|
+
|
|
10
|
+
// Helper: Generate a semi-realistic value based on field type
|
|
11
|
+
const generateValue = (field: FieldDefinition, index: number) => {
|
|
12
|
+
const i = index + 1;
|
|
13
|
+
const label = field.label || 'Item';
|
|
14
|
+
|
|
15
|
+
switch (field.type) {
|
|
16
|
+
case 'text':
|
|
17
|
+
return `${label} ${i}`;
|
|
18
|
+
case 'number':
|
|
19
|
+
return Math.floor(Math.random() * 100) + 1;
|
|
20
|
+
case 'currency':
|
|
21
|
+
return (Math.random() * 1000).toFixed(2);
|
|
22
|
+
case 'boolean':
|
|
23
|
+
return Math.random() > 0.5;
|
|
24
|
+
case 'date':
|
|
25
|
+
// Return a date within the last 30 days
|
|
26
|
+
const d = new Date();
|
|
27
|
+
d.setDate(d.getDate() - (i * 2));
|
|
28
|
+
return d.toISOString();
|
|
29
|
+
case 'status':
|
|
30
|
+
return ['Active', 'Pending', 'Inactive', 'Archived'][index % 4];
|
|
31
|
+
case 'email':
|
|
32
|
+
return `user${i}@example.com`;
|
|
33
|
+
case 'image':
|
|
34
|
+
return `https://i.pravatar.cc/150?u=${i}`;
|
|
35
|
+
case 'textarea':
|
|
36
|
+
return `This is a sample description for ${label} ${i}. It contains enough text to test multi-line rendering.`;
|
|
37
|
+
default:
|
|
38
|
+
return `${label}-${i}`;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const useJustInTimeSeeder = (dataId: string, resourceDef?: ResourceDefinition | null) => {
|
|
43
|
+
return useMemo(() => {
|
|
44
|
+
// 1. Priority: Static Data (The "Golden Copy")
|
|
45
|
+
// If the developer has manually added data to mockData.ts, always use that.
|
|
46
|
+
const staticData = getMockData(dataId);
|
|
47
|
+
if (staticData && staticData.length > 0) {
|
|
48
|
+
return staticData;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Fallback: JIT Generation (The "Safety Net")
|
|
52
|
+
// If we have a schema but no data, generate it now.
|
|
53
|
+
if (resourceDef) {
|
|
54
|
+
console.log(`🌱 [JIT Seeder] Generating 5 mock records for: ${dataId}`);
|
|
55
|
+
|
|
56
|
+
return Array.from({ length: 5 }).map((_, idx) => {
|
|
57
|
+
const row: Record<string, any> = {
|
|
58
|
+
// Generate a robust ID that won't collide with future real IDs
|
|
59
|
+
id: `jit_${dataId}_${idx + 1}`
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
resourceDef.fields.forEach(field => {
|
|
63
|
+
// Don't overwrite ID if it was part of the fields list
|
|
64
|
+
if (field.key !== 'id') {
|
|
65
|
+
row[field.key] = generateValue(field, idx);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return row;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 3. Empty State (If no schema is found)
|
|
74
|
+
return [];
|
|
75
|
+
}, [dataId, resourceDef]);
|
|
76
|
+
};
|