@goplusvn/core 0.1.59 → 0.1.60

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 (139) hide show
  1. package/PLATFORM.md +18 -0
  2. package/bin/goerp-init.mjs +141 -0
  3. package/package.json +4 -4
  4. package/src/cron/db-cron-manager.ts +3 -3
  5. package/src/cron/index.ts +2 -2
  6. package/src/{infrastructure/cron/cron-manager.ts → cron/simple-cron-job.ts} +1 -1
  7. package/src/infrastructure/__tests__/architecture-verification.spec.ts +13 -97
  8. package/src/infrastructure/index.ts +4 -7
  9. package/src/ui/management/index.ts +3 -2
  10. package/templates/starter-app/.dockerignore +41 -0
  11. package/templates/starter-app/.env.example +25 -0
  12. package/templates/starter-app/AGENTS.md +52 -0
  13. package/templates/starter-app/Dockerfile +74 -0
  14. package/templates/starter-app/README.md +141 -0
  15. package/templates/starter-app/gitignore +9 -0
  16. package/templates/starter-app/next.config.mjs +50 -0
  17. package/templates/starter-app/package.json +55 -0
  18. package/templates/starter-app/postcss.config.mjs +5 -0
  19. package/templates/starter-app/prisma/migrations/20260801000000_init/migration.sql +504 -0
  20. package/templates/starter-app/prisma/migrations/20260801091814_goerp_audit_logs_0001_init/migration.sql +33 -0
  21. package/templates/starter-app/prisma/migrations/20260801091815_goerp_background_tasks_0001_init/migration.sql +23 -0
  22. package/templates/starter-app/prisma/migrations/20260801091816_goerp_error_logs_0001_init/migration.sql +32 -0
  23. package/templates/starter-app/prisma/migrations/20260801091817_goerp_notifications_0001_init/migration.sql +59 -0
  24. package/templates/starter-app/prisma/migrations/20260801091818_goerp_system_jobs_0001_init/migration.sql +47 -0
  25. package/templates/starter-app/prisma/schema/auth.prisma +87 -0
  26. package/templates/starter-app/prisma/schema/domain.prisma +24 -0
  27. package/templates/starter-app/prisma/schema/goerp-audit-logs.prisma +23 -0
  28. package/templates/starter-app/prisma/schema/goerp-background-tasks.prisma +25 -0
  29. package/templates/starter-app/prisma/schema/goerp-error-logs.prisma +31 -0
  30. package/templates/starter-app/prisma/schema/goerp-notifications.prisma +49 -0
  31. package/templates/starter-app/prisma/schema/goerp-system-jobs.prisma +45 -0
  32. package/templates/starter-app/prisma/schema/organization.prisma +31 -0
  33. package/templates/starter-app/prisma/schema/rbac.prisma +100 -0
  34. package/templates/starter-app/prisma/schema/schema.prisma +8 -0
  35. package/templates/starter-app/prisma/schema/system.prisma +22 -0
  36. package/templates/starter-app/prisma/seed.ts +127 -0
  37. package/templates/starter-app/prisma.config.ts +20 -0
  38. package/templates/starter-app/public/.gitkeep +2 -0
  39. package/templates/starter-app/scripts/rbac-sync.ts +235 -0
  40. package/templates/starter-app/src/__tests__/architecture.test.ts +151 -0
  41. package/templates/starter-app/src/app/[lang]/(main)/admin/system/audit/page.tsx +24 -0
  42. package/templates/starter-app/src/app/[lang]/(main)/admin/system/error-logs/page.tsx +21 -0
  43. package/templates/starter-app/src/app/[lang]/(main)/admin/system/jobs/page.tsx +18 -0
  44. package/templates/starter-app/src/app/[lang]/(main)/admin/system/settings/page.tsx +22 -0
  45. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +41 -0
  46. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +13 -0
  47. package/templates/starter-app/src/app/[lang]/(main)/notifications/page.tsx +13 -0
  48. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +130 -0
  49. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +60 -0
  50. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +41 -0
  51. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +48 -0
  52. package/templates/starter-app/src/app/[lang]/(main)/tasks/page.tsx +13 -0
  53. package/templates/starter-app/src/app/[lang]/(plain)/layout.tsx +10 -0
  54. package/templates/starter-app/src/app/[lang]/(plain)/sign-in/page.tsx +39 -0
  55. package/templates/starter-app/src/app/[lang]/layout.tsx +21 -0
  56. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +60 -0
  57. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +39 -0
  58. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +96 -0
  59. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +17 -0
  60. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +19 -0
  61. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +20 -0
  62. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +49 -0
  63. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +28 -0
  64. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +24 -0
  65. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +23 -0
  66. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +13 -0
  67. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +13 -0
  68. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +14 -0
  69. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +23 -0
  70. package/templates/starter-app/src/app/api/error-logs/route.ts +124 -0
  71. package/templates/starter-app/src/app/api/files/[...key]/route.ts +25 -0
  72. package/templates/starter-app/src/app/api/notifications/read/route.ts +29 -0
  73. package/templates/starter-app/src/app/api/notifications/route.ts +26 -0
  74. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +14 -0
  75. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +11 -0
  76. package/templates/starter-app/src/app/api/roles/[id]/route.ts +14 -0
  77. package/templates/starter-app/src/app/api/roles/route.ts +18 -0
  78. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +52 -0
  79. package/templates/starter-app/src/app/api/tasks/route.ts +37 -0
  80. package/templates/starter-app/src/app/api/upload/route.ts +15 -0
  81. package/templates/starter-app/src/app/globals.css +15 -0
  82. package/templates/starter-app/src/app/layout.tsx +16 -0
  83. package/templates/starter-app/src/app/page.tsx +8 -0
  84. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +30 -0
  85. package/templates/starter-app/src/configs/entities/department.config.ts +24 -0
  86. package/templates/starter-app/src/configs/entities/index.ts +13 -0
  87. package/templates/starter-app/src/configs/i18n.ts +12 -0
  88. package/templates/starter-app/src/configs/permissions/index.ts +45 -0
  89. package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +31 -0
  90. package/templates/starter-app/src/configs/permissions/system.permissions.ts +131 -0
  91. package/templates/starter-app/src/configs/permissions/types.ts +63 -0
  92. package/templates/starter-app/src/configs/tenant.ts +18 -0
  93. package/templates/starter-app/src/data/dictionary.ts +8 -0
  94. package/templates/starter-app/src/data/navigations.ts +61 -0
  95. package/templates/starter-app/src/instrumentation.ts +105 -0
  96. package/templates/starter-app/src/lib/api-handler.ts +157 -0
  97. package/templates/starter-app/src/lib/auth-client.ts +57 -0
  98. package/templates/starter-app/src/lib/auth.ts +62 -0
  99. package/templates/starter-app/src/lib/better-auth.ts +107 -0
  100. package/templates/starter-app/src/lib/branch-scope.ts +53 -0
  101. package/templates/starter-app/src/lib/cron/db-cron-manager.ts +42 -0
  102. package/templates/starter-app/src/lib/crud/index.ts +13 -0
  103. package/templates/starter-app/src/lib/errors/app-error.ts +2 -0
  104. package/templates/starter-app/src/lib/errors/error-handler.ts +5 -0
  105. package/templates/starter-app/src/lib/errors/log-server-error.ts +15 -0
  106. package/templates/starter-app/src/lib/errors/server-error.ts +11 -0
  107. package/templates/starter-app/src/lib/logger.ts +30 -0
  108. package/templates/starter-app/src/lib/page-guard.ts +35 -0
  109. package/templates/starter-app/src/lib/prisma.ts +80 -0
  110. package/templates/starter-app/src/lib/rbac/access.ts +87 -0
  111. package/templates/starter-app/src/lib/storage.ts +28 -0
  112. package/templates/starter-app/src/providers/index.tsx +54 -0
  113. package/templates/starter-app/src/providers/mode-provider.tsx +31 -0
  114. package/templates/starter-app/src/providers/theme-provider.tsx +21 -0
  115. package/templates/starter-app/src/proxy.ts +45 -0
  116. package/templates/starter-app/src/server/services/notification-service.ts +31 -0
  117. package/templates/starter-app/src/server/services/system-config-service.ts +163 -0
  118. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +58 -0
  119. package/templates/starter-app/src/server/tasks/index.ts +16 -0
  120. package/templates/starter-app/src/server/tasks/task-runner.ts +40 -0
  121. package/templates/starter-app/src/types/session.ts +29 -0
  122. package/templates/starter-app/tsconfig.json +47 -0
  123. package/templates/starter-app/vitest.config.ts +17 -0
  124. package/src/infrastructure/cron/index.ts +0 -6
  125. package/src/infrastructure/event-bus/event-bus.ts +0 -145
  126. package/src/infrastructure/event-bus/index.ts +0 -2
  127. package/src/infrastructure/event-bus/types.ts +0 -22
  128. package/src/infrastructure/lock/decorators.ts +0 -67
  129. package/src/infrastructure/lock/index.ts +0 -2
  130. package/src/infrastructure/lock/lock-manager.ts +0 -33
  131. package/src/plugin/apps-registry.ts +0 -97
  132. package/src/plugin/index.ts +0 -5
  133. package/src/plugin/types.ts +0 -41
  134. package/src/ui/management/audit-log-page.tsx +0 -14
  135. package/src/ui/management/job-management.tsx +0 -308
  136. package/src/workflow/activity-timeline.tsx +0 -412
  137. package/src/workflow/approval-workflow.tsx +0 -31
  138. package/src/workflow/index.ts +0 -2
  139. /package/src/{infrastructure/cron → cron}/types.ts +0 -0
@@ -1,67 +0,0 @@
1
- import type { LockOptions } from "./lock-manager";
2
- import { LockManager } from "./lock-manager";
3
-
4
- /**
5
- * Decorator to acquire a lock before executing a method.
6
- * Requires the class to have a `lockManager` property.
7
- */
8
- export function WithLock(keyPath: string, options?: LockOptions) {
9
- return function (
10
- target: any,
11
- propertyKey: string,
12
- descriptor: PropertyDescriptor,
13
- ) {
14
- const originalMethod = descriptor.value;
15
-
16
- descriptor.value = async function (...args: any[]) {
17
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
- const self = this as any;
19
-
20
- if (!self.lockManager) {
21
- throw new Error(
22
- `Class '${target.constructor.name}' must have a 'lockManager' property to use @WithLock`,
23
- );
24
- }
25
-
26
- const resolvedKey = resolveKey(keyPath, args);
27
- // Prefix lock key to avoid collisions
28
- const lockKey = `lock:${resolvedKey}`;
29
-
30
- const acquired = await self.lockManager.acquire(lockKey, options);
31
-
32
- if (!acquired) {
33
- throw new Error(`Could not acquire lock for key: ${lockKey}`);
34
- }
35
-
36
- try {
37
- return await originalMethod.apply(this, args);
38
- } finally {
39
- await self.lockManager.release(lockKey);
40
- }
41
- };
42
-
43
- return descriptor;
44
- };
45
- }
46
-
47
- function resolveKey(path: string, args: any[]): string {
48
- // If path format is "{0}" or "{0}.id"
49
- const match = path.match(/\{(\d+)\}(.*)/);
50
- if (match) {
51
- const argIndex = parseInt(match[1]);
52
- let value = args[argIndex];
53
- const subPath = match[2]; // e.g., ".id"
54
-
55
- if (!subPath) return String(value);
56
-
57
- const props = subPath.replace(/^\./, "").split(".");
58
- for (const prop of props) {
59
- if (value === undefined || value === null) break;
60
- value = value[prop];
61
- }
62
- return String(value);
63
- }
64
-
65
- // Legacy/Simple support: direct string
66
- return path;
67
- }
@@ -1,2 +0,0 @@
1
- export * from "./lock-manager";
2
- export * from "./decorators";
@@ -1,33 +0,0 @@
1
- import type { Cache } from "../cache/types";
2
-
3
- export interface LockOptions {
4
- ttl?: number;
5
- retryTimes?: number;
6
- retryDelay?: number;
7
- }
8
-
9
- export class LockManager {
10
- constructor(private cache: Cache) {}
11
-
12
- async acquire(key: string, options?: LockOptions): Promise<boolean> {
13
- const ttl = options?.ttl || 10;
14
- const retryTimes = options?.retryTimes || 1;
15
- const retryDelay = options?.retryDelay || 100;
16
-
17
- for (let i = 0; i < retryTimes; i++) {
18
- const existing = await this.cache.get(key);
19
- if (!existing) {
20
- await this.cache.set(key, "LOCKED", ttl);
21
- return true;
22
- }
23
- if (i < retryTimes - 1) {
24
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
25
- }
26
- }
27
- return false;
28
- }
29
-
30
- async release(key: string): Promise<void> {
31
- await this.cache.del(key);
32
- }
33
- }
@@ -1,97 +0,0 @@
1
- // @goerp/core/plugin
2
- // AppsRegistry - Central registry for GoerpApp instances
3
-
4
- import type { EntityConfig, NavigationType } from "../types"
5
-
6
- import type { GoerpApp } from "./types"
7
-
8
- /**
9
- * AppsRegistry - Manages registration and lookup of GoerpApp
10
- * Host app registers installed apps; registry merges navigation and entity configs
11
- */
12
- export class AppsRegistry {
13
- private apps: GoerpApp[] = []
14
- private appsById = new Map<string, GoerpApp>()
15
-
16
- /**
17
- * Register an app. Later registrations override earlier entity configs for same entity.
18
- */
19
- register(app: GoerpApp): void {
20
- if (this.appsById.has(app.appId)) {
21
- console.warn(`[AppsRegistry] App "${app.appId}" already registered. Replacing.`)
22
- }
23
- this.appsById.set(app.appId, app)
24
- this.apps = Array.from(this.appsById.values())
25
- }
26
-
27
- /**
28
- * Register multiple apps in order
29
- */
30
- registerAll(apps: GoerpApp[]): void {
31
- for (const app of apps) {
32
- this.register(app)
33
- }
34
- }
35
-
36
- /**
37
- * Get app by appId
38
- */
39
- getApp(appId: string): GoerpApp | undefined {
40
- return this.appsById.get(appId)
41
- }
42
-
43
- /**
44
- * Get all registered apps
45
- */
46
- getAllApps(): GoerpApp[] {
47
- return [...this.apps]
48
- }
49
-
50
- /**
51
- * Get merged navigation from all apps (order: registration order)
52
- * Groups with same title have their items merged
53
- */
54
- getNavigation(): NavigationType[] {
55
- const byTitle = new Map<string, NavigationType>()
56
-
57
- for (const app of this.apps) {
58
- for (const group of app.navigation) {
59
- const existing = byTitle.get(group.title)
60
- const items = group.items ?? []
61
- if (existing) {
62
- existing.items = [...(existing.items ?? []), ...items]
63
- } else {
64
- byTitle.set(group.title, { ...group, items: [...items] })
65
- }
66
- }
67
- }
68
- return Array.from(byTitle.values())
69
- }
70
-
71
- /**
72
- * Get entity config for entity key.
73
- * Later-registered apps override earlier for same entity.
74
- */
75
- getEntityConfig(entity: string): EntityConfig | undefined {
76
- for (let i = this.apps.length - 1; i >= 0; i--) {
77
- const config = this.apps[i].entityConfigs[entity]
78
- if (config) return config
79
- }
80
- return undefined
81
- }
82
-
83
- /**
84
- * Check if entity exists in any app
85
- */
86
- hasEntity(entity: string): boolean {
87
- return this.getEntityConfig(entity) !== undefined
88
- }
89
-
90
- /**
91
- * Clear all registrations (for testing)
92
- */
93
- clear(): void {
94
- this.apps = []
95
- this.appsById.clear()
96
- }
97
- }
@@ -1,5 +0,0 @@
1
- // @goerp/core/plugin
2
- // GoerpApp API - Plugin/App types and registry
3
-
4
- export { AppsRegistry } from "./apps-registry"
5
- export type { GoerpApp, RouteDefinition } from "./types"
@@ -1,41 +0,0 @@
1
- // @goerp/core/plugin
2
- // GoerpApp types - maps to AppDefinition in core-db
3
-
4
- import type { ComponentType } from "react"
5
-
6
- import type { EntityConfig, NavigationType } from "../types"
7
-
8
- /**
9
- * Route definition for /app/[appId]/[pageId]
10
- * Used when an app has custom pages beyond CRUD
11
- */
12
- export interface RouteDefinition {
13
- pageId: string
14
- label: string
15
- path: string
16
- component: ComponentType<Record<string, unknown>>
17
- permission?: string
18
- }
19
-
20
- /**
21
- * GoerpApp - Plugin/App descriptor
22
- * Maps to AppDefinition.appId in core-db
23
- */
24
- export interface GoerpApp {
25
- /** Unique app identifier - maps to AppDefinition.appId */
26
- appId: string
27
- /** Display name */
28
- name: string
29
- /** Semantic version */
30
- version: string
31
- /** Human-readable description */
32
- description?: string
33
- /** Core system app (always enabled) */
34
- isCore?: boolean
35
- /** Navigation items for sidebar */
36
- navigation: NavigationType[]
37
- /** Entity configs for CRUD (/crud/[entity]) */
38
- entityConfigs: Record<string, EntityConfig>
39
- /** Custom routes for /app/[appId]/[pageId] */
40
- routes?: RouteDefinition[]
41
- }
@@ -1,14 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * @deprecated Dùng `@goerp/core/system/pages/system-audit-page` (`SystemAuditPage`).
5
- *
6
- * Bản cũ ở đây đọc `auditManager` trong RAM ngay trong CLIENT component — luôn
7
- * rỗng, vì dữ liệu audit nằm ở server. Nay trỏ thẳng sang trang thật (đọc bảng
8
- * `audit_logs` qua API) để app đang import tên `AuditLogPage` không phải sửa
9
- * ngay; app mới hãy import thẳng `SystemAuditPage`.
10
- */
11
- export {
12
- SystemAuditPage as AuditLogPage,
13
- type SystemAuditPageProps as AuditLogPageProps,
14
- } from "../../system/pages/system-audit-page";
@@ -1,308 +0,0 @@
1
- "use client";
2
-
3
- import React, { useState, useEffect } from "react";
4
- import type { Job, JobLog } from "../../system/job-manager";
5
- import { jobManager } from "../../system/job-manager";
6
- // Ideally we would import components from @goerp/core/ui/primitives but we are inside core
7
- // So we mock or use basic HTML/Tailwind for now to avoid circular deps if structure is complex
8
- // In a real app we'd use the proper UI components.
9
- // Assuming this page is used inside an app that provides the context or we use relative imports carefully.
10
-
11
- // Relative imports to core UI components
12
- import { Badge } from "../primitives/badge";
13
- import { Button } from "../primitives/button";
14
- import { Card, CardHeader, CardTitle, CardContent } from "../primitives/card";
15
- import { ScrollArea } from "../primitives/scroll-area";
16
- import {
17
- Table,
18
- TableBody,
19
- TableCell,
20
- TableHead,
21
- TableHeader,
22
- TableRow,
23
- } from "../primitives/table";
24
-
25
- import {
26
- Dialog,
27
- DialogContent,
28
- DialogHeader,
29
- DialogTitle,
30
- DialogTrigger,
31
- DialogFooter,
32
- } from "../primitives/dialog";
33
- import { Input } from "../primitives/input";
34
- import { Label } from "../primitives/label";
35
-
36
- export const JobManagementPage = () => {
37
- const [jobs, setJobs] = useState<Job[]>([]);
38
- const [selectedJob, setSelectedJob] = useState<string | null>(null);
39
- const [logs, setLogs] = useState<JobLog[]>([]);
40
- const [refreshTrigger, setRefreshTrigger] = useState(0);
41
- const [editingJob, setEditingJob] = useState<Job | null>(null);
42
- const [editForm, setEditForm] = useState({ schedule: "", description: "" });
43
- const [isDialogOpen, setIsDialogOpen] = useState(false);
44
-
45
- // Initial load and periodic refresh
46
- useEffect(() => {
47
- setJobs(jobManager.getJobs());
48
- const interval = setInterval(() => {
49
- setJobs(jobManager.getJobs());
50
- if (selectedJob) {
51
- // Refresh logs if a job is selected
52
- setLogs(jobManager.getLogs(selectedJob));
53
- }
54
- }, 1000); // 1s refresh for demo niceness
55
- return () => clearInterval(interval);
56
- }, [refreshTrigger, selectedJob]);
57
-
58
- useEffect(() => {
59
- if (selectedJob) {
60
- setLogs(jobManager.getLogs(selectedJob));
61
- } else {
62
- setLogs([]);
63
- }
64
- }, [selectedJob]);
65
-
66
- const handleStart = (id: string) => {
67
- jobManager.startJob(id);
68
- setRefreshTrigger((prev) => prev + 1);
69
- };
70
-
71
- const handleStop = (id: string) => {
72
- jobManager.stopJob(id);
73
- setRefreshTrigger((prev) => prev + 1);
74
- };
75
-
76
- const handleEditClick = (job: Job, e: React.MouseEvent) => {
77
- e.stopPropagation();
78
- setEditingJob(job);
79
- setEditForm({
80
- schedule: job.schedule || "",
81
- description: job.description || "",
82
- });
83
- setIsDialogOpen(true);
84
- };
85
-
86
- const handleSaveEdit = () => {
87
- if (editingJob) {
88
- jobManager.updateJob(editingJob.id, editForm);
89
- setRefreshTrigger((prev) => prev + 1);
90
- setIsDialogOpen(false);
91
- setEditingJob(null);
92
- }
93
- };
94
-
95
- const getStatusBadge = (status: string) => {
96
- switch (status) {
97
- case "running":
98
- return (
99
- <Badge
100
- variant="default"
101
- className="bg-emerald-500 hover:bg-emerald-600"
102
- >
103
- Running
104
- </Badge>
105
- );
106
- case "stopped":
107
- return <Badge variant="secondary">Stopped</Badge>;
108
- case "failed":
109
- return <Badge variant="destructive">Failed</Badge>;
110
- default:
111
- return <Badge variant="outline">{status}</Badge>;
112
- }
113
- };
114
-
115
- const [logSearchTerm, setLogSearchTerm] = useState("");
116
-
117
- const filteredLogs = logs.filter(
118
- (log) =>
119
- log.message.toLowerCase().includes(logSearchTerm.toLowerCase()) ||
120
- log.level.toLowerCase().includes(logSearchTerm.toLowerCase()),
121
- );
122
-
123
- return (
124
- <div className="p-6 space-y-6 h-full flex flex-col">
125
- <div className="flex justify-between items-center mb-4">
126
- <div>
127
- <h1 className="text-2xl font-bold tracking-tight">Job Management</h1>
128
- <p className="text-muted-foreground text-sm">
129
- Monitor and control system background jobs.
130
- </p>
131
- </div>
132
- <Button
133
- onClick={() => setRefreshTrigger((prev) => prev + 1)}
134
- variant="outline"
135
- >
136
- Refresh
137
- </Button>
138
- </div>
139
-
140
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6 flex-1 min-h-0">
141
- {/* Job List */}
142
- <Card className="md:col-span-2 flex flex-col min-h-0">
143
- <CardHeader>
144
- <CardTitle>Active Jobs</CardTitle>
145
- </CardHeader>
146
- <CardContent className="flex-1 overflow-auto p-0">
147
- <Table>
148
- <TableHeader>
149
- <TableRow>
150
- <TableHead>Job Name</TableHead>
151
- <TableHead>Schedule</TableHead>
152
- <TableHead>Status</TableHead>
153
- <TableHead>Last Run</TableHead>
154
- <TableHead className="text-right">Actions</TableHead>
155
- </TableRow>
156
- </TableHeader>
157
- <TableBody>
158
- {jobs.map((job) => (
159
- <TableRow
160
- key={job.id}
161
- className={`cursor-pointer ${selectedJob === job.id ? "bg-muted/50" : ""}`}
162
- onClick={() => setSelectedJob(job.id)}
163
- >
164
- <TableCell>
165
- <div className="font-medium">{job.name}</div>
166
- <div className="text-xs text-muted-foreground">
167
- {job.description}
168
- </div>
169
- </TableCell>
170
- <TableCell>{job.schedule}</TableCell>
171
- <TableCell>{getStatusBadge(job.status)}</TableCell>
172
- <TableCell className="text-muted-foreground">
173
- {job.lastRun ? job.lastRun.toLocaleTimeString() : "-"}
174
- </TableCell>
175
- <TableCell className="text-right">
176
- <div
177
- className="flex justify-end gap-2"
178
- onClick={(e) => e.stopPropagation()}
179
- >
180
- <Button
181
- size="sm"
182
- variant="outline"
183
- onClick={(e) => handleEditClick(job, e)}
184
- >
185
- Edit
186
- </Button>
187
- {job.status === "running" ? (
188
- <Button
189
- size="sm"
190
- variant="destructive"
191
- onClick={() => handleStop(job.id)}
192
- >
193
- Stop
194
- </Button>
195
- ) : (
196
- <Button size="sm" onClick={() => handleStart(job.id)}>
197
- Start
198
- </Button>
199
- )}
200
- </div>
201
- </TableCell>
202
- </TableRow>
203
- ))}
204
- </TableBody>
205
- </Table>
206
- </CardContent>
207
- </Card>
208
-
209
- {/* Edit Dialog */}
210
- <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
211
- <DialogContent>
212
- <DialogHeader>
213
- <DialogTitle>Edit Job: {editingJob?.name}</DialogTitle>
214
- </DialogHeader>
215
- <div className="space-y-4 py-4">
216
- <div className="space-y-2">
217
- <Label htmlFor="schedule">Schedule</Label>
218
- <Input
219
- id="schedule"
220
- value={editForm.schedule}
221
- onChange={(e) =>
222
- setEditForm({ ...editForm, schedule: e.target.value })
223
- }
224
- />
225
- </div>
226
- <div className="space-y-2">
227
- <Label htmlFor="description">Description</Label>
228
- <Input
229
- id="description"
230
- value={editForm.description}
231
- onChange={(e) =>
232
- setEditForm({ ...editForm, description: e.target.value })
233
- }
234
- />
235
- </div>
236
- </div>
237
- <DialogFooter>
238
- <Button variant="outline" onClick={() => setIsDialogOpen(false)}>
239
- Cancel
240
- </Button>
241
- <Button onClick={handleSaveEdit}>Save Changes</Button>
242
- </DialogFooter>
243
- </DialogContent>
244
- </Dialog>
245
-
246
- {/* Log Viewer */}
247
- <Card className="flex flex-col min-h-0 h-full">
248
- <CardHeader className="pb-3 border-b flex flex-row items-center justify-between space-y-0">
249
- <CardTitle>
250
- {selectedJob
251
- ? `Logs: ${jobs.find((j) => j.id === selectedJob)?.name}`
252
- : "Job Logs"}
253
- </CardTitle>
254
- </CardHeader>
255
- <div className="p-2 border-b">
256
- <input
257
- type="text"
258
- placeholder="Search logs..."
259
- className="w-full px-3 py-2 text-sm border rounded-md bg-background"
260
- value={logSearchTerm}
261
- onChange={(e) => setLogSearchTerm(e.target.value)}
262
- />
263
- </div>
264
- <CardContent className="flex-1 p-0 min-h-0 bg-slate-50 dark:bg-slate-900/50">
265
- <ScrollArea className="h-[600px] w-full p-4">
266
- {!selectedJob && (
267
- <div className="text-center text-muted-foreground py-10">
268
- Select a job to view its logs
269
- </div>
270
- )}
271
- {selectedJob && filteredLogs.length === 0 && (
272
- <div className="text-center text-muted-foreground py-10">
273
- {logs.length === 0
274
- ? "No logs available"
275
- : "No logs match your search"}
276
- </div>
277
- )}
278
- <div className="space-y-2 font-mono text-xs">
279
- {filteredLogs.map((log) => (
280
- <div
281
- key={log.id}
282
- className="flex gap-2 border-b border-border/50 pb-1 last:border-0"
283
- >
284
- <span className="text-muted-foreground whitespace-nowrap">
285
- {log.timestamp.toLocaleTimeString()}
286
- </span>
287
- <span
288
- className={`font-semibold uppercase w-16 ${
289
- log.level === "error"
290
- ? "text-red-500"
291
- : log.level === "warning"
292
- ? "text-yellow-500"
293
- : "text-blue-500"
294
- }`}
295
- >
296
- [{log.level}]
297
- </span>
298
- <span className="break-all">{log.message}</span>
299
- </div>
300
- ))}
301
- </div>
302
- </ScrollArea>
303
- </CardContent>
304
- </Card>
305
- </div>
306
- </div>
307
- );
308
- };