@microlight/core 0.9.9 → 0.11.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.
Files changed (30) hide show
  1. package/dist/server/app/layout.js +2 -18
  2. package/dist/server/app/library/[[...f_path]]/ViewFolder.js +37 -66
  3. package/dist/server/app/monitoring/MonitoringDashboard.js +329 -0
  4. package/dist/server/app/monitoring/action.js +147 -0
  5. package/dist/server/app/monitoring/page.js +8 -0
  6. package/dist/server/app/tasks/[slug]/ViewTask.js +81 -173
  7. package/dist/server/app/tasks/[slug]/runs/[r_id]/ViewRun.js +68 -164
  8. package/dist/server/app/tasks/[slug]/runs/[r_id]/_components/DropdownActions/DropdownActions.js +19 -25
  9. package/dist/server/components/Link.js +6 -15
  10. package/dist/server/components/MLInput.js +19 -22
  11. package/dist/server/components/Navbar/Navbar.js +25 -31
  12. package/dist/server/components/Navbar/NavbarContainer.js +7 -20
  13. package/dist/server/components/PageHeader.js +22 -54
  14. package/dist/server/components/StatusChip.js +5 -5
  15. package/dist/server/components/ui/alert.js +61 -0
  16. package/dist/server/components/ui/badge.js +37 -0
  17. package/dist/server/components/ui/breadcrumb.js +82 -0
  18. package/dist/server/components/ui/button.js +65 -0
  19. package/dist/server/components/ui/card.js +81 -0
  20. package/dist/server/components/ui/dropdown-menu.js +222 -0
  21. package/dist/server/components/ui/input.js +21 -0
  22. package/dist/server/components/ui/label.js +20 -0
  23. package/dist/server/components/ui/select.js +165 -0
  24. package/dist/server/components/ui/stack.js +104 -0
  25. package/dist/server/components/ui/table.js +77 -0
  26. package/dist/server/components/ui/tabs.js +59 -0
  27. package/dist/server/components/ui/typography.js +229 -0
  28. package/dist/server/utils/css/cn.js +11 -0
  29. package/package.json +15 -4
  30. package/dist/server/components/Icon.js +0 -22
@@ -2,35 +2,19 @@ import { Inter } from "next/font/google";
2
2
  const inter = Inter({
3
3
  subsets: ["latin"]
4
4
  });
5
+ import "./globals.css";
5
6
  import TopLoader from "../components/TopLoader";
6
7
  // import ServiceWorkerRegistration from "@/components/ServiceWorkerRegistration";
7
8
  import NavbarContainer from "../components/Navbar/NavbarContainer";
8
9
  export const metadata = {
9
10
  title: "Microlight",
10
11
  description: "Simple single server task runner"
11
- // icons: {
12
- // icon: [
13
- // { url: '/favicon.ico' },
14
- // { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
15
- // { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
16
- // { url: '/favicon-48x48.png', sizes: '48x48', type: 'image/png' },
17
- // ],
18
- // apple: [
19
- // { url: '/apple-touch-icon.png' },
20
- // ],
21
- // },
22
12
  };
23
13
  export default function RootLayout({
24
14
  children
25
15
  }) {
26
16
  return <html lang="en">
27
- <head>
28
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
29
- </head>
30
- <body className={inter.className} style={{
31
- margin: 0,
32
- padding: 0
33
- }}>
17
+ <body className={`${inter.className} m-0 p-0`}>
34
18
  {/* <ServiceWorkerRegistration /> */}
35
19
  <TopLoader />
36
20
  <NavbarContainer>
@@ -1,13 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { Table, Box, Container, Typography } from '@mui/joy';
4
3
  import React from 'react';
5
- import Icon from "../../../components/Icon";
6
-
7
- // import Link from '@/components/Link';
8
-
9
- import { Link } from 'switchless';
4
+ import { Folder, Send } from 'lucide-react';
5
+ import Link from "../../../components/Link";
10
6
  import PageHeader from "../../../components/PageHeader";
7
+ import { Typography } from "../../../components/ui/typography";
8
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../components/ui/table";
11
9
  function generateBreadcrumbs({
12
10
  params
13
11
  }) {
@@ -47,67 +45,40 @@ export default function ViewFolder({
47
45
  params
48
46
  });
49
47
  const dir = params.f_path ? '/' + params.f_path?.join('/') : '';
50
- return <>
51
- <Container>
48
+ return <div className="max-w-7xl mx-auto">
52
49
  <PageHeader breadcrumbs={breadcrumbs} header={{
53
- part1: 'Folder:',
54
- part2: folder.name
55
- }} />
50
+ part1: 'Folder:',
51
+ part2: folder.name
52
+ }} />
56
53
  <Typography level='body-sm'>{folder.description}</Typography>
57
- <Table sx={{
58
- pt: 1,
59
- "--Table-headerUnderlineThickness": "2px",
60
- "--TableCell-height": "25px"
61
- }}>
62
- <thead>
63
- <tr>
64
- <th>Name</th>
65
- <th>Description</th>
66
- <th>Tasks</th>
67
- </tr>
68
- </thead>
69
- <tbody>
70
- {contents.map(content => {
71
- return <React.Fragment key={`${content.type}__${content.slug}`}>
72
- <tr>
73
- <td>
74
- {content.type == 'folder' && <>
75
- <Box sx={{
76
- display: 'flex',
77
- alignItems: 'center',
78
- gap: 1
79
- }}>
80
- {/* <Icon icon='folder' color='#444444'/> */}
81
- <i class="fa-regular fa-folder fa-xl"></i>
82
- <Link href={'/library' + '/' + content.slug}>{content.name}</Link>
83
- </Box>
84
- </>}
85
- {content.type == 'task' && <>
86
- <Box sx={{
87
- display: 'flex',
88
- alignItems: 'center',
89
- gap: 1
90
- }}>
91
- {/* <Icon icon='send' color='#6435c9'/> */}
92
- <i class="fa-solid fa-paper-plane fa-xl" style={{
93
- color: '#6435c9'
94
- }}></i>
95
- <Link href={'/tasks/' + content.slug}>{content.name}</Link>
96
- </Box>
97
- </>}
98
- </td>
99
- <td>{content.description}</td>
100
- <td></td>
101
- </tr>
102
- </React.Fragment>;
54
+ <div className="mt-2">
55
+ <Table>
56
+ <TableHeader>
57
+ <TableRow>
58
+ <TableHead>Name</TableHead>
59
+ <TableHead>Description</TableHead>
60
+ <TableHead>Tasks</TableHead>
61
+ </TableRow>
62
+ </TableHeader>
63
+ <TableBody>
64
+ {contents.map(content => {
65
+ return <TableRow key={`${content.type}__${content.slug}`}>
66
+ <TableCell>
67
+ {content.type == 'folder' && <div className="flex items-center gap-2">
68
+ <Folder className="h-5 w-5 text-muted-foreground" />
69
+ <Link href={'/library' + '/' + content.slug}>{content.name}</Link>
70
+ </div>}
71
+ {content.type == 'task' && <div className="flex items-center gap-2">
72
+ <Send className="h-5 w-5 text-[#6435c9]" />
73
+ <Link href={'/tasks/' + content.slug}>{content.name}</Link>
74
+ </div>}
75
+ </TableCell>
76
+ <TableCell>{content.description}</TableCell>
77
+ <TableCell></TableCell>
78
+ </TableRow>;
103
79
  })}
104
-
105
-
106
- </tbody>
107
- </Table>
108
-
109
- {/* <pre>{JSON.stringify(folder,null,2)}</pre> */}
110
- {/* <pre>{JSON.stringify(contents,null,2)}</pre> */}
111
- </Container>
112
- </>;
80
+ </TableBody>
81
+ </Table>
82
+ </div>
83
+ </div>;
113
84
  }
@@ -0,0 +1,329 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { RefreshCw, Search, Play, Square, AlertCircle, CheckCircle } from 'lucide-react';
5
+ import { getOverviewData, getTasksData, getRunsData, getLogsData } from "./action";
6
+ import { Typography } from "../../components/ui/typography";
7
+ import { Card, CardContent } from "../../components/ui/card";
8
+ import { Button } from "../../components/ui/button";
9
+ import { Input } from "../../components/ui/input";
10
+ import { Badge } from "../../components/ui/badge";
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select";
12
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../components/ui/table";
13
+ export default function MonitoringDashboard() {
14
+ const [overview, setOverview] = useState(null);
15
+ const [tasks, setTasks] = useState([]);
16
+ const [recentRuns, setRecentRuns] = useState([]);
17
+ const [logs, setLogs] = useState([]);
18
+ const [loading, setLoading] = useState(true);
19
+ const [autoRefresh, setAutoRefresh] = useState(false);
20
+ const [filters, setFilters] = useState({
21
+ status: 'all',
22
+ task: '',
23
+ dateRange: '24h'
24
+ });
25
+ const fetchOverview = async () => {
26
+ try {
27
+ const data = await getOverviewData();
28
+ setOverview(data);
29
+ } catch (error) {
30
+ console.error('Failed to fetch overview:', error);
31
+ }
32
+ };
33
+ const fetchTasks = async () => {
34
+ try {
35
+ const data = await getTasksData();
36
+ setTasks(data);
37
+ } catch (error) {
38
+ console.error('Failed to fetch tasks:', error);
39
+ }
40
+ };
41
+ const DEFAULT_RUNS_LIMIT = 50;
42
+ const DEFAULT_LOGS_LIMIT = 100;
43
+ const fetchRecentRuns = async () => {
44
+ try {
45
+ const data = await getRunsData({
46
+ status: filters.status,
47
+ task: filters.task,
48
+ limit: DEFAULT_RUNS_LIMIT
49
+ });
50
+ setRecentRuns(data);
51
+ } catch (error) {
52
+ console.error('Failed to fetch recent runs:', error);
53
+ }
54
+ };
55
+ const fetchLogs = async () => {
56
+ try {
57
+ const data = await getLogsData({
58
+ limit: DEFAULT_LOGS_LIMIT
59
+ });
60
+ setLogs(data);
61
+ } catch (error) {
62
+ console.error('Failed to fetch logs:', error);
63
+ }
64
+ };
65
+ const refreshAll = async () => {
66
+ setLoading(true);
67
+ await Promise.all([fetchOverview(), fetchTasks(), fetchRecentRuns(), fetchLogs()]);
68
+ setLoading(false);
69
+ };
70
+ useEffect(() => {
71
+ refreshAll();
72
+ }, [filters]);
73
+ const REFRESH_INTERVAL = 30000; // Refresh every 30 seconds
74
+
75
+ useEffect(() => {
76
+ let interval;
77
+ if (autoRefresh) {
78
+ interval = setInterval(refreshAll, REFRESH_INTERVAL);
79
+ }
80
+ return () => {
81
+ if (interval) clearInterval(interval);
82
+ };
83
+ }, [autoRefresh]);
84
+ const getStatusVariant = status => {
85
+ switch (status) {
86
+ case 'complete':
87
+ return 'success';
88
+ case 'failed':
89
+ return 'destructive';
90
+ case 'running':
91
+ return 'default';
92
+ case 'pending':
93
+ return 'secondary';
94
+ default:
95
+ return 'secondary';
96
+ }
97
+ };
98
+ const getStatusIcon = status => {
99
+ switch (status) {
100
+ case 'complete':
101
+ return <CheckCircle className="h-3 w-3" />;
102
+ case 'failed':
103
+ return <AlertCircle className="h-3 w-3" />;
104
+ case 'running':
105
+ return <Play className="h-3 w-3" />;
106
+ case 'pending':
107
+ return <Square className="h-3 w-3" />;
108
+ default:
109
+ return <Square className="h-3 w-3" />;
110
+ }
111
+ };
112
+ const formatDuration = duration => {
113
+ if (!duration) return 'N/A';
114
+ const seconds = Math.floor(duration / 1000);
115
+ const minutes = Math.floor(seconds / 60);
116
+ const hours = Math.floor(minutes / 60);
117
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
118
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
119
+ return `${seconds}s`;
120
+ };
121
+ const formatDate = dateString => {
122
+ if (!dateString) return 'N/A';
123
+ return new Date(dateString).toLocaleString();
124
+ };
125
+ if (loading && !overview) {
126
+ return <div className="p-6">
127
+ <Typography level="h2">Loading...</Typography>
128
+ </div>;
129
+ }
130
+ return <div className="p-6">
131
+ {/* Header */}
132
+ <div className="mb-6 flex justify-between items-center">
133
+ <Typography level="h2">Task Monitoring Dashboard</Typography>
134
+ <div className="flex gap-2 items-center">
135
+ <Button variant={autoRefresh ? 'default' : 'outline'} onClick={() => setAutoRefresh(!autoRefresh)} size="sm">
136
+ {autoRefresh ? 'Auto-refresh ON' : 'Auto-refresh OFF'}
137
+ </Button>
138
+ <Button variant="outline" onClick={refreshAll} loading={loading} size="icon">
139
+ <RefreshCw className="h-4 w-4" />
140
+ </Button>
141
+ </div>
142
+ </div>
143
+
144
+ {/* Overview Cards */}
145
+ {overview && <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-6">
146
+ <Card>
147
+ <CardContent className="pt-4">
148
+ <Typography level="title-sm" color="muted">Total Runs</Typography>
149
+ <Typography level="h2">{overview.totalRuns?.toLocaleString()}</Typography>
150
+ </CardContent>
151
+ </Card>
152
+ <Card>
153
+ <CardContent className="pt-4">
154
+ <Typography level="title-sm" color="success">Success Rate</Typography>
155
+ <Typography level="h2" color="success">
156
+ {overview.successRate ? `${overview.successRate.toFixed(1)}%` : 'N/A'}
157
+ </Typography>
158
+ </CardContent>
159
+ </Card>
160
+ <Card>
161
+ <CardContent className="pt-4">
162
+ <Typography level="title-sm" color="primary">Running Tasks</Typography>
163
+ <Typography level="h2" color="primary">{overview.runningTasks || 0}</Typography>
164
+ </CardContent>
165
+ </Card>
166
+ <Card>
167
+ <CardContent className="pt-4">
168
+ <Typography level="title-sm" color="danger">Failed (24h)</Typography>
169
+ <Typography level="h2" color="danger">{overview.recentFailures || 0}</Typography>
170
+ </CardContent>
171
+ </Card>
172
+ </div>}
173
+
174
+ {/* Filters */}
175
+ <div className="mb-6 flex gap-4 items-center flex-wrap">
176
+ <Select value={filters.status} onValueChange={value => setFilters(prev => ({
177
+ ...prev,
178
+ status: value
179
+ }))}>
180
+ <SelectTrigger size="sm" className="w-[140px]">
181
+ <SelectValue />
182
+ </SelectTrigger>
183
+ <SelectContent>
184
+ <SelectItem value="all">All Status</SelectItem>
185
+ <SelectItem value="complete">Complete</SelectItem>
186
+ <SelectItem value="failed">Failed</SelectItem>
187
+ <SelectItem value="running">Running</SelectItem>
188
+ <SelectItem value="pending">Pending</SelectItem>
189
+ </SelectContent>
190
+ </Select>
191
+
192
+ <div className="relative">
193
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
194
+ <Input placeholder="Filter by task name..." value={filters.task} onChange={e => setFilters(prev => ({
195
+ ...prev,
196
+ task: e.target.value
197
+ }))} size="sm" className="pl-8 w-[200px]" />
198
+ </div>
199
+
200
+ <Select value={filters.dateRange} onValueChange={value => setFilters(prev => ({
201
+ ...prev,
202
+ dateRange: value
203
+ }))}>
204
+ <SelectTrigger size="sm" className="w-[120px]">
205
+ <SelectValue />
206
+ </SelectTrigger>
207
+ <SelectContent>
208
+ <SelectItem value="1h">Last Hour</SelectItem>
209
+ <SelectItem value="24h">Last 24h</SelectItem>
210
+ <SelectItem value="7d">Last 7 days</SelectItem>
211
+ <SelectItem value="30d">Last 30 days</SelectItem>
212
+ </SelectContent>
213
+ </Select>
214
+ </div>
215
+
216
+ {/* Task Statistics Table */}
217
+ <div className="mb-8">
218
+ <Typography level="h3" className="mb-4">Task Statistics</Typography>
219
+ <div className="rounded-md border overflow-auto">
220
+ <Table>
221
+ <TableHeader>
222
+ <TableRow>
223
+ <TableHead>Task Name</TableHead>
224
+ <TableHead>Total Runs</TableHead>
225
+ <TableHead>Success Rate</TableHead>
226
+ <TableHead>Last Run</TableHead>
227
+ <TableHead>Status</TableHead>
228
+ <TableHead>Avg Duration</TableHead>
229
+ </TableRow>
230
+ </TableHeader>
231
+ <TableBody>
232
+ {tasks.map(task => <TableRow key={task.task}>
233
+ <TableCell>
234
+ <Typography level="body-sm" weight="medium">
235
+ {task.task}
236
+ </Typography>
237
+ </TableCell>
238
+ <TableCell>{task.totalRuns}</TableCell>
239
+ <TableCell>
240
+ <Typography level="body-sm" color={task.successRate > 90 ? 'success' : task.successRate > 70 ? 'warning' : 'danger'}>
241
+ {task.successRate.toFixed(1)}%
242
+ </Typography>
243
+ </TableCell>
244
+ <TableCell>
245
+ <Typography level="body-sm">
246
+ {formatDate(task.lastRun)}
247
+ </Typography>
248
+ </TableCell>
249
+ <TableCell>
250
+ <Badge variant={getStatusVariant(task.lastStatus)} className="gap-1">
251
+ {getStatusIcon(task.lastStatus)}
252
+ {task.lastStatus}
253
+ </Badge>
254
+ </TableCell>
255
+ <TableCell>
256
+ <Typography level="body-sm">
257
+ {formatDuration(task.avgDuration)}
258
+ </Typography>
259
+ </TableCell>
260
+ </TableRow>)}
261
+ </TableBody>
262
+ </Table>
263
+ </div>
264
+ </div>
265
+
266
+ {/* Recent Runs */}
267
+ <div className="mb-8">
268
+ <Typography level="h3" className="mb-4">Recent Runs</Typography>
269
+ <div className="rounded-md border overflow-auto max-h-[400px]">
270
+ <Table>
271
+ <TableHeader>
272
+ <TableRow>
273
+ <TableHead>Task</TableHead>
274
+ <TableHead>Status</TableHead>
275
+ <TableHead>Started</TableHead>
276
+ <TableHead>Duration</TableHead>
277
+ <TableHead>Triggered By</TableHead>
278
+ </TableRow>
279
+ </TableHeader>
280
+ <TableBody>
281
+ {recentRuns.map(run => <TableRow key={run.id}>
282
+ <TableCell>
283
+ <Typography level="body-sm" weight="medium">
284
+ {run.task}
285
+ </Typography>
286
+ </TableCell>
287
+ <TableCell>
288
+ <Badge variant={getStatusVariant(run.status)} className="gap-1">
289
+ {getStatusIcon(run.status)}
290
+ {run.status}
291
+ </Badge>
292
+ </TableCell>
293
+ <TableCell>
294
+ <Typography level="body-sm">
295
+ {formatDate(run.started_at)}
296
+ </Typography>
297
+ </TableCell>
298
+ <TableCell>
299
+ <Typography level="body-sm">
300
+ {formatDuration(run.duration)}
301
+ </Typography>
302
+ </TableCell>
303
+ <TableCell>
304
+ <Badge variant="outline">
305
+ {run.triggered_by}
306
+ </Badge>
307
+ </TableCell>
308
+ </TableRow>)}
309
+ </TableBody>
310
+ </Table>
311
+ </div>
312
+ </div>
313
+
314
+ {/* Recent Logs */}
315
+ <div>
316
+ <Typography level="h3" className="mb-4">Recent Logs</Typography>
317
+ <div className="rounded-md border p-4 max-h-[300px] overflow-auto bg-muted/30">
318
+ {logs.map(log => <div key={log.id} className="mb-2 font-mono text-sm">
319
+ <Typography level="body-xs" color="muted">
320
+ [{formatDate(log.created_at)}] [{log.type}]
321
+ </Typography>
322
+ <Typography level="body-sm" className="whitespace-pre-wrap">
323
+ {log.content}
324
+ </Typography>
325
+ </div>)}
326
+ </div>
327
+ </div>
328
+ </div>;
329
+ }
@@ -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
+ }