@ramme-io/create-app 1.1.9 → 1.2.1

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.
@@ -1,61 +1,67 @@
1
- // ------------------------------------------------------------------
2
- // GENERATED ADAPTER CODE
3
- // Config: LIVE MODE (HTTP)
4
- // ------------------------------------------------------------------
5
- import { useEffect, useState } from 'react';
6
-
7
- // Signal IDs defined in your Acme Corp schema
8
- export type SignalId = 'mrr_stripe' | 'active_users';
9
-
10
- export function useGeneratedSignals() {
11
- // 1. Initialize state with default values (0)
12
- const [signals, setSignals] = useState<Record<SignalId, any>>({
13
- 'mrr_stripe': { value: 0, unit: 'USD', status: 'stale' },
14
- 'active_users': { value: 0, unit: '', status: 'stale' }
15
- });
16
-
17
- // 2. HTTP Polling Implementation
18
- useEffect(() => {
19
- // Helper to extract nested data (e.g. "data.finance.mrr")
20
- const getNestedValue = (obj: any, path: string) => {
21
- return path.split('.').reduce((acc, part) => acc && acc[part], obj);
22
- };
1
+ import { create } from 'zustand';
2
+ import { useEffect } from 'react';
3
+
4
+ // --- 1. SIGNAL STORE ---
5
+ export interface SignalValue {
6
+ value: any;
7
+ timestamp: number;
8
+ }
9
+
10
+ interface SignalStore {
11
+ signals: Record<string, SignalValue>;
12
+ updateSignal: (id: string, value: any) => void;
13
+ updateSignals: (updates: Record<string, any>) => void;
14
+ }
15
+
16
+ // Initial State matches app.manifest.ts
17
+ const initialState = {
18
+ living_room_ac: { value: 72, timestamp: Date.now() },
19
+ living_room_hum: { value: 45, timestamp: Date.now() },
20
+ server_01: { value: 42, timestamp: Date.now() },
21
+ front_door_lock: { value: 'LOCKED', timestamp: Date.now() }
22
+ };
23
+
24
+ export const useSignalStore = create<SignalStore>((set) => ({
25
+ signals: initialState,
26
+
27
+ updateSignal: (id, value) => set((state) => ({
28
+ signals: {
29
+ ...state.signals,
30
+ [id]: { value, timestamp: Date.now() }
31
+ }
32
+ })),
33
+
34
+ updateSignals: (updates) => set((state) => {
35
+ const newSignals = { ...state.signals };
36
+ Object.entries(updates).forEach(([id, val]) => {
37
+ newSignals[id] = { value: val, timestamp: Date.now() };
38
+ });
39
+ return { signals: newSignals };
40
+ })
41
+ }));
23
42
 
24
- const pollEndpoints = async () => {
25
- console.log('🔄 Polling /api_mock.json...');
43
+ export const useGeneratedSignals = () => {
44
+ return useSignalStore((state) => state.signals);
45
+ };
46
+
47
+ // --- 2. SIMULATION ENGINE ---
48
+ export const useSimulation = () => {
49
+ const { updateSignals } = useSignalStore();
50
+
51
+ useEffect(() => {
52
+ console.log("[System] Simulation Started");
53
+ const interval = setInterval(() => {
54
+ // Simulate random fluctuations for sensors
55
+ const updates: Record<string, any> = {};
26
56
 
27
- try {
28
- const response = await fetch('/api_mock.json');
29
- if (response.ok) {
30
- const json = await response.json();
31
-
32
- // Update Signals based on the paths defined in manifest
33
- setSignals(prev => ({
34
- ...prev,
35
- 'mrr_stripe': {
36
- value: getNestedValue(json, 'data.finance.mrr'),
37
- unit: 'USD',
38
- status: 'fresh'
39
- },
40
- 'active_users': {
41
- value: getNestedValue(json, 'data.users.total'),
42
- unit: '',
43
- status: 'fresh'
44
- }
45
- }));
46
- }
47
- } catch (err) {
48
- console.error("API Polling Error:", err);
49
- }
50
- };
51
-
52
- // Initial Fetch
53
- pollEndpoints();
54
-
55
- // Poll every 5s
56
- const interval = setInterval(pollEndpoints, 5000);
57
- return () => clearInterval(interval);
58
- }, []);
57
+ // Randomize values slightly
58
+ updates['living_room_ac'] = Number((72 + (Math.random() * 4 - 2)).toFixed(1));
59
+ updates['living_room_hum'] = Number((45 + (Math.random() * 6 - 3)).toFixed(1));
60
+ updates['server_01'] = Math.floor(Math.random() * 100);
61
+
62
+ updateSignals(updates);
63
+ }, 2000); // Update every 2 seconds
59
64
 
60
- return signals;
61
- }
65
+ return () => clearInterval(interval);
66
+ }, [updateSignals]);
67
+ };
@@ -0,0 +1,84 @@
1
+ import { useMemo } from 'react';
2
+
3
+ // --- 1. Export the Missing Types (Fixes ts(2305)) ---
4
+
5
+ export type SortDirection = 'asc' | 'desc';
6
+
7
+ export interface SortOption {
8
+ field: string;
9
+ direction: SortDirection;
10
+ }
11
+
12
+ export interface FilterOption {
13
+ field: string;
14
+ operator: 'equals' | 'contains' | 'gt' | 'lt' | 'neq';
15
+ value: any;
16
+ }
17
+
18
+ export interface QueryOptions {
19
+ filters?: FilterOption[];
20
+ sort?: SortOption;
21
+ page?: number;
22
+ pageSize?: number;
23
+ }
24
+
25
+ export interface QueryResult<T> {
26
+ data: T[];
27
+ total: number;
28
+ pageCount: number;
29
+ }
30
+
31
+ // --- 2. Update the Hook Signature (Fixes ts(2345)) ---
32
+
33
+ export function useDataQuery<T>(
34
+ rawData: T[], // <--- Now accepts an Array, NOT a string ID
35
+ options: QueryOptions = {}
36
+ ): QueryResult<T> {
37
+ const { filters, sort, page = 1, pageSize = 10 } = options;
38
+
39
+ // A. Filtering Logic
40
+ const filteredData = useMemo(() => {
41
+ if (!filters || filters.length === 0) return rawData;
42
+
43
+ return rawData.filter((item: any) => {
44
+ return filters.every((filter) => {
45
+ const itemValue = item[filter.field];
46
+
47
+ switch (filter.operator) {
48
+ case 'equals': return itemValue == filter.value;
49
+ case 'neq': return itemValue != filter.value;
50
+ case 'contains':
51
+ return String(itemValue).toLowerCase().includes(String(filter.value).toLowerCase());
52
+ case 'gt': return itemValue > filter.value;
53
+ case 'lt': return itemValue < filter.value;
54
+ default: return true;
55
+ }
56
+ });
57
+ });
58
+ }, [rawData, filters]);
59
+
60
+ // B. Sorting Logic
61
+ const sortedData = useMemo(() => {
62
+ if (!sort) return filteredData;
63
+
64
+ return [...filteredData].sort((a: any, b: any) => {
65
+ const aValue = a[sort.field];
66
+ const bValue = b[sort.field];
67
+ if (aValue < bValue) return sort.direction === 'asc' ? -1 : 1;
68
+ if (aValue > bValue) return sort.direction === 'asc' ? 1 : -1;
69
+ return 0;
70
+ });
71
+ }, [filteredData, sort]);
72
+
73
+ // C. Pagination Logic
74
+ const paginatedResult = useMemo(() => {
75
+ const startIndex = (page - 1) * pageSize;
76
+ return sortedData.slice(startIndex, startIndex + pageSize);
77
+ }, [sortedData, page, pageSize]);
78
+
79
+ return {
80
+ data: paginatedResult,
81
+ total: filteredData.length,
82
+ pageCount: Math.ceil(filteredData.length / pageSize),
83
+ };
84
+ }
@@ -1,73 +1,83 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { useMqtt } from '../contexts/MqttContext';
2
3
  import type { Signal } from '../types/signal';
3
4
 
4
- /**
5
- * Configuration for the Mock Generator.
6
- * This allows us to simulate specific hardware constraints.
7
- */
8
5
  interface SignalConfig<T> {
9
6
  initialValue?: T;
10
- min?: number; // Prevent drifting too low
11
- max?: number; // Prevent drifting too high
12
- interval?: number; // Update speed (ms)
13
- unit?: string; // e.g., "°F", "%", "RPM"
7
+ min?: number;
8
+ max?: number;
9
+ interval?: number; // Mock mode only
10
+ unit?: string;
11
+ topic?: string; // Real mode only
14
12
  }
15
13
 
16
- /**
17
- * The "Universal Socket."
18
- * UI Components use this hook to subscribe to data.
19
- * Currently running in "Synthetic Mode" (Mock) with bounded randomization.
20
- */
21
14
  export function useSignal<T = any>(signalId: string, config: SignalConfig<T> = {}): Signal<T> {
22
- // Defaults
23
15
  const {
24
16
  initialValue,
25
17
  min = -Infinity,
26
18
  max = Infinity,
27
19
  interval = 2000,
28
- unit
20
+ unit,
21
+ topic
29
22
  } = config;
30
23
 
31
- // 1. Initialize
24
+ const { subscribe, unsubscribe, lastMessage, isConnected } = useMqtt();
25
+
32
26
  const [signal, setSignal] = useState<Signal<T>>({
33
27
  id: signalId,
34
28
  value: initialValue as T,
35
29
  unit: unit,
36
30
  timestamp: Date.now(),
37
31
  status: 'fresh',
32
+ max: max
38
33
  });
39
34
 
35
+ // --- REAL MODE: MQTT ---
36
+ useEffect(() => {
37
+ if (!topic || !isConnected) return;
38
+ subscribe(topic);
39
+ return () => unsubscribe(topic);
40
+ }, [topic, isConnected, subscribe, unsubscribe]);
41
+
40
42
  useEffect(() => {
41
- // 2. SIMULATION: Generate synthetic data updates
43
+ if (!topic || !lastMessage[topic]) return;
44
+
45
+ const rawValue = lastMessage[topic];
46
+ let parsedValue: any = rawValue;
47
+
48
+ // Auto-parse numbers and booleans
49
+ if (!isNaN(Number(rawValue))) parsedValue = Number(rawValue);
50
+ else if (rawValue.toLowerCase() === 'true' || rawValue === 'on') parsedValue = true;
51
+ else if (rawValue.toLowerCase() === 'false' || rawValue === 'off') parsedValue = false;
52
+
53
+ setSignal(prev => ({
54
+ ...prev,
55
+ value: parsedValue,
56
+ timestamp: Date.now(),
57
+ status: 'fresh'
58
+ }));
59
+ }, [lastMessage, topic]);
60
+
61
+ // --- MOCK MODE: SIMULATION ---
62
+ useEffect(() => {
63
+ if (topic) return; // Disable mock if topic exists
64
+
42
65
  const timer = setInterval(() => {
43
66
  setSignal(prev => {
44
67
  let newValue: any = prev.value;
45
-
46
- // Only apply math if it's a number
47
68
  if (typeof prev.value === 'number') {
48
- // Generate drift
49
- const variance = (Math.random() - 0.5) * 2; // +/- 1
69
+ const variance = (Math.random() - 0.5) * 2;
50
70
  let nextNum = prev.value + variance;
51
-
52
- // 🛡️ CLAMPING: Apply the physical bounds
53
71
  if (min !== undefined) nextNum = Math.max(min, nextNum);
54
72
  if (max !== undefined) nextNum = Math.min(max, nextNum);
55
-
56
73
  newValue = Number(nextNum.toFixed(1));
57
74
  }
58
-
59
- return {
60
- ...prev,
61
- value: newValue,
62
- timestamp: Date.now(),
63
- status: 'fresh',
64
- unit: unit // Ensure unit persists
65
- };
75
+ return { ...prev, value: newValue, timestamp: Date.now(), status: 'fresh' };
66
76
  });
67
77
  }, interval);
68
78
 
69
79
  return () => clearInterval(timer);
70
- }, [signalId, min, max, interval, unit]);
80
+ }, [topic, min, max, interval]);
71
81
 
72
82
  return signal;
73
83
  }
@@ -0,0 +1,123 @@
1
+ import { useEffect } from 'react';
2
+ import { useToast } from '@ramme-io/ui';
3
+ // @ts-ignore - These generated hooks exist in the build
4
+ import { useGeneratedSignals, useSimulation } from '../generated/hooks';
5
+ import { appManifest } from '../config/app.manifest';
6
+ // ✅ Import types to fix implicit 'any' errors
7
+ import type { ActionDefinition, WorkflowDefinition } from '../types/schema';
8
+
9
+ /**
10
+ * THE RUNTIME LOGIC ENGINE
11
+ * This hook breathes life into the application.
12
+ * It watches for signal changes and executes the workflows defined in the manifest.
13
+ */
14
+ export const useWorkflowEngine = () => {
15
+ const signals = useGeneratedSignals();
16
+ const { addToast } = useToast();
17
+
18
+ // 1. Activate the Data Simulation (Randomizer)
19
+ useSimulation();
20
+
21
+ // 2. Define the Action Executor
22
+ // ✅ Explicitly typed 'action'
23
+ const executeAction = async (action: ActionDefinition, context: any) => {
24
+ console.log(`[Engine] Executing: ${action.type}`, action);
25
+
26
+ switch (action.type) {
27
+ case 'send_notification':
28
+ addToast(action.config.message || 'Notification Sent', 'info');
29
+ break;
30
+
31
+ case 'update_resource':
32
+ // In a real app, this would call the API
33
+ addToast(`Updating Resource: ${JSON.stringify(action.config)}`, 'success');
34
+ break;
35
+
36
+ case 'agent_task':
37
+ // Simulate AI Agent processing
38
+ console.log(`[AI Agent] Thinking about: "${action.config.prompt}"...`);
39
+ // ✅ FIX: 'loading' is not a valid ToastType. Using 'info' instead.
40
+ addToast('AI Agent Analyzing...', 'info');
41
+
42
+ setTimeout(() => {
43
+ // Mock response based on prompt context
44
+ const response = action.config.prompt?.includes('health')
45
+ ? "System Operating Normally."
46
+ : "Optimization Recommended.";
47
+
48
+ addToast(`🤖 Agent: "${response}"`, 'success', 5000);
49
+ }, 1500);
50
+ break;
51
+
52
+ case 'navigate':
53
+ window.location.href = action.config.path;
54
+ break;
55
+
56
+ default:
57
+ console.warn('Unknown action type:', action.type);
58
+ }
59
+ };
60
+
61
+ // 3. Watch Signals & Trigger Workflows
62
+ useEffect(() => {
63
+ if (!appManifest.domain?.workflows) return;
64
+
65
+ // ✅ Explicitly typed 'flow'
66
+ appManifest.domain.workflows.forEach((flow: WorkflowDefinition) => {
67
+ if (!flow.active) return;
68
+
69
+ // Check Trigger: Signal Change
70
+ if (flow.trigger.type === 'signal_change') {
71
+ const signalId = flow.trigger.config.signalId;
72
+ const condition = flow.trigger.config.condition; // e.g., "> 80"
73
+
74
+ // @ts-ignore
75
+ const signal = signals[signalId];
76
+
77
+ if (signal) {
78
+ const val = typeof signal === 'object' ? signal.value : signal;
79
+
80
+ try {
81
+ // Check if condition is met (e.g. "50 > 80")
82
+ const isMet = checkCondition(val, condition);
83
+
84
+ if (isMet) {
85
+ // Throttling logic would go here to prevent spam
86
+ console.log(`[Engine] Trigger Fired: ${flow.name}`);
87
+ flow.actions.forEach(action => executeAction(action, { signal: val }));
88
+ }
89
+ } catch (e) {
90
+ // Ignore parse errors
91
+ }
92
+ }
93
+ }
94
+ });
95
+ }, [signals, addToast]);
96
+
97
+ return {
98
+ // Exposed for manual triggering (e.g. Buttons)
99
+ triggerWorkflow: (workflowId: string) => {
100
+ const flow = appManifest.domain?.workflows?.find(w => w.id === workflowId);
101
+ if (flow) {
102
+ flow.actions.forEach(action => executeAction(action, { manual: true }));
103
+ }
104
+ }
105
+ };
106
+ };
107
+
108
+ // --- HELPER: Safe Condition Checker ---
109
+ const checkCondition = (value: number, condition: string): boolean => {
110
+ if (!condition) return false;
111
+ const parts = condition.trim().split(' ');
112
+ const operator = parts[0];
113
+ const target = parseFloat(parts[1]);
114
+
115
+ switch (operator) {
116
+ case '>': return value > target;
117
+ case '<': return value < target;
118
+ case '>=': return value >= target;
119
+ case '<=': return value <= target;
120
+ case '==': return value === target;
121
+ default: return false;
122
+ }
123
+ };
@@ -1,105 +1,58 @@
1
1
  import React from 'react';
2
- import { PageHeader, ToggleSwitch, Button, Icon } from '@ramme-io/ui';
3
-
4
- // --- IMPORTS ---
5
- import { getComponent } from '../core/component-registry';
6
- import { appManifest } from '../config/app.manifest';
7
- import { useGeneratedSignals } from '../generated/hooks';
8
- import type { EntityDefinition } from '../types/schema';
9
- import { useAction } from '../hooks/useAction';
10
-
11
- // --- DEV TOOLS (Restored) ---
12
- import { useDevTools } from '../hooks/useDevTools';
13
- import { GhostOverlay } from '../components/dev/GhostOverlay';
2
+ import { useLocation } from 'react-router-dom';
3
+ import { PageHeader, Alert } from '@ramme-io/ui';
4
+ import { appManifest } from '../config/app.manifest';
5
+ import { DynamicBlock } from '../components/DynamicBlock';
14
6
 
15
7
  const Dashboard: React.FC = () => {
16
- // 1. Initialize State Machine
17
- const signals = useGeneratedSignals();
18
- const { meta, domain } = appManifest;
8
+ const location = useLocation();
19
9
 
20
- // 2. Initialize DevTools
21
- const { isGhostMode, toggleGhostMode } = useDevTools();
22
- const { sendAction } = useAction();
10
+ // 1. Determine current slug from URL
11
+ const pathParts = location.pathname.split('/').filter(Boolean);
12
+ // If path is root or /dashboard, slug is 'dashboard', else take the last part
13
+ const currentSlug = pathParts.length > 1 ? pathParts[pathParts.length - 1] : 'dashboard';
14
+
15
+ // 2. Find the matching Page Definition
16
+ const pageDef = appManifest.pages?.find(p => p.slug === currentSlug)
17
+ || appManifest.pages?.[0];
18
+
19
+ if (!pageDef) {
20
+ return (
21
+ <div className="p-8">
22
+ <Alert variant="warning" title="Page Not Found">
23
+ Could not find a definition for slug: <code>{currentSlug}</code>
24
+ </Alert>
25
+ </div>
26
+ );
27
+ }
23
28
 
24
29
  return (
25
30
  <div className="space-y-8 relative">
26
31
  <PageHeader
27
- title={meta.name || "Command Center"}
28
- description={meta.description || "Real-time device monitoring."}
29
- actions={
30
- // 3. Restore the Toggle Button
31
- <Button
32
- variant={isGhostMode ? 'accent' : 'outline'}
33
- size="sm"
34
- onClick={toggleGhostMode}
35
- title="Toggle Ghost Mode (Ctrl+Shift+G)"
36
- >
37
- <Icon name={isGhostMode ? 'eye' : 'eye-off'} className="mr-2" />
38
- {isGhostMode ? 'Ghost Mode: ON' : 'Dev Tools'}
39
- </Button>
40
- }
32
+ title={pageDef.title}
33
+ description={pageDef.description}
41
34
  />
42
35
 
43
- {/* --- DYNAMIC RUNTIME ENGINE --- */}
44
- <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
45
-
46
- {domain.entities.map((entity: EntityDefinition) => {
47
- // A. Resolve Component & Data
48
- const componentType = entity.ui?.dashboardComponent || 'DeviceCard';
49
- const Component = getComponent(componentType);
36
+ {pageDef.sections.map((section) => (
37
+ <div key={section.id} className="relative">
38
+ {section.title && (
39
+ <h3 className="text-lg font-semibold mb-4 text-foreground">
40
+ {section.title}
41
+ </h3>
42
+ )}
50
43
 
51
- const primarySignalId = entity.signals[0];
52
- const signal = primarySignalId ? signals[primarySignalId as keyof typeof signals] : null;
53
-
54
- // B. Prepare Props
55
- const dynamicProps: any = {
56
- title: entity.name,
57
- description: entity.description || `ID: ${entity.id}`,
58
- icon: entity.ui?.icon || 'activity',
59
- status: 'offline',
60
- };
61
-
62
- // C. Inject Signal Data
63
- if (signal) {
64
- dynamicProps.value = `${signal.value} ${signal.unit || ''}`;
65
- dynamicProps.status = 'active';
66
-
67
- if (componentType === 'ToggleCard') {
68
- dynamicProps.children = (
69
- <div className="flex items-center justify-between mt-2">
70
- <span className="text-sm text-muted-foreground">Active</span>
71
- <ToggleSwitch
72
- label="Toggle"
73
- checked={(signal.value as any) === true || signal.value === 'true' || signal.value === 1}
74
- // THE UPDATE:
75
- onChange={(val) => sendAction(entity.id, val)}
76
- />
77
- </div>
78
- );
79
- delete dynamicProps.value;
80
- }
81
- }
82
-
83
- return (
84
- // 4. Restore the GhostOverlay Wrapper
85
- <GhostOverlay
86
- key={entity.id}
87
- isActive={isGhostMode}
88
- componentId={entity.id}
89
- componentType={componentType}
90
- signalId={primarySignalId}
91
- >
92
- <Component {...dynamicProps} />
93
- </GhostOverlay>
94
- );
95
- })}
96
- </div>
97
-
98
- {domain.entities.length === 0 && (
99
- <div className="p-12 text-center border-2 border-dashed border-slate-200 rounded-lg text-slate-400">
100
- <p>No entities defined.</p>
44
+ <div
45
+ className="grid gap-6"
46
+ style={{
47
+ gridTemplateColumns: `repeat(${section.layout?.columns || 3}, minmax(300px, 1fr))`
48
+ }}
49
+ >
50
+ {section.blocks.map((block) => (
51
+ <DynamicBlock key={block.id} block={block} />
52
+ ))}
53
+ </div>
101
54
  </div>
102
- )}
55
+ ))}
103
56
  </div>
104
57
  );
105
58
  };