upfynai-code 2.4.0 → 2.5.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.
- package/client/dist/assets/AppContent-CRld2UWX.js +513 -0
- package/client/dist/assets/CanvasPanel-CB4sweQq.js +34 -0
- package/client/dist/assets/CanvasPanel-WhZulBJw.css +1 -0
- package/client/dist/assets/DashboardPanel-BXaA-b9z.js +1 -0
- package/client/dist/assets/LoginModal-BwkvjfPR.js +19 -0
- package/client/dist/assets/{Onboarding-CtIoXiTp.js → Onboarding-2A_5fPxy.js} +1 -1
- package/client/dist/assets/{SetupForm-B4p8im5O.js → SetupForm-CH5EA5W0.js} +1 -1
- package/client/dist/assets/WorkflowsPanel-CO5g5yGG.js +1 -0
- package/client/dist/assets/{ar-SA-G6X2FPQ2-2gfmdvHk.js → ar-SA-G6X2FPQ2-DoJuo98H.js} +2 -2
- package/client/dist/assets/{arc-DCZSHhoJ.js → arc-B0wBaTeh.js} +1 -1
- package/client/dist/assets/az-AZ-76LH7QW2-xdrt1Z13.js +1 -0
- package/client/dist/assets/{bg-BG-XCXSNQG7-D6__XtOK.js → bg-BG-XCXSNQG7-D8NAiF6Y.js} +2 -2
- package/client/dist/assets/{blockDiagram-38ab4fdb-Cfbaeyp6.js → blockDiagram-38ab4fdb-DSnyKzK4.js} +2 -2
- package/client/dist/assets/{bn-BD-2XOGV67Q-DHNJw3OG.js → bn-BD-2XOGV67Q-B0qWv8_J.js} +2 -2
- package/client/dist/assets/{c4Diagram-3d4e48cf-BBCnjOTy.js → c4Diagram-3d4e48cf-DoZJ13XA.js} +2 -2
- package/client/dist/assets/{ca-ES-6MX7JW3Y-r5g4o3zQ.js → ca-ES-6MX7JW3Y-RgLhfbZZ.js} +3 -3
- package/client/dist/assets/channel-BmO6nY0W.js +1 -0
- package/client/dist/assets/classDiagram-70f12bd4-GNyDrRCk.js +2 -0
- package/client/dist/assets/classDiagram-v2-f2320105-CxdGhHm2.js +2 -0
- package/client/dist/assets/clone-xuHMqFoD.js +1 -0
- package/client/dist/assets/{createText-2e5e7dd3-B8jCDmF_.js → createText-2e5e7dd3-DiPywQOa.js} +1 -1
- package/client/dist/assets/{cs-CZ-2BRQDIVT-p08jRLRC.js → cs-CZ-2BRQDIVT-BAjmnuoC.js} +2 -2
- package/client/dist/assets/{da-DK-5WZEPLOC-CnhOImFf.js → da-DK-5WZEPLOC-JxKVGt8o.js} +2 -2
- package/client/dist/assets/{de-DE-XR44H4JA-BunSXZ-Y.js → de-DE-XR44H4JA-CrnRlt4z.js} +2 -2
- package/client/dist/assets/{edges-e0da2a9e-CGBBhG8k.js → edges-e0da2a9e-DDsXzXLJ.js} +1 -1
- package/client/dist/assets/{el-GR-BZB4AONW-D4wv1oIz.js → el-GR-BZB4AONW-DQd8iogq.js} +2 -2
- package/client/dist/assets/{erDiagram-9861fffd-CYaF3q1I.js → erDiagram-9861fffd-CBiCC4rl.js} +2 -2
- package/client/dist/assets/{es-ES-U4NZUMDT-CGeTKXgd.js → es-ES-U4NZUMDT-vvUblc5i.js} +2 -2
- package/client/dist/assets/{eu-ES-A7QVB2H4-Cayx1TxR.js → eu-ES-A7QVB2H4-De4NNCc1.js} +2 -2
- package/client/dist/assets/{fa-IR-HGAKTJCU-CmUg8pmw.js → fa-IR-HGAKTJCU-DFBXqIqq.js} +2 -2
- package/client/dist/assets/{fi-FI-Z5N7JZ37-xvHcPhsU.js → fi-FI-Z5N7JZ37-DV9zESPg.js} +2 -2
- package/client/dist/assets/{flowDb-956e92f1-C-_LFz70.js → flowDb-956e92f1-BhdSHbdO.js} +1 -1
- package/client/dist/assets/{flowDiagram-66a62f08-C1sHdSjn.js → flowDiagram-66a62f08-M-fp1_Ie.js} +2 -2
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-C5eiN8Pg.js +1 -0
- package/client/dist/assets/{flowchart-elk-definition-4a651766-CNGfpudb.js → flowchart-elk-definition-4a651766-Bp0SonQx.js} +2 -2
- package/client/dist/assets/{fr-FR-RHASNOE6-DBoHEcNj.js → fr-FR-RHASNOE6-CKTMXuGk.js} +2 -2
- package/client/dist/assets/ganttDiagram-c361ad54-iA737GUS.js +257 -0
- package/client/dist/assets/{gitGraphDiagram-72cf32ee-DojCDvlS.js → gitGraphDiagram-72cf32ee-BX-wj-PV.js} +2 -2
- package/client/dist/assets/{gl-ES-HMX3MZ6V-p6hrn2cN.js → gl-ES-HMX3MZ6V-Cdiqq4jY.js} +2 -2
- package/client/dist/assets/{graph-DXM7lcy1.js → graph-Rxkx3sEa.js} +1 -1
- package/client/dist/assets/{he-IL-6SHJWFNN-y2jEX6-0.js → he-IL-6SHJWFNN-gYmR5_KT.js} +2 -2
- package/client/dist/assets/{hi-IN-IWLTKZ5I-99pNfyWr.js → hi-IN-IWLTKZ5I-pyqK94AR.js} +2 -2
- package/client/dist/assets/{hu-HU-A5ZG7DT2-hygceGMS.js → hu-HU-A5ZG7DT2-DpacJgJy.js} +2 -2
- package/client/dist/assets/{id-ID-SAP4L64H-CyIqi1hv.js → id-ID-SAP4L64H-CAvIX-mj.js} +2 -2
- package/client/dist/assets/{index-3862675e-4idOQN2N.js → index-3862675e-BX3Fpn6V.js} +1 -1
- package/client/dist/assets/{index-BHZfFT_V.js → index-BBlwbHq_.js} +4 -4
- package/client/dist/assets/{index-BGmwbRlb.js → index-ClfzLIqY.js} +6 -6
- package/client/dist/assets/index-Td4UdtLF.css +1 -0
- package/client/dist/assets/{infoDiagram-f8f76790-CFLrHqtc.js → infoDiagram-f8f76790-Ckv8imiv.js} +2 -2
- package/client/dist/assets/{it-IT-JPQ66NNP-DzVvVdQI.js → it-IT-JPQ66NNP-BtpNRSce.js} +2 -2
- package/client/dist/assets/{ja-JP-DBVTYXUO-BI4fPexV.js → ja-JP-DBVTYXUO-CwJRyY6M.js} +2 -2
- package/client/dist/assets/{journeyDiagram-49397b02-C3CFDo8z.js → journeyDiagram-49397b02-DWWZssji.js} +2 -2
- package/client/dist/assets/kaa-6HZHGXH3-DIWQEb4A.js +1 -0
- package/client/dist/assets/{kab-KAB-ZGHBKWFO-DBI_ri48.js → kab-KAB-ZGHBKWFO-DjGbqhUg.js} +2 -2
- package/client/dist/assets/kk-KZ-P5N5QNE5-B_VzJdWf.js +1 -0
- package/client/dist/assets/{km-KH-HSX4SM5Z-DOMFSres.js → km-KH-HSX4SM5Z-DUD5mi0o.js} +2 -2
- package/client/dist/assets/{ko-KR-MTYHY66A-tb08hXzd.js → ko-KR-MTYHY66A--sDB10db.js} +3 -3
- package/client/dist/assets/{ku-TR-6OUDTVRD-DlIQCCY4.js → ku-TR-6OUDTVRD-CKvKrkcX.js} +2 -2
- package/client/dist/assets/{layout-B_11mCXA.js → layout-CkB7sSeq.js} +1 -1
- package/client/dist/assets/{line-B-qmK_vI.js → line-DC7MA9qY.js} +1 -1
- package/client/dist/assets/{linear-Ph6uuYcX.js → linear-C1lBBthf.js} +1 -1
- package/client/dist/assets/{lt-LT-XHIRWOB4--qWy24_Z.js → lt-LT-XHIRWOB4-MSZf7xYG.js} +2 -2
- package/client/dist/assets/{lv-LV-5QDEKY6T-Bnd_1GDb.js → lv-LV-5QDEKY6T-C-gvvmBB.js} +2 -2
- package/client/dist/assets/{mindmap-definition-fc14e90a-Do79tIc0.js → mindmap-definition-fc14e90a-B3O7hztq.js} +2 -2
- package/client/dist/assets/{mr-IN-CRQNXWMA-BsV6HaD9.js → mr-IN-CRQNXWMA-XHtBUWQH.js} +2 -2
- package/client/dist/assets/my-MM-5M5IBNSE-D9eD2edL.js +1 -0
- package/client/dist/assets/{nb-NO-T6EIAALU-Cvf9FdSF.js → nb-NO-T6EIAALU-BlImC6gp.js} +3 -3
- package/client/dist/assets/{nl-NL-IS3SIHDZ-DA1yqpXw.js → nl-NL-IS3SIHDZ-CPFhnaSP.js} +2 -2
- package/client/dist/assets/{nn-NO-6E72VCQL-89lm3vku.js → nn-NO-6E72VCQL-BMvoJSKQ.js} +2 -2
- package/client/dist/assets/{oc-FR-POXYY2M6-BsrjTJQh.js → oc-FR-POXYY2M6-Buye63LS.js} +2 -2
- package/client/dist/assets/{pa-IN-N4M65BXN-CczefYaj.js → pa-IN-N4M65BXN-D9uQ3niy.js} +2 -2
- package/client/dist/assets/{percentages-BXMCSKIN-Be6p9phi.js → percentages-BXMCSKIN-BzXIakGM.js} +7 -7
- package/client/dist/assets/{pieDiagram-8a3498a8-CfblQHdm.js → pieDiagram-8a3498a8-BU38mzx-.js} +3 -3
- package/client/dist/assets/{pl-PL-T2D74RX3-DdhH-zcK.js → pl-PL-T2D74RX3-BqM4xdcg.js} +2 -2
- package/client/dist/assets/{pt-BR-5N22H2LF-gpwlheL6.js → pt-BR-5N22H2LF-rAjrxGyI.js} +2 -2
- package/client/dist/assets/{pt-PT-UZXXM6DQ-Cs87vICi.js → pt-PT-UZXXM6DQ-DXsqcwLt.js} +2 -2
- package/client/dist/assets/{quadrantDiagram-120e2f19-CRMSamSP.js → quadrantDiagram-120e2f19-HhK4H1WU.js} +2 -2
- package/client/dist/assets/{requirementDiagram-deff3bca-D3LBN016.js → requirementDiagram-deff3bca-aDrcyj-A.js} +2 -2
- package/client/dist/assets/{ro-RO-JPDTUUEW-CWTSJ1Dt.js → ro-RO-JPDTUUEW-D_F9UKer.js} +2 -2
- package/client/dist/assets/{ru-RU-B4JR7IUQ-Bq7aN2ep.js → ru-RU-B4JR7IUQ-MirqN29p.js} +2 -2
- package/client/dist/assets/sankeyDiagram-04a897e0-C6ij7qbQ.js +8 -0
- package/client/dist/assets/{sequenceDiagram-704730f1-BRYXVDGX.js → sequenceDiagram-704730f1-C0EKO3th.js} +2 -2
- package/client/dist/assets/si-LK-N5RQ5JYF-DyZC3mkC.js +1 -0
- package/client/dist/assets/{sk-SK-C5VTKIMK-ByjKQzUb.js → sk-SK-C5VTKIMK-D-ksz-WY.js} +2 -2
- package/client/dist/assets/{sl-SI-NN7IZMDC-B8WCyMBU.js → sl-SI-NN7IZMDC-CknuYoQ1.js} +2 -2
- package/client/dist/assets/stateDiagram-587899a1-CYoq2VjL.js +1 -0
- package/client/dist/assets/stateDiagram-v2-d93cdb3a-C5lbp5px.js +1 -0
- package/client/dist/assets/{styles-6aaf32cf-Dr-lfIOW.js → styles-6aaf32cf-Dkfsk8gt.js} +1 -1
- package/client/dist/assets/{styles-9a916d00-DS4wRpL7.js → styles-9a916d00-CMYqtcEN.js} +1 -1
- package/client/dist/assets/{styles-c10674c1-nKRF6NrH.js → styles-c10674c1-Bp-5OlRU.js} +1 -1
- package/client/dist/assets/{subset-shared.chunk-KT79s7KG.js → subset-shared.chunk-kfIB1Zam.js} +3 -3
- package/client/dist/assets/subset-worker.chunk-DwQBgc4z.js +1 -0
- package/client/dist/assets/{sv-SE-XGPEYMSR-BiIPUVbv.js → sv-SE-XGPEYMSR-DwN13se1.js} +2 -2
- package/client/dist/assets/{svgDrawCommon-08f97a94-C3uP9PYr.js → svgDrawCommon-08f97a94-CEgCMqs4.js} +1 -1
- package/client/dist/assets/{ta-IN-2NMHFXQM-Cidadso2.js → ta-IN-2NMHFXQM-ejDfFhwa.js} +2 -2
- package/client/dist/assets/th-TH-HPSO5L25-Bqc90ZNn.js +2 -0
- package/client/dist/assets/{timeline-definition-85554ec2-BSsLsIgF.js → timeline-definition-85554ec2-BmGdKqG0.js} +2 -2
- package/client/dist/assets/{tr-TR-DEFEU3FU-DaFcI-KL.js → tr-TR-DEFEU3FU-CJvlPbcW.js} +2 -2
- package/client/dist/assets/{uk-UA-QMV73CPH-DkBW36St.js → uk-UA-QMV73CPH-D26-cbWL.js} +3 -3
- package/client/dist/assets/vendor-codemirror-D_s0aGBu.js +35 -0
- package/client/dist/assets/{vendor-icons-Dh9m_Ydt.js → vendor-icons-aNdOvTr_.js} +159 -119
- package/client/dist/assets/{vi-VN-M7AON7JQ-KrtfxOzl.js → vi-VN-M7AON7JQ-MbqIIwYM.js} +2 -2
- package/client/dist/assets/{xychartDiagram-e933f94c-CgNgZ4pp.js → xychartDiagram-e933f94c-gfcTauxU.js} +2 -2
- package/client/dist/assets/{zh-CN-LNUGB5OW-BQu12RoD.js → zh-CN-LNUGB5OW-BZSmhUdL.js} +3 -3
- package/client/dist/assets/zh-HK-E62DVLB3-BJqejpiX.js +1 -0
- package/client/dist/assets/{zh-TW-RAJ6MFWO-ffJWgVxn.js → zh-TW-RAJ6MFWO-BBXtV-Uz.js} +2 -2
- package/client/dist/index.html +3 -3
- package/package.json +5 -2
- package/server/cli.js +64 -5
- package/server/constants/config.js +29 -3
- package/server/database/auth.db +0 -0
- package/server/database/db.js +203 -1
- package/server/index.js +348 -48
- package/server/mcp-server.js +2 -1
- package/server/middleware/auth.js +20 -9
- package/server/projects.js +95 -202
- package/server/relay-client.js +205 -11
- package/server/routes/auth.js +6 -0
- package/server/routes/commands.js +1 -1
- package/server/routes/dashboard.js +52 -0
- package/server/routes/projects.js +38 -35
- package/server/routes/voice.js +198 -0
- package/server/routes/webhooks.js +166 -0
- package/server/routes/workflows.js +118 -0
- package/server/services/whisperService.js +84 -0
- package/server/services/workflowScheduler.js +186 -0
- package/client/dist/assets/AppContent-DTZ2FbvM.js +0 -513
- package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +0 -6
- package/client/dist/assets/CanvasPanel-q4HEqNtV.css +0 -1
- package/client/dist/assets/LoginModal-CWoFm0au.js +0 -19
- package/client/dist/assets/az-AZ-76LH7QW2-CDdeucRZ.js +0 -1
- package/client/dist/assets/channel-O3ovC0x9.js +0 -1
- package/client/dist/assets/classDiagram-70f12bd4-D0lhAcxU.js +0 -2
- package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +0 -2
- package/client/dist/assets/clone-BG9u7vLi.js +0 -1
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +0 -1
- package/client/dist/assets/ganttDiagram-c361ad54-B8HJQqjt.js +0 -257
- package/client/dist/assets/index-B8wwD_Xo.css +0 -1
- package/client/dist/assets/kaa-6HZHGXH3-fwOleoQB.js +0 -1
- package/client/dist/assets/kk-KZ-P5N5QNE5-zpl7uvyF.js +0 -1
- package/client/dist/assets/my-MM-5M5IBNSE-kZQURVIi.js +0 -1
- package/client/dist/assets/sankeyDiagram-04a897e0-CsFqOQZN.js +0 -8
- package/client/dist/assets/si-LK-N5RQ5JYF-BBjcNYQh.js +0 -1
- package/client/dist/assets/stateDiagram-587899a1-BHoy9LtD.js +0 -1
- package/client/dist/assets/stateDiagram-v2-d93cdb3a-BvMUA6bS.js +0 -1
- package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +0 -1
- package/client/dist/assets/th-TH-HPSO5L25-CFNnJwSv.js +0 -2
- package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +0 -20
- package/client/dist/assets/vendor-codemirror-rix45NST.js +0 -16
- package/client/dist/assets/zh-HK-E62DVLB3-zx9CvERq.js +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { getProjects, getSessions } from '../projects.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/dashboard/stats — Dashboard usage analytics
|
|
8
|
+
* Returns session counts, provider breakdown, and today's activity.
|
|
9
|
+
*/
|
|
10
|
+
router.get('/stats', async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const projects = await getProjects();
|
|
13
|
+
let totalSessions = 0;
|
|
14
|
+
let todaySessions = 0;
|
|
15
|
+
const providers = {};
|
|
16
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
17
|
+
|
|
18
|
+
for (const project of projects) {
|
|
19
|
+
// Count sessions per provider
|
|
20
|
+
for (const provider of ['claude', 'cursor', 'codex']) {
|
|
21
|
+
try {
|
|
22
|
+
const sessions = await getSessions(project.name, provider);
|
|
23
|
+
if (sessions && sessions.length) {
|
|
24
|
+
totalSessions += sessions.length;
|
|
25
|
+
providers[provider] = (providers[provider] || 0) + sessions.length;
|
|
26
|
+
|
|
27
|
+
// Count today's sessions
|
|
28
|
+
for (const s of sessions) {
|
|
29
|
+
const created = s.created_at || s.createdAt || '';
|
|
30
|
+
if (created.startsWith(today)) {
|
|
31
|
+
todaySessions++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Provider not available for this project
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
res.json({
|
|
42
|
+
total: totalSessions,
|
|
43
|
+
today: todaySessions,
|
|
44
|
+
providers,
|
|
45
|
+
projectCount: projects.length,
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
res.status(500).json({ error: 'Failed to fetch dashboard stats' });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export default router;
|
|
@@ -12,8 +12,11 @@ function sanitizeGitError(message, token) {
|
|
|
12
12
|
return message.replace(new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '***');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// Configure allowed workspace root
|
|
16
|
-
|
|
15
|
+
// Configure allowed workspace root.
|
|
16
|
+
// In local/platform mode, allow any path (no root restriction) unless explicitly set.
|
|
17
|
+
// In hosted mode, default to user's home directory for security.
|
|
18
|
+
const IS_LOCAL = !process.env.RAILWAY_ENVIRONMENT && !process.env.VERCEL && !process.env.RENDER;
|
|
19
|
+
export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || (IS_LOCAL ? null : os.homedir());
|
|
17
20
|
|
|
18
21
|
// System-critical paths that should never be used as workspace directories
|
|
19
22
|
export const FORBIDDEN_PATHS = [
|
|
@@ -110,42 +113,41 @@ export async function validateWorkspacePath(requestedPath) {
|
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
//
|
|
114
|
-
|
|
116
|
+
// If a workspace root is configured, enforce containment
|
|
117
|
+
if (WORKSPACES_ROOT) {
|
|
118
|
+
const resolvedWorkspaceRoot = await fs.realpath(WORKSPACES_ROOT);
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
}
|
|
120
|
+
if (!realPath.startsWith(resolvedWorkspaceRoot + path.sep) &&
|
|
121
|
+
realPath !== resolvedWorkspaceRoot) {
|
|
122
|
+
return {
|
|
123
|
+
valid: false,
|
|
124
|
+
error: `Workspace path must be within the allowed workspace root: ${WORKSPACES_ROOT}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
128
|
+
// Additional symlink check for existing paths
|
|
129
|
+
try {
|
|
130
|
+
await fs.access(absolutePath);
|
|
131
|
+
const stats = await fs.lstat(absolutePath);
|
|
132
|
+
|
|
133
|
+
if (stats.isSymbolicLink()) {
|
|
134
|
+
const linkTarget = await fs.readlink(absolutePath);
|
|
135
|
+
const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget);
|
|
136
|
+
const realTarget = await fs.realpath(resolvedTarget);
|
|
137
|
+
|
|
138
|
+
if (!realTarget.startsWith(resolvedWorkspaceRoot + path.sep) &&
|
|
139
|
+
realTarget !== resolvedWorkspaceRoot) {
|
|
140
|
+
return {
|
|
141
|
+
valid: false,
|
|
142
|
+
error: 'Symlink target is outside the allowed workspace root'
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error.code !== 'ENOENT') {
|
|
148
|
+
throw error;
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
|
-
} catch (error) {
|
|
145
|
-
if (error.code !== 'ENOENT') {
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
// Path doesn't exist - that's fine for new workspace creation
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
return {
|
|
@@ -301,7 +303,8 @@ router.post('/create-workspace', async (req, res) => {
|
|
|
301
303
|
} catch (error) {
|
|
302
304
|
// workspace creation error
|
|
303
305
|
res.status(500).json({
|
|
304
|
-
error: 'Failed to create workspace'
|
|
306
|
+
error: 'Failed to create workspace',
|
|
307
|
+
details: error.message
|
|
305
308
|
});
|
|
306
309
|
}
|
|
307
310
|
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import multer from 'multer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import { isWhisperAvailable, transcribeLocal } from '../services/whisperService.js';
|
|
7
|
+
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } }); // 25MB max
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper: Get user's OpenAI key (BYOK) or server env key.
|
|
13
|
+
* Imported lazily to avoid circular deps.
|
|
14
|
+
*/
|
|
15
|
+
async function getOpenAIKey(userId) {
|
|
16
|
+
try {
|
|
17
|
+
const { credentialsDb } = await import('../database/db.js');
|
|
18
|
+
const creds = await credentialsDb.getCredentials(userId, 'openai_key');
|
|
19
|
+
const active = creds.find(c => c.is_active);
|
|
20
|
+
return active?.credential_value || process.env.OPENAI_API_KEY || null;
|
|
21
|
+
} catch {
|
|
22
|
+
return process.env.OPENAI_API_KEY || null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* POST /api/voice/stt — Speech-to-text
|
|
28
|
+
* Primary: OpenAI Whisper API (cloud)
|
|
29
|
+
* Fallback: nodejs-whisper (local, when no API key)
|
|
30
|
+
*/
|
|
31
|
+
router.post('/stt', upload.single('audio'), async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
if (!req.file) {
|
|
34
|
+
return res.status(400).json({ error: 'No audio file provided' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const mode = req.body?.mode || 'default';
|
|
38
|
+
const apiKey = await getOpenAIKey(req.user?.id);
|
|
39
|
+
|
|
40
|
+
// Try OpenAI Whisper API first
|
|
41
|
+
if (apiKey) {
|
|
42
|
+
try {
|
|
43
|
+
const FormData = (await import('form-data')).default;
|
|
44
|
+
const formData = new FormData();
|
|
45
|
+
formData.append('file', req.file.buffer, {
|
|
46
|
+
filename: req.file.originalname || 'audio.webm',
|
|
47
|
+
contentType: req.file.mimetype || 'audio/webm'
|
|
48
|
+
});
|
|
49
|
+
formData.append('model', 'whisper-1');
|
|
50
|
+
formData.append('response_format', 'json');
|
|
51
|
+
formData.append('language', 'en');
|
|
52
|
+
|
|
53
|
+
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
57
|
+
...formData.getHeaders()
|
|
58
|
+
},
|
|
59
|
+
body: formData
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const errorData = await response.json().catch(() => ({}));
|
|
64
|
+
throw new Error(errorData.error?.message || `Whisper API error: ${response.status}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
let text = data.text || '';
|
|
69
|
+
|
|
70
|
+
// Enhancement modes (prompt, vibe, architect)
|
|
71
|
+
if (text && mode !== 'default') {
|
|
72
|
+
text = await enhanceTranscription(text, mode, apiKey);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return res.json({ text, source: 'openai' });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
// If cloud fails, try local fallback
|
|
78
|
+
console.warn('[Voice STT] OpenAI API failed, trying local:', err.message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fallback: local nodejs-whisper
|
|
83
|
+
const localAvailable = await isWhisperAvailable();
|
|
84
|
+
if (localAvailable) {
|
|
85
|
+
// Write buffer to temp file for nodejs-whisper
|
|
86
|
+
const tmpFile = path.join(os.tmpdir(), `stt-${Date.now()}.webm`);
|
|
87
|
+
await fs.writeFile(tmpFile, req.file.buffer);
|
|
88
|
+
try {
|
|
89
|
+
const text = await transcribeLocal(tmpFile);
|
|
90
|
+
if (text) return res.json({ text, source: 'local' });
|
|
91
|
+
} finally {
|
|
92
|
+
await fs.unlink(tmpFile).catch(() => {});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return res.status(500).json({ error: 'No STT engine available. Add your OpenAI key in Settings > AI Providers, or install nodejs-whisper + ffmpeg for local transcription.' });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('[Voice STT] Error:', error.message);
|
|
99
|
+
res.status(500).json({ error: 'Transcription failed' });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* POST /api/voice/tts — Text-to-speech using Edge-TTS
|
|
105
|
+
* Returns audio/mp3 blob
|
|
106
|
+
*/
|
|
107
|
+
router.post('/tts', async (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const { text, voice = 'en-US-AriaNeural' } = req.body;
|
|
110
|
+
if (!text || typeof text !== 'string') {
|
|
111
|
+
return res.status(400).json({ error: 'Text is required' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Truncate very long text to avoid excessive audio generation
|
|
115
|
+
const truncated = text.length > 5000 ? text.slice(0, 5000) : text;
|
|
116
|
+
|
|
117
|
+
const { EdgeTTS } = await import('edge-tts-universal');
|
|
118
|
+
const tts = new EdgeTTS(truncated, voice);
|
|
119
|
+
const result = await tts.synthesize();
|
|
120
|
+
const audioBuffer = Buffer.from(await result.audio.arrayBuffer());
|
|
121
|
+
|
|
122
|
+
res.setHeader('Content-Type', 'audio/mp3');
|
|
123
|
+
res.setHeader('Content-Length', audioBuffer.length);
|
|
124
|
+
res.send(audioBuffer);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('[Voice TTS] Error:', error.message);
|
|
127
|
+
res.status(500).json({ error: 'TTS synthesis failed' });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* GET /api/voice/voices — List available TTS voices
|
|
133
|
+
*/
|
|
134
|
+
router.get('/voices', async (req, res) => {
|
|
135
|
+
try {
|
|
136
|
+
const { listVoices } = await import('edge-tts-universal');
|
|
137
|
+
const voices = await listVoices();
|
|
138
|
+
|
|
139
|
+
// Filter to English voices and simplify the response
|
|
140
|
+
const englishVoices = voices
|
|
141
|
+
.filter(v => v.Locale?.startsWith('en-'))
|
|
142
|
+
.map(v => ({
|
|
143
|
+
id: v.ShortName,
|
|
144
|
+
name: v.FriendlyName || v.ShortName,
|
|
145
|
+
locale: v.Locale,
|
|
146
|
+
gender: v.Gender
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
res.json({ voices: englishVoices });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('[Voice Voices] Error:', error.message);
|
|
152
|
+
res.status(500).json({ error: 'Failed to fetch voices' });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Enhance transcribed text using GPT (prompt engineering, vibe coding, etc.)
|
|
158
|
+
*/
|
|
159
|
+
async function enhanceTranscription(text, mode, apiKey) {
|
|
160
|
+
try {
|
|
161
|
+
const OpenAI = (await import('openai')).default;
|
|
162
|
+
const openai = new OpenAI({ apiKey });
|
|
163
|
+
|
|
164
|
+
let systemMessage, prompt, temperature = 0.7, maxTokens = 800;
|
|
165
|
+
|
|
166
|
+
switch (mode) {
|
|
167
|
+
case 'prompt':
|
|
168
|
+
systemMessage = 'You are an expert prompt engineer who creates clear, detailed, and effective prompts.';
|
|
169
|
+
prompt = `Transform the following rough instruction into a clear, detailed, and context-aware AI prompt.\n\nYour enhanced prompt should:\n1. Be specific and unambiguous\n2. Include relevant context and constraints\n3. Specify the desired output format\n4. Use clear, actionable language\n\nRough instruction: "${text}"\n\nEnhanced prompt:`;
|
|
170
|
+
break;
|
|
171
|
+
case 'vibe':
|
|
172
|
+
case 'instructions':
|
|
173
|
+
case 'architect':
|
|
174
|
+
systemMessage = 'You are a helpful assistant that formats ideas into clear, actionable instructions for AI agents.';
|
|
175
|
+
temperature = 0.5;
|
|
176
|
+
prompt = `Transform the following idea into clear, well-structured instructions that an AI agent can easily understand and execute.\n\nIMPORTANT RULES:\n- Format as clear, step-by-step instructions\n- Add reasonable implementation details based on common patterns\n- Only include details directly related to what was asked\n- Do NOT add features or functionality not mentioned\n- Keep the original intent and scope intact\n\nIdea: "${text}"\n\nAgent instructions:`;
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
return text;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const completion = await openai.chat.completions.create({
|
|
183
|
+
model: 'gpt-4o-mini',
|
|
184
|
+
messages: [
|
|
185
|
+
{ role: 'system', content: systemMessage },
|
|
186
|
+
{ role: 'user', content: prompt }
|
|
187
|
+
],
|
|
188
|
+
temperature,
|
|
189
|
+
max_tokens: maxTokens
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return completion.choices[0]?.message?.content || text;
|
|
193
|
+
} catch {
|
|
194
|
+
return text; // Fallback to original on error
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default router;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { webhookDb } from '../database/db.js';
|
|
3
|
+
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
|
|
6
|
+
// GET /api/webhooks — list all webhooks for the user
|
|
7
|
+
router.get('/', async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const webhooks = await webhookDb.getAll(req.user.id);
|
|
10
|
+
res.json({ webhooks });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
res.status(500).json({ error: 'Failed to fetch webhooks' });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// POST /api/webhooks — create a webhook
|
|
17
|
+
router.post('/', async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const { name, url, method, headers, description } = req.body;
|
|
20
|
+
if (!name || !name.trim()) return res.status(400).json({ error: 'Name is required' });
|
|
21
|
+
if (!url || !url.trim()) return res.status(400).json({ error: 'URL is required' });
|
|
22
|
+
|
|
23
|
+
try { new URL(url); } catch { return res.status(400).json({ error: 'Invalid URL' }); }
|
|
24
|
+
|
|
25
|
+
const allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
26
|
+
const m = (method || 'POST').toUpperCase();
|
|
27
|
+
if (!allowedMethods.includes(m)) return res.status(400).json({ error: 'Invalid HTTP method' });
|
|
28
|
+
|
|
29
|
+
// Validate headers is valid JSON if provided
|
|
30
|
+
let headersStr = '{}';
|
|
31
|
+
if (headers) {
|
|
32
|
+
if (typeof headers === 'string') {
|
|
33
|
+
try { JSON.parse(headers); headersStr = headers; } catch { return res.status(400).json({ error: 'Headers must be valid JSON' }); }
|
|
34
|
+
} else {
|
|
35
|
+
headersStr = JSON.stringify(headers);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const webhook = await webhookDb.create(req.user.id, {
|
|
40
|
+
name: name.trim(),
|
|
41
|
+
url: url.trim(),
|
|
42
|
+
method: m,
|
|
43
|
+
headers: headersStr,
|
|
44
|
+
description: description?.trim() || null
|
|
45
|
+
});
|
|
46
|
+
res.json({ success: true, webhook });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
res.status(500).json({ error: 'Failed to create webhook' });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// PUT /api/webhooks/:id — update a webhook
|
|
53
|
+
router.put('/:id', async (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const { name, url, method, headers, description } = req.body;
|
|
56
|
+
if (!name || !name.trim()) return res.status(400).json({ error: 'Name is required' });
|
|
57
|
+
if (!url || !url.trim()) return res.status(400).json({ error: 'URL is required' });
|
|
58
|
+
|
|
59
|
+
try { new URL(url); } catch { return res.status(400).json({ error: 'Invalid URL' }); }
|
|
60
|
+
|
|
61
|
+
let headersStr = '{}';
|
|
62
|
+
if (headers) {
|
|
63
|
+
if (typeof headers === 'string') {
|
|
64
|
+
try { JSON.parse(headers); headersStr = headers; } catch { return res.status(400).json({ error: 'Headers must be valid JSON' }); }
|
|
65
|
+
} else {
|
|
66
|
+
headersStr = JSON.stringify(headers);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const updated = await webhookDb.update(Number(req.params.id), req.user.id, {
|
|
71
|
+
name: name.trim(),
|
|
72
|
+
url: url.trim(),
|
|
73
|
+
method: (method || 'POST').toUpperCase(),
|
|
74
|
+
headers: headersStr,
|
|
75
|
+
description: description?.trim() || null
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!updated) return res.status(404).json({ error: 'Webhook not found' });
|
|
79
|
+
res.json({ success: true });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
res.status(500).json({ error: 'Failed to update webhook' });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// DELETE /api/webhooks/:id — delete a webhook
|
|
86
|
+
router.delete('/:id', async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const deleted = await webhookDb.delete(Number(req.params.id), req.user.id);
|
|
89
|
+
if (!deleted) return res.status(404).json({ error: 'Webhook not found' });
|
|
90
|
+
res.json({ success: true });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
res.status(500).json({ error: 'Failed to delete webhook' });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// POST /api/webhooks/:id/test — fire a test request to the webhook
|
|
97
|
+
router.post('/:id/test', async (req, res) => {
|
|
98
|
+
try {
|
|
99
|
+
const webhook = await webhookDb.getById(Number(req.params.id), req.user.id);
|
|
100
|
+
if (!webhook) return res.status(404).json({ error: 'Webhook not found' });
|
|
101
|
+
|
|
102
|
+
let parsedHeaders = {};
|
|
103
|
+
try { parsedHeaders = JSON.parse(webhook.headers || '{}'); } catch { /* ignore */ }
|
|
104
|
+
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const fetchOptions = {
|
|
110
|
+
method: webhook.method,
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
'User-Agent': 'UpfynAI-Webhook/1.0',
|
|
114
|
+
...parsedHeaders
|
|
115
|
+
},
|
|
116
|
+
signal: controller.signal
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Add test body for methods that support it
|
|
120
|
+
if (['POST', 'PUT', 'PATCH'].includes(webhook.method)) {
|
|
121
|
+
fetchOptions.body = JSON.stringify({
|
|
122
|
+
event: 'test',
|
|
123
|
+
webhook_id: webhook.id,
|
|
124
|
+
webhook_name: webhook.name,
|
|
125
|
+
timestamp: new Date().toISOString()
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const response = await fetch(webhook.url, fetchOptions);
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
|
|
132
|
+
let body;
|
|
133
|
+
const contentType = response.headers.get('content-type') || '';
|
|
134
|
+
if (contentType.includes('application/json')) {
|
|
135
|
+
body = await response.json();
|
|
136
|
+
} else {
|
|
137
|
+
body = await response.text();
|
|
138
|
+
if (body.length > 2000) body = body.slice(0, 2000) + '...';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await webhookDb.updateLastTriggered(webhook.id);
|
|
142
|
+
|
|
143
|
+
res.json({
|
|
144
|
+
success: true,
|
|
145
|
+
result: {
|
|
146
|
+
status: response.status,
|
|
147
|
+
statusText: response.statusText,
|
|
148
|
+
body,
|
|
149
|
+
headers: Object.fromEntries(response.headers.entries())
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} catch (fetchError) {
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
res.json({
|
|
155
|
+
success: false,
|
|
156
|
+
result: {
|
|
157
|
+
error: fetchError.name === 'AbortError' ? 'Request timed out (10s)' : fetchError.message
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
res.status(500).json({ error: 'Failed to test webhook' });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export default router;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { workflowDb, webhookDb } from '../database/db.js';
|
|
3
|
+
import { refreshWorkflowSchedule, stopWorkflowSchedule, executeWorkflow } from '../services/workflowScheduler.js';
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
// GET /api/workflows — list all workflows for the user
|
|
8
|
+
router.get('/', async (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const workflows = await workflowDb.getAll(req.user.id);
|
|
11
|
+
const parsed = workflows.map(w => ({
|
|
12
|
+
...w,
|
|
13
|
+
steps: typeof w.steps === 'string' ? JSON.parse(w.steps) : w.steps
|
|
14
|
+
}));
|
|
15
|
+
res.json({ workflows: parsed });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
res.status(500).json({ error: 'Failed to fetch workflows' });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// POST /api/workflows — create a workflow
|
|
22
|
+
router.post('/', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const { name, description, steps, schedule, schedule_enabled, schedule_timezone } = req.body;
|
|
25
|
+
if (!name || !name.trim()) return res.status(400).json({ error: 'Name is required' });
|
|
26
|
+
if (!Array.isArray(steps)) return res.status(400).json({ error: 'Steps must be an array' });
|
|
27
|
+
|
|
28
|
+
const workflow = await workflowDb.create(req.user.id, {
|
|
29
|
+
name: name.trim(),
|
|
30
|
+
description: description?.trim() || null,
|
|
31
|
+
steps,
|
|
32
|
+
schedule: schedule || null,
|
|
33
|
+
schedule_enabled: !!schedule_enabled,
|
|
34
|
+
schedule_timezone: schedule_timezone || 'UTC'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Sync cron scheduler
|
|
38
|
+
if (workflow.id) refreshWorkflowSchedule(workflow.id, req.user.id);
|
|
39
|
+
|
|
40
|
+
res.json({ success: true, workflow });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
res.status(500).json({ error: 'Failed to create workflow' });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// PUT /api/workflows/:id — update a workflow
|
|
47
|
+
router.put('/:id', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const { name, description, steps, schedule, schedule_enabled, schedule_timezone } = req.body;
|
|
50
|
+
if (!name || !name.trim()) return res.status(400).json({ error: 'Name is required' });
|
|
51
|
+
|
|
52
|
+
const wfId = Number(req.params.id);
|
|
53
|
+
const updated = await workflowDb.update(wfId, req.user.id, {
|
|
54
|
+
name: name.trim(),
|
|
55
|
+
description: description?.trim() || null,
|
|
56
|
+
steps: steps || [],
|
|
57
|
+
schedule: schedule || null,
|
|
58
|
+
schedule_enabled: !!schedule_enabled,
|
|
59
|
+
schedule_timezone: schedule_timezone || 'UTC'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!updated) return res.status(404).json({ error: 'Workflow not found' });
|
|
63
|
+
|
|
64
|
+
// Sync cron scheduler
|
|
65
|
+
refreshWorkflowSchedule(wfId, req.user.id);
|
|
66
|
+
|
|
67
|
+
res.json({ success: true });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
res.status(500).json({ error: 'Failed to update workflow' });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// DELETE /api/workflows/:id — delete a workflow
|
|
74
|
+
router.delete('/:id', async (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
const wfId = Number(req.params.id);
|
|
77
|
+
const deleted = await workflowDb.delete(wfId, req.user.id);
|
|
78
|
+
if (!deleted) return res.status(404).json({ error: 'Workflow not found' });
|
|
79
|
+
|
|
80
|
+
stopWorkflowSchedule(wfId);
|
|
81
|
+
|
|
82
|
+
res.json({ success: true });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
res.status(500).json({ error: 'Failed to delete workflow' });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// POST /api/workflows/:id/run — execute a workflow (manual trigger)
|
|
89
|
+
router.post('/:id/run', async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const workflow = await workflowDb.getById(Number(req.params.id), req.user.id);
|
|
92
|
+
if (!workflow) return res.status(404).json({ error: 'Workflow not found' });
|
|
93
|
+
|
|
94
|
+
const steps = typeof workflow.steps === 'string' ? JSON.parse(workflow.steps) : workflow.steps;
|
|
95
|
+
if (!steps.length) return res.status(400).json({ error: 'Workflow has no steps' });
|
|
96
|
+
|
|
97
|
+
const result = await executeWorkflow({ ...workflow, steps });
|
|
98
|
+
res.json(result);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
res.status(500).json({ error: 'Failed to run workflow' });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// GET /api/workflows/:id/runs — list execution history for a workflow
|
|
105
|
+
router.get('/:id/runs', async (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const runs = await workflowDb.getRuns(Number(req.params.id), req.user.id);
|
|
108
|
+
const parsed = runs.map(r => ({
|
|
109
|
+
...r,
|
|
110
|
+
result: typeof r.result === 'string' ? (() => { try { return JSON.parse(r.result); } catch { return r.result; } })() : r.result
|
|
111
|
+
}));
|
|
112
|
+
res.json({ runs: parsed });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
res.status(500).json({ error: 'Failed to fetch workflow runs' });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export default router;
|