@ramme-io/create-app 1.2.1 → 1.2.2
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 +1 -2
- package/template/package.json +41 -0
- package/template/pkg.json +1 -1
- package/template/src/App.tsx +62 -31
- package/template/src/components/AIChatWidget.tsx +2 -2
- package/template/src/components/AppHeader.tsx +2 -2
- 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/config/app.manifest.ts +3 -1
- package/template/src/{core → config}/component-registry.tsx +1 -1
- 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 +27 -7
- package/template/src/{pages → engine/renderers}/DynamicPage.tsx +23 -4
- package/template/src/{contexts → engine/runtime}/MqttContext.tsx +25 -11
- package/template/src/{contexts → engine/runtime}/SitemapContext.tsx +1 -1
- package/template/src/{core → engine/runtime}/data-seeder.ts +15 -5
- package/template/src/{hooks → engine/runtime}/useAction.ts +19 -8
- 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/useSignal.ts +51 -0
- package/template/src/{generated/hooks.ts → engine/runtime/useSignalStore.ts} +35 -8
- package/template/src/{hooks → engine/runtime}/useWorkflowEngine.ts +34 -13
- 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 +4 -6
- 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/index.css +1 -1
- package/template/src/main.tsx +3 -3
- package/template/src/templates/dashboard/DashboardLayout.tsx +75 -106
- package/template/src/templates/dashboard/dashboard.sitemap.ts +26 -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/vite.config.ts +12 -9
- 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/config/dashboard.layout.ts +0 -110
- package/template/src/contexts/AuthContext.tsx +0 -64
- package/template/src/data/mockUsers.ts +0 -18
- package/template/src/hooks/useSignal.ts +0 -83
- 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/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/{components/dev → features/developer}/GhostOverlay.tsx +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,17 +1,32 @@
|
|
|
1
|
-
// src/hooks/useAction.ts
|
|
2
1
|
import { useCallback } from 'react';
|
|
3
|
-
import { useMqtt } from '
|
|
4
|
-
|
|
2
|
+
import { useMqtt } from './MqttContext';
|
|
3
|
+
// ✅ User Verified Path
|
|
4
|
+
import { appManifest } from '../../config/app.manifest';
|
|
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
|
+
*/
|
|
5
15
|
|
|
6
16
|
export const useAction = () => {
|
|
7
17
|
const { publish, isConnected } = useMqtt();
|
|
18
|
+
|
|
19
|
+
// Destructure config/domain from the manifest
|
|
8
20
|
const { config, domain } = appManifest;
|
|
9
21
|
|
|
10
22
|
const sendAction = useCallback(async (entityId: string, value: any) => {
|
|
11
23
|
// 1. Find the Entity definition
|
|
24
|
+
// Note: If domain.entities is empty (early stage), this is where we'd add fallback logic
|
|
12
25
|
const entity = domain.entities.find(e => e.id === entityId);
|
|
26
|
+
|
|
13
27
|
if (!entity) {
|
|
14
|
-
|
|
28
|
+
// For development/debugging, we log this even if the entity is missing
|
|
29
|
+
console.warn(`[Action] Entity ID '${entityId}' not found in manifest.`);
|
|
15
30
|
return;
|
|
16
31
|
}
|
|
17
32
|
|
|
@@ -29,7 +44,6 @@ export const useAction = () => {
|
|
|
29
44
|
// --- Mock Mode ---
|
|
30
45
|
if (config.mockMode) {
|
|
31
46
|
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
47
|
return;
|
|
34
48
|
}
|
|
35
49
|
|
|
@@ -47,8 +61,6 @@ export const useAction = () => {
|
|
|
47
61
|
// --- Live Mode (HTTP) ---
|
|
48
62
|
if (signal.source === 'http' && signal.endpoint) {
|
|
49
63
|
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
64
|
try {
|
|
53
65
|
await fetch(signal.endpoint, {
|
|
54
66
|
method: 'POST',
|
|
@@ -56,7 +68,6 @@ export const useAction = () => {
|
|
|
56
68
|
body: JSON.stringify({ id: signal.id, value })
|
|
57
69
|
});
|
|
58
70
|
} catch (err) {
|
|
59
|
-
// We expect this to fail on the static demo, so we suppress the error alert
|
|
60
71
|
console.log('[HTTP] (Simulation) Request sent.');
|
|
61
72
|
}
|
|
62
73
|
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useSignalStore } from './useSignalStore';
|
|
3
|
+
// ✅ Import Manifest to get static metadata (min, max, units)
|
|
4
|
+
import { appManifest } from '../../config/app.manifest';
|
|
5
|
+
|
|
6
|
+
export interface SignalState {
|
|
7
|
+
id: string;
|
|
8
|
+
value: any;
|
|
9
|
+
unit?: string;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
timestamp?: number;
|
|
13
|
+
status: 'fresh' | 'stale'; // Simplified status
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @hook useSignal
|
|
18
|
+
* @description The "Signal Selector".
|
|
19
|
+
*
|
|
20
|
+
* ARCHITECTURAL ROLE:
|
|
21
|
+
* This hook is the bridge between the Static Manifest and the Dynamic Store.
|
|
22
|
+
* 1. It finds the signal definition in the Manifest (for min/max/unit).
|
|
23
|
+
* 2. It grabs the live value from the Zustand SignalStore.
|
|
24
|
+
* 3. It merges them into a single object for the UI to consume.
|
|
25
|
+
*/
|
|
26
|
+
export const useSignal = (signalId: string): SignalState => {
|
|
27
|
+
// 1. Get Live Data from Store
|
|
28
|
+
const signalData = useSignalStore((state: { signals: { [x: string]: any; }; }) => state.signals[signalId]);
|
|
29
|
+
|
|
30
|
+
// 2. Get Static Definition from Manifest
|
|
31
|
+
// We use useMemo so we don't search the array on every render
|
|
32
|
+
const signalDef = useMemo(() => {
|
|
33
|
+
return appManifest.domain.signals.find((s) => s.id === signalId);
|
|
34
|
+
}, [signalId]);
|
|
35
|
+
|
|
36
|
+
// 3. Merge and Return
|
|
37
|
+
const value = signalData?.value ?? signalDef?.defaultValue ?? 0;
|
|
38
|
+
|
|
39
|
+
// Determine if data is stale (older than 10 seconds)
|
|
40
|
+
const isStale = signalData ? (Date.now() - signalData.timestamp > 10000) : true;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: signalId,
|
|
44
|
+
value: value,
|
|
45
|
+
unit: signalDef?.unit,
|
|
46
|
+
min: signalDef?.min,
|
|
47
|
+
max: signalDef?.max,
|
|
48
|
+
timestamp: signalData?.timestamp,
|
|
49
|
+
status: isStale ? 'stale' : 'fresh'
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* @file useSignalStore.ts
|
|
6
|
+
* @description The "Short-Term Memory" of the application.
|
|
7
|
+
*
|
|
8
|
+
* ARCHITECTURAL ROLE:
|
|
9
|
+
* This Zustand store holds the instantaneous value of every Signal (IoT sensor).
|
|
10
|
+
* It acts as the buffer between the high-speed data stream (MQTT/Simulation)
|
|
11
|
+
* and the UI components.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// --- 1. SIGNAL STORE TYPES ---
|
|
5
15
|
export interface SignalValue {
|
|
6
16
|
value: any;
|
|
7
17
|
timestamp: number;
|
|
@@ -9,11 +19,13 @@ export interface SignalValue {
|
|
|
9
19
|
|
|
10
20
|
interface SignalStore {
|
|
11
21
|
signals: Record<string, SignalValue>;
|
|
22
|
+
// Single update (used by UI controls)
|
|
12
23
|
updateSignal: (id: string, value: any) => void;
|
|
24
|
+
// Batch update (used by MQTT/Simulation)
|
|
13
25
|
updateSignals: (updates: Record<string, any>) => void;
|
|
14
26
|
}
|
|
15
27
|
|
|
16
|
-
// Initial State matches
|
|
28
|
+
// Initial State matches your dashboard IDs to prevent "undefined" errors on load
|
|
17
29
|
const initialState = {
|
|
18
30
|
living_room_ac: { value: 72, timestamp: Date.now() },
|
|
19
31
|
living_room_hum: { value: 45, timestamp: Date.now() },
|
|
@@ -40,28 +52,43 @@ export const useSignalStore = create<SignalStore>((set) => ({
|
|
|
40
52
|
})
|
|
41
53
|
}));
|
|
42
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Hook to access specific signals in a component.
|
|
57
|
+
* Usage: const signals = useGeneratedSignals();
|
|
58
|
+
*/
|
|
43
59
|
export const useGeneratedSignals = () => {
|
|
44
60
|
return useSignalStore((state) => state.signals);
|
|
45
61
|
};
|
|
46
62
|
|
|
47
|
-
// --- 2. SIMULATION ENGINE ---
|
|
48
|
-
|
|
63
|
+
// --- 2. SIMULATION ENGINE (The Missing Piece) ---
|
|
64
|
+
/**
|
|
65
|
+
* A hook that generates fake data for testing when no MQTT broker is connected.
|
|
66
|
+
* You can toggle this on/off via the manifest config.
|
|
67
|
+
*/
|
|
68
|
+
export const useSimulation = (isEnabled: boolean = true) => {
|
|
49
69
|
const { updateSignals } = useSignalStore();
|
|
50
70
|
|
|
51
71
|
useEffect(() => {
|
|
52
|
-
|
|
72
|
+
if (!isEnabled) return;
|
|
73
|
+
|
|
74
|
+
console.log("[System] Simulation Mode: ON 🎲");
|
|
53
75
|
const interval = setInterval(() => {
|
|
54
|
-
// Simulate random fluctuations
|
|
76
|
+
// Simulate random fluctuations
|
|
55
77
|
const updates: Record<string, any> = {};
|
|
56
78
|
|
|
57
|
-
// Randomize values slightly
|
|
79
|
+
// Randomize values slightly around a baseline
|
|
58
80
|
updates['living_room_ac'] = Number((72 + (Math.random() * 4 - 2)).toFixed(1));
|
|
59
81
|
updates['living_room_hum'] = Number((45 + (Math.random() * 6 - 3)).toFixed(1));
|
|
60
82
|
updates['server_01'] = Math.floor(Math.random() * 100);
|
|
61
83
|
|
|
84
|
+
// Randomly flip the lock status occasionally (1% chance)
|
|
85
|
+
if (Math.random() > 0.99) {
|
|
86
|
+
updates['front_door_lock'] = Math.random() > 0.5 ? 'LOCKED' : 'UNLOCKED';
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
updateSignals(updates);
|
|
63
90
|
}, 2000); // Update every 2 seconds
|
|
64
91
|
|
|
65
92
|
return () => clearInterval(interval);
|
|
66
|
-
}, [updateSignals]);
|
|
93
|
+
}, [isEnabled, updateSignals]);
|
|
67
94
|
};
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import { useToast } from '@ramme-io/ui';
|
|
3
|
-
//
|
|
4
|
-
import { useGeneratedSignals, useSimulation } from '
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
// ✅ 1. Correct Imports from Store
|
|
4
|
+
import { useGeneratedSignals, useSimulation } from './useSignalStore';
|
|
5
|
+
// ✅ 2. Correct Import from Manifest
|
|
6
|
+
import { appManifest } from '../../config/app.manifest';
|
|
7
|
+
|
|
8
|
+
// Minimal Types for internal use (until schema is formalized)
|
|
9
|
+
interface ActionDefinition {
|
|
10
|
+
type: string;
|
|
11
|
+
config: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface WorkflowDefinition {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
active: boolean;
|
|
18
|
+
trigger: {
|
|
19
|
+
type: string;
|
|
20
|
+
config: {
|
|
21
|
+
signalId: string;
|
|
22
|
+
condition: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
actions: ActionDefinition[];
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
/**
|
|
10
29
|
* THE RUNTIME LOGIC ENGINE
|
|
@@ -16,10 +35,10 @@ export const useWorkflowEngine = () => {
|
|
|
16
35
|
const { addToast } = useToast();
|
|
17
36
|
|
|
18
37
|
// 1. Activate the Data Simulation (Randomizer)
|
|
19
|
-
|
|
38
|
+
// This now checks internally if it should run (based on boolean arg)
|
|
39
|
+
useSimulation(appManifest.config.mockMode);
|
|
20
40
|
|
|
21
41
|
// 2. Define the Action Executor
|
|
22
|
-
// ✅ Explicitly typed 'action'
|
|
23
42
|
const executeAction = async (action: ActionDefinition, context: any) => {
|
|
24
43
|
console.log(`[Engine] Executing: ${action.type}`, action);
|
|
25
44
|
|
|
@@ -36,7 +55,6 @@ export const useWorkflowEngine = () => {
|
|
|
36
55
|
case 'agent_task':
|
|
37
56
|
// Simulate AI Agent processing
|
|
38
57
|
console.log(`[AI Agent] Thinking about: "${action.config.prompt}"...`);
|
|
39
|
-
// ✅ FIX: 'loading' is not a valid ToastType. Using 'info' instead.
|
|
40
58
|
addToast('AI Agent Analyzing...', 'info');
|
|
41
59
|
|
|
42
60
|
setTimeout(() => {
|
|
@@ -60,10 +78,11 @@ export const useWorkflowEngine = () => {
|
|
|
60
78
|
|
|
61
79
|
// 3. Watch Signals & Trigger Workflows
|
|
62
80
|
useEffect(() => {
|
|
81
|
+
// Safety check for undefined domain/workflows
|
|
63
82
|
if (!appManifest.domain?.workflows) return;
|
|
64
83
|
|
|
65
|
-
//
|
|
66
|
-
appManifest.domain.workflows.forEach((flow
|
|
84
|
+
// We cast to our local type since manifest might be "any"
|
|
85
|
+
(appManifest.domain.workflows as unknown as WorkflowDefinition[]).forEach((flow) => {
|
|
67
86
|
if (!flow.active) return;
|
|
68
87
|
|
|
69
88
|
// Check Trigger: Signal Change
|
|
@@ -71,18 +90,18 @@ export const useWorkflowEngine = () => {
|
|
|
71
90
|
const signalId = flow.trigger.config.signalId;
|
|
72
91
|
const condition = flow.trigger.config.condition; // e.g., "> 80"
|
|
73
92
|
|
|
74
|
-
// @ts-ignore
|
|
75
93
|
const signal = signals[signalId];
|
|
76
94
|
|
|
77
95
|
if (signal) {
|
|
78
|
-
const val =
|
|
96
|
+
const val = signal.value; // Cleaned up access
|
|
79
97
|
|
|
80
98
|
try {
|
|
81
99
|
// Check if condition is met (e.g. "50 > 80")
|
|
82
100
|
const isMet = checkCondition(val, condition);
|
|
83
101
|
|
|
102
|
+
// Debounce: In a real app, we'd check timestamps to avoid firing every 2ms
|
|
103
|
+
// For now, we rely on the simulation being slow (2s)
|
|
84
104
|
if (isMet) {
|
|
85
|
-
// Throttling logic would go here to prevent spam
|
|
86
105
|
console.log(`[Engine] Trigger Fired: ${flow.name}`);
|
|
87
106
|
flow.actions.forEach(action => executeAction(action, { signal: val }));
|
|
88
107
|
}
|
|
@@ -97,8 +116,10 @@ export const useWorkflowEngine = () => {
|
|
|
97
116
|
return {
|
|
98
117
|
// Exposed for manual triggering (e.g. Buttons)
|
|
99
118
|
triggerWorkflow: (workflowId: string) => {
|
|
119
|
+
// @ts-ignore
|
|
100
120
|
const flow = appManifest.domain?.workflows?.find(w => w.id === workflowId);
|
|
101
121
|
if (flow) {
|
|
122
|
+
// @ts-ignore
|
|
102
123
|
flow.actions.forEach(action => executeAction(action, { manual: true }));
|
|
103
124
|
}
|
|
104
125
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
// src/core/manifest-types.ts
|
|
2
2
|
|
|
3
|
+
import { type IconName } from '@ramme-io/ui';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Defines a simple navigation link structure used in menus.
|
|
7
|
+
*/
|
|
8
|
+
export interface ManifestLink {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
path: string;
|
|
12
|
+
icon?: IconName;
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
/**
|
|
4
16
|
* The fundamental unit of the UI.
|
|
5
17
|
* Corresponds to a specific React component in the registry.
|
|
@@ -40,12 +52,32 @@ export interface PageDefinition {
|
|
|
40
52
|
sections: PageSection[];
|
|
41
53
|
}
|
|
42
54
|
|
|
55
|
+
// ✅ NEW: Full App Specification Types (Matches app.manifest.ts)
|
|
56
|
+
export interface AppMeta {
|
|
57
|
+
name: string;
|
|
58
|
+
version: string;
|
|
59
|
+
description: string;
|
|
60
|
+
author: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AppConfig {
|
|
64
|
+
theme: 'system' | 'light' | 'dark';
|
|
65
|
+
mockMode: boolean;
|
|
66
|
+
brokerUrl?: string; // Added for MQTT
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AppDomain {
|
|
70
|
+
signals: any[];
|
|
71
|
+
entities: any[];
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
/**
|
|
44
75
|
* The "Brain" of the application.
|
|
45
76
|
* This JSON structure drives the entire UI.
|
|
46
77
|
*/
|
|
47
|
-
export interface
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
export interface AppSpecification {
|
|
79
|
+
meta: AppMeta;
|
|
80
|
+
config: AppConfig;
|
|
81
|
+
domain: AppDomain;
|
|
50
82
|
pages: PageDefinition[];
|
|
51
83
|
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @file schema.ts
|
|
5
|
+
* @description The "Application Constitution".
|
|
6
|
+
*
|
|
7
|
+
* ARCHITECTURAL ROLE:
|
|
8
|
+
* This file uses Zod to define the strict runtime validation rules for the
|
|
9
|
+
* App Manifest. While `manifest-types.ts` handles compile-time TypeScript checks,
|
|
10
|
+
* this file handles runtime validation, ensuring that any JSON loaded into the
|
|
11
|
+
* engine (whether from a file, API, or user input) is structurally sound.
|
|
12
|
+
*
|
|
13
|
+
* LAYERS DEFINED:
|
|
14
|
+
* 1. **SaaS Layer:** Data resources, fields, and tables.
|
|
15
|
+
* 2. **Physical Layer:** IoT signals, sensors, and entities.
|
|
16
|
+
* 3. **Logic Layer:** Workflows, triggers, and automated actions.
|
|
17
|
+
* 4. **Presentation Layer:** Pages, sections, and UI blocks.
|
|
18
|
+
*/
|
|
19
|
+
|
|
3
20
|
// ------------------------------------------------------------------
|
|
4
21
|
// 1. DATA RESOURCE DEFINITIONS (SaaS Layer)
|
|
5
22
|
// ------------------------------------------------------------------
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
Message,
|
|
8
8
|
PromptInput,
|
|
9
9
|
} from '@ramme-io/ui';
|
|
10
|
-
import { useMockChat } from '
|
|
10
|
+
import { useMockChat } from '../../assistant/useMockChat'; // <--- The new brain
|
|
11
11
|
|
|
12
12
|
const AiChat: React.FC = () => {
|
|
13
13
|
const { messages, isLoading, sendMessage } = useMockChat();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
// Import the User type from your mock data to ensure shape consistency
|
|
3
|
+
import type { User } from '../../data/mockData';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file AuthContext.tsx
|
|
7
|
+
* @description The Central Identity Manager.
|
|
8
|
+
* * ARCHITECTURAL ROLE:
|
|
9
|
+
* This context provides global access to the current user's state (Identity).
|
|
10
|
+
* It decouples the UI components from the specific authentication implementation.
|
|
11
|
+
* * KEY FEATURES:
|
|
12
|
+
* 1. **Session Persistence:** Checks localStorage on boot.
|
|
13
|
+
* 2. **Data Lake Connection:** Reads from 'ramme_db_users' to validate real accounts.
|
|
14
|
+
* 3. **Simulation:** Adds artificial latency to test loading states.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ✅ CRITICAL FIX: Match the key used by useCrudLocalStorage and data-seeder
|
|
18
|
+
const USER_DB_KEY = 'ramme_db_users';
|
|
19
|
+
const SESSION_KEY = 'ramme_session';
|
|
20
|
+
|
|
21
|
+
interface AuthContextType {
|
|
22
|
+
user: User | null;
|
|
23
|
+
login: (email: string, password: string) => Promise<void>;
|
|
24
|
+
logout: () => void;
|
|
25
|
+
isAuthenticated: boolean;
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
32
|
+
const [user, setUser] = useState<User | null>(null);
|
|
33
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
34
|
+
|
|
35
|
+
// Check for existing session on mount
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const storedUser = localStorage.getItem(SESSION_KEY);
|
|
38
|
+
if (storedUser) {
|
|
39
|
+
try {
|
|
40
|
+
setUser(JSON.parse(storedUser));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error("Failed to parse session", e);
|
|
43
|
+
localStorage.removeItem(SESSION_KEY);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const login = async (email: string, password: string) => {
|
|
50
|
+
setIsLoading(true);
|
|
51
|
+
|
|
52
|
+
// ✅ FIX: We now use the 'password' variable to satisfy TypeScript (ts(6133))
|
|
53
|
+
// This also adds a layer of realism to the mock validation.
|
|
54
|
+
if (!password || password.length < 3) {
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
throw new Error("Password must be at least 3 characters");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`🔐 Attempting login for: ${email}`);
|
|
60
|
+
|
|
61
|
+
// Simulate API Delay
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
63
|
+
|
|
64
|
+
// ✅ READ FROM THE CORRECT DATA LAKE
|
|
65
|
+
const storedUsers = localStorage.getItem(USER_DB_KEY);
|
|
66
|
+
|
|
67
|
+
// Debugging: See what is actually in storage
|
|
68
|
+
if (!storedUsers) {
|
|
69
|
+
console.warn(`⚠️ Data Lake '${USER_DB_KEY}' is empty! Did you run the seeder or signup?`);
|
|
70
|
+
} else {
|
|
71
|
+
// Optional: Log count for debugging (remove in production)
|
|
72
|
+
// console.log(`✅ Data Lake found. Users in DB:`, JSON.parse(storedUsers).length);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const users: User[] = storedUsers ? JSON.parse(storedUsers) : [];
|
|
76
|
+
|
|
77
|
+
// Case-insensitive email check
|
|
78
|
+
const foundUser = users.find(u => u.email.toLowerCase() === email.toLowerCase());
|
|
79
|
+
|
|
80
|
+
if (foundUser) {
|
|
81
|
+
console.log("🎉 User found:", foundUser);
|
|
82
|
+
setUser(foundUser);
|
|
83
|
+
localStorage.setItem(SESSION_KEY, JSON.stringify(foundUser));
|
|
84
|
+
setIsLoading(false);
|
|
85
|
+
} else {
|
|
86
|
+
console.error("❌ User NOT found. Available emails:", users.map(u => u.email));
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
throw new Error("Invalid email or password");
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const logout = () => {
|
|
93
|
+
setUser(null);
|
|
94
|
+
localStorage.removeItem(SESSION_KEY);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<AuthContext.Provider
|
|
99
|
+
value={{
|
|
100
|
+
user,
|
|
101
|
+
login,
|
|
102
|
+
logout,
|
|
103
|
+
isAuthenticated: !!user,
|
|
104
|
+
isLoading
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</AuthContext.Provider>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const useAuth = () => {
|
|
113
|
+
const context = useContext(AuthContext);
|
|
114
|
+
if (context === undefined) {
|
|
115
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
116
|
+
}
|
|
117
|
+
return context;
|
|
118
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Outlet } from 'react-router-dom';
|
|
3
|
+
import { useTheme, Button, availableThemes, type ThemeConfig } from '@ramme-io/ui';
|
|
4
|
+
|
|
5
|
+
// --- Temporary Theme Switcher (Unchanged) ---
|
|
6
|
+
const MiniThemeSwitcher = () => {
|
|
7
|
+
const { setTheme, customTheme, setCustomTheme } = useTheme();
|
|
8
|
+
|
|
9
|
+
const cyberTheme: ThemeConfig = {
|
|
10
|
+
name: 'Cyberpunk',
|
|
11
|
+
colors: {
|
|
12
|
+
primary: '255 238 88', primaryForeground: '0 0 0',
|
|
13
|
+
secondary: '30 30 35', secondaryForeground: '255 255 255',
|
|
14
|
+
accent: '0 255 255', accentForeground: '0 0 0',
|
|
15
|
+
danger: '255 0 85', dangerForeground: '255 255 255',
|
|
16
|
+
background: '5 5 10', card: '20 20 25', text: '240 240 240',
|
|
17
|
+
border: '255 238 88', muted: '40 40 50', mutedForeground: '150 150 160',
|
|
18
|
+
input: '30 30 35', inputBorder: '80 80 90', ring: '255 238 88'
|
|
19
|
+
},
|
|
20
|
+
borderRadius: '0px',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="absolute top-4 right-4 flex gap-2 z-50">
|
|
25
|
+
{availableThemes.slice(0, 3).map(t => (
|
|
26
|
+
<Button key={t} size="sm" variant="ghost" onClick={() => setTheme(t)} className="opacity-50 hover:opacity-100">
|
|
27
|
+
{t}
|
|
28
|
+
</Button>
|
|
29
|
+
))}
|
|
30
|
+
<Button size="sm" variant="ghost" onClick={() => setCustomTheme(customTheme ? null : cyberTheme)} className="opacity-50 hover:opacity-100">
|
|
31
|
+
AI
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const AuthLayout: React.FC = () => {
|
|
38
|
+
return (
|
|
39
|
+
<div className="min-h-screen w-full flex items-center justify-center bg-background text-foreground relative transition-colors duration-300">
|
|
40
|
+
|
|
41
|
+
{/* Background Pattern */}
|
|
42
|
+
<div className="absolute inset-0 z-0 opacity-[0.03] pointer-events-none bg-[radial-gradient(#888_1px,transparent_1px)] [background-size:16px_16px]" />
|
|
43
|
+
|
|
44
|
+
<MiniThemeSwitcher />
|
|
45
|
+
|
|
46
|
+
{/* ✅ FIX: Removed the outer <Card>, Header, and Footer.
|
|
47
|
+
This is now a "Passthrough" layout. It centers the content
|
|
48
|
+
but lets the Page (LoginPage/SignupPage) own the UI completely.
|
|
49
|
+
*/}
|
|
50
|
+
<div className="z-10 relative w-full flex justify-center p-4">
|
|
51
|
+
<Outlet />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|