feat: complete node task 2a

This commit is contained in:
Ayobami
2025-07-28 06:42:00 +01:00
parent fe95626d9f
commit bd70df60b9
17 changed files with 1968 additions and 170 deletions
+2
View File
@@ -1,3 +1,5 @@
PORT=8000 PORT=8000
WEATHER_API_KEY=de18eeaec53e4caca18170027252507 WEATHER_API_KEY=de18eeaec53e4caca18170027252507
DB_PASSWORD=ayobamidavid DB_PASSWORD=ayobamidavid
STRIPE_SECRET_KEY=sk_test_51IWQUwH8oljXErmds28KftkL6o6jYIcPgYbBdfEmCPSuAlIh0fgoS4NADcCmsIZbdQ3p5nbAeCOcGkSmo38U9BIe00BdOenrqo
STRIPE_PUBLIC_KEY=pk_test_51IWQUwH8oljXErmdg6L4MhsuB6tDdmumlHFfyNaopty2U27pmRcqMX1c868zn838lGQtU1eYV6bKRSQtMFWf36VT00aNsvnTOE
+26
View File
@@ -0,0 +1,26 @@
module.exports = (sequelize, DataTypes) => {
const chat = sequelize.define(
"chat",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
create_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
chat_messages: {
type: DataTypes.TEXT,
allowNull: false,
},
},
{
timestamps: false,
freezeTableName: true,
tableName: "chat",
}
);
return chat;
};
+30
View File
@@ -0,0 +1,30 @@
module.exports = (sequelize, DataTypes) => {
const flow = sequelize.define(
"flow",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
},
{
timestamps: false,
freezeTableName: true,
tableName: "flow",
}
);
return flow;
};
+39
View File
@@ -0,0 +1,39 @@
module.exports = (sequelize, DataTypes) => {
const flow_log = sequelize.define(
"flow_log",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
flow_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
task_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
result: {
type: DataTypes.TEXT,
allowNull: true,
},
status: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "pending",
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
},
{
timestamps: false,
freezeTableName: true,
tableName: "flow_logs",
}
);
return flow_log;
};
+42
View File
@@ -0,0 +1,42 @@
module.exports = (sequelize, DataTypes) => {
const task = sequelize.define(
"task",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
flow_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: "flow",
key: "id",
},
},
action_type: {
type: DataTypes.STRING,
allowNull: false,
},
input_data: {
type: DataTypes.TEXT,
allowNull: false,
},
order_index: {
type: DataTypes.INTEGER,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
},
{
timestamps: false,
freezeTableName: true,
tableName: "task",
}
);
return task;
};
+34
View File
@@ -0,0 +1,34 @@
module.exports = (sequelize, DataTypes) => {
const upload = sequelize.define(
"upload",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
filename: {
type: DataTypes.STRING,
allowNull: false,
},
mimetype: {
type: DataTypes.STRING,
allowNull: false,
},
path: {
type: DataTypes.STRING,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
},
{
timestamps: false,
freezeTableName: true,
tableName: "upload",
}
);
return upload;
};
+800 -24
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -17,11 +17,14 @@
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"mariadb": "^3.0.1", "mariadb": "^3.0.1",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"multer": "^2.0.2",
"mysql2": "^2.3.3", "mysql2": "^2.3.3",
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"ol": "^7.1.0", "ol": "^7.1.0",
"pug": "^3.0.2", "pug": "^3.0.2",
"redis": "^5.6.1",
"sequelize": "^6.21.6", "sequelize": "^6.21.6",
"stripe": "^18.3.0",
"xmlbuilder2": "^3.1.1" "xmlbuilder2": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
+95
View File
@@ -0,0 +1,95 @@
const messageInput = document.getElementById("message-input");
const sendBtn = document.getElementById("send-btn");
const messageList = document.getElementById("message-list");
const saveBtn = document.getElementById("save-btn");
const chatMessages = document.getElementById("chat-messages");
let lastMessageCount = 0;
// Send message
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
try {
const response = await fetch("/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
if (response.ok) {
messageInput.value = "";
await fetchMessages();
}
} catch (err) {
console.error("Failed to send message:", err);
}
}
// Fetch all messages
async function fetchMessages() {
try {
const response = await fetch("/chat/all");
const messages = await response.json();
messageList.innerHTML = "";
messages.forEach((msg) => {
const li = document.createElement("li");
li.className = "p-2 bg-gray-100 rounded";
li.innerHTML = `
<div class="text-sm text-gray-600">${new Date(
msg.timestamp
).toLocaleString()}</div>
<div class="font-medium">${msg.message}</div>
`;
messageList.appendChild(li);
});
// Auto-scroll to bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
lastMessageCount = messages.length;
} catch (err) {
console.error("Failed to fetch messages:", err);
}
}
// Poll for updates
async function pollForUpdates() {
try {
const response = await fetch(`/poll?lastCheck=${lastMessageCount}`);
const data = await response.json();
if (data.updated) {
await fetchMessages();
}
} catch (err) {
console.error("Poll failed:", err);
}
}
// Save chat
async function saveChat() {
try {
const response = await fetch("/chat/save", { method: "POST" });
if (response.ok) {
alert("Chat saved successfully!");
} else {
alert("Failed to save chat");
}
} catch (err) {
console.error("Failed to save chat:", err);
alert("Failed to save chat");
}
}
// Event listeners
sendBtn.addEventListener("click", sendMessage);
messageInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") sendMessage();
});
saveBtn.addEventListener("click", saveChat);
// Initial load and start polling
fetchMessages();
setInterval(pollForUpdates, 2000);
+227
View File
@@ -0,0 +1,227 @@
// DOM elements
const createFlowBtn = document.getElementById("create-flow-btn");
const flowNameInput = document.getElementById("flow-name");
const flowDescriptionInput = document.getElementById("flow-description");
const flowSelect = document.getElementById("flow-select");
const actionTypeSelect = document.getElementById("action-type");
const taskInput = document.getElementById("task-input");
const orderIndexInput = document.getElementById("order-index");
const addTaskBtn = document.getElementById("add-task-btn");
const flowDetails = document.getElementById("flow-details");
const executeFlowSelect = document.getElementById("execute-flow-select");
const executePayloadInput = document.getElementById("execute-payload");
const executeBtn = document.getElementById("execute-btn");
const webhookBtn = document.getElementById("webhook-btn");
const executionResults = document.getElementById("execution-results");
let currentFlows = [];
// Create new flow
async function createFlow() {
const name = flowNameInput.value.trim();
const description = flowDescriptionInput.value.trim();
if (!name) {
alert("Flow name is required");
return;
}
try {
const response = await fetch("/flow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, description }),
});
if (response.ok) {
const flow = await response.json();
alert(`Flow "${flow.name}" created successfully!`);
flowNameInput.value = "";
flowDescriptionInput.value = "";
loadFlows();
} else {
const error = await response.json();
alert(`Error: ${error.error}`);
}
} catch (err) {
alert("Failed to create flow");
}
}
// Load all flows
async function loadFlows() {
try {
const response = await fetch("/flows");
const flows = await response.json();
currentFlows = flows;
// Update flow selects
flowSelect.innerHTML = '<option value="">Select Flow</option>';
executeFlowSelect.innerHTML =
'<option value="">Select Flow to Execute</option>';
flows.forEach((flow) => {
flowSelect.innerHTML += `<option value="${flow.id}">${flow.name}</option>`;
executeFlowSelect.innerHTML += `<option value="${flow.id}">${flow.name}</option>`;
});
} catch (err) {
console.error("Failed to load flows");
}
}
// Add task to flow
async function addTask() {
const flowId = flowSelect.value;
const actionType = actionTypeSelect.value;
const inputData = taskInput.value.trim();
const orderIndex = parseInt(orderIndexInput.value);
if (!flowId || !actionType || !inputData || isNaN(orderIndex)) {
alert("All fields are required");
return;
}
try {
const response = await fetch(`/flow/${flowId}/task`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action_type: actionType,
input_data: inputData,
order_index: orderIndex,
}),
});
if (response.ok) {
const task = await response.json();
alert(`Task "${task.action_type}" added successfully!`);
taskInput.value = "";
orderIndexInput.value = "";
loadFlowDetails(flowId);
} else {
const error = await response.json();
alert(`Error: ${error.error}`);
}
} catch (err) {
alert("Failed to add task");
}
}
// Load flow details
async function loadFlowDetails(flowId) {
if (!flowId) return;
try {
const response = await fetch(`/flow/${flowId}`);
const data = await response.json();
flowDetails.innerHTML = `
<div class="border rounded p-4">
<h3 class="font-semibold text-lg">${data.flow.name}</h3>
<p class="text-gray-600">${
data.flow.description || "No description"
}</p>
<div class="mt-4">
<h4 class="font-medium">Tasks:</h4>
<div class="space-y-2 mt-2">
${data.tasks
.map(
(task) => `
<div class="bg-gray-100 p-2 rounded">
<div class="font-medium">${task.action_type}</div>
<div class="text-sm text-gray-600">Input: ${task.input_data}</div>
<div class="text-xs text-gray-500">Order: ${task.order_index}</div>
</div>
`
)
.join("")}
</div>
</div>
</div>
`;
} catch (err) {
console.error("Failed to load flow details");
}
}
// Execute flow
async function executeFlow() {
const flowId = executeFlowSelect.value;
const payload = executePayloadInput.value.trim();
if (!flowId) {
alert("Please select a flow to execute");
return;
}
try {
const response = await fetch(`/flow/${flowId}/execute`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload }),
});
if (response.ok) {
const result = await response.json();
showExecutionResults(result);
} else {
const error = await response.json();
alert(`Error: ${error.error}`);
}
} catch (err) {
alert("Failed to execute flow");
}
}
// Show execution results
function showExecutionResults(result) {
executionResults.innerHTML = `
<h4 class="font-semibold mb-2">Execution Results:</h4>
<div class="space-y-2">
${result.results
.map(
(r) => `
<div class="p-2 ${
r.status === "success" ? "bg-green-100" : "bg-red-100"
} rounded">
<div class="font-medium">Task ${r.task_id}</div>
<div class="text-sm">${r.result}</div>
<div class="text-xs text-gray-600">Status: ${r.status}</div>
</div>
`
)
.join("")}
</div>
`;
executionResults.classList.remove("hidden");
}
// Get webhook URL
function getWebhookUrl() {
const flowId = executeFlowSelect.value;
if (!flowId) {
alert("Please select a flow");
return;
}
const webhookUrl = `${window.location.origin}/flow/${flowId}/trigger?payload=test@example.com`;
alert(
`Webhook URL:\n${webhookUrl}\n\nCopy this URL to trigger the flow via webhook.`
);
}
// Event listeners
createFlowBtn.addEventListener("click", createFlow);
addTaskBtn.addEventListener("click", addTask);
executeBtn.addEventListener("click", executeFlow);
webhookBtn.addEventListener("click", getWebhookUrl);
flowSelect.addEventListener("change", (e) => {
if (e.target.value) {
loadFlowDetails(e.target.value);
}
});
// Initialize
loadFlows();
+54 -66
View File
@@ -79,68 +79,6 @@ async function updateClocks() {
updateClocks(); updateClocks();
setInterval(updateClocks, 1000); setInterval(updateClocks, 1000);
// --- Airport Autocomplete ---
// const airportInput = document.querySelector('input[placeholder*="airport"]');
// let dropdownDiv;
// if (airportInput) {
// console.log("hi");
// dropdownDiv = document.createElement("div");
// dropdownDiv.className =
// "absolute bg-white border border-gray-300 rounded shadow z-10 w-full max-h-48 overflow-y-auto top-[calc(100%+10px)]";
// dropdownDiv.style.display = "none";
// airportInput.parentNode.appendChild(dropdownDiv);
// airportInput.addEventListener("input", async function () {
// const val = airportInput.value.trim();
// if (val.length < 3) {
// dropdownDiv.style.display = "none";
// return;
// }
// try {
// const res = await fetch(`/airports?search=${encodeURIComponent(val)}`);
// if (!res.ok) throw new Error("Failed to fetch airports");
// const airports = await res.json();
// if (!Array.isArray(airports) || airports.length === 0) {
// dropdownDiv.innerHTML =
// '<div class="p-2 text-gray-500">No results</div>';
// dropdownDiv.style.display = "block";
// return;
// }
// dropdownDiv.innerHTML = airports
// .map(
// (a, i) =>
// `<div class="p-2 hover:bg-sky-100 cursor-pointer" data-index="${i}">${a.name}</div>`
// )
// .join("");
// dropdownDiv.style.display = "block";
// Array.from(dropdownDiv.children).forEach((child, i) => {
// child.addEventListener("click", () => {
// airportInput.value = child.textContent;
// dropdownDiv.style.display = "none";
// selectedAirport = airports[i];
// console.log("hey");
// airportInput.textContent = `hold`;
// if (selectedAirport) {
// console.log(selectedAirport);
// airportInput.value = `${selectedAirport.name} (${selectedAirport.code})`;
// showMap(
// Number(selectedAirport.latitude_deg),
// Number(selectedAirport.longitude_deg)
// );
// }
// });
// });
// } catch (err) {
// dropdownDiv.innerHTML =
// '<div class="p-2 text-red-500">Error loading airports</div>';
// dropdownDiv.style.display = "block";
// }
// });
// // Hide dropdown on blur
// airportInput.addEventListener("blur", () =>
// setTimeout(() => (dropdownDiv.style.display = "none"), 200)
// );
// }
// --- Airport Autocomplete --- // --- Airport Autocomplete ---
const airportInput = document.querySelector('input[placeholder*="airport"]'); const airportInput = document.querySelector('input[placeholder*="airport"]');
let dropdownDiv; let dropdownDiv;
@@ -163,10 +101,6 @@ if (airportInput) {
selectedAirport = currentAirports[index]; // Use latest fetched data selectedAirport = currentAirports[index]; // Use latest fetched data
airportInput.value = `${selectedAirport.name} (${selectedAirport.code})`; // Set once airportInput.value = `${selectedAirport.name} (${selectedAirport.code})`; // Set once
// showMap(
// Number(selectedAirport.latitude_deg),
// Number(selectedAirport.longitude_deg)
// );
onAirportSelected(selectedAirport); onAirportSelected(selectedAirport);
dropdownDiv.style.display = "none"; dropdownDiv.style.display = "none";
}); });
@@ -307,6 +241,11 @@ function logWidgetClick(widgetName) {
widget_name: widgetName, widget_name: widgetName,
browser_type: navigator.userAgent, browser_type: navigator.userAgent,
}), }),
}).then(async (res) => {
if (res.status === 429) {
const data = await res.json();
if (data.redirect) window.location.href = data.redirect;
}
}); });
} }
@@ -394,6 +333,55 @@ if (coinInput && coinBtn && coinResult) {
}); });
} }
// --- Upload Widget ---
const uploadInput = document.querySelector(
'input[type="file"][id="upload-input"]'
);
const uploadBtn = document.getElementById("upload-btn");
const uploadPreview = document.getElementById("upload-preview");
const uploadError = document.getElementById("upload-error");
async function fetchLatestUpload() {
try {
const res = await fetch("/upload/latest");
const data = await res.json();
if (data.url && uploadPreview) {
uploadPreview.src = data.url;
uploadPreview.style.display = "";
} else if (uploadPreview) {
uploadPreview.style.display = "none";
}
} catch {}
}
if (uploadBtn && uploadInput) {
uploadBtn.addEventListener("click", async function () {
if (!uploadInput.files || !uploadInput.files[0]) {
if (uploadError) uploadError.textContent = "Please select an image.";
return;
}
const formData = new FormData();
formData.append("image", uploadInput.files[0]);
try {
const res = await fetch("/upload", { method: "POST", body: formData });
const data = await res.json();
if (data.url) {
if (uploadPreview) {
uploadPreview.src = data.url;
uploadPreview.style.display = "";
}
if (uploadError) uploadError.textContent = "";
} else {
if (uploadError)
uploadError.textContent = data.error || "Upload failed.";
}
} catch (err) {
if (uploadError) uploadError.textContent = "Upload failed.";
}
});
}
fetchLatestUpload();
// Attach click listeners to widgets // Attach click listeners to widgets
function attachAnalyticListeners() { function attachAnalyticListeners() {
const widgets = [ const widgets = [
+104 -73
View File
@@ -8,14 +8,20 @@
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace; "Courier New", monospace;
--color-red-500: oklch(63.7% 0.237 25.331); --color-red-500: oklch(63.7% 0.237 25.331);
--color-green-500: oklch(72.3% 0.219 149.579);
--color-green-600: oklch(62.7% 0.194 149.214);
--color-sky-100: oklch(95.1% 0.026 236.824); --color-sky-100: oklch(95.1% 0.026 236.824);
--color-sky-400: oklch(74.6% 0.16 232.661); --color-sky-400: oklch(74.6% 0.16 232.661);
--color-blue-100: oklch(93.2% 0.032 255.585); --color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376);
--color-gray-100: oklch(96.7% 0.003 264.542); --color-gray-100: oklch(96.7% 0.003 264.542);
--color-gray-300: oklch(87.2% 0.01 258.338); --color-gray-300: oklch(87.2% 0.01 258.338);
--color-gray-500: oklch(55.1% 0.027 264.364); --color-gray-500: oklch(55.1% 0.027 264.364);
--color-gray-600: oklch(44.6% 0.03 256.802);
--color-white: #fff; --color-white: #fff;
--spacing: 0.25rem; --spacing: 0.25rem;
--container-2xl: 42rem;
--text-xs: 0.75rem; --text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75); --text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem; --text-sm: 0.875rem;
@@ -198,6 +204,27 @@
.row-span-2 { .row-span-2 {
grid-row: span 2 / span 2; grid-row: span 2 / span 2;
} }
.container {
width: 100%;
@media (width >= 40rem) {
max-width: 40rem;
}
@media (width >= 48rem) {
max-width: 48rem;
}
@media (width >= 64rem) {
max-width: 64rem;
}
@media (width >= 80rem) {
max-width: 80rem;
}
@media (width >= 96rem) {
max-width: 96rem;
}
}
.mx-auto {
margin-inline: auto;
}
.mt-2 { .mt-2 {
margin-top: calc(var(--spacing) * 2); margin-top: calc(var(--spacing) * 2);
} }
@@ -210,6 +237,12 @@
.mb-2 { .mb-2 {
margin-bottom: calc(var(--spacing) * 2); margin-bottom: calc(var(--spacing) * 2);
} }
.mb-4 {
margin-bottom: calc(var(--spacing) * 4);
}
.mb-6 {
margin-bottom: calc(var(--spacing) * 6);
}
.ml-2 { .ml-2 {
margin-left: calc(var(--spacing) * 2); margin-left: calc(var(--spacing) * 2);
} }
@@ -228,6 +261,9 @@
.h-24 { .h-24 {
height: calc(var(--spacing) * 24); height: calc(var(--spacing) * 24);
} }
.h-96 {
height: calc(var(--spacing) * 96);
}
.h-244 { .h-244 {
height: calc(var(--spacing) * 244); height: calc(var(--spacing) * 244);
} }
@@ -252,6 +288,12 @@
.w-full { .w-full {
width: 100%; width: 100%;
} }
.max-w-2xl {
max-width: var(--container-2xl);
}
.flex-1 {
flex: 1;
}
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
@@ -276,6 +318,9 @@
.justify-center { .justify-center {
justify-content: center; justify-content: center;
} }
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.gap-8 { .gap-8 {
gap: calc(var(--spacing) * 8); gap: calc(var(--spacing) * 8);
} }
@@ -302,12 +347,15 @@
.border-gray-300 { .border-gray-300 {
border-color: var(--color-gray-300); border-color: var(--color-gray-300);
} }
.bg-blue-100 { .bg-blue-500 {
background-color: var(--color-blue-100); background-color: var(--color-blue-500);
} }
.bg-gray-100 { .bg-gray-100 {
background-color: var(--color-gray-100); background-color: var(--color-gray-100);
} }
.bg-green-500 {
background-color: var(--color-green-500);
}
.bg-inherit { .bg-inherit {
background-color: inherit; background-color: inherit;
} }
@@ -317,6 +365,9 @@
.bg-white { .bg-white {
background-color: var(--color-white); background-color: var(--color-white);
} }
.object-cover {
object-fit: cover;
}
.p-2 { .p-2 {
padding: calc(var(--spacing) * 2); padding: calc(var(--spacing) * 2);
} }
@@ -329,6 +380,12 @@
.p-8 { .p-8 {
padding: calc(var(--spacing) * 8); padding: calc(var(--spacing) * 8);
} }
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
.px-6 {
padding-inline: calc(var(--spacing) * 6);
}
.px-8 { .px-8 {
padding-inline: calc(var(--spacing) * 8); padding-inline: calc(var(--spacing) * 8);
} }
@@ -373,9 +430,15 @@
--tw-font-weight: var(--font-weight-semibold); --tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
} }
.text-blue-700 {
color: var(--color-blue-700);
}
.text-gray-500 { .text-gray-500 {
color: var(--color-gray-500); color: var(--color-gray-500);
} }
.text-gray-600 {
color: var(--color-gray-600);
}
.text-red-500 { .text-red-500 {
color: var(--color-red-500); color: var(--color-red-500);
} }
@@ -390,14 +453,24 @@
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.blur {
--tw-blur: blur(8px);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.outline-none { .outline-none {
--tw-outline-style: none; --tw-outline-style: none;
outline-style: none; outline-style: none;
} }
.hover\:bg-blue-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-blue-600);
}
}
}
.hover\:bg-green-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-green-600);
}
}
}
.hover\:bg-sky-100 { .hover\:bg-sky-100 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -405,6 +478,30 @@
} }
} }
} }
.hover\:underline {
&:hover {
@media (hover: hover) {
text-decoration-line: underline;
}
}
}
.focus\:ring-2 {
&:focus {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
.focus\:ring-blue-500 {
&:focus {
--tw-ring-color: var(--color-blue-500);
}
}
.focus\:outline-none {
&:focus {
--tw-outline-style: none;
outline-style: none;
}
}
} }
@property --tw-space-y-reverse { @property --tw-space-y-reverse {
syntax: "*"; syntax: "*";
@@ -485,59 +582,6 @@
inherits: false; inherits: false;
initial-value: 0 0 #0000; initial-value: 0 0 #0000;
} }
@property --tw-blur {
syntax: "*";
inherits: false;
}
@property --tw-brightness {
syntax: "*";
inherits: false;
}
@property --tw-contrast {
syntax: "*";
inherits: false;
}
@property --tw-grayscale {
syntax: "*";
inherits: false;
}
@property --tw-hue-rotate {
syntax: "*";
inherits: false;
}
@property --tw-invert {
syntax: "*";
inherits: false;
}
@property --tw-opacity {
syntax: "*";
inherits: false;
}
@property --tw-saturate {
syntax: "*";
inherits: false;
}
@property --tw-sepia {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-drop-shadow-size {
syntax: "*";
inherits: false;
}
@layer properties { @layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop { *, ::before, ::after, ::backdrop {
@@ -558,19 +602,6 @@
--tw-ring-offset-width: 0px; --tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff; --tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000; --tw-ring-offset-shadow: 0 0 #0000;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
--tw-grayscale: initial;
--tw-hue-rotate: initial;
--tw-invert: initial;
--tw-opacity: initial;
--tw-saturate: initial;
--tw-sepia: initial;
--tw-drop-shadow: initial;
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
} }
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+323 -1
View File
@@ -8,6 +8,25 @@ const airportData = JSON.parse(
); );
const db = require("../models"); const db = require("../models");
const { create } = require("xmlbuilder2"); const { create } = require("xmlbuilder2");
const rateLimitMap = new Map();
const multer = require("multer");
const uploadDir = path.join(__dirname, "../public/uploads");
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadDir);
},
filename: function (req, file, cb) {
const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);
cb(null, unique + "-" + file.originalname);
},
});
const uploadMulter = multer({ storage });
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
const redis = require("redis");
const client = redis.createClient();
client.connect().catch(console.error);
/* GET home page. */ /* GET home page. */
router.get("/", function (req, res, next) { router.get("/", function (req, res, next) {
@@ -60,9 +79,20 @@ router.get("/airports", function (req, res) {
res.json(matches); res.json(matches);
}); });
// Log analytic event // Log analytic event with rate limiting
router.post("/analytic", async function (req, res) { router.post("/analytic", async function (req, res) {
try { try {
const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
const now = Date.now();
const windowMs = 60 * 1000;
const maxReq = 10;
if (!rateLimitMap.has(ip)) rateLimitMap.set(ip, []);
let timestamps = rateLimitMap.get(ip).filter((ts) => now - ts < windowMs);
if (timestamps.length >= maxReq) {
return res.status(429).json({ redirect: "/pay" });
}
timestamps.push(now);
rateLimitMap.set(ip, timestamps);
const { widget_name, browser_type } = req.body; const { widget_name, browser_type } = req.body;
if (!widget_name || !browser_type) { if (!widget_name || !browser_type) {
return res return res
@@ -162,4 +192,296 @@ router.post("/coin-calc", function (req, res) {
} }
}); });
// Stripe payment page
router.get("/pay", async function (req, res) {
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: { name: "Widget Analytics Access" },
unit_amount: 500,
},
quantity: 1,
},
],
mode: "payment",
success_url: req.protocol + "://" + req.get("host") + "/?paid=1",
cancel_url: req.protocol + "://" + req.get("host") + "/pay?cancel=1",
});
res.send(`
<html>
<head><title>Pay for Analytics</title></head>
<body style="display:flex;align-items:center;justify-content:center;height:100vh;flex-direction:column;">
<h2>Rate limit exceeded</h2>
<p>You need to pay $5 to continue using analytics.</p>
<button id="checkout">Pay with Stripe</button>
<script src="https://js.stripe.com/v3/"></script>
<script>
document.getElementById('checkout').onclick = function() {
var stripe = Stripe('${STRIPE_PUBLIC_KEY}');
stripe.redirectToCheckout({ sessionId: '${session.id}' });
}
</script>
</body>
</html>
`);
});
// Upload image
router.post("/upload", uploadMulter.single("image"), async function (req, res) {
try {
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
const file = req.file;
const dbUpload = await db.upload.create({
filename: file.filename,
mimetype: file.mimetype,
path: "/uploads/" + file.filename,
});
res.json({ url: "/uploads/" + file.filename });
} catch (err) {
res.status(500).json({ error: "Failed to upload image" });
}
});
// Get latest uploaded image
router.get("/upload/latest", async function (req, res) {
try {
const latest = await db.upload.findOne({ order: [["created_at", "DESC"]] });
if (!latest) return res.json({ url: null });
res.json({ url: latest.path });
} catch (err) {
res.status(500).json({ error: "Failed to fetch latest upload" });
}
});
// Chat routes
router.post("/send", async function (req, res) {
try {
const { message } = req.body;
if (!message) return res.status(400).json({ error: "Message required" });
const chatMessage = {
message,
timestamp: new Date().toISOString(),
id: Date.now(),
};
await client.lPush("chatroom", JSON.stringify(chatMessage));
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: "Failed to send message" });
}
});
router.get("/chat/all", async function (req, res) {
try {
const messages = await client.lRange("chatroom", 0, -1);
const parsedMessages = messages.map((msg) => JSON.parse(msg));
res.json(parsedMessages);
} catch (err) {
res.status(500).json({ error: "Failed to fetch messages" });
}
});
router.get("/poll", async function (req, res) {
try {
const messageCount = await client.lLen("chatroom");
const lastCheck = req.query.lastCheck || 0;
if (messageCount > lastCheck) {
res.json({ updated: true, count: messageCount });
} else {
res.json({ updated: false, count: messageCount });
}
} catch (err) {
res.status(500).json({ error: "Poll failed" });
}
});
router.post("/chat/save", async function (req, res) {
try {
const messages = await client.lRange("chatroom", 0, -1);
const chatMessages = JSON.stringify(messages.map((msg) => JSON.parse(msg)));
await db.chat.create({
chat_messages: chatMessages,
});
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: "Failed to save chat" });
}
});
// Chat page
router.get("/chat", function (req, res) {
res.sendFile(path.join(__dirname, "../views/chat.html"));
});
// Flow Builder routes
router.get("/flow", function (req, res) {
res.sendFile(path.join(__dirname, "../views/flow.html"));
});
// Get all flows
router.get("/flows", async function (req, res) {
try {
const flows = await db.flow.findAll({
order: [["created_at", "DESC"]],
});
res.json(flows);
} catch (err) {
res.status(500).json({ error: "Failed to fetch flows" });
}
});
// Create new flow
router.post("/flow", async function (req, res) {
try {
const { name, description } = req.body;
if (!name) return res.status(400).json({ error: "Flow name required" });
const flow = await db.flow.create({ name, description });
res.json({ id: flow.id, name: flow.name });
} catch (err) {
res.status(500).json({ error: "Failed to create flow" });
}
});
// Add task to flow
router.post("/flow/:id/task", async function (req, res) {
try {
const flowId = req.params.id;
const { action_type, input_data, order_index } = req.body;
if (!action_type || !input_data || order_index === undefined) {
return res
.status(400)
.json({ error: "action_type, input_data, and order_index required" });
}
const task = await db.task.create({
flow_id: flowId,
action_type,
input_data,
order_index,
});
res.json({ id: task.id, action_type: task.action_type });
} catch (err) {
res.status(500).json({ error: "Failed to add task" });
}
});
// Get flow details
router.get("/flow/:id", async function (req, res) {
try {
const flowId = req.params.id;
const flow = await db.flow.findByPk(flowId);
const tasks = await db.task.findAll({
where: { flow_id: flowId },
order: [["order_index", "ASC"]],
});
res.json({ flow, tasks });
} catch (err) {
res.status(500).json({ error: "Failed to fetch flow" });
}
});
// Execute flow
router.post("/flow/:id/execute", async function (req, res) {
try {
const flowId = req.params.id;
const { payload } = req.body;
const flow = await db.flow.findByPk(flowId);
const tasks = await db.task.findAll({
where: { flow_id: flowId },
order: [["order_index", "ASC"]],
});
const results = [];
for (const task of tasks) {
let result = "";
let status = "success";
try {
switch (task.action_type) {
case "send_test_mail":
// Simulate sending email
result = `Email sent to: ${task.input_data}`;
break;
case "http_get_request":
const response = await fetch(task.input_data);
result = `HTTP GET ${task.input_data}: ${response.status}`;
break;
case "mysql_select":
const [table, id] = task.input_data.split("|");
const query = `SELECT * FROM ${table} WHERE id = ${id}`;
result = `Query executed: ${query}`;
break;
case "drive_upload":
result = `File uploaded to Google Drive with content: ${task.input_data}`;
break;
default:
result = `Unknown action type: ${task.action_type}`;
status = "error";
}
} catch (err) {
result = `Error: ${err.message}`;
status = "error";
}
// Log the execution
await db.flow_log.create({
flow_id: flowId,
task_id: task.id,
result,
status,
});
results.push({ task_id: task.id, result, status });
}
res.json({ flow_id: flowId, results });
} catch (err) {
res.status(500).json({ error: "Failed to execute flow" });
}
});
// Webhook trigger
router.get("/flow/:id/trigger", async function (req, res) {
try {
const flowId = req.params.id;
const payload = req.query.payload;
if (!payload) {
return res.status(400).json({ error: "Payload required" });
}
// Execute the flow with the payload
const response = await fetch(
`${req.protocol}://${req.get("host")}/flow/${flowId}/execute`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload }),
}
);
const result = await response.json();
res.json(result);
} catch (err) {
res.status(500).json({ error: "Failed to trigger flow" });
}
});
module.exports = router; module.exports = router;
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chat Room</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto p-4 max-w-2xl">
<h1 class="text-3xl font-bold text-center mb-6">Chat Room</h1>
<div class="bg-white rounded-lg shadow-md p-6">
<div
id="chat-messages"
class="h-96 overflow-y-auto mb-4 border rounded p-4"
>
<ul id="message-list" class="space-y-2">
<!-- Messages will be populated here -->
</ul>
</div>
<div class="flex gap-2 mb-4">
<input
type="text"
id="message-input"
placeholder="Type your message..."
class="flex-1 px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
id="send-btn"
class="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600"
>
Send
</button>
</div>
<button
id="save-btn"
class="bg-green-500 text-white px-6 py-2 rounded hover:bg-green-600"
>
Save Chat
</button>
</div>
</div>
<script src="/chat.js"></script>
</body>
</html>
+127
View File
@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flow Builder</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto p-4 max-w-4xl">
<h1 class="text-3xl font-bold text-center mb-6">Flow Builder</h1>
<!-- Create Flow Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 class="text-xl font-semibold mb-4">Create New Flow</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input
type="text"
id="flow-name"
placeholder="Flow Name"
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
id="flow-description"
placeholder="Description"
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
id="create-flow-btn"
class="mt-4 bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600"
>
Create Flow
</button>
</div>
<!-- Add Task Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 class="text-xl font-semibold mb-4">Add Task to Flow</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<select
id="flow-select"
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select Flow</option>
</select>
<select
id="action-type"
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select Action</option>
<option value="send_test_mail">Send Test Mail</option>
<option value="http_get_request">HTTP GET Request</option>
<option value="mysql_select">MySQL Select</option>
<option value="drive_upload">Drive Upload</option>
</select>
<input
type="text"
id="task-input"
placeholder="Input Data"
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
id="order-index"
placeholder="Order"
min="0"
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
id="add-task-btn"
class="mt-4 bg-green-500 text-white px-6 py-2 rounded hover:bg-green-600"
>
Add Task
</button>
</div>
<!-- Flow Details Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 class="text-xl font-semibold mb-4">Flow Details</h2>
<div id="flow-details" class="space-y-4">
<!-- Flow details will be populated here -->
</div>
</div>
<!-- Execute Flow Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 class="text-xl font-semibold mb-4">Execute Flow</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<select
id="execute-flow-select"
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select Flow to Execute</option>
</select>
<input
type="text"
id="execute-payload"
placeholder="Payload (optional)"
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div class="mt-4 space-x-4">
<button
id="execute-btn"
class="bg-purple-500 text-white px-6 py-2 rounded hover:bg-purple-600"
>
Execute Flow
</button>
<button
id="webhook-btn"
class="bg-orange-500 text-white px-6 py-2 rounded hover:bg-orange-600"
>
Get Webhook URL
</button>
</div>
<div id="execution-results" class="mt-4 p-4 bg-gray-100 rounded hidden">
<!-- Execution results will be shown here -->
</div>
</div>
</div>
<script src="/flow.js"></script>
</body>
</html>
+12 -5
View File
@@ -137,14 +137,21 @@ https://cdn.jsdelivr.net/npm/tailwindcss@4.1.11/dist/lib.min.js
<div <div
class="bg-white rounded-lg shadow-md p-6 flex flex-col items-center justify-center mt-8" class="bg-white rounded-lg shadow-md p-6 flex flex-col items-center justify-center mt-8"
> >
<div <img
class="w-24 h-24 flex items-center justify-center bg-blue-100 rounded mb-2" id="upload-preview"
src=""
alt="Latest upload"
class="w-24 h-24 object-cover rounded mb-2"
style="display: none"
/>
<input type="file" id="upload-input" accept="image/*" class="mb-2" />
<button
id="upload-btn"
class="bg-sky-400 text-white font-semibold px-8 py-2 rounded"
> >
<span class="text-4xl">&#128247;</span>
</div>
<button class="bg-sky-400 text-white font-semibold px-8 py-2 rounded">
Upload Upload
</button> </button>
<span id="upload-error" class="text-xs text-red-500 mt-2"></span>
</div> </div>
</div> </div>
</div> </div>