version 2
This commit is contained in:
parent
852719d2c1
commit
1cd6cbb6dc
49
FOLDER_STRUCTURE.md
Normal file
49
FOLDER_STRUCTURE.md
Normal 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
@ -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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,4 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const db = require('../db/models');
|
||||||
|
|
||||||
|
|
||||||
const WorkflowsService = require('../services/workflows');
|
const WorkflowsService = require('../services/workflows');
|
||||||
const WorkflowsDBApi = require('../db/api/workflows');
|
const WorkflowsDBApi = require('../db/api/workflows');
|
||||||
@ -439,6 +441,31 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
* 404:
|
* 404:
|
||||||
* description: Item not found
|
* description: Item not found
|
||||||
* 500:
|
* 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
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.get(
|
router.get(
|
||||||
|
|||||||
1
frontend/json/runtimeError.json
Normal file
1
frontend/json/runtimeError.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@ -60,6 +60,15 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
: icon.mdiTable ?? icon.mdiTable,
|
: icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_WORKFLOWS',
|
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',
|
href: '/roles/roles-list',
|
||||||
label: 'Roles',
|
label: 'Roles',
|
||||||
|
|||||||
108
frontend/src/pages/workflows/builder.tsx
Normal file
108
frontend/src/pages/workflows/builder.tsx
Normal 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;
|
||||||
Loading…
x
Reference in New Issue
Block a user