Compare commits

...

4 Commits

Author SHA1 Message Date
Flatlogic Bot
16d4ccd49d version3.2 2025-07-06 19:58:08 +00:00
Flatlogic Bot
3c85cdfb87 version 3.0 2025-07-06 19:39:23 +00:00
Flatlogic Bot
1cd6cbb6dc version 2 2025-07-06 19:16:22 +00:00
Flatlogic Bot
852719d2c1 version 1 2025-07-06 18:22:24 +00:00
14 changed files with 1362 additions and 8 deletions

5
.gitignore vendored
View File

@ -1,3 +1,8 @@
node_modules/
*/node_modules/
*/build/
**/node_modules/
**/build/
.DS_Store
.env

49
FOLDER_STRUCTURE.md Normal file
View File

@ -0,0 +1,49 @@
# Project Folder Structure
This document outlines the recommended GitHub-ready layout for your CulturalSync AI project.
```
/
├── client/ # Frontend (React + Tailwind)
│ ├── public/
│ └── src/
│ ├── assets/
│ ├── components/
│ ├── hooks/
│ ├── pages/
│ ├── stores/
│ ├── styles/
│ └── utils/
│ ├── package.json
│ ├── tailwind.config.js
│ └── tsconfig.json
├── server/ # Backend (Node.js + Express + MongoDB)
│ ├── models/ # Mongoose schemas
│ ├── routes/ # Express routers (users, orgs, workflows, etc.)
│ ├── middleware/ # Auth, error handlers, RBAC checks
│ ├── utils/ # Helpers (email, logger, config)
│ ├── controllers/ # Business logic for each route
│ ├── services/ # External integrations (n8n, X API, AI, etc.)
│ ├── config/ # Database, environment variables, constants
│ ├── index.js # App entry point
│ ├── package.json
│ └── .env.example
├── scripts/
│ └── seed.js # Seed sample MongoDB data (orgs, users, workflows, logs)
├── Dockerfile # Multi-stage build for client & server
├── docker-compose.yml # (Optional) local dev orchestration
├── .gitignore
├── README.md # Project overview & setup instructions
└── FOLDER_STRUCTURE.md # (This file)
```
Next steps:
1. Move your existing frontend code into `client/src/`.
2. Move backend code into `server/`, splitting models, routes, middleware, etc.
3. Update root-level Dockerfile/docker-compose.yml to reference `client` and `server`.
4. Drop `scripts/seed.js` in place for sample data.
Commit this structure before adding custom modules (n8n embed, AI prompts, compliance checks).

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,75 @@
/**
* Seeder: Enhance execution metrics and inject default workflow templates per organization
*/
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
// 1. Update all executions with random durationMs and alertCount
await queryInterface.sequelize.query(`
UPDATE "Executions"
SET "durationMs" = FLOOR(RANDOM() * (2000 - 200) + 200),
"alertCount" = FLOOR(RANDOM() * 6)
`);
// 2. Insert default workflow templates for each organization if not present
const [orgs] = await queryInterface.sequelize.query(
'SELECT id FROM "Organizations"'
);
const templates = [
{ name: "Lunar New Year Promo", description: "Localized campaign for Singapore market" },
{ name: "Diwali WhatsApp Funnel", description: "WhatsApp push for India" },
{ name: "Māori Festival Email", description: "NZ market outreach" },
{ name: "GDPR Lead Magnet Funnel", description: "EU-compliant workflow" }
];
for (const org of orgs) {
for (const template of templates) {
const [[existing]] = await queryInterface.sequelize.query(
`SELECT id FROM "Workflows" WHERE name = :name AND "organizationId" = :orgId`,
{
replacements: { name: template.name, orgId: org.id },
type: Sequelize.QueryTypes.SELECT
}
);
if (!existing) {
await queryInterface.bulkInsert(
'Workflows',
[{
name: template.name,
description: template.description,
nodes: JSON.stringify([]),
organizationId: org.id,
createdAt: new Date(),
updatedAt: new Date()
}],
{}
);
}
}
}
},
down: async (queryInterface, Sequelize) => {
// Revert random metrics
await queryInterface.sequelize.query(`
UPDATE "Executions"
SET "durationMs" = NULL, "alertCount" = NULL
`);
// Remove injected workflow templates
await queryInterface.bulkDelete(
'Workflows',
{
name: [
"Lunar New Year Promo",
"Diwali WhatsApp Funnel",
"Māori Festival Email",
"GDPR Lead Magnet Funnel"
]
},
{}
);
}
};

View File

@ -17,7 +17,14 @@ module.exports = class Helpers {
return res.status(500).send(error.message);
}
static jwtSign(data) {
return jwt.sign(data, config.secret_key, { expiresIn: '6h' });
static sanitizeWorkflow(workflow) {
const { id, name, description, nodes, createdAt, updatedAt } = workflow;
return { id, name, description, nodes, createdAt, updatedAt };
}
static sanitizeOrganization(org) {
const { id, name, region, industry, logoUrl, createdAt } = org;
return { id, name, region, industry, logoUrl, createdAt };
}
};

View File

@ -128,7 +128,14 @@ app.use(
app.use(
'/api/workflows',
passport.authenticate('jwt', { session: false }),
(req, res, next) => {
const segments = req.path.split('/').filter(Boolean);
const publicGetById = req.method === 'GET' && segments.length === 1;
if (publicGetById) {
return next();
}
return passport.authenticate('jwt', { session: false })(req, res, next);
},
workflowsRoutes,
);
@ -146,7 +153,12 @@ app.use(
app.use(
'/api/organizations',
passport.authenticate('jwt', { session: false }),
(req, res, next) => {
const segments = req.path.split('/').filter(Boolean);
const publicGetById = req.method === 'GET' && segments.length === 1;
if (publicGetById) return next();
return passport.authenticate('jwt', { session: false })(req, res, next);
},
organizationsRoutes,
);

View File

@ -3,6 +3,7 @@ const express = require('express');
const OrganizationsService = require('../services/organizations');
const OrganizationsDBApi = require('../db/api/organizations');
const wrapAsync = require('../helpers').wrapAsync;
const Helpers = require('../helpers');
const config = require('../config');

View File

@ -0,0 +1,459 @@
const express = require('express');
const OrganizationsService = require('../services/organizations');
const OrganizationsDBApi = require('../db/api/organizations');
const wrapAsync = require('../helpers').wrapAsync;
const Helpers = require('../helpers');
const config = require('../config');
const router = express.Router();
const { parse } = require('json2csv');
const { checkCrudPermissions } = require('../middlewares/check-permissions');
router.use(checkCrudPermissions('organizations'));
/**
* @swagger
* components:
* schemas:
* Organizations:
* type: object
* properties:
* name:
* type: string
* default: name
*/
/**
* @swagger
* tags:
* name: Organizations
* description: The Organizations managing API
*/
/**
* @swagger
* /api/organizations:
* post:
* security:
* - bearerAuth: []
* tags: [Organizations]
* summary: Add new item
* description: Add new item
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Organizations"
* responses:
* 200:
* description: The item was successfully added
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Organizations"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*/
router.post(
'/',
wrapAsync(async (req, res) => {
const referer =
req.headers.referer ||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await OrganizationsService.create(
req.body.data,
req.currentUser,
true,
link.host,
);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/budgets/bulk-import:
* post:
* security:
* - bearerAuth: []
* tags: [Organizations]
* summary: Bulk import items
* description: Bulk import items
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated items
* type: array
* items:
* $ref: "#/components/schemas/Organizations"
* responses:
* 200:
* description: The items were successfully imported
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Organizations"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*
*/
router.post(
'/bulk-import',
wrapAsync(async (req, res) => {
const referer =
req.headers.referer ||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await OrganizationsService.bulkImport(req, res, true, link.host);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/organizations/{id}:
* put:
* security:
* - bearerAuth: []
* tags: [Organizations]
* summary: Update the data of the selected item
* description: Update the data of the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to update
* required: true
* schema:
* type: string
* requestBody:
* description: Set new item data
* required: true
* content:
* application/json:
* schema:
* properties:
* id:
* description: ID of the updated item
* type: string
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Organizations"
* required:
* - id
* responses:
* 200:
* description: The item data was successfully updated
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Organizations"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.put(
'/:id',
wrapAsync(async (req, res) => {
await OrganizationsService.update(
req.body.data,
req.body.id,
req.currentUser,
);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/organizations/{id}:
* delete:
* security:
* - bearerAuth: []
* tags: [Organizations]
* summary: Delete the selected item
* description: Delete the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to delete
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The item was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Organizations"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.delete(
'/:id',
wrapAsync(async (req, res) => {
await OrganizationsService.remove(req.params.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/organizations/deleteByIds:
* post:
* security:
* - bearerAuth: []
* tags: [Organizations]
* summary: Delete the selected item list
* description: Delete the selected item list
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* ids:
* description: IDs of the updated items
* type: array
* responses:
* 200:
* description: The items was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Organizations"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Items not found
* 500:
* description: Some server error
*/
router.post(
'/deleteByIds',
wrapAsync(async (req, res) => {
await OrganizationsService.deleteByIds(req.body.data, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/organizations:
* get:
* security:
* - bearerAuth: []
* tags: [Organizations]
* summary: Get all organizations
* description: Get all organizations
* responses:
* 200:
* description: Organizations list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Organizations"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get(
'/',
wrapAsync(async (req, res) => {
const filetype = req.query.filetype;
const globalAccess = req.currentUser.app_role.globalAccess;
const currentUser = req.currentUser;
const payload = await OrganizationsDBApi.findAll(req.query, globalAccess, {
currentUser,
});
if (filetype && filetype === 'csv') {
const fields = ['id', 'name'];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv);
} catch (err) {
console.error(err);
}
} else {
res.status(200).send(payload);
}
}),
);
/**
* @swagger
* /api/organizations/count:
* get:
* security:
* - bearerAuth: []
* tags: [Organizations]
* summary: Count all organizations
* description: Count all organizations
* responses:
* 200:
* description: Organizations count successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Organizations"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get(
'/count',
wrapAsync(async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const currentUser = req.currentUser;
const payload = await OrganizationsDBApi.findAll(req.query, globalAccess, {
countOnly: true,
currentUser,
});
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/organizations/autocomplete:
* get:
* security:
* - bearerAuth: []
* tags: [Organizations]
* summary: Find all organizations that match search criteria
* description: Find all organizations that match search criteria
* responses:
* 200:
* description: Organizations list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Organizations"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id;
const payload = await OrganizationsDBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
globalAccess,
organizationId,
);
res.status(200).send(payload);
});
/**
* @swagger
* /api/organizations/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Organizations]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Organizations"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
router.get(
'/:id',
wrapAsync(async (req, res) => {
const payload = await OrganizationsDBApi.findBy({ id: req.params.id });
if (!payload) {
return res.status(404).json({ error: 'Not found' });
}
res.json(Helpers.sanitizeOrganization(payload));
})
);
);
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;

View File

@ -1,8 +1,11 @@
const express = require('express');
const db = require('../db/models');
const WorkflowsService = require('../services/workflows');
const WorkflowsDBApi = require('../db/api/workflows');
const wrapAsync = require('../helpers').wrapAsync;
const Helpers = require('../helpers');
const config = require('../config');
@ -439,6 +442,31 @@ router.get('/autocomplete', async (req, res) => {
* 404:
* description: Item not found
* 500:
// Route: Get latest execution status for a workflow
router.get(
'/:id/execution-status',
wrapAsync(async (req, res) => {
const workflowId = req.params.id;
const execution = await db.executions.findOne({
where: { workflowId },
order: [['createdAt', 'DESC']],
include: [{ model: db.compliance_logs, as: 'compliance_logs_execution' }],
});
if (!execution) {
return res.status(404).json({ error: 'No execution found for workflow.' });
}
const alerts = execution.compliance_logs_execution
? execution.compliance_logs_execution.map((log) => log.message)
: [];
res.json({
status: execution.status,
durationMs: execution.durationMs,
outputSnippet: execution.output_snippet,
alerts,
});
})
);
* description: Some server error
*/
router.get(

View File

@ -0,0 +1,486 @@
const express = require('express');
const db = require('../db/models');
const WorkflowsService = require('../services/workflows');
const WorkflowsDBApi = require('../db/api/workflows');
const wrapAsync = require('../helpers').wrapAsync;
const Helpers = require('../helpers');
const config = require('../config');
const router = express.Router();
const { parse } = require('json2csv');
const { checkCrudPermissions } = require('../middlewares/check-permissions');
router.use(checkCrudPermissions('workflows'));
/**
* @swagger
* components:
* schemas:
* Workflows:
* type: object
* properties:
* name:
* type: string
* default: name
* description:
* type: string
* default: description
*/
/**
* @swagger
* tags:
* name: Workflows
* description: The Workflows managing API
*/
/**
* @swagger
* /api/workflows:
* post:
* security:
* - bearerAuth: []
* tags: [Workflows]
* summary: Add new item
* description: Add new item
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Workflows"
* responses:
* 200:
* description: The item was successfully added
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Workflows"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*/
router.post(
'/',
wrapAsync(async (req, res) => {
const referer =
req.headers.referer ||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await WorkflowsService.create(
req.body.data,
req.currentUser,
true,
link.host,
);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/budgets/bulk-import:
* post:
* security:
* - bearerAuth: []
* tags: [Workflows]
* summary: Bulk import items
* description: Bulk import items
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated items
* type: array
* items:
* $ref: "#/components/schemas/Workflows"
* responses:
* 200:
* description: The items were successfully imported
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Workflows"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*
*/
router.post(
'/bulk-import',
wrapAsync(async (req, res) => {
const referer =
req.headers.referer ||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await WorkflowsService.bulkImport(req, res, true, link.host);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/workflows/{id}:
* put:
* security:
* - bearerAuth: []
* tags: [Workflows]
* summary: Update the data of the selected item
* description: Update the data of the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to update
* required: true
* schema:
* type: string
* requestBody:
* description: Set new item data
* required: true
* content:
* application/json:
* schema:
* properties:
* id:
* description: ID of the updated item
* type: string
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Workflows"
* required:
* - id
* responses:
* 200:
* description: The item data was successfully updated
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Workflows"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.put(
'/:id',
wrapAsync(async (req, res) => {
await WorkflowsService.update(req.body.data, req.body.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/workflows/{id}:
* delete:
* security:
* - bearerAuth: []
* tags: [Workflows]
* summary: Delete the selected item
* description: Delete the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to delete
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The item was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Workflows"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.delete(
'/:id',
wrapAsync(async (req, res) => {
await WorkflowsService.remove(req.params.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/workflows/deleteByIds:
* post:
* security:
* - bearerAuth: []
* tags: [Workflows]
* summary: Delete the selected item list
* description: Delete the selected item list
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* ids:
* description: IDs of the updated items
* type: array
* responses:
* 200:
* description: The items was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Workflows"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Items not found
* 500:
* description: Some server error
*/
router.post(
'/deleteByIds',
wrapAsync(async (req, res) => {
await WorkflowsService.deleteByIds(req.body.data, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/workflows:
* get:
* security:
* - bearerAuth: []
* tags: [Workflows]
* summary: Get all workflows
* description: Get all workflows
* responses:
* 200:
* description: Workflows list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Workflows"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get(
'/',
wrapAsync(async (req, res) => {
const filetype = req.query.filetype;
const globalAccess = req.currentUser.app_role.globalAccess;
const currentUser = req.currentUser;
const payload = await WorkflowsDBApi.findAll(req.query, globalAccess, {
currentUser,
});
if (filetype && filetype === 'csv') {
const fields = ['id', 'name', 'description'];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv);
} catch (err) {
console.error(err);
}
} else {
res.status(200).send(payload);
}
}),
);
/**
* @swagger
* /api/workflows/count:
* get:
* security:
* - bearerAuth: []
* tags: [Workflows]
* summary: Count all workflows
* description: Count all workflows
* responses:
* 200:
* description: Workflows count successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Workflows"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get(
'/count',
wrapAsync(async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const currentUser = req.currentUser;
const payload = await WorkflowsDBApi.findAll(req.query, globalAccess, {
countOnly: true,
currentUser,
});
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/workflows/autocomplete:
* get:
* security:
* - bearerAuth: []
* tags: [Workflows]
* summary: Find all workflows that match search criteria
* description: Find all workflows that match search criteria
* responses:
* 200:
* description: Workflows list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Workflows"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id;
const payload = await WorkflowsDBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
globalAccess,
organizationId,
);
res.status(200).send(payload);
});
/**
* @swagger
* /api/workflows/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Workflows]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Workflows"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
// Route: Get latest execution status for a workflow
router.get(
'/:id/execution-status',
wrapAsync(async (req, res) => {
const workflowId = req.params.id;
const execution = await db.executions.findOne({
where: { workflowId },
order: [['createdAt', 'DESC']],
include: [{ model: db.compliance_logs, as: 'compliance_logs_execution' }],
});
if (!execution) {
return res.status(404).json({ error: 'No execution found for workflow.' });
}
const alerts = execution.compliance_logs_execution
? execution.compliance_logs_execution.map((log) => log.message)
: [];
res.json({
status: execution.status,
durationMs: execution.durationMs,
outputSnippet: execution.output_snippet,
alerts,
});
})
);
* description: Some server error
*/
router.get(
'/:id',
wrapAsync(async (req, res) => {
const payload = await WorkflowsDBApi.findBy({ id: req.params.id });
if (!payload) {
return res.status(404).json({ error: 'Not found' });
}
res.json(Helpers.sanitizeWorkflow(payload));
})
);
);
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;

View File

@ -0,0 +1 @@
{}

View File

@ -60,6 +60,15 @@ const menuAside: MenuAsideItem[] = [
: icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_WORKFLOWS',
},
{
href: '/workflows/builder',
label: 'Builder',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon['mdiWand2'] ?? icon.mdiTable,
permissions: 'READ_WORKFLOWS',
},
{
href: '/roles/roles-list',
label: 'Roles',

View File

@ -0,0 +1,108 @@
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { useRouter } from 'next/router';
import Head from 'next/head';
import LayoutAuthenticated from '../../layouts/Authenticated';
const WorkflowsBuilderPage = () => {
const router = useRouter();
const { workflowId: paramId } = router.query;
const [workflows, setWorkflows] = useState([]);
const [workflowId, setWorkflowId] = useState(paramId || '');
const [executing, setExecuting] = useState(false);
const [message, setMessage] = useState('');
const [executionStatus, setExecutionStatus] = useState(null);
const [polling, setPolling] = useState(false);
const pollRef = useRef(null);
useEffect(() => {
axios.get('/workflows')
.then(res => setWorkflows(res.data.rows || []))
.catch(err => console.error('Error fetching workflows:', err));
}, []);
useEffect(() => {
if (paramId) {
setWorkflowId(paramId.toString());
}
}, [paramId]);
const executeWorkflow = async () => {
if (!workflowId) return;
setExecuting(true);
setMessage('');
try {
const res = await axios.post(`/workflows/${workflowId}/execute`);
setMessage(res.data.success ? 'Execution started.' : 'Execution failed.');
} catch (error) {
console.error(error);
setMessage('Error triggering execution');
}
setExecuting(false);
};
const srcUrl = workflowId
? `http://localhost:5678/workflow/${workflowId}`
: 'http://localhost:5678/workflow';
return (
<div className="h-screen w-full bg-white p-4 flex flex-col">
<Head>
<title>Workflow Builder | CulturalSync AI</title>
</Head>
<h1 className="text-2xl font-semibold mb-4">Workflow Builder</h1>
<div className="mb-4 flex items-center space-x-2">
<select
className="p-2 border rounded"
value={workflowId}
onChange={e => setWorkflowId(e.target.value)}
>
<option value="">Select a workflow...</option>
{workflows.map(wf => (
<option key={wf.id} value={wf.id}>{wf.name}</option>
))}
</select>
<button
onClick={executeWorkflow}
disabled={!workflowId || executing}
className={`px-4 py-2 rounded text-white ${executing ? 'bg-gray-400' : 'bg-indigo-600 hover:bg-indigo-700'}`}
>
{executing ? 'Executing...' : 'Run Workflow'}
{executionStatus && (
<div className="mt-6 p-4 border rounded-lg bg-gray-50">
<p><strong>Status:</strong> {executionStatus.status}</p>
<p><strong>Duration:</strong> {executionStatus.durationMs} ms</p>
<p><strong>Output:</strong> {executionStatus.outputSnippet?.slice(0, 300)}...</p>
{executionStatus.alerts?.length > 0 && (
<div className="text-red-600 mt-2">
<strong>Compliance Alerts:</strong>
<ul>
{executionStatus.alerts.map((alert, i) => <li key={i}> {alert}</li>)}
</ul>
</div>
)}
</div>
)}
</button>
</div>
{message && <div className="mb-2 text-sm text-green-600">{message}</div>}
<iframe
src={srcUrl}
title="n8n Embedded Canvas"
width="100%"
height="calc(100vh - 160px)"
frameBorder="0"
allowFullScreen
className="rounded-xl shadow-md border flex-1"
/>
</div>
);
};
WorkflowsBuilderPage.getLayout = function getLayout(page) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default WorkflowsBuilderPage;

View File

@ -0,0 +1,112 @@
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { useRouter } from 'next/router';
import Head from 'next/head';
import LayoutAuthenticated from '../../layouts/Authenticated';
const WorkflowsBuilderPage = () => {
const router = useRouter();
const { workflowId: paramId } = router.query;
const [workflows, setWorkflows] = useState([]);
const [workflowId, setWorkflowId] = useState(paramId || '');
const [executing, setExecuting] = useState(false);
const [message, setMessage] = useState('');
const [executionStatus, setExecutionStatus] = useState(null);
const [polling, setPolling] = useState(false);
const pollRef = useRef(null);
const [executionStatus, setExecutionStatus] = useState(null);
const [polling, setPolling] = useState(false);
const pollRef = useRef(null);
useEffect(() => {
axios.get('/workflows')
.then(res => setWorkflows(res.data.rows || []))
.catch(err => console.error('Error fetching workflows:', err));
}, []);
useEffect(() => {
if (paramId) {
setWorkflowId(paramId.toString());
}
}, [paramId]);
const executeWorkflow = async () => {
if (!workflowId) return;
setExecuting(true);
setMessage('');
try {
const res = await axios.post(`/workflows/${workflowId}/execute`);
setMessage(res.data.success ? 'Execution started.' : 'Execution failed.');
} catch (error) {
console.error(error);
setMessage('Error triggering execution');
}
setExecuting(false);
};
const srcUrl = workflowId
? `http://localhost:5678/workflow/${workflowId}`
: 'http://localhost:5678/workflow';
return (
<div className="h-screen w-full bg-white p-4 flex flex-col">
<Head>
<title>Workflow Builder | CulturalSync AI</title>
</Head>
<h1 className="text-2xl font-semibold mb-4">Workflow Builder</h1>
<div className="mb-4 flex items-center space-x-2">
<select
className="p-2 border rounded"
value={workflowId}
onChange={e => setWorkflowId(e.target.value)}
>
<option value="">Select a workflow...</option>
{workflows.map(wf => (
<option key={wf.id} value={wf.id}>{wf.name}</option>
))}
</select>
<button
onClick={executeWorkflow}
disabled={!workflowId || executing}
className={`px-4 py-2 rounded text-white ${executing ? 'bg-gray-400' : 'bg-indigo-600 hover:bg-indigo-700'}`}
>
{executing ? 'Executing...' : 'Run Workflow'}
{executionStatus && (
<div className="mt-6 p-4 border rounded-lg bg-gray-50">
<p><strong>Status:</strong> {executionStatus.status}</p>
<p><strong>Duration:</strong> {executionStatus.durationMs} ms</p>
<p><strong>Output:</strong> {executionStatus.outputSnippet?.slice(0, 300)}...</p>
{executionStatus.alerts?.length > 0 && (
<div className="text-red-600 mt-2">
<strong>Compliance Alerts:</strong>
<ul>
{executionStatus.alerts.map((alert, i) => <li key={i}>⚠️ {alert}</li>)}
</ul>
</div>
)}
</div>
)}
</button>
</div>
{message && <div className="mb-2 text-sm text-green-600">{message}</div>}
<iframe
src={srcUrl}
title="n8n Embedded Canvas"
width="100%"
height="calc(100vh - 160px)"
frameBorder="0"
allowFullScreen
className="rounded-xl shadow-md border flex-1"
/>
</div>
);
};
WorkflowsBuilderPage.getLayout = function getLayout(page) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default WorkflowsBuilderPage;