version 2

This commit is contained in:
Flatlogic Bot 2025-07-06 19:16:22 +00:00
parent 852719d2c1
commit 1cd6cbb6dc
7 changed files with 271 additions and 1 deletions

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

@ -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(

View File

@ -0,0 +1 @@
{}

View File

@ -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',

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;