From 52f3d16b65e64558f590cb69e60eae030fce3b60 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 20 Feb 2026 00:44:53 -0800 Subject: [PATCH] =?UTF-8?q?feat(local-llm):=20Phase=20F=20=E2=80=94=20sche?= =?UTF-8?q?duled=20tasks=20(F1-F7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F1: cron-parser integration + cron utility functions (parse, nextRun, toHuman, shouldRunNow) F2: ScheduledTask + Project + Orchestration CRUD in IndexedDB F3: Task editor modal (schedule, model, input source, output action, prompt) F4: Browser-based task runner with setInterval + cron matching F5: /api/system/exec — safe shell command execution with allowlist F6: Task run history stored per task (last 20 runs) F7: 5 built-in task templates (morning brief, git diff, disk usage, code review, deps) --- __LOCAL_LLMs/dashboard/package-lock.json | 39 +++ __LOCAL_LLMs/dashboard/package.json | 1 + .../app/(workspace)/components/TaskEditor.tsx | 223 ++++++++++++++++++ .../app/(workspace)/components/TaskRunner.tsx | 129 ++++++++++ .../src/app/api/system/exec/route.ts | 67 ++++++ __LOCAL_LLMs/dashboard/src/app/lib/cron.ts | 54 +++++ __LOCAL_LLMs/dashboard/src/app/lib/db.ts | 103 ++++++++ .../dashboard/src/app/lib/scheduled-tasks.ts | 64 +++++ 8 files changed, 680 insertions(+) create mode 100644 __LOCAL_LLMs/dashboard/src/app/(workspace)/components/TaskEditor.tsx create mode 100644 __LOCAL_LLMs/dashboard/src/app/(workspace)/components/TaskRunner.tsx create mode 100644 __LOCAL_LLMs/dashboard/src/app/api/system/exec/route.ts create mode 100644 __LOCAL_LLMs/dashboard/src/app/lib/cron.ts create mode 100644 __LOCAL_LLMs/dashboard/src/app/lib/scheduled-tasks.ts diff --git a/__LOCAL_LLMs/dashboard/package-lock.json b/__LOCAL_LLMs/dashboard/package-lock.json index d40cdc4f..185746e0 100644 --- a/__LOCAL_LLMs/dashboard/package-lock.json +++ b/__LOCAL_LLMs/dashboard/package-lock.json @@ -9,6 +9,9 @@ "version": "0.1.0", "dependencies": { "@types/react-syntax-highlighter": "^15.5.13", + "cron-parser": "^5.5.0", + "fuse.js": "^7.1.0", + "idb": "^8.0.3", "lucide-react": "^0.575.0", "next": "16.1.6", "react": "19.2.3", @@ -2781,6 +2784,18 @@ "dev": true, "license": "MIT" }, + "node_modules/cron-parser": { + "version": "5.5.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/cron-parser/-/cron-parser-5.5.0.tgz", + "integrity": "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==", + "license": "MIT", + "dependencies": { + "luxon": "^3.7.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3875,6 +3890,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/generator-function/-/generator-function-2.0.1.tgz", @@ -4234,6 +4258,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/ignore/-/ignore-5.3.2.tgz", @@ -5272,6 +5302,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/magic-string/-/magic-string-0.30.21.tgz", diff --git a/__LOCAL_LLMs/dashboard/package.json b/__LOCAL_LLMs/dashboard/package.json index eb9c30d5..8d44f3db 100644 --- a/__LOCAL_LLMs/dashboard/package.json +++ b/__LOCAL_LLMs/dashboard/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@types/react-syntax-highlighter": "^15.5.13", + "cron-parser": "^5.5.0", "fuse.js": "^7.1.0", "idb": "^8.0.3", "lucide-react": "^0.575.0", diff --git a/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/TaskEditor.tsx b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/TaskEditor.tsx new file mode 100644 index 00000000..af6562f6 --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/TaskEditor.tsx @@ -0,0 +1,223 @@ +'use client'; + +import { useState } from 'react'; +import { X } from 'lucide-react'; +import type { ScheduledTask } from '../../lib/types'; +import { parseCron, cronToHuman } from '../../lib/cron'; + +interface TaskEditorProps { + task?: ScheduledTask; + onSave: (task: ScheduledTask) => void; + onClose: () => void; +} + +export function TaskEditor({ task, onSave, onClose }: TaskEditorProps) { + const [name, setName] = useState(task?.name || ''); + const [schedule, setSchedule] = useState(task?.schedule || '0 8 * * *'); + const [model, setModel] = useState(task?.model || ''); + const [prompt, setPrompt] = useState(task?.prompt || ''); + const [enabled, setEnabled] = useState(task?.enabled ?? false); + const [inputType, setInputType] = useState(task?.inputSource?.type || 'static'); + const [inputCommand, setInputCommand] = useState( + task?.inputSource?.type === 'command' ? task.inputSource.command : '' + ); + const [inputFilePath, setInputFilePath] = useState( + task?.inputSource?.type === 'file' ? task.inputSource.path : '' + ); + const [outputType, setOutputType] = useState(task?.outputAction?.type || 'conversation'); + + const cronCheck = parseCron(schedule); + const humanSchedule = cronCheck.valid ? cronToHuman(schedule) : ''; + + const handleSave = () => { + if (!name.trim() || !prompt.trim() || !cronCheck.valid) return; + + const inputSource = (() => { + if (inputType === 'command') return { type: 'command' as const, command: inputCommand }; + if (inputType === 'file') return { type: 'file' as const, path: inputFilePath }; + if (inputType === 'clipboard') return { type: 'clipboard' as const }; + return { type: 'static' as const }; + })(); + + const outputAction = (() => { + if (outputType === 'clipboard') return { type: 'clipboard' as const }; + if (outputType === 'notification') return { type: 'notification' as const }; + return { type: 'conversation' as const }; + })(); + + const result: ScheduledTask = { + id: task?.id || crypto.randomUUID(), + name: name.trim(), + schedule, + scheduleHuman: humanSchedule, + model, + prompt: prompt.trim(), + inputSource, + outputAction, + enabled, + lastRun: task?.lastRun, + runHistory: task?.runHistory || [], + createdAt: task?.createdAt || Date.now(), + }; + + onSave(result); + }; + + return ( +
+
+
+

+ {task ? 'Edit Task' : 'New Scheduled Task'} +

+ +
+ +
+
+ + setName(e.target.value)} + className="w-full rounded border border-white/10 bg-[var(--surface-card)] px-3 py-1.5 text-sm text-[var(--text-primary)] outline-none" + placeholder="Morning Brief" + /> +
+ +
+ + setSchedule(e.target.value)} + className="w-full rounded border border-white/10 bg-[var(--surface-card)] px-3 py-1.5 font-mono text-sm text-[var(--text-primary)] outline-none" + placeholder="0 8 * * *" + /> + {cronCheck.valid ? ( +

{humanSchedule}

+ ) : ( +

{cronCheck.error}

+ )} +
+ +
+ + setModel(e.target.value)} + className="w-full rounded border border-white/10 bg-[var(--surface-card)] px-3 py-1.5 text-sm text-[var(--text-primary)] outline-none" + placeholder="Auto-detect" + /> +
+ +
+ +