@microlight/core 0.9.9 → 0.10.0

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.
@@ -0,0 +1,377 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Box, Typography, Card, CardContent, Grid, Table, Sheet, Button, Select, Option, Input, Chip, IconButton } from '@mui/joy';
5
+ import RefreshIcon from '@mui/icons-material/Refresh';
6
+ import SearchIcon from '@mui/icons-material/Search';
7
+ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
8
+ import StopIcon from '@mui/icons-material/Stop';
9
+ import ErrorIcon from '@mui/icons-material/Error';
10
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
11
+ import { getOverviewData, getTasksData, getRunsData, getLogsData } from "./action";
12
+ export default function MonitoringDashboard() {
13
+ const [overview, setOverview] = useState(null);
14
+ const [tasks, setTasks] = useState([]);
15
+ const [recentRuns, setRecentRuns] = useState([]);
16
+ const [logs, setLogs] = useState([]);
17
+ const [loading, setLoading] = useState(true);
18
+ const [autoRefresh, setAutoRefresh] = useState(false);
19
+ const [filters, setFilters] = useState({
20
+ status: 'all',
21
+ task: '',
22
+ dateRange: '24h'
23
+ });
24
+ const fetchOverview = async () => {
25
+ try {
26
+ const data = await getOverviewData();
27
+ setOverview(data);
28
+ } catch (error) {
29
+ console.error('Failed to fetch overview:', error);
30
+ }
31
+ };
32
+ const fetchTasks = async () => {
33
+ try {
34
+ const data = await getTasksData();
35
+ setTasks(data);
36
+ } catch (error) {
37
+ console.error('Failed to fetch tasks:', error);
38
+ }
39
+ };
40
+ const DEFAULT_RUNS_LIMIT = 50;
41
+ const DEFAULT_LOGS_LIMIT = 100;
42
+ const fetchRecentRuns = async () => {
43
+ try {
44
+ const data = await getRunsData({
45
+ status: filters.status,
46
+ task: filters.task,
47
+ limit: DEFAULT_RUNS_LIMIT
48
+ });
49
+ setRecentRuns(data);
50
+ } catch (error) {
51
+ console.error('Failed to fetch recent runs:', error);
52
+ }
53
+ };
54
+ const fetchLogs = async () => {
55
+ try {
56
+ const data = await getLogsData({
57
+ limit: DEFAULT_LOGS_LIMIT
58
+ });
59
+ setLogs(data);
60
+ } catch (error) {
61
+ console.error('Failed to fetch logs:', error);
62
+ }
63
+ };
64
+ const refreshAll = async () => {
65
+ setLoading(true);
66
+ await Promise.all([fetchOverview(), fetchTasks(), fetchRecentRuns(), fetchLogs()]);
67
+ setLoading(false);
68
+ };
69
+ useEffect(() => {
70
+ refreshAll();
71
+ }, [filters]);
72
+ const REFRESH_INTERVAL = 30000; // Refresh every 30 seconds
73
+
74
+ useEffect(() => {
75
+ let interval;
76
+ if (autoRefresh) {
77
+ interval = setInterval(refreshAll, REFRESH_INTERVAL);
78
+ }
79
+ return () => {
80
+ if (interval) clearInterval(interval);
81
+ };
82
+ }, [autoRefresh]);
83
+ const getStatusColor = status => {
84
+ switch (status) {
85
+ case 'complete':
86
+ return 'success';
87
+ case 'failed':
88
+ return 'danger';
89
+ case 'running':
90
+ return 'primary';
91
+ case 'pending':
92
+ return 'neutral';
93
+ default:
94
+ return 'neutral';
95
+ }
96
+ };
97
+ const getStatusIcon = status => {
98
+ switch (status) {
99
+ case 'complete':
100
+ return <CheckCircleIcon />;
101
+ case 'failed':
102
+ return <ErrorIcon />;
103
+ case 'running':
104
+ return <PlayArrowIcon />;
105
+ case 'pending':
106
+ return <StopIcon />;
107
+ default:
108
+ return <StopIcon />;
109
+ }
110
+ };
111
+ const formatDuration = duration => {
112
+ if (!duration) return 'N/A';
113
+ const seconds = Math.floor(duration / 1000);
114
+ const minutes = Math.floor(seconds / 60);
115
+ const hours = Math.floor(minutes / 60);
116
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
117
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
118
+ return `${seconds}s`;
119
+ };
120
+ const formatDate = dateString => {
121
+ if (!dateString) return 'N/A';
122
+ return new Date(dateString).toLocaleString();
123
+ };
124
+ if (loading && !overview) {
125
+ return <Box sx={{
126
+ p: 3
127
+ }}>
128
+ <Typography level="h2">Loading...</Typography>
129
+ </Box>;
130
+ }
131
+ return <Box sx={{
132
+ p: 3
133
+ }}>
134
+ {/* Header */}
135
+ <Box sx={{
136
+ mb: 3,
137
+ display: 'flex',
138
+ justifyContent: 'space-between',
139
+ alignItems: 'center'
140
+ }}>
141
+ <Typography level="h2">Task Monitoring Dashboard</Typography>
142
+ <Box sx={{
143
+ display: 'flex',
144
+ gap: 1,
145
+ alignItems: 'center'
146
+ }}>
147
+ <Button variant={autoRefresh ? 'solid' : 'outlined'} color={autoRefresh ? 'primary' : 'neutral'} onClick={() => setAutoRefresh(!autoRefresh)} size="sm">
148
+ {autoRefresh ? 'Auto-refresh ON' : 'Auto-refresh OFF'}
149
+ </Button>
150
+ <IconButton variant="outlined" onClick={refreshAll} loading={loading} size="sm">
151
+ <RefreshIcon />
152
+ </IconButton>
153
+ </Box>
154
+ </Box>
155
+
156
+ {/* Overview Cards */}
157
+ {overview && <Grid container spacing={2} sx={{
158
+ mb: 3
159
+ }}>
160
+ <Grid xs={12} sm={6} md={3}>
161
+ <Card>
162
+ <CardContent>
163
+ <Typography level="title-sm" color="neutral">Total Runs</Typography>
164
+ <Typography level="h2">{overview.totalRuns?.toLocaleString()}</Typography>
165
+ </CardContent>
166
+ </Card>
167
+ </Grid>
168
+ <Grid xs={12} sm={6} md={3}>
169
+ <Card>
170
+ <CardContent>
171
+ <Typography level="title-sm" color="success">Success Rate</Typography>
172
+ <Typography level="h2" color="success">
173
+ {overview.successRate ? `${overview.successRate.toFixed(1)}%` : 'N/A'}
174
+ </Typography>
175
+ </CardContent>
176
+ </Card>
177
+ </Grid>
178
+ <Grid xs={12} sm={6} md={3}>
179
+ <Card>
180
+ <CardContent>
181
+ <Typography level="title-sm" color="primary">Running Tasks</Typography>
182
+ <Typography level="h2" color="primary">{overview.runningTasks || 0}</Typography>
183
+ </CardContent>
184
+ </Card>
185
+ </Grid>
186
+ <Grid xs={12} sm={6} md={3}>
187
+ <Card>
188
+ <CardContent>
189
+ <Typography level="title-sm" color="danger">Failed (24h)</Typography>
190
+ <Typography level="h2" color="danger">{overview.recentFailures || 0}</Typography>
191
+ </CardContent>
192
+ </Card>
193
+ </Grid>
194
+ </Grid>}
195
+
196
+ {/* Filters */}
197
+ <Box sx={{
198
+ mb: 3,
199
+ display: 'flex',
200
+ gap: 2,
201
+ alignItems: 'center',
202
+ flexWrap: 'wrap'
203
+ }}>
204
+ <Select value={filters.status} onChange={(_, value) => setFilters(prev => ({
205
+ ...prev,
206
+ status: value
207
+ }))} size="sm" sx={{
208
+ minWidth: 120
209
+ }}>
210
+ <Option value="all">All Status</Option>
211
+ <Option value="complete">Complete</Option>
212
+ <Option value="failed">Failed</Option>
213
+ <Option value="running">Running</Option>
214
+ <Option value="pending">Pending</Option>
215
+ </Select>
216
+
217
+ <Input placeholder="Filter by task name..." value={filters.task} onChange={e => setFilters(prev => ({
218
+ ...prev,
219
+ task: e.target.value
220
+ }))} size="sm" sx={{
221
+ minWidth: 200
222
+ }} startDecorator={<SearchIcon />} />
223
+
224
+ <Select value={filters.dateRange} onChange={(_, value) => setFilters(prev => ({
225
+ ...prev,
226
+ dateRange: value
227
+ }))} size="sm" sx={{
228
+ minWidth: 100
229
+ }}>
230
+ <Option value="1h">Last Hour</Option>
231
+ <Option value="24h">Last 24h</Option>
232
+ <Option value="7d">Last 7 days</Option>
233
+ <Option value="30d">Last 30 days</Option>
234
+ </Select>
235
+ </Box>
236
+
237
+ {/* Task Statistics Table */}
238
+ <Box sx={{
239
+ mb: 4
240
+ }}>
241
+ <Typography level="h3" sx={{
242
+ mb: 2
243
+ }}>Task Statistics</Typography>
244
+ <Sheet sx={{
245
+ borderRadius: 'sm',
246
+ overflow: 'auto'
247
+ }}>
248
+ <Table>
249
+ <thead>
250
+ <tr>
251
+ <th>Task Name</th>
252
+ <th>Total Runs</th>
253
+ <th>Success Rate</th>
254
+ <th>Last Run</th>
255
+ <th>Status</th>
256
+ <th>Avg Duration</th>
257
+ </tr>
258
+ </thead>
259
+ <tbody>
260
+ {tasks.map(task => <tr key={task.task}>
261
+ <td>
262
+ <Typography level="body-sm" fontWeight="md">
263
+ {task.task}
264
+ </Typography>
265
+ </td>
266
+ <td>{task.totalRuns}</td>
267
+ <td>
268
+ <Typography level="body-sm" color={task.successRate > 90 ? 'success' : task.successRate > 70 ? 'warning' : 'danger'}>
269
+ {task.successRate.toFixed(1)}%
270
+ </Typography>
271
+ </td>
272
+ <td>
273
+ <Typography level="body-sm">
274
+ {formatDate(task.lastRun)}
275
+ </Typography>
276
+ </td>
277
+ <td>
278
+ <Chip color={getStatusColor(task.lastStatus)} size="sm" startDecorator={getStatusIcon(task.lastStatus)}>
279
+ {task.lastStatus}
280
+ </Chip>
281
+ </td>
282
+ <td>
283
+ <Typography level="body-sm">
284
+ {formatDuration(task.avgDuration)}
285
+ </Typography>
286
+ </td>
287
+ </tr>)}
288
+ </tbody>
289
+ </Table>
290
+ </Sheet>
291
+ </Box>
292
+
293
+ {/* Recent Runs */}
294
+ <Box sx={{
295
+ mb: 4
296
+ }}>
297
+ <Typography level="h3" sx={{
298
+ mb: 2
299
+ }}>Recent Runs</Typography>
300
+ <Sheet sx={{
301
+ borderRadius: 'sm',
302
+ overflow: 'auto',
303
+ maxHeight: 400
304
+ }}>
305
+ <Table>
306
+ <thead>
307
+ <tr>
308
+ <th>Task</th>
309
+ <th>Status</th>
310
+ <th>Started</th>
311
+ <th>Duration</th>
312
+ <th>Triggered By</th>
313
+ </tr>
314
+ </thead>
315
+ <tbody>
316
+ {recentRuns.map(run => <tr key={run.id}>
317
+ <td>
318
+ <Typography level="body-sm" fontWeight="md">
319
+ {run.task}
320
+ </Typography>
321
+ </td>
322
+ <td>
323
+ <Chip color={getStatusColor(run.status)} size="sm" startDecorator={getStatusIcon(run.status)}>
324
+ {run.status}
325
+ </Chip>
326
+ </td>
327
+ <td>
328
+ <Typography level="body-sm">
329
+ {formatDate(run.started_at)}
330
+ </Typography>
331
+ </td>
332
+ <td>
333
+ <Typography level="body-sm">
334
+ {formatDuration(run.duration)}
335
+ </Typography>
336
+ </td>
337
+ <td>
338
+ <Chip size="sm" variant="outlined">
339
+ {run.triggered_by}
340
+ </Chip>
341
+ </td>
342
+ </tr>)}
343
+ </tbody>
344
+ </Table>
345
+ </Sheet>
346
+ </Box>
347
+
348
+ {/* Recent Logs */}
349
+ <Box>
350
+ <Typography level="h3" sx={{
351
+ mb: 2
352
+ }}>Recent Logs</Typography>
353
+ <Sheet sx={{
354
+ borderRadius: 'sm',
355
+ p: 2,
356
+ maxHeight: 300,
357
+ overflow: 'auto',
358
+ bgcolor: 'neutral.50'
359
+ }}>
360
+ {logs.map(log => <Box key={log.id} sx={{
361
+ mb: 1,
362
+ fontFamily: 'monospace',
363
+ fontSize: 'sm'
364
+ }}>
365
+ <Typography level="body-xs" color="neutral">
366
+ [{formatDate(log.created_at)}] [{log.type}]
367
+ </Typography>
368
+ <Typography level="body-sm" sx={{
369
+ whiteSpace: 'pre-wrap'
370
+ }}>
371
+ {log.content}
372
+ </Typography>
373
+ </Box>)}
374
+ </Sheet>
375
+ </Box>
376
+ </Box>;
377
+ }
@@ -0,0 +1,147 @@
1
+ 'use server';
2
+
3
+ import microlightDB from "../../database/microlight/index.js";
4
+ export async function getOverviewData() {
5
+ try {
6
+ // Get overview statistics
7
+ const total_runs_result = await microlightDB.sequelize.query('SELECT COUNT(*) as count FROM runs', {
8
+ type: microlightDB.sequelize.QueryTypes.SELECT
9
+ });
10
+ const status_stats_result = await microlightDB.sequelize.query(`SELECT
11
+ status,
12
+ COUNT(*) as count
13
+ FROM runs
14
+ GROUP BY status`, {
15
+ type: microlightDB.sequelize.QueryTypes.SELECT
16
+ });
17
+ const recent_failures_result = await microlightDB.sequelize.query(`SELECT COUNT(*) as count
18
+ FROM runs
19
+ WHERE status = 'failed'
20
+ AND created_at >= datetime('now', '-24 hours')`, {
21
+ type: microlightDB.sequelize.QueryTypes.SELECT
22
+ });
23
+ const total_runs = total_runs_result[0]?.count || 0;
24
+ const successful_runs = status_stats_result.find(s => s.status === 'complete')?.count || 0;
25
+ const running_tasks = status_stats_result.find(s => s.status === 'running')?.count || 0;
26
+ const recent_failures = recent_failures_result[0]?.count || 0;
27
+ const success_rate = total_runs > 0 ? successful_runs / total_runs * 100 : 0;
28
+ return {
29
+ totalRuns: total_runs,
30
+ successRate: success_rate,
31
+ runningTasks: running_tasks,
32
+ recentFailures: recent_failures,
33
+ statusBreakdown: status_stats_result
34
+ };
35
+ } catch (error) {
36
+ console.error('Error fetching overview:', error);
37
+ throw new Error('Failed to fetch overview data');
38
+ }
39
+ }
40
+ export async function getTasksData() {
41
+ try {
42
+ // Get task statistics
43
+ const task_stats_result = await microlightDB.sequelize.query(`SELECT
44
+ task,
45
+ COUNT(*) as totalRuns,
46
+ COUNT(CASE WHEN status = 'complete' THEN 1 END) as successfulRuns,
47
+ COUNT(CASE WHEN status = 'failed' THEN 1 END) as failedRuns,
48
+ MAX(started_at) as lastRun,
49
+ (
50
+ SELECT status
51
+ FROM runs r2
52
+ WHERE r2.task = runs.task
53
+ ORDER BY started_at DESC
54
+ LIMIT 1
55
+ ) as lastStatus,
56
+ AVG(duration) as avgDuration
57
+ FROM runs
58
+ GROUP BY task
59
+ ORDER BY totalRuns DESC`, {
60
+ type: microlightDB.sequelize.QueryTypes.SELECT
61
+ });
62
+ return task_stats_result.map(task => ({
63
+ ...task,
64
+ successRate: task.totalRuns > 0 ? task.successfulRuns / task.totalRuns * 100 : 0
65
+ }));
66
+ } catch (error) {
67
+ console.error('Error fetching task statistics:', error);
68
+ throw new Error('Failed to fetch task statistics');
69
+ }
70
+ }
71
+ export async function getRunsData(filters = {}) {
72
+ try {
73
+ const {
74
+ status,
75
+ task,
76
+ limit = 50
77
+ } = filters;
78
+ let where_clause = '';
79
+ const where_conditions = [];
80
+ if (status && status !== 'all') {
81
+ where_conditions.push(`status = '${status}'`);
82
+ }
83
+ if (task && task.trim() !== '') {
84
+ where_conditions.push(`task LIKE '%${task}%'`);
85
+ }
86
+ if (where_conditions.length > 0) {
87
+ where_clause = `WHERE ${where_conditions.join(' AND ')}`;
88
+ }
89
+ const runs_result = await microlightDB.sequelize.query(`SELECT
90
+ id,
91
+ task,
92
+ status,
93
+ started_at,
94
+ completed_at,
95
+ duration,
96
+ triggered_by,
97
+ inputs
98
+ FROM runs
99
+ ${where_clause}
100
+ ORDER BY started_at DESC
101
+ LIMIT ${limit}`, {
102
+ type: microlightDB.sequelize.QueryTypes.SELECT
103
+ });
104
+ return runs_result;
105
+ } catch (error) {
106
+ console.error('Error fetching runs:', error);
107
+ throw new Error('Failed to fetch runs data');
108
+ }
109
+ }
110
+ export async function getLogsData(filters = {}) {
111
+ try {
112
+ const {
113
+ runId,
114
+ type,
115
+ limit = 100
116
+ } = filters;
117
+ let where_clause = '';
118
+ const where_conditions = [];
119
+ if (runId) {
120
+ where_conditions.push(`run = ${runId}`);
121
+ }
122
+ if (type && type !== 'all') {
123
+ where_conditions.push(`type = '${type}'`);
124
+ }
125
+ if (where_conditions.length > 0) {
126
+ where_clause = `WHERE ${where_conditions.join(' AND ')}`;
127
+ }
128
+ const logs_result = await microlightDB.sequelize.query(`SELECT
129
+ l.id,
130
+ l.created_at,
131
+ l.type,
132
+ l.content,
133
+ l.run,
134
+ r.task
135
+ FROM logs l
136
+ LEFT JOIN runs r ON l.run = r.id
137
+ ${where_clause}
138
+ ORDER BY l.created_at DESC
139
+ LIMIT ${limit}`, {
140
+ type: microlightDB.sequelize.QueryTypes.SELECT
141
+ });
142
+ return logs_result;
143
+ } catch (error) {
144
+ console.error('Error fetching logs:', error);
145
+ throw new Error('Failed to fetch logs data');
146
+ }
147
+ }
@@ -0,0 +1,8 @@
1
+ import MonitoringDashboard from "./MonitoringDashboard";
2
+ export const metadata = {
3
+ title: "Monitoring - Microlight",
4
+ description: "Task monitoring and analytics dashboard"
5
+ };
6
+ export default function MonitoringPage() {
7
+ return <MonitoringDashboard />;
8
+ }
@@ -1,11 +1,14 @@
1
1
  'use client';
2
2
 
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
3
5
  // import Logo from '../Logo';
4
- import { Box, Sheet } from '@mui/joy';
6
+ import { Box, Sheet, Button } from '@mui/joy';
5
7
  export default function Navbar({
6
8
  user,
7
9
  signOut
8
10
  }) {
11
+ const pathname = usePathname();
9
12
  return <Sheet component="nav" sx={{
10
13
  px: 1,
11
14
  // py: 0.5,
@@ -27,10 +30,38 @@ export default function Navbar({
27
30
  <Box sx={{
28
31
  display: 'flex',
29
32
  alignItems: 'center',
30
- gap: 1
33
+ gap: 2
31
34
  }}>
32
35
  {/* <Logo offering='Transactions' /> */}
33
- Microlight
36
+ <Link href="/" style={{
37
+ textDecoration: 'none',
38
+ color: 'inherit'
39
+ }}>
40
+ <Box sx={{
41
+ fontWeight: 'bold'
42
+ }}>Microlight</Box>
43
+ </Link>
44
+
45
+ <Box sx={{
46
+ display: 'flex',
47
+ gap: 1
48
+ }}>
49
+ <Link href="/library" style={{
50
+ textDecoration: 'none'
51
+ }}>
52
+ <Button variant={pathname?.startsWith('/library') ? 'solid' : 'plain'} size="sm" color="neutral">
53
+ Library
54
+ </Button>
55
+ </Link>
56
+
57
+ <Link href="/monitoring" style={{
58
+ textDecoration: 'none'
59
+ }}>
60
+ <Button variant={pathname?.startsWith('/monitoring') ? 'solid' : 'plain'} size="sm" color="neutral">
61
+ Monitoring
62
+ </Button>
63
+ </Link>
64
+ </Box>
34
65
  </Box>
35
66
 
36
67
  </Sheet>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microlight/core",
3
- "version": "0.9.9",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -51,7 +51,6 @@
51
51
  "switchless": "0.19.1"
52
52
  },
53
53
  "devDependencies": {
54
- "@babel/cli": "^7.28.3",
55
54
  "@babel/preset-env": "^7.28.0",
56
55
  "@babel/preset-react": "^7.27.1",
57
56
  "babel-plugin-module-resolver": "^5.0.2",