← back to Designer Wallcoverings
security: remove hardcoded secrets from live files + untrack key-bearing backups
2b9cc4d080e0eb0c5fcfc59b30984c490d1184ec · 2026-09-17 17:08:00 -0700 · Steve
Env-first the remaining live leaks (Slack bot token in agent-slack/slack-agent.cjs;
Shopify Admin token in GrassclothWallcoverings/fetch-shopify-{collection,products}.js),
replace a real Anthropic key in DW-Agents/dw-agents/.env.example with a placeholder,
git rm --cached the 20 *.ts.backup files that carried live Anthropic/Shopify/Slack
tokens (files kept on disk), and gitignore *.backup-* / *.ts.backup*.
Working tree is now free of real secret literals. NOTE: the keys remain in git
HISTORY and MUST be rotated in their consoles (Anthropic, Shopify Admin, Slack) —
removing from the tree does not un-leak history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016oL3M6pMU1jANjBAqcpgtR
Files touched
M .gitignoreM DW-Agents/dw-agents/.env.exampleD DW-Agents/dw-agents/agent-digital-samples/digital-samples-agent.ts.backup-auth-1762615744D DW-Agents/dw-agents/agent-digital-samples/digital-samples-agent.ts.backup-oauth-20251112D DW-Agents/dw-agents/agent-legal-team/legal-agent.ts.backupD DW-Agents/dw-agents/agent-marketing/marketing-agent.ts.backup-1762750384D DW-Agents/dw-agents/agent-purchasing-office/purchasing-office-agent.ts.backup-auth-1762615744D DW-Agents/dw-agents/agent-security/security-agent.ts.backup-oauth-20251112M DW-Agents/dw-agents/agent-slack/slack-agent.cjsD DW-Agents/dw-agents/agent-zendesk-chat/zendesk-chat-agent.ts.backup-auth-1762615744D DW-Agents/dw-agents/agent-zendesk-chat/zendesk-chat-agent.ts.backup-oauth-20251112D DW-Agents/dw-agents/digital-samples-agent.ts.backupD DW-Agents/dw-agents/digital-samples-agent.ts.backup-1762549988D DW-Agents/dw-agents/digital-samples-agent.ts.backup-pre-themeD DW-Agents/dw-agents/needs-attention-agent.ts.backupD DW-Agents/dw-agents/purchasing-office-agent.ts.backupD DW-Agents/dw-agents/purchasing-office-agent.ts.backup-pre-themeD DW-Agents/dw-agents/task-orchestrator-agent.ts.backupD DW-Agents/dw-agents/todays-highlights-agent.ts.backupD DW-Agents/dw-agents/zendesk-chat-agent.ts.backupD DW-Agents/dw-agents/zendesk-chat-agent.ts.backup-1762549988D DW-Agents/dw-agents/zendesk-chat-agent.ts.backup-pre-themeD DW-Programming/ImportNewSkufromURL/dwjs-ai-retag.ts.backupM DW-Websites/GrassclothWallcoverings/fetch-shopify-collection.jsM DW-Websites/GrassclothWallcoverings/fetch-shopify-products.js
Diff
commit 2b9cc4d080e0eb0c5fcfc59b30984c490d1184ec
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 17 17:08:00 2026 -0700
security: remove hardcoded secrets from live files + untrack key-bearing backups
Env-first the remaining live leaks (Slack bot token in agent-slack/slack-agent.cjs;
Shopify Admin token in GrassclothWallcoverings/fetch-shopify-{collection,products}.js),
replace a real Anthropic key in DW-Agents/dw-agents/.env.example with a placeholder,
git rm --cached the 20 *.ts.backup files that carried live Anthropic/Shopify/Slack
tokens (files kept on disk), and gitignore *.backup-* / *.ts.backup*.
Working tree is now free of real secret literals. NOTE: the keys remain in git
HISTORY and MUST be rotated in their consoles (Anthropic, Shopify Admin, Slack) —
removing from the tree does not un-leak history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016oL3M6pMU1jANjBAqcpgtR
---
.gitignore | 2 +
DW-Agents/dw-agents/.env.example | 2 +-
...digital-samples-agent.ts.backup-auth-1762615744 | 1137 ----------
.../digital-samples-agent.ts.backup-oauth-20251112 | 1167 ----------
.../agent-legal-team/legal-agent.ts.backup | 2322 --------------------
.../marketing-agent.ts.backup-1762750384 | 1506 -------------
...rchasing-office-agent.ts.backup-auth-1762615744 | 1470 -------------
.../security-agent.ts.backup-oauth-20251112 | 871 --------
DW-Agents/dw-agents/agent-slack/slack-agent.cjs | 2 +-
.../zendesk-chat-agent.ts.backup-auth-1762615744 | 1459 ------------
.../zendesk-chat-agent.ts.backup-oauth-20251112 | 1489 -------------
.../dw-agents/digital-samples-agent.ts.backup | 1246 -----------
.../digital-samples-agent.ts.backup-1762549988 | 1164 ----------
.../digital-samples-agent.ts.backup-pre-theme | 1241 -----------
.../dw-agents/needs-attention-agent.ts.backup | 768 -------
.../dw-agents/purchasing-office-agent.ts.backup | 1544 -------------
.../purchasing-office-agent.ts.backup-pre-theme | 1539 -------------
.../dw-agents/task-orchestrator-agent.ts.backup | 1723 ---------------
.../dw-agents/todays-highlights-agent.ts.backup | 942 --------
DW-Agents/dw-agents/zendesk-chat-agent.ts.backup | 1744 ---------------
.../zendesk-chat-agent.ts.backup-1762549988 | 1459 ------------
.../zendesk-chat-agent.ts.backup-pre-theme | 1739 ---------------
.../ImportNewSkufromURL/dwjs-ai-retag.ts.backup | 698 ------
.../fetch-shopify-collection.js | 2 +-
.../fetch-shopify-products.js | 2 +-
25 files changed, 6 insertions(+), 27232 deletions(-)
diff --git a/.gitignore b/.gitignore
index c7953d2c..3389cd34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -117,3 +117,5 @@ onboarding/graduate-new-2026/shopify-upload/*.png
**/*-images-RESULT-*.json
shopify/scripts/out/
+*.backup-*
+*.ts.backup*
diff --git a/DW-Agents/dw-agents/.env.example b/DW-Agents/dw-agents/.env.example
index 6c30cc95..0bf0a51b 100644
--- a/DW-Agents/dw-agents/.env.example
+++ b/DW-Agents/dw-agents/.env.example
@@ -9,4 +9,4 @@ PAAPI_HOST=webservices.amazon.com
PAAPI_REGION=us-east-1
# Anthropic API Key (already configured)
-ANTHROPIC_API_KEY=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
+ANTHROPIC_API_KEY=your_anthropic_api_key_here
diff --git a/DW-Agents/dw-agents/agent-digital-samples/digital-samples-agent.ts.backup-auth-1762615744 b/DW-Agents/dw-agents/agent-digital-samples/digital-samples-agent.ts.backup-auth-1762615744
deleted file mode 100644
index 41fcb4fe..00000000
--- a/DW-Agents/dw-agents/agent-digital-samples/digital-samples-agent.ts.backup-auth-1762615744
+++ /dev/null
@@ -1,1137 +0,0 @@
-/**
- * DW-Agents: Digital Samples Agent
- *
- * Monitors Shopify orders for DIG-series products and automates sample delivery:
- * 1. Detects DIG-* SKU orders from Shopify webhooks
- * 2. Searches Google Drive for matching sample files
- * 3. Resizes images to 24" wide at 150 DPI
- * 4. Posts to Slack #new-products channel with approval workflow
- * 5. Sends files to customers via email
- *
- * Port: 9879
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import { WebClient } from '@slack/web-api';
-import sharp from 'sharp';
-import fetch from 'node-fetch';
-import { google } from 'googleapis';
-import dotenv from 'dotenv';
-
-// Load environment variables
-dotenv.config();
-
-const app = express();
-const PORT = 9879;
-
-// Authentication
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-// Shopify configuration
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || 'shpat_REDACTED';
-const API_VERSION = '2024-07';
-
-// Slack configuration
-const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || 'xoxb-3958182050256-9814366234406-HiR1ybK6HT8JViWDEpjXpmsq';
-const SLACK_CHANNEL = 'new-products-on-shopify';
-const slackClient = new WebClient(SLACK_BOT_TOKEN);
-
-// Anthropic Claude
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY,
-});
-
-// Google Drive API setup
-const oauth2Client = new google.auth.OAuth2(
- process.env.GOOGLE_CLIENT_ID,
- process.env.GOOGLE_CLIENT_SECRET,
- 'http://localhost'
-);
-
-oauth2Client.setCredentials({
- refresh_token: process.env.GOOGLE_REFRESH_TOKEN
-});
-
-const drive = google.drive({ version: 'v3', auth: oauth2Client });
-
-// Statistics tracking
-let stats = {
- totalOrders: 0,
- digOrdersFound: 0,
- filesProcessed: 0,
- slackPostsSent: 0,
- emailsSent: 0,
- lastOrder: null as {
- orderId: string;
- customerName: string;
- digItems: string[];
- timestamp: Date;
- } | null,
- pendingApprovals: [] as Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- resizedImageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- }>
-};
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- type: 'order' | 'processing' | 'slack' | 'email' | 'error';
- message: string;
-}> = [];
-
-function addLog(message: string, type: 'order' | 'processing' | 'slack' | 'email' | 'error' = 'processing') {
- const entry = {
- timestamp: new Date(),
- type,
- message
- };
- activityLog.unshift(entry);
-
- // Keep only last 100 entries
- if (activityLog.length > 100) {
- activityLog.pop();
- }
-
- console.log(`[${type.toUpperCase()}] ${message}`);
-}
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-digital-samples-agent-2025',
- resave: false,
- saveUninitialized: false,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 24 * 60 * 60 * 1000,
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- }
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
-};
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Digital Samples Agent</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #4facfe; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>📦 Digital Samples Agent</h1>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
-
- // Set SSO cookie for cross-agent authentication
- setSSOToken(res);
-
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard
-app.get('/', requireAuth, (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Digital Samples Agent - DW-Agents</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #4facfe; font-size: 2.5em; margin-bottom: 10px; }
- .header .subtitle { color: #666; font-size: 1.2em; }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 20px;
- margin-bottom: 20px;
- }
- .stat-card {
- background: white;
- padding: 25px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stat-card h3 { color: #888; font-size: 0.9em; text-transform: uppercase; margin-bottom: 10px; }
- .stat-card .value { font-size: 2.5em; font-weight: bold; color: #4facfe; }
- .approvals-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .approvals-section h2 { color: #4facfe; margin-bottom: 20px; }
- .approval-card {
- background: #f8f9fa;
- padding: 20px;
- border-radius: 10px;
- margin-bottom: 15px;
- border-left: 4px solid #4facfe;
- }
- .approval-card img { max-width: 200px; border-radius: 8px; margin: 10px 0; }
- .btn {
- padding: 10px 20px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-right: 10px;
- }
- .btn-approve { background: #4CAF50; color: white; }
- .btn-reject { background: #f44336; color: white; }
- .log-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .log-entry {
- padding: 10px;
- margin: 5px 0;
- border-radius: 6px;
- font-family: 'Courier New', monospace;
- font-size: 0.9em;
- }
- .log-entry.order { background: #e3f2fd; border-left: 4px solid #2196F3; }
- .log-entry.processing { background: #fff3e0; border-left: 4px solid #FF9800; }
- .log-entry.slack { background: #f3e5f5; border-left: 4px solid #9C27B0; }
- .log-entry.email { background: #e8f5e9; border-left: 4px solid #4CAF50; }
- .log-entry.error { background: #ffebee; border-left: 4px solid #f44336; }
- .drive-file-card {
- background: #f8f9fa;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 4px solid #4facfe;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- gap: 15px;
- align-items: center;
- }
- .drive-file-card:hover {
- background: #e9ecef;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .drive-file-card.selected {
- background: #e8f5e9;
- border-left-color: #4CAF50;
- }
- .drive-file-thumbnail {
- width: 60px;
- height: 60px;
- border-radius: 6px;
- object-fit: cover;
- background: #e0e0e0;
- flex-shrink: 0;
- }
- .drive-file-info {
- flex: 1;
- }
- .drive-file-name {
- font-weight: 600;
- color: #333;
- margin-bottom: 5px;
- }
- .drive-file-meta {
- color: #888;
- font-size: 0.85em;
- }
- </style>
-</head>
-<body>
- <div class="header">
- <h1>📦 Digital Samples Agent</h1>
- <div class="subtitle">Automated DIG-series order processing & delivery</div>
- <div style="text-align: right; margin-top: 10px;">
- <a href="/logout" style="color: #4facfe; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-grid">
- <div class="stat-card">
- <h3>Total Orders</h3>
- <div class="value" id="totalOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>DIG Orders</h3>
- <div class="value" id="digOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>Files Processed</h3>
- <div class="value" id="filesProcessed">0</div>
- </div>
- <div class="stat-card">
- <h3>Pending Approvals</h3>
- <div class="value" id="pendingApprovals">0</div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>💬 Slack #graphics-needed Channel</h2>
- <p style="color: #666; margin-bottom: 15px;">Recent messages and requests from the graphics team</p>
- <div id="slackMessages">
- <p style="color: #888;">Loading Slack messages...</p>
- </div>
- <div style="margin-top: 15px;">
- <input type="text" id="slackMessageInput" placeholder="Type a message to #graphics-needed..."
- style="width: calc(100% - 120px); padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px; margin-right: 10px;">
- <button onclick="sendSlackMessage()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 📤 Send
- </button>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📁 Google Drive - Digital Samples Library</h2>
- <p style="color: #666; margin-bottom: 15px;">Browse and attach sample files from Google Drive</p>
-
- <div style="display: flex; gap: 10px; margin-bottom: 15px;">
- <input type="text" id="driveSearchInput" placeholder="Search files by name or SKU..."
- style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <select id="driveFilterType" style="padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="all">All Files</option>
- <option value="images">Images Only</option>
- <option value="pdfs">PDFs Only</option>
- </select>
- <button onclick="refreshDriveFiles()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh
- </button>
- </div>
-
- <div id="driveFilesList" style="max-height: 500px; overflow-y: auto;">
- <p style="color: #888;">Loading Google Drive files...</p>
- </div>
-
- <div id="selectedFilesPanel" style="display: none; margin-top: 20px; padding: 20px; background: #e8f5e9; border-radius: 8px; border-left: 4px solid #4CAF50;">
- <h3 style="margin: 0 0 15px 0; color: #2E7D32;">📎 Selected Files (<span id="selectedCount">0</span>)</h3>
- <div id="selectedFilesList"></div>
- <div style="margin-top: 15px; display: flex; gap: 10px;">
- <select id="attachToOrder" style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="">Select an order to attach files...</option>
- </select>
- <button onclick="attachFilesToOrder()" style="background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ✅ Attach to Order
- </button>
- <button onclick="clearSelectedFiles()" style="background: #f44336; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ❌ Clear
- </button>
- </div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📋 Pending Approvals</h2>
- <div id="approvalsList">
- <p style="color: #888;">No pending approvals</p>
- </div>
- </div>
-
- <div class="approvals-section">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
- <h2 style="margin: 0;">📦 All Samples by Date</h2>
- <div style="display: flex; gap: 10px; align-items: center;">
- <span id="lastRefresh" style="color: #888; font-size: 0.9em;">Last updated: --:--</span>
- <button onclick="manualRefresh()" class="btn" style="background: #4facfe; color: white; padding: 8px 16px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh Now
- </button>
- </div>
- </div>
- <div style="margin-bottom: 15px;">
- <input
- type="text"
- id="searchSamples"
- placeholder="Search by customer, SKU, or order ID..."
- style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;"
- >
- </div>
- <div id="allSamplesList">
- <p style="color: #888;">Loading samples history...</p>
- </div>
- </div>
-
- <div class="log-section">
- <h2>📝 Activity Log</h2>
- <div id="activityLog"></div>
- </div>
-
- <script>
- // Clean SKU - extract only DIG-number (e.g., "DIG-25-some-extra" becomes "DIG-25")
- function cleanSku(sku) {
- if (!sku) return '';
- // Match DIG- followed by digits, ignore everything after
- const match = sku.match(/DIG-\d+/i);
- return match ? match[0] : sku;
- }
-
- async function updateDashboard() {
- try {
- const statsData = await fetch('/api/stats').then(r => r.json());
-
- document.getElementById('totalOrders').textContent = statsData.totalOrders;
- document.getElementById('digOrders').textContent = statsData.digOrdersFound;
- document.getElementById('filesProcessed').textContent = statsData.filesProcessed;
- document.getElementById('pendingApprovals').textContent = statsData.pendingApprovals.length;
-
- // Update approvals list
- const approvalsList = document.getElementById('approvalsList');
- if (statsData.pendingApprovals.length === 0) {
- approvalsList.innerHTML = '<p style="color: #888;">No pending approvals</p>';
- } else {
- approvalsList.innerHTML = statsData.pendingApprovals.map(approval => \`
- <div class="approval-card">
- <h3>\${approval.productTitle} (SKU: \${cleanSku(approval.digSku)})</h3>
- <p><strong>Customer:</strong> \${approval.customerName} (\${approval.customerEmail})</p>
- <p><strong>Order ID:</strong> \${approval.orderId}</p>
- <p><strong>Time:</strong> \${new Date(approval.timestamp).toLocaleString()}</p>
- <img src="\${approval.resizedImageUrl || approval.imageUrl}" alt="Product">
- <div>
- <button class="btn btn-approve" onclick="approveOrder('\${approval.id}')">✅ Approve & Send</button>
- <button class="btn btn-reject" onclick="rejectOrder('\${approval.id}')">❌ Reject</button>
- </div>
- </div>
- \`).join('');
- }
-
- // Update Slack messages
- const slackData = await fetch('/api/slack/messages').then(r => r.json());
- const slackDiv = document.getElementById('slackMessages');
- if (slackData.messages && slackData.messages.length > 0) {
- slackDiv.innerHTML = slackData.messages.map(msg => \`
- <div style="background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid #4facfe;">
- <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
- <strong style="color: #333;">\${msg.user}</strong>
- <span style="color: #888; font-size: 0.85em;">\${new Date(msg.timestamp * 1000).toLocaleString()}</span>
- </div>
- <div style="color: #555;">\${msg.text}</div>
- </div>
- \`).join('');
- } else {
- slackDiv.innerHTML = '<p style="color: #888;">No recent messages in #graphics-needed</p>';
- }
-
- // Update all samples list
- const samplesData = await fetch('/api/samples/all').then(r => r.json());
- window.allSamplesData = samplesData.samples || [];
- renderSamples(window.allSamplesData, samplesData);
-
- // Update activity log
- const logs = await fetch('/api/activity').then(r => r.json());
- const logDiv = document.getElementById('activityLog');
- logDiv.innerHTML = logs.map(log => \`
- <div class="log-entry \${log.type}">
- [\${new Date(log.timestamp).toLocaleTimeString()}] \${log.message}
- </div>
- \`).join('');
-
- updateLastRefreshTime();
- } catch (error) {
- console.error('Update error:', error);
- }
- }
-
- // Render samples function
- function renderSamples(samples, data) {
- const samplesList = document.getElementById('allSamplesList');
- if (samples && samples.length > 0) {
- const totalCount = data.total || samples.length;
- const last90Count = data.last90Days || samples.filter(s => {
- const daysAgo = (Date.now() - new Date(s.timestamp).getTime()) / (1000 * 60 * 60 * 24);
- return daysAgo <= 90;
- }).length;
-
- samplesList.innerHTML = \`
- <div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #2196F3;">
- <h3 style="margin: 0 0 10px 0; color: #1976D2;">📊 Summary</h3>
- <p style="margin: 5px 0;"><strong>DIG Samples (Last 90 Days):</strong> \${last90Count}</p>
- <p style="margin: 5px 0;"><strong>Total Displayed:</strong> \${samples.length}</p>
- </div>
- \` + samples.map(sample => {
- const statusColor = sample.status === 'approved' ? '#4CAF50' :
- sample.status === 'pending' ? '#FFA500' :
- sample.status === 'ordered' ? '#2196F3' : '#f44336';
- const statusText = sample.status === 'ordered' ? 'ORDERED' : sample.status.toUpperCase();
- const orderLink = sample.orderUrl ?
- \`<a href="\${sample.orderUrl}" target="_blank" style="color: #4facfe; text-decoration: none; font-weight: 600;">🔗 View Order in Shopify →</a>\` :
- '';
-
- return \`
- <div class="approval-card" style="opacity: \${sample.status === 'approved' || sample.status === 'ordered' ? '0.85' : '1'}; position: relative;">
- <h3>\${sample.productTitle}</h3>
- <p><strong>SKU:</strong> <span style="background: #f0f0f0; padding: 3px 8px; border-radius: 4px; font-family: monospace;">\${cleanSku(sample.digSku)}</span></p>
- <p><strong>Customer:</strong> \${sample.customerName} (\${sample.customerEmail})</p>
- <p><strong>Order:</strong> \${sample.orderId || sample.orderNumber || 'N/A'}</p>
- <p><strong>Status:</strong> <span style="color: \${statusColor}; font-weight: bold; background: \${statusColor}22; padding: 3px 10px; border-radius: 4px;">\${statusText}</span></p>
- <p><strong>Date:</strong> \${new Date(sample.timestamp).toLocaleString()}</p>
- \${orderLink ? \`<p style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;">\${orderLink}</p>\` : ''}
- </div>
- \`;
- }).join('');
- } else {
- samplesList.innerHTML = '<p style="color: #888;">No samples found</p>';
- }
- }
-
- // Search functionality
- window.allSamplesData = [];
- document.getElementById('searchSamples').addEventListener('input', function(e) {
- const searchTerm = e.target.value.toLowerCase();
-
- if (!searchTerm) {
- // Show all samples when search is empty
- fetch('/api/samples/all')
- .then(r => r.json())
- .then(data => {
- window.allSamplesData = data.samples || [];
- renderSamples(window.allSamplesData, data);
- });
- return;
- }
-
- const filtered = window.allSamplesData.filter(sample =>
- (sample.customerName && sample.customerName.toLowerCase().includes(searchTerm)) ||
- (sample.customerEmail && sample.customerEmail.toLowerCase().includes(searchTerm)) ||
- (sample.digSku && sample.digSku.toLowerCase().includes(searchTerm)) ||
- (sample.orderId && sample.orderId.toLowerCase().includes(searchTerm)) ||
- (sample.orderNumber && sample.orderNumber.toLowerCase().includes(searchTerm)) ||
- (sample.productTitle && sample.productTitle.toLowerCase().includes(searchTerm))
- );
-
- renderSamples(filtered, { total: filtered.length, last90Days: filtered.length });
- });
-
- function approveOrder(id) {
- fetch(\`/api/approve/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- function rejectOrder(id) {
- fetch(\`/api/reject/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- async function sendSlackMessage() {
- const input = document.getElementById('slackMessageInput');
- const message = input.value.trim();
-
- if (!message) {
- alert('Please enter a message');
- return;
- }
-
- try {
- const response = await fetch('/api/slack/send', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
- if (data.success) {
- input.value = '';
- updateDashboard(); // Refresh to show new message
- } else {
- alert('Failed to send message: ' + data.error);
- }
- } catch (error) {
- alert('Error sending message');
- }
- }
-
- function manualRefresh() {
- const button = event.target;
- button.disabled = true;
- button.textContent = '⏳ Refreshing...';
-
- updateDashboard().then(() => {
- button.disabled = false;
- button.textContent = '🔄 Refresh Now';
- });
- }
-
- function updateLastRefreshTime() {
- const now = new Date();
- const timeStr = now.toLocaleTimeString();
- document.getElementById('lastRefresh').textContent = \`Last updated: \${timeStr}\`;
- }
-
- // Auto-refresh every 30 seconds
- setInterval(() => {
- updateDashboard();
- updateLastRefreshTime();
- }, 30000);
-
- // Initial load
- updateDashboard();
- updateLastRefreshTime();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.get('/api/activity', requireAuth, (req, res) => {
- res.json(activityLog.slice(0, 50));
-});
-
-// Get Slack channel ID
-async function getGraphicsNeededChannelId() {
- try {
- const result = await slackClient.conversations.list({
- types: 'public_channel,private_channel',
- limit: 200
- });
-
- const channel = result.channels?.find((c: any) => c.name === 'graphics-needed');
- if (channel) {
- return channel.id;
- }
-
- console.error('Channel #graphics-needed not found in:', result.channels?.map((c: any) => c.name));
- return null;
- } catch (error) {
- console.error('Error getting channel list:', error);
- return null;
- }
-}
-
-// Slack API endpoints
-app.get('/api/slack/messages', requireAuth, async (req, res) => {
- try {
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ messages: [], error: 'Channel #graphics-needed not found' });
- }
-
- const result = await slackClient.conversations.history({
- channel: channelId,
- limit: 20
- });
-
- const messages = result.messages?.map((msg: any) => ({
- user: msg.user || 'Unknown',
- text: msg.text,
- timestamp: msg.ts
- })) || [];
-
- res.json({ messages });
- } catch (error) {
- console.error('Slack fetch error:', error);
- res.json({ messages: [], error: String(error) });
- }
-});
-
-app.post('/api/slack/send', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ success: false, error: 'Channel #graphics-needed not found' });
- }
-
- // Try to join the channel first if we get not_in_channel error
- try {
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (postError: any) {
- // If not in channel, try to join it
- if (postError.data?.error === 'not_in_channel') {
- console.log('🔧 Bot not in channel, attempting to join #graphics-needed...');
- try {
- await slackClient.conversations.join({ channel: channelId });
- console.log('✅ Successfully joined #graphics-needed');
-
- // Retry posting message
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (joinError) {
- console.error('❌ Failed to join channel:', joinError);
- res.json({ success: false, error: 'Bot needs to be invited to #graphics-needed channel by a workspace admin' });
- }
- } else {
- throw postError;
- }
- }
- } catch (error) {
- console.error('Slack send error:', error);
- res.json({ success: false, error: String(error) });
- }
-});
-
-// Keep track of all samples history (approved + pending)
-const samplesHistory: Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- processedAt?: Date;
-}> = [];
-
-// Get all samples (pending + approved history + last 90 days from Shopify)
-app.get('/api/samples/all', requireAuth, async (req, res) => {
- try {
- // Combine current pending with history
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected',
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders`
- }));
-
- // Fetch orders from last 90 days from Shopify
- const ninetyDaysAgo = new Date();
- ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
-
- const shopifyUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/orders.json?status=any&created_at_min=${ninetyDaysAgo.toISOString()}&limit=250`;
-
- const response = await fetch(shopifyUrl, {
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- },
- });
-
- const shopifyData: any = await response.json();
- const orders = shopifyData.orders || [];
-
- addLog(`Fetched ${orders.length} orders from Shopify (last 90 days)`, 'processing');
-
- // Extract DIG samples from Shopify orders
- const shopifySamples: Array<any> = [];
-
- for (const order of orders) {
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || 'No email';
-
- for (const item of digItems) {
- shopifySamples.push({
- id: `shopify-${order.id}-${item.id}`,
- orderId: order.name || `#${order.order_number}`,
- orderNumber: order.order_number.toString(),
- shopifyOrderId: order.id.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- status: 'ordered',
- timestamp: new Date(order.created_at),
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders/${order.id}`,
- financialStatus: order.financial_status,
- fulfillmentStatus: order.fulfillment_status || 'unfulfilled'
- });
- }
- }
- }
-
- addLog(`Found ${shopifySamples.length} DIG samples in recent orders`, 'processing');
-
- const allSamples = [...samplesHistory, ...currentPending, ...shopifySamples].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length, last90Days: shopifySamples.length });
- } catch (error) {
- console.error('Error fetching samples:', error);
- addLog(`Error fetching Shopify orders: ${error}`, 'error');
-
- // Fallback to local history only
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected'
- }));
-
- const allSamples = [...samplesHistory, ...currentPending].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length });
- }
-});
-
-app.post('/api/approve/:id', requireAuth, async (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (!approval) {
- return res.status(404).json({ error: 'Approval not found' });
- }
-
- approval.status = 'approved';
-
- try {
- // Send to Slack #new-products channel
- await slackClient.chat.postMessage({
- channel: SLACK_CHANNEL,
- text: `🆕 *DIG Sample Approved & Sent*\n\n📦 *${approval.productTitle}*\n• SKU: \`${approval.digSku}\`\n• Customer: ${approval.customerName}\n• Order: ${approval.orderId}\n\n✅ Sample sent to customer!`,
- unfurl_links: false,
- unfurl_media: false
- });
-
- stats.slackPostsSent++;
- stats.emailsSent++;
- addLog(`Approved and sent DIG-${approval.digSku} to ${approval.customerName}`, 'email');
-
- // Add to history before removing from pending
- samplesHistory.push({
- ...approval,
- status: 'approved',
- processedAt: new Date()
- });
-
- // Remove from pending
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
-
- res.json({ success: true, message: 'Sample approved and sent to customer!' });
- } catch (error) {
- addLog(`Error sending approval: ${error}`, 'error');
- res.status(500).json({ error: 'Failed to send sample' });
- }
-});
-
-app.post('/api/reject/:id', requireAuth, (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (approval) {
- // Add to history before removing
- samplesHistory.push({
- ...approval,
- status: 'rejected',
- processedAt: new Date()
- });
- }
-
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
- addLog(`Rejected DIG sample order ${req.params.id}`, 'processing');
- res.json({ success: true, message: 'Order rejected' });
-});
-
-// Google Drive API endpoints
-app.get('/api/drive/search', requireAuth, async (req, res) => {
- try {
- const query = (req.query.q as string) || '';
- const files = await searchDriveFiles(query);
-
- res.json({
- success: true,
- files: files.map((f: any) => ({
- id: f.id,
- name: f.name,
- mimeType: f.mimeType,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink,
- size: f.size,
- modified: f.modifiedTime
- })),
- total: files.length
- });
- } catch (error) {
- res.status(500).json({ error: `Drive search failed: ${error}` });
- }
-});
-
-app.get('/api/drive/file/:id', requireAuth, async (req, res) => {
- try {
- const fileInfo = await getDriveFileLink(req.params.id);
- res.json({ success: true, file: fileInfo });
- } catch (error) {
- res.status(500).json({ error: `Failed to get file: ${error}` });
- }
-});
-
-// Shopify webhook endpoint (receives new orders)
-app.post('/webhook/shopify/orders/create', async (req, res) => {
- try {
- const order = req.body;
- stats.totalOrders++;
-
- addLog(`New order received: #${order.order_number}`, 'order');
-
- // Check for DIG-series items
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- stats.digOrdersFound++;
-
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || '';
-
- stats.lastOrder = {
- orderId: order.name || order.order_number.toString(),
- customerName,
- digItems: digItems.map((item: any) => item.sku),
- timestamp: new Date()
- };
-
- addLog(`Found ${digItems.length} DIG items in order #${order.order_number}`, 'processing');
-
- // Process each DIG item
- for (const item of digItems) {
- await processDIGItem(order, item, customerEmail, customerName);
- }
- }
-
- res.json({ success: true });
- } catch (error) {
- addLog(`Webhook error: ${error}`, 'error');
- res.status(500).json({ error: 'Webhook processing failed' });
- }
-});
-
-// Google Drive search function
-async function searchDriveFiles(query: string = '') {
- try {
- const response = await drive.files.list({
- q: query ? `name contains '${query}' and mimeType contains 'image/'` : "mimeType contains 'image/'",
- fields: 'files(id, name, mimeType, webViewLink, thumbnailLink, size, createdTime, modifiedTime)',
- orderBy: 'modifiedTime desc',
- pageSize: 50
- });
-
- return response.data.files || [];
- } catch (error) {
- addLog(`Error searching Drive: ${error}`, 'error');
- return [];
- }
-}
-
-// Get Drive file download link
-async function getDriveFileLink(fileId: string) {
- try {
- const response = await drive.files.get({
- fileId,
- fields: 'webContentLink, webViewLink'
- });
- return response.data;
- } catch (error) {
- addLog(`Error getting Drive file link: ${error}`, 'error');
- return null;
- }
-}
-
-async function processDIGItem(order: any, item: any, customerEmail: string, customerName: string) {
- try {
- stats.filesProcessed++;
-
- // Search Google Drive for matching files
- const sku = item.sku.replace('DIG-', '');
- const driveFiles = await searchDriveFiles(sku);
-
- addLog(`Found ${driveFiles.length} Drive files matching SKU: ${sku}`, 'processing');
-
- // Create approval entry
- const approval = {
- id: `${order.id}-${item.id}-${Date.now()}`,
- orderId: order.name || order.order_number.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- resizedImageUrl: '', // Will be populated after image processing
- driveFiles: driveFiles.map((f: any) => ({
- id: f.id,
- name: f.name,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink
- })),
- status: 'pending' as const,
- timestamp: new Date()
- };
-
- stats.pendingApprovals.push(approval);
-
- addLog(`Created approval for ${item.sku} - ${customerName} with ${driveFiles.length} Drive files`, 'processing');
-
- } catch (error) {
- addLog(`Error processing DIG item: ${error}`, 'error');
- }
-}
-
-// Manual test endpoint
-app.post('/api/test/dig-order', requireAuth, async (req, res) => {
- const mockOrder = {
- id: Date.now(),
- order_number: 12345,
- name: '#TEST-' + Date.now(),
- customer: {
- first_name: 'Test',
- last_name: 'Customer',
- email: 'test@example.com'
- },
- line_items: [
- {
- id: Date.now(),
- sku: 'DIG-12345',
- title: 'Digital Sample - Test Pattern',
- properties: [
- { name: 'Image', value: 'https://via.placeholder.com/800x600' }
- ]
- }
- ]
- };
-
- await processDIGItem(mockOrder, mockOrder.line_items[0], 'test@example.com', 'Test Customer');
-
- res.json({ success: true, message: 'Test DIG order created!' });
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('📦 Digital Samples Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('✅ Monitoring Shopify for DIG-series orders...');
- console.log('📁 Google Drive integration: Ready');
- console.log('🖼️ Image resizing: 24" @ 150 DPI');
- console.log('📢 Slack channel: #new-products');
- console.log('');
- addLog('Digital Samples Agent initialized and running', 'processing');
-});
diff --git a/DW-Agents/dw-agents/agent-digital-samples/digital-samples-agent.ts.backup-oauth-20251112 b/DW-Agents/dw-agents/agent-digital-samples/digital-samples-agent.ts.backup-oauth-20251112
deleted file mode 100644
index 5ed81abd..00000000
--- a/DW-Agents/dw-agents/agent-digital-samples/digital-samples-agent.ts.backup-oauth-20251112
+++ /dev/null
@@ -1,1167 +0,0 @@
-import cookieParser from "cookie-parser";
-/**
- * DW-Agents: Digital Samples Agent
- *
- * Monitors Shopify orders for DIG-series products and automates sample delivery:
- * 1. Detects DIG-* SKU orders from Shopify webhooks
- * 2. Searches Google Drive for matching sample files
- * 3. Resizes images to 24" wide at 150 DPI
- * 4. Posts to Slack #new-products channel with approval workflow
- * 5. Sends files to customers via email
- *
- * Port: 9879
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-// import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import { WebClient } from '@slack/web-api';
-import sharp from 'sharp';
-import fetch from 'node-fetch';
-import { google } from 'googleapis';
-import dotenv from 'dotenv';
-
-import { chatMiddleware } from '../shared-chat-integration';
-
-// Load environment variables
-dotenv.config();
-
-const app = express();
-
-// Global Authentication
-const PORT = 9879;
-
-// Global Authentication - MUST come before any other middleware
-app.use(cookieParser());
-
-// Add inter-agent chat widget (after auth)
-app.use(chatMiddleware({
- agentId: 'agent-digital-samples',
- agentName: 'Digital Samples',
- port: 9879,
- category: 'agent'
-}));
-
-
-// Authentication
-const AUTH_USERNAME = 'admin2025';
-const AUTH_PASSWORD = 'Otis';
-
-// Shopify configuration
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || 'shpat_REDACTED';
-const API_VERSION = '2024-07';
-
-// Slack configuration
-const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || 'xoxb-3958182050256-9814366234406-HiR1ybK6HT8JViWDEpjXpmsq';
-const SLACK_CHANNEL = 'new-products-on-shopify';
-const slackClient = new WebClient(SLACK_BOT_TOKEN);
-
-// Anthropic Claude
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY,
-});
-
-// Google Drive API setup
-const oauth2Client = new google.auth.OAuth2(
- process.env.GOOGLE_CLIENT_ID,
- process.env.GOOGLE_CLIENT_SECRET,
- 'http://localhost'
-);
-
-oauth2Client.setCredentials({
- refresh_token: process.env.GOOGLE_REFRESH_TOKEN
-});
-
-const drive = google.drive({ version: 'v3', auth: oauth2Client });
-
-// Statistics tracking
-let stats = {
- totalOrders: 0,
- digOrdersFound: 0,
- filesProcessed: 0,
- slackPostsSent: 0,
- emailsSent: 0,
- lastOrder: null as {
- orderId: string;
- customerName: string;
- digItems: string[];
- timestamp: Date;
- } | null,
- pendingApprovals: [] as Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- resizedImageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- }>
-};
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- type: 'order' | 'processing' | 'slack' | 'email' | 'error';
- message: string;
-}> = [];
-
-function addLog(message: string, type: 'order' | 'processing' | 'slack' | 'email' | 'error' = 'processing') {
- const entry = {
- timestamp: new Date(),
- type,
- message
- };
- activityLog.unshift(entry);
-
- // Keep only last 100 entries
- if (activityLog.length > 100) {
- activityLog.pop();
- }
-
- console.log(`[${type.toUpperCase()}] ${message}`);
-}
-
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-digital-samples-agent-2025',
- resave: false,
- saveUninitialized: false,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 24 * 60 * 60 * 1000,
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- }
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
-};
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Digital Samples Agent</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #4facfe; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>📦 Digital Samples Agent</h1>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
-
- // Set SSO cookie for cross-agent authentication
- setSSOToken(res);
-
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard
-app.get('/', requireAuth, (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Digital Samples Agent - DW-Agents</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #4facfe; font-size: 2.5em; margin-bottom: 10px; }
- .header .subtitle { color: #666; font-size: 1.2em; }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 20px;
- margin-bottom: 20px;
- }
- .stat-card {
- background: white;
- padding: 25px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stat-card h3 { color: #888; font-size: 0.9em; text-transform: uppercase; margin-bottom: 10px; }
- .stat-card .value { font-size: 2.5em; font-weight: bold; color: #4facfe; }
- .approvals-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .approvals-section h2 { color: #4facfe; margin-bottom: 20px; }
- .approval-card {
- background: #f8f9fa;
- padding: 20px;
- border-radius: 10px;
- margin-bottom: 15px;
- border-left: 4px solid #4facfe;
- }
- .approval-card img { max-width: 200px; border-radius: 8px; margin: 10px 0; }
- .btn {
- padding: 10px 20px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-right: 10px;
- }
- .btn-approve { background: #4CAF50; color: white; }
- .btn-reject { background: #f44336; color: white; }
- .log-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .log-entry {
- padding: 10px;
- margin: 5px 0;
- border-radius: 6px;
- font-family: 'Courier New', monospace;
- font-size: 0.9em;
- }
- .log-entry.order { background: #e3f2fd; border-left: 4px solid #2196F3; }
- .log-entry.processing { background: #fff3e0; border-left: 4px solid #FF9800; }
- .log-entry.slack { background: #f3e5f5; border-left: 4px solid #9C27B0; }
- .log-entry.email { background: #e8f5e9; border-left: 4px solid #4CAF50; }
- .log-entry.error { background: #ffebee; border-left: 4px solid #f44336; }
- .drive-file-card {
- background: #f8f9fa;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 4px solid #4facfe;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- gap: 15px;
- align-items: center;
- }
- .drive-file-card:hover {
- background: #e9ecef;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .drive-file-card.selected {
- background: #e8f5e9;
- border-left-color: #4CAF50;
- }
- .drive-file-thumbnail {
- width: 60px;
- height: 60px;
- border-radius: 6px;
- object-fit: cover;
- background: #e0e0e0;
- flex-shrink: 0;
- }
- .drive-file-info {
- flex: 1;
- }
- .drive-file-name {
- font-weight: 600;
- color: #333;
- margin-bottom: 5px;
- }
- .drive-file-meta {
- color: #888;
- font-size: 0.85em;
- }
- </style>
-</head>
-<body>
- <div class="header">
- <h1>📦 Digital Samples Agent</h1>
- <div class="subtitle">Automated DIG-series order processing & delivery</div>
- <div style="text-align: right; margin-top: 10px;">
- <a href="/logout" style="color: #4facfe; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-grid">
- <div class="stat-card">
- <h3>Total Orders</h3>
- <div class="value" id="totalOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>DIG Orders</h3>
- <div class="value" id="digOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>Files Processed</h3>
- <div class="value" id="filesProcessed">0</div>
- </div>
- <div class="stat-card">
- <h3>Pending Approvals</h3>
- <div class="value" id="pendingApprovals">0</div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>💬 Slack #graphics-needed Channel</h2>
- <p style="color: #666; margin-bottom: 15px;">Recent messages and requests from the graphics team</p>
- <div id="slackMessages">
- <p style="color: #888;">Loading Slack messages...</p>
- </div>
- <div style="margin-top: 15px;">
- <input type="text" id="slackMessageInput" placeholder="Type a message to #graphics-needed..."
- style="width: calc(100% - 120px); padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px; margin-right: 10px;">
- <button onclick="sendSlackMessage()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 📤 Send
- </button>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📁 Google Drive - Digital Samples Library</h2>
- <p style="color: #666; margin-bottom: 15px;">Browse and attach sample files from Google Drive</p>
-
- <div style="display: flex; gap: 10px; margin-bottom: 15px;">
- <input type="text" id="driveSearchInput" placeholder="Search files by name or SKU..."
- style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <select id="driveFilterType" style="padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="all">All Files</option>
- <option value="images">Images Only</option>
- <option value="pdfs">PDFs Only</option>
- </select>
- <button onclick="refreshDriveFiles()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh
- </button>
- </div>
-
- <div id="driveFilesList" style="max-height: 500px; overflow-y: auto;">
- <p style="color: #888;">Loading Google Drive files...</p>
- </div>
-
- <div id="selectedFilesPanel" style="display: none; margin-top: 20px; padding: 20px; background: #e8f5e9; border-radius: 8px; border-left: 4px solid #4CAF50;">
- <h3 style="margin: 0 0 15px 0; color: #2E7D32;">📎 Selected Files (<span id="selectedCount">0</span>)</h3>
- <div id="selectedFilesList"></div>
- <div style="margin-top: 15px; display: flex; gap: 10px;">
- <select id="attachToOrder" style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="">Select an order to attach files...</option>
- </select>
- <button onclick="attachFilesToOrder()" style="background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ✅ Attach to Order
- </button>
- <button onclick="clearSelectedFiles()" style="background: #f44336; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ❌ Clear
- </button>
- </div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📋 Pending Approvals</h2>
- <div id="approvalsList">
- <p style="color: #888;">No pending approvals</p>
- </div>
- </div>
-
- <div class="approvals-section">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
- <h2 style="margin: 0;">📦 All Samples by Date</h2>
- <div style="display: flex; gap: 10px; align-items: center;">
- <span id="lastRefresh" style="color: #888; font-size: 0.9em;">Last updated: --:--</span>
- <button onclick="manualRefresh()" class="btn" style="background: #4facfe; color: white; padding: 8px 16px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh Now
- </button>
- </div>
- </div>
- <div style="margin-bottom: 15px;">
- <input
- type="text"
- id="searchSamples"
- placeholder="Search by customer, SKU, or order ID..."
- style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;"
- >
- </div>
- <div id="allSamplesList">
- <p style="color: #888;">Loading samples history...</p>
- </div>
- </div>
-
- <div class="log-section">
- <h2>📝 Activity Log</h2>
- <div id="activityLog"></div>
- </div>
-
- <script>
- // Clean SKU - extract only DIG-number (e.g., "DIG-25-some-extra" becomes "DIG-25")
- function cleanSku(sku) {
- if (!sku) return '';
- // Match DIG- followed by digits, ignore everything after
- const match = sku.match(/DIG-\d+/i);
- return match ? match[0] : sku;
- }
-
- async function updateDashboard() {
- try {
- const statsData = await fetch('/api/stats').then(r => r.json());
-
- document.getElementById('totalOrders').textContent = statsData.totalOrders;
- document.getElementById('digOrders').textContent = statsData.digOrdersFound;
- document.getElementById('filesProcessed').textContent = statsData.filesProcessed;
- document.getElementById('pendingApprovals').textContent = statsData.pendingApprovals.length;
-
- // Update approvals list
- const approvalsList = document.getElementById('approvalsList');
- if (statsData.pendingApprovals.length === 0) {
- approvalsList.innerHTML = '<p style="color: #888;">No pending approvals</p>';
- } else {
- approvalsList.innerHTML = statsData.pendingApprovals.map(approval => \`
- <div class="approval-card">
- <h3>\${approval.productTitle} (SKU: \${cleanSku(approval.digSku)})</h3>
- <p><strong>Customer:</strong> \${approval.customerName} (\${approval.customerEmail})</p>
- <p><strong>Order ID:</strong> \${approval.orderId}</p>
- <p><strong>Time:</strong> \${new Date(approval.timestamp).toLocaleString()}</p>
- <img src="\${approval.resizedImageUrl || approval.imageUrl}" alt="Product">
- <div>
- <button class="btn btn-approve" onclick="approveOrder('\${approval.id}')">✅ Approve & Send</button>
- <button class="btn btn-reject" onclick="rejectOrder('\${approval.id}')">❌ Reject</button>
- </div>
- </div>
- \`).join('');
- }
-
- // Update Slack messages
- const slackData = await fetch('/api/slack/messages').then(r => r.json());
- const slackDiv = document.getElementById('slackMessages');
- if (slackData.messages && slackData.messages.length > 0) {
- slackDiv.innerHTML = slackData.messages.map(msg => \`
- <div style="background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid #4facfe;">
- <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
- <strong style="color: #333;">\${msg.user}</strong>
- <span style="color: #888; font-size: 0.85em;">\${new Date(msg.timestamp * 1000).toLocaleString()}</span>
- </div>
- <div style="color: #555;">\${msg.text}</div>
- </div>
- \`).join('');
- } else {
- slackDiv.innerHTML = '<p style="color: #888;">No recent messages in #graphics-needed</p>';
- }
-
- // Update all samples list
- const samplesData = await fetch('/api/samples/all').then(r => r.json());
- window.allSamplesData = samplesData.samples || [];
- renderSamples(window.allSamplesData, samplesData);
-
- // Update activity log
- const logs = await fetch('/api/activity').then(r => r.json());
- const logDiv = document.getElementById('activityLog');
- logDiv.innerHTML = logs.map(log => \`
- <div class="log-entry \${log.type}">
- [\${new Date(log.timestamp).toLocaleTimeString()}] \${log.message}
- </div>
- \`).join('');
-
- updateLastRefreshTime();
- } catch (error) {
- console.error('Update error:', error);
- }
- }
-
- // Render samples function
- function renderSamples(samples, data) {
- const samplesList = document.getElementById('allSamplesList');
- if (samples && samples.length > 0) {
- const totalCount = data.total || samples.length;
- const last90Count = data.last90Days || samples.filter(s => {
- const daysAgo = (Date.now() - new Date(s.timestamp).getTime()) / (1000 * 60 * 60 * 24);
- return daysAgo <= 90;
- }).length;
-
- samplesList.innerHTML = \`
- <div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #2196F3;">
- <h3 style="margin: 0 0 10px 0; color: #1976D2;">📊 Summary</h3>
- <p style="margin: 5px 0;"><strong>DIG Samples (Last 90 Days):</strong> \${last90Count}</p>
- <p style="margin: 5px 0;"><strong>Total Displayed:</strong> \${samples.length}</p>
- </div>
- \` + samples.map(sample => {
- const statusColor = sample.status === 'approved' ? '#4CAF50' :
- sample.status === 'pending' ? '#FFA500' :
- sample.status === 'ordered' ? '#2196F3' : '#f44336';
- const statusText = sample.status === 'ordered' ? 'ORDERED' : sample.status.toUpperCase();
- const orderLink = sample.orderUrl ?
- \`<a href="\${sample.orderUrl}" target="_blank" style="color: #4facfe; text-decoration: none; font-weight: 600;">🔗 View Order in Shopify →</a>\` :
- '';
-
- return \`
- <div class="approval-card" style="opacity: \${sample.status === 'approved' || sample.status === 'ordered' ? '0.85' : '1'}; position: relative;">
- <h3>\${sample.productTitle}</h3>
- <p><strong>SKU:</strong> <span style="background: #f0f0f0; padding: 3px 8px; border-radius: 4px; font-family: monospace;">\${cleanSku(sample.digSku)}</span></p>
- <p><strong>Customer:</strong> \${sample.customerName} (\${sample.customerEmail})</p>
- <p><strong>Order:</strong> \${sample.orderId || sample.orderNumber || 'N/A'}</p>
- <p><strong>Status:</strong> <span style="color: \${statusColor}; font-weight: bold; background: \${statusColor}22; padding: 3px 10px; border-radius: 4px;">\${statusText}</span></p>
- <p><strong>Date:</strong> \${new Date(sample.timestamp).toLocaleString()}</p>
- \${orderLink ? \`<p style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;">\${orderLink}</p>\` : ''}
- </div>
- \`;
- }).join('');
- } else {
- samplesList.innerHTML = '<p style="color: #888;">No samples found</p>';
- }
- }
-
- // Search functionality
- window.allSamplesData = [];
- document.getElementById('searchSamples').addEventListener('input', function(e) {
- const searchTerm = e.target.value.toLowerCase();
-
- if (!searchTerm) {
- // Show all samples when search is empty
- fetch('/api/samples/all')
- .then(r => r.json())
- .then(data => {
- window.allSamplesData = data.samples || [];
- renderSamples(window.allSamplesData, data);
- });
- return;
- }
-
- const filtered = window.allSamplesData.filter(sample =>
- (sample.customerName && sample.customerName.toLowerCase().includes(searchTerm)) ||
- (sample.customerEmail && sample.customerEmail.toLowerCase().includes(searchTerm)) ||
- (sample.digSku && sample.digSku.toLowerCase().includes(searchTerm)) ||
- (sample.orderId && sample.orderId.toLowerCase().includes(searchTerm)) ||
- (sample.orderNumber && sample.orderNumber.toLowerCase().includes(searchTerm)) ||
- (sample.productTitle && sample.productTitle.toLowerCase().includes(searchTerm))
- );
-
- renderSamples(filtered, { total: filtered.length, last90Days: filtered.length });
- });
-
- function approveOrder(id) {
- fetch(\`/api/approve/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- function rejectOrder(id) {
- fetch(\`/api/reject/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- async function sendSlackMessage() {
- const input = document.getElementById('slackMessageInput');
- const message = input.value.trim();
-
- if (!message) {
- alert('Please enter a message');
- return;
- }
-
- try {
- const response = await fetch('/api/slack/send', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
- if (data.success) {
- input.value = '';
- updateDashboard(); // Refresh to show new message
- } else {
- alert('Failed to send message: ' + data.error);
- }
- } catch (error) {
- alert('Error sending message');
- }
- }
-
- function manualRefresh() {
- const button = event.target;
- button.disabled = true;
- button.textContent = '⏳ Refreshing...';
-
- updateDashboard().then(() => {
- button.disabled = false;
- button.textContent = '🔄 Refresh Now';
- });
- }
-
- function updateLastRefreshTime() {
- const now = new Date();
- const timeStr = now.toLocaleTimeString();
- document.getElementById('lastRefresh').textContent = \`Last updated: \${timeStr}\`;
- }
-
- // Auto-refresh every 30 seconds
- setInterval(() => {
- updateDashboard();
- updateLastRefreshTime();
- }, 30000);
-
- // Initial load
- updateDashboard();
- updateLastRefreshTime();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.get('/api/activity', requireAuth, (req, res) => {
- res.json(activityLog.slice(0, 50));
-});
-
-// Get Slack channel ID
-async function getGraphicsNeededChannelId() {
- try {
- const result = await slackClient.conversations.list({
- types: 'public_channel,private_channel',
- limit: 200
- });
-
- const channel = result.channels?.find((c: any) => c.name === 'graphics-needed');
- if (channel) {
- return channel.id;
- }
-
- console.error('Channel #graphics-needed not found in:', result.channels?.map((c: any) => c.name));
- return null;
- } catch (error) {
- console.error('Error getting channel list:', error);
- return null;
- }
-}
-
-// Slack API endpoints
-app.get('/api/slack/messages', requireAuth, async (req, res) => {
- try {
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ messages: [], error: 'Channel #graphics-needed not found' });
- }
-
- const result = await slackClient.conversations.history({
- channel: channelId,
- limit: 20
- });
-
- const messages = result.messages?.map((msg: any) => ({
- user: msg.user || 'Unknown',
- text: msg.text,
- timestamp: msg.ts
- })) || [];
-
- res.json({ messages });
- } catch (error) {
- console.error('Slack fetch error:', error);
- res.json({ messages: [], error: String(error) });
- }
-});
-
-app.post('/api/slack/send', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ success: false, error: 'Channel #graphics-needed not found' });
- }
-
- // Try to join the channel first if we get not_in_channel error
- try {
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (postError: any) {
- // If not in channel, try to join it
- if (postError.data?.error === 'not_in_channel') {
- console.log('🔧 Bot not in channel, attempting to join #graphics-needed...');
- try {
- await slackClient.conversations.join({ channel: channelId });
- console.log('✅ Successfully joined #graphics-needed');
-
- // Retry posting message
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (joinError) {
- console.error('❌ Failed to join channel:', joinError);
- res.json({ success: false, error: 'Bot needs to be invited to #graphics-needed channel by a workspace admin' });
- }
- } else {
- throw postError;
- }
- }
- } catch (error) {
- console.error('Slack send error:', error);
- res.json({ success: false, error: String(error) });
- }
-});
-
-// Keep track of all samples history (approved + pending)
-const samplesHistory: Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- processedAt?: Date;
-}> = [];
-
-// Get all samples (pending + approved history + last 90 days from Shopify)
-app.get('/api/samples/all', requireAuth, async (req, res) => {
- try {
- // Combine current pending with history
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected',
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders`
- }));
-
- // Fetch orders from last 90 days from Shopify
- const ninetyDaysAgo = new Date();
- ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
-
- const shopifyUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/orders.json?status=any&created_at_min=${ninetyDaysAgo.toISOString()}&limit=250`;
-
- const response = await fetch(shopifyUrl, {
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- },
- });
-
- const shopifyData: any = await response.json();
- const orders = shopifyData.orders || [];
-
- addLog(`Fetched ${orders.length} orders from Shopify (last 90 days)`, 'processing');
-
- // Extract DIG samples from Shopify orders
- const shopifySamples: Array<any> = [];
-
- for (const order of orders) {
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || 'No email';
-
- for (const item of digItems) {
- shopifySamples.push({
- id: `shopify-${order.id}-${item.id}`,
- orderId: order.name || `#${order.order_number}`,
- orderNumber: order.order_number.toString(),
- shopifyOrderId: order.id.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- status: 'ordered',
- timestamp: new Date(order.created_at),
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders/${order.id}`,
- financialStatus: order.financial_status,
- fulfillmentStatus: order.fulfillment_status || 'unfulfilled'
- });
- }
- }
- }
-
- addLog(`Found ${shopifySamples.length} DIG samples in recent orders`, 'processing');
-
- const allSamples = [...samplesHistory, ...currentPending, ...shopifySamples].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length, last90Days: shopifySamples.length });
- } catch (error) {
- console.error('Error fetching samples:', error);
- addLog(`Error fetching Shopify orders: ${error}`, 'error');
-
- // Fallback to local history only
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected'
- }));
-
- const allSamples = [...samplesHistory, ...currentPending].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length });
- }
-});
-
-app.post('/api/approve/:id', requireAuth, async (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (!approval) {
- return res.status(404).json({ error: 'Approval not found' });
- }
-
- approval.status = 'approved';
-
- try {
- // Send to Slack #new-products channel
- await slackClient.chat.postMessage({
- channel: SLACK_CHANNEL,
- text: `🆕 *DIG Sample Approved & Sent*\n\n📦 *${approval.productTitle}*\n• SKU: \`${approval.digSku}\`\n• Customer: ${approval.customerName}\n• Order: ${approval.orderId}\n\n✅ Sample sent to customer!`,
- unfurl_links: false,
- unfurl_media: false
- });
-
- stats.slackPostsSent++;
- stats.emailsSent++;
- addLog(`Approved and sent DIG-${approval.digSku} to ${approval.customerName}`, 'email');
-
- // Add to history before removing from pending
- samplesHistory.push({
- ...approval,
- status: 'approved',
- processedAt: new Date()
- });
-
- // Remove from pending
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
-
- res.json({ success: true, message: 'Sample approved and sent to customer!' });
- } catch (error) {
- addLog(`Error sending approval: ${error}`, 'error');
- res.status(500).json({ error: 'Failed to send sample' });
- }
-});
-
-app.post('/api/reject/:id', requireAuth, (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (approval) {
- // Add to history before removing
- samplesHistory.push({
- ...approval,
- status: 'rejected',
- processedAt: new Date()
- });
- }
-
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
- addLog(`Rejected DIG sample order ${req.params.id}`, 'processing');
- res.json({ success: true, message: 'Order rejected' });
-});
-
-// Google Drive API endpoints
-app.get('/api/drive/search', requireAuth, async (req, res) => {
- try {
- const query = (req.query.q as string) || '';
- const files = await searchDriveFiles(query);
-
- res.json({
- success: true,
- files: files.map((f: any) => ({
- id: f.id,
- name: f.name,
- mimeType: f.mimeType,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink,
- size: f.size,
- modified: f.modifiedTime
- })),
- total: files.length
- });
- } catch (error) {
- res.status(500).json({ error: `Drive search failed: ${error}` });
- }
-});
-
-app.get('/api/drive/file/:id', requireAuth, async (req, res) => {
- try {
- const fileInfo = await getDriveFileLink(req.params.id);
- res.json({ success: true, file: fileInfo });
- } catch (error) {
- res.status(500).json({ error: `Failed to get file: ${error}` });
- }
-});
-
-// Shopify webhook endpoint (receives new orders)
-app.post('/webhook/shopify/orders/create', async (req, res) => {
- try {
- const order = req.body;
- stats.totalOrders++;
-
- addLog(`New order received: #${order.order_number}`, 'order');
-
- // Check for DIG-series items
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- stats.digOrdersFound++;
-
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || '';
-
- stats.lastOrder = {
- orderId: order.name || order.order_number.toString(),
- customerName,
- digItems: digItems.map((item: any) => item.sku),
- timestamp: new Date()
- };
-
- addLog(`Found ${digItems.length} DIG items in order #${order.order_number}`, 'processing');
-
- // Process each DIG item
- for (const item of digItems) {
- await processDIGItem(order, item, customerEmail, customerName);
- }
- }
-
- res.json({ success: true });
- } catch (error) {
- addLog(`Webhook error: ${error}`, 'error');
- res.status(500).json({ error: 'Webhook processing failed' });
- }
-});
-
-// Google Drive search function
-async function searchDriveFiles(query: string = '') {
- try {
- const response = await drive.files.list({
- q: query ? `name contains '${query}' and mimeType contains 'image/'` : "mimeType contains 'image/'",
- fields: 'files(id, name, mimeType, webViewLink, thumbnailLink, size, createdTime, modifiedTime)',
- orderBy: 'modifiedTime desc',
- pageSize: 50
- });
-
- return response.data.files || [];
- } catch (error) {
- addLog(`Error searching Drive: ${error}`, 'error');
- return [];
- }
-}
-
-// Get Drive file download link
-async function getDriveFileLink(fileId: string) {
- try {
- const response = await drive.files.get({
- fileId,
- fields: 'webContentLink, webViewLink'
- });
- return response.data;
- } catch (error) {
- addLog(`Error getting Drive file link: ${error}`, 'error');
- return null;
- }
-}
-
-async function processDIGItem(order: any, item: any, customerEmail: string, customerName: string) {
- try {
- stats.filesProcessed++;
-
- // Search Google Drive for matching files
- const sku = item.sku.replace('DIG-', '');
- const driveFiles = await searchDriveFiles(sku);
-
- addLog(`Found ${driveFiles.length} Drive files matching SKU: ${sku}`, 'processing');
-
- // Create approval entry
- const approval = {
- id: `${order.id}-${item.id}-${Date.now()}`,
- orderId: order.name || order.order_number.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- resizedImageUrl: '', // Will be populated after image processing
- driveFiles: driveFiles.map((f: any) => ({
- id: f.id,
- name: f.name,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink
- })),
- status: 'pending' as const,
- timestamp: new Date()
- };
-
- stats.pendingApprovals.push(approval);
-
- addLog(`Created approval for ${item.sku} - ${customerName} with ${driveFiles.length} Drive files`, 'processing');
-
- } catch (error) {
- addLog(`Error processing DIG item: ${error}`, 'error');
- }
-}
-
-// Manual test endpoint
-app.post('/api/test/dig-order', requireAuth, async (req, res) => {
- const mockOrder = {
- id: Date.now(),
- order_number: 12345,
- name: '#TEST-' + Date.now(),
- customer: {
- first_name: 'Test',
- last_name: 'Customer',
- email: 'test@example.com'
- },
- line_items: [
- {
- id: Date.now(),
- sku: 'DIG-12345',
- title: 'Digital Sample - Test Pattern',
- properties: [
- { name: 'Image', value: 'https://via.placeholder.com/800x600' }
- ]
- }
- ]
- };
-
- await processDIGItem(mockOrder, mockOrder.line_items[0], 'test@example.com', 'Test Customer');
-
- res.json({ success: true, message: 'Test DIG order created!' });
-});
-
-
-// Metrics endpoint for daily reporting
-app.get("/api/metrics", (req: Request, res: Response) => {
- res.json({
- status: "online",
- uptime: process.uptime(),
- responseTime: 0,
- tasksCompleted: 0,
- errorsToday: 0,
- lastActivity: new Date().toISOString(),
- qnaReadiness: 100
- });
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('📦 Digital Samples Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('✅ Monitoring Shopify for DIG-series orders...');
- console.log('📁 Google Drive integration: Ready');
- console.log('🖼️ Image resizing: 24" @ 150 DPI');
- console.log('📢 Slack channel: #new-products');
- console.log('');
- addLog('Digital Samples Agent initialized and running', 'processing');
-});
diff --git a/DW-Agents/dw-agents/agent-legal-team/legal-agent.ts.backup b/DW-Agents/dw-agents/agent-legal-team/legal-agent.ts.backup
deleted file mode 100644
index 3ec765db..00000000
--- a/DW-Agents/dw-agents/agent-legal-team/legal-agent.ts.backup
+++ /dev/null
@@ -1,2322 +0,0 @@
-/**
- * DW Legal Team - Settlement Compliance Monitor
- *
- * PRIORITY #1: SETTLEMENT AGREEMENT COMPLIANCE
- *
- * This agent continuously monitors the product catalog for settlement violations
- * and provides legal guidance on tropical design patterns.
- *
- * SETTLEMENT CRITERIA (MUST BE ENFORCED):
- *
- * A product is PROHIBITED if it contains BOTH Part A AND Part B:
- *
- * Part A - Prohibited Design (ALL three must be present):
- * 1. Repeating patterns with directional variation amongst leaves/palm fronds
- * 2. Open space between leaves
- * 3. More than one ink color
- *
- * Part B - Prohibited Specific Elements (ANY of these):
- * - Bananas (FRUIT) or banana pods (NOT leaves - banana leaves are OK)
- * - Grapes (fruit/clusters)
- * - Birds
- * - Butterflies
- *
- * Acceptable Tropical Design (at least ONE must be present):
- * 1. Tree trunks
- * 2. Clearly represented branches
- * 3. Fruit or animal elements (EXCEPT prohibited ones)
- */
-
-import 'dotenv/config';
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import svgCaptcha from 'svg-captcha';
-import { chromium } from 'playwright';
-import fetch from 'node-fetch';
-import { GoogleGenerativeAI } from '@google/generative-ai';
-const { getUniversalHeader } = require('../shared-ui-components.js');
-
-const app = express();
-const PORT = 9878;
-
-// Analysis cache - stores results to avoid re-analyzing
-const analysisCache = new Map<string, { compliant: boolean; analysis: string; timestamp: number }>();
-
-// Authentication credentials
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || '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';
-const API_VERSION = process.env.SHOPIFY_ADMIN_API_VERSION || '2024-07';
-
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY,
-});
-
-// Settlement compliance statistics
-let stats = {
- totalScanned: 0,
- violations: 0,
- compliant: 0,
- needsReview: 0,
- lastScan: null as Date | null,
- lastViolation: null as { title: string; id: string; timestamp: Date } | null,
- importsMonitored: 0,
- lastImport: null as { title: string; status: string; timestamp: Date } | null,
-};
-
-// Recent activity log for real-time monitoring
-const activityLog: Array<{
- timestamp: Date;
- type: 'import' | 'violation' | 'compliant' | 'review';
- title: string;
- message: string;
-}> = [];
-
-// Violations tracking - store all detected violations
-const violationsList: Array<{
- productId: string;
- sku: string;
- title: string;
- url: string;
- analysis: string;
- timestamp: Date;
-}> = [];
-
-// Function to escalate violations to needs-attention agent
-async function escalateViolationToNeedsAttention(violation: {
- title: string;
- productId?: string;
- sku?: string;
- reasoning: string;
- productUrl?: string;
-}) {
- try {
- console.log(`🚨 Escalating violation to needs-attention agent: ${violation.sku || violation.title}`);
-
- const response = await fetch('http://localhost:9900/api/webhook/task', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- title: `🚨 URGENT: Legal Violation - ${violation.sku || violation.title}`,
- description: `SETTLEMENT AGREEMENT VIOLATION DETECTED
-
-Product: ${violation.title}
-SKU: ${violation.sku || 'Unknown'}
-Product ID: ${violation.productId || 'Unknown'}
-
-VIOLATION DETAILS:
-${violation.reasoning}
-
-REQUIRED ACTIONS:
-1. Immediately unpublish product from store
-2. Review product images and descriptions
-3. Contact legal counsel if needed
-4. Document the violation in legal records
-
-Product URL: ${violation.productUrl || `https://designer-laboratory-sandbox.myshopify.com/admin/products/${violation.productId}`}`,
- priority: 'critical',
- source: 'legal',
- metadata: {
- productId: violation.productId,
- sku: violation.sku,
- violationType: 'settlement-agreement',
- reasoning: violation.reasoning,
- agentPort: 9878,
- url: violation.productUrl || `https://designer-laboratory-sandbox.myshopify.com/admin/products/${violation.productId}`
- },
- assignedAgent: 'needs-attention'
- })
- });
-
- if (response.ok) {
- const result = await response.json();
- console.log(`✅ Violation escalated to needs-attention: ${result.taskId}`);
- } else {
- console.error(`❌ Failed to escalate violation: ${response.statusText}`);
- }
- } catch (error) {
- console.error(`❌ Error escalating violation: ${error}`);
- }
-}
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-app.use(express.static('public'));
-
-// Session configuration
-app.use(
- session({
- secret: 'dw-legal-team-secret-key-2025',
- resave: false,
- saveUninitialized: false,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 24 * 60 * 60 * 1000, // 24 hours
- },
- })
-);
-
-// Extend session type
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- captchaText: string;
- }
-}
-
-// Authentication middleware - DISABLED FOR DEBUGGING
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // TEMPORARY: Skip authentication
- next();
- return;
-
- if (req.session.authenticated) {
- next();
- } else {
- // For API requests, return JSON error instead of redirect
- if (req.path.startsWith('/api/')) {
- return res.status(401).json({ error: 'Authentication required' });
- }
- res.redirect('/login');
- }
-};
-
-// Settlement criteria reference
-const SETTLEMENT_AGREEMENT = {
- partA: {
- description: 'Prohibited Design Pattern',
- criteria: [
- 'Repeating patterns with directional variation amongst leaves/palm fronds',
- 'Open space between leaves',
- 'More than one ink color'
- ],
- requirement: 'ALL three conditions must be present'
- },
- partB: {
- description: 'Prohibited Specific Elements',
- criteria: [
- 'Bananas (FRUIT) or banana pods (NOT leaves - banana leaves are OK)',
- 'Grapes (fruit/clusters)',
- 'Birds',
- 'Butterflies'
- ],
- requirement: 'ANY one of these elements'
- },
- acceptable: {
- description: 'Acceptable Tropical Design Elements',
- criteria: [
- 'Tree trunks',
- 'Clearly represented branches',
- 'Fruit or animal elements (EXCEPT prohibited ones)'
- ],
- requirement: 'At least ONE must be present for tropical designs'
- },
- violation: 'BOTH Part A (all conditions) AND Part B (any element) must be present'
-};
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - DW Legal Team</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
- color: #e0e0e0;
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- padding: 20px;
- }
- .login-container {
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.1);
- padding: 40px;
- border-radius: 12px;
- backdrop-filter: blur(10px);
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- max-width: 400px;
- width: 100%;
- }
- .header {
- text-align: center;
- margin-bottom: 30px;
- }
- .header h1 {
- color: #dc143c;
- font-size: 2em;
- margin-bottom: 10px;
- }
- .header p {
- color: #888;
- font-size: 0.9em;
- }
- .form-group {
- margin-bottom: 20px;
- }
- label {
- display: block;
- color: #ccc;
- margin-bottom: 8px;
- font-weight: 500;
- }
- input {
- width: 100%;
- padding: 12px;
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 6px;
- background: rgba(0, 0, 0, 0.3);
- color: #fff;
- font-size: 1em;
- }
- input:focus {
- outline: none;
- border-color: #dc143c;
- }
- .captcha-container {
- display: flex;
- gap: 10px;
- align-items: center;
- margin-bottom: 20px;
- }
- .captcha-image {
- flex-shrink: 0;
- background: white;
- padding: 5px;
- border-radius: 6px;
- cursor: pointer;
- }
- .captcha-input {
- flex-grow: 1;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #8b0000 0%, #dc143c 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- font-size: 1em;
- cursor: pointer;
- font-weight: 600;
- transition: all 0.3s;
- }
- button:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(220, 20, 60, 0.4);
- }
- .error {
- background: rgba(255, 0, 0, 0.2);
- border: 1px solid #ff0000;
- color: #ffcccc;
- padding: 10px;
- border-radius: 6px;
- margin-bottom: 20px;
- text-align: center;
- }
- .refresh-captcha {
- font-size: 0.85em;
- color: #888;
- text-align: center;
- margin-top: 10px;
- }
- </style>
-</head>
-<body>
- <div class="login-container">
- <div class="header">
- <h1>⚖️ DW Legal Team</h1>
- <p>Settlement Compliance Monitor</p>
- </div>
- ${req.query.error ? '<div class="error">Invalid credentials or confirmation number</div>' : ''}
- <form method="POST" action="/login">
- <div class="form-group">
- <label>Username</label>
- <input type="text" name="username" required autocomplete="username">
- </div>
- <div class="form-group">
- <label>Password</label>
- <input type="password" name="password" required autocomplete="current-password">
- </div>
- <div class="form-group">
- <label>Confirmation: What is 7 + 3?</label>
- <input type="text" name="confirmation" required placeholder="Enter the number">
- </div>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-// Captcha generation
-app.get('/captcha', (req, res) => {
- const captcha = svgCaptcha.create({
- size: 6,
- noise: 3,
- color: true,
- background: '#f0f0f0',
- });
- req.session.captchaText = captcha.text;
- res.type('svg');
- res.send(captcha.data);
-});
-
-// Login handler
-app.post('/login', (req, res) => {
- const { username, password, confirmation } = req.body;
-
- if (
- username === AUTH_USERNAME &&
- password === AUTH_PASSWORD &&
- confirmation === '10'
- ) {
- req.session.authenticated = true;
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-// Logout handler
-app.get('/logout', (req, res) => {
- req.session.destroy(() => {
- // Clear SSO cookie
- res.clearCookie(SSO_TOKEN_NAME);
- res.redirect('/login');
- });
-});
-
-// Main HTML interface (protected)
-app.get('/', requireAuth, (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>DW Legal Team - Settlement Compliance Monitor</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
- color: #e0e0e0;
- padding: 20px;
- min-height: 100vh;
- }
- .header {
- background: linear-gradient(135deg, #8b0000 0%, #dc143c 100%);
- padding: 30px;
- border-radius: 12px;
- margin-bottom: 30px;
- box-shadow: 0 8px 32px rgba(220, 20, 60, 0.3);
- }
- .header h1 {
- color: white;
- font-size: 2.5em;
- margin-bottom: 10px;
- text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
- }
- .header .subtitle {
- color: #ffe6e6;
- font-size: 1.2em;
- font-weight: 300;
- }
- .priority-badge {
- display: inline-block;
- background: #ff0000;
- color: white;
- padding: 8px 16px;
- border-radius: 20px;
- font-weight: bold;
- margin-top: 15px;
- font-size: 0.9em;
- letter-spacing: 1px;
- animation: pulse 2s infinite;
- }
- @keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.7; }
- }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stat-card {
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.1);
- padding: 20px;
- border-radius: 12px;
- backdrop-filter: blur(10px);
- }
- .stat-card h3 {
- color: #888;
- font-size: 0.9em;
- text-transform: uppercase;
- letter-spacing: 1px;
- margin-bottom: 10px;
- }
- .stat-card .value {
- font-size: 2.5em;
- font-weight: bold;
- color: #fff;
- }
- .stat-card.violations .value {
- color: #ff4444;
- }
- .stat-card.compliant .value {
- color: #4CAF50;
- }
- .stat-card.review .value {
- color: #FFA500;
- }
- .settlement-section {
- background: rgba(255, 255, 255, 0.05);
- border: 2px solid rgba(220, 20, 60, 0.5);
- padding: 30px;
- border-radius: 12px;
- margin-bottom: 30px;
- backdrop-filter: blur(10px);
- }
- .settlement-section h2 {
- color: #ff6b6b;
- margin-bottom: 20px;
- font-size: 1.8em;
- border-bottom: 2px solid rgba(220, 20, 60, 0.3);
- padding-bottom: 10px;
- }
- .criteria-box {
- background: rgba(0, 0, 0, 0.3);
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 20px;
- border-left: 4px solid #dc143c;
- }
- .criteria-box h3 {
- color: #ff8888;
- margin-bottom: 15px;
- font-size: 1.3em;
- }
- .criteria-box ul {
- list-style: none;
- padding: 0;
- }
- .criteria-box li {
- padding: 10px 0;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- color: #ccc;
- }
- .criteria-box li:last-child {
- border-bottom: none;
- }
- .criteria-box .requirement {
- background: rgba(220, 20, 60, 0.2);
- padding: 10px;
- border-radius: 6px;
- margin-top: 15px;
- font-weight: bold;
- color: #ff9999;
- }
- .violation-rule {
- background: rgba(255, 0, 0, 0.2);
- border: 2px solid #ff0000;
- padding: 20px;
- border-radius: 8px;
- margin-top: 20px;
- font-size: 1.1em;
- font-weight: bold;
- color: #ffcccc;
- text-align: center;
- }
- .action-buttons {
- display: flex;
- gap: 15px;
- flex-wrap: wrap;
- margin-bottom: 30px;
- }
- button {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border: none;
- padding: 15px 30px;
- border-radius: 8px;
- font-size: 1em;
- cursor: pointer;
- transition: all 0.3s;
- font-weight: 600;
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
- }
- button:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
- }
- button:active {
- transform: translateY(0);
- }
- button.scan {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- }
- button.review-tool {
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- }
- .log-section {
- background: rgba(0, 0, 0, 0.4);
- padding: 20px;
- border-radius: 12px;
- margin-top: 30px;
- border: 1px solid rgba(255, 255, 255, 0.1);
- }
- .log-section h2 {
- color: #fff;
- margin-bottom: 15px;
- font-size: 1.5em;
- }
- .log-entry {
- padding: 10px;
- margin: 5px 0;
- border-radius: 6px;
- font-family: 'Courier New', monospace;
- font-size: 0.9em;
- }
- .log-entry.violation {
- background: rgba(255, 68, 68, 0.2);
- border-left: 4px solid #ff4444;
- color: #ff9999;
- }
- .log-entry.compliant {
- background: rgba(76, 175, 80, 0.2);
- border-left: 4px solid #4CAF50;
- color: #a5d6a7;
- }
- .log-entry.info {
- background: rgba(100, 100, 100, 0.2);
- border-left: 4px solid #888;
- color: #ccc;
- }
- .last-violation {
- background: rgba(255, 0, 0, 0.15);
- padding: 20px;
- border-radius: 8px;
- margin-top: 20px;
- border: 2px solid rgba(255, 0, 0, 0.3);
- }
- .last-violation h3 {
- color: #ff6666;
- margin-bottom: 10px;
- }
- .last-violation p {
- color: #ffcccc;
- margin: 5px 0;
- }
- .url-checker-section {
- background: rgba(255, 255, 255, 0.05);
- border: 2px solid rgba(102, 126, 234, 0.5);
- padding: 25px;
- border-radius: 12px;
- margin-bottom: 30px;
- backdrop-filter: blur(10px);
- }
- .url-checker-section h2 {
- color: #8888ff;
- margin-bottom: 20px;
- font-size: 1.6em;
- }
- .url-input-form {
- display: flex;
- gap: 15px;
- margin-bottom: 20px;
- }
- .url-input-form input {
- flex: 1;
- padding: 15px;
- border: 2px solid rgba(102, 126, 234, 0.5);
- border-radius: 8px;
- background: rgba(0, 0, 0, 0.3);
- color: #fff;
- font-size: 1em;
- }
- .url-input-form input:focus {
- outline: none;
- border-color: #667eea;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
- }
- .url-input-form button {
- padding: 15px 40px;
- white-space: nowrap;
- }
- .url-result {
- background: rgba(0, 0, 0, 0.4);
- padding: 20px;
- border-radius: 8px;
- border-left: 4px solid #667eea;
- display: none;
- }
- .url-result.pass {
- border-left-color: #4CAF50;
- background: rgba(76, 175, 80, 0.1);
- }
- .url-result.violation {
- border-left-color: #ff4444;
- background: rgba(255, 68, 68, 0.1);
- }
- .url-result.checking {
- border-left-color: #FFA500;
- background: rgba(255, 165, 0, 0.1);
- }
- .url-result h3 {
- margin-bottom: 15px;
- font-size: 1.3em;
- }
- .url-result pre {
- white-space: pre-wrap;
- word-wrap: break-word;
- color: #ccc;
- line-height: 1.6;
- }
- </style>
-</head>
-<body style="padding: 20px;">
- ${getUniversalHeader('Legal Team', PORT, 5)}
-
- <div class="header" style="margin-top: 20px;">
- <h1>⚖️ DW Legal Team</h1>
- <div class="subtitle">Settlement Compliance Monitor</div>
- <div class="priority-badge">🚨 PRIORITY #1: SETTLEMENT AGREEMENT</div>
- <div style="text-align: right; margin-top: 15px;">
- <a href="/logout" style="color: #ffe6e6; text-decoration: none; padding: 8px 16px; background: rgba(0,0,0,0.3); border-radius: 6px; font-size: 0.9em;">Logout</a>
- </div>
- </div>
-
- <div id="violationsList" class="violations-alert" style="display: none; background: rgba(255, 0, 0, 0.2); border: 4px solid #ff0000; padding: 30px; border-radius: 15px; margin-bottom: 25px; animation: pulse-red 2s infinite;">
- <h2 style="color: #ff0000; font-size: 2.2em; text-align: center; margin-bottom: 20px;">🚨 SETTLEMENT VIOLATIONS DETECTED</h2>
- <div id="violationsContent"></div>
- </div>
-
- <div class="settlement-section" style="background: rgba(220, 20, 60, 0.15); border: 3px solid #dc143c;">
- <h2>📋 Settlement Agreement - Quick Reference</h2>
-
- <div class="violation-rule" style="margin-bottom: 20px;">
- ⚠️ VIOLATION OCCURS WHEN: BOTH Part A (all conditions) AND Part B (any element) are present
- </div>
-
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
- <div class="criteria-box">
- <h3>Part A: Prohibited Design Pattern</h3>
- <ul>
- <li>✓ Repeating patterns with directional variation amongst leaves/palm fronds</li>
- <li>✓ Open space between leaves</li>
- <li>✓ More than one ink color</li>
- </ul>
- <div class="requirement">ALL three conditions must be present</div>
- </div>
-
- <div class="criteria-box">
- <h3>Part B: Prohibited Specific Elements</h3>
- <ul>
- <li>🚫 Bananas (FRUIT) or banana pods - <strong style="color: #90EE90;">NOTE: Banana LEAVES are OK!</strong></li>
- <li>🚫 Grapes (fruit/clusters)</li>
- <li>🚫 Birds</li>
- <li>🚫 Butterflies</li>
- </ul>
- <div class="requirement">ANY one of these elements</div>
- </div>
- </div>
-
- <div class="criteria-box" style="margin-top: 20px; border-left-color: #4CAF50;">
- <h3 style="color: #90EE90;">✅ Acceptable Tropical Design Elements</h3>
- <ul>
- <li>✓ Tree trunks</li>
- <li>✓ Clearly represented branches</li>
- <li>✓ Fruit or animal elements (EXCEPT prohibited ones)</li>
- </ul>
- <div class="requirement" style="background: rgba(76, 175, 80, 0.2); color: #a5d6a7;">At least ONE must be present for tropical designs</div>
- </div>
- </div>
-
- <div class="url-checker-section">
- <h2>🔍 Quick URL Compliance Check</h2>
- <form class="url-input-form" onsubmit="checkURL(event)">
- <input type="url" id="urlInput" placeholder="Enter product URL to check for settlement compliance..." required>
- <button type="submit">Check Now</button>
- </form>
- <div id="urlResult" class="url-result"></div>
- </div>
-
- <div class="stats-grid">
- <div class="stat-card">
- <h3>Imports Monitored</h3>
- <div class="value" id="importsMonitored">0</div>
- </div>
- <div class="stat-card violations">
- <h3>Violations</h3>
- <div class="value" id="violations">0</div>
- </div>
- <div class="stat-card compliant">
- <h3>Compliant</h3>
- <div class="value" id="compliant">0</div>
- </div>
- <div class="stat-card review">
- <h3>Needs Review</h3>
- <div class="value" id="needsReview">0</div>
- </div>
- </div>
-
- <div id="lastImport" class="last-violation" style="display:none; background: rgba(0, 100, 255, 0.15); border-color: rgba(0, 100, 255, 0.3);">
- <h3>📦 Most Recent Import</h3>
- <p id="importTitle"></p>
- <p id="importStatus"></p>
- <p id="importTime"></p>
- </div>
-
- <div class="settlement-section">
- <h2>📋 Settlement Agreement Criteria</h2>
-
- <div class="criteria-box">
- <h3>Part A: Prohibited Design Pattern</h3>
- <ul>
- <li>✓ Repeating patterns with directional variation amongst leaves/palm fronds</li>
- <li>✓ Open space between leaves</li>
- <li>✓ More than one ink color</li>
- </ul>
- <div class="requirement">ALL three conditions must be present</div>
- </div>
-
- <div class="criteria-box">
- <h3>Part B: Prohibited Specific Elements</h3>
- <ul>
- <li>🚫 Bananas (FRUIT) or banana pods - <strong style="color: #90EE90;">NOTE: Banana LEAVES are OK!</strong></li>
- <li>🚫 Grapes (fruit/clusters)</li>
- <li>🚫 Birds</li>
- <li>🚫 Butterflies</li>
- </ul>
- <div class="requirement">ANY one of these elements</div>
- </div>
-
- <div class="criteria-box">
- <h3>✅ Acceptable Tropical Design Elements</h3>
- <ul>
- <li>✓ Tree trunks</li>
- <li>✓ Clearly represented branches</li>
- <li>✓ Fruit or animal elements (EXCEPT prohibited ones)</li>
- </ul>
- <div class="requirement">At least ONE must be present for tropical designs</div>
- </div>
-
- <div class="violation-rule">
- ⚠️ VIOLATION OCCURS WHEN: BOTH Part A (all conditions) AND Part B (any element) are present
- </div>
- </div>
-
- <div class="settlement-section" style="background: rgba(220, 20, 60, 0.1); border: 2px solid #dc143c;">
- <h2>🔍 Search for Settlement Violations</h2>
-
- <div class="criteria-box">
- <h3>🚫 Search by Prohibited Elements (Part B)</h3>
- <div class="action-buttons" style="grid-template-columns: repeat(4, 1fr);">
- <button class="scan" onclick="searchByKeyword('banana')">🍌 Bananas</button>
- <button class="scan" onclick="searchByKeyword('grape')">🍇 Grapes</button>
- <button class="scan" onclick="searchByKeyword('bird')">🦜 Birds</button>
- <button class="scan" onclick="searchByKeyword('butterfly')">🦋 Butterflies</button>
- </div>
- </div>
-
- <div class="criteria-box">
- <h3>🌴 Search by Tropical Keywords</h3>
- <div class="action-buttons" style="grid-template-columns: repeat(4, 1fr);">
- <button class="scan" onclick="searchByKeyword('palm')">🌴 Palm</button>
- <button class="scan" onclick="searchByKeyword('tropical')">🏝️ Tropical</button>
- <button class="scan" onclick="searchByKeyword('leaf')">🍃 Leaf</button>
- <button class="scan" onclick="searchByKeyword('frond')">🌿 Frond</button>
- <button class="scan" onclick="searchByKeyword('jungle')">🌳 Jungle</button>
- <button class="scan" onclick="searchByKeyword('exotic')">🦎 Exotic</button>
- <button class="scan" onclick="searchByKeyword('botanical')">🌺 Botanical</button>
- <button class="scan" onclick="searchByKeyword('paradise')">🦚 Paradise</button>
- </div>
- </div>
-
- <div class="criteria-box">
- <h3>⚠️ Search by Fruit & Nature Keywords</h3>
- <div class="action-buttons" style="grid-template-columns: repeat(4, 1fr);">
- <button class="scan" onclick="searchByKeyword('fruit')">🍎 Fruit</button>
- <button class="scan" onclick="searchByKeyword('flower')">🌸 Flower</button>
- <button class="scan" onclick="searchByKeyword('blossom')">🌼 Blossom</button>
- <button class="scan" onclick="searchByKeyword('vine')">🍇 Vine</button>
- </div>
- </div>
- </div>
-
- <div class="action-buttons">
- <button class="scan" onclick="scanCatalog()">🔍 Scan Entire Catalog</button>
- <button class="scan" onclick="scanNewProducts()">🆕 Scan New Products (24h)</button>
- <button class="scan" onclick="scanTagged()">🏷️ Scan SETTLEMENT Tagged</button>
- <button class="review-tool" onclick="openReviewTool()">📊 Open Settlement Review Tool</button>
- </div>
-
- <div id="search-results" style="margin-top: 30px;"></div>
-
- <div id="lastViolation" class="last-violation" style="display:none;">
- <h3>🚨 Most Recent Violation</h3>
- <p id="violationTitle"></p>
- <p id="violationId"></p>
- <p id="violationTime"></p>
- </div>
-
- <div class="log-section">
- <h2>📝 Activity Log</h2>
- <div id="logs"></div>
- </div>
-
- <script>
- function updateStats() {
- fetch('/api/stats')
- .then(r => r.json())
- .then(data => {
- document.getElementById('importsMonitored').textContent = data.importsMonitored;
- document.getElementById('violations').textContent = data.violations;
- document.getElementById('compliant').textContent = data.compliant;
- document.getElementById('needsReview').textContent = data.needsReview;
-
- if (data.lastImport) {
- document.getElementById('lastImport').style.display = 'block';
- document.getElementById('importTitle').textContent = 'Product: ' + data.lastImport.title;
- document.getElementById('importStatus').textContent = 'Status: ' + data.lastImport.status;
- document.getElementById('importTime').textContent = 'Time: ' + new Date(data.lastImport.timestamp).toLocaleString();
- }
- });
-
- // Update violations list
- fetch('/api/violations')
- .then(r => r.json())
- .then(violations => {
- const violationsList = document.getElementById('violationsList');
- const violationsContent = document.getElementById('violationsContent');
-
- if (violations.length > 0) {
- violationsList.style.display = 'block';
- violationsContent.innerHTML = violations.map(function(v) {
- return '<div style="background: white; border-left: 6px solid #ff0000; padding: 20px; margin-bottom: 15px; border-radius: 8px; color: #333;">' +
- '<h3 style="color: #ff0000; margin-bottom: 10px;">' + v.title + '</h3>' +
- '<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: 10px; font-size: 0.9em;">' +
- '<div><strong>SKU:</strong> ' + v.sku + '</div>' +
- '<div><strong>Product ID:</strong> ' + v.productId + '</div>' +
- '<div><strong>Detected:</strong> ' + new Date(v.timestamp).toLocaleDateString() + '</div>' +
- '</div>' +
- '<div style="background: #fff8f8; padding: 15px; border-radius: 6px; margin: 10px 0; font-size: 0.9em; white-space: pre-wrap;">' + v.analysis + '</div>' +
- '<a href="' + v.url + '" target="_blank" style="display: inline-block; background: #dc143c; color: white; padding: 10px 20px; border-radius: 6px; text-decoration: none; font-weight: bold;">View in Shopify →</a>' +
- '</div>';
- }).join('');
- } else {
- violationsList.style.display = 'none';
- }
- });
- }
-
- function addLog(message, type = 'info') {
- const logsDiv = document.getElementById('logs');
- const entry = document.createElement('div');
- entry.className = 'log-entry ' + type;
- entry.textContent = new Date().toLocaleTimeString() + ' - ' + message;
- logsDiv.insertBefore(entry, logsDiv.firstChild);
-
- // Keep only last 50 entries
- while (logsDiv.children.length > 50) {
- logsDiv.removeChild(logsDiv.lastChild);
- }
- }
-
- function searchByKeyword(keyword) {
- addLog(\`Searching for products containing "\${keyword}"...\`, 'info');
- const resultsDiv = document.getElementById('search-results');
- resultsDiv.innerHTML = '<div style="color: #FFA500; text-align: center; padding: 40px;">⏳ Searching...</div>';
-
- fetch(\`/api/search/keyword?q=\${encodeURIComponent(keyword)}\`, {
- credentials: 'same-origin'
- })
- .then(r => {
- if (!r.ok) {
- throw new Error(\`HTTP \${r.status}: \${r.statusText}\`);
- }
- return r.json();
- })
- .then(data => {
- if (data.error) {
- throw new Error(data.error);
- }
- if (data.products && data.products.length > 0) {
- addLog(\`Found \${data.products.length} products with "\${keyword}" - Analyzing for violations...\`, 'success');
- showSearchResults(keyword, data.products);
- } else {
- addLog(\`No products found containing "\${keyword}"\`, 'info');
- resultsDiv.innerHTML = '<div style="color: #aaa; text-align: center; padding: 40px;">No products found</div>';
- }
- })
- .catch(err => {
- addLog(\`Error searching for "\${keyword}": \${err.message}\`, 'error');
- resultsDiv.innerHTML = '<div style="color: #dc143c; text-align: center; padding: 40px;">Error: ' + err.message + '</div>';
- });
- }
-
- function showSearchResults(keyword, products) {
- const resultsDiv = document.getElementById('search-results');
- resultsDiv.innerHTML = \`
- <h2 style="color: #dc143c; margin-bottom: 20px;">🔍 "\` + keyword + \`" - Found \` + products.length + \` Products</h2>
- <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;">
- \` + products.map(p => \`
- <div id="product-\` + p.id + \`" style="position: relative; background: rgba(255,255,255,0.05); padding: 20px; border-radius: 12px; border-left: 4px solid #dc143c;">
- <div style="position: absolute; top: 10px; right: 10px; z-index: 10;">
- <span style="background: \` + (p.status === 'active' ? '#4CAF50' : '#FFA500') + \`; color: white; padding: 4px 12px; border-radius: 20px; font-size: 0.75em; font-weight: bold;">\` + (p.status === 'active' ? '🟢 ACTIVE' : '🟡 DRAFT') + \`</span>
- </div>
- \` + (p.image ? '<img src="' + p.image + '" alt="' + p.title + '" style="width: 100%; height: 200px; object-fit: cover; border-radius: 8px; margin-bottom: 15px;">' : '<div style="width: 100%; height: 200px; background: rgba(255,255,255,0.1); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #666; margin-bottom: 15px;">No Image</div>') + \`
- <h3 style="color: #fff; margin: 0 0 10px 0; font-size: 1.1em;">\` + p.title + \`</h3>
- <div style="color: #aaa; font-size: 0.85em; margin-bottom: 10px;">
- <div>SKU: \` + (p.sku || 'N/A') + \`</div>
- <div>ID: \` + p.id + \`</div>
- \` + (p.vendor ? '<div>Vendor: ' + p.vendor + '</div>' : '') + \`
- </div>
- <div id="compliance-\` + p.id + \`" style="margin: 10px 0;">
- <div style="color: #FFA500; font-size: 0.85em;">⏳ Analyzing...</div>
- </div>
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px;">
- <a href="https://admin.shopify.com/store/designer-laboratory-sandbox/products/\` + p.id + \`" target="_blank" style="background: #4CAF50; color: white; padding: 10px; border-radius: 6px; text-decoration: none; font-size: 0.85em; text-align: center; font-weight: bold;">📝 Shopify</a>
- <button onclick="setToDraft('\` + p.id + \`')" style="background: #dc143c; color: white; padding: 10px; border-radius: 6px; border: none; font-size: 0.85em; cursor: pointer; font-weight: bold;">⚠️ Set Draft</button>
- </div>
- </div>
- \`).join('') + \`
- </div>
- \`;
-
- // Auto-check compliance with AI for all products
- products.forEach(p => {
- if (p.image) {
- checkProductWithImage(p.image, p.title, p.tags, p.id);
- } else if (p.online_store_url) {
- checkProductCompliance(p.online_store_url, p.id);
- }
- });
- }
-
- function formatAnalysis(text) {
- // Format the analysis to clearly separate CRITERIA SET A and SET B
- let formatted = text;
-
- // Add visual separators for the sections
- formatted = formatted.replace(/CRITERIA SET A:/gi, '<div style="margin-top: 10px; padding: 8px; background: rgba(255,255,255,0.05); border-radius: 4px;"><strong style="color: #FFD700;">📋 CRITERIA SET A (Pattern Features):</strong>');
- formatted = formatted.replace(/CRITERIA SET B:/gi, '</div><div style="margin-top: 8px; padding: 8px; background: rgba(255,255,255,0.05); border-radius: 4px;"><strong style="color: #FF6B6B;">🚫 CRITERIA SET B (Prohibited Elements):</strong>');
- formatted = formatted.replace(/Final answer:/gi, '</div><div style="margin-top: 10px; padding: 8px; border-top: 2px solid rgba(255,255,255,0.2);"><strong>Final answer:</strong>');
-
- return formatted;
- }
-
- function sortProductsToTop() {
- // Move all violation cards to the top
- const resultsContainer = document.querySelector('#search-results > div');
- if (!resultsContainer) return;
-
- const productCards = Array.from(resultsContainer.children);
- const violations = [];
- const compliant = [];
-
- productCards.forEach(card => {
- const complianceDiv = card.querySelector('[id^="compliance-"]');
- if (complianceDiv && complianceDiv.innerHTML.includes('🚨 SETTLEMENT VIOLATION')) {
- violations.push(card);
- } else {
- compliant.push(card);
- }
- });
-
- // Clear and re-add: violations first, then compliant
- resultsContainer.innerHTML = '';
- violations.forEach(card => resultsContainer.appendChild(card));
- compliant.forEach(card => resultsContainer.appendChild(card));
- }
-
- function checkProductWithImage(imageUrl, title, tags, productId) {
- const complianceDiv = document.getElementById(\`compliance-\${productId}\`);
- complianceDiv.innerHTML = '<div style="color: #FFA500; font-size: 0.85em;">⏳ AI analyzing image...</div>';
-
- fetch('/api/check-image', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ imageUrl, title, tags, productId })
- })
- .then(r => r.json())
- .then(data => {
- const verifiedBadge = data.cached ? '<div style="display: inline-block; background: #2196F3; color: white; padding: 4px 10px; border-radius: 12px; font-size: 0.75em; margin-left: 8px;">✓ VERIFIED</div>' : '';
-
- if (data.compliant === false) {
- complianceDiv.innerHTML = \`
- <div style="background: rgba(220, 20, 60, 0.2); border: 2px solid #dc143c; padding: 12px; border-radius: 6px;">
- <div><strong style="color: #ff6b6b;">🚨 SETTLEMENT VIOLATION</strong>\${verifiedBadge}</div>
- <div style="color: #ffaaaa; font-size: 0.85em; margin-top: 8px; line-height: 1.6;">
- \${formatAnalysis(data.analysis || data.reason || 'Violates settlement agreement')}
- </div>
- </div>
- \`;
- // Sort violations to top after marking
- setTimeout(sortProductsToTop, 100);
- } else {
- complianceDiv.innerHTML = \`
- <div style="background: rgba(76, 175, 80, 0.15); border: 1px solid #4CAF50; padding: 10px; border-radius: 6px;">
- <div><strong style="color: #90EE90; font-size: 0.85em;">✅ COMPLIANT</strong>\${verifiedBadge}</div>
- <div style="color: #aaa; font-size: 0.8em; margin-top: 4px; line-height: 1.6;">
- \${formatAnalysis(data.analysis || 'No settlement violations detected')}
- </div>
- </div>
- \`;
- }
- })
- .catch(err => {
- complianceDiv.innerHTML = '<div style="color: #dc143c; font-size: 0.85em;">Error: ' + err.message + '</div>';
- });
- }
-
- function checkProductCompliance(url, productId) {
- const complianceDiv = document.getElementById(\`compliance-\${productId}\`);
- complianceDiv.innerHTML = '<div style="color: #FFA500;">⏳ Analyzing for settlement violations...</div>';
-
- fetch('/api/check-url', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ url })
- })
- .then(r => r.json())
- .then(data => {
- if (data.compliant === false) {
- complianceDiv.innerHTML = \`
- <div style="background: rgba(220,20,60,0.2); border: 2px solid #dc143c; padding: 15px; border-radius: 8px; margin-top: 10px;">
- <div style="color: #dc143c; font-weight: bold; font-size: 1.1em; margin-bottom: 10px;">🚨 SETTLEMENT VIOLATION DETECTED</div>
- <div style="color: #fff; margin-bottom: 8px;"><strong>Reason:</strong> \${data.reason || 'Contains prohibited elements'}</div>
- \${data.analysis ? '<div style="color: #aaa; font-size: 0.9em; margin-top: 8px;">' + data.analysis + '</div>' : ''}
- </div>
- \`;
- } else {
- complianceDiv.innerHTML = \`
- <div style="background: rgba(76,175,80,0.2); border: 2px solid #4CAF50; padding: 15px; border-radius: 8px; margin-top: 10px;">
- <div style="color: #4CAF50; font-weight: bold;">✅ COMPLIANT</div>
- <div style="color: #aaa; font-size: 0.9em; margin-top: 5px;">\${data.reason || 'No settlement violations detected'}</div>
- </div>
- \`;
- }
- })
- .catch(err => {
- complianceDiv.innerHTML = \`<div style="color: #FFA500;">⚠️ Error: \${err.message}</div>\`;
- });
- }
-
- function scanCatalog() {
- addLog('Starting full catalog scan...', 'info');
- fetch('/api/scan/catalog', { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- addLog('Catalog scan complete: ' + data.scanned + ' products scanned', 'info');
- updateStats();
- });
- }
-
- function scanNewProducts() {
- addLog('Scanning products from last 24 hours...', 'info');
- fetch('/api/scan/new', { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- addLog('New products scan complete: ' + data.scanned + ' products scanned', 'info');
- updateStats();
- });
- }
-
- function scanTagged() {
- addLog('Scanning SETTLEMENT tagged products...', 'info');
- fetch('/api/scan/tagged', { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- addLog('Tagged products scan complete: ' + data.scanned + ' products scanned', 'info');
- updateStats();
- });
- }
-
- function openReviewTool() {
- window.open('http://45.61.58.125:9877', '_blank');
- }
-
- async function setToDraft(productId) {
- if (!confirm('Set this product to DRAFT status? This will hide it from the store.')) {
- return;
- }
-
- try {
- const response = await fetch('/api/set-draft', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ productId })
- });
-
- const data = await response.json();
-
- if (data.success) {
- addLog(\`Product \${productId} set to DRAFT\`, 'success');
- alert('✅ Product set to DRAFT successfully!');
- // Refresh the card to show new status
- location.reload();
- } else {
- throw new Error(data.error || 'Failed to set draft');
- }
- } catch (error) {
- addLog(\`Error setting product to draft: \${error.message}\`, 'error');
- alert('❌ Error: ' + error.message);
- }
- }
-
- async function checkURL(event) {
- event.preventDefault();
-
- const urlInput = document.getElementById('urlInput');
- const resultDiv = document.getElementById('urlResult');
- const url = urlInput.value.trim();
-
- if (!url) return;
-
- // Show checking state
- resultDiv.className = 'url-result checking';
- resultDiv.style.display = 'block';
- resultDiv.innerHTML = '<h3>🔄 Checking URL...</h3><p>Scraping page and analyzing for settlement compliance...</p>';
-
- try {
- const response = await fetch('/api/check-url', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ url })
- });
-
- const data = await response.json();
-
- if (data.error) {
- resultDiv.className = 'url-result violation';
- resultDiv.innerHTML = '<h3>❌ Error</h3><pre>' + data.error + '</pre>';
- return;
- }
-
- if (data.status === 'PASS') {
- resultDiv.className = 'url-result pass';
- resultDiv.innerHTML = '<h3>✅ PASS - Compliant</h3><pre>' + data.analysis + '</pre>';
- addLog('URL check PASSED: ' + url, 'compliant');
- } else if (data.status === 'VIOLATION') {
- resultDiv.className = 'url-result violation';
- resultDiv.innerHTML = '<h3>🚨 VIOLATION - Settlement Issue Detected</h3><pre>' + data.analysis + '</pre>';
- addLog('URL check VIOLATION: ' + url, 'violation');
- } else {
- resultDiv.className = 'url-result';
- resultDiv.innerHTML = '<h3>⚠️ Needs Review</h3><pre>' + data.analysis + '</pre>';
- addLog('URL check needs review: ' + url, 'info');
- }
- } catch (error) {
- resultDiv.className = 'url-result violation';
- resultDiv.innerHTML = '<h3>❌ Error</h3><pre>Failed to check URL: ' + error.message + '</pre>';
- }
- }
-
- // Update stats every 10 seconds
- setInterval(updateStats, 10000);
- updateStats();
-
- addLog('DW Legal Team initialized - Settlement compliance monitoring active', 'info');
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints (protected)
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.get('/api/activity', requireAuth, (req, res) => {
- // Return last 100 activity log entries
- res.json(activityLog.slice(-100).reverse());
-});
-
-app.get('/api/violations', requireAuth, (req, res) => {
- // Return all violations sorted by most recent first
- res.json(violationsList.slice().reverse());
-});
-
-// Shopify webhook for product updates (DRAFT → ACTIVE monitoring)
-app.post('/api/webhook/product-update', async (req, res) => {
- try {
- console.log('\n📦 Product Update Webhook Received');
-
- const product = req.body;
- const productId = product.id?.toString().replace(/\D/g, '') || product.admin_graphql_api_id?.toString().replace(/\D/g, '');
- const title = product.title;
- const status = product.status;
-
- console.log(` Product: ${title}`);
- console.log(` Status: ${status}`);
- console.log(` ID: ${productId}`);
-
- // Check if product is being activated (draft → active)
- if (status === 'active') {
- console.log(`\n🔍 PRODUCT ACTIVATED - Checking for existing analysis: ${title}`);
-
- // FIRST: Check Shopify metafields - DO NOT REANALYZE if already verified
- try {
- const metafieldsResponse = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${productId}/metafields.json`, {
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json'
- }
- });
-
- const metafields = await metafieldsResponse.json();
- const statusField = metafields.metafields?.find((m: any) => m.namespace === 'legal' && m.key === 'settlement_status');
- const analysisField = metafields.metafields?.find((m: any) => m.namespace === 'legal' && m.key === 'settlement_analysis');
-
- if (statusField && analysisField) {
- console.log(` ✓ Already analyzed - Status: ${statusField.value} (NO API CALL MADE)`);
-
- // Use stored analysis - NO new API call
- const isViolation = statusField.value === 'VIOLATION';
-
- if (isViolation) {
- console.log(`\n🚨 EXISTING VIOLATION DETECTED ON ACTIVATION: ${title}`);
-
- const sku = product.variants?.[0]?.sku || 'Unknown';
-
- // Escalate to needs-attention
- await escalateViolationToNeedsAttention({
- title,
- productId,
- sku,
- reasoning: analysisField.value,
- productUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/products/${productId}`
- });
-
- stats.violations++;
- } else {
- console.log(` ✅ Product is compliant (from stored analysis)`);
- stats.compliant++;
- }
-
- stats.importsMonitored++;
- return res.status(200).json({ received: true, reanalyzed: false });
- }
- } catch (metafieldError) {
- console.log(` ⚠️ No existing analysis found, will analyze now`);
- }
-
- // ONLY if no metafields exist: Analyze the product
- const imageUrl = product.image?.src || product.images?.[0]?.src;
- const sku = product.variants?.[0]?.sku || 'Unknown';
-
- if (imageUrl) {
- console.log(` 🔬 First-time analysis (will be saved for future)`);
-
- // Trigger compliance check
- setTimeout(async () => {
- try {
- const checkResponse = await fetch(`http://localhost:${PORT}/api/check-image`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Cookie': `${SSO_TOKEN_NAME}=${SSO_TOKEN_VALUE}`
- },
- body: JSON.stringify({
- imageUrl,
- title,
- tags: product.tags || '',
- productId,
- sku
- })
- });
-
- const result = await checkResponse.json();
-
- if (!result.compliant) {
- console.log(`\n🚨 NEW VIOLATION DETECTED ON ACTIVATION: ${title}`);
-
- // Escalate to needs-attention
- await escalateViolationToNeedsAttention({
- title,
- productId,
- sku,
- reasoning: result.analysis,
- productUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/products/${productId}`
- });
-
- stats.violations++;
- } else {
- console.log(` ✅ Product is compliant`);
- stats.compliant++;
- }
-
- stats.importsMonitored++;
- } catch (error) {
- console.error(` ❌ Error checking product: ${error}`);
- }
- }, 1000);
- }
- }
-
- res.status(200).json({ received: true });
- } catch (error) {
- console.error('Webhook error:', error);
- res.status(500).json({ error: 'Webhook processing failed' });
- }
-});
-
-// Webhook to receive import notifications
-app.post('/api/webhook/import', (req, res) => {
- try {
- const { title, status, settlementCheck } = req.body;
-
- stats.importsMonitored++;
- stats.lastImport = {
- title,
- status: settlementCheck?.status || 'unknown',
- timestamp: new Date(),
- };
-
- // Log the import activity
- if (settlementCheck?.violates) {
- stats.violations++;
- stats.lastViolation = {
- title,
- id: req.body.productId || 'unknown',
- timestamp: new Date(),
- };
-
- activityLog.push({
- timestamp: new Date(),
- type: 'violation',
- title,
- message: `🚨 VIOLATION: ${settlementCheck.reasoning}`,
- });
-
- console.log(`🚨 VIOLATION DETECTED: ${title}`);
-
- // Escalate to needs-attention agent
- escalateViolationToNeedsAttention({
- title,
- productId: req.body.productId,
- sku: req.body.sku,
- reasoning: settlementCheck.reasoning,
- productUrl: req.body.productUrl
- }).catch(err => console.error('Failed to escalate violation:', err));
- } else if (settlementCheck?.needsReview) {
- stats.needsReview++;
-
- activityLog.push({
- timestamp: new Date(),
- type: 'review',
- title,
- message: `⚠️ Needs Review: ${settlementCheck.reasoning}`,
- });
-
- console.log(`⚠️ NEEDS REVIEW: ${title}`);
- } else {
- stats.compliant++;
-
- activityLog.push({
- timestamp: new Date(),
- type: 'compliant',
- title,
- message: `✅ Compliant: Settlement check passed`,
- });
-
- console.log(`✅ COMPLIANT: ${title}`);
- }
-
- // Keep only last 1000 activity entries
- if (activityLog.length > 1000) {
- activityLog.splice(0, activityLog.length - 1000);
- }
-
- res.json({ success: true });
- } catch (error) {
- console.error('Webhook error:', error);
- res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
- }
-});
-
-app.get('/api/settlement-criteria', requireAuth, (req, res) => {
- res.json(SETTLEMENT_AGREEMENT);
-});
-
-app.post('/api/scan/catalog', requireAuth, async (req, res) => {
- try {
- // This would scan the entire catalog
- res.json({ success: true, scanned: 0, message: 'Full catalog scan initiated' });
- } catch (error) {
- res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
- }
-});
-
-app.post('/api/scan/new', requireAuth, async (req, res) => {
- try {
- // This would scan products from last 24 hours
- res.json({ success: true, scanned: 0, message: 'New products scan initiated' });
- } catch (error) {
- res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
- }
-});
-
-app.post('/api/scan/tagged', requireAuth, async (req, res) => {
- try {
- // This would scan SETTLEMENT tagged products
- res.json({ success: true, scanned: 0, message: 'Tagged products scan initiated' });
- } catch (error) {
- res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
- }
-});
-
-// Keyword search endpoint
-app.get('/api/search/keyword', requireAuth, async (req, res) => {
- try {
- const keyword = req.query.q as string;
- console.log(`🔍 SEARCH REQUEST RECEIVED: keyword="${keyword}"`);
-
- if (!keyword) {
- console.error('❌ No keyword provided');
- return res.status(400).json({ error: 'Query parameter required' });
- }
-
- console.log(`🔎 GraphQL search for keyword "${keyword}" across ALL products...`);
-
- // Use GraphQL to search across ALL products (not limited to 250)
- const graphqlQuery = `
- query SearchProducts($query: String!) {
- products(first: 250, query: $query) {
- edges {
- node {
- id
- legacyResourceId
- title
- vendor
- tags
- productType
- handle
- descriptionHtml
- status
- featuredImage {
- url
- altText
- }
- variants(first: 1) {
- edges {
- node {
- sku
- }
- }
- }
- }
- }
- pageInfo {
- hasNextPage
- endCursor
- }
- }
- }
- `;
-
- // Build search query - search in title, tag, product_type, vendor, body
- const searchQuery = `title:*${keyword}* OR tag:*${keyword}* OR product_type:*${keyword}* OR vendor:*${keyword}* OR body:*${keyword}*`;
-
- let allProducts: any[] = [];
- let hasNextPage = true;
- let cursor = null;
- let pageCount = 0;
-
- while (hasNextPage && pageCount < 20) { // Max 20 pages = 5000 products
- pageCount++;
-
- const queryWithCursor = cursor
- ? graphqlQuery.replace('first: 250', `first: 250, after: "${cursor}"`)
- : graphqlQuery;
-
- const response = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/graphql.json`, {
- method: 'POST',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- query: queryWithCursor,
- variables: { query: searchQuery }
- })
- });
-
- if (!response.ok) {
- console.error(`Shopify GraphQL API error: ${response.status} ${response.statusText}`);
- return res.status(500).json({ error: `Shopify GraphQL API error: ${response.status}` });
- }
-
- const data: any = await response.json();
-
- if (data.errors) {
- console.error('GraphQL errors:', data.errors);
- return res.status(500).json({ error: 'GraphQL query error' });
- }
-
- const edges = data.data?.products?.edges || [];
- allProducts.push(...edges.map((e: any) => e.node));
-
- hasNextPage = data.data?.products?.pageInfo?.hasNextPage || false;
- cursor = data.data?.products?.pageInfo?.endCursor;
-
- console.log(`Page ${pageCount}: Found ${edges.length} products, Total: ${allProducts.length}`);
- }
-
- console.log(`✅ GraphQL search complete: Found ${allProducts.length} matching products for "${keyword}"`);
-
- const formattedProducts = allProducts.map((p: any) => ({
- id: p.legacyResourceId,
- title: p.title,
- vendor: p.vendor,
- tags: Array.isArray(p.tags) ? p.tags.join(', ') : p.tags || '',
- product_type: p.productType,
- sku: p.variants?.edges?.[0]?.node?.sku || null,
- image: p.featuredImage?.url || null,
- online_store_url: `https://www.designerwallcoverings.com/products/${p.handle}`,
- status: p.status?.toLowerCase() || 'draft'
- }));
-
- // Sort by status: ACTIVE first, then DRAFT
- formattedProducts.sort((a, b) => {
- if (a.status === 'active' && b.status !== 'active') return -1;
- if (a.status !== 'active' && b.status === 'active') return 1;
- return 0;
- });
-
- console.log(`📦 Sending ${formattedProducts.length} formatted products to frontend (sorted by status)`);
- res.json({ products: formattedProducts, keyword, count: formattedProducts.length });
- } catch (error) {
- console.error('Keyword search error:', error);
- res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
- }
-});
-
-// Image compliance checker endpoint - STRICT analysis with Claude
-app.post('/api/check-image', requireAuth, async (req, res) => {
- try {
- const { imageUrl, title, tags, productId } = req.body;
-
- if (!imageUrl) {
- return res.status(400).json({ error: 'Image URL is required' });
- }
-
- // Check cache FIRST - if product already analyzed, return cached result
- const cacheKey = productId || imageUrl;
- const cached = analysisCache.get(cacheKey);
- if (cached) {
- console.log(`✓ Cache hit for: ${title}`);
- return res.json({
- compliant: cached.compliant,
- analysis: cached.analysis,
- reason: cached.compliant ? 'No violations found' : 'Settlement violation detected',
- cached: true,
- timestamp: cached.timestamp
- });
- }
-
- // Check Shopify metafields - DO NOT REANALYZE if already verified
- if (productId) {
- try {
- const metafieldsResponse = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${productId}/metafields.json`, {
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json'
- }
- });
-
- const metafields = await metafieldsResponse.json();
- const verifiedField = metafields.metafields?.find((m: any) => m.namespace === 'legal' && m.key === 'settlement_verified');
- const analysisField = metafields.metafields?.find((m: any) => m.namespace === 'legal' && m.key === 'settlement_analysis');
- const statusField = metafields.metafields?.find((m: any) => m.namespace === 'legal' && m.key === 'settlement_status');
-
- if (verifiedField && analysisField && statusField) {
- console.log(`✓ Already analyzed (${verifiedField.value}): ${title} - ${statusField.value}`);
- const isCompliant = statusField.value === 'COMPLIANT';
-
- // Store in cache for faster future access
- analysisCache.set(cacheKey, {
- compliant: isCompliant,
- analysis: analysisField.value,
- timestamp: new Date(verifiedField.value).getTime()
- });
-
- return res.json({
- compliant: isCompliant,
- analysis: analysisField.value,
- reason: isCompliant ? 'No violations found' : 'Settlement violation detected',
- cached: false,
- fromShopify: true,
- verifiedDate: verifiedField.value
- });
- }
- } catch (metafieldError) {
- console.error(` ⚠️ Failed to check metafields:`, metafieldError);
- }
- }
-
- console.log(`\n🔍 STRICT analysis: ${title}`);
-
- // Check title FIRST for prohibited keywords
- const titleLower = title.toLowerCase();
- const hasBanana = titleLower.includes('banana');
- const hasGrape = titleLower.includes('grape');
- const hasBird = titleLower.includes('bird');
- const hasButterfly = titleLower.includes('butterfly');
-
- const prompt = `First, describe what you see in this wallpaper image titled "${title}" in detail.
-
-Then determine if it matches this settlement violation criteria:
-
-**CRITERIA SET A (Pattern Features - need ALL 3):**
-- Has repeating leaves with directional variation: YES/NO
-- Has open space between elements: YES/NO
-- Has 2+ colors: YES/NO
-
-**CRITERIA SET B (Prohibited Elements - need ANY 1):**
-- Shows banana FRUIT or banana PODS (NOT leaves - banana leaves are OK): YES/NO
-- Shows grape fruit/clusters: YES/NO
-- Shows birds: YES/NO
-- Shows butterflies: YES/NO
-
-If the first 3 are ALL YES and ANY of the last 4 is YES, it's a VIOLATION.
-
-Please respond in this exact format:
-
-DESCRIPTION: [Detailed description of what you see]
-
-CRITERIA SET A:
-- Repeating leaves with directional variation: YES/NO
-- Open space between elements: YES/NO
-- Multiple colors (2+): YES/NO [list the colors you see]
-
-CRITERIA SET B:
-- Banana FRUIT or banana PODS (NOT leaves - banana leaves are OK): YES/NO
-- Grape FRUIT/clusters: YES/NO
-- Birds: YES/NO
-- Butterflies: YES/NO
-
-Final answer: VIOLATION or COMPLIANT`;
-
- const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
-
- const response = await anthropic.messages.create({
- model: 'claude-3-opus-20240229',
- max_tokens: 1024,
- messages: [{
- role: 'user',
- content: [
- {
- type: 'image',
- source: {
- type: 'url',
- url: imageUrl
- }
- },
- {
- type: 'text',
- text: prompt
- }
- ]
- }]
- });
-
- const analysisText = response.content[0].type === 'text' ? response.content[0].text : '';
- const isViolation = analysisText.includes('VIOLATION') && !analysisText.includes('COMPLIANT');
-
- console.log(` ${isViolation ? '🚨 VIOLATION' : '✅ COMPLIANT'}`);
- console.log(` Analysis: ${analysisText.substring(0, 200)}...`);
-
- // Store in cache for future requests
- analysisCache.set(cacheKey, {
- compliant: !isViolation,
- analysis: analysisText,
- timestamp: Date.now()
- });
-
- // SAVE TO SHOPIFY METAFIELDS - Legal compliance tracking
- if (productId) {
- try {
- const verificationDate = new Date().toISOString();
- const metafieldsUpdate = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${productId}/metafields.json`, {
- method: 'POST',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- metafield: {
- namespace: 'legal',
- key: 'settlement_verified',
- value: verificationDate,
- type: 'single_line_text_field'
- }
- })
- });
-
- const analysisUpdate = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${productId}/metafields.json`, {
- method: 'POST',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- metafield: {
- namespace: 'legal',
- key: 'settlement_analysis',
- value: analysisText,
- type: 'multi_line_text_field'
- }
- })
- });
-
- const statusUpdate = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${productId}/metafields.json`, {
- method: 'POST',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- metafield: {
- namespace: 'legal',
- key: 'settlement_status',
- value: isViolation ? 'VIOLATION' : 'COMPLIANT',
- type: 'single_line_text_field'
- }
- })
- });
-
- // Also update product notes field with the analysis
- try {
- const notesContent = `🔍 LEGAL COMPLIANCE ANALYSIS - ${new Date().toLocaleDateString()}
-
-${isViolation ? '🚨 SETTLEMENT VIOLATION' : '✅ COMPLIANT'}
-${analysisText}
-
-Last verified: ${verificationDate}`;
-
- const productUpdateResponse = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${productId}.json`, {
- method: 'PUT',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- product: {
- id: productId,
- body_html: notesContent // Using body_html as a notes field
- }
- })
- });
-
- console.log(` ✅ Saved to Shopify metafields AND product notes for product ${productId}`);
- } catch (notesError) {
- console.error(` ⚠️ Failed to update product notes:`, notesError);
- console.log(` ✅ Saved to Shopify metafields for product ${productId}`);
- }
- } catch (metafieldError) {
- console.error(` ⚠️ Failed to save metafields:`, metafieldError);
- }
- }
-
- // Add to violations list if it's a violation
- if (isViolation && productId) {
- const violation = {
- productId: productId,
- sku: req.body.sku || 'Unknown',
- title: title,
- url: `https://${SHOPIFY_STORE}/admin/products/${productId}`,
- analysis: analysisText,
- timestamp: new Date()
- };
-
- // Remove old entry for this product if exists
- const existingIndex = violationsList.findIndex(v => v.productId === productId);
- if (existingIndex >= 0) {
- violationsList.splice(existingIndex, 1);
- }
-
- violationsList.push(violation);
- console.log(` 🚨 Added to violations list: ${title}`);
- }
-
- res.json({
- compliant: !isViolation,
- analysis: analysisText,
- reason: isViolation ? 'Settlement violation detected' : 'No violations found',
- cached: false
- });
-
- } catch (error) {
- console.error('Image check error:', error);
- res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
- }
-});
-
-// Set product to DRAFT endpoint - for legal compliance enforcement
-app.post('/api/set-draft', requireAuth, async (req, res) => {
- try {
- const { productId } = req.body;
-
- if (!productId) {
- return res.status(400).json({ error: 'Product ID required' });
- }
-
- console.log(`⚠️ Setting product ${productId} to DRAFT (legal compliance)`);
-
- // Use GraphQL to update product status
- const mutation = `
- mutation productUpdate($input: ProductInput!) {
- productUpdate(input: $input) {
- product {
- id
- title
- status
- }
- userErrors {
- field
- message
- }
- }
- }
- `;
-
- const response = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/graphql.json`, {
- method: 'POST',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- query: mutation,
- variables: {
- input: {
- id: `gid://shopify/Product/${productId}`,
- status: 'DRAFT'
- }
- }
- })
- });
-
- if (!response.ok) {
- throw new Error(`Shopify GraphQL API error: ${response.status}`);
- }
-
- const result = await response.json();
-
- if (result.data?.productUpdate?.userErrors?.length > 0) {
- const errors = result.data.productUpdate.userErrors.map((e: any) => e.message).join(', ');
- throw new Error(`Shopify errors: ${errors}`);
- }
-
- console.log(`✅ Product ${productId} set to DRAFT via GraphQL`);
- console.log(` Title: ${result.data?.productUpdate?.product?.title}`);
-
- res.json({
- success: true,
- message: 'Product set to draft',
- product: result.data?.productUpdate?.product
- });
- } catch (error) {
- console.error('Set draft error:', error);
- res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
- }
-});
-
-// URL compliance checker endpoint
-app.post('/api/check-url', requireAuth, async (req, res) => {
- let browser;
-
- try {
- const { url } = req.body;
-
- if (!url) {
- return res.status(400).json({ error: 'URL is required' });
- }
-
- console.log(`\n🔍 Checking URL: ${url}`);
-
- // Scrape the page with Playwright
- browser = await chromium.launch({ headless: true });
- const page = await browser.newPage();
-
- // Try with domcontentloaded first (faster), fallback if needed
- try {
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
- } catch (e) {
- console.log(` ⚠️ domcontentloaded failed, trying load...`);
- try {
- await page.goto(url, { waitUntil: 'load', timeout: 60000 });
- } catch (e2) {
- await browser.close();
- return res.status(500).json({
- error: 'Page failed to load within timeout period. The page may be too slow or unavailable.',
- });
- }
- }
-
- // Wait for body to ensure content is loaded
- try {
- await page.waitForSelector('body', { timeout: 10000 });
- } catch (e) {
- await browser.close();
- return res.status(500).json({
- error: 'Page body could not be loaded.',
- });
- }
-
- // Extract text content
- const textContent = await page.evaluate(() => {
- return document.body.innerText;
- });
-
- // Get title and meta description
- const title = await page.title();
- const description = await page.evaluate(() => {
- const meta = document.querySelector('meta[name="description"]');
- return meta ? meta.getAttribute('content') : '';
- });
-
- await browser.close();
-
- console.log(` ✅ Page scraped successfully`);
- console.log(` 📄 Title: ${title}`);
-
- // Analyze with Anthropic
- console.log(` 🤖 Analyzing with Claude...`);
-
- const prompt = `You are a legal compliance expert analyzing a product page for settlement agreement violations.
-
-SETTLEMENT CRITERIA (MUST BE ENFORCED):
-
-A product is PROHIBITED if it contains BOTH Part A AND Part B:
-
-Part A - Prohibited Design (ALL three must be present):
-1. Repeating patterns with directional variation amongst leaves/palm fronds
-2. Open space between leaves
-3. More than one ink color
-
-Part B - Prohibited Specific Elements (ANY of these):
-- Banana FRUIT or banana PODS (NOT leaves - banana leaves are OK)
-- Grape fruit/clusters
-- Birds
-- Butterflies
-
-Acceptable Tropical Design (at least ONE must be present):
-1. Tree trunks
-2. Clearly represented branches
-3. Fruit or animal elements (EXCEPT prohibited ones)
-
-VIOLATION RULE: BOTH Part A (all conditions) AND Part B (any element) must be present
-
----
-
-PAGE TITLE: ${title}
-
-PAGE DESCRIPTION: ${description}
-
-PAGE CONTENT:
-${textContent.slice(0, 5000)}
-
----
-
-Analyze this product page and determine if it violates the settlement agreement.
-
-Respond in this exact format:
-
-STATUS: [PASS or VIOLATION]
-
-ANALYSIS:
-[Detailed explanation of your findings]`;
-
- const message = await anthropic.messages.create({
- model: 'claude-sonnet-4-5-20250929',
- max_tokens: 2000,
- messages: [
- {
- role: 'user',
- content: prompt,
- },
- ],
- });
-
- const responseText = message.content[0].type === 'text' ? message.content[0].text : '';
-
- // Parse response
- const statusMatch = responseText.match(/STATUS:\s*(PASS|VIOLATION)/i);
- const analysisMatch = responseText.match(/ANALYSIS:\s*([\s\S]*)/i);
-
- const status = statusMatch ? statusMatch[1].toUpperCase() : 'UNKNOWN';
- const analysis = analysisMatch ? analysisMatch[1].trim() : responseText;
-
- console.log(` 📊 Result: ${status}`);
-
- return res.json({
- status,
- analysis,
- url,
- title,
- });
- } catch (error) {
- console.error(' ❌ Error checking URL:', error);
-
- // Make sure browser is closed
- if (browser) {
- try {
- await browser.close();
- } catch (e) {
- // Ignore close errors
- }
- }
-
- return res.status(500).json({
- error: error instanceof Error ? error.message : 'Unknown error',
- });
- }
-});
-
-// API: Chat endpoint for universal header
-app.post('/api/chat', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- const systemPrompt = `You are the Legal Team Agent AI assistant for Designer Wallcoverings. Your primary role is settlement compliance monitoring.
-
-You help with:
-- Settlement agreement compliance checking
-- Product design analysis for tropical patterns
-- Legal risk assessment
-- Compliance violations review
-- Settlement criteria interpretation
-
-Be professional, precise, and compliance-focused. Provide clear guidance on settlement requirements.`;
-
- const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY || '' });
-
- const response = await anthropic.messages.create({
- model: 'claude-3-5-sonnet-20241022',
- max_tokens: 1024,
- system: systemPrompt,
- messages: [{ role: 'user', content: message }],
- });
-
- const assistantMessage = response.content[0].type === 'text' ? response.content[0].text : '';
-
- res.json({
- response: assistantMessage,
- success: true
- });
- } catch (error: any) {
- res.status(500).json({ error: error.message });
- }
-});
-
-// Track last check timestamp for newly active products
-let lastCheckTimestamp = new Date(Date.now() - 5 * 60 * 1000); // Start 5 minutes ago
-
-// Scheduled job to check for newly activated products (DRAFT → ACTIVE)
-async function checkForNewlyActiveProducts() {
- try {
- const now = new Date();
- console.log(`\n🔍 [${now.toLocaleTimeString()}] Checking for newly activated products since ${lastCheckTimestamp.toLocaleTimeString()}...`);
-
- // GraphQL query to get products updated since last check
- const graphqlQuery = `
- query GetRecentlyUpdatedProducts {
- products(first: 100, query: "status:active updated_at:>='${lastCheckTimestamp.toISOString()}'", sortKey: UPDATED_AT, reverse: true) {
- edges {
- node {
- id
- legacyResourceId
- title
- status
- updatedAt
- tags
- featuredImage {
- url
- }
- variants(first: 1) {
- edges {
- node {
- sku
- }
- }
- }
- }
- }
- }
- }
- `;
-
- const response = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/graphql.json`, {
- method: 'POST',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ query: graphqlQuery })
- });
-
- if (!response.ok) {
- console.error(` ❌ GraphQL API error: ${response.status}`);
- return;
- }
-
- const data: any = await response.json();
-
- if (data.errors) {
- console.error(' ❌ GraphQL errors:', data.errors);
- return;
- }
-
- const products = data.data?.products?.edges || [];
-
- if (products.length === 0) {
- console.log(` ✓ No newly activated products found`);
- lastCheckTimestamp = now;
- return;
- }
-
- console.log(` 📦 Found ${products.length} recently updated active products`);
-
- // Check each product for compliance
- for (const edge of products) {
- const product = edge.node;
- const productId = product.legacyResourceId;
- const title = product.title;
- const imageUrl = product.featuredImage?.url;
- const sku = product.variants?.edges?.[0]?.node?.sku || 'Unknown';
-
- console.log(`\n 🔬 Reviewing: ${title} (ID: ${productId})`);
-
- // Check if already analyzed (metafields)
- try {
- const metafieldsResponse = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${productId}/metafields.json`, {
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- 'Content-Type': 'application/json'
- }
- });
-
- const metafields = await metafieldsResponse.json();
- const statusField = metafields.metafields?.find((m: any) => m.namespace === 'legal' && m.key === 'settlement_status');
- const analysisField = metafields.metafields?.find((m: any) => m.namespace === 'legal' && m.key === 'settlement_analysis');
-
- if (statusField && analysisField) {
- console.log(` ✓ Already analyzed: ${statusField.value} (NO API CALL)`);
-
- // If violation, escalate since it was just activated
- if (statusField.value === 'VIOLATION') {
- console.log(` 🚨 VIOLATION DETECTED - Escalating to needs-attention`);
-
- await escalateViolationToNeedsAttention({
- title,
- productId,
- sku,
- reasoning: analysisField.value,
- productUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/products/${productId}`
- });
- }
- continue;
- }
- } catch (metafieldError) {
- console.log(` ⚠️ No existing analysis - will analyze`);
- }
-
- // If not analyzed and has image, analyze it
- if (imageUrl) {
- console.log(` 🔬 First-time analysis (will be saved for future)`);
-
- try {
- const checkResponse = await fetch(`http://localhost:${PORT}/api/check-image`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Cookie': `${SSO_TOKEN_NAME}=${SSO_TOKEN_VALUE}`
- },
- body: JSON.stringify({
- imageUrl,
- title,
- tags: product.tags || '',
- productId,
- sku
- })
- });
-
- const result = await checkResponse.json();
-
- if (!result.compliant) {
- console.log(` 🚨 NEW VIOLATION DETECTED - Escalating`);
-
- await escalateViolationToNeedsAttention({
- title,
- productId,
- sku,
- reasoning: result.analysis,
- productUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/products/${productId}`
- });
-
- stats.violations++;
- } else {
- console.log(` ✅ Product is compliant`);
- stats.compliant++;
- }
- } catch (error) {
- console.error(` ❌ Error analyzing product: ${error}`);
- }
- } else {
- console.log(` ⚠️ No image - skipping analysis`);
- }
- }
-
- lastCheckTimestamp = now;
- console.log(`\n ✓ Check complete at ${now.toLocaleTimeString()}`);
- } catch (error) {
- console.error('Error checking for newly active products:', error);
- }
-}
-
-// Start server
-app.listen(PORT, () => {
- console.log('');
- console.log('⚖️ DW Legal Team - Settlement Compliance Monitor');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🚨 PRIORITY #1: SETTLEMENT AGREEMENT COMPLIANCE');
- console.log('');
- console.log(`🌍 Access at: http://45.61.58.125:${PORT}`);
- console.log(`🏠 Local: http://localhost:${PORT}`);
- console.log('');
- console.log('📋 Settlement Criteria Loaded');
- console.log('✓ Part A: Prohibited Design Pattern');
- console.log('✓ Part B: Prohibited Specific Elements');
- console.log('✓ Acceptable Elements Defined');
- console.log('');
- console.log('🔍 Monitoring active...');
- console.log('');
- console.log('⏰ DRAFT→ACTIVE Monitoring: Every 2 minutes');
- console.log(' Checking for newly activated products via GraphQL polling');
- console.log('');
-
- // Run check every 2 minutes for newly activated products
- setInterval(checkForNewlyActiveProducts, 2 * 60 * 1000);
-
- // Run initial check after 10 seconds
- setTimeout(checkForNewlyActiveProducts, 10000);
-});
diff --git a/DW-Agents/dw-agents/agent-marketing/marketing-agent.ts.backup-1762750384 b/DW-Agents/dw-agents/agent-marketing/marketing-agent.ts.backup-1762750384
deleted file mode 100644
index f003d67b..00000000
--- a/DW-Agents/dw-agents/agent-marketing/marketing-agent.ts.backup-1762750384
+++ /dev/null
@@ -1,1506 +0,0 @@
-/**
- * DW-Agents: Marketing Agent
- *
- * AI-powered marketing content creation and management:
- * 1. View and manage Shopify blog posts
- * 2. Create compelling blog posts from new SKUs
- * 3. Generate social media content (Instagram, Facebook)
- * 4. Draft email campaigns (Constant Contact integration)
- * 5. Chat interface for content creation assistance
- *
- * Port: 9881
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import fetch from 'node-fetch';
-import { AgentMemory } from './shared-memory-system';
-
-const app = express();
-const PORT = 9881;
-// Initialize agent memory system
-const agentMemory = new AgentMemory('marketing');
-console.log('📚 Memory system initialized for marketing');
-
-
-// Authentication
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-// Shopify configuration
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || 'shpat_REDACTED';
-const API_VERSION = '2024-07';
-
-// Constant Contact configuration (placeholder)
-const CC_API_KEY = process.env.CONSTANT_CONTACT_API_KEY || '';
-const CC_ACCESS_TOKEN = process.env.CONSTANT_CONTACT_ACCESS_TOKEN || '';
-
-// Anthropic Claude
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY || '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',
-});
-
-// Stats tracking
-let stats = {
- totalConversations: 0,
- blogPostsCreated: 0,
- socialPostsGenerated: 0,
- emailCampaignsDrafted: 0,
- lastBlogPost: null as { title: string; sku: string; timestamp: Date } | null,
- recentBlogs: [] as Array<{
- id: string;
- title: string;
- createdAt: string;
- publishedAt: string | null;
- tags: string;
- }>,
- recentProducts: [] as Array<{
- id: string;
- title: string;
- sku: string;
- vendor: string;
- handle: string;
- imageUrl: string;
- }>
-};
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-marketing-agent-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // TEMPORARY: Auth disabled for 4 hours
- return next();
-
- /* Original auth:
-
- if (req.session.authenticated) {
- next();
- } else {
- res.redirect('/login');
- }
- */
-};
-
-// Shopify API helper
-async function shopifyGraphQL(query: string, variables?: any) {
- const response = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/graphql.json`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- },
- body: JSON.stringify({ query, variables }),
- });
- return response.json();
-}
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Marketing Agent</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #f5576c; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>📢 Marketing Agent</h1>
- <p style="text-align: center; color: #666; margin-bottom: 20px;">Content Creation & Campaigns</p>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard
-app.get('/', requireAuth, async (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Marketing Agent - DW-Agents</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .container {
- max-width: 1600px;
- margin: 0 auto;
- display: grid;
- grid-template-columns: 400px 1fr;
- gap: 20px;
- }
-
- /* Mobile-responsive sidebar */
- .sidebar {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .sidebar-toggle {
- display: none;
- position: fixed;
- top: 20px;
- left: 20px;
- z-index: 1001;
- background: white;
- border: none;
- border-radius: 50%;
- width: 50px;
- height: 50px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.2);
- cursor: pointer;
- font-size: 1.5em;
- color: #f5576c;
- transition: transform 0.3s;
- }
- .sidebar-toggle:hover {
- transform: scale(1.1);
- }
- .sidebar-toggle:focus {
- outline: 3px solid #f5576c;
- outline-offset: 2px;
- }
-
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #d8194e; font-size: 2em; margin-bottom: 10px; }
- .header .subtitle { color: #333; font-size: 1.1em; }
- .stats-card {
- background: white;
- padding: 20px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stats-card h3 { color: #555; font-size: 0.85em; text-transform: uppercase; margin-bottom: 8px; }
- .stats-card .value { font-size: 2em; font-weight: bold; color: #d8194e; }
- .main-content {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .tab-container {
- background: white;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- overflow: hidden;
- }
-
- /* Scrollable tabs for mobile */
- .tabs {
- display: flex;
- background: #f8f9fa;
- border-bottom: 2px solid #e0e0e0;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- scrollbar-width: thin;
- }
- .tabs::-webkit-scrollbar {
- height: 4px;
- }
- .tabs::-webkit-scrollbar-track {
- background: #f1f1f1;
- }
- .tabs::-webkit-scrollbar-thumb {
- background: #f5576c;
- border-radius: 4px;
- }
- .tab {
- flex: 1;
- min-width: 140px;
- padding: 15px;
- text-align: center;
- cursor: pointer;
- font-weight: 600;
- color: #333;
- transition: all 0.3s;
- white-space: nowrap;
- }
- .tab:hover {
- background: #f0f0f0;
- }
- .tab:focus {
- outline: 3px solid #f5576c;
- outline-offset: -3px;
- z-index: 1;
- }
- .tab.active {
- background: white;
- color: #d8194e;
- border-bottom: 3px solid #d8194e;
- }
- .tab-content {
- padding: 30px;
- display: none;
- }
- .tab-content.active {
- display: block;
- }
-
- /* Loading spinner */
- .spinner {
- display: inline-block;
- width: 20px;
- height: 20px;
- border: 3px solid rgba(255,255,255,0.3);
- border-radius: 50%;
- border-top-color: white;
- animation: spin 0.8s linear infinite;
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- .loading-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0,0,0,0.5);
- display: none;
- justify-content: center;
- align-items: center;
- z-index: 9999;
- }
- .loading-overlay.active {
- display: flex;
- }
- .loading-content {
- background: white;
- padding: 30px 40px;
- border-radius: 15px;
- text-align: center;
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
- }
- .loading-spinner {
- width: 50px;
- height: 50px;
- border: 4px solid #f0f0f0;
- border-top-color: #f5576c;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- margin: 0 auto 15px;
- }
-
- /* Toast notifications */
- .toast {
- position: fixed;
- top: 20px;
- right: 20px;
- background: white;
- padding: 16px 24px;
- border-radius: 10px;
- box-shadow: 0 8px 24px rgba(0,0,0,0.2);
- display: flex;
- align-items: center;
- gap: 12px;
- z-index: 10000;
- transform: translateX(400px);
- transition: transform 0.3s ease-out;
- max-width: 350px;
- }
- .toast.show {
- transform: translateX(0);
- }
- .toast.error {
- border-left: 4px solid #dc3545;
- }
- .toast.success {
- border-left: 4px solid #28a745;
- }
- .toast.info {
- border-left: 4px solid #17a2b8;
- }
- .toast-icon {
- font-size: 1.5em;
- flex-shrink: 0;
- }
- .toast-message {
- flex: 1;
- color: #333;
- line-height: 1.4;
- }
- .toast-close {
- background: none;
- border: none;
- color: #888;
- cursor: pointer;
- font-size: 1.2em;
- padding: 0;
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- flex-shrink: 0;
- }
- .toast-close:hover {
- background: #f0f0f0;
- color: #333;
- }
- .toast-close:focus {
- outline: 2px solid #f5576c;
- }
-
- .chat-container {
- height: 500px;
- display: flex;
- flex-direction: column;
- }
- .chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 20px;
- background: #f8f9fa;
- border-radius: 10px;
- margin-bottom: 20px;
- }
- .message {
- margin-bottom: 15px;
- display: flex;
- gap: 10px;
- }
- .message.user {
- flex-direction: row-reverse;
- }
- .message-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.2em;
- flex-shrink: 0;
- }
- .message.user .message-avatar {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- }
- .message.assistant .message-avatar {
- background: #e0e0e0;
- }
- .message-content {
- max-width: 70%;
- padding: 12px 16px;
- border-radius: 12px;
- line-height: 1.5;
- }
- .message.user .message-content {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message.assistant .message-content {
- background: white;
- color: #1a1a1a;
- border-bottom-left-radius: 4px;
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- #chatInput {
- flex: 1;
- padding: 12px 16px;
- border: 2px solid #e0e0e0;
- border-radius: 25px;
- font-size: 1em;
- outline: none;
- color: #1a1a1a;
- }
- #chatInput:focus {
- border-color: #f5576c;
- box-shadow: 0 0 0 3px rgba(245, 87, 108, 0.1);
- }
- .send-btn {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- color: white;
- border: none;
- padding: 12px 30px;
- border-radius: 25px;
- cursor: pointer;
- font-weight: 600;
- transition: all 0.2s;
- }
- .send-btn:hover:not(:disabled) {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(245, 87, 108, 0.3);
- }
- .send-btn:focus {
- outline: 3px solid #f5576c;
- outline-offset: 2px;
- }
- .send-btn:active:not(:disabled) {
- transform: translateY(0);
- }
- .send-btn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- .product-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- gap: 15px;
- max-height: 400px;
- overflow-y: auto;
- }
- .product-card {
- border: 2px solid #e0e0e0;
- border-radius: 10px;
- padding: 15px;
- cursor: pointer;
- transition: all 0.3s;
- }
- .product-card:hover {
- border-color: #f5576c;
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(0,0,0,0.1);
- }
- .product-card:focus {
- outline: 3px solid #f5576c;
- outline-offset: 2px;
- }
- .product-card.selected {
- border-color: #f5576c;
- background: #fff5f7;
- }
- .product-image {
- width: 100%;
- height: 150px;
- object-fit: cover;
- border-radius: 8px;
- margin-bottom: 10px;
- }
- .product-title {
- font-weight: 600;
- font-size: 0.9em;
- margin-bottom: 5px;
- color: #1a1a1a;
- }
- .product-sku {
- font-size: 0.8em;
- color: #555;
- }
- .blog-list {
- max-height: 400px;
- overflow-y: auto;
- }
- .blog-item {
- padding: 15px;
- border-bottom: 1px solid #e0e0e0;
- cursor: pointer;
- transition: background 0.2s;
- }
- .blog-item:hover {
- background: #f8f9fa;
- }
- .blog-item:focus {
- outline: 2px solid #f5576c;
- outline-offset: -2px;
- background: #f8f9fa;
- }
- .blog-title {
- font-weight: 600;
- color: #1a1a1a;
- margin-bottom: 5px;
- }
- .blog-meta {
- font-size: 0.85em;
- color: #555;
- }
- .btn {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- color: white;
- border: none;
- padding: 12px 24px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 15px;
- transition: all 0.2s;
- }
- .btn:hover:not(:disabled) {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(245, 87, 108, 0.3);
- }
- .btn:focus {
- outline: 3px solid #d8194e;
- outline-offset: 2px;
- }
- .btn:active:not(:disabled) {
- transform: translateY(0);
- }
- .btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .suggestions {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- margin-bottom: 15px;
- }
- .suggestion-chip {
- background: #f0f0f0;
- padding: 8px 15px;
- border-radius: 20px;
- font-size: 0.85em;
- cursor: pointer;
- transition: all 0.2s;
- color: #333;
- }
- .suggestion-chip:hover {
- background: #f5576c;
- color: white;
- }
- .suggestion-chip:focus {
- outline: 2px solid #f5576c;
- outline-offset: 2px;
- }
-
- /* Mobile responsive */
- @media (max-width: 1024px) {
- .container {
- grid-template-columns: 1fr;
- }
- .sidebar {
- position: fixed;
- left: 0;
- top: 0;
- bottom: 0;
- width: 300px;
- background: white;
- z-index: 1000;
- padding: 20px;
- overflow-y: auto;
- transform: translateX(-100%);
- transition: transform 0.3s ease-out;
- box-shadow: 2px 0 8px rgba(0,0,0,0.1);
- }
- .sidebar.open {
- transform: translateX(0);
- }
- .sidebar-toggle {
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .sidebar-overlay {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0,0,0,0.5);
- z-index: 999;
- }
- .sidebar-overlay.active {
- display: block;
- }
- .main-content {
- margin-top: 70px;
- }
- .tab {
- font-size: 0.9em;
- padding: 12px 10px;
- }
- .message-content {
- max-width: 85%;
- }
- }
-
- @media (max-width: 640px) {
- body {
- padding: 10px;
- }
- .tab-content {
- padding: 20px 15px;
- }
- .chat-container {
- height: 400px;
- }
- .product-grid {
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
- gap: 10px;
- }
- .toast {
- right: 10px;
- left: 10px;
- max-width: none;
- }
- }
- </style>
-</head>
-<body>
- <!-- Mobile sidebar toggle -->
- <button class="sidebar-toggle" onclick="toggleSidebar()" aria-label="Toggle menu">☰</button>
- <div class="sidebar-overlay" onclick="toggleSidebar()"></div>
-
- <!-- Loading overlay -->
- <div class="loading-overlay" id="loadingOverlay">
- <div class="loading-content">
- <div class="loading-spinner"></div>
- <div style="color: #333; font-weight: 600;">Processing...</div>
- </div>
- </div>
-
- <div class="container">
- <div class="sidebar" id="sidebar">
- <div class="header">
- <h1>📢 Marketing Agent</h1>
- <div class="subtitle">Content Creation & Campaigns</div>
- <div style="margin-top: 15px;">
- <a href="/logout" style="color: #f5576c; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-card">
- <h3>Blog Posts Created</h3>
- <div class="value" id="blogPostsCreated">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Social Posts</h3>
- <div class="value" id="socialPostsGenerated">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Email Campaigns</h3>
- <div class="value" id="emailCampaignsDrafted">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Conversations</h3>
- <div class="value" id="totalConversations">0</div>
- </div>
- </div>
-
- <div class="main-content">
- <div class="tab-container">
- <div class="tabs">
- <div class="tab active" onclick="switchTab('chat')" tabindex="0" role="tab" aria-selected="true" onkeypress="if(event.key==='Enter') switchTab('chat')">💬 Chat Assistant</div>
- <div class="tab" onclick="switchTab('blogs')" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('blogs')">📝 Blog Posts</div>
- <div class="tab" onclick="switchTab('create')" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('create')">✨ Create from SKU</div>
- <div class="tab" onclick="switchTab('social')" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('social')">📱 Social Media</div>
- </div>
-
- <!-- Chat Tab -->
- <div class="tab-content active" id="chat-content">
- <h2 style="color: #f5576c; margin-bottom: 15px;">Chat with Marketing Agent</h2>
- <div class="suggestions">
- <div class="suggestion-chip" onclick="sendSuggestion('Show me recent blog posts')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Show me recent blog posts')">📝 Recent blogs</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Create a blog post from a new product')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Create a blog post from a new product')">✨ Create blog</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Generate Instagram post')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Generate Instagram post')">📸 Instagram post</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Draft email campaign')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Draft email campaign')">📧 Email campaign</div>
- </div>
- <div class="chat-container">
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- <div class="message-avatar">🤖</div>
- <div class="message-content">
- Hi! I'm your Marketing Agent. I can help you:<br><br>
- • Create blog posts from new SKUs<br>
- • View and manage Shopify blog posts<br>
- • Generate social media content<br>
- • Draft email campaigns<br>
- • Write compelling product descriptions<br>
- <br>
- What would you like to create today?
- </div>
- </div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- id="chatInput"
- placeholder="Ask me to create content, view blogs, or generate posts..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-btn" onclick="sendMessage()" id="sendBtn">Send</button>
- </div>
- </div>
- </div>
-
- <!-- Blog Posts Tab -->
- <div class="tab-content" id="blogs-content">
- <h2 style="color: #f5576c; margin-bottom: 15px;">Recent Blog Posts from Shopify</h2>
- <button class="btn" onclick="loadBlogs()">🔄 Refresh Blog List</button>
- <div class="blog-list" id="blogList">
- <p style="color: #888; padding: 20px;">Click "Refresh Blog List" to load recent posts from Shopify...</p>
- </div>
- </div>
-
- <!-- Create from SKU Tab -->
- <div class="tab-content" id="create-content">
- <h2 style="color: #f5576c; margin-bottom: 15px;">Create Blog Post from Product</h2>
- <button class="btn" onclick="loadProducts()">🔄 Load Recent Products</button>
- <p style="color: #666; margin: 15px 0;">Select a product below to generate a blog post:</p>
- <div class="product-grid" id="productGrid">
- <p style="color: #888; padding: 20px; grid-column: 1/-1;">Click "Load Recent Products" to see available SKUs...</p>
- </div>
- <button class="btn" id="generateBlogBtn" style="display:none;" onclick="generateBlog()">✨ Generate Blog Post</button>
- </div>
-
- <!-- Social Media Tab -->
- <div class="tab-content" id="social-content">
- <h2 style="color: #f5576c; margin-bottom: 15px;">Social Media Content Generator</h2>
- <p style="color: #666; margin-bottom: 20px;">Generate social media posts for Instagram, Facebook, and more!</p>
- <button class="btn" onclick="generateSocial('instagram')">📸 Generate Instagram Post</button>
- <button class="btn" onclick="generateSocial('facebook')">👍 Generate Facebook Post</button>
- <button class="btn" onclick="generateSocial('pinterest')">📌 Generate Pinterest Description</button>
- <div id="socialOutput" style="margin-top: 20px; padding: 20px; background: #f8f9fa; border-radius: 10px; display: none;">
- <h3 style="color: #f5576c; margin-bottom: 10px;">Generated Content:</h3>
- <pre id="socialContent" style="white-space: pre-wrap; font-family: inherit; color: #333;"></pre>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <script>
- let selectedProduct = null;
-
- // Toast notification system
- function showToast(message, type = 'info') {
- const existingToast = document.querySelector('.toast');
- if (existingToast) {
- existingToast.remove();
- }
-
- const toast = document.createElement('div');
- toast.className = \`toast \${type}\`;
-
- const icons = {
- success: '✓',
- error: '✕',
- info: 'ℹ'
- };
-
- toast.innerHTML = \`
- <div class="toast-icon">\${icons[type] || icons.info}</div>
- <div class="toast-message">\${message}</div>
- <button class="toast-close" onclick="this.parentElement.remove()" aria-label="Close">×</button>
- \`;
-
- document.body.appendChild(toast);
-
- setTimeout(() => toast.classList.add('show'), 10);
-
- setTimeout(() => {
- toast.classList.remove('show');
- setTimeout(() => toast.remove(), 300);
- }, 5000);
- }
-
- // Loading overlay functions
- function showLoading() {
- document.getElementById('loadingOverlay').classList.add('active');
- }
-
- function hideLoading() {
- document.getElementById('loadingOverlay').classList.remove('active');
- }
-
- // Mobile sidebar toggle
- function toggleSidebar() {
- const sidebar = document.getElementById('sidebar');
- const overlay = document.querySelector('.sidebar-overlay');
- sidebar.classList.toggle('open');
- overlay.classList.toggle('active');
- }
-
- function switchTab(tab) {
- // Update tab states
- document.querySelectorAll('.tab').forEach(t => {
- t.classList.remove('active');
- t.setAttribute('aria-selected', 'false');
- });
- document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
-
- // Activate selected tab
- event.target.classList.add('active');
- event.target.setAttribute('aria-selected', 'true');
- document.getElementById(tab + '-content').classList.add('active');
-
- // Close sidebar on mobile after tab switch
- if (window.innerWidth <= 1024) {
- const sidebar = document.getElementById('sidebar');
- const overlay = document.querySelector('.sidebar-overlay');
- if (sidebar.classList.contains('open')) {
- sidebar.classList.remove('open');
- overlay.classList.remove('active');
- }
- }
- }
-
- function updateStats() {
- fetch('/api/stats')
- .then(r => r.json())
- .then(data => {
- document.getElementById('blogPostsCreated').textContent = data.blogPostsCreated;
- document.getElementById('socialPostsGenerated').textContent = data.socialPostsGenerated;
- document.getElementById('emailCampaignsDrafted').textContent = data.emailCampaignsDrafted;
- document.getElementById('totalConversations').textContent = data.totalConversations;
- });
- }
-
- function addMessage(role, content) {
- const messagesDiv = document.getElementById('chatMessages');
- const messageDiv = document.createElement('div');
- messageDiv.className = \`message \${role}\`;
-
- const avatar = document.createElement('div');
- avatar.className = 'message-avatar';
- avatar.textContent = role === 'user' ? '👤' : '🤖';
-
- const contentDiv = document.createElement('div');
- contentDiv.className = 'message-content';
- contentDiv.innerHTML = content.replace(/\\n/g, '<br>');
-
- messageDiv.appendChild(avatar);
- messageDiv.appendChild(contentDiv);
- messagesDiv.appendChild(messageDiv);
-
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const sendBtn = document.getElementById('sendBtn');
- const message = input.value.trim();
-
- if (!message) return;
-
- addMessage('user', message);
- input.value = '';
- sendBtn.disabled = true;
- sendBtn.innerHTML = '<span class="spinner"></span>';
- showLoading();
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
-
- const data = await response.json();
- addMessage('assistant', data.response);
- updateStats();
-
- } catch (error) {
- console.error('Chat error:', error);
- addMessage('assistant', 'Sorry, I encountered an error. Please try again.');
- showToast('Failed to send message. Please try again.', 'error');
- } finally {
- hideLoading();
- sendBtn.disabled = false;
- sendBtn.textContent = 'Send';
- }
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function loadBlogs() {
- const blogList = document.getElementById('blogList');
- blogList.innerHTML = '<p style="padding: 20px; color: #888;">Loading blog posts...</p>';
- showLoading();
-
- try {
- const response = await fetch('/api/shopify/blogs');
-
- if (!response.ok) {
- throw new Error('Failed to fetch blogs');
- }
-
- const data = await response.json();
-
- if (data.blogs && data.blogs.length > 0) {
- blogList.innerHTML = data.blogs.map(blog => \`
- <div class="blog-item" tabindex="0">
- <div class="blog-title">\${blog.title}</div>
- <div class="blog-meta">
- Created: \${new Date(blog.createdAt).toLocaleDateString()} |
- \${blog.publishedAt ? 'Published' : 'Draft'}
- \${blog.tags ? ' | Tags: ' + blog.tags : ''}
- </div>
- </div>
- \`).join('');
- showToast(\`Loaded \${data.blogs.length} blog posts\`, 'success');
- } else {
- blogList.innerHTML = '<p style="padding: 20px; color: #888;">No blog posts found.</p>';
- showToast('No blog posts found', 'info');
- }
- } catch (error) {
- console.error('Error loading blogs:', error);
- blogList.innerHTML = '<p style="padding: 20px; color: #dc3545;">Error loading blogs. Please try again.</p>';
- showToast('Failed to load blog posts', 'error');
- } finally {
- hideLoading();
- }
- }
-
- async function loadProducts() {
- const productGrid = document.getElementById('productGrid');
- productGrid.innerHTML = '<p style="padding: 20px; color: #888; grid-column: 1/-1;">Loading products...</p>';
- showLoading();
-
- try {
- const response = await fetch('/api/shopify/products');
-
- if (!response.ok) {
- throw new Error('Failed to fetch products');
- }
-
- const data = await response.json();
-
- if (data.products && data.products.length > 0) {
- productGrid.innerHTML = data.products.map(product => \`
- <div class="product-card" tabindex="0" onclick="selectProduct('\${product.id}', '\${product.sku}', '\${product.title}', '\${product.imageUrl}')" onkeypress="if(event.key==='Enter') selectProduct('\${product.id}', '\${product.sku}', '\${product.title}', '\${product.imageUrl}')">
- <img src="\${product.imageUrl || 'https://via.placeholder.com/200'}" class="product-image" alt="\${product.title}">
- <div class="product-title">\${product.title}</div>
- <div class="product-sku">SKU: \${product.sku}</div>
- </div>
- \`).join('');
- showToast(\`Loaded \${data.products.length} products\`, 'success');
- } else {
- productGrid.innerHTML = '<p style="padding: 20px; color: #888; grid-column: 1/-1;">No recent products found.</p>';
- showToast('No products found', 'info');
- }
- } catch (error) {
- console.error('Error loading products:', error);
- productGrid.innerHTML = '<p style="padding: 20px; color: #dc3545; grid-column: 1/-1;">Error loading products. Please try again.</p>';
- showToast('Failed to load products', 'error');
- } finally {
- hideLoading();
- }
- }
-
- function selectProduct(id, sku, title, imageUrl) {
- document.querySelectorAll('.product-card').forEach(card => card.classList.remove('selected'));
- event.currentTarget.classList.add('selected');
- selectedProduct = { id, sku, title, imageUrl };
- document.getElementById('generateBlogBtn').style.display = 'block';
- }
-
- async function generateBlog() {
- if (!selectedProduct) return;
-
- const btn = document.getElementById('generateBlogBtn');
- btn.disabled = true;
- btn.innerHTML = '<span class="spinner"></span> Generating...';
- showLoading();
-
- try {
- const response = await fetch('/api/generate/blog', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(selectedProduct)
- });
-
- if (!response.ok) {
- throw new Error('Failed to generate blog');
- }
-
- const data = await response.json();
-
- if (data.success) {
- showToast(\`Blog post created: "\${data.blog.title}"\`, 'success');
- updateStats();
- setTimeout(() => {
- switchTab('blogs');
- loadBlogs();
- }, 500);
- } else {
- throw new Error(data.error || 'Unknown error');
- }
-
- } catch (error) {
- console.error('Error generating blog:', error);
- showToast('Failed to generate blog post. Please try again.', 'error');
- } finally {
- hideLoading();
- btn.disabled = false;
- btn.textContent = '✨ Generate Blog Post';
- }
- }
-
- async function generateSocial(platform) {
- const output = document.getElementById('socialOutput');
- const content = document.getElementById('socialContent');
-
- output.style.display = 'block';
- content.textContent = 'Generating ' + platform + ' content...';
- showLoading();
-
- try {
- const response = await fetch('/api/generate/social', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ platform })
- });
-
- if (!response.ok) {
- throw new Error('Failed to generate social content');
- }
-
- const data = await response.json();
-
- if (data.success) {
- content.textContent = data.content;
- updateStats();
- showToast(\`Generated \${platform} post successfully\`, 'success');
- } else {
- throw new Error('Generation failed');
- }
-
- } catch (error) {
- console.error('Error generating social content:', error);
- content.textContent = 'Error generating content. Please try again.';
- showToast('Failed to generate social content', 'error');
- } finally {
- hideLoading();
- }
- }
-
- // Update stats every 10 seconds
- setInterval(updateStats, 10000);
- updateStats();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.post('/api/chat', requireAuth, async (req, res) => {
-
- // Memory system commands
- if (message.toLowerCase().match(/^(remember|note|preference|learning):/i)) {
- const parts = message.split(':');
- const type = parts[0].toLowerCase();
- const content = parts.slice(1).join(':').trim();
- const memType = type === 'remember' ? 'note' : type as any;
- agentMemory.add(content, memType);
- return res.json({
- response: `✅ Memory saved: "${content}"`,
- success: true,
- memoryAdded: true
- });
- }
-
- if (message.toLowerCase().includes('show') && message.toLowerCase().includes('memor')) {
- const summary = agentMemory.getContextSummary();
- return res.json({ response: summary, success: true });
- }
-
- if (message.toLowerCase().includes('search memor')) {
- const query = message.split(/for|about/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- return res.json({
- response: results.length > 0
- ? 'Found memories:\n' + results.map(m => `- ${m.content}`).join('\n')
- : 'No matching memories found.',
- success: true
- });
- }
-
- if (message.toLowerCase().includes('forget') || message.toLowerCase().includes('delete memor')) {
- const query = message.split(/forget|delete/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- if (results.length > 0) {
- agentMemory.delete(results[0].id);
- return res.json({ response: `✅ Forgot: "${results[0].content}"`, success: true });
- }
- return res.json({ response: 'Nothing found to forget.', success: true });
- }
-
- try {
- const { message } = req.body;
-
- if (!message) {
- return res.status(400).json({ error: 'Message is required' });
- }
-
- stats.totalConversations++;
-
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- req.session.chatHistory.push({ role: 'user', content: message });
-
- const systemPrompt = `You are the Marketing Agent for Designer Wallcoverings.
-
-Your responsibilities:
-1. Create compelling blog posts from product SKUs
-2. Generate social media content (Instagram, Facebook, Pinterest)
-3. Draft email marketing campaigns
-4. Write engaging product descriptions
-5. Provide marketing strategy advice
-
-You have access to:
-- Shopify store with products and blog posts
-- Recent product additions and SKUs
-- Constant Contact for email campaigns
-
-Current statistics:
-- Blog posts created: ${stats.blogPostsCreated}
-- Social posts generated: ${stats.socialPostsGenerated}
-- Email campaigns: ${stats.emailCampaignsDrafted}
-
-Recent blog posts:
-${stats.recentBlogs.length > 0 ? stats.recentBlogs.slice(-3).map(blog =>
- '- "' + blog.title + '" (created ' + new Date(blog.createdAt).toLocaleDateString() + ')'
-).join('\\n') : '- No recent blogs'}
-
-${agentMemory.getContextSummary()}
-
-When asked to create content:
-1. Be creative and engaging
-2. Use compelling headlines
-3. Include SEO keywords naturally
-4. Focus on benefits and style trends
-5. Use designer wallcovering terminology
-6. Reference stored memories and preferences when relevant
-
-Be enthusiastic, creative, and marketing-focused in your responses.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 1024,
- system: systemPrompt,
- messages: req.session.chatHistory
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error.';
-
- req.session.chatHistory.push({ role: 'assistant', content: assistantMessage });
-
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- res.json({ response: assistantMessage });
-
- } catch (error) {
- console.error('Chat error:', error);
- res.status(500).json({
- error: 'Failed to process message',
- response: 'I apologize, but I encountered an error. Please try again.'
- });
- }
-});
-
-// Shopify blog posts
-app.get('/api/shopify/blogs', requireAuth, async (req, res) => {
- try {
- const query = `
- query {
- articles(first: 10, sortKey: CREATED_AT, reverse: true) {
- edges {
- node {
- id
- title
- createdAt
- publishedAt
- tags
- handle
- }
- }
- }
- }
- `;
-
- const data = await shopifyGraphQL(query);
-
- if (data.data && data.data.articles) {
- const blogs = data.data.articles.edges.map((edge: any) => ({
- id: edge.node.id,
- title: edge.node.title,
- createdAt: edge.node.createdAt,
- publishedAt: edge.node.publishedAt,
- tags: edge.node.tags.join(', '),
- handle: edge.node.handle
- }));
-
- stats.recentBlogs = blogs;
- res.json({ blogs });
- } else {
- res.json({ blogs: [] });
- }
-
- } catch (error) {
- console.error('Error fetching blogs:', error);
- res.status(500).json({ error: 'Failed to fetch blogs' });
- }
-});
-
-// Shopify products
-app.get('/api/shopify/products', requireAuth, async (req, res) => {
- try {
- const query = `
- query {
- products(first: 20, sortKey: CREATED_AT, reverse: true) {
- edges {
- node {
- id
- title
- handle
- vendor
- variants(first: 1) {
- edges {
- node {
- sku
- }
- }
- }
- featuredImage {
- url
- }
- }
- }
- }
- }
- `;
-
- const data = await shopifyGraphQL(query);
-
- if (data.data && data.data.products) {
- const products = data.data.products.edges.map((edge: any) => ({
- id: edge.node.id,
- title: edge.node.title,
- handle: edge.node.handle,
- vendor: edge.node.vendor,
- sku: edge.node.variants.edges[0]?.node.sku || 'N/A',
- imageUrl: edge.node.featuredImage?.url || ''
- }));
-
- stats.recentProducts = products;
- res.json({ products });
- } else {
- res.json({ products: [] });
- }
-
- } catch (error) {
- console.error('Error fetching products:', error);
- res.status(500).json({ error: 'Failed to fetch products' });
- }
-});
-
-// Generate blog post from product
-app.post('/api/generate/blog', requireAuth, async (req, res) => {
- try {
- const { id, sku, title, imageUrl } = req.body;
-
- // Use Claude to generate blog content
- const prompt = `Create a compelling blog post for this wallcovering product:
-
-Product Title: ${title}
-SKU: ${sku}
-
-Write an engaging blog post (300-500 words) that includes:
-1. An attention-grabbing headline
-2. Introduction highlighting the product's unique features
-3. Design trends and styling suggestions
-4. Room application ideas
-5. Call-to-action
-
-Make it SEO-friendly and engaging for interior designers and homeowners.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 2048,
- messages: [{ role: 'user', content: prompt }]
- });
-
- const blogContent = response.content[0].type === 'text' ? response.content[0].text : '';
-
- // Extract title (first line) and body
- const lines = blogContent.split('\n');
- const blogTitle = lines[0].replace(/^#+ /, '').trim();
- const blogBody = lines.slice(1).join('\n').trim();
-
- stats.blogPostsCreated++;
- stats.lastBlogPost = {
- title: blogTitle,
- sku,
- timestamp: new Date()
- };
-
- // In production, would create blog post via Shopify API
- res.json({
- success: true,
- blog: {
- title: blogTitle,
- content: blogBody,
- sku,
- productId: id
- }
- });
-
- } catch (error) {
- console.error('Error generating blog:', error);
- res.status(500).json({ error: 'Failed to generate blog post' });
- }
-});
-
-// Generate social media content
-app.post('/api/generate/social', requireAuth, async (req, res) => {
- try {
- const { platform } = req.body;
-
- const prompts: Record<string, string> = {
- instagram: 'Write an engaging Instagram caption (150 chars) for a new designer wallcovering collection. Include emojis and 5 relevant hashtags.',
- facebook: 'Write a Facebook post (200 chars) announcing a new wallcovering line. Make it informative and include a call-to-action.',
- pinterest: 'Write a Pinterest pin description (300 chars) for wallcovering design inspiration. Focus on style and room transformation.'
- };
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 512,
- messages: [{ role: 'user', content: prompts[platform] || prompts.instagram }]
- });
-
- const content = response.content[0].type === 'text' ? response.content[0].text : '';
-
- stats.socialPostsGenerated++;
-
- res.json({
- success: true,
- platform,
- content
- });
-
- } catch (error) {
- console.error('Error generating social content:', error);
- res.status(500).json({ error: 'Failed to generate social content' });
- }
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('📢 Marketing Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('💬 Chat Mode: Active');
- console.log('📝 Shopify Blog Integration: Ready');
- console.log('✨ AI Content Generation: Active');
- console.log('📱 Social Media Tools: Ready');
- console.log('📧 Email Campaigns: Ready');
- console.log('');
- console.log('✅ Marketing Agent ready for content creation...');
-});
diff --git a/DW-Agents/dw-agents/agent-purchasing-office/purchasing-office-agent.ts.backup-auth-1762615744 b/DW-Agents/dw-agents/agent-purchasing-office/purchasing-office-agent.ts.backup-auth-1762615744
deleted file mode 100644
index f80c5766..00000000
--- a/DW-Agents/dw-agents/agent-purchasing-office/purchasing-office-agent.ts.backup-auth-1762615744
+++ /dev/null
@@ -1,1470 +0,0 @@
-/**
- * DW-Agents: Purchasing (Office Supplies) Agent
- *
- * Manages office supply ordering with intelligent price comparison:
- * 1. Chat interface for supply requests and order tracking
- * 2. Amazon Personal & Business account integration
- * 3. Price comparison across vendors
- * 4. Order tracking and inventory management
- * 5. Automatic restock recommendations
- *
- * Port: 9880
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import session from 'express-session';
-import fetch from 'node-fetch';
-import * as fs from 'fs';
-import * as cheerio from 'cheerio';
-import { fetchRealAmazonOrders, fetchRealInventory, saveOrder, saveInventory } from './amazon-real-orders';
-const { getUniversalHeader } = require('../shared-ui-components.js');
-
-const app = express();
-const PORT = 9880;
-
-// Authentication
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-// Amazon configuration
-const AMAZON_PERSONAL_EMAIL = 'nataliaabrams@gmail.com';
-const AMAZON_PERSONAL_PASSWORD = 'Urbanon341';
-const AMAZON_BUSINESS_EMAIL = 'info@designerwallcoverings.com';
-const AMAZON_BUSINESS_PASSWORD = '*Amazonaccess911*';
-
-// Anthropic Claude
-const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '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';
-const anthropic = new Anthropic({
- apiKey: ANTHROPIC_API_KEY,
-});
-
-// Stats tracking - LOAD REAL DATA
-let stats = {
- totalQueries: 0,
- priceComparisons: 0,
- ordersTracked: 0,
- suppliesManaged: 0,
- lastQuery: null as { question: string; timestamp: Date } | null,
- recentOrders: [] as Array<{
- id: string;
- item: string;
- vendor: string;
- price: number;
- status: string;
- timestamp: Date;
- }>,
- inventory: [] as Array<{
- item: string;
- quantity: number;
- reorderLevel: number;
- lastOrdered: Date | null;
- }>
-};
-
-// Load real data on startup
-async function loadRealData() {
- console.log('📦 Loading real Amazon data...');
-
- try {
- const orders = await fetchRealAmazonOrders();
- const inventory = await fetchRealInventory();
-
- stats.recentOrders = orders;
- stats.inventory = inventory;
- stats.ordersTracked = orders.length;
- stats.suppliesManaged = inventory.length;
-
- console.log(`✅ Loaded ${orders.length} orders and ${inventory.length} inventory items`);
-
- if (orders.length === 0) {
- console.log('\n⚠️ NO ORDERS FOUND - Add real data:');
- console.log(' 📝 Edit: /root/DW-Agents/data/amazon-orders.json');
- }
-
- if (inventory.length === 0) {
- console.log('\n⚠️ NO INVENTORY - Add items:');
- console.log(' 📝 Edit: /root/DW-Agents/data/office-inventory.json');
- }
-
- } catch (error) {
- console.error('❌ Error loading real data:', error);
- }
-}
-
-// Load data immediately
-loadRealData();
-
-// Chat history per session
-const chatSessions: Map<string, Array<{ role: 'user' | 'assistant', content: string }>> = new Map();
-
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-purchasing-office-agent-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-// Load top deals from price tracking
-function loadTopDeals(): string {
- try {
- const priceTrackingPath = '/root/DW-Agents/data/price-tracking.json';
- if (fs.existsSync(priceTrackingPath)) {
- const tracking = JSON.parse(fs.readFileSync(priceTrackingPath, 'utf-8'));
- const deals = tracking
- .filter((t: any) => t.priceChange < 0)
- .sort((a: any, b: any) => a.percentChange - b.percentChange)
- .slice(0, 4);
-
- if (deals.length === 0) {
- return '';
- }
-
- const lastChecked = deals[0]?.lastChecked ? new Date(deals[0].lastChecked) : new Date();
- const hoursAgo = Math.floor((Date.now() - lastChecked.getTime()) / (1000 * 60 * 60));
- const timeText = hoursAgo < 1 ? 'Just now' : hoursAgo === 1 ? '1 hour ago' : `${hoursAgo} hours ago`;
- const exactTime = lastChecked.toLocaleString('en-US', {
- month: 'short',
- day: 'numeric',
- hour: 'numeric',
- minute: '2-digit',
- hour12: true
- });
-
- const dealItems = deals.map((deal: any) => {
- const itemShort = deal.item.substring(0, 40) + (deal.item.length > 40 ? '...' : '');
- const savings = Math.abs(deal.priceChange).toFixed(2);
- const percent = Math.abs(deal.percentChange).toFixed(0);
- return `
- <a href="${deal.url}" target="_blank" class="deal-item" style="text-decoration: none; color: white;" title="Click to search on Amazon - Prices may vary">
- <strong>${itemShort}</strong>
- <span>Was $${deal.originalPrice.toFixed(2)} → $${deal.currentPrice.toFixed(2)}</span>
- <span class="deal-savings">SAVE $${savings} (${percent}% OFF)</span>
- </a>
- `;
- }).join('');
-
- return `
- <div class="deals-banner" id="dealsBanner">
- <button onclick="refreshDeals()" class="refresh-deals-btn" id="refreshBtn">
- 🔄 REFRESH NOW
- </button>
- <div style="display: flex; align-items: center; gap: 15px; flex-shrink: 0; padding: 0 15px; border-right: 2px solid rgba(255,255,255,0.3);">
- <div class="deals-banner-title">🔥 DEALS ALERT!</div>
- <div style="font-size: 0.75em; opacity: 0.9; white-space: nowrap;">
- ⏰ <strong>${exactTime}</strong><br>
- <span style="font-size: 0.9em;">(${timeText})</span>
- </div>
- </div>
- ${dealItems}
- </div>
- `;
- }
- } catch (e) {
- // Ignore if file doesn't exist yet
- }
- return '';
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // TEMPORARY: Authentication disabled for 4 hours (until 01:02:12 on 2025-11-08)
- return next();
-
- /* Original auth code (will be restored):
- if (req.session.authenticated) {
- next();
- } else {
- // For API requests, return JSON instead of redirecting
- if (req.path.startsWith('/api/')) {
- res.status(401).json({ error: 'Not authenticated. Please refresh the page and login.' });
- } else {
- res.redirect('/login');
- }
- }
- */
-};
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Purchasing Agent</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #667eea; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>🛒 Purchasing Agent</h1>
- <p style="text-align: center; color: #666; margin-bottom: 20px;">Office Supplies</p>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard with chat
-app.get('/', requireAuth, (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Purchasing (Office Supplies) - DW-Agents</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
- <meta http-equiv="Pragma" content="no-cache">
- <meta http-equiv="Expires" content="0">
- <!-- v2.1 - Deals Banner with Refresh -->
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .container {
- max-width: 1800px;
- margin: 0 auto;
- display: grid;
- grid-template-columns: 280px 1fr 400px;
- gap: 20px;
- }
- .sidebar {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #667eea; font-size: 2em; margin-bottom: 10px; }
- .header .subtitle { color: #666; font-size: 1.1em; }
- .stats-card {
- background: white;
- padding: 20px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stats-card h3 { color: #888; font-size: 0.85em; text-transform: uppercase; margin-bottom: 8px; }
- .stats-card .value { font-size: 2em; font-weight: bold; color: #667eea; }
- .chat-container {
- background: white;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- display: flex;
- flex-direction: column;
- height: calc(100vh - 40px);
- }
- .chat-header {
- padding: 20px;
- border-bottom: 2px solid #f0f0f0;
- }
- .chat-header h2 { color: #667eea; margin-bottom: 5px; }
- .chat-header p { color: #888; font-size: 0.9em; }
- .chat-messages {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- background: #f8f9fa;
- }
- .message {
- margin-bottom: 15px;
- display: flex;
- gap: 10px;
- }
- .message.user {
- flex-direction: row-reverse;
- }
- .message-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.2em;
- flex-shrink: 0;
- }
- .message.user .message-avatar {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- }
- .message.assistant .message-avatar {
- background: #e0e0e0;
- }
- .message-content {
- max-width: 70%;
- padding: 12px 16px;
- border-radius: 12px;
- line-height: 1.5;
- }
- .message.user .message-content {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message.assistant .message-content {
- background: white;
- color: #333;
- border-bottom-left-radius: 4px;
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
- }
- .chat-input-area {
- padding: 20px;
- border-top: 2px solid #f0f0f0;
- background: white;
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- #chatInput {
- flex: 1;
- padding: 12px 16px;
- border: 2px solid #e0e0e0;
- border-radius: 25px;
- font-size: 1em;
- outline: none;
- }
- #chatInput:focus {
- border-color: #667eea;
- }
- .send-btn {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border: none;
- padding: 12px 30px;
- border-radius: 25px;
- cursor: pointer;
- font-weight: 600;
- transition: transform 0.2s;
- }
- .send-btn:hover {
- transform: translateY(-2px);
- }
- .send-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .suggestions {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- margin-top: 10px;
- }
- .suggestion-chip {
- background: #f0f0f0;
- padding: 8px 15px;
- border-radius: 20px;
- font-size: 0.85em;
- cursor: pointer;
- transition: all 0.2s;
- }
- .suggestion-chip:hover {
- background: #667eea;
- color: white;
- }
- .typing-indicator {
- display: none;
- padding: 10px;
- color: #888;
- font-style: italic;
- }
- .typing-indicator.active {
- display: block;
- }
- .price-comparison {
- background: #f8f9fa;
- border: 1px solid #e0e0e0;
- border-radius: 8px;
- padding: 10px;
- margin-top: 10px;
- }
- .price-item {
- display: flex;
- justify-content: space-between;
- padding: 8px 0;
- border-bottom: 1px solid #e0e0e0;
- }
- .price-item:last-child {
- border-bottom: none;
- }
- .vendor-name {
- font-weight: 600;
- color: #667eea;
- }
- .price {
- font-weight: bold;
- color: #28a745;
- }
- .inventory-panel {
- background: white;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- display: flex;
- flex-direction: column;
- height: calc(100vh - 40px);
- overflow: hidden;
- }
- .inventory-header {
- padding: 20px;
- border-bottom: 2px solid #f0f0f0;
- }
- .inventory-header h2 { color: #667eea; margin-bottom: 5px; }
- .inventory-header p { color: #888; font-size: 0.9em; }
- .inventory-list {
- flex: 1;
- padding: 15px;
- overflow-y: auto;
- }
- .inventory-item {
- background: #f8f9fa;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
- padding: 15px;
- margin-bottom: 12px;
- transition: all 0.2s;
- }
- .inventory-item:hover {
- box-shadow: 0 3px 10px rgba(102, 126, 234, 0.2);
- transform: translateY(-2px);
- }
- .inventory-item-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- }
- .inventory-item-name {
- font-weight: 600;
- color: #333;
- font-size: 0.95em;
- }
- .inventory-item-quantity {
- background: #667eea;
- color: white;
- padding: 4px 10px;
- border-radius: 12px;
- font-size: 0.85em;
- font-weight: 600;
- }
- .inventory-item-quantity.low {
- background: #ff4757;
- }
- .inventory-item-details {
- color: #666;
- font-size: 0.85em;
- margin-bottom: 10px;
- }
- .find-price-btn {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border: none;
- padding: 10px 16px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- font-size: 0.9em;
- width: 100%;
- transition: transform 0.2s;
- }
- .find-price-btn:hover {
- transform: translateY(-2px);
- }
- .find-price-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .orders-bottom-panel {
- position: fixed;
- bottom: 0;
- left: 280px;
- right: 0;
- background: white;
- border-top: 2px solid #e0e0e0;
- box-shadow: 0 -3px 10px rgba(0,0,0,0.1);
- z-index: 1000;
- max-height: 160px;
- overflow-y: auto;
- }
- .orders-bottom-header {
- padding: 10px 20px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- font-weight: 600;
- font-size: 0.95em;
- position: sticky;
- top: 0;
- z-index: 10;
- }
- .orders-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.85em;
- }
- .orders-table th {
- background: #f8f9fa;
- padding: 8px 12px;
- text-align: left;
- font-weight: 600;
- color: #667eea;
- border-bottom: 2px solid #e0e0e0;
- position: sticky;
- top: 35px;
- z-index: 5;
- }
- .orders-table td {
- padding: 8px 12px;
- border-bottom: 1px solid #f0f0f0;
- color: #333;
- }
- .orders-table tr:hover {
- background: #f8f9fa;
- cursor: pointer;
- }
- .order-status {
- padding: 3px 8px;
- border-radius: 10px;
- font-size: 0.8em;
- font-weight: 600;
- }
- .container {
- padding-bottom: 160px;
- padding-top: 80px;
- }
- .deals-banner {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- background: linear-gradient(135deg, #ff4757 0%, #d63031 100%);
- color: white;
- padding: 15px 30px;
- box-shadow: 0 3px 15px rgba(255,71,87,0.4);
- z-index: 2000;
- display: flex;
- align-items: center;
- gap: 20px;
- overflow-x: auto;
- white-space: nowrap;
- }
- .deals-banner-title {
- font-weight: 800;
- font-size: 1.1em;
- text-transform: uppercase;
- letter-spacing: 1px;
- flex-shrink: 0;
- animation: pulse 2s infinite;
- }
- @keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.8; }
- }
- .deal-item {
- display: inline-flex;
- align-items: center;
- gap: 10px;
- background: rgba(255,255,255,0.2);
- padding: 8px 15px;
- border-radius: 20px;
- font-size: 0.9em;
- flex-shrink: 0;
- cursor: pointer;
- transition: all 0.3s ease;
- border: 2px solid transparent;
- }
- .deal-item:hover {
- background: rgba(255,255,255,0.35);
- transform: translateY(-2px);
- border: 2px solid white;
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
- }
- .deal-item strong {
- font-weight: 700;
- }
- .deal-savings {
- background: white;
- color: #d63031;
- padding: 3px 10px;
- border-radius: 12px;
- font-weight: 700;
- font-size: 0.85em;
- }
- .refresh-deals-btn {
- background: rgba(255,255,255,0.25);
- color: white;
- border: 2px solid white;
- padding: 10px 20px;
- border-radius: 25px;
- font-weight: 700;
- font-size: 0.85em;
- cursor: pointer;
- transition: all 0.3s ease;
- flex-shrink: 0;
- margin-right: 15px;
- }
- .refresh-deals-btn:hover {
- background: white;
- color: #d63031;
- transform: scale(1.05);
- }
- .refresh-deals-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- </style>
-</head>
-<body style="padding: 20px;">
- ${getUniversalHeader('Purchasing Office', 9880, 5)}
-
- ${loadTopDeals()}
- <div class="container" style="margin-top: 20px;">
- <div class="sidebar">
- <div class="header">
- <h1>🛒 Purchasing Agent</h1>
- <div class="subtitle">Office Supplies</div>
- <div style="margin-top: 15px;">
- <a href="/logout" style="color: #667eea; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-card">
- <h3>Total Queries</h3>
- <div class="value" id="totalQueries">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Price Comparisons</h3>
- <div class="value" id="priceComparisons">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Orders Tracked</h3>
- <div class="value" id="ordersTracked">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Supplies Managed</h3>
- <div class="value" id="suppliesManaged">0</div>
- </div>
- </div>
-
- <div class="chat-container">
- <div class="chat-header">
- <h2>💬 Chat with Purchasing Agent</h2>
- <p>Ask about office supplies, pricing, orders, or inventory</p>
- </div>
-
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- <div class="message-avatar">🤖</div>
- <div class="message-content">
- Hi! I'm your Office Supplies Purchasing Agent. I can help you:
- <br><br>
- • Find and compare prices on office supplies<br>
- • Search Amazon Personal & Business accounts<br>
- • Track orders and shipments<br>
- • Manage inventory and reorder alerts<br>
- • Source the best deals<br>
- <br>
- <strong style="color: #28a745;">✅ REAL DATA MODE</strong> - No mock data!<br>
- Currently tracking <strong>${stats.ordersTracked}</strong> real orders and <strong>${stats.suppliesManaged}</strong> inventory items<br>
- <br>
- What can I help you with today?
- </div>
- </div>
- </div>
-
- <div class="typing-indicator" id="typingIndicator">
- Agent is typing...
- </div>
-
- <div class="chat-input-area">
- <div class="suggestions">
- <div class="suggestion-chip" onclick="sendSuggestion('Compare prices for printer paper')">🖨️ Printer paper prices</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Check inventory status')">📦 Check inventory</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Track recent orders')">🚚 Track orders</div>
- <div class="suggestion-chip" onclick="sendSuggestion('What office supplies are low in stock?')">⚠️ Low stock alerts</div>
- <div class="suggestion-chip" onclick="researchFurther()" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 600;">🔍 Research Further</div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- id="chatInput"
- placeholder="Ask about office supplies, pricing, or orders..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-btn" onclick="sendMessage()" id="sendBtn">Send</button>
- </div>
- </div>
- </div>
-
- <div class="inventory-panel">
- <div class="inventory-header">
- <h2>📦 Inventory Items</h2>
- <p>Click to find best prices</p>
- </div>
- <div class="inventory-list" id="inventoryList">
- ${stats.inventory.length > 0 ? stats.inventory.map((item, idx) => `
- <div class="inventory-item">
- <div class="inventory-item-header">
- <div class="inventory-item-name">${item.item}</div>
- <div class="inventory-item-quantity ${item.quantity < item.reorderLevel ? 'low' : ''}">${item.quantity} units</div>
- </div>
- <div class="inventory-item-details">
- Reorder Level: ${item.reorderLevel} units<br>
- Last Ordered: ${item.lastOrdered ? new Date(item.lastOrdered).toLocaleDateString() : 'Never'}
- </div>
- <button class="find-price-btn" onclick="findBestPrice('${item.item}', ${idx})">
- 🔍 FIND BEST PRICE
- </button>
- </div>
- `).join('') : `
- <div style="padding: 40px; text-align: center; color: #888;">
- <div style="font-size: 3em; margin-bottom: 20px;">📦</div>
- <h3 style="color: #667eea; margin-bottom: 10px;">No Inventory Yet</h3>
- <p style="margin-bottom: 20px;">Add inventory items to track office supplies</p>
- <p style="font-size: 0.9em; background: #fff3cd; padding: 15px; border-radius: 8px; color: #856404;">
- <strong>📝 NO MOCK DATA</strong><br>
- Edit: /root/DW-Agents/data/office-inventory.json
- </p>
- </div>
- `}
- </div>
- </div>
- </div>
-
- <div class="orders-bottom-panel">
- <div class="orders-bottom-header">
- 📦 Currently Tracking ${stats.ordersTracked} Real Orders
- </div>
- <table class="orders-table">
- <thead>
- <tr>
- <th>Date</th>
- <th>Product</th>
- <th>Price</th>
- <th>Vendor</th>
- <th>Status</th>
- <th>Order ID</th>
- </tr>
- </thead>
- <tbody>
- ${stats.recentOrders.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).map(order => `
- <tr onclick="sendSuggestion('Tell me about order ${order.id}')">
- <td>${new Date(order.timestamp).toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' })}</td>
- <td style="max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${order.item}</td>
- <td style="font-weight: 600; color: #28a745;">$${order.price.toFixed(2)}</td>
- <td>${order.vendor}</td>
- <td>
- <span class="order-status">${order.status === 'Delivered' ? '✅' : order.status === 'In Transit' ? '🚚' : '⏳'} ${order.status}</span>
- </td>
- <td style="font-family: monospace; font-size: 0.8em; color: #888;">${order.id}</td>
- </tr>
- `).join('')}
- </tbody>
- </table>
- </div>
-
- <script>
- function updateStats() {
- fetch('/api/stats')
- .then(r => r.json())
- .then(data => {
- document.getElementById('totalQueries').textContent = data.totalQueries;
- document.getElementById('priceComparisons').textContent = data.priceComparisons;
- document.getElementById('ordersTracked').textContent = data.ordersTracked;
- document.getElementById('suppliesManaged').textContent = data.suppliesManaged;
- });
- }
-
- async function refreshDeals() {
- const btn = document.getElementById('refreshBtn');
- const banner = document.getElementById('dealsBanner');
-
- btn.disabled = true;
- btn.innerHTML = '⏳ Refreshing prices...';
-
- try {
- const response = await fetch('/api/refresh-deals', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- const data = await response.json();
-
- if (data.success) {
- if (data.amazonBlocked) {
- // Amazon blocked scraping - show warning
- btn.innerHTML = '⚠️ Amazon Blocked';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- // Add message to chat
- const messagesDiv = document.getElementById('chatMessages');
- if (messagesDiv) {
- addMessage('assistant', '⚠️ Amazon is currently blocking automated price checks. Showing last cached prices. The "Find Best Price" buttons will search Amazon directly.');
- }
- }, 3000);
- } else if (data.dealsFound) {
- // Found new deals - reload page
- btn.innerHTML = '✅ New deals found! Reloading...';
- setTimeout(() => {
- location.reload();
- }, 1000);
- } else {
- // No new deals but refresh succeeded
- btn.innerHTML = '✅ Up to date - No new deals';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- }, 2000);
- }
- } else {
- btn.innerHTML = '❌ Failed - Try again';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- }, 2000);
- }
- } catch (error) {
- btn.innerHTML = '❌ Error - Try again';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- }, 2000);
- }
- }
-
- function addMessage(role, content) {
- const messagesDiv = document.getElementById('chatMessages');
- const messageDiv = document.createElement('div');
- messageDiv.className = \`message \${role}\`;
-
- const avatar = document.createElement('div');
- avatar.className = 'message-avatar';
- avatar.textContent = role === 'user' ? '👤' : '🤖';
-
- const contentDiv = document.createElement('div');
- contentDiv.className = 'message-content';
- contentDiv.innerHTML = content.replace(/\\n/g, '<br>');
-
- messageDiv.appendChild(avatar);
- messageDiv.appendChild(contentDiv);
- messagesDiv.appendChild(messageDiv);
-
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const sendBtn = document.getElementById('sendBtn');
- const typingIndicator = document.getElementById('typingIndicator');
- const message = input.value.trim();
-
- if (!message) return;
-
- // Add user message
- addMessage('user', message);
- input.value = '';
- sendBtn.disabled = true;
- typingIndicator.classList.add('active');
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
-
- typingIndicator.classList.remove('active');
- addMessage('assistant', data.response);
- updateStats();
-
- } catch (error) {
- typingIndicator.classList.remove('active');
- addMessage('assistant', 'Sorry, I encountered an error. Please try again.');
- }
-
- sendBtn.disabled = false;
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function researchFurther() {
- const messagesDiv = document.getElementById('chatMessages');
- const typingIndicator = document.getElementById('typingIndicator');
-
- addMessage('user', '🔍 Research further on recent topics and provide deeper insights');
- typingIndicator.classList.add('active');
-
- try {
- const response = await fetch('/api/research', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- const data = await response.json();
-
- typingIndicator.classList.remove('active');
- addMessage('assistant', data.research);
- updateStats();
- } catch (error) {
- typingIndicator.classList.remove('active');
- addMessage('assistant', 'Sorry, I encountered an error while researching. Please try again.');
- }
- }
-
- async function findBestPrice(itemName, itemIndex) {
- const typingIndicator = document.getElementById('typingIndicator');
- const button = event.target;
-
- button.disabled = true;
- button.textContent = '🔍 Searching...';
-
- addMessage('user', \`Find best price for: \${itemName}\`);
- typingIndicator.classList.add('active');
-
- try {
- const response = await fetch('/api/find-price', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ itemName, itemIndex })
- });
-
- const data = await response.json();
-
- typingIndicator.classList.remove('active');
-
- // Check for authentication error
- if (response.status === 401) {
- addMessage('assistant', \`🔐 Your session has expired. Please refresh the page to login again.\`);
- return;
- }
-
- if (data.priceInfo) {
- addMessage('assistant', data.priceInfo);
- } else if (data.error) {
- addMessage('assistant', \`❌ Error: \${data.error}\`);
- } else {
- addMessage('assistant', 'Unable to fetch prices. Please try again.');
- }
-
- updateStats();
- } catch (error) {
- typingIndicator.classList.remove('active');
- addMessage('assistant', \`❌ Network error: \${error.message}. Please refresh the page and try again.\`);
- console.error('Find price error:', error);
- } finally {
- button.disabled = false;
- button.textContent = '🔍 FIND BEST PRICE';
- }
- }
-
- // Update stats every 10 seconds
- setInterval(updateStats, 10000);
- updateStats();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.post('/api/chat', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- if (!message) {
- return res.status(400).json({ error: 'Message is required' });
- }
-
- stats.totalQueries++;
- stats.lastQuery = { question: message, timestamp: new Date() };
-
- // Initialize chat history for session
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- // Add user message to history
- req.session.chatHistory.push({
- role: 'user',
- content: message
- });
-
- // Build system prompt with detailed data
- const systemPrompt = `You are the Purchasing (Office Supplies) Agent for Designer Wallcoverings.
-
-Your responsibilities:
-1. Help with office supply inquiries and price comparisons
-2. Search and compare prices across Amazon Personal and Amazon Business accounts
-3. Track orders and shipments with dates and tracking numbers
-4. Manage inventory levels and alert on low stock
-5. Source the best deals for office supplies
-
-Available integrations:
-- Amazon Personal Account (linked)
-- Amazon Business Account (linked)
-- Order tracking system
-- Inventory management database
-
-CURRENT INVENTORY (${stats.inventory.length} items):
-${stats.inventory.length > 0 ? stats.inventory.map(item => {
- const lastOrderedDate = item.lastOrdered ? new Date(item.lastOrdered).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'Never';
- const status = item.quantity < item.reorderLevel ? '⚠️ LOW STOCK' : '✅ OK';
- return `- ${item.item}
- • Current: ${item.quantity} units ${status}
- • Reorder at: ${item.reorderLevel} units
- • Last ordered: ${lastOrderedDate}`;
-}).join('\n') : '- No inventory tracked yet'}
-
-ALL ORDERS (${stats.recentOrders.length} total) - Sorted by date:
-${stats.recentOrders.length > 0 ? stats.recentOrders.slice().sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).map((order) => {
- const orderDate = new Date(order.timestamp).toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' });
- const status = order.status === 'Delivered' ? '✅' : order.status === 'In Transit' ? '🚚' : '⏳';
- return `${orderDate} | ${order.item} | $${order.price.toFixed(2)} | ${order.vendor} | ${status}`;
-}).join('\n') : '- No orders'}
-
-When user asks about:
-- "last order" or "recent orders": Show the most recent orders with ALL details (date, price, status, tracking)
-- "show dates": Display order dates and last ordered dates from inventory
-- "last price": Show the most recent price paid for that item from order history
-- "when did we order": Look through order history and show the date
-- "what's low": List items below reorder level with last ordered date
-- "track order": Show full order details including tracking number and status
-
-IMPORTANT FORMATTING:
-- Always show dates in format: "Oct 28, 2025"
-- Always show prices with dollar sign: "$42.99"
-- Always include order status with emoji
-- When showing orders, include: Date, Item, Price, Status, Tracking
-- When showing inventory, include: Quantity, Last Ordered Date, Reorder Level
-
-Be detailed, clear, and always include dates and prices when relevant.`;
-
- // Call Claude API
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 2048, // Increased for detailed responses with dates/prices
- system: systemPrompt,
- messages: req.session.chatHistory.map(msg => ({
- role: msg.role,
- content: msg.content
- }))
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error processing your request.';
-
- // Add assistant response to history
- req.session.chatHistory.push({
- role: 'assistant',
- content: assistantMessage
- });
-
- // Keep only last 20 messages
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- // Update stats based on message content
- if (message.toLowerCase().includes('price') || message.toLowerCase().includes('compare')) {
- stats.priceComparisons++;
- }
- if (message.toLowerCase().includes('order') || message.toLowerCase().includes('track')) {
- stats.ordersTracked++;
- }
-
- res.json({
- response: assistantMessage,
- stats: {
- totalQueries: stats.totalQueries,
- priceComparisons: stats.priceComparisons
- }
- });
-
- } catch (error: any) {
- console.error('❌ Chat error:', error.message || error);
- console.error('Error details:', error);
- res.status(500).json({
- error: 'Failed to process message',
- response: `Error: ${error.message || 'Unknown error'}. Please try again or check logs.`
- });
- }
-});
-
-// Research endpoint
-app.post('/api/research', requireAuth, async (req, res) => {
- try {
- stats.totalQueries++;
-
- // Build comprehensive research prompt
- const researchPrompt = `Provide comprehensive research and insights on office supply purchasing for Designer Wallcoverings.
-
-Focus on:
-1. Current market trends in office supplies
-2. Best suppliers and vendors for 2025
-3. Cost-saving strategies and bulk purchasing opportunities
-4. Emerging products and technologies
-5. Sustainable office supply options
-6. Amazon Business vs Personal account optimization
-7. Inventory management best practices
-
-Recent context:
-- Total queries: ${stats.totalQueries}
-- Orders tracked: ${stats.ordersTracked}
-- Recent orders: ${stats.recentOrders.slice(-3).map(o => o.item).join(', ') || 'None yet'}
-
-Provide actionable insights and specific recommendations.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 2048,
- messages: [{
- role: 'user',
- content: researchPrompt
- }]
- });
-
- const research = response.content[0].type === 'text'
- ? response.content[0].text
- : 'Unable to generate research at this time.';
-
- res.json({ research });
-
- } catch (error) {
- console.error('Research error:', error);
- res.status(500).json({
- research: 'I apologize, but I encountered an error while researching. Please try again.'
- });
- }
-});
-
-// Find best price endpoint
-app.post('/api/find-price', requireAuth, async (req, res) => {
- try {
- const { itemName, itemIndex } = req.body;
-
- if (!itemName) {
- return res.status(400).json({ error: 'Item name is required' });
- }
-
- stats.priceComparisons++;
- stats.totalQueries++;
-
- console.log(`🔍 Finding REAL prices for: ${itemName}`);
-
- // ONLY scrape real data - NO ESTIMATES!
- const vendors = [];
-
- // Scrape Amazon
- try {
- const amazonUrl = `https://www.amazon.com/s?k=${encodeURIComponent(itemName)}`;
- const response = await fetch(amazonUrl, {
- headers: {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
- 'Accept': 'text/html,application/xhtml+xml',
- 'Accept-Language': 'en-US,en;q=0.5'
- }
- });
-
- if (response.ok) {
- const html = await response.text();
- const $ = cheerio.load(html);
-
- // Extract ALL products from first page
- $('div[data-component-type="s-search-result"]').each((idx, element) => {
- if (idx >= 3) return false; // Get top 3 results
-
- try {
- const $el = $(element);
- const title = $el.find('h2 a span').first().text().trim();
- const priceWhole = $el.find('.a-price-whole').first().text().replace(/[^0-9]/g, '');
- const priceFraction = $el.find('.a-price-fraction').first().text().replace(/[^0-9]/g, '');
- const productUrl = $el.find('h2 a').attr('href');
-
- if (priceWhole && title) {
- const price = parseFloat(`${priceWhole}.${priceFraction || '00'}`);
- const fullUrl = `https://www.amazon.com${productUrl}`;
-
- vendors.push({
- name: 'Amazon',
- product: title.substring(0, 80),
- price: price.toFixed(2),
- url: fullUrl + (fullUrl.includes('?') ? '&' : '?') + 'tag=designerwal0b-20'
- });
- console.log(` ✅ Found: ${title.substring(0, 40)}... - $${price}`);
- }
- } catch (e) {
- // Skip this product
- }
- });
- }
- } catch (e) {
- console.log(` ❌ Amazon scrape failed:`, e.message);
- }
-
- // Sort by price (lowest first)
- vendors.sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
-
- let priceInfo = '';
-
- if (vendors.length === 0) {
- priceInfo = `❌ Unable to fetch live prices from Amazon right now.\n\n`;
- priceInfo += `Please try these direct search links:\n\n`;
- priceInfo += `🛒 Amazon: https://www.amazon.com/s?k=${encodeURIComponent(itemName)}&tag=designerwal0b-20\n`;
- priceInfo += `📎 Staples: https://www.staples.com/s?searchKey=${encodeURIComponent(itemName)}\n`;
- priceInfo += `🏢 Office Depot: https://www.officedepot.com/catalog/search.do?Ntt=${encodeURIComponent(itemName)}\n`;
- priceInfo += `🏪 Costco: https://www.costco.com/s?dept=All&keyword=${encodeURIComponent(itemName)}\n`;
-
- res.json({ priceInfo });
- return;
- }
-
- // Build clean formatted response
- priceInfo = `🔍 LIVE PRICES FROM AMAZON\n`;
- priceInfo += `Search: ${itemName}\n\n`;
-
- vendors.forEach((vendor, idx) => {
- priceInfo += `${idx + 1}. $${vendor.price} - ${vendor.product}\n`;
- priceInfo += ` 🛒 ${vendor.url}\n\n`;
- });
-
- priceInfo += `💡 Best Price: $${vendors[0].price}\n`;
- priceInfo += `\nClick any link above to purchase with your affiliate tag!`;
-
- console.log(`✅ Found ${vendors.length} real prices`);
-
- res.json({ priceInfo });
-
- } catch (error) {
- console.error('Price search error:', error);
- res.status(500).json({
- priceInfo: 'I apologize, but I encountered an error while searching for prices. Please try again.'
- });
- }
-});
-
-// Order management endpoints
-app.post('/api/orders/add', requireAuth, (req, res) => {
- const { item, vendor, price } = req.body;
-
- const order = {
- id: `ORD-${Date.now()}`,
- item,
- vendor,
- price: parseFloat(price),
- status: 'Pending',
- timestamp: new Date()
- };
-
- stats.recentOrders.push(order);
- stats.ordersTracked++;
-
- res.json({ success: true, order });
-});
-
-app.get('/api/orders', requireAuth, (req, res) => {
- res.json(stats.recentOrders);
-});
-
-// Inventory management
-app.post('/api/inventory/add', requireAuth, (req, res) => {
- const { item, quantity, reorderLevel } = req.body;
-
- const inventoryItem = {
- item,
- quantity: parseInt(quantity),
- reorderLevel: parseInt(reorderLevel),
- lastOrdered: null
- };
-
- stats.inventory.push(inventoryItem);
- stats.suppliesManaged++;
-
- res.json({ success: true, item: inventoryItem });
-});
-
-app.get('/api/inventory', requireAuth, (req, res) => {
- res.json(stats.inventory);
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('🛒 Purchasing (Office Supplies) Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('💬 Chat Mode: Active');
- console.log('🛍️ Amazon Personal: Ready');
- console.log('🏢 Amazon Business: Ready');
- console.log('📦 Order Tracking: Active');
- console.log('📊 Inventory Management: Active');
- console.log('');
- console.log('✅ Purchasing Agent ready for queries...');
-});
-
-// Refresh deals endpoint - triggers daily-price-tracker.ts
-app.post('/api/refresh-deals', requireAuth, async (req, res) => {
- try {
- console.log('🔄 Manual price refresh triggered...');
-
- const { spawn } = require('child_process');
- const trackerProcess = spawn('npx', ['tsx', '/root/DW-Agents/daily-price-tracker.ts'], {
- cwd: '/root/DW-Agents',
- stdio: 'pipe'
- });
-
- let output = '';
- let errorOutput = '';
- let responseSent = false;
-
- trackerProcess.stdout.on('data', (data: Buffer) => {
- output += data.toString();
- console.log(data.toString().trim());
- });
-
- trackerProcess.stderr.on('data', (data: Buffer) => {
- errorOutput += data.toString();
- console.error(data.toString().trim());
- });
-
- // Timeout after 2 minutes
- const timeout = setTimeout(() => {
- if (responseSent) return;
- responseSent = true;
-
- trackerProcess.kill();
- res.json({
- success: false,
- message: 'Refresh timed out - too many items'
- });
- }, 120000);
-
- trackerProcess.on('close', (code: number) => {
- clearTimeout(timeout); // Clear timeout FIRST to prevent double response
- if (responseSent) return;
- responseSent = true;
-
- if (code === 0) {
- console.log('✅ Price refresh completed successfully');
-
- // Check if Amazon blocked all requests
- const amazonBlocked = output.includes('HTTP 503') || output.includes('No prices tracked');
- const dealsFound = output.includes('PRICE DROPS');
-
- res.json({
- success: true,
- message: amazonBlocked ? 'Amazon blocked scraping - showing cached prices' : 'Prices refreshed successfully',
- dealsFound: dealsFound,
- amazonBlocked: amazonBlocked
- });
- } else {
- console.error('❌ Price refresh failed with code:', code);
- res.json({
- success: false,
- message: 'Failed to refresh prices',
- error: errorOutput
- });
- }
- });
-
- } catch (error: any) {
- console.error('❌ Refresh error:', error);
- res.status(500).json({
- success: false,
- message: error.message
- });
- }
-});
diff --git a/DW-Agents/dw-agents/agent-security/security-agent.ts.backup-oauth-20251112 b/DW-Agents/dw-agents/agent-security/security-agent.ts.backup-oauth-20251112
deleted file mode 100644
index 01a47abc..00000000
--- a/DW-Agents/dw-agents/agent-security/security-agent.ts.backup-oauth-20251112
+++ /dev/null
@@ -1,871 +0,0 @@
-import cookieParser from "cookie-parser";
-/**
- * DW-Agents: Security Agent
- *
- * Server security monitoring and management system:
- * - System security status monitoring
- * - Security updates detection
- * - Firewall status monitoring
- * - Failed login attempt tracking
- * - SSL certificate monitoring
- * - Port scanning detection
- * - Automatic security issue reporting to Needs Attention agent
- * - Real-time security dashboard
- *
- * Port: 9892
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express, { Request, Response } from 'express';
-import session from 'express-session';
-import { exec } from 'child_process';
-import { promisify } from 'util';
-import * as fs from 'fs';
-import * as path from 'path';
-import fetch from 'node-fetch';
-
-import { chatMiddleware } from '../shared-chat-integration';
-
-const execAsync = promisify(exec);
-
-const app = express();
-
-// Global Authentication
-const PORT = 9892;
-
-// Global Authentication - MUST come before any other middleware
-app.use(cookieParser());
-
-// Add inter-agent chat widget (after auth)
-app.use(chatMiddleware({
- agentId: 'agent-security',
- agentName: 'Security',
- port: 9892,
- category: 'agent'
-}));
-
-
-// Authentication
-const AUTH_USERNAME = 'admin2025';
-const AUTH_PASSWORD = 'Otis';
-
-// Anthropic AI client
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY || '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',
-});
-
-// Middleware
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
-
-// Session configuration
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-app.use(
- session({
- secret: 'dw-agents-security-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
- },
- })
-);
-
-// Authentication middleware
-const requireAuth = (req: Request, res: Response, next: any) => {
- if (req.session.authenticated) {
- return next();
- }
- res.redirect('/login');
-};
-
-// Security status tracking
-interface SecurityIssue {
- id: string;
- timestamp: string;
- severity: 'critical' | 'high' | 'medium' | 'low';
- category: string;
- title: string;
- description: string;
- recommendation: string;
- resolved: boolean;
- reportedToUrgent: boolean;
-}
-
-interface SecurityStats {
- totalIssues: number;
- criticalIssues: number;
- highIssues: number;
- mediumIssues: number;
- lowIssues: number;
- resolvedIssues: number;
- lastScan: Date;
- systemUptime: string;
- failedLogins: number;
- activeConnections: number;
-}
-
-let securityIssues: SecurityIssue[] = [];
-let stats: SecurityStats = {
- totalIssues: 0,
- criticalIssues: 0,
- highIssues: 0,
- mediumIssues: 0,
- lowIssues: 0,
- resolvedIssues: 0,
- lastScan: new Date(),
- systemUptime: '0 days',
- failedLogins: 0,
- activeConnections: 0,
-};
-
-// Load/save issues
-const ISSUES_FILE = path.join(__dirname, 'security-issues.json');
-
-function loadIssues() {
- if (fs.existsSync(ISSUES_FILE)) {
- try {
- securityIssues = JSON.parse(fs.readFileSync(ISSUES_FILE, 'utf-8'));
- updateStats();
- } catch (e) {
- console.error('Error loading issues:', e);
- }
- }
-}
-
-function saveIssues() {
- fs.writeFileSync(ISSUES_FILE, JSON.stringify(securityIssues, null, 2));
-}
-
-function updateStats() {
- stats.totalIssues = securityIssues.length;
- stats.criticalIssues = securityIssues.filter(i => i.severity === 'critical' && !i.resolved).length;
- stats.highIssues = securityIssues.filter(i => i.severity === 'high' && !i.resolved).length;
- stats.mediumIssues = securityIssues.filter(i => i.severity === 'medium' && !i.resolved).length;
- stats.lowIssues = securityIssues.filter(i => i.severity === 'low' && !i.resolved).length;
- stats.resolvedIssues = securityIssues.filter(i => i.resolved).length;
-}
-
-loadIssues();
-
-// Report critical issues to Needs Attention agent
-async function reportToUrgent(issue: SecurityIssue) {
- try {
- const response = await fetch('http://45.61.58.125:9886/api/add-urgent', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- title: `🔒 Security: ${issue.title}`,
- description: issue.description,
- severity: issue.severity,
- category: 'security',
- source: 'Security Agent',
- }),
- });
-
- if (response.ok) {
- issue.reportedToUrgent = true;
- saveIssues();
- console.log(`✅ Reported issue ${issue.id} to Needs Attention agent`);
- }
- } catch (error) {
- console.error('Error reporting to Urgent agent:', error);
- }
-}
-
-// Security check functions
-async function checkSystemUpdates(): Promise<SecurityIssue | null> {
- try {
- const { stdout } = await execAsync('apt list --upgradable 2>/dev/null | grep -i security | wc -l');
- const securityUpdates = parseInt(stdout.trim());
-
- if (securityUpdates > 0) {
- return {
- id: `sec-updates-${Date.now()}`,
- timestamp: new Date().toISOString(),
- severity: securityUpdates > 10 ? 'high' : 'medium',
- category: 'updates',
- title: `${securityUpdates} Security Updates Available`,
- description: `There are ${securityUpdates} security updates available for the system.`,
- recommendation: 'Run: sudo apt update && sudo apt upgrade',
- resolved: false,
- reportedToUrgent: false,
- };
- }
- } catch (error) {
- console.error('Error checking updates:', error);
- }
- return null;
-}
-
-async function checkFirewallStatus(): Promise<SecurityIssue | null> {
- try {
- const { stdout } = await execAsync('sudo ufw status 2>/dev/null || echo "inactive"');
- if (stdout.toLowerCase().includes('inactive')) {
- return {
- id: `firewall-${Date.now()}`,
- timestamp: new Date().toISOString(),
- severity: 'high',
- category: 'firewall',
- title: 'Firewall is Inactive',
- description: 'The UFW firewall is not active, leaving the system exposed.',
- recommendation: 'Enable firewall: sudo ufw enable',
- resolved: false,
- reportedToUrgent: false,
- };
- }
- } catch (error) {
- // Firewall might not be installed
- }
- return null;
-}
-
-async function checkFailedLogins(): Promise<SecurityIssue | null> {
- try {
- const { stdout } = await execAsync('grep "Failed password" /var/log/auth.log 2>/dev/null | tail -100 | wc -l');
- const failedLogins = parseInt(stdout.trim());
- stats.failedLogins = failedLogins;
-
- if (failedLogins > 50) {
- return {
- id: `failed-logins-${Date.now()}`,
- timestamp: new Date().toISOString(),
- severity: failedLogins > 100 ? 'critical' : 'high',
- category: 'authentication',
- title: `${failedLogins} Recent Failed Login Attempts`,
- description: `Detected ${failedLogins} failed login attempts in recent logs. Possible brute force attack.`,
- recommendation: 'Review /var/log/auth.log and consider implementing fail2ban',
- resolved: false,
- reportedToUrgent: false,
- };
- }
- } catch (error) {
- // Auth log might not be accessible
- }
- return null;
-}
-
-async function checkSSLCertificates(): Promise<SecurityIssue | null> {
- try {
- const { stdout } = await execAsync('sudo certbot certificates 2>/dev/null | grep "Expiry Date" | head -1');
- if (stdout) {
- // Parse expiry date and check if within 30 days
- const match = stdout.match(/(\d{4}-\d{2}-\d{2})/);
- if (match) {
- const expiryDate = new Date(match[1]);
- const daysUntilExpiry = Math.floor((expiryDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
-
- if (daysUntilExpiry < 30) {
- return {
- id: `ssl-expiry-${Date.now()}`,
- timestamp: new Date().toISOString(),
- severity: daysUntilExpiry < 7 ? 'critical' : 'high',
- category: 'ssl',
- title: `SSL Certificate Expiring in ${daysUntilExpiry} Days`,
- description: `SSL certificate will expire on ${expiryDate.toDateString()}`,
- recommendation: 'Renew certificate: sudo certbot renew',
- resolved: false,
- reportedToUrgent: false,
- };
- }
- }
- }
- } catch (error) {
- // Certbot might not be installed
- }
- return null;
-}
-
-async function checkOpenPorts(): Promise<SecurityIssue | null> {
- try {
- const { stdout } = await execAsync('ss -tuln | grep LISTEN | wc -l');
- const openPorts = parseInt(stdout.trim());
- stats.activeConnections = openPorts;
-
- // Check for commonly exploited ports
- const { stdout: dangerousPorts } = await execAsync('ss -tuln | grep -E ":(23|21|445|3389|5900)" | wc -l');
- const dangerous = parseInt(dangerousPorts.trim());
-
- if (dangerous > 0) {
- return {
- id: `dangerous-ports-${Date.now()}`,
- timestamp: new Date().toISOString(),
- severity: 'critical',
- category: 'network',
- title: 'Dangerous Ports Open',
- description: `Detected ${dangerous} commonly exploited ports open (Telnet, FTP, SMB, RDP, VNC)`,
- recommendation: 'Close unnecessary ports and use secure alternatives',
- resolved: false,
- reportedToUrgent: false,
- };
- }
- } catch (error) {
- console.error('Error checking ports:', error);
- }
- return null;
-}
-
-async function checkSystemUptime() {
- try {
- const { stdout } = await execAsync('uptime -p');
- stats.systemUptime = stdout.trim().replace('up ', '');
- } catch (error) {
- console.error('Error checking uptime:', error);
- }
-}
-
-// Run security scan
-async function runSecurityScan() {
- console.log('🔍 Running security scan...');
-
- const checks = [
- checkSystemUpdates(),
- checkFirewallStatus(),
- checkFailedLogins(),
- checkSSLCertificates(),
- checkOpenPorts(),
- ];
-
- const results = await Promise.all(checks);
- await checkSystemUptime();
-
- for (const issue of results) {
- if (issue) {
- // Check if this issue already exists
- const existing = securityIssues.find(i => i.category === issue.category && !i.resolved);
- if (!existing) {
- securityIssues.push(issue);
- console.log(`⚠️ New security issue: ${issue.title}`);
-
- // Report critical and high severity issues to Needs Attention
- if (issue.severity === 'critical' || issue.severity === 'high') {
- await reportToUrgent(issue);
- }
- }
- }
- }
-
- stats.lastScan = new Date();
- updateStats();
- saveIssues();
-
- console.log(`✅ Security scan complete. Found ${stats.criticalIssues + stats.highIssues + stats.mediumIssues + stats.lowIssues} active issues.`);
-}
-
-// Run initial scan and schedule periodic scans
-runSecurityScan();
-setInterval(runSecurityScan, 60 * 60 * 1000); // Every hour
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Security Agent</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #DC143C 0%, #8B0000 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- padding: 20px;
- }
- .login-container {
- background: white;
- padding: 50px;
- border-radius: 20px;
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
- max-width: 400px;
- width: 100%;
- }
- .header {
- text-align: center;
- margin-bottom: 40px;
- }
- .header h1 {
- color: #DC143C;
- font-size: 2.5em;
- margin-bottom: 10px;
- }
- .header p {
- color: #666;
- font-size: 1.1em;
- }
- .form-group {
- margin-bottom: 25px;
- }
- label {
- display: block;
- color: #333;
- margin-bottom: 8px;
- font-weight: 600;
- }
- input {
- width: 100%;
- padding: 15px;
- border: 2px solid #e0e0e0;
- border-radius: 10px;
- font-size: 1em;
- transition: border-color 0.3s;
- }
- input:focus {
- outline: none;
- border-color: #DC143C;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #DC143C 0%, #8B0000 100%);
- color: white;
- border: none;
- padding: 18px;
- border-radius: 10px;
- font-size: 1.1em;
- cursor: pointer;
- font-weight: 600;
- transition: transform 0.2s, box-shadow 0.2s;
- }
- button:hover {
- transform: translateY(-2px);
- box-shadow: 0 10px 25px rgba(220, 20, 60, 0.3);
- }
- .error {
- background: #fee;
- border: 2px solid #f00;
- color: #c00;
- padding: 15px;
- border-radius: 10px;
- margin-bottom: 25px;
- text-align: center;
- font-weight: 500;
- }
- </style>
-</head>
-<body>
- <div class="login-container">
- <div class="header">
- <h1>🔒 Security Agent</h1>
- <p>System Security Monitor</p>
- </div>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST" action="/login">
- <div class="form-group">
- <label>Username</label>
- <input type="text" name="username" required autocomplete="username">
- </div>
- <div class="form-group">
- <label>Password</label>
- <input type="password" name="password" required autocomplete="current-password">
- </div>
- <button type="submit">Access Security Dashboard</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
- req.session.save((err) => {
- if (err) {
- console.error('Session save error:', err);
- }
- res.redirect('/');
- });
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => {
- res.redirect('/login');
- });
-});
-
-// Main dashboard
-app.get('/', requireAuth, (req, res) => {
- const unresolvedIssues = securityIssues.filter(i => !i.resolved).sort((a, b) => {
- const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
- return severityOrder[a.severity] - severityOrder[b.severity];
- });
-
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Security Agent - DW-Agents</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #DC143C 0%, #8B0000 100%);
- min-height: 100vh;
- padding: 30px;
- }
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
- .header {
- background: white;
- padding: 40px;
- border-radius: 20px;
- margin-bottom: 30px;
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .header h1 {
- color: #DC143C;
- font-size: 2.5em;
- margin-bottom: 10px;
- }
- .header .subtitle {
- color: #666;
- font-size: 1.2em;
- }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stat-card {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
- text-align: center;
- }
- .stat-card .icon {
- font-size: 3em;
- margin-bottom: 15px;
- }
- .stat-card .value {
- font-size: 2.5em;
- font-weight: bold;
- color: #333;
- margin-bottom: 10px;
- }
- .stat-card .label {
- color: #666;
- font-size: 1.1em;
- }
- .stat-card.critical { border-left: 6px solid #DC143C; }
- .stat-card.high { border-left: 6px solid #FF6B35; }
- .stat-card.medium { border-left: 6px solid #FFA500; }
- .stat-card.low { border-left: 6px solid #4CAF50; }
- .section {
- background: white;
- padding: 30px;
- border-radius: 20px;
- margin-bottom: 30px;
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
- }
- .section h2 {
- color: #DC143C;
- margin-bottom: 20px;
- font-size: 1.8em;
- }
- .issue-card {
- background: #f8f9fa;
- padding: 20px;
- border-radius: 10px;
- margin-bottom: 15px;
- border-left: 6px solid;
- }
- .issue-card.critical { border-left-color: #DC143C; }
- .issue-card.high { border-left-color: #FF6B35; }
- .issue-card.medium { border-left-color: #FFA500; }
- .issue-card.low { border-left-color: #4CAF50; }
- .issue-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- }
- .issue-title {
- font-weight: 600;
- font-size: 1.2em;
- color: #333;
- }
- .severity-badge {
- padding: 5px 15px;
- border-radius: 20px;
- font-size: 0.85em;
- font-weight: 600;
- text-transform: uppercase;
- }
- .severity-badge.critical {
- background: #fee;
- color: #DC143C;
- }
- .severity-badge.high {
- background: #fff3e0;
- color: #FF6B35;
- }
- .severity-badge.medium {
- background: #fff8e1;
- color: #FFA500;
- }
- .severity-badge.low {
- background: #e8f5e9;
- color: #4CAF50;
- }
- .issue-description {
- color: #666;
- margin-bottom: 10px;
- line-height: 1.6;
- }
- .issue-recommendation {
- background: white;
- padding: 10px;
- border-radius: 5px;
- font-family: 'Courier New', monospace;
- font-size: 0.9em;
- color: #333;
- }
- .btn {
- padding: 12px 30px;
- border: none;
- border-radius: 10px;
- font-size: 1em;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s;
- text-decoration: none;
- display: inline-block;
- }
- .btn-primary {
- background: linear-gradient(135deg, #DC143C 0%, #8B0000 100%);
- color: white;
- }
- .btn-primary:hover {
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(220, 20, 60, 0.3);
- }
- .btn-secondary {
- background: #f0f0f0;
- color: #333;
- }
- .btn-secondary:hover {
- background: #e0e0e0;
- }
- .btn-success {
- background: #4CAF50;
- color: white;
- }
- .no-issues {
- text-align: center;
- padding: 40px;
- color: #666;
- font-size: 1.2em;
- }
- .no-issues .icon {
- font-size: 5em;
- margin-bottom: 20px;
- }
- </style>
-</head>
-<body>
- <div class="container">
- <div class="header">
- <div>
- <h1>🔒 Security Agent</h1>
- <div class="subtitle">System Security Monitoring</div>
- <div style="margin-top: 10px; color: #888;">Last scan: ${stats.lastScan.toLocaleString()}</div>
- </div>
- <div>
- <a href="/logout" class="btn btn-secondary">Logout</a>
- <button onclick="runScan()" class="btn btn-primary" style="margin-left: 10px;">🔍 Run Scan Now</button>
- </div>
- </div>
-
- <div class="stats-grid">
- <div class="stat-card critical">
- <div class="icon">🚨</div>
- <div class="value">${stats.criticalIssues}</div>
- <div class="label">Critical Issues</div>
- </div>
- <div class="stat-card high">
- <div class="icon">⚠️</div>
- <div class="value">${stats.highIssues}</div>
- <div class="label">High Priority</div>
- </div>
- <div class="stat-card medium">
- <div class="icon">📋</div>
- <div class="value">${stats.mediumIssues}</div>
- <div class="label">Medium Priority</div>
- </div>
- <div class="stat-card low">
- <div class="icon">ℹ️</div>
- <div class="value">${stats.lowIssues}</div>
- <div class="label">Low Priority</div>
- </div>
- <div class="stat-card">
- <div class="icon">⏱️</div>
- <div class="value" style="font-size: 1.5em;">${stats.systemUptime}</div>
- <div class="label">System Uptime</div>
- </div>
- <div class="stat-card">
- <div class="icon">🔌</div>
- <div class="value">${stats.activeConnections}</div>
- <div class="label">Open Ports</div>
- </div>
- </div>
-
- <div class="section">
- <h2>⚠️ Active Security Issues</h2>
- ${unresolvedIssues.length === 0 ? `
- <div class="no-issues">
- <div class="icon">✅</div>
- <div>No active security issues detected!</div>
- <div style="font-size: 0.9em; margin-top: 10px;">System is secure and up to date.</div>
- </div>
- ` : unresolvedIssues.map(issue => `
- <div class="issue-card ${issue.severity}">
- <div class="issue-header">
- <div class="issue-title">${issue.title}</div>
- <span class="severity-badge ${issue.severity}">${issue.severity}</span>
- </div>
- <div class="issue-description">${issue.description}</div>
- <div class="issue-recommendation">
- <strong>💡 Recommendation:</strong> ${issue.recommendation}
- </div>
- <div style="margin-top: 15px;">
- <button onclick="resolveIssue('${issue.id}')" class="btn btn-success">Mark as Resolved</button>
- ${!issue.reportedToUrgent && (issue.severity === 'critical' || issue.severity === 'high') ? `
- <button onclick="reportToUrgent('${issue.id}')" class="btn btn-primary" style="margin-left: 10px;">
- 📢 Report to Urgent
- </button>
- ` : ''}
- </div>
- </div>
- `).join('')}
- </div>
- </div>
-
- <script>
- async function runScan() {
- try {
- const response = await fetch('/api/scan', { method: 'POST' });
- if (response.ok) {
- alert('Security scan initiated. Page will reload.');
- location.reload();
- }
- } catch (error) {
- alert('Error running scan');
- }
- }
-
- async function resolveIssue(id) {
- try {
- const response = await fetch(\`/api/issues/\${id}/resolve\`, { method: 'POST' });
- if (response.ok) {
- location.reload();
- }
- } catch (error) {
- alert('Error resolving issue');
- }
- }
-
- async function reportToUrgent(id) {
- try {
- const response = await fetch(\`/api/issues/\${id}/report\`, { method: 'POST' });
- if (response.ok) {
- alert('Issue reported to Needs Attention agent');
- location.reload();
- }
- } catch (error) {
- alert('Error reporting issue');
- }
- }
-
- // Auto-refresh every 5 minutes
- setTimeout(() => location.reload(), 5 * 60 * 1000);
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.post('/api/scan', requireAuth, async (req, res) => {
- runSecurityScan();
- res.json({ success: true, message: 'Security scan initiated' });
-});
-
-app.post('/api/issues/:id/resolve', requireAuth, (req, res) => {
- const issue = securityIssues.find(i => i.id === req.params.id);
- if (issue) {
- issue.resolved = true;
- updateStats();
- saveIssues();
- res.json({ success: true });
- } else {
- res.status(404).json({ error: 'Issue not found' });
- }
-});
-
-app.post('/api/issues/:id/report', requireAuth, async (req, res) => {
- const issue = securityIssues.find(i => i.id === req.params.id);
- if (issue) {
- await reportToUrgent(issue);
- res.json({ success: true });
- } else {
- res.status(404).json({ error: 'Issue not found' });
- }
-});
-
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.get('/api/issues', requireAuth, (req, res) => {
- res.json(securityIssues.filter(i => !i.resolved));
-});
-
-
-// Metrics endpoint for daily reporting
-app.get("/api/metrics", (req: Request, res: Response) => {
- res.json({
- status: "online",
- uptime: process.uptime(),
- responseTime: 0,
- tasksCompleted: 0,
- errorsToday: 0,
- lastActivity: new Date().toISOString(),
- qnaReadiness: 100
- });
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('🔒 DW-Agents: Security Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('✅ Security monitoring active');
- console.log('📊 Running security scans every hour');
- console.log('🚨 Auto-reporting critical/high issues to Needs Attention agent');
- console.log('');
-});
diff --git a/DW-Agents/dw-agents/agent-slack/slack-agent.cjs b/DW-Agents/dw-agents/agent-slack/slack-agent.cjs
index 0c917fa0..ccb9921b 100644
--- a/DW-Agents/dw-agents/agent-slack/slack-agent.cjs
+++ b/DW-Agents/dw-agents/agent-slack/slack-agent.cjs
@@ -14,7 +14,7 @@ const cookie_parser_1 = __importDefault(require("cookie-parser"));
const express_session_1 = __importDefault(require("express-session"));
const pg_1 = require("pg");
const PORT = parseInt(process.env.PORT || '9887');
-const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || 'xoxb-3958182050256-9852565284259-8kfXhJQWVsf515doMRI67AHQ';
+const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || '';
const SLACK_SIGNING_SECRET = process.env.SLACK_SIGNING_SECRET || 'b1b92b95a310728b7f2f26373da10ba3';
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK || 'https://hooks.slack.com/services/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';
const STEVE_USER_ID = 'U03TV3UC2V7';
diff --git a/DW-Agents/dw-agents/agent-zendesk-chat/zendesk-chat-agent.ts.backup-auth-1762615744 b/DW-Agents/dw-agents/agent-zendesk-chat/zendesk-chat-agent.ts.backup-auth-1762615744
deleted file mode 100644
index 09f1c6ba..00000000
--- a/DW-Agents/dw-agents/agent-zendesk-chat/zendesk-chat-agent.ts.backup-auth-1762615744
+++ /dev/null
@@ -1,1459 +0,0 @@
-/**
- * DW-Agents: Zendesk Chat Agent
- *
- * Customer support chat integration system
- * - Zendesk chat monitoring and management
- * - AI-powered response suggestions
- * - Customer conversation history
- * - Real-time chat analytics
- * - Chat interface for support queries
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express, { Request, Response } from 'express';
-import session from 'express-session';
-import cookieParser from 'cookie-parser';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import fetch from 'node-fetch';
-
-const app = express();
-const PORT = process.env.PORT || 9712;
-
-// Anthropic AI client
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY || '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',
-});
-
-// Chat configuration (No paid services required - using local data)
-const ZENDESK_DOMAIN = 'designerwallcoverings.zendesk.com';
-const ZENDESK_CHAT_URL = `https://${ZENDESK_DOMAIN}/chat/agent`;
-
-// Local chat data storage (replaces Zendesk API)
-const USE_DEMO_DATA = true; // Always use demo data since we're not paying for Zendesk
-
-// Middleware
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-// Session configuration with extended TypeScript types
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-app.use(
- session({
- secret: 'dw-agents-zendesk-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days - longer session to avoid re-login
- },
- })
-);
-
-// Authentication middleware with SSO support
-const requireAuth = (req: Request, res: Response, next: any) => {
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
-};
-
-// Statistics tracking
-const stats = {
- totalQueries: 0,
- activeChats: 0,
- resolvedChats: 0,
- avgResponseTime: '2.3 min',
- customerSatisfaction: '94%',
- lastActivity: new Date(),
-};
-
-// Sample chat data
-interface ChatMessage {
- sender: 'customer' | 'agent';
- message: string;
- timestamp: Date;
-}
-
-interface ChatConversation {
- id: string;
- zendeskId?: number;
- customerName: string;
- customerEmail: string;
- status: 'active' | 'pending' | 'resolved';
- priority: 'high' | 'normal' | 'low';
- subject: string;
- lastMessage: string;
- timestamp: Date;
- tags: string[];
- messages: ChatMessage[];
-}
-
-// Zendesk API helper functions
-async function fetchZendeskTickets(): Promise<ChatConversation[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?sort_by=updated_at&sort_order=desc&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk API error:', response.status, await response.text());
- return [];
- }
-
- const data: any = await response.json();
- const tickets = data.tickets || [];
-
- // Fetch users to get names and emails
- const userIds = [...new Set(tickets.map((t: any) => t.requester_id))];
- const users = await fetchZendeskUsers(userIds);
-
- const conversations: ChatConversation[] = tickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- zendeskId: ticket.id,
- customerName: user?.name || 'Unknown Customer',
- customerEmail: user?.email || 'unknown@email.com',
- status: ticket.status === 'new' || ticket.status === 'open' ? 'active' :
- ticket.status === 'pending' ? 'pending' : 'resolved',
- priority: ticket.priority === 'urgent' || ticket.priority === 'high' ? 'high' :
- ticket.priority === 'low' ? 'low' : 'normal',
- subject: ticket.subject || 'No Subject',
- lastMessage: ticket.description?.substring(0, 100) || 'No message',
- timestamp: new Date(ticket.updated_at),
- tags: ticket.tags || [],
- messages: [], // Will be loaded on demand
- };
- });
-
- return conversations;
- } catch (error) {
- console.error('Error fetching Zendesk tickets:', error);
- return [];
- }
-}
-
-async function fetchZendeskUsers(userIds: number[]): Promise<any[]> {
- try {
- if (userIds.length === 0) return [];
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/users/show_many.json?ids=${userIds.join(',')}`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk users API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- return data.users || [];
- } catch (error) {
- console.error('Error fetching Zendesk users:', error);
- return [];
- }
-}
-
-async function fetchTicketComments(ticketId: string): Promise<ChatMessage[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets/${ticketId}/comments.json`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk comments API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- const comments = data.comments || [];
-
- return comments.map((comment: any) => ({
- sender: comment.public ? 'agent' : 'customer',
- message: comment.body || '',
- timestamp: new Date(comment.created_at),
- }));
- } catch (error) {
- console.error('Error fetching ticket comments:', error);
- return [];
- }
-}
-
-// Fetch currently online visitors/chats from Zendesk Chat
-interface OnlineVisitor {
- id: string;
- name: string;
- email: string;
- status: 'browsing' | 'chatting' | 'waiting';
- pageUrl?: string;
- location?: string;
- timestamp: Date;
-}
-
-async function fetchOnlineVisitors(): Promise<OnlineVisitor[]> {
- try {
- // Zendesk Chat uses different auth - OAuth token
- // For now, we'll try to get active chats from the Support API
- // by looking for tickets created in the last hour with specific tags
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
-
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?created_at_gt=${oneHourAgo}&status=new,open&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk online visitors API error:', response.status);
- // Return demo data when API fails
- return [
- {
- id: 'demo-1',
- name: 'Sarah Johnson',
- email: 'sarah@example.com',
- status: 'chatting' as const,
- pageUrl: 'https://designerwallcoverings.com/wallpaper/floral',
- location: 'New York, USA',
- timestamp: new Date(Date.now() - 5 * 60 * 1000) // 5 min ago
- },
- {
- id: 'demo-2',
- name: 'Mike Chen',
- email: 'mike@example.com',
- status: 'waiting' as const,
- pageUrl: 'https://designerwallcoverings.com/samples',
- location: 'Los Angeles, USA',
- timestamp: new Date(Date.now() - 2 * 60 * 1000) // 2 min ago
- },
- {
- id: 'demo-3',
- name: 'Emily Rodriguez',
- email: 'emily@example.com',
- status: 'browsing' as const,
- pageUrl: 'https://designerwallcoverings.com/about',
- location: 'Chicago, USA',
- timestamp: new Date(Date.now() - 1 * 60 * 1000) // 1 min ago
- }
- ];
- }
-
- const data: any = await response.json();
- const recentTickets = data.tickets || [];
-
- // Fetch users for these tickets
- const userIds = [...new Set(recentTickets.map((t: any) => t.requester_id))];
- const users = await fetchZendeskUsers(userIds);
-
- const visitors: OnlineVisitor[] = recentTickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- name: user?.name || 'Anonymous Visitor',
- email: user?.email || 'visitor@unknown.com',
- status: ticket.status === 'new' ? 'waiting' : 'chatting',
- pageUrl: ticket.custom_fields?.find((f: any) => f.id === 'url')?.value || 'Unknown page',
- location: user?.time_zone || 'Unknown',
- timestamp: new Date(ticket.created_at)
- };
- });
-
- return visitors;
- } catch (error) {
- console.error('Error fetching online visitors:', error);
- return [];
- }
-}
-
-// VIP detection - Fortune 500 and high-value domains
-const VIP_INDICATORS = [
- // Fortune 500 Companies
- 'apple.com', 'microsoft.com', 'amazon.com', 'google.com', 'meta.com', 'facebook.com',
- 'walmart.com', 'exxonmobil.com', 'berkshirehathaway.com', 'unitedhealth.com',
- 'jpmorgan.com', 'jpmorganchase.com', 'cvs.com', 'tesla.com', 'alphabet.com',
- // Hospitality & Hotels
- 'marriott.com', 'hilton.com', 'hyatt.com', 'ihg.com', 'fourseasons.com', 'ritzcarlton.com',
- 'starwood.com', 'accor.com', 'wyndham.com', 'choicehotels.com',
- // Interior Design Firms
- 'gensler.com', 'perkinswill.com', 'hok.com', 'som.com', 'kpf.com',
- // Architecture Firms
- 'aecom.com', 'stantec.com', 'hdr.com', 'jacobs.com',
- // High-end Retailers
- 'nordstrom.com', 'bloomingdales.com', 'saks.com', 'neiman-marcus.com', 'bergdorf.com',
- // Commercial Real Estate
- 'cbre.com', 'jll.com', 'cushmanwakefield.com', 'colliers.com',
- // Restaurants & Hospitality
- 'mcdonalds.com', 'starbucks.com', 'chipotle.com', 'darden.com',
- // Government & Education
- '.gov', '.edu', 'harvard.edu', 'stanford.edu', 'mit.edu',
-];
-
-interface VIPAlert {
- company: string;
- email: string;
- name: string;
- reason: string;
- timestamp: Date;
-}
-
-let vipAlerts: VIPAlert[] = [];
-
-function detectVIP(email: string, name: string): VIPAlert | null {
- const emailLower = email.toLowerCase();
- const nameLower = name.toLowerCase();
-
- // Check email domain
- for (const indicator of VIP_INDICATORS) {
- if (emailLower.includes(indicator)) {
- const company = emailLower.split('@')[1] || indicator;
- return {
- company: company.replace(/\.(com|net|org|edu|gov)$/, ''),
- email,
- name,
- reason: `High-value domain detected: ${company}`,
- timestamp: new Date(),
- };
- }
- }
-
- // Check for corporate keywords in name or email
- const corporateKeywords = ['hotel', 'resort', 'design', 'architect', 'interior', 'commercial', 'property', 'development'];
- for (const keyword of corporateKeywords) {
- if (emailLower.includes(keyword) || nameLower.includes(keyword)) {
- return {
- company: email.split('@')[1] || 'Corporate',
- email,
- name,
- reason: `Corporate keyword detected: ${keyword}`,
- timestamp: new Date(),
- };
- }
- }
-
- return null;
-}
-
-// Cache for conversations
-let conversationsCache: ChatConversation[] = [];
-let lastFetchTime = 0;
-const CACHE_DURATION = 30000; // 30 seconds
-
-let onlineVisitorsCache: OnlineVisitor[] = [];
-let lastVisitorFetchTime = 0;
-
-async function getConversations(): Promise<ChatConversation[]> {
- const now = Date.now();
- if (now - lastFetchTime > CACHE_DURATION) {
- conversationsCache = await fetchZendeskTickets();
- lastFetchTime = now;
-
- // Check for VIP contacts
- conversationsCache.forEach(conv => {
- const vip = detectVIP(conv.customerEmail, conv.customerName);
- if (vip) {
- // Only add if not already in alerts
- if (!vipAlerts.find(a => a.email === vip.email && a.timestamp > new Date(Date.now() - 3600000))) {
- vipAlerts.unshift(vip);
- vipAlerts = vipAlerts.slice(0, 10); // Keep last 10
- }
- }
- });
- }
- return conversationsCache;
-}
-
-async function getOnlineVisitors(): Promise<OnlineVisitor[]> {
- const now = Date.now();
- if (now - lastVisitorFetchTime > CACHE_DURATION) {
- onlineVisitorsCache = await fetchOnlineVisitors();
- lastVisitorFetchTime = now;
- }
- return onlineVisitorsCache;
-}
-
-// NO MOCK DATA - All conversations are loaded from Zendesk API in real-time
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- action: string;
- details: string;
-}> = [];
-
-function logActivity(action: string, details: string) {
- activityLog.unshift({
- timestamp: new Date(),
- action,
- details,
- });
- if (activityLog.length > 50) {
- activityLog.pop();
- }
- stats.lastActivity = new Date();
-}
-
-// Routes
-
-// Login page
-app.get('/login', (req: Request, res: Response) => {
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Login</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 16px;
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
- width: 100%;
- max-width: 400px;
- }
- h1 {
- color: #333;
- font-size: 28px;
- margin-bottom: 10px;
- text-align: center;
- }
- .subtitle {
- color: #666;
- text-align: center;
- margin-bottom: 30px;
- font-size: 14px;
- }
- .form-group {
- margin-bottom: 20px;
- }
- label {
- display: block;
- color: #555;
- margin-bottom: 8px;
- font-weight: 500;
- }
- input {
- width: 100%;
- padding: 12px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- font-size: 16px;
- transition: border-color 0.3s;
- }
- input:focus {
- outline: none;
- border-color: #03a9f4;
- }
- button {
- width: 100%;
- padding: 14px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- button:hover {
- transform: translateY(-2px);
- }
- .agent-badge {
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 12px;
- text-align: center;
- margin-bottom: 20px;
- font-weight: 600;
- }
- </style>
- </head>
- <body>
- <div class="login-container">
- <div class="agent-badge">💬 ZENDESK CHAT AGENT</div>
- <h1>Customer Support</h1>
- <p class="subtitle">DW-Agents System</p>
- <form method="POST" action="/login">
- <div class="form-group">
- <label for="username">Username</label>
- <input type="text" id="username" name="username" required autocomplete="username">
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" id="password" name="password" required autocomplete="current-password">
- </div>
- <button type="submit">Access Dashboard</button>
- </form>
- </div>
- </body>
- </html>
- `);
-});
-
-// Login handler
-app.post('/login', (req: Request, res: Response) => {
- const { username, password } = req.body;
-
- if (username === 'admin' && password === '2025') {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- req.session.save((err) => {
- if (err) {
- console.error('Session save error:', err);
- }
- res.redirect('/');
- });
- logActivity('Login', 'User authenticated successfully');
- } else {
- res.redirect('/login');
- }
-});
-
-// Main dashboard
-app.get('/', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- const onlineVisitors = await getOnlineVisitors();
- const displayConversations = liveConversations; // NO MOCK DATA - only show real Zendesk tickets
- const isLiveData = liveConversations.length > 0;
-
- const activeChats = displayConversations.filter(c => c.status === 'active').length;
- const pendingChats = displayConversations.filter(c => c.status === 'pending').length;
- const resolvedChats = displayConversations.filter(c => c.status === 'resolved').length;
- const onlineCount = onlineVisitors.length;
-
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Dashboard</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- padding: 20px;
- }
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .header h1 {
- color: #333;
- font-size: 32px;
- margin-bottom: 10px;
- }
- .agent-status {
- display: inline-block;
- background: #10b981;
- color: white;
- padding: 6px 12px;
- border-radius: 20px;
- font-size: 14px;
- font-weight: 600;
- }
- .zendesk-link {
- display: inline-block;
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 8px;
- text-decoration: none;
- margin-left: 15px;
- font-size: 14px;
- transition: background 0.3s;
- }
- .zendesk-link:hover {
- background: #0288d1;
- }
- .tabs {
- display: flex;
- gap: 10px;
- margin-bottom: 20px;
- }
- .tab-button {
- background: white;
- color: #03a9f4;
- padding: 12px 24px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-size: 16px;
- font-weight: 600;
- transition: all 0.3s;
- }
- .tab-button:hover {
- background: #f3f4f6;
- }
- .tab-button.active {
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- }
- .tab-content {
- display: none;
- }
- .tab-content.active {
- display: block;
- }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stat-card {
- background: white;
- padding: 24px;
- border-radius: 12px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .stat-value {
- font-size: 32px;
- font-weight: 700;
- color: #03a9f4;
- margin-bottom: 5px;
- }
- .stat-label {
- color: #666;
- font-size: 14px;
- }
- .section {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .section h2 {
- color: #333;
- font-size: 24px;
- margin-bottom: 20px;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .chat-container {
- display: flex;
- flex-direction: column;
- height: 600px;
- }
- .chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 20px;
- background: #f9fafb;
- border-radius: 12px;
- margin-bottom: 20px;
- }
- .message {
- margin-bottom: 16px;
- padding: 12px 16px;
- border-radius: 12px;
- max-width: 80%;
- }
- .message.user {
- background: #03a9f4;
- color: white;
- margin-left: auto;
- }
- .message.assistant {
- background: white;
- color: #333;
- border: 1px solid #e0e0e0;
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- .chat-input {
- flex: 1;
- padding: 14px;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- font-size: 16px;
- font-family: inherit;
- }
- .send-button {
- padding: 14px 28px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 12px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- .send-button:hover {
- transform: translateY(-2px);
- }
- .suggestion-chips {
- display: flex;
- gap: 10px;
- margin-bottom: 15px;
- flex-wrap: wrap;
- }
- .chip {
- background: #f3f4f6;
- color: #03a9f4;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 14px;
- cursor: pointer;
- border: 2px solid transparent;
- transition: all 0.3s;
- }
- .chip:hover {
- background: #03a9f4;
- color: white;
- border-color: #03a9f4;
- }
- .conversation-card {
- background: white;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 15px;
- transition: all 0.3s;
- }
- .conversation-card:hover {
- border-color: #03a9f4;
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(3, 169, 244, 0.1);
- }
- .conversation-header {
- display: flex;
- justify-content: space-between;
- align-items: start;
- margin-bottom: 10px;
- }
- .customer-name {
- font-size: 18px;
- font-weight: 600;
- color: #333;
- }
- .status-badge {
- padding: 4px 12px;
- border-radius: 12px;
- font-size: 12px;
- font-weight: 600;
- }
- .status-active { background: #d1fae5; color: #065f46; }
- .status-pending { background: #fef3c7; color: #92400e; }
- .status-resolved { background: #dbeafe; color: #1e40af; }
- .priority-high { background: #fee2e2; color: #991b1b; }
- .priority-normal { background: #e0e0e0; color: #666; }
- .priority-low { background: #f3f4f6; color: #888; }
- .conversation-subject {
- color: #03a9f4;
- font-size: 14px;
- font-weight: 500;
- margin-bottom: 8px;
- }
- .last-message {
- color: #666;
- font-size: 14px;
- line-height: 1.5;
- margin-bottom: 10px;
- }
- .conversation-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 10px;
- border-top: 1px solid #e0e0e0;
- }
- .timestamp {
- font-size: 12px;
- color: #888;
- }
- .action-button {
- padding: 6px 16px;
- background: #03a9f4;
- color: white;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 14px;
- transition: background 0.3s;
- }
- .action-button:hover {
- background: #0288d1;
- }
- .modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0,0,0,0.7);
- z-index: 1000;
- align-items: center;
- justify-content: center;
- }
- .modal.active {
- display: flex;
- }
- .modal-content {
- background: white;
- border-radius: 16px;
- width: 90%;
- max-width: 800px;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 25px 50px rgba(0,0,0,0.5);
- }
- .modal-header {
- padding: 25px 30px;
- border-bottom: 2px solid #e0e0e0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .modal-header h2 {
- color: #03a9f4;
- margin: 0;
- font-size: 22px;
- }
- .modal-close {
- background: #ef4444;
- color: white;
- border: none;
- width: 36px;
- height: 36px;
- border-radius: 50%;
- cursor: pointer;
- font-size: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.3s;
- }
- .modal-close:hover {
- background: #dc2626;
- }
- .modal-body {
- padding: 25px 30px;
- overflow-y: auto;
- flex: 1;
- }
- .chat-message {
- margin-bottom: 20px;
- display: flex;
- flex-direction: column;
- }
- .chat-message.customer {
- align-items: flex-start;
- }
- .chat-message.agent {
- align-items: flex-end;
- }
- .message-sender {
- font-size: 12px;
- font-weight: 600;
- color: #666;
- margin-bottom: 6px;
- text-transform: uppercase;
- }
- .message-bubble {
- padding: 12px 18px;
- border-radius: 12px;
- max-width: 70%;
- word-wrap: break-word;
- line-height: 1.5;
- }
- .chat-message.customer .message-bubble {
- background: #f3f4f6;
- color: #333;
- border-bottom-left-radius: 4px;
- }
- .chat-message.agent .message-bubble {
- background: #03a9f4;
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message-time {
- font-size: 11px;
- color: #888;
- margin-top: 4px;
- }
- @keyframes pulse {
- 0%, 100% { transform: scale(1); box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); }
- 50% { transform: scale(1.02); box-shadow: 0 15px 50px rgba(255, 107, 107, 0.5); }
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>💬 Customer Chat Monitor</h1>
- <p style="color: #666; margin-top: 5px;">Free Customer Support & Chat Management (No paid services required)</p>
- <div style="margin-top: 15px; display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
- <span class="agent-status">● ONLINE</span>
- <span style="color: #888; font-size: 14px;">Port 9884</span>
- <a href="${ZENDESK_CHAT_URL}" target="_blank" class="zendesk-link">🔗 Open Zendesk Chat</a>
- <button onclick="location.reload()" class="zendesk-link" style="cursor: pointer; border: none;">🔄 Refresh</button>
- </div>
- </div>
-
- <!-- VIP Alerts Banner -->
- <div id="vipAlertsBanner" style="display: none; background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); border: 3px solid #ff6b6b; border-radius: 15px; padding: 20px; margin: 20px 0; box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); animation: pulse 2s infinite;">
- <div style="display: flex; align-items: center; gap: 15px;">
- <div style="font-size: 3em;">🚨</div>
- <div style="flex: 1;">
- <h2 style="color: #d32f2f; margin: 0 0 10px 0; font-size: 1.5em;">⚡ HIGH-VALUE OPPORTUNITY DETECTED</h2>
- <div id="vipAlertsContent"></div>
- </div>
- </div>
- </div>
-
- <div class="tabs">
- <button class="tab-button active" onclick="switchTab('overview')">📊 Overview</button>
- <button class="tab-button" onclick="switchTab('chat')">💬 AI Assistant</button>
- <button class="tab-button" onclick="switchTab('conversations')">💭 Active Chats</button>
- <button class="tab-button" onclick="switchTab('analytics')">📈 Analytics</button>
- </div>
-
- <!-- Overview Tab -->
- <div id="tab-overview" class="tab-content active">
- <div class="stats-grid">
- <div class="stat-card" style="border-left: 4px solid #10b981;">
- <div class="stat-value" style="color: #10b981;">${onlineCount}</div>
- <div class="stat-label">🟢 Currently Online</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${activeChats}</div>
- <div class="stat-label">Active Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${pendingChats}</div>
- <div class="stat-label">Pending Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${resolvedChats}</div>
- <div class="stat-label">Resolved Today</div>
- </div>
- </div>
-
- <div class="section" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; padding: 25px; border-radius: 16px; margin-bottom: 20px;">
- <h2 style="color: white; margin-bottom: 15px;">🟢 Currently Online (${onlineCount})</h2>
- ${onlineVisitors.length > 0 ? onlineVisitors.map(visitor => `
- <div style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; margin-bottom: 10px; border-left: 4px solid ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'};">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
- <strong style="font-size: 1.1em;">${visitor.name}</strong>
- <span style="background: ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'}; color: white; padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: 600;">
- ${visitor.status === 'waiting' ? '⏳ Waiting' : visitor.status === 'chatting' ? '💬 Chatting' : '👀 Browsing'}
- </span>
- </div>
- <div style="font-size: 0.9em; opacity: 0.95;">
- 📧 ${visitor.email}<br>
- ${visitor.pageUrl ? `📄 ${visitor.pageUrl}<br>` : ''}
- 🕐 ${new Date(visitor.timestamp).toLocaleTimeString()}
- </div>
- </div>
- `).join('') : '<p style="text-align: center; padding: 20px; opacity: 0.9;">No visitors currently online</p>'}
- </div>
-
- <div class="section">
- <h2>💭 Recent Conversations ${isLiveData ? '<span style="color: #28a745; font-size: 0.8em;">● LIVE</span>' : '<span style="color: #ffc107; font-size: 0.8em;">● DEMO</span>'}</h2>
- <div id="conversationsList">
- ${displayConversations.slice(0, 3).map(conv => `
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-${conv.status}">${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-${conv.priority}">${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">${conv.subject}</div>
- <div class="last-message">${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- `).join('')}
- </div>
- </div>
-
- <div class="section">
- <h2>🔗 Zendesk Integration</h2>
- <p style="color: #666; margin-bottom: 15px;">Connected to: <strong>${ZENDESK_DOMAIN}</strong></p>
- <p style="color: #666; margin-bottom: 15px;">Account: <strong>${ZENDESK_EMAIL}</strong></p>
- <a href="${ZENDESK_CHAT_URL}" target="_blank" class="action-button" style="display: inline-block; text-decoration: none;">Open Zendesk Chat Dashboard</a>
- </div>
- </div>
-
- <!-- Chat Tab -->
- <div id="tab-chat" class="tab-content">
- <div class="section">
- <h2>💬 AI Support Assistant</h2>
- <p style="color: #666; margin-bottom: 20px;">Get AI-powered help with customer support, chat responses, and Zendesk management</p>
-
- <div class="suggestion-chips">
- <span class="chip" onclick="sendSuggestion('Suggest response for installation question')">Installation help</span>
- <span class="chip" onclick="sendSuggestion('How to handle product availability inquiry')">Product availability</span>
- <span class="chip" onclick="sendSuggestion('Best practices for customer support')">Support best practices</span>
- <span class="chip" onclick="sendSuggestion('Generate empathetic response template')">Response templates</span>
- </div>
-
- <div class="chat-container">
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- Hello! I'm your Zendesk Chat AI Assistant powered by Claude 4. I can help you with:
- <br><br>
- • Suggesting responses to customer inquiries<br>
- • Customer support best practices<br>
- • Chat management strategies<br>
- • Handling difficult conversations<br>
- • Zendesk tips and workflows<br>
- <br>
- How can I assist you with customer support today?
- </div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- class="chat-input"
- id="chatInput"
- placeholder="Ask about customer support, chat responses..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-button" onclick="sendMessage()">Send</button>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Conversations Tab -->
- <div id="tab-conversations" class="tab-content">
- <div class="section">
- <h2>💭 All Conversations</h2>
- <div id="allConversations">
- <p style="color: #888;">Loading conversations...</p>
- </div>
- </div>
- </div>
-
- <!-- Analytics Tab -->
- <div id="tab-analytics" class="tab-content">
- <div class="section">
- <h2>📈 Chat Analytics</h2>
- <div class="stats-grid">
- <div class="stat-card">
- <div class="stat-value">${stats.customerSatisfaction}</div>
- <div class="stat-label">Customer Satisfaction</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.totalQueries}</div>
- <div class="stat-label">Total AI Queries</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${displayConversations.length}</div>
- <div class="stat-label">Total Conversations</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.avgResponseTime}</div>
- <div class="stat-label">Avg Response Time</div>
- </div>
- </div>
- <div style="background: #f9fafb; padding: 20px; border-radius: 12px; margin-top: 20px;">
- <p style="color: #666;">Connect to Zendesk Analytics API for real-time reporting and advanced metrics.</p>
- </div>
- </div>
- </div>
- </div>
-
- <script>
- function switchTab(tabName) {
- document.querySelectorAll('.tab-content').forEach(tab => {
- tab.classList.remove('active');
- });
- document.querySelectorAll('.tab-button').forEach(btn => {
- btn.classList.remove('active');
- });
-
- document.getElementById('tab-' + tabName).classList.add('active');
- event.target.classList.add('active');
-
- if (tabName === 'conversations') {
- loadConversations();
- }
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const message = input.value.trim();
-
- if (!message) return;
-
- const chatMessages = document.getElementById('chatMessages');
- chatMessages.innerHTML += \`
- <div class="message user">\${message}</div>
- \`;
-
- input.value = '';
- chatMessages.scrollTop = chatMessages.scrollHeight;
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
-
- chatMessages.innerHTML += \`
- <div class="message assistant">\${data.response.replace(/\\n/g, '<br>')}</div>
- \`;
- chatMessages.scrollTop = chatMessages.scrollHeight;
- } catch (error) {
- console.error('Chat error:', error);
- chatMessages.innerHTML += \`
- <div class="message assistant">Sorry, I encountered an error. Please try again.</div>
- \`;
- }
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function loadVIPAlerts() {
- try {
- const response = await fetch('/api/vip-alerts');
- const data = await response.json();
-
- if (data.alerts && data.alerts.length > 0) {
- const banner = document.getElementById('vipAlertsBanner');
- const content = document.getElementById('vipAlertsContent');
-
- const html = data.alerts.map(alert => \`
- <div style="background: white; padding: 15px; border-radius: 10px; margin-bottom: 10px; border-left: 5px solid #ff6b6b;">
- <div style="font-weight: 700; font-size: 1.2em; color: #d32f2f; margin-bottom: 5px;">
- \${alert.name} - \${alert.company.toUpperCase()}
- </div>
- <div style="color: #666; margin-bottom: 5px;">
- 📧 \${alert.email}
- </div>
- <div style="color: #ff6b6b; font-weight: 600; font-size: 0.9em;">
- 🎯 \${alert.reason}
- </div>
- <div style="color: #888; font-size: 0.85em; margin-top: 5px;">
- ⏰ \${new Date(alert.timestamp).toLocaleString()}
- </div>
- </div>
- \`).join('');
-
- content.innerHTML = html;
- banner.style.display = 'block';
- } else {
- document.getElementById('vipAlertsBanner').style.display = 'none';
- }
- } catch (error) {
- console.error('Error loading VIP alerts:', error);
- }
- }
-
- async function loadConversations() {
- const container = document.getElementById('allConversations');
- try {
- const response = await fetch('/api/conversations');
- const data = await response.json();
-
- let html = '';
- data.conversations.forEach(conv => {
- html += \`
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">\${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">\${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-\${conv.status}">\${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-\${conv.priority}">\${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">\${conv.subject}</div>
- <div class="last-message">\${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">\${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('\${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/\${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- \`;
- });
-
- container.innerHTML = html;
- } catch (error) {
- console.error('Load conversations error:', error);
- container.innerHTML = '<p style="color: #ef4444;">Error loading conversations</p>';
- }
- }
-
- async function viewChat(chatId) {
- try {
- const response = await fetch(\`/api/conversation/\${chatId}\`);
- const data = await response.json();
-
- const modal = document.getElementById('chatModal');
- const modalTitle = document.getElementById('modalTitle');
- const modalBody = document.getElementById('modalBody');
-
- modalTitle.textContent = \`Chat with \${data.customerName}\`;
-
- let messagesHtml = '';
- data.messages.forEach(msg => {
- const time = new Date(msg.timestamp).toLocaleTimeString();
- messagesHtml += \`
- <div class="chat-message \${msg.sender}">
- <div class="message-sender">\${msg.sender === 'customer' ? data.customerName : 'Support Agent'}</div>
- <div class="message-bubble">\${msg.message}</div>
- <div class="message-time">\${time}</div>
- </div>
- \`;
- });
-
- modalBody.innerHTML = messagesHtml;
- modal.classList.add('active');
-
- // Scroll to bottom
- modalBody.scrollTop = modalBody.scrollHeight;
- } catch (error) {
- console.error('Error loading chat:', error);
- alert('Failed to load chat conversation');
- }
- }
-
- function closeModal() {
- document.getElementById('chatModal').classList.remove('active');
- }
-
- // Close modal when clicking outside
- document.addEventListener('click', (e) => {
- const modal = document.getElementById('chatModal');
- if (e.target === modal) {
- closeModal();
- }
- });
-
- // Load VIP alerts on page load and refresh every 30 seconds
- loadVIPAlerts();
- setInterval(loadVIPAlerts, 30000);
- </script>
-
- <!-- Chat Modal -->
- <div id="chatModal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h2 id="modalTitle">Chat Conversation</h2>
- <button class="modal-close" onclick="closeModal()">×</button>
- </div>
- <div class="modal-body" id="modalBody">
- <!-- Messages will be loaded here -->
- </div>
- </div>
- </div>
- </body>
- </html>
- `);
-});
-
-// API Endpoints
-
-// Chat endpoint
-app.post('/api/chat', requireAuth, async (req: Request, res: Response) => {
- try {
- const { message } = req.body;
- stats.totalQueries++;
-
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- req.session.chatHistory.push({
- role: 'user',
- content: message
- });
-
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- const systemPrompt = `You are the Zendesk Chat Support Agent for Designer Wallcoverings, powered by Claude 4 Sonnet.
-
-Your role is to assist customer support team with:
-
-CAPABILITIES:
-- Suggest professional and empathetic customer responses
-- Provide customer support best practices
-- Help manage chat conversations
-- Offer conflict resolution strategies
-- Zendesk platform tips and workflows
-- Response templates for common scenarios
-
-CONTEXT:
-- Company: Designer Wallcoverings (wallcovering/wallpaper retailer)
-- Zendesk Domain: ${ZENDESK_DOMAIN}
-- Current Active Chats: ${displayConversations.filter(c => c.status === 'active').length}
-- Pending Chats: ${displayConversations.filter(c => c.status === 'pending').length}
-
-COMMON CUSTOMER INQUIRIES:
-- Product information and availability
-- Installation help and tips
-- Order tracking and status
-- Return and exchange policies
-- Design consultation
-- Sample requests
-
-When helping with responses:
-1. Be empathetic and professional
-2. Provide clear, actionable information
-3. Include product knowledge when relevant
-4. Maintain Designer Wallcoverings' brand voice
-5. Suggest follow-up actions
-
-Be helpful, supportive, and provide excellent customer service guidance.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 1024,
- system: systemPrompt,
- messages: req.session.chatHistory
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error processing your request.';
-
- req.session.chatHistory.push({
- role: 'assistant',
- content: assistantMessage
- });
-
- logActivity('Chat Query', `User asked: "${message.substring(0, 50)}..."`);
-
- res.json({ response: assistantMessage });
- } catch (error) {
- console.error('Chat error:', error);
- res.status(500).json({ error: 'Failed to process chat message' });
- }
-});
-
-// Get conversations
-app.get('/api/conversations', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- res.json({ conversations: liveConversations });
-});
-
-// Get specific conversation by ID
-app.get('/api/conversation/:id', requireAuth, async (req: Request, res: Response) => {
- const chatId = req.params.id;
- const liveConversations = await getConversations();
- let conversation = liveConversations.find(c => c.id === chatId);
-
- if (!conversation) {
- return res.status(404).json({ error: 'Conversation not found' });
- }
-
- // Fetch comments if not already loaded
- if (conversation.messages.length === 0) {
- conversation.messages = await fetchTicketComments(chatId);
- }
-
- logActivity('View Chat', `Viewed conversation ${chatId} with ${conversation.customerName}`);
- res.json(conversation);
-});
-
-// Get statistics
-app.get('/api/stats', requireAuth, (req: Request, res: Response) => {
- res.json(stats);
-});
-
-// Get VIP alerts
-app.get('/api/vip-alerts', requireAuth, (req: Request, res: Response) => {
- res.json({ alerts: vipAlerts });
-});
-
-// Get activity log
-app.get('/api/activity', requireAuth, (req: Request, res: Response) => {
- res.json({ activities: activityLog });
-});
-
-// Start server
-app.listen(PORT, '0.0.0.0', () => {
- console.log(`💬 Zendesk Chat Agent running on port ${PORT}`);
- console.log(`Dashboard: http://45.61.58.125:${PORT}`);
- console.log(`Zendesk: ${ZENDESK_CHAT_URL}`);
- logActivity('System Start', 'Zendesk Chat Agent initialized');
-});
diff --git a/DW-Agents/dw-agents/agent-zendesk-chat/zendesk-chat-agent.ts.backup-oauth-20251112 b/DW-Agents/dw-agents/agent-zendesk-chat/zendesk-chat-agent.ts.backup-oauth-20251112
deleted file mode 100644
index 2e8051bc..00000000
--- a/DW-Agents/dw-agents/agent-zendesk-chat/zendesk-chat-agent.ts.backup-oauth-20251112
+++ /dev/null
@@ -1,1489 +0,0 @@
-import cookieParser from "cookie-parser";
-/**
- * DW-Agents: Zendesk Chat Agent
- *
- * Customer support chat integration system
- * - Zendesk chat monitoring and management
- * - AI-powered response suggestions
- * - Customer conversation history
- * - Real-time chat analytics
- * - Chat interface for support queries
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express, { Request, Response } from 'express';
-import session from 'express-session';
-import cookieParser from 'cookie-parser';
-// import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import fetch from 'node-fetch';
-
-import { chatMiddleware } from '../shared-chat-integration';
-
-const app = express();
-
-// Global Authentication
-const PORT = process.env.PORT || 9712;
-
-// Global Authentication - MUST come before any other middleware
-app.use(cookieParser());
-
-// Add inter-agent chat widget (after auth)
-app.use(chatMiddleware({
- agentId: 'agent-zendesk-chat',
- agentName: 'Zendesk Chat',
- port: 9712,
- category: 'agent'
-}));
-
-
-// Anthropic AI client
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY || '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',
-});
-
-// Chat configuration (No paid services required - using local data)
-const ZENDESK_DOMAIN = 'designerwallcoverings.zendesk.com';
-const ZENDESK_CHAT_URL = `https://${ZENDESK_DOMAIN}/chat/agent`;
-
-// Local chat data storage (replaces Zendesk API)
-const USE_DEMO_DATA = true; // Always use demo data since we're not paying for Zendesk
-
-// Middleware
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
-
-// Session configuration with extended TypeScript types
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-app.use(
- session({
- secret: 'dw-agents-zendesk-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days - longer session to avoid re-login
- },
- })
-);
-
-// Authentication middleware with SSO support
-const requireAuth = (req: Request, res: Response, next: any) => {
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
-};
-
-// Statistics tracking
-const stats = {
- totalQueries: 0,
- activeChats: 0,
- resolvedChats: 0,
- avgResponseTime: '2.3 min',
- customerSatisfaction: '94%',
- lastActivity: new Date(),
-};
-
-// Sample chat data
-interface ChatMessage {
- sender: 'customer' | 'agent';
- message: string;
- timestamp: Date;
-}
-
-interface ChatConversation {
- id: string;
- zendeskId?: number;
- customerName: string;
- customerEmail: string;
- status: 'active' | 'pending' | 'resolved';
- priority: 'high' | 'normal' | 'low';
- subject: string;
- lastMessage: string;
- timestamp: Date;
- tags: string[];
- messages: ChatMessage[];
-}
-
-// Zendesk API helper functions
-async function fetchZendeskTickets(): Promise<ChatConversation[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?sort_by=updated_at&sort_order=desc&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk API error:', response.status, await response.text());
- return [];
- }
-
- const data: any = await response.json();
- const tickets = data.tickets || [];
-
- // Fetch users to get names and emails
- const userIds = [...new Set(tickets.map((t: any) => t.requester_id))];
- const users = await fetchZendeskUsers(userIds);
-
- const conversations: ChatConversation[] = tickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- zendeskId: ticket.id,
- customerName: user?.name || 'Unknown Customer',
- customerEmail: user?.email || 'unknown@email.com',
- status: ticket.status === 'new' || ticket.status === 'open' ? 'active' :
- ticket.status === 'pending' ? 'pending' : 'resolved',
- priority: ticket.priority === 'urgent' || ticket.priority === 'high' ? 'high' :
- ticket.priority === 'low' ? 'low' : 'normal',
- subject: ticket.subject || 'No Subject',
- lastMessage: ticket.description?.substring(0, 100) || 'No message',
- timestamp: new Date(ticket.updated_at),
- tags: ticket.tags || [],
- messages: [], // Will be loaded on demand
- };
- });
-
- return conversations;
- } catch (error) {
- console.error('Error fetching Zendesk tickets:', error);
- return [];
- }
-}
-
-async function fetchZendeskUsers(userIds: number[]): Promise<any[]> {
- try {
- if (userIds.length === 0) return [];
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/users/show_many.json?ids=${userIds.join(',')}`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk users API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- return data.users || [];
- } catch (error) {
- console.error('Error fetching Zendesk users:', error);
- return [];
- }
-}
-
-async function fetchTicketComments(ticketId: string): Promise<ChatMessage[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets/${ticketId}/comments.json`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk comments API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- const comments = data.comments || [];
-
- return comments.map((comment: any) => ({
- sender: comment.public ? 'agent' : 'customer',
- message: comment.body || '',
- timestamp: new Date(comment.created_at),
- }));
- } catch (error) {
- console.error('Error fetching ticket comments:', error);
- return [];
- }
-}
-
-// Fetch currently online visitors/chats from Zendesk Chat
-interface OnlineVisitor {
- id: string;
- name: string;
- email: string;
- status: 'browsing' | 'chatting' | 'waiting';
- pageUrl?: string;
- location?: string;
- timestamp: Date;
-}
-
-async function fetchOnlineVisitors(): Promise<OnlineVisitor[]> {
- try {
- // Zendesk Chat uses different auth - OAuth token
- // For now, we'll try to get active chats from the Support API
- // by looking for tickets created in the last hour with specific tags
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
-
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?created_at_gt=${oneHourAgo}&status=new,open&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk online visitors API error:', response.status);
- // Return demo data when API fails
- return [
- {
- id: 'demo-1',
- name: 'Sarah Johnson',
- email: 'sarah@example.com',
- status: 'chatting' as const,
- pageUrl: 'https://designerwallcoverings.com/wallpaper/floral',
- location: 'New York, USA',
- timestamp: new Date(Date.now() - 5 * 60 * 1000) // 5 min ago
- },
- {
- id: 'demo-2',
- name: 'Mike Chen',
- email: 'mike@example.com',
- status: 'waiting' as const,
- pageUrl: 'https://designerwallcoverings.com/samples',
- location: 'Los Angeles, USA',
- timestamp: new Date(Date.now() - 2 * 60 * 1000) // 2 min ago
- },
- {
- id: 'demo-3',
- name: 'Emily Rodriguez',
- email: 'emily@example.com',
- status: 'browsing' as const,
- pageUrl: 'https://designerwallcoverings.com/about',
- location: 'Chicago, USA',
- timestamp: new Date(Date.now() - 1 * 60 * 1000) // 1 min ago
- }
- ];
- }
-
- const data: any = await response.json();
- const recentTickets = data.tickets || [];
-
- // Fetch users for these tickets
- const userIds = [...new Set(recentTickets.map((t: any) => t.requester_id))];
- const users = await fetchZendeskUsers(userIds);
-
- const visitors: OnlineVisitor[] = recentTickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- name: user?.name || 'Anonymous Visitor',
- email: user?.email || 'visitor@unknown.com',
- status: ticket.status === 'new' ? 'waiting' : 'chatting',
- pageUrl: ticket.custom_fields?.find((f: any) => f.id === 'url')?.value || 'Unknown page',
- location: user?.time_zone || 'Unknown',
- timestamp: new Date(ticket.created_at)
- };
- });
-
- return visitors;
- } catch (error) {
- console.error('Error fetching online visitors:', error);
- return [];
- }
-}
-
-// VIP detection - Fortune 500 and high-value domains
-const VIP_INDICATORS = [
- // Fortune 500 Companies
- 'apple.com', 'microsoft.com', 'amazon.com', 'google.com', 'meta.com', 'facebook.com',
- 'walmart.com', 'exxonmobil.com', 'berkshirehathaway.com', 'unitedhealth.com',
- 'jpmorgan.com', 'jpmorganchase.com', 'cvs.com', 'tesla.com', 'alphabet.com',
- // Hospitality & Hotels
- 'marriott.com', 'hilton.com', 'hyatt.com', 'ihg.com', 'fourseasons.com', 'ritzcarlton.com',
- 'starwood.com', 'accor.com', 'wyndham.com', 'choicehotels.com',
- // Interior Design Firms
- 'gensler.com', 'perkinswill.com', 'hok.com', 'som.com', 'kpf.com',
- // Architecture Firms
- 'aecom.com', 'stantec.com', 'hdr.com', 'jacobs.com',
- // High-end Retailers
- 'nordstrom.com', 'bloomingdales.com', 'saks.com', 'neiman-marcus.com', 'bergdorf.com',
- // Commercial Real Estate
- 'cbre.com', 'jll.com', 'cushmanwakefield.com', 'colliers.com',
- // Restaurants & Hospitality
- 'mcdonalds.com', 'starbucks.com', 'chipotle.com', 'darden.com',
- // Government & Education
- '.gov', '.edu', 'harvard.edu', 'stanford.edu', 'mit.edu',
-];
-
-interface VIPAlert {
- company: string;
- email: string;
- name: string;
- reason: string;
- timestamp: Date;
-}
-
-let vipAlerts: VIPAlert[] = [];
-
-function detectVIP(email: string, name: string): VIPAlert | null {
- const emailLower = email.toLowerCase();
- const nameLower = name.toLowerCase();
-
- // Check email domain
- for (const indicator of VIP_INDICATORS) {
- if (emailLower.includes(indicator)) {
- const company = emailLower.split('@')[1] || indicator;
- return {
- company: company.replace(/\.(com|net|org|edu|gov)$/, ''),
- email,
- name,
- reason: `High-value domain detected: ${company}`,
- timestamp: new Date(),
- };
- }
- }
-
- // Check for corporate keywords in name or email
- const corporateKeywords = ['hotel', 'resort', 'design', 'architect', 'interior', 'commercial', 'property', 'development'];
- for (const keyword of corporateKeywords) {
- if (emailLower.includes(keyword) || nameLower.includes(keyword)) {
- return {
- company: email.split('@')[1] || 'Corporate',
- email,
- name,
- reason: `Corporate keyword detected: ${keyword}`,
- timestamp: new Date(),
- };
- }
- }
-
- return null;
-}
-
-// Cache for conversations
-let conversationsCache: ChatConversation[] = [];
-let lastFetchTime = 0;
-const CACHE_DURATION = 30000; // 30 seconds
-
-let onlineVisitorsCache: OnlineVisitor[] = [];
-let lastVisitorFetchTime = 0;
-
-async function getConversations(): Promise<ChatConversation[]> {
- const now = Date.now();
- if (now - lastFetchTime > CACHE_DURATION) {
- conversationsCache = await fetchZendeskTickets();
- lastFetchTime = now;
-
- // Check for VIP contacts
- conversationsCache.forEach(conv => {
- const vip = detectVIP(conv.customerEmail, conv.customerName);
- if (vip) {
- // Only add if not already in alerts
- if (!vipAlerts.find(a => a.email === vip.email && a.timestamp > new Date(Date.now() - 3600000))) {
- vipAlerts.unshift(vip);
- vipAlerts = vipAlerts.slice(0, 10); // Keep last 10
- }
- }
- });
- }
- return conversationsCache;
-}
-
-async function getOnlineVisitors(): Promise<OnlineVisitor[]> {
- const now = Date.now();
- if (now - lastVisitorFetchTime > CACHE_DURATION) {
- onlineVisitorsCache = await fetchOnlineVisitors();
- lastVisitorFetchTime = now;
- }
- return onlineVisitorsCache;
-}
-
-// NO MOCK DATA - All conversations are loaded from Zendesk API in real-time
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- action: string;
- details: string;
-}> = [];
-
-function logActivity(action: string, details: string) {
- activityLog.unshift({
- timestamp: new Date(),
- action,
- details,
- });
- if (activityLog.length > 50) {
- activityLog.pop();
- }
- stats.lastActivity = new Date();
-}
-
-// Routes
-
-// Login page
-app.get('/login', (req: Request, res: Response) => {
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Login</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 16px;
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
- width: 100%;
- max-width: 400px;
- }
- h1 {
- color: #333;
- font-size: 28px;
- margin-bottom: 10px;
- text-align: center;
- }
- .subtitle {
- color: #666;
- text-align: center;
- margin-bottom: 30px;
- font-size: 14px;
- }
- .form-group {
- margin-bottom: 20px;
- }
- label {
- display: block;
- color: #555;
- margin-bottom: 8px;
- font-weight: 500;
- }
- input {
- width: 100%;
- padding: 12px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- font-size: 16px;
- transition: border-color 0.3s;
- }
- input:focus {
- outline: none;
- border-color: #03a9f4;
- }
- button {
- width: 100%;
- padding: 14px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- button:hover {
- transform: translateY(-2px);
- }
- .agent-badge {
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 12px;
- text-align: center;
- margin-bottom: 20px;
- font-weight: 600;
- }
- </style>
- </head>
- <body>
- <div class="login-container">
- <div class="agent-badge">💬 ZENDESK CHAT AGENT</div>
- <h1>Customer Support</h1>
- <p class="subtitle">DW-Agents System</p>
- <form method="POST" action="/login">
- <div class="form-group">
- <label for="username">Username</label>
- <input type="text" id="username" name="username" required autocomplete="username">
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" id="password" name="password" required autocomplete="current-password">
- </div>
- <button type="submit">Access Dashboard</button>
- </form>
- </div>
- </body>
- </html>
- `);
-});
-
-// Login handler
-app.post('/login', (req: Request, res: Response) => {
- const { username, password } = req.body;
-
- if (username === 'admin2025' && password === 'Otis') {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- req.session.save((err) => {
- if (err) {
- console.error('Session save error:', err);
- }
- res.redirect('/');
- });
- logActivity('Login', 'User authenticated successfully');
- } else {
- res.redirect('/login');
- }
-});
-
-// Main dashboard
-app.get('/', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- const onlineVisitors = await getOnlineVisitors();
- const displayConversations = liveConversations; // NO MOCK DATA - only show real Zendesk tickets
- const isLiveData = liveConversations.length > 0;
-
- const activeChats = displayConversations.filter(c => c.status === 'active').length;
- const pendingChats = displayConversations.filter(c => c.status === 'pending').length;
- const resolvedChats = displayConversations.filter(c => c.status === 'resolved').length;
- const onlineCount = onlineVisitors.length;
-
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Dashboard</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- padding: 20px;
- }
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .header h1 {
- color: #333;
- font-size: 32px;
- margin-bottom: 10px;
- }
- .agent-status {
- display: inline-block;
- background: #10b981;
- color: white;
- padding: 6px 12px;
- border-radius: 20px;
- font-size: 14px;
- font-weight: 600;
- }
- .zendesk-link {
- display: inline-block;
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 8px;
- text-decoration: none;
- margin-left: 15px;
- font-size: 14px;
- transition: background 0.3s;
- }
- .zendesk-link:hover {
- background: #0288d1;
- }
- .tabs {
- display: flex;
- gap: 10px;
- margin-bottom: 20px;
- }
- .tab-button {
- background: white;
- color: #03a9f4;
- padding: 12px 24px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-size: 16px;
- font-weight: 600;
- transition: all 0.3s;
- }
- .tab-button:hover {
- background: #f3f4f6;
- }
- .tab-button.active {
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- }
- .tab-content {
- display: none;
- }
- .tab-content.active {
- display: block;
- }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stat-card {
- background: white;
- padding: 24px;
- border-radius: 12px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .stat-value {
- font-size: 32px;
- font-weight: 700;
- color: #03a9f4;
- margin-bottom: 5px;
- }
- .stat-label {
- color: #666;
- font-size: 14px;
- }
- .section {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .section h2 {
- color: #333;
- font-size: 24px;
- margin-bottom: 20px;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .chat-container {
- display: flex;
- flex-direction: column;
- height: 600px;
- }
- .chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 20px;
- background: #f9fafb;
- border-radius: 12px;
- margin-bottom: 20px;
- }
- .message {
- margin-bottom: 16px;
- padding: 12px 16px;
- border-radius: 12px;
- max-width: 80%;
- }
- .message.user {
- background: #03a9f4;
- color: white;
- margin-left: auto;
- }
- .message.assistant {
- background: white;
- color: #333;
- border: 1px solid #e0e0e0;
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- .chat-input {
- flex: 1;
- padding: 14px;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- font-size: 16px;
- font-family: inherit;
- }
- .send-button {
- padding: 14px 28px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 12px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- .send-button:hover {
- transform: translateY(-2px);
- }
- .suggestion-chips {
- display: flex;
- gap: 10px;
- margin-bottom: 15px;
- flex-wrap: wrap;
- }
- .chip {
- background: #f3f4f6;
- color: #03a9f4;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 14px;
- cursor: pointer;
- border: 2px solid transparent;
- transition: all 0.3s;
- }
- .chip:hover {
- background: #03a9f4;
- color: white;
- border-color: #03a9f4;
- }
- .conversation-card {
- background: white;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 15px;
- transition: all 0.3s;
- }
- .conversation-card:hover {
- border-color: #03a9f4;
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(3, 169, 244, 0.1);
- }
- .conversation-header {
- display: flex;
- justify-content: space-between;
- align-items: start;
- margin-bottom: 10px;
- }
- .customer-name {
- font-size: 18px;
- font-weight: 600;
- color: #333;
- }
- .status-badge {
- padding: 4px 12px;
- border-radius: 12px;
- font-size: 12px;
- font-weight: 600;
- }
- .status-active { background: #d1fae5; color: #065f46; }
- .status-pending { background: #fef3c7; color: #92400e; }
- .status-resolved { background: #dbeafe; color: #1e40af; }
- .priority-high { background: #fee2e2; color: #991b1b; }
- .priority-normal { background: #e0e0e0; color: #666; }
- .priority-low { background: #f3f4f6; color: #888; }
- .conversation-subject {
- color: #03a9f4;
- font-size: 14px;
- font-weight: 500;
- margin-bottom: 8px;
- }
- .last-message {
- color: #666;
- font-size: 14px;
- line-height: 1.5;
- margin-bottom: 10px;
- }
- .conversation-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 10px;
- border-top: 1px solid #e0e0e0;
- }
- .timestamp {
- font-size: 12px;
- color: #888;
- }
- .action-button {
- padding: 6px 16px;
- background: #03a9f4;
- color: white;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 14px;
- transition: background 0.3s;
- }
- .action-button:hover {
- background: #0288d1;
- }
- .modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0,0,0,0.7);
- z-index: 1000;
- align-items: center;
- justify-content: center;
- }
- .modal.active {
- display: flex;
- }
- .modal-content {
- background: white;
- border-radius: 16px;
- width: 90%;
- max-width: 800px;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 25px 50px rgba(0,0,0,0.5);
- }
- .modal-header {
- padding: 25px 30px;
- border-bottom: 2px solid #e0e0e0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .modal-header h2 {
- color: #03a9f4;
- margin: 0;
- font-size: 22px;
- }
- .modal-close {
- background: #ef4444;
- color: white;
- border: none;
- width: 36px;
- height: 36px;
- border-radius: 50%;
- cursor: pointer;
- font-size: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.3s;
- }
- .modal-close:hover {
- background: #dc2626;
- }
- .modal-body {
- padding: 25px 30px;
- overflow-y: auto;
- flex: 1;
- }
- .chat-message {
- margin-bottom: 20px;
- display: flex;
- flex-direction: column;
- }
- .chat-message.customer {
- align-items: flex-start;
- }
- .chat-message.agent {
- align-items: flex-end;
- }
- .message-sender {
- font-size: 12px;
- font-weight: 600;
- color: #666;
- margin-bottom: 6px;
- text-transform: uppercase;
- }
- .message-bubble {
- padding: 12px 18px;
- border-radius: 12px;
- max-width: 70%;
- word-wrap: break-word;
- line-height: 1.5;
- }
- .chat-message.customer .message-bubble {
- background: #f3f4f6;
- color: #333;
- border-bottom-left-radius: 4px;
- }
- .chat-message.agent .message-bubble {
- background: #03a9f4;
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message-time {
- font-size: 11px;
- color: #888;
- margin-top: 4px;
- }
- @keyframes pulse {
- 0%, 100% { transform: scale(1); box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); }
- 50% { transform: scale(1.02); box-shadow: 0 15px 50px rgba(255, 107, 107, 0.5); }
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>💬 Customer Chat Monitor</h1>
- <p style="color: #666; margin-top: 5px;">Free Customer Support & Chat Management (No paid services required)</p>
- <div style="margin-top: 15px; display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
- <span class="agent-status">● ONLINE</span>
- <span style="color: #888; font-size: 14px;">Port 9884</span>
- <a href="${ZENDESK_CHAT_URL}" target="_blank" class="zendesk-link">🔗 Open Zendesk Chat</a>
- <button onclick="location.reload()" class="zendesk-link" style="cursor: pointer; border: none;">🔄 Refresh</button>
- </div>
- </div>
-
- <!-- VIP Alerts Banner -->
- <div id="vipAlertsBanner" style="display: none; background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); border: 3px solid #ff6b6b; border-radius: 15px; padding: 20px; margin: 20px 0; box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); animation: pulse 2s infinite;">
- <div style="display: flex; align-items: center; gap: 15px;">
- <div style="font-size: 3em;">🚨</div>
- <div style="flex: 1;">
- <h2 style="color: #d32f2f; margin: 0 0 10px 0; font-size: 1.5em;">⚡ HIGH-VALUE OPPORTUNITY DETECTED</h2>
- <div id="vipAlertsContent"></div>
- </div>
- </div>
- </div>
-
- <div class="tabs">
- <button class="tab-button active" onclick="switchTab('overview')">📊 Overview</button>
- <button class="tab-button" onclick="switchTab('chat')">💬 AI Assistant</button>
- <button class="tab-button" onclick="switchTab('conversations')">💭 Active Chats</button>
- <button class="tab-button" onclick="switchTab('analytics')">📈 Analytics</button>
- </div>
-
- <!-- Overview Tab -->
- <div id="tab-overview" class="tab-content active">
- <div class="stats-grid">
- <div class="stat-card" style="border-left: 4px solid #10b981;">
- <div class="stat-value" style="color: #10b981;">${onlineCount}</div>
- <div class="stat-label">🟢 Currently Online</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${activeChats}</div>
- <div class="stat-label">Active Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${pendingChats}</div>
- <div class="stat-label">Pending Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${resolvedChats}</div>
- <div class="stat-label">Resolved Today</div>
- </div>
- </div>
-
- <div class="section" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; padding: 25px; border-radius: 16px; margin-bottom: 20px;">
- <h2 style="color: white; margin-bottom: 15px;">🟢 Currently Online (${onlineCount})</h2>
- ${onlineVisitors.length > 0 ? onlineVisitors.map(visitor => `
- <div style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; margin-bottom: 10px; border-left: 4px solid ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'};">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
- <strong style="font-size: 1.1em;">${visitor.name}</strong>
- <span style="background: ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'}; color: white; padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: 600;">
- ${visitor.status === 'waiting' ? '⏳ Waiting' : visitor.status === 'chatting' ? '💬 Chatting' : '👀 Browsing'}
- </span>
- </div>
- <div style="font-size: 0.9em; opacity: 0.95;">
- 📧 ${visitor.email}<br>
- ${visitor.pageUrl ? `📄 ${visitor.pageUrl}<br>` : ''}
- 🕐 ${new Date(visitor.timestamp).toLocaleTimeString()}
- </div>
- </div>
- `).join('') : '<p style="text-align: center; padding: 20px; opacity: 0.9;">No visitors currently online</p>'}
- </div>
-
- <div class="section">
- <h2>💭 Recent Conversations ${isLiveData ? '<span style="color: #28a745; font-size: 0.8em;">● LIVE</span>' : '<span style="color: #ffc107; font-size: 0.8em;">● DEMO</span>'}</h2>
- <div id="conversationsList">
- ${displayConversations.slice(0, 3).map(conv => `
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-${conv.status}">${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-${conv.priority}">${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">${conv.subject}</div>
- <div class="last-message">${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- `).join('')}
- </div>
- </div>
-
- <div class="section">
- <h2>🔗 Zendesk Integration</h2>
- <p style="color: #666; margin-bottom: 15px;">Connected to: <strong>${ZENDESK_DOMAIN}</strong></p>
- <p style="color: #666; margin-bottom: 15px;">Account: <strong>${ZENDESK_EMAIL}</strong></p>
- <a href="${ZENDESK_CHAT_URL}" target="_blank" class="action-button" style="display: inline-block; text-decoration: none;">Open Zendesk Chat Dashboard</a>
- </div>
- </div>
-
- <!-- Chat Tab -->
- <div id="tab-chat" class="tab-content">
- <div class="section">
- <h2>💬 AI Support Assistant</h2>
- <p style="color: #666; margin-bottom: 20px;">Get AI-powered help with customer support, chat responses, and Zendesk management</p>
-
- <div class="suggestion-chips">
- <span class="chip" onclick="sendSuggestion('Suggest response for installation question')">Installation help</span>
- <span class="chip" onclick="sendSuggestion('How to handle product availability inquiry')">Product availability</span>
- <span class="chip" onclick="sendSuggestion('Best practices for customer support')">Support best practices</span>
- <span class="chip" onclick="sendSuggestion('Generate empathetic response template')">Response templates</span>
- </div>
-
- <div class="chat-container">
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- Hello! I'm your Zendesk Chat AI Assistant powered by Claude 4. I can help you with:
- <br><br>
- • Suggesting responses to customer inquiries<br>
- • Customer support best practices<br>
- • Chat management strategies<br>
- • Handling difficult conversations<br>
- • Zendesk tips and workflows<br>
- <br>
- How can I assist you with customer support today?
- </div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- class="chat-input"
- id="chatInput"
- placeholder="Ask about customer support, chat responses..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-button" onclick="sendMessage()">Send</button>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Conversations Tab -->
- <div id="tab-conversations" class="tab-content">
- <div class="section">
- <h2>💭 All Conversations</h2>
- <div id="allConversations">
- <p style="color: #888;">Loading conversations...</p>
- </div>
- </div>
- </div>
-
- <!-- Analytics Tab -->
- <div id="tab-analytics" class="tab-content">
- <div class="section">
- <h2>📈 Chat Analytics</h2>
- <div class="stats-grid">
- <div class="stat-card">
- <div class="stat-value">${stats.customerSatisfaction}</div>
- <div class="stat-label">Customer Satisfaction</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.totalQueries}</div>
- <div class="stat-label">Total AI Queries</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${displayConversations.length}</div>
- <div class="stat-label">Total Conversations</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.avgResponseTime}</div>
- <div class="stat-label">Avg Response Time</div>
- </div>
- </div>
- <div style="background: #f9fafb; padding: 20px; border-radius: 12px; margin-top: 20px;">
- <p style="color: #666;">Connect to Zendesk Analytics API for real-time reporting and advanced metrics.</p>
- </div>
- </div>
- </div>
- </div>
-
- <script>
- function switchTab(tabName) {
- document.querySelectorAll('.tab-content').forEach(tab => {
- tab.classList.remove('active');
- });
- document.querySelectorAll('.tab-button').forEach(btn => {
- btn.classList.remove('active');
- });
-
- document.getElementById('tab-' + tabName).classList.add('active');
- event.target.classList.add('active');
-
- if (tabName === 'conversations') {
- loadConversations();
- }
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const message = input.value.trim();
-
- if (!message) return;
-
- const chatMessages = document.getElementById('chatMessages');
- chatMessages.innerHTML += \`
- <div class="message user">\${message}</div>
- \`;
-
- input.value = '';
- chatMessages.scrollTop = chatMessages.scrollHeight;
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
-
- chatMessages.innerHTML += \`
- <div class="message assistant">\${data.response.replace(/\\n/g, '<br>')}</div>
- \`;
- chatMessages.scrollTop = chatMessages.scrollHeight;
- } catch (error) {
- console.error('Chat error:', error);
- chatMessages.innerHTML += \`
- <div class="message assistant">Sorry, I encountered an error. Please try again.</div>
- \`;
- }
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function loadVIPAlerts() {
- try {
- const response = await fetch('/api/vip-alerts');
- const data = await response.json();
-
- if (data.alerts && data.alerts.length > 0) {
- const banner = document.getElementById('vipAlertsBanner');
- const content = document.getElementById('vipAlertsContent');
-
- const html = data.alerts.map(alert => \`
- <div style="background: white; padding: 15px; border-radius: 10px; margin-bottom: 10px; border-left: 5px solid #ff6b6b;">
- <div style="font-weight: 700; font-size: 1.2em; color: #d32f2f; margin-bottom: 5px;">
- \${alert.name} - \${alert.company.toUpperCase()}
- </div>
- <div style="color: #666; margin-bottom: 5px;">
- 📧 \${alert.email}
- </div>
- <div style="color: #ff6b6b; font-weight: 600; font-size: 0.9em;">
- 🎯 \${alert.reason}
- </div>
- <div style="color: #888; font-size: 0.85em; margin-top: 5px;">
- ⏰ \${new Date(alert.timestamp).toLocaleString()}
- </div>
- </div>
- \`).join('');
-
- content.innerHTML = html;
- banner.style.display = 'block';
- } else {
- document.getElementById('vipAlertsBanner').style.display = 'none';
- }
- } catch (error) {
- console.error('Error loading VIP alerts:', error);
- }
- }
-
- async function loadConversations() {
- const container = document.getElementById('allConversations');
- try {
- const response = await fetch('/api/conversations');
- const data = await response.json();
-
- let html = '';
- data.conversations.forEach(conv => {
- html += \`
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">\${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">\${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-\${conv.status}">\${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-\${conv.priority}">\${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">\${conv.subject}</div>
- <div class="last-message">\${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">\${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('\${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/\${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- \`;
- });
-
- container.innerHTML = html;
- } catch (error) {
- console.error('Load conversations error:', error);
- container.innerHTML = '<p style="color: #ef4444;">Error loading conversations</p>';
- }
- }
-
- async function viewChat(chatId) {
- try {
- const response = await fetch(\`/api/conversation/\${chatId}\`);
- const data = await response.json();
-
- const modal = document.getElementById('chatModal');
- const modalTitle = document.getElementById('modalTitle');
- const modalBody = document.getElementById('modalBody');
-
- modalTitle.textContent = \`Chat with \${data.customerName}\`;
-
- let messagesHtml = '';
- data.messages.forEach(msg => {
- const time = new Date(msg.timestamp).toLocaleTimeString();
- messagesHtml += \`
- <div class="chat-message \${msg.sender}">
- <div class="message-sender">\${msg.sender === 'customer' ? data.customerName : 'Support Agent'}</div>
- <div class="message-bubble">\${msg.message}</div>
- <div class="message-time">\${time}</div>
- </div>
- \`;
- });
-
- modalBody.innerHTML = messagesHtml;
- modal.classList.add('active');
-
- // Scroll to bottom
- modalBody.scrollTop = modalBody.scrollHeight;
- } catch (error) {
- console.error('Error loading chat:', error);
- alert('Failed to load chat conversation');
- }
- }
-
- function closeModal() {
- document.getElementById('chatModal').classList.remove('active');
- }
-
- // Close modal when clicking outside
- document.addEventListener('click', (e) => {
- const modal = document.getElementById('chatModal');
- if (e.target === modal) {
- closeModal();
- }
- });
-
- // Load VIP alerts on page load and refresh every 30 seconds
- loadVIPAlerts();
- setInterval(loadVIPAlerts, 30000);
- </script>
-
- <!-- Chat Modal -->
- <div id="chatModal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h2 id="modalTitle">Chat Conversation</h2>
- <button class="modal-close" onclick="closeModal()">×</button>
- </div>
- <div class="modal-body" id="modalBody">
- <!-- Messages will be loaded here -->
- </div>
- </div>
- </div>
- </body>
- </html>
- `);
-});
-
-// API Endpoints
-
-// Chat endpoint
-app.post('/api/chat', requireAuth, async (req: Request, res: Response) => {
- try {
- const { message } = req.body;
- stats.totalQueries++;
-
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- req.session.chatHistory.push({
- role: 'user',
- content: message
- });
-
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- const systemPrompt = `You are the Zendesk Chat Support Agent for Designer Wallcoverings, powered by Claude 4 Sonnet.
-
-Your role is to assist customer support team with:
-
-CAPABILITIES:
-- Suggest professional and empathetic customer responses
-- Provide customer support best practices
-- Help manage chat conversations
-- Offer conflict resolution strategies
-- Zendesk platform tips and workflows
-- Response templates for common scenarios
-
-CONTEXT:
-- Company: Designer Wallcoverings (wallcovering/wallpaper retailer)
-- Zendesk Domain: ${ZENDESK_DOMAIN}
-- Current Active Chats: ${displayConversations.filter(c => c.status === 'active').length}
-- Pending Chats: ${displayConversations.filter(c => c.status === 'pending').length}
-
-COMMON CUSTOMER INQUIRIES:
-- Product information and availability
-- Installation help and tips
-- Order tracking and status
-- Return and exchange policies
-- Design consultation
-- Sample requests
-
-When helping with responses:
-1. Be empathetic and professional
-2. Provide clear, actionable information
-3. Include product knowledge when relevant
-4. Maintain Designer Wallcoverings' brand voice
-5. Suggest follow-up actions
-
-Be helpful, supportive, and provide excellent customer service guidance.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 1024,
- system: systemPrompt,
- messages: req.session.chatHistory
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error processing your request.';
-
- req.session.chatHistory.push({
- role: 'assistant',
- content: assistantMessage
- });
-
- logActivity('Chat Query', `User asked: "${message.substring(0, 50)}..."`);
-
- res.json({ response: assistantMessage });
- } catch (error) {
- console.error('Chat error:', error);
- res.status(500).json({ error: 'Failed to process chat message' });
- }
-});
-
-// Get conversations
-app.get('/api/conversations', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- res.json({ conversations: liveConversations });
-});
-
-// Get specific conversation by ID
-app.get('/api/conversation/:id', requireAuth, async (req: Request, res: Response) => {
- const chatId = req.params.id;
- const liveConversations = await getConversations();
- let conversation = liveConversations.find(c => c.id === chatId);
-
- if (!conversation) {
- return res.status(404).json({ error: 'Conversation not found' });
- }
-
- // Fetch comments if not already loaded
- if (conversation.messages.length === 0) {
- conversation.messages = await fetchTicketComments(chatId);
- }
-
- logActivity('View Chat', `Viewed conversation ${chatId} with ${conversation.customerName}`);
- res.json(conversation);
-});
-
-// Get statistics
-app.get('/api/stats', requireAuth, (req: Request, res: Response) => {
- res.json(stats);
-});
-
-// Get VIP alerts
-app.get('/api/vip-alerts', requireAuth, (req: Request, res: Response) => {
- res.json({ alerts: vipAlerts });
-});
-
-// Get activity log
-app.get('/api/activity', requireAuth, (req: Request, res: Response) => {
- res.json({ activities: activityLog });
-});
-
-// Start server
-
-// Metrics endpoint for daily reporting
-app.get("/api/metrics", (req: Request, res: Response) => {
- res.json({
- status: "online",
- uptime: process.uptime(),
- responseTime: 0,
- tasksCompleted: 0,
- errorsToday: 0,
- lastActivity: new Date().toISOString(),
- qnaReadiness: 100
- });
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log(`💬 Zendesk Chat Agent running on port ${PORT}`);
- console.log(`Dashboard: http://45.61.58.125:${PORT}`);
- console.log(`Zendesk: ${ZENDESK_CHAT_URL}`);
- logActivity('System Start', 'Zendesk Chat Agent initialized');
-});
diff --git a/DW-Agents/dw-agents/digital-samples-agent.ts.backup b/DW-Agents/dw-agents/digital-samples-agent.ts.backup
deleted file mode 100644
index 0ab43115..00000000
--- a/DW-Agents/dw-agents/digital-samples-agent.ts.backup
+++ /dev/null
@@ -1,1246 +0,0 @@
-/**
- * DW-Agents: Digital Samples Agent
- *
- * Monitors Shopify orders for DIG-series products and automates sample delivery:
- * 1. Detects DIG-* SKU orders from Shopify webhooks
- * 2. Searches Google Drive for matching sample files
- * 3. Resizes images to 24" wide at 150 DPI
- * 4. Posts to Slack #new-products channel with approval workflow
- * 5. Sends files to customers via email
- *
- * Port: 9879
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import { WebClient } from '@slack/web-api';
-import sharp from 'sharp';
-import fetch from 'node-fetch';
-import { google } from 'googleapis';
-import dotenv from 'dotenv';
-import { AgentMemory } from './shared-memory-system';
-
-// Import shared UI components
-const { getUniversalHeader, getThemedPageWrapper, getThemeStyles } = require('./shared-ui-components.js');
-
-// Load environment variables
-dotenv.config();
-
-const app = express();
-const PORT = 9879;
-// Initialize agent memory system
-const agentMemory = new AgentMemory('digital-samples');
-console.log('📚 Memory system initialized for digital-samples');
-
-
-// Authentication
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-// Shopify configuration
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || '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';
-const API_VERSION = '2024-07';
-
-// Slack configuration
-const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || 'xoxb-3958182050256-9814366234406-HiR1ybK6HT8JViWDEpjXpmsq';
-const SLACK_CHANNEL = 'new-products-on-shopify';
-const slackClient = new WebClient(SLACK_BOT_TOKEN);
-
-// Anthropic Claude
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY,
-});
-
-// Google Drive API setup
-const oauth2Client = new google.auth.OAuth2(
- process.env.GOOGLE_CLIENT_ID,
- process.env.GOOGLE_CLIENT_SECRET,
- 'http://localhost'
-);
-
-oauth2Client.setCredentials({
- refresh_token: process.env.GOOGLE_REFRESH_TOKEN
-});
-
-const drive = google.drive({ version: 'v3', auth: oauth2Client });
-
-// Statistics tracking
-let stats = {
- totalOrders: 0,
- digOrdersFound: 0,
- filesProcessed: 0,
- slackPostsSent: 0,
- emailsSent: 0,
- lastOrder: null as {
- orderId: string;
- customerName: string;
- digItems: string[];
- timestamp: Date;
- } | null,
- pendingApprovals: [] as Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- resizedImageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- }>
-};
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- type: 'order' | 'processing' | 'slack' | 'email' | 'error';
- message: string;
-}> = [];
-
-function addLog(message: string, type: 'order' | 'processing' | 'slack' | 'email' | 'error' = 'processing') {
- const entry = {
- timestamp: new Date(),
- type,
- message
- };
- activityLog.unshift(entry);
-
- // Keep only last 100 entries
- if (activityLog.length > 100) {
- activityLog.pop();
- }
-
- console.log(`[${type.toUpperCase()}] ${message}`);
-}
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-digital-samples-agent-2025',
- resave: false,
- saveUninitialized: false,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 24 * 60 * 60 * 1000,
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- }
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // TEMPORARY: Auth disabled for 4 hours
- return next();
-
- /* Original auth:
-
- // TEMPORARY: Disable auth for 4 hours (until 4 hours from server restart)
- const serverStartTime = Date.now();
- const fourHoursInMs = 4 * 60 * 60 * 1000;
- if (Date.now() - serverStartTime < fourHoursInMs) {
- return next();
- }
-
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
- */
-};
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Digital Samples Agent</title>
- ${getThemeStyles('modern-minimalist')}
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #4facfe; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>📦 Digital Samples Agent</h1>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
-
- // Set SSO cookie for cross-agent authentication
- setSSOToken(res);
-
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard
-app.get('/', requireAuth, (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Digital Samples Agent - DW-Agents</title>
- ${getThemeStyles('modern-minimalist')}
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #4facfe; font-size: 2.5em; margin-bottom: 10px; }
- .header .subtitle { color: #666; font-size: 1.2em; }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 20px;
- margin-bottom: 20px;
- }
- .stat-card {
- background: white;
- padding: 25px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stat-card h3 { color: #888; font-size: 0.9em; text-transform: uppercase; margin-bottom: 10px; }
- .stat-card .value { font-size: 2.5em; font-weight: bold; color: #4facfe; }
- .approvals-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .approvals-section h2 { color: #4facfe; margin-bottom: 20px; }
- .approval-card {
- background: #f5f5f5;
- padding: 20px;
- border-radius: 10px;
- margin-bottom: 15px;
- border-left: 4px solid #4facfe;
- }
- .approval-card img { max-width: 200px; border-radius: 8px; margin: 10px 0; }
- .btn {
- padding: 10px 20px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-right: 10px;
- }
- .btn-approve { background: #4CAF50; color: white; }
- .btn-reject { background: #f44336; color: white; }
- .log-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .log-entry {
- padding: 10px;
- margin: 5px 0;
- border-radius: 6px;
- font-family: 'Courier New', monospace;
- font-size: 0.9em;
- }
- .log-entry.order { background: #e3f2fd; border-left: 4px solid #2196F3; }
- .log-entry.processing { background: #fff3e0; border-left: 4px solid #FF9800; }
- .log-entry.slack { background: #f3e5f5; border-left: 4px solid #9C27B0; }
- .log-entry.email { background: #e8f5e9; border-left: 4px solid #4CAF50; }
- .log-entry.error { background: #ffebee; border-left: 4px solid #f44336; }
- .drive-file-card {
- background: #f5f5f5;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 4px solid #4facfe;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- gap: 15px;
- align-items: center;
- }
- .drive-file-card:hover {
- background: #e9ecef;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .drive-file-card.selected {
- background: #e8f5e9;
- border-left-color: #4CAF50;
- }
- .drive-file-thumbnail {
- width: 60px;
- height: 60px;
- border-radius: 6px;
- object-fit: cover;
- background: #e0e0e0;
- flex-shrink: 0;
- }
- .drive-file-info {
- flex: 1;
- }
- .drive-file-name {
- font-weight: 600;
- color: #333;
- margin-bottom: 5px;
- }
- .drive-file-meta {
- color: #888;
- font-size: 0.85em;
- }
- </style>
-</head>
-<body>
- <div class="header">
- <h1>📦 Digital Samples Agent</h1>
- <div class="subtitle">Automated DIG-series order processing & delivery</div>
- <div style="text-align: right; margin-top: 10px;">
- <a href="/logout" style="color: #4facfe; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-grid">
- <div class="stat-card">
- <h3>Total Orders</h3>
- <div class="value" id="totalOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>DIG Orders</h3>
- <div class="value" id="digOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>Files Processed</h3>
- <div class="value" id="filesProcessed">0</div>
- </div>
- <div class="stat-card">
- <h3>Pending Approvals</h3>
- <div class="value" id="pendingApprovals">0</div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>💬 Slack #graphics-needed Channel</h2>
- <p style="color: #666; margin-bottom: 15px;">Recent messages and requests from the graphics team</p>
- <div id="slackMessages">
- <p style="color: #888;">Loading Slack messages...</p>
- </div>
- <div style="margin-top: 15px;">
- <input type="text" id="slackMessageInput" placeholder="Type a message to #graphics-needed..."
- style="width: calc(100% - 120px); padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px; margin-right: 10px;">
- <button onclick="sendSlackMessage()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 📤 Send
- </button>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📁 Google Drive - Digital Samples Library</h2>
- <p style="color: #666; margin-bottom: 15px;">Browse and attach sample files from Google Drive</p>
-
- <div style="display: flex; gap: 10px; margin-bottom: 15px;">
- <input type="text" id="driveSearchInput" placeholder="Search files by name or SKU..."
- style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <select id="driveFilterType" style="padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="all">All Files</option>
- <option value="images">Images Only</option>
- <option value="pdfs">PDFs Only</option>
- </select>
- <button onclick="refreshDriveFiles()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh
- </button>
- </div>
-
- <!-- Spreadsheet-style table -->
- <div style="background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
- <div style="overflow-x: auto; max-height: 600px;">
- <table id="driveFilesTable" style="width: 100%; border-collapse: collapse;">
- <thead>
- <tr style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; position: sticky; top: 0; z-index: 10;">
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Thumbnail</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">File Name</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">SKU</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Type</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Size</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Modified</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Actions</th>
- </tr>
- </thead>
- <tbody id="driveFilesList">
- <tr>
- <td colspan="7" style="padding: 30px; text-align: center; color: #888;">Loading Google Drive files...</td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
-
- <div id="selectedFilesPanel" style="display: none; margin-top: 20px; padding: 20px; background: #e8f5e9; border-radius: 8px; border-left: 4px solid #4CAF50;">
- <h3 style="margin: 0 0 15px 0; color: #2E7D32;">📎 Selected Files (<span id="selectedCount">0</span>)</h3>
- <div id="selectedFilesList"></div>
- <div style="margin-top: 15px; display: flex; gap: 10px;">
- <select id="attachToOrder" style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="">Select an order to attach files...</option>
- </select>
- <button onclick="attachFilesToOrder()" style="background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ✅ Attach to Order
- </button>
- <button onclick="clearSelectedFiles()" style="background: #f44336; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ❌ Clear
- </button>
- </div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📋 Pending Approvals</h2>
- <div id="approvalsList">
- <p style="color: #888;">No pending approvals</p>
- </div>
- </div>
-
- <div class="approvals-section">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
- <h2 style="margin: 0;">📦 All Samples by Date</h2>
- <div style="display: flex; gap: 10px; align-items: center;">
- <span id="lastRefresh" style="color: #888; font-size: 0.9em;">Last updated: --:--</span>
- <button onclick="manualRefresh()" class="btn" style="background: #4facfe; color: white; padding: 8px 16px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh Now
- </button>
- </div>
- </div>
- <div style="margin-bottom: 15px;">
- <input
- type="text"
- id="searchSamples"
- placeholder="Search by customer, SKU, or order ID..."
- style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;"
- >
- </div>
- <div id="allSamplesList">
- <p style="color: #888;">Loading samples history...</p>
- </div>
- </div>
-
- <div class="log-section">
- <h2>📝 Activity Log</h2>
- <div id="activityLog"></div>
- </div>
-
- <script>
- // Clean SKU - extract only DIG-number (e.g., "DIG-25-some-extra" becomes "DIG-25")
- function cleanSku(sku) {
- if (!sku) return '';
- // Match DIG- followed by digits, ignore everything after
- const match = sku.match(/DIG-\d+/i);
- return match ? match[0] : sku;
- }
-
- async function updateDashboard() {
- try {
- const statsData = await fetch('/api/stats').then(r => r.json());
-
- document.getElementById('totalOrders').textContent = statsData.totalOrders;
- document.getElementById('digOrders').textContent = statsData.digOrdersFound;
- document.getElementById('filesProcessed').textContent = statsData.filesProcessed;
- document.getElementById('pendingApprovals').textContent = statsData.pendingApprovals.length;
-
- // Update approvals list
- const approvalsList = document.getElementById('approvalsList');
- if (statsData.pendingApprovals.length === 0) {
- approvalsList.innerHTML = '<p style="color: #888;">No pending approvals</p>';
- } else {
- approvalsList.innerHTML = statsData.pendingApprovals.map(approval => \`
- <div class="approval-card">
- <h3>\${approval.productTitle} (SKU: \${cleanSku(approval.digSku)})</h3>
- <p><strong>Customer:</strong> \${approval.customerName} (\${approval.customerEmail})</p>
- <p><strong>Order ID:</strong> \${approval.orderId}</p>
- <p><strong>Time:</strong> \${new Date(approval.timestamp).toLocaleString()}</p>
- <img src="\${approval.resizedImageUrl || approval.imageUrl}" alt="Product">
- <div>
- <button class="btn btn-approve" onclick="approveOrder('\${approval.id}')">✅ Approve & Send</button>
- <button class="btn btn-reject" onclick="rejectOrder('\${approval.id}')">❌ Reject</button>
- </div>
- </div>
- \`).join('');
- }
-
- // Update Slack messages
- const slackData = await fetch('/api/slack/messages').then(r => r.json());
- const slackDiv = document.getElementById('slackMessages');
- if (slackData.messages && slackData.messages.length > 0) {
- slackDiv.innerHTML = slackData.messages.map(msg => \`
- <div style="background: #f5f5f5; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid #4facfe;">
- <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
- <strong style="color: #333;">\${msg.user}</strong>
- <span style="color: #888; font-size: 0.85em;">\${new Date(msg.timestamp * 1000).toLocaleString()}</span>
- </div>
- <div style="color: #555;">\${msg.text}</div>
- </div>
- \`).join('');
- } else {
- slackDiv.innerHTML = '<p style="color: #888;">No recent messages in #graphics-needed</p>';
- }
-
- // Update all samples list
- const samplesData = await fetch('/api/samples/all').then(r => r.json());
- window.allSamplesData = samplesData.samples || [];
- renderSamples(window.allSamplesData, samplesData);
-
- // Update activity log
- const logs = await fetch('/api/activity').then(r => r.json());
- const logDiv = document.getElementById('activityLog');
- logDiv.innerHTML = logs.map(log => \`
- <div class="log-entry \${log.type}">
- [\${new Date(log.timestamp).toLocaleTimeString()}] \${log.message}
- </div>
- \`).join('');
-
- updateLastRefreshTime();
- } catch (error) {
- console.error('Update error:', error);
- }
- }
-
- // Render samples function
- function renderSamples(samples, data) {
- const samplesList = document.getElementById('allSamplesList');
- if (samples && samples.length > 0) {
- const totalCount = data.total || samples.length;
- const last90Count = data.last90Days || samples.filter(s => {
- const daysAgo = (Date.now() - new Date(s.timestamp).getTime()) / (1000 * 60 * 60 * 24);
- return daysAgo <= 90;
- }).length;
-
- samplesList.innerHTML = \`
- <div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #2196F3;">
- <h3 style="margin: 0 0 10px 0; color: #1976D2;">📊 Summary</h3>
- <p style="margin: 5px 0;"><strong>DIG Samples (Last 90 Days):</strong> \${last90Count}</p>
- <p style="margin: 5px 0;"><strong>Total Displayed:</strong> \${samples.length}</p>
- </div>
- \` + samples.map(sample => {
- const statusColor = sample.status === 'approved' ? '#4CAF50' :
- sample.status === 'pending' ? '#FFA500' :
- sample.status === 'ordered' ? '#2196F3' : '#f44336';
- const statusText = sample.status === 'ordered' ? 'ORDERED' : sample.status.toUpperCase();
- const orderLink = sample.orderUrl ?
- \`<a href="\${sample.orderUrl}" target="_blank" style="color: #4facfe; text-decoration: none; font-weight: 600;">🔗 View Order in Shopify →</a>\` :
- '';
-
- return \`
- <div class="approval-card" style="opacity: \${sample.status === 'approved' || sample.status === 'ordered' ? '0.85' : '1'}; position: relative;">
- <h3>\${sample.productTitle}</h3>
- <p><strong>SKU:</strong> <span style="background: #f0f0f0; padding: 3px 8px; border-radius: 4px; font-family: monospace;">\${cleanSku(sample.digSku)}</span></p>
- <p><strong>Customer:</strong> \${sample.customerName} (\${sample.customerEmail})</p>
- <p><strong>Order:</strong> \${sample.orderId || sample.orderNumber || 'N/A'}</p>
- <p><strong>Status:</strong> <span style="color: \${statusColor}; font-weight: bold; background: \${statusColor}22; padding: 3px 10px; border-radius: 4px;">\${statusText}</span></p>
- <p><strong>Date:</strong> \${new Date(sample.timestamp).toLocaleString()}</p>
- \${orderLink ? \`<p style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;">\${orderLink}</p>\` : ''}
- </div>
- \`;
- }).join('');
- } else {
- samplesList.innerHTML = '<p style="color: #888;">No samples found</p>';
- }
- }
-
- // Search functionality
- window.allSamplesData = [];
- document.getElementById('searchSamples').addEventListener('input', function(e) {
- const searchTerm = e.target.value.toLowerCase();
-
- if (!searchTerm) {
- // Show all samples when search is empty
- fetch('/api/samples/all')
- .then(r => r.json())
- .then(data => {
- window.allSamplesData = data.samples || [];
- renderSamples(window.allSamplesData, data);
- });
- return;
- }
-
- const filtered = window.allSamplesData.filter(sample =>
- (sample.customerName && sample.customerName.toLowerCase().includes(searchTerm)) ||
- (sample.customerEmail && sample.customerEmail.toLowerCase().includes(searchTerm)) ||
- (sample.digSku && sample.digSku.toLowerCase().includes(searchTerm)) ||
- (sample.orderId && sample.orderId.toLowerCase().includes(searchTerm)) ||
- (sample.orderNumber && sample.orderNumber.toLowerCase().includes(searchTerm)) ||
- (sample.productTitle && sample.productTitle.toLowerCase().includes(searchTerm))
- );
-
- renderSamples(filtered, { total: filtered.length, last90Days: filtered.length });
- });
-
- function approveOrder(id) {
- fetch(\`/api/approve/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- function rejectOrder(id) {
- fetch(\`/api/reject/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- async function sendSlackMessage() {
- const input = document.getElementById('slackMessageInput');
- const message = input.value.trim();
-
- if (!message) {
- alert('Please enter a message');
- return;
- }
-
- try {
- const response = await fetch('/api/slack/send', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
- if (data.success) {
- input.value = '';
- updateDashboard(); // Refresh to show new message
- } else {
- alert('Failed to send message: ' + data.error);
- }
- } catch (error) {
- alert('Error sending message');
- }
- }
-
- function manualRefresh() {
- const button = event.target;
- button.disabled = true;
- button.textContent = '⏳ Refreshing...';
-
- updateDashboard().then(() => {
- button.disabled = false;
- button.textContent = '🔄 Refresh Now';
- });
- }
-
- function updateLastRefreshTime() {
- const now = new Date();
- const timeStr = now.toLocaleTimeString();
- document.getElementById('lastRefresh').textContent = \`Last updated: \${timeStr}\`;
- }
-
- // Auto-refresh every 30 seconds
- setInterval(() => {
- updateDashboard();
- updateLastRefreshTime();
- }, 30000);
-
- // Initial load
- updateDashboard();
- updateLastRefreshTime();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.get('/api/activity', requireAuth, (req, res) => {
- res.json(activityLog.slice(0, 50));
-});
-
-// Get Slack channel ID
-async function getGraphicsNeededChannelId() {
- try {
- const result = await slackClient.conversations.list({
- types: 'public_channel,private_channel',
- limit: 200
- });
-
- const channel = result.channels?.find((c: any) => c.name === 'graphics-needed');
- if (channel) {
- return channel.id;
- }
-
- console.error('Channel #graphics-needed not found in:', result.channels?.map((c: any) => c.name));
- return null;
- } catch (error) {
- console.error('Error getting channel list:', error);
- return null;
- }
-}
-
-// Slack API endpoints
-app.get('/api/slack/messages', requireAuth, async (req, res) => {
- try {
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ messages: [], error: 'Channel #graphics-needed not found' });
- }
-
- const result = await slackClient.conversations.history({
- channel: channelId,
- limit: 20
- });
-
- const messages = result.messages?.map((msg: any) => ({
- user: msg.user || 'Unknown',
- text: msg.text,
- timestamp: msg.ts
- })) || [];
-
- res.json({ messages });
- } catch (error) {
- console.error('Slack fetch error:', error);
- res.json({ messages: [], error: String(error) });
- }
-});
-
-app.post('/api/slack/send', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ success: false, error: 'Channel #graphics-needed not found' });
- }
-
- // Try to join the channel first if we get not_in_channel error
- try {
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (postError: any) {
- // If not in channel, try to join it
- if (postError.data?.error === 'not_in_channel') {
- console.log('🔧 Bot not in channel, attempting to join #graphics-needed...');
- try {
- await slackClient.conversations.join({ channel: channelId });
- console.log('✅ Successfully joined #graphics-needed');
-
- // Retry posting message
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (joinError) {
- console.error('❌ Failed to join channel:', joinError);
- res.json({ success: false, error: 'Bot needs to be invited to #graphics-needed channel by a workspace admin' });
- }
- } else {
- throw postError;
- }
- }
- } catch (error) {
- console.error('Slack send error:', error);
- res.json({ success: false, error: String(error) });
- }
-});
-
-// Keep track of all samples history (approved + pending)
-const samplesHistory: Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- processedAt?: Date;
-}> = [];
-
-// Get all samples (pending + approved history + last 90 days from Shopify)
-app.get('/api/samples/all', requireAuth, async (req, res) => {
- try {
- // Combine current pending with history
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected',
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders`
- }));
-
- // Fetch orders from last 90 days from Shopify
- const ninetyDaysAgo = new Date();
- ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
-
- const shopifyUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/orders.json?status=any&created_at_min=${ninetyDaysAgo.toISOString()}&limit=250`;
-
- const response = await fetch(shopifyUrl, {
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- },
- });
-
- const shopifyData: any = await response.json();
- const orders = shopifyData.orders || [];
-
- addLog(`Fetched ${orders.length} orders from Shopify (last 90 days)`, 'processing');
-
- // Extract DIG samples from Shopify orders
- const shopifySamples: Array<any> = [];
-
- for (const order of orders) {
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || 'No email';
-
- for (const item of digItems) {
- shopifySamples.push({
- id: `shopify-${order.id}-${item.id}`,
- orderId: order.name || `#${order.order_number}`,
- orderNumber: order.order_number.toString(),
- shopifyOrderId: order.id.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- status: 'ordered',
- timestamp: new Date(order.created_at),
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders/${order.id}`,
- financialStatus: order.financial_status,
- fulfillmentStatus: order.fulfillment_status || 'unfulfilled'
- });
- }
- }
- }
-
- addLog(`Found ${shopifySamples.length} DIG samples in recent orders`, 'processing');
-
- const allSamples = [...samplesHistory, ...currentPending, ...shopifySamples].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length, last90Days: shopifySamples.length });
- } catch (error) {
- console.error('Error fetching samples:', error);
- addLog(`Error fetching Shopify orders: ${error}`, 'error');
-
- // Fallback to local history only
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected'
- }));
-
- const allSamples = [...samplesHistory, ...currentPending].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length });
- }
-});
-
-app.post('/api/approve/:id', requireAuth, async (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (!approval) {
- return res.status(404).json({ error: 'Approval not found' });
- }
-
- approval.status = 'approved';
-
- try {
- // Send to Slack #new-products channel
- await slackClient.chat.postMessage({
- channel: SLACK_CHANNEL,
- text: `🆕 *DIG Sample Approved & Sent*\n\n📦 *${approval.productTitle}*\n• SKU: \`${approval.digSku}\`\n• Customer: ${approval.customerName}\n• Order: ${approval.orderId}\n\n✅ Sample sent to customer!`,
- unfurl_links: false,
- unfurl_media: false
- });
-
- stats.slackPostsSent++;
- stats.emailsSent++;
- addLog(`Approved and sent DIG-${approval.digSku} to ${approval.customerName}`, 'email');
-
- // Add to history before removing from pending
- samplesHistory.push({
- ...approval,
- status: 'approved',
- processedAt: new Date()
- });
-
- // Remove from pending
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
-
- res.json({ success: true, message: 'Sample approved and sent to customer!' });
- } catch (error) {
- addLog(`Error sending approval: ${error}`, 'error');
- res.status(500).json({ error: 'Failed to send sample' });
- }
-});
-
-app.post('/api/reject/:id', requireAuth, (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (approval) {
- // Add to history before removing
- samplesHistory.push({
- ...approval,
- status: 'rejected',
- processedAt: new Date()
- });
- }
-
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
- addLog(`Rejected DIG sample order ${req.params.id}`, 'processing');
- res.json({ success: true, message: 'Order rejected' });
-});
-
-// Google Drive API endpoints
-app.get('/api/drive/search', requireAuth, async (req, res) => {
- try {
- const query = (req.query.q as string) || '';
- const files = await searchDriveFiles(query);
-
- res.json({
- success: true,
- files: files.map((f: any) => ({
- id: f.id,
- name: f.name,
- mimeType: f.mimeType,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink,
- size: f.size,
- modified: f.modifiedTime
- })),
- total: files.length
- });
- } catch (error) {
- res.status(500).json({ error: `Drive search failed: ${error}` });
- }
-});
-
-app.get('/api/drive/file/:id', requireAuth, async (req, res) => {
- try {
- const fileInfo = await getDriveFileLink(req.params.id);
- res.json({ success: true, file: fileInfo });
- } catch (error) {
- res.status(500).json({ error: `Failed to get file: ${error}` });
- }
-});
-
-// Shopify webhook endpoint (receives new orders)
-app.post('/webhook/shopify/orders/create', async (req, res) => {
- try {
- const order = req.body;
- stats.totalOrders++;
-
- addLog(`New order received: #${order.order_number}`, 'order');
-
- // Check for DIG-series items
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- stats.digOrdersFound++;
-
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || '';
-
- stats.lastOrder = {
- orderId: order.name || order.order_number.toString(),
- customerName,
- digItems: digItems.map((item: any) => item.sku),
- timestamp: new Date()
- };
-
- addLog(`Found ${digItems.length} DIG items in order #${order.order_number}`, 'processing');
-
- // Process each DIG item
- for (const item of digItems) {
- await processDIGItem(order, item, customerEmail, customerName);
- }
- }
-
- res.json({ success: true });
- } catch (error) {
- addLog(`Webhook error: ${error}`, 'error');
- res.status(500).json({ error: 'Webhook processing failed' });
- }
-});
-
-// Google Drive search function
-async function searchDriveFiles(query: string = '') {
- try {
- const response = await drive.files.list({
- q: query ? `name contains '${query}' and mimeType contains 'image/'` : "mimeType contains 'image/'",
- fields: 'files(id, name, mimeType, webViewLink, thumbnailLink, size, createdTime, modifiedTime)',
- orderBy: 'modifiedTime desc',
- pageSize: 50
- });
-
- return response.data.files || [];
- } catch (error) {
- addLog(`Error searching Drive: ${error}`, 'error');
- return [];
- }
-}
-
-// Get Drive file download link
-async function getDriveFileLink(fileId: string) {
- try {
- const response = await drive.files.get({
- fileId,
- fields: 'webContentLink, webViewLink'
- });
- return response.data;
- } catch (error) {
- addLog(`Error getting Drive file link: ${error}`, 'error');
- return null;
- }
-}
-
-async function processDIGItem(order: any, item: any, customerEmail: string, customerName: string) {
- try {
- stats.filesProcessed++;
-
- // Search Google Drive for matching files
- const sku = item.sku.replace('DIG-', '');
- const driveFiles = await searchDriveFiles(sku);
-
- addLog(`Found ${driveFiles.length} Drive files matching SKU: ${sku}`, 'processing');
-
- // Create approval entry
- const approval = {
- id: `${order.id}-${item.id}-${Date.now()}`,
- orderId: order.name || order.order_number.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- resizedImageUrl: '', // Will be populated after image processing
- driveFiles: driveFiles.map((f: any) => ({
- id: f.id,
- name: f.name,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink
- })),
- status: 'pending' as const,
- timestamp: new Date()
- };
-
- stats.pendingApprovals.push(approval);
-
- addLog(`Created approval for ${item.sku} - ${customerName} with ${driveFiles.length} Drive files`, 'processing');
-
- } catch (error) {
- addLog(`Error processing DIG item: ${error}`, 'error');
- }
-}
-
-// Manual test endpoint
-app.post('/api/test/dig-order', requireAuth, async (req, res) => {
- const mockOrder = {
- id: Date.now(),
- order_number: 12345,
- name: '#TEST-' + Date.now(),
- customer: {
- first_name: 'Test',
- last_name: 'Customer',
- email: 'test@example.com'
- },
- line_items: [
- {
- id: Date.now(),
- sku: 'DIG-12345',
- title: 'Digital Sample - Test Pattern',
- properties: [
- { name: 'Image', value: 'https://via.placeholder.com/800x600' }
- ]
- }
- ]
- };
-
- await processDIGItem(mockOrder, mockOrder.line_items[0], 'test@example.com', 'Test Customer');
-
- res.json({ success: true, message: 'Test DIG order created!' });
-});
-
-
-// API: Chat endpoint for universal header
-app.post('/api/chat', requireAuth, async (req, res) => {
-
- // Memory system commands
- if (message.toLowerCase().match(/^(remember|note|preference|learning):/i)) {
- const parts = message.split(':');
- const type = parts[0].toLowerCase();
- const content = parts.slice(1).join(':').trim();
- const memType = type === 'remember' ? 'note' : type as any;
- agentMemory.add(content, memType);
- return res.json({
- response: `✅ Memory saved: "${content}"`,
- success: true,
- memoryAdded: true
- });
- }
-
- if (message.toLowerCase().includes('show') && message.toLowerCase().includes('memor')) {
- const summary = agentMemory.getContextSummary();
- return res.json({ response: summary, success: true });
- }
-
- if (message.toLowerCase().includes('search memor')) {
- const query = message.split(/for|about/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- return res.json({
- response: results.length > 0
- ? 'Found memories:\n' + results.map(m => `- ${m.content}`).join('\n')
- : 'No matching memories found.',
- success: true
- });
- }
-
- if (message.toLowerCase().includes('forget') || message.toLowerCase().includes('delete memor')) {
- const query = message.split(/forget|delete/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- if (results.length > 0) {
- agentMemory.delete(results[0].id);
- return res.json({ response: `✅ Forgot: "${results[0].content}"`, success: true });
- }
- return res.json({ response: 'Nothing found to forget.', success: true });
- }
-
- try {
- const { message } = req.body;
- const systemPrompt = `You are the Digital Samples AI assistant for Designer Wallcoverings. Process DIG-series orders and file delivery`;
-
- const Anthropic = require('@anthropic-ai/sdk').default;
- const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY || '' });
-
- const response = await anthropic.messages.create({
- model: 'claude-3-5-sonnet-20241022',
- max_tokens: 1024,
- system: systemPrompt,
- messages: [{ role: 'user', content: message }],
- });
-
- const assistantMessage = response.content[0].type === 'text' ? response.content[0].text : '';
- res.json({ response: assistantMessage, success: true });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-// Start server
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('📦 Digital Samples Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('✅ Monitoring Shopify for DIG-series orders...');
- console.log('📁 Google Drive integration: Ready');
- console.log('🖼️ Image resizing: 24" @ 150 DPI');
- console.log('📢 Slack channel: #new-products');
- console.log('');
- addLog('Digital Samples Agent initialized and running', 'processing');
-});
diff --git a/DW-Agents/dw-agents/digital-samples-agent.ts.backup-1762549988 b/DW-Agents/dw-agents/digital-samples-agent.ts.backup-1762549988
deleted file mode 100644
index eb9ccf2a..00000000
--- a/DW-Agents/dw-agents/digital-samples-agent.ts.backup-1762549988
+++ /dev/null
@@ -1,1164 +0,0 @@
-/**
- * DW-Agents: Digital Samples Agent
- *
- * Monitors Shopify orders for DIG-series products and automates sample delivery:
- * 1. Detects DIG-* SKU orders from Shopify webhooks
- * 2. Searches Google Drive for matching sample files
- * 3. Resizes images to 24" wide at 150 DPI
- * 4. Posts to Slack #new-products channel with approval workflow
- * 5. Sends files to customers via email
- *
- * Port: 9879
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import { WebClient } from '@slack/web-api';
-import sharp from 'sharp';
-import fetch from 'node-fetch';
-import { google } from 'googleapis';
-import dotenv from 'dotenv';
-
-// Load environment variables
-dotenv.config();
-
-const app = express();
-const PORT = 9879;
-
-// Authentication
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-// Shopify configuration
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || 'shpat_REDACTED';
-const API_VERSION = '2024-07';
-
-// Slack configuration
-const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || 'xoxb-3958182050256-9814366234406-HiR1ybK6HT8JViWDEpjXpmsq';
-const SLACK_CHANNEL = 'new-products-on-shopify';
-const slackClient = new WebClient(SLACK_BOT_TOKEN);
-
-// Anthropic Claude
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY,
-});
-
-// Google Drive API setup
-const oauth2Client = new google.auth.OAuth2(
- process.env.GOOGLE_CLIENT_ID,
- process.env.GOOGLE_CLIENT_SECRET,
- 'http://localhost'
-);
-
-oauth2Client.setCredentials({
- refresh_token: process.env.GOOGLE_REFRESH_TOKEN
-});
-
-const drive = google.drive({ version: 'v3', auth: oauth2Client });
-
-// Statistics tracking
-let stats = {
- totalOrders: 0,
- digOrdersFound: 0,
- filesProcessed: 0,
- slackPostsSent: 0,
- emailsSent: 0,
- lastOrder: null as {
- orderId: string;
- customerName: string;
- digItems: string[];
- timestamp: Date;
- } | null,
- pendingApprovals: [] as Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- resizedImageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- }>
-};
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- type: 'order' | 'processing' | 'slack' | 'email' | 'error';
- message: string;
-}> = [];
-
-function addLog(message: string, type: 'order' | 'processing' | 'slack' | 'email' | 'error' = 'processing') {
- const entry = {
- timestamp: new Date(),
- type,
- message
- };
- activityLog.unshift(entry);
-
- // Keep only last 100 entries
- if (activityLog.length > 100) {
- activityLog.pop();
- }
-
- console.log(`[${type.toUpperCase()}] ${message}`);
-}
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-digital-samples-agent-2025',
- resave: false,
- saveUninitialized: false,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 24 * 60 * 60 * 1000,
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- }
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // TEMPORARY: Disable auth for 4 hours (until 4 hours from server restart)
- const serverStartTime = Date.now();
- const fourHoursInMs = 4 * 60 * 60 * 1000;
- if (Date.now() - serverStartTime < fourHoursInMs) {
- return next();
- }
-
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
-};
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Digital Samples Agent</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #4facfe; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>📦 Digital Samples Agent</h1>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
-
- // Set SSO cookie for cross-agent authentication
- setSSOToken(res);
-
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard
-app.get('/', requireAuth, (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Digital Samples Agent - DW-Agents</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #4facfe; font-size: 2.5em; margin-bottom: 10px; }
- .header .subtitle { color: #666; font-size: 1.2em; }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 20px;
- margin-bottom: 20px;
- }
- .stat-card {
- background: white;
- padding: 25px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stat-card h3 { color: #888; font-size: 0.9em; text-transform: uppercase; margin-bottom: 10px; }
- .stat-card .value { font-size: 2.5em; font-weight: bold; color: #4facfe; }
- .approvals-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .approvals-section h2 { color: #4facfe; margin-bottom: 20px; }
- .approval-card {
- background: #f8f9fa;
- padding: 20px;
- border-radius: 10px;
- margin-bottom: 15px;
- border-left: 4px solid #4facfe;
- }
- .approval-card img { max-width: 200px; border-radius: 8px; margin: 10px 0; }
- .btn {
- padding: 10px 20px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-right: 10px;
- }
- .btn-approve { background: #4CAF50; color: white; }
- .btn-reject { background: #f44336; color: white; }
- .log-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .log-entry {
- padding: 10px;
- margin: 5px 0;
- border-radius: 6px;
- font-family: 'Courier New', monospace;
- font-size: 0.9em;
- }
- .log-entry.order { background: #e3f2fd; border-left: 4px solid #2196F3; }
- .log-entry.processing { background: #fff3e0; border-left: 4px solid #FF9800; }
- .log-entry.slack { background: #f3e5f5; border-left: 4px solid #9C27B0; }
- .log-entry.email { background: #e8f5e9; border-left: 4px solid #4CAF50; }
- .log-entry.error { background: #ffebee; border-left: 4px solid #f44336; }
- .drive-file-card {
- background: #f8f9fa;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 4px solid #4facfe;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- gap: 15px;
- align-items: center;
- }
- .drive-file-card:hover {
- background: #e9ecef;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .drive-file-card.selected {
- background: #e8f5e9;
- border-left-color: #4CAF50;
- }
- .drive-file-thumbnail {
- width: 60px;
- height: 60px;
- border-radius: 6px;
- object-fit: cover;
- background: #e0e0e0;
- flex-shrink: 0;
- }
- .drive-file-info {
- flex: 1;
- }
- .drive-file-name {
- font-weight: 600;
- color: #333;
- margin-bottom: 5px;
- }
- .drive-file-meta {
- color: #888;
- font-size: 0.85em;
- }
- </style>
-</head>
-<body>
- <div class="header">
- <h1>📦 Digital Samples Agent</h1>
- <div class="subtitle">Automated DIG-series order processing & delivery</div>
- <div style="text-align: right; margin-top: 10px;">
- <a href="/logout" style="color: #4facfe; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-grid">
- <div class="stat-card">
- <h3>Total Orders</h3>
- <div class="value" id="totalOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>DIG Orders</h3>
- <div class="value" id="digOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>Files Processed</h3>
- <div class="value" id="filesProcessed">0</div>
- </div>
- <div class="stat-card">
- <h3>Pending Approvals</h3>
- <div class="value" id="pendingApprovals">0</div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>💬 Slack #graphics-needed Channel</h2>
- <p style="color: #666; margin-bottom: 15px;">Recent messages and requests from the graphics team</p>
- <div id="slackMessages">
- <p style="color: #888;">Loading Slack messages...</p>
- </div>
- <div style="margin-top: 15px;">
- <input type="text" id="slackMessageInput" placeholder="Type a message to #graphics-needed..."
- style="width: calc(100% - 120px); padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px; margin-right: 10px;">
- <button onclick="sendSlackMessage()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 📤 Send
- </button>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📁 Google Drive - Digital Samples Library</h2>
- <p style="color: #666; margin-bottom: 15px;">Browse and attach sample files from Google Drive</p>
-
- <div style="display: flex; gap: 10px; margin-bottom: 15px;">
- <input type="text" id="driveSearchInput" placeholder="Search files by name or SKU..."
- style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <select id="driveFilterType" style="padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="all">All Files</option>
- <option value="images">Images Only</option>
- <option value="pdfs">PDFs Only</option>
- </select>
- <button onclick="refreshDriveFiles()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh
- </button>
- </div>
-
- <!-- Spreadsheet-style table -->
- <div style="background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
- <div style="overflow-x: auto; max-height: 600px;">
- <table id="driveFilesTable" style="width: 100%; border-collapse: collapse;">
- <thead>
- <tr style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; position: sticky; top: 0; z-index: 10;">
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Thumbnail</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">File Name</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">SKU</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Type</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Size</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Modified</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Actions</th>
- </tr>
- </thead>
- <tbody id="driveFilesList">
- <tr>
- <td colspan="7" style="padding: 30px; text-align: center; color: #888;">Loading Google Drive files...</td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
-
- <div id="selectedFilesPanel" style="display: none; margin-top: 20px; padding: 20px; background: #e8f5e9; border-radius: 8px; border-left: 4px solid #4CAF50;">
- <h3 style="margin: 0 0 15px 0; color: #2E7D32;">📎 Selected Files (<span id="selectedCount">0</span>)</h3>
- <div id="selectedFilesList"></div>
- <div style="margin-top: 15px; display: flex; gap: 10px;">
- <select id="attachToOrder" style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="">Select an order to attach files...</option>
- </select>
- <button onclick="attachFilesToOrder()" style="background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ✅ Attach to Order
- </button>
- <button onclick="clearSelectedFiles()" style="background: #f44336; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ❌ Clear
- </button>
- </div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📋 Pending Approvals</h2>
- <div id="approvalsList">
- <p style="color: #888;">No pending approvals</p>
- </div>
- </div>
-
- <div class="approvals-section">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
- <h2 style="margin: 0;">📦 All Samples by Date</h2>
- <div style="display: flex; gap: 10px; align-items: center;">
- <span id="lastRefresh" style="color: #888; font-size: 0.9em;">Last updated: --:--</span>
- <button onclick="manualRefresh()" class="btn" style="background: #4facfe; color: white; padding: 8px 16px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh Now
- </button>
- </div>
- </div>
- <div style="margin-bottom: 15px;">
- <input
- type="text"
- id="searchSamples"
- placeholder="Search by customer, SKU, or order ID..."
- style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;"
- >
- </div>
- <div id="allSamplesList">
- <p style="color: #888;">Loading samples history...</p>
- </div>
- </div>
-
- <div class="log-section">
- <h2>📝 Activity Log</h2>
- <div id="activityLog"></div>
- </div>
-
- <script>
- // Clean SKU - extract only DIG-number (e.g., "DIG-25-some-extra" becomes "DIG-25")
- function cleanSku(sku) {
- if (!sku) return '';
- // Match DIG- followed by digits, ignore everything after
- const match = sku.match(/DIG-\d+/i);
- return match ? match[0] : sku;
- }
-
- async function updateDashboard() {
- try {
- const statsData = await fetch('/api/stats').then(r => r.json());
-
- document.getElementById('totalOrders').textContent = statsData.totalOrders;
- document.getElementById('digOrders').textContent = statsData.digOrdersFound;
- document.getElementById('filesProcessed').textContent = statsData.filesProcessed;
- document.getElementById('pendingApprovals').textContent = statsData.pendingApprovals.length;
-
- // Update approvals list
- const approvalsList = document.getElementById('approvalsList');
- if (statsData.pendingApprovals.length === 0) {
- approvalsList.innerHTML = '<p style="color: #888;">No pending approvals</p>';
- } else {
- approvalsList.innerHTML = statsData.pendingApprovals.map(approval => \`
- <div class="approval-card">
- <h3>\${approval.productTitle} (SKU: \${cleanSku(approval.digSku)})</h3>
- <p><strong>Customer:</strong> \${approval.customerName} (\${approval.customerEmail})</p>
- <p><strong>Order ID:</strong> \${approval.orderId}</p>
- <p><strong>Time:</strong> \${new Date(approval.timestamp).toLocaleString()}</p>
- <img src="\${approval.resizedImageUrl || approval.imageUrl}" alt="Product">
- <div>
- <button class="btn btn-approve" onclick="approveOrder('\${approval.id}')">✅ Approve & Send</button>
- <button class="btn btn-reject" onclick="rejectOrder('\${approval.id}')">❌ Reject</button>
- </div>
- </div>
- \`).join('');
- }
-
- // Update Slack messages
- const slackData = await fetch('/api/slack/messages').then(r => r.json());
- const slackDiv = document.getElementById('slackMessages');
- if (slackData.messages && slackData.messages.length > 0) {
- slackDiv.innerHTML = slackData.messages.map(msg => \`
- <div style="background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid #4facfe;">
- <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
- <strong style="color: #333;">\${msg.user}</strong>
- <span style="color: #888; font-size: 0.85em;">\${new Date(msg.timestamp * 1000).toLocaleString()}</span>
- </div>
- <div style="color: #555;">\${msg.text}</div>
- </div>
- \`).join('');
- } else {
- slackDiv.innerHTML = '<p style="color: #888;">No recent messages in #graphics-needed</p>';
- }
-
- // Update all samples list
- const samplesData = await fetch('/api/samples/all').then(r => r.json());
- window.allSamplesData = samplesData.samples || [];
- renderSamples(window.allSamplesData, samplesData);
-
- // Update activity log
- const logs = await fetch('/api/activity').then(r => r.json());
- const logDiv = document.getElementById('activityLog');
- logDiv.innerHTML = logs.map(log => \`
- <div class="log-entry \${log.type}">
- [\${new Date(log.timestamp).toLocaleTimeString()}] \${log.message}
- </div>
- \`).join('');
-
- updateLastRefreshTime();
- } catch (error) {
- console.error('Update error:', error);
- }
- }
-
- // Render samples function
- function renderSamples(samples, data) {
- const samplesList = document.getElementById('allSamplesList');
- if (samples && samples.length > 0) {
- const totalCount = data.total || samples.length;
- const last90Count = data.last90Days || samples.filter(s => {
- const daysAgo = (Date.now() - new Date(s.timestamp).getTime()) / (1000 * 60 * 60 * 24);
- return daysAgo <= 90;
- }).length;
-
- samplesList.innerHTML = \`
- <div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #2196F3;">
- <h3 style="margin: 0 0 10px 0; color: #1976D2;">📊 Summary</h3>
- <p style="margin: 5px 0;"><strong>DIG Samples (Last 90 Days):</strong> \${last90Count}</p>
- <p style="margin: 5px 0;"><strong>Total Displayed:</strong> \${samples.length}</p>
- </div>
- \` + samples.map(sample => {
- const statusColor = sample.status === 'approved' ? '#4CAF50' :
- sample.status === 'pending' ? '#FFA500' :
- sample.status === 'ordered' ? '#2196F3' : '#f44336';
- const statusText = sample.status === 'ordered' ? 'ORDERED' : sample.status.toUpperCase();
- const orderLink = sample.orderUrl ?
- \`<a href="\${sample.orderUrl}" target="_blank" style="color: #4facfe; text-decoration: none; font-weight: 600;">🔗 View Order in Shopify →</a>\` :
- '';
-
- return \`
- <div class="approval-card" style="opacity: \${sample.status === 'approved' || sample.status === 'ordered' ? '0.85' : '1'}; position: relative;">
- <h3>\${sample.productTitle}</h3>
- <p><strong>SKU:</strong> <span style="background: #f0f0f0; padding: 3px 8px; border-radius: 4px; font-family: monospace;">\${cleanSku(sample.digSku)}</span></p>
- <p><strong>Customer:</strong> \${sample.customerName} (\${sample.customerEmail})</p>
- <p><strong>Order:</strong> \${sample.orderId || sample.orderNumber || 'N/A'}</p>
- <p><strong>Status:</strong> <span style="color: \${statusColor}; font-weight: bold; background: \${statusColor}22; padding: 3px 10px; border-radius: 4px;">\${statusText}</span></p>
- <p><strong>Date:</strong> \${new Date(sample.timestamp).toLocaleString()}</p>
- \${orderLink ? \`<p style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;">\${orderLink}</p>\` : ''}
- </div>
- \`;
- }).join('');
- } else {
- samplesList.innerHTML = '<p style="color: #888;">No samples found</p>';
- }
- }
-
- // Search functionality
- window.allSamplesData = [];
- document.getElementById('searchSamples').addEventListener('input', function(e) {
- const searchTerm = e.target.value.toLowerCase();
-
- if (!searchTerm) {
- // Show all samples when search is empty
- fetch('/api/samples/all')
- .then(r => r.json())
- .then(data => {
- window.allSamplesData = data.samples || [];
- renderSamples(window.allSamplesData, data);
- });
- return;
- }
-
- const filtered = window.allSamplesData.filter(sample =>
- (sample.customerName && sample.customerName.toLowerCase().includes(searchTerm)) ||
- (sample.customerEmail && sample.customerEmail.toLowerCase().includes(searchTerm)) ||
- (sample.digSku && sample.digSku.toLowerCase().includes(searchTerm)) ||
- (sample.orderId && sample.orderId.toLowerCase().includes(searchTerm)) ||
- (sample.orderNumber && sample.orderNumber.toLowerCase().includes(searchTerm)) ||
- (sample.productTitle && sample.productTitle.toLowerCase().includes(searchTerm))
- );
-
- renderSamples(filtered, { total: filtered.length, last90Days: filtered.length });
- });
-
- function approveOrder(id) {
- fetch(\`/api/approve/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- function rejectOrder(id) {
- fetch(\`/api/reject/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- async function sendSlackMessage() {
- const input = document.getElementById('slackMessageInput');
- const message = input.value.trim();
-
- if (!message) {
- alert('Please enter a message');
- return;
- }
-
- try {
- const response = await fetch('/api/slack/send', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
- if (data.success) {
- input.value = '';
- updateDashboard(); // Refresh to show new message
- } else {
- alert('Failed to send message: ' + data.error);
- }
- } catch (error) {
- alert('Error sending message');
- }
- }
-
- function manualRefresh() {
- const button = event.target;
- button.disabled = true;
- button.textContent = '⏳ Refreshing...';
-
- updateDashboard().then(() => {
- button.disabled = false;
- button.textContent = '🔄 Refresh Now';
- });
- }
-
- function updateLastRefreshTime() {
- const now = new Date();
- const timeStr = now.toLocaleTimeString();
- document.getElementById('lastRefresh').textContent = \`Last updated: \${timeStr}\`;
- }
-
- // Auto-refresh every 30 seconds
- setInterval(() => {
- updateDashboard();
- updateLastRefreshTime();
- }, 30000);
-
- // Initial load
- updateDashboard();
- updateLastRefreshTime();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.get('/api/activity', requireAuth, (req, res) => {
- res.json(activityLog.slice(0, 50));
-});
-
-// Get Slack channel ID
-async function getGraphicsNeededChannelId() {
- try {
- const result = await slackClient.conversations.list({
- types: 'public_channel,private_channel',
- limit: 200
- });
-
- const channel = result.channels?.find((c: any) => c.name === 'graphics-needed');
- if (channel) {
- return channel.id;
- }
-
- console.error('Channel #graphics-needed not found in:', result.channels?.map((c: any) => c.name));
- return null;
- } catch (error) {
- console.error('Error getting channel list:', error);
- return null;
- }
-}
-
-// Slack API endpoints
-app.get('/api/slack/messages', requireAuth, async (req, res) => {
- try {
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ messages: [], error: 'Channel #graphics-needed not found' });
- }
-
- const result = await slackClient.conversations.history({
- channel: channelId,
- limit: 20
- });
-
- const messages = result.messages?.map((msg: any) => ({
- user: msg.user || 'Unknown',
- text: msg.text,
- timestamp: msg.ts
- })) || [];
-
- res.json({ messages });
- } catch (error) {
- console.error('Slack fetch error:', error);
- res.json({ messages: [], error: String(error) });
- }
-});
-
-app.post('/api/slack/send', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ success: false, error: 'Channel #graphics-needed not found' });
- }
-
- // Try to join the channel first if we get not_in_channel error
- try {
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (postError: any) {
- // If not in channel, try to join it
- if (postError.data?.error === 'not_in_channel') {
- console.log('🔧 Bot not in channel, attempting to join #graphics-needed...');
- try {
- await slackClient.conversations.join({ channel: channelId });
- console.log('✅ Successfully joined #graphics-needed');
-
- // Retry posting message
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (joinError) {
- console.error('❌ Failed to join channel:', joinError);
- res.json({ success: false, error: 'Bot needs to be invited to #graphics-needed channel by a workspace admin' });
- }
- } else {
- throw postError;
- }
- }
- } catch (error) {
- console.error('Slack send error:', error);
- res.json({ success: false, error: String(error) });
- }
-});
-
-// Keep track of all samples history (approved + pending)
-const samplesHistory: Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- processedAt?: Date;
-}> = [];
-
-// Get all samples (pending + approved history + last 90 days from Shopify)
-app.get('/api/samples/all', requireAuth, async (req, res) => {
- try {
- // Combine current pending with history
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected',
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders`
- }));
-
- // Fetch orders from last 90 days from Shopify
- const ninetyDaysAgo = new Date();
- ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
-
- const shopifyUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/orders.json?status=any&created_at_min=${ninetyDaysAgo.toISOString()}&limit=250`;
-
- const response = await fetch(shopifyUrl, {
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- },
- });
-
- const shopifyData: any = await response.json();
- const orders = shopifyData.orders || [];
-
- addLog(`Fetched ${orders.length} orders from Shopify (last 90 days)`, 'processing');
-
- // Extract DIG samples from Shopify orders
- const shopifySamples: Array<any> = [];
-
- for (const order of orders) {
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || 'No email';
-
- for (const item of digItems) {
- shopifySamples.push({
- id: `shopify-${order.id}-${item.id}`,
- orderId: order.name || `#${order.order_number}`,
- orderNumber: order.order_number.toString(),
- shopifyOrderId: order.id.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- status: 'ordered',
- timestamp: new Date(order.created_at),
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders/${order.id}`,
- financialStatus: order.financial_status,
- fulfillmentStatus: order.fulfillment_status || 'unfulfilled'
- });
- }
- }
- }
-
- addLog(`Found ${shopifySamples.length} DIG samples in recent orders`, 'processing');
-
- const allSamples = [...samplesHistory, ...currentPending, ...shopifySamples].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length, last90Days: shopifySamples.length });
- } catch (error) {
- console.error('Error fetching samples:', error);
- addLog(`Error fetching Shopify orders: ${error}`, 'error');
-
- // Fallback to local history only
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected'
- }));
-
- const allSamples = [...samplesHistory, ...currentPending].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length });
- }
-});
-
-app.post('/api/approve/:id', requireAuth, async (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (!approval) {
- return res.status(404).json({ error: 'Approval not found' });
- }
-
- approval.status = 'approved';
-
- try {
- // Send to Slack #new-products channel
- await slackClient.chat.postMessage({
- channel: SLACK_CHANNEL,
- text: `🆕 *DIG Sample Approved & Sent*\n\n📦 *${approval.productTitle}*\n• SKU: \`${approval.digSku}\`\n• Customer: ${approval.customerName}\n• Order: ${approval.orderId}\n\n✅ Sample sent to customer!`,
- unfurl_links: false,
- unfurl_media: false
- });
-
- stats.slackPostsSent++;
- stats.emailsSent++;
- addLog(`Approved and sent DIG-${approval.digSku} to ${approval.customerName}`, 'email');
-
- // Add to history before removing from pending
- samplesHistory.push({
- ...approval,
- status: 'approved',
- processedAt: new Date()
- });
-
- // Remove from pending
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
-
- res.json({ success: true, message: 'Sample approved and sent to customer!' });
- } catch (error) {
- addLog(`Error sending approval: ${error}`, 'error');
- res.status(500).json({ error: 'Failed to send sample' });
- }
-});
-
-app.post('/api/reject/:id', requireAuth, (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (approval) {
- // Add to history before removing
- samplesHistory.push({
- ...approval,
- status: 'rejected',
- processedAt: new Date()
- });
- }
-
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
- addLog(`Rejected DIG sample order ${req.params.id}`, 'processing');
- res.json({ success: true, message: 'Order rejected' });
-});
-
-// Google Drive API endpoints
-app.get('/api/drive/search', requireAuth, async (req, res) => {
- try {
- const query = (req.query.q as string) || '';
- const files = await searchDriveFiles(query);
-
- res.json({
- success: true,
- files: files.map((f: any) => ({
- id: f.id,
- name: f.name,
- mimeType: f.mimeType,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink,
- size: f.size,
- modified: f.modifiedTime
- })),
- total: files.length
- });
- } catch (error) {
- res.status(500).json({ error: `Drive search failed: ${error}` });
- }
-});
-
-app.get('/api/drive/file/:id', requireAuth, async (req, res) => {
- try {
- const fileInfo = await getDriveFileLink(req.params.id);
- res.json({ success: true, file: fileInfo });
- } catch (error) {
- res.status(500).json({ error: `Failed to get file: ${error}` });
- }
-});
-
-// Shopify webhook endpoint (receives new orders)
-app.post('/webhook/shopify/orders/create', async (req, res) => {
- try {
- const order = req.body;
- stats.totalOrders++;
-
- addLog(`New order received: #${order.order_number}`, 'order');
-
- // Check for DIG-series items
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- stats.digOrdersFound++;
-
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || '';
-
- stats.lastOrder = {
- orderId: order.name || order.order_number.toString(),
- customerName,
- digItems: digItems.map((item: any) => item.sku),
- timestamp: new Date()
- };
-
- addLog(`Found ${digItems.length} DIG items in order #${order.order_number}`, 'processing');
-
- // Process each DIG item
- for (const item of digItems) {
- await processDIGItem(order, item, customerEmail, customerName);
- }
- }
-
- res.json({ success: true });
- } catch (error) {
- addLog(`Webhook error: ${error}`, 'error');
- res.status(500).json({ error: 'Webhook processing failed' });
- }
-});
-
-// Google Drive search function
-async function searchDriveFiles(query: string = '') {
- try {
- const response = await drive.files.list({
- q: query ? `name contains '${query}' and mimeType contains 'image/'` : "mimeType contains 'image/'",
- fields: 'files(id, name, mimeType, webViewLink, thumbnailLink, size, createdTime, modifiedTime)',
- orderBy: 'modifiedTime desc',
- pageSize: 50
- });
-
- return response.data.files || [];
- } catch (error) {
- addLog(`Error searching Drive: ${error}`, 'error');
- return [];
- }
-}
-
-// Get Drive file download link
-async function getDriveFileLink(fileId: string) {
- try {
- const response = await drive.files.get({
- fileId,
- fields: 'webContentLink, webViewLink'
- });
- return response.data;
- } catch (error) {
- addLog(`Error getting Drive file link: ${error}`, 'error');
- return null;
- }
-}
-
-async function processDIGItem(order: any, item: any, customerEmail: string, customerName: string) {
- try {
- stats.filesProcessed++;
-
- // Search Google Drive for matching files
- const sku = item.sku.replace('DIG-', '');
- const driveFiles = await searchDriveFiles(sku);
-
- addLog(`Found ${driveFiles.length} Drive files matching SKU: ${sku}`, 'processing');
-
- // Create approval entry
- const approval = {
- id: `${order.id}-${item.id}-${Date.now()}`,
- orderId: order.name || order.order_number.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- resizedImageUrl: '', // Will be populated after image processing
- driveFiles: driveFiles.map((f: any) => ({
- id: f.id,
- name: f.name,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink
- })),
- status: 'pending' as const,
- timestamp: new Date()
- };
-
- stats.pendingApprovals.push(approval);
-
- addLog(`Created approval for ${item.sku} - ${customerName} with ${driveFiles.length} Drive files`, 'processing');
-
- } catch (error) {
- addLog(`Error processing DIG item: ${error}`, 'error');
- }
-}
-
-// Manual test endpoint
-app.post('/api/test/dig-order', requireAuth, async (req, res) => {
- const mockOrder = {
- id: Date.now(),
- order_number: 12345,
- name: '#TEST-' + Date.now(),
- customer: {
- first_name: 'Test',
- last_name: 'Customer',
- email: 'test@example.com'
- },
- line_items: [
- {
- id: Date.now(),
- sku: 'DIG-12345',
- title: 'Digital Sample - Test Pattern',
- properties: [
- { name: 'Image', value: 'https://via.placeholder.com/800x600' }
- ]
- }
- ]
- };
-
- await processDIGItem(mockOrder, mockOrder.line_items[0], 'test@example.com', 'Test Customer');
-
- res.json({ success: true, message: 'Test DIG order created!' });
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('📦 Digital Samples Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('✅ Monitoring Shopify for DIG-series orders...');
- console.log('📁 Google Drive integration: Ready');
- console.log('🖼️ Image resizing: 24" @ 150 DPI');
- console.log('📢 Slack channel: #new-products');
- console.log('');
- addLog('Digital Samples Agent initialized and running', 'processing');
-});
diff --git a/DW-Agents/dw-agents/digital-samples-agent.ts.backup-pre-theme b/DW-Agents/dw-agents/digital-samples-agent.ts.backup-pre-theme
deleted file mode 100644
index dd4f160e..00000000
--- a/DW-Agents/dw-agents/digital-samples-agent.ts.backup-pre-theme
+++ /dev/null
@@ -1,1241 +0,0 @@
-/**
- * DW-Agents: Digital Samples Agent
- *
- * Monitors Shopify orders for DIG-series products and automates sample delivery:
- * 1. Detects DIG-* SKU orders from Shopify webhooks
- * 2. Searches Google Drive for matching sample files
- * 3. Resizes images to 24" wide at 150 DPI
- * 4. Posts to Slack #new-products channel with approval workflow
- * 5. Sends files to customers via email
- *
- * Port: 9879
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import { WebClient } from '@slack/web-api';
-import sharp from 'sharp';
-import fetch from 'node-fetch';
-import { google } from 'googleapis';
-import dotenv from 'dotenv';
-import { AgentMemory } from './shared-memory-system';
-
-// Load environment variables
-dotenv.config();
-
-const app = express();
-const PORT = 9879;
-// Initialize agent memory system
-const agentMemory = new AgentMemory('digital-samples');
-console.log('📚 Memory system initialized for digital-samples');
-
-
-// Authentication
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-// Shopify configuration
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || 'shpat_REDACTED';
-const API_VERSION = '2024-07';
-
-// Slack configuration
-const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || 'xoxb-3958182050256-9814366234406-HiR1ybK6HT8JViWDEpjXpmsq';
-const SLACK_CHANNEL = 'new-products-on-shopify';
-const slackClient = new WebClient(SLACK_BOT_TOKEN);
-
-// Anthropic Claude
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY,
-});
-
-// Google Drive API setup
-const oauth2Client = new google.auth.OAuth2(
- process.env.GOOGLE_CLIENT_ID,
- process.env.GOOGLE_CLIENT_SECRET,
- 'http://localhost'
-);
-
-oauth2Client.setCredentials({
- refresh_token: process.env.GOOGLE_REFRESH_TOKEN
-});
-
-const drive = google.drive({ version: 'v3', auth: oauth2Client });
-
-// Statistics tracking
-let stats = {
- totalOrders: 0,
- digOrdersFound: 0,
- filesProcessed: 0,
- slackPostsSent: 0,
- emailsSent: 0,
- lastOrder: null as {
- orderId: string;
- customerName: string;
- digItems: string[];
- timestamp: Date;
- } | null,
- pendingApprovals: [] as Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- resizedImageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- }>
-};
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- type: 'order' | 'processing' | 'slack' | 'email' | 'error';
- message: string;
-}> = [];
-
-function addLog(message: string, type: 'order' | 'processing' | 'slack' | 'email' | 'error' = 'processing') {
- const entry = {
- timestamp: new Date(),
- type,
- message
- };
- activityLog.unshift(entry);
-
- // Keep only last 100 entries
- if (activityLog.length > 100) {
- activityLog.pop();
- }
-
- console.log(`[${type.toUpperCase()}] ${message}`);
-}
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-digital-samples-agent-2025',
- resave: false,
- saveUninitialized: false,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 24 * 60 * 60 * 1000,
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- }
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // TEMPORARY: Auth disabled for 4 hours
- return next();
-
- /* Original auth:
-
- // TEMPORARY: Disable auth for 4 hours (until 4 hours from server restart)
- const serverStartTime = Date.now();
- const fourHoursInMs = 4 * 60 * 60 * 1000;
- if (Date.now() - serverStartTime < fourHoursInMs) {
- return next();
- }
-
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
- */
-};
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Digital Samples Agent</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #4facfe; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>📦 Digital Samples Agent</h1>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
-
- // Set SSO cookie for cross-agent authentication
- setSSOToken(res);
-
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard
-app.get('/', requireAuth, (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Digital Samples Agent - DW-Agents</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #4facfe; font-size: 2.5em; margin-bottom: 10px; }
- .header .subtitle { color: #666; font-size: 1.2em; }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 20px;
- margin-bottom: 20px;
- }
- .stat-card {
- background: white;
- padding: 25px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stat-card h3 { color: #888; font-size: 0.9em; text-transform: uppercase; margin-bottom: 10px; }
- .stat-card .value { font-size: 2.5em; font-weight: bold; color: #4facfe; }
- .approvals-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .approvals-section h2 { color: #4facfe; margin-bottom: 20px; }
- .approval-card {
- background: #f5f5f5;
- padding: 20px;
- border-radius: 10px;
- margin-bottom: 15px;
- border-left: 4px solid #4facfe;
- }
- .approval-card img { max-width: 200px; border-radius: 8px; margin: 10px 0; }
- .btn {
- padding: 10px 20px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-right: 10px;
- }
- .btn-approve { background: #4CAF50; color: white; }
- .btn-reject { background: #f44336; color: white; }
- .log-section {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .log-entry {
- padding: 10px;
- margin: 5px 0;
- border-radius: 6px;
- font-family: 'Courier New', monospace;
- font-size: 0.9em;
- }
- .log-entry.order { background: #e3f2fd; border-left: 4px solid #2196F3; }
- .log-entry.processing { background: #fff3e0; border-left: 4px solid #FF9800; }
- .log-entry.slack { background: #f3e5f5; border-left: 4px solid #9C27B0; }
- .log-entry.email { background: #e8f5e9; border-left: 4px solid #4CAF50; }
- .log-entry.error { background: #ffebee; border-left: 4px solid #f44336; }
- .drive-file-card {
- background: #f5f5f5;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 4px solid #4facfe;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- gap: 15px;
- align-items: center;
- }
- .drive-file-card:hover {
- background: #e9ecef;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .drive-file-card.selected {
- background: #e8f5e9;
- border-left-color: #4CAF50;
- }
- .drive-file-thumbnail {
- width: 60px;
- height: 60px;
- border-radius: 6px;
- object-fit: cover;
- background: #e0e0e0;
- flex-shrink: 0;
- }
- .drive-file-info {
- flex: 1;
- }
- .drive-file-name {
- font-weight: 600;
- color: #333;
- margin-bottom: 5px;
- }
- .drive-file-meta {
- color: #888;
- font-size: 0.85em;
- }
- </style>
-</head>
-<body>
- <div class="header">
- <h1>📦 Digital Samples Agent</h1>
- <div class="subtitle">Automated DIG-series order processing & delivery</div>
- <div style="text-align: right; margin-top: 10px;">
- <a href="/logout" style="color: #4facfe; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-grid">
- <div class="stat-card">
- <h3>Total Orders</h3>
- <div class="value" id="totalOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>DIG Orders</h3>
- <div class="value" id="digOrders">0</div>
- </div>
- <div class="stat-card">
- <h3>Files Processed</h3>
- <div class="value" id="filesProcessed">0</div>
- </div>
- <div class="stat-card">
- <h3>Pending Approvals</h3>
- <div class="value" id="pendingApprovals">0</div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>💬 Slack #graphics-needed Channel</h2>
- <p style="color: #666; margin-bottom: 15px;">Recent messages and requests from the graphics team</p>
- <div id="slackMessages">
- <p style="color: #888;">Loading Slack messages...</p>
- </div>
- <div style="margin-top: 15px;">
- <input type="text" id="slackMessageInput" placeholder="Type a message to #graphics-needed..."
- style="width: calc(100% - 120px); padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px; margin-right: 10px;">
- <button onclick="sendSlackMessage()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 📤 Send
- </button>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📁 Google Drive - Digital Samples Library</h2>
- <p style="color: #666; margin-bottom: 15px;">Browse and attach sample files from Google Drive</p>
-
- <div style="display: flex; gap: 10px; margin-bottom: 15px;">
- <input type="text" id="driveSearchInput" placeholder="Search files by name or SKU..."
- style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <select id="driveFilterType" style="padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="all">All Files</option>
- <option value="images">Images Only</option>
- <option value="pdfs">PDFs Only</option>
- </select>
- <button onclick="refreshDriveFiles()" style="background: #4facfe; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh
- </button>
- </div>
-
- <!-- Spreadsheet-style table -->
- <div style="background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
- <div style="overflow-x: auto; max-height: 600px;">
- <table id="driveFilesTable" style="width: 100%; border-collapse: collapse;">
- <thead>
- <tr style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; position: sticky; top: 0; z-index: 10;">
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Thumbnail</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">File Name</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">SKU</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Type</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Size</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Modified</th>
- <th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #00f2fe;">Actions</th>
- </tr>
- </thead>
- <tbody id="driveFilesList">
- <tr>
- <td colspan="7" style="padding: 30px; text-align: center; color: #888;">Loading Google Drive files...</td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
-
- <div id="selectedFilesPanel" style="display: none; margin-top: 20px; padding: 20px; background: #e8f5e9; border-radius: 8px; border-left: 4px solid #4CAF50;">
- <h3 style="margin: 0 0 15px 0; color: #2E7D32;">📎 Selected Files (<span id="selectedCount">0</span>)</h3>
- <div id="selectedFilesList"></div>
- <div style="margin-top: 15px; display: flex; gap: 10px;">
- <select id="attachToOrder" style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;">
- <option value="">Select an order to attach files...</option>
- </select>
- <button onclick="attachFilesToOrder()" style="background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ✅ Attach to Order
- </button>
- <button onclick="clearSelectedFiles()" style="background: #f44336; color: white; padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- ❌ Clear
- </button>
- </div>
- </div>
- </div>
-
- <div class="approvals-section">
- <h2>📋 Pending Approvals</h2>
- <div id="approvalsList">
- <p style="color: #888;">No pending approvals</p>
- </div>
- </div>
-
- <div class="approvals-section">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
- <h2 style="margin: 0;">📦 All Samples by Date</h2>
- <div style="display: flex; gap: 10px; align-items: center;">
- <span id="lastRefresh" style="color: #888; font-size: 0.9em;">Last updated: --:--</span>
- <button onclick="manualRefresh()" class="btn" style="background: #4facfe; color: white; padding: 8px 16px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
- 🔄 Refresh Now
- </button>
- </div>
- </div>
- <div style="margin-bottom: 15px;">
- <input
- type="text"
- id="searchSamples"
- placeholder="Search by customer, SKU, or order ID..."
- style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px;"
- >
- </div>
- <div id="allSamplesList">
- <p style="color: #888;">Loading samples history...</p>
- </div>
- </div>
-
- <div class="log-section">
- <h2>📝 Activity Log</h2>
- <div id="activityLog"></div>
- </div>
-
- <script>
- // Clean SKU - extract only DIG-number (e.g., "DIG-25-some-extra" becomes "DIG-25")
- function cleanSku(sku) {
- if (!sku) return '';
- // Match DIG- followed by digits, ignore everything after
- const match = sku.match(/DIG-\d+/i);
- return match ? match[0] : sku;
- }
-
- async function updateDashboard() {
- try {
- const statsData = await fetch('/api/stats').then(r => r.json());
-
- document.getElementById('totalOrders').textContent = statsData.totalOrders;
- document.getElementById('digOrders').textContent = statsData.digOrdersFound;
- document.getElementById('filesProcessed').textContent = statsData.filesProcessed;
- document.getElementById('pendingApprovals').textContent = statsData.pendingApprovals.length;
-
- // Update approvals list
- const approvalsList = document.getElementById('approvalsList');
- if (statsData.pendingApprovals.length === 0) {
- approvalsList.innerHTML = '<p style="color: #888;">No pending approvals</p>';
- } else {
- approvalsList.innerHTML = statsData.pendingApprovals.map(approval => \`
- <div class="approval-card">
- <h3>\${approval.productTitle} (SKU: \${cleanSku(approval.digSku)})</h3>
- <p><strong>Customer:</strong> \${approval.customerName} (\${approval.customerEmail})</p>
- <p><strong>Order ID:</strong> \${approval.orderId}</p>
- <p><strong>Time:</strong> \${new Date(approval.timestamp).toLocaleString()}</p>
- <img src="\${approval.resizedImageUrl || approval.imageUrl}" alt="Product">
- <div>
- <button class="btn btn-approve" onclick="approveOrder('\${approval.id}')">✅ Approve & Send</button>
- <button class="btn btn-reject" onclick="rejectOrder('\${approval.id}')">❌ Reject</button>
- </div>
- </div>
- \`).join('');
- }
-
- // Update Slack messages
- const slackData = await fetch('/api/slack/messages').then(r => r.json());
- const slackDiv = document.getElementById('slackMessages');
- if (slackData.messages && slackData.messages.length > 0) {
- slackDiv.innerHTML = slackData.messages.map(msg => \`
- <div style="background: #f5f5f5; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid #4facfe;">
- <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
- <strong style="color: #333;">\${msg.user}</strong>
- <span style="color: #888; font-size: 0.85em;">\${new Date(msg.timestamp * 1000).toLocaleString()}</span>
- </div>
- <div style="color: #555;">\${msg.text}</div>
- </div>
- \`).join('');
- } else {
- slackDiv.innerHTML = '<p style="color: #888;">No recent messages in #graphics-needed</p>';
- }
-
- // Update all samples list
- const samplesData = await fetch('/api/samples/all').then(r => r.json());
- window.allSamplesData = samplesData.samples || [];
- renderSamples(window.allSamplesData, samplesData);
-
- // Update activity log
- const logs = await fetch('/api/activity').then(r => r.json());
- const logDiv = document.getElementById('activityLog');
- logDiv.innerHTML = logs.map(log => \`
- <div class="log-entry \${log.type}">
- [\${new Date(log.timestamp).toLocaleTimeString()}] \${log.message}
- </div>
- \`).join('');
-
- updateLastRefreshTime();
- } catch (error) {
- console.error('Update error:', error);
- }
- }
-
- // Render samples function
- function renderSamples(samples, data) {
- const samplesList = document.getElementById('allSamplesList');
- if (samples && samples.length > 0) {
- const totalCount = data.total || samples.length;
- const last90Count = data.last90Days || samples.filter(s => {
- const daysAgo = (Date.now() - new Date(s.timestamp).getTime()) / (1000 * 60 * 60 * 24);
- return daysAgo <= 90;
- }).length;
-
- samplesList.innerHTML = \`
- <div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #2196F3;">
- <h3 style="margin: 0 0 10px 0; color: #1976D2;">📊 Summary</h3>
- <p style="margin: 5px 0;"><strong>DIG Samples (Last 90 Days):</strong> \${last90Count}</p>
- <p style="margin: 5px 0;"><strong>Total Displayed:</strong> \${samples.length}</p>
- </div>
- \` + samples.map(sample => {
- const statusColor = sample.status === 'approved' ? '#4CAF50' :
- sample.status === 'pending' ? '#FFA500' :
- sample.status === 'ordered' ? '#2196F3' : '#f44336';
- const statusText = sample.status === 'ordered' ? 'ORDERED' : sample.status.toUpperCase();
- const orderLink = sample.orderUrl ?
- \`<a href="\${sample.orderUrl}" target="_blank" style="color: #4facfe; text-decoration: none; font-weight: 600;">🔗 View Order in Shopify →</a>\` :
- '';
-
- return \`
- <div class="approval-card" style="opacity: \${sample.status === 'approved' || sample.status === 'ordered' ? '0.85' : '1'}; position: relative;">
- <h3>\${sample.productTitle}</h3>
- <p><strong>SKU:</strong> <span style="background: #f0f0f0; padding: 3px 8px; border-radius: 4px; font-family: monospace;">\${cleanSku(sample.digSku)}</span></p>
- <p><strong>Customer:</strong> \${sample.customerName} (\${sample.customerEmail})</p>
- <p><strong>Order:</strong> \${sample.orderId || sample.orderNumber || 'N/A'}</p>
- <p><strong>Status:</strong> <span style="color: \${statusColor}; font-weight: bold; background: \${statusColor}22; padding: 3px 10px; border-radius: 4px;">\${statusText}</span></p>
- <p><strong>Date:</strong> \${new Date(sample.timestamp).toLocaleString()}</p>
- \${orderLink ? \`<p style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;">\${orderLink}</p>\` : ''}
- </div>
- \`;
- }).join('');
- } else {
- samplesList.innerHTML = '<p style="color: #888;">No samples found</p>';
- }
- }
-
- // Search functionality
- window.allSamplesData = [];
- document.getElementById('searchSamples').addEventListener('input', function(e) {
- const searchTerm = e.target.value.toLowerCase();
-
- if (!searchTerm) {
- // Show all samples when search is empty
- fetch('/api/samples/all')
- .then(r => r.json())
- .then(data => {
- window.allSamplesData = data.samples || [];
- renderSamples(window.allSamplesData, data);
- });
- return;
- }
-
- const filtered = window.allSamplesData.filter(sample =>
- (sample.customerName && sample.customerName.toLowerCase().includes(searchTerm)) ||
- (sample.customerEmail && sample.customerEmail.toLowerCase().includes(searchTerm)) ||
- (sample.digSku && sample.digSku.toLowerCase().includes(searchTerm)) ||
- (sample.orderId && sample.orderId.toLowerCase().includes(searchTerm)) ||
- (sample.orderNumber && sample.orderNumber.toLowerCase().includes(searchTerm)) ||
- (sample.productTitle && sample.productTitle.toLowerCase().includes(searchTerm))
- );
-
- renderSamples(filtered, { total: filtered.length, last90Days: filtered.length });
- });
-
- function approveOrder(id) {
- fetch(\`/api/approve/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- function rejectOrder(id) {
- fetch(\`/api/reject/\${id}\`, { method: 'POST' })
- .then(r => r.json())
- .then(data => {
- alert(data.message);
- updateDashboard();
- });
- }
-
- async function sendSlackMessage() {
- const input = document.getElementById('slackMessageInput');
- const message = input.value.trim();
-
- if (!message) {
- alert('Please enter a message');
- return;
- }
-
- try {
- const response = await fetch('/api/slack/send', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
- if (data.success) {
- input.value = '';
- updateDashboard(); // Refresh to show new message
- } else {
- alert('Failed to send message: ' + data.error);
- }
- } catch (error) {
- alert('Error sending message');
- }
- }
-
- function manualRefresh() {
- const button = event.target;
- button.disabled = true;
- button.textContent = '⏳ Refreshing...';
-
- updateDashboard().then(() => {
- button.disabled = false;
- button.textContent = '🔄 Refresh Now';
- });
- }
-
- function updateLastRefreshTime() {
- const now = new Date();
- const timeStr = now.toLocaleTimeString();
- document.getElementById('lastRefresh').textContent = \`Last updated: \${timeStr}\`;
- }
-
- // Auto-refresh every 30 seconds
- setInterval(() => {
- updateDashboard();
- updateLastRefreshTime();
- }, 30000);
-
- // Initial load
- updateDashboard();
- updateLastRefreshTime();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.get('/api/activity', requireAuth, (req, res) => {
- res.json(activityLog.slice(0, 50));
-});
-
-// Get Slack channel ID
-async function getGraphicsNeededChannelId() {
- try {
- const result = await slackClient.conversations.list({
- types: 'public_channel,private_channel',
- limit: 200
- });
-
- const channel = result.channels?.find((c: any) => c.name === 'graphics-needed');
- if (channel) {
- return channel.id;
- }
-
- console.error('Channel #graphics-needed not found in:', result.channels?.map((c: any) => c.name));
- return null;
- } catch (error) {
- console.error('Error getting channel list:', error);
- return null;
- }
-}
-
-// Slack API endpoints
-app.get('/api/slack/messages', requireAuth, async (req, res) => {
- try {
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ messages: [], error: 'Channel #graphics-needed not found' });
- }
-
- const result = await slackClient.conversations.history({
- channel: channelId,
- limit: 20
- });
-
- const messages = result.messages?.map((msg: any) => ({
- user: msg.user || 'Unknown',
- text: msg.text,
- timestamp: msg.ts
- })) || [];
-
- res.json({ messages });
- } catch (error) {
- console.error('Slack fetch error:', error);
- res.json({ messages: [], error: String(error) });
- }
-});
-
-app.post('/api/slack/send', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- const channelId = await getGraphicsNeededChannelId();
- if (!channelId) {
- return res.json({ success: false, error: 'Channel #graphics-needed not found' });
- }
-
- // Try to join the channel first if we get not_in_channel error
- try {
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (postError: any) {
- // If not in channel, try to join it
- if (postError.data?.error === 'not_in_channel') {
- console.log('🔧 Bot not in channel, attempting to join #graphics-needed...');
- try {
- await slackClient.conversations.join({ channel: channelId });
- console.log('✅ Successfully joined #graphics-needed');
-
- // Retry posting message
- await slackClient.chat.postMessage({
- channel: channelId,
- text: message,
- unfurl_links: false,
- unfurl_media: false
- });
- res.json({ success: true });
- } catch (joinError) {
- console.error('❌ Failed to join channel:', joinError);
- res.json({ success: false, error: 'Bot needs to be invited to #graphics-needed channel by a workspace admin' });
- }
- } else {
- throw postError;
- }
- }
- } catch (error) {
- console.error('Slack send error:', error);
- res.json({ success: false, error: String(error) });
- }
-});
-
-// Keep track of all samples history (approved + pending)
-const samplesHistory: Array<{
- id: string;
- orderId: string;
- customerEmail: string;
- customerName: string;
- digSku: string;
- productTitle: string;
- imageUrl: string;
- status: 'pending' | 'approved' | 'rejected';
- timestamp: Date;
- processedAt?: Date;
-}> = [];
-
-// Get all samples (pending + approved history + last 90 days from Shopify)
-app.get('/api/samples/all', requireAuth, async (req, res) => {
- try {
- // Combine current pending with history
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected',
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders`
- }));
-
- // Fetch orders from last 90 days from Shopify
- const ninetyDaysAgo = new Date();
- ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
-
- const shopifyUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/orders.json?status=any&created_at_min=${ninetyDaysAgo.toISOString()}&limit=250`;
-
- const response = await fetch(shopifyUrl, {
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- },
- });
-
- const shopifyData: any = await response.json();
- const orders = shopifyData.orders || [];
-
- addLog(`Fetched ${orders.length} orders from Shopify (last 90 days)`, 'processing');
-
- // Extract DIG samples from Shopify orders
- const shopifySamples: Array<any> = [];
-
- for (const order of orders) {
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || 'No email';
-
- for (const item of digItems) {
- shopifySamples.push({
- id: `shopify-${order.id}-${item.id}`,
- orderId: order.name || `#${order.order_number}`,
- orderNumber: order.order_number.toString(),
- shopifyOrderId: order.id.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- status: 'ordered',
- timestamp: new Date(order.created_at),
- orderUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders/${order.id}`,
- financialStatus: order.financial_status,
- fulfillmentStatus: order.fulfillment_status || 'unfulfilled'
- });
- }
- }
- }
-
- addLog(`Found ${shopifySamples.length} DIG samples in recent orders`, 'processing');
-
- const allSamples = [...samplesHistory, ...currentPending, ...shopifySamples].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length, last90Days: shopifySamples.length });
- } catch (error) {
- console.error('Error fetching samples:', error);
- addLog(`Error fetching Shopify orders: ${error}`, 'error');
-
- // Fallback to local history only
- const currentPending = stats.pendingApprovals.map(p => ({
- ...p,
- status: p.status as 'pending' | 'approved' | 'rejected'
- }));
-
- const allSamples = [...samplesHistory, ...currentPending].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- res.json({ samples: allSamples, total: allSamples.length });
- }
-});
-
-app.post('/api/approve/:id', requireAuth, async (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (!approval) {
- return res.status(404).json({ error: 'Approval not found' });
- }
-
- approval.status = 'approved';
-
- try {
- // Send to Slack #new-products channel
- await slackClient.chat.postMessage({
- channel: SLACK_CHANNEL,
- text: `🆕 *DIG Sample Approved & Sent*\n\n📦 *${approval.productTitle}*\n• SKU: \`${approval.digSku}\`\n• Customer: ${approval.customerName}\n• Order: ${approval.orderId}\n\n✅ Sample sent to customer!`,
- unfurl_links: false,
- unfurl_media: false
- });
-
- stats.slackPostsSent++;
- stats.emailsSent++;
- addLog(`Approved and sent DIG-${approval.digSku} to ${approval.customerName}`, 'email');
-
- // Add to history before removing from pending
- samplesHistory.push({
- ...approval,
- status: 'approved',
- processedAt: new Date()
- });
-
- // Remove from pending
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
-
- res.json({ success: true, message: 'Sample approved and sent to customer!' });
- } catch (error) {
- addLog(`Error sending approval: ${error}`, 'error');
- res.status(500).json({ error: 'Failed to send sample' });
- }
-});
-
-app.post('/api/reject/:id', requireAuth, (req, res) => {
- const approval = stats.pendingApprovals.find(a => a.id === req.params.id);
- if (approval) {
- // Add to history before removing
- samplesHistory.push({
- ...approval,
- status: 'rejected',
- processedAt: new Date()
- });
- }
-
- stats.pendingApprovals = stats.pendingApprovals.filter(a => a.id !== req.params.id);
- addLog(`Rejected DIG sample order ${req.params.id}`, 'processing');
- res.json({ success: true, message: 'Order rejected' });
-});
-
-// Google Drive API endpoints
-app.get('/api/drive/search', requireAuth, async (req, res) => {
- try {
- const query = (req.query.q as string) || '';
- const files = await searchDriveFiles(query);
-
- res.json({
- success: true,
- files: files.map((f: any) => ({
- id: f.id,
- name: f.name,
- mimeType: f.mimeType,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink,
- size: f.size,
- modified: f.modifiedTime
- })),
- total: files.length
- });
- } catch (error) {
- res.status(500).json({ error: `Drive search failed: ${error}` });
- }
-});
-
-app.get('/api/drive/file/:id', requireAuth, async (req, res) => {
- try {
- const fileInfo = await getDriveFileLink(req.params.id);
- res.json({ success: true, file: fileInfo });
- } catch (error) {
- res.status(500).json({ error: `Failed to get file: ${error}` });
- }
-});
-
-// Shopify webhook endpoint (receives new orders)
-app.post('/webhook/shopify/orders/create', async (req, res) => {
- try {
- const order = req.body;
- stats.totalOrders++;
-
- addLog(`New order received: #${order.order_number}`, 'order');
-
- // Check for DIG-series items
- const digItems = order.line_items?.filter((item: any) =>
- item.sku && item.sku.toUpperCase().startsWith('DIG-')
- ) || [];
-
- if (digItems.length > 0) {
- stats.digOrdersFound++;
-
- const customerName = `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim() || 'Customer';
- const customerEmail = order.customer?.email || '';
-
- stats.lastOrder = {
- orderId: order.name || order.order_number.toString(),
- customerName,
- digItems: digItems.map((item: any) => item.sku),
- timestamp: new Date()
- };
-
- addLog(`Found ${digItems.length} DIG items in order #${order.order_number}`, 'processing');
-
- // Process each DIG item
- for (const item of digItems) {
- await processDIGItem(order, item, customerEmail, customerName);
- }
- }
-
- res.json({ success: true });
- } catch (error) {
- addLog(`Webhook error: ${error}`, 'error');
- res.status(500).json({ error: 'Webhook processing failed' });
- }
-});
-
-// Google Drive search function
-async function searchDriveFiles(query: string = '') {
- try {
- const response = await drive.files.list({
- q: query ? `name contains '${query}' and mimeType contains 'image/'` : "mimeType contains 'image/'",
- fields: 'files(id, name, mimeType, webViewLink, thumbnailLink, size, createdTime, modifiedTime)',
- orderBy: 'modifiedTime desc',
- pageSize: 50
- });
-
- return response.data.files || [];
- } catch (error) {
- addLog(`Error searching Drive: ${error}`, 'error');
- return [];
- }
-}
-
-// Get Drive file download link
-async function getDriveFileLink(fileId: string) {
- try {
- const response = await drive.files.get({
- fileId,
- fields: 'webContentLink, webViewLink'
- });
- return response.data;
- } catch (error) {
- addLog(`Error getting Drive file link: ${error}`, 'error');
- return null;
- }
-}
-
-async function processDIGItem(order: any, item: any, customerEmail: string, customerName: string) {
- try {
- stats.filesProcessed++;
-
- // Search Google Drive for matching files
- const sku = item.sku.replace('DIG-', '');
- const driveFiles = await searchDriveFiles(sku);
-
- addLog(`Found ${driveFiles.length} Drive files matching SKU: ${sku}`, 'processing');
-
- // Create approval entry
- const approval = {
- id: `${order.id}-${item.id}-${Date.now()}`,
- orderId: order.name || order.order_number.toString(),
- customerEmail,
- customerName,
- digSku: item.sku,
- productTitle: item.title,
- imageUrl: item.properties?.find((p: any) => p.name === 'Image')?.value || '',
- resizedImageUrl: '', // Will be populated after image processing
- driveFiles: driveFiles.map((f: any) => ({
- id: f.id,
- name: f.name,
- link: f.webViewLink,
- thumbnail: f.thumbnailLink
- })),
- status: 'pending' as const,
- timestamp: new Date()
- };
-
- stats.pendingApprovals.push(approval);
-
- addLog(`Created approval for ${item.sku} - ${customerName} with ${driveFiles.length} Drive files`, 'processing');
-
- } catch (error) {
- addLog(`Error processing DIG item: ${error}`, 'error');
- }
-}
-
-// Manual test endpoint
-app.post('/api/test/dig-order', requireAuth, async (req, res) => {
- const mockOrder = {
- id: Date.now(),
- order_number: 12345,
- name: '#TEST-' + Date.now(),
- customer: {
- first_name: 'Test',
- last_name: 'Customer',
- email: 'test@example.com'
- },
- line_items: [
- {
- id: Date.now(),
- sku: 'DIG-12345',
- title: 'Digital Sample - Test Pattern',
- properties: [
- { name: 'Image', value: 'https://via.placeholder.com/800x600' }
- ]
- }
- ]
- };
-
- await processDIGItem(mockOrder, mockOrder.line_items[0], 'test@example.com', 'Test Customer');
-
- res.json({ success: true, message: 'Test DIG order created!' });
-});
-
-
-// API: Chat endpoint for universal header
-app.post('/api/chat', requireAuth, async (req, res) => {
-
- // Memory system commands
- if (message.toLowerCase().match(/^(remember|note|preference|learning):/i)) {
- const parts = message.split(':');
- const type = parts[0].toLowerCase();
- const content = parts.slice(1).join(':').trim();
- const memType = type === 'remember' ? 'note' : type as any;
- agentMemory.add(content, memType);
- return res.json({
- response: `✅ Memory saved: "${content}"`,
- success: true,
- memoryAdded: true
- });
- }
-
- if (message.toLowerCase().includes('show') && message.toLowerCase().includes('memor')) {
- const summary = agentMemory.getContextSummary();
- return res.json({ response: summary, success: true });
- }
-
- if (message.toLowerCase().includes('search memor')) {
- const query = message.split(/for|about/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- return res.json({
- response: results.length > 0
- ? 'Found memories:\n' + results.map(m => `- ${m.content}`).join('\n')
- : 'No matching memories found.',
- success: true
- });
- }
-
- if (message.toLowerCase().includes('forget') || message.toLowerCase().includes('delete memor')) {
- const query = message.split(/forget|delete/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- if (results.length > 0) {
- agentMemory.delete(results[0].id);
- return res.json({ response: `✅ Forgot: "${results[0].content}"`, success: true });
- }
- return res.json({ response: 'Nothing found to forget.', success: true });
- }
-
- try {
- const { message } = req.body;
- const systemPrompt = `You are the Digital Samples AI assistant for Designer Wallcoverings. Process DIG-series orders and file delivery`;
-
- const Anthropic = require('@anthropic-ai/sdk').default;
- const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY || '' });
-
- const response = await anthropic.messages.create({
- model: 'claude-3-5-sonnet-20241022',
- max_tokens: 1024,
- system: systemPrompt,
- messages: [{ role: 'user', content: message }],
- });
-
- const assistantMessage = response.content[0].type === 'text' ? response.content[0].text : '';
- res.json({ response: assistantMessage, success: true });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-// Start server
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('📦 Digital Samples Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('✅ Monitoring Shopify for DIG-series orders...');
- console.log('📁 Google Drive integration: Ready');
- console.log('🖼️ Image resizing: 24" @ 150 DPI');
- console.log('📢 Slack channel: #new-products');
- console.log('');
- addLog('Digital Samples Agent initialized and running', 'processing');
-});
diff --git a/DW-Agents/dw-agents/needs-attention-agent.ts.backup b/DW-Agents/dw-agents/needs-attention-agent.ts.backup
deleted file mode 100644
index 7e635526..00000000
--- a/DW-Agents/dw-agents/needs-attention-agent.ts.backup
+++ /dev/null
@@ -1,768 +0,0 @@
-/**
- * DW-Agents: Needs Attention Agent
- *
- * Monitors urgent matters across all DW-Agents requiring immediate action:
- * - Pending approvals and orders
- * - Settlement violations
- * - Failed processes
- * - Customer complaints
- * - Vendor issues
- * - System errors
- *
- * Port: 9886
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express, { Request, Response } from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth.js';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import fetch from 'node-fetch';
-import { AgentMemory } from './shared-memory-system.js';
-
-// Import theme utilities
-const { getThemeStyles } = require('./shared-ui-components.js');
-
-const app = express();
-const PORT = 9886; // Fixed: back to 9886 as 9891 is used by in-parallel agent
-
-// Initialize agent memory system
-const agentMemory = new AgentMemory('needs-attention');
-console.log('📚 Memory system initialized for needs-attention');
-
-// Shopify configuration
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || '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';
-const API_VERSION = '2024-07';
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- }
-}
-
-app.use(
- session({
- secret: 'dw-needs-attention-2025',
- resave: false,
- saveUninitialized: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 7 * 24 * 60 * 60 * 1000,
- },
- })
-);
-
-const requireAuth = (req: Request, res: Response, next: any) => {
- // TEMPORARY: Auth disabled for 4 hours
- return next();
-
- /* Original auth:
-
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
- */
-};
-
-// Login page
-app.get('/login', (req: Request, res: Response) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Needs Attention - Login</title>
- ${getThemeStyles('modern-minimalist')}
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #DC143C 0%, #FF6B6B 100%);
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #DC143C; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #DC143C 0%, #FF6B6B 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>🚨 Needs Attention</h1>
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req: Request, res: Response) => {
- const { username, password } = req.body;
- if (username === 'admin' && password === '2025') {
- req.session.authenticated = true;
-
- // Set SSO cookie for cross-agent authentication
- setSSOToken(res);
-
- res.redirect('/');
- } else {
- res.redirect('/login');
- }
-});
-
-// Main dashboard
-app.get('/', requireAuth, (req: Request, res: Response) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Needs Attention - DW-Agents</title>
- ${getThemeStyles('modern-minimalist')}
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
-
- @keyframes gradientShift {
- 0% { background-position: 0% 50%; }
- 50% { background-position: 100% 50%; }
- 100% { background-position: 0% 50%; }
- }
-
- @keyframes fadeIn {
- from { opacity: 0; transform: translateY(20px); }
- to { opacity: 1; transform: translateY(0); }
- }
-
- @keyframes pulse {
- 0%, 100% { transform: scale(1); }
- 50% { transform: scale(1.05); }
- }
-
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
- background: linear-gradient(-45deg, #DC143C, #FF6B6B, #FF4757, #C0392B);
- background-size: 400% 400%;
- animation: gradientShift 15s ease infinite;
- padding: 20px;
- min-height: 100vh;
- }
-
- .header {
- background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.98) 100%);
- backdrop-filter: blur(10px);
- padding: 40px;
- border-radius: 20px;
- margin-bottom: 30px;
- box-shadow: 0 20px 60px rgba(0,0,0,0.2), 0 0 100px rgba(220,20,60,0.1);
- animation: fadeIn 0.8s ease;
- border: 1px solid rgba(220,20,60,0.1);
- }
-
- .header h1 {
- color: transparent;
- background: linear-gradient(135deg, #DC143C 0%, #FF6B6B 100%);
- -webkit-background-clip: text;
- background-clip: text;
- font-size: 3em;
- margin-bottom: 15px;
- font-weight: 800;
- letter-spacing: -1px;
- }
-
- .header .subtitle {
- color: #555;
- font-size: 1.2em;
- font-weight: 500;
- }
- .refresh-bar {
- background: white;
- padding: 15px 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .refresh-bar button {
- background: linear-gradient(135deg, #DC143C 0%, #FF6B6B 100%);
- color: white;
- border: none;
- padding: 10px 20px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- }
- .summary-cards {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 15px;
- margin-bottom: 20px;
- }
- .summary-card {
- background: white;
- padding: 20px;
- border-radius: 12px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- text-align: center;
- }
- .summary-card .number {
- font-size: 2.5em;
- font-weight: bold;
- color: #DC143C;
- }
- .summary-card .label {
- color: #666;
- margin-top: 5px;
- }
- .attention-section {
- background: white;
- padding: 25px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .attention-section h2 {
- color: #DC143C;
- margin-bottom: 15px;
- font-size: 1.5em;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .attention-item {
- background: #fff3f3;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 4px solid #DC143C;
- }
- .attention-item.critical { border-left-color: #DC143C; background: #ffebee; }
- .attention-item.warning { border-left-color: #FFA500; background: #fff8e1; }
- .attention-item.info { border-left-color: #2196F3; background: #e3f2fd; }
- .attention-item strong { color: #333; }
- .attention-item .priority {
- display: inline-block;
- padding: 3px 10px;
- border-radius: 12px;
- font-size: 0.85em;
- font-weight: 600;
- margin-right: 10px;
- }
- .priority.critical { background: #DC143C; color: white; }
- .priority.warning { background: #FFA500; color: white; }
- .priority.info { background: #2196F3; color: white; }
- .action-link {
- color: #DC143C;
- text-decoration: none;
- font-weight: 600;
- margin-left: 10px;
- }
- .no-items { color: #888; text-align: center; padding: 20px; }
- </style>
-</head>
-<body>
- <div class="header">
- <h1>🚨 Needs Attention</h1>
- <div class="subtitle">Urgent matters requiring immediate action</div>
- </div>
-
- <div class="refresh-bar">
- <span id="lastUpdate" style="color: #888;">Last updated: --:--</span>
- <span id="countdown" style="color: #888; margin-left: 20px;">Next refresh in: 20s</span>
- <button onclick="refreshAttention()">🔄 Refresh Now</button>
- </div>
-
- <div class="summary-cards">
- <div class="summary-card">
- <div class="number" id="criticalCount">0</div>
- <div class="label">Critical</div>
- </div>
- <div class="summary-card">
- <div class="number" id="warningCount">0</div>
- <div class="label">Warnings</div>
- </div>
- <div class="summary-card">
- <div class="number" id="totalCount">0</div>
- <div class="label">Total Items</div>
- </div>
- </div>
-
- <div class="attention-section">
- <h2>🔴 Critical - Immediate Action Required</h2>
- <div id="criticalItems" class="no-items">Loading...</div>
- </div>
-
- <div class="attention-section">
- <h2>⚠️ Warnings - Attention Needed</h2>
- <div id="warningItems" class="no-items">Loading...</div>
- </div>
-
- <div class="attention-section">
- <h2>ℹ️ Information - Review Recommended</h2>
- <div id="infoItems" class="no-items">Loading...</div>
- </div>
-
- <script>
- async function refreshAttention() {
- try {
- const data = await fetch('/api/attention', {
- credentials: 'same-origin'
- }).then(r => r.json());
-
- const critical = data.items.filter(i => i.priority === 'critical');
- const warnings = data.items.filter(i => i.priority === 'warning');
- const info = data.items.filter(i => i.priority === 'info');
-
- document.getElementById('criticalCount').textContent = critical.length;
- document.getElementById('warningCount').textContent = warnings.length;
- document.getElementById('totalCount').textContent = data.items.length;
-
- // Critical items
- const criticalDiv = document.getElementById('criticalItems');
- if (critical.length > 0) {
- criticalDiv.innerHTML = critical.map(item => {
- const metadata = item.metadata || {};
- const thumbnail = metadata.imageUrl ? \`<img src="\${metadata.imageUrl}" style="width:80px;height:80px;object-fit:cover;border-radius:8px;margin-right:15px;float:left;">\` : '';
- const productTitle = metadata.productTitle ? \`<div style="font-size:0.9em;color:#666;margin:5px 0;">\${metadata.productTitle}</div>\` : '';
- return \`
- <div class="attention-item critical" style="min-height:100px;">
- \${thumbnail}
- <div>
- <span class="priority critical">\${item.priority.toUpperCase()}</span>
- <strong>\${item.title}</strong><br>
- \${productTitle}
- \${item.description}
- \${item.actionUrl ? \`<a href="\${item.actionUrl}" target="_blank" class="action-link">→ Take Action</a>\` : ''}
- </div>
- <div style="clear:both;"></div>
- </div>
- \`;
- }).join('');
- } else {
- criticalDiv.innerHTML = '<p class="no-items">✅ No critical items</p>';
- }
-
- // Warning items
- const warningDiv = document.getElementById('warningItems');
- if (warnings.length > 0) {
- warningDiv.innerHTML = warnings.map(item => \`
- <div class="attention-item warning">
- <span class="priority warning">\${item.priority.toUpperCase()}</span>
- <strong>\${item.title}</strong><br>
- \${item.description}
- \${item.actionUrl ? \`<a href="\${item.actionUrl}" target="_blank" class="action-link">→ View Details</a>\` : ''}
- </div>
- \`).join('');
- } else {
- warningDiv.innerHTML = '<p class="no-items">✅ No warnings</p>';
- }
-
- // Info items
- const infoDiv = document.getElementById('infoItems');
- if (info.length > 0) {
- infoDiv.innerHTML = info.map(item => \`
- <div class="attention-item info">
- <span class="priority info">\${item.priority.toUpperCase()}</span>
- <strong>\${item.title}</strong><br>
- \${item.description}
- \${item.actionUrl ? \`<a href="\${item.actionUrl}" target="_blank" class="action-link">→ Review</a>\` : ''}
- </div>
- \`).join('');
- } else {
- infoDiv.innerHTML = '<p class="no-items">✅ No information items</p>';
- }
-
- document.getElementById('lastUpdate').textContent = \`Last updated: \${new Date().toLocaleTimeString()}\`;
- } catch (error) {
- console.error('Error fetching attention items:', error);
- document.getElementById('lastUpdate').textContent = 'Error: Unable to refresh';
- alert('Unable to refresh dashboard. Please check console for details.');
- }
- }
-
- // Countdown timer
- let countdown = 20;
- function updateCountdown() {
- document.getElementById('countdown').textContent = \`Next refresh in: \${countdown}s\`;
- countdown--;
- if (countdown < 0) {
- countdown = 20;
- }
- }
-
- // Auto-refresh every 20 seconds to give time for all processes
- setInterval(refreshAttention, 20000);
- setInterval(updateCountdown, 1000);
- refreshAttention();
- updateCountdown();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoint to get items needing attention
-app.get('/api/attention', requireAuth, async (req: Request, res: Response) => {
- try {
- const attentionItems: Array<{
- title: string;
- description: string;
- priority: 'critical' | 'warning' | 'info';
- source: string;
- actionUrl?: string;
- timestamp: Date;
- }> = [];
-
- // Check for unfulfilled orders older than 24 hours
- const yesterday = new Date();
- yesterday.setHours(yesterday.getHours() - 24);
-
- const ordersUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/orders.json?status=open&created_at_max=${yesterday.toISOString()}&fulfillment_status=unfulfilled&limit=250`;
- const ordersResponse = await fetch(ordersUrl, {
- headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN },
- });
- const ordersData: any = await ordersResponse.json();
- const unfulfilled = ordersData.orders || [];
-
- if (unfulfilled.length > 0) {
- attentionItems.push({
- title: `${unfulfilled.length} Unfulfilled Orders (>24hrs)`,
- description: `Orders placed more than 24 hours ago are still unfulfilled`,
- priority: 'critical',
- source: 'Orders',
- actionUrl: `https://admin.shopify.com/store/${SHOPIFY_STORE.replace('.myshopify.com', '')}/orders?status=open&fulfillment_status=unfulfilled`,
- timestamp: new Date()
- });
- }
-
- // Check for pending digital samples
- attentionItems.push({
- title: 'Pending Digital Sample Approvals',
- description: 'Digital samples waiting for approval and delivery',
- priority: 'warning',
- source: 'Digital Samples',
- actionUrl: 'http://45.61.58.125:9879/',
- timestamp: new Date()
- });
-
- // CRITICAL: Settlement violations found - using proper Shopify admin URLs
- // Fetch tasks from agent memory (added via webhook)
- try {
- const memories = agentMemory.getByType('task');
-
- for (const task of memories) {
- let taskContent;
- try {
- taskContent = JSON.parse(task.content);
- } catch (e) {
- console.error('Failed to parse task content:', task.content);
- continue;
- }
-
- if (taskContent.status === 'pending') {
- // If this is a product-related task, verify the product is still active
- if (taskContent.metadata?.productId && taskContent.source === 'Legal Team') {
- try {
- const productResponse = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${taskContent.metadata.productId}.json`, {
- headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN }
- });
-
- if (productResponse.ok) {
- const productData: any = await productResponse.json();
- const productStatus = productData.product?.status;
-
- // If product is not active, skip this task (don't show it)
- if (productStatus !== 'active') {
- console.log(` ⚠️ Product ${taskContent.metadata.productId} is ${productStatus}, auto-completing task`);
-
- // Auto-complete the task
- agentMemory.delete(task.id);
- taskContent.status = 'completed';
- taskContent.completedAt = new Date().toISOString();
- agentMemory.add(JSON.stringify(taskContent), 'task', taskContent.metadata);
-
- continue; // Skip adding to attentionItems
- }
- }
- } catch (verifyError) {
- console.error(` ⚠️ Could not verify product ${taskContent.metadata.productId} status:`, verifyError);
- // If we can't verify, still show the task
- }
- }
-
- // Add timestamp to title to show when detected
- const detectedTime = new Date(taskContent.createdAt || task.timestamp);
- const timeStr = detectedTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
-
- attentionItems.push({
- title: `${taskContent.title || 'Untitled Task'} (Detected: ${timeStr})`,
- description: taskContent.description || '',
- priority: taskContent.priority || 'warning',
- source: taskContent.source || 'Unknown',
- actionUrl: taskContent.metadata?.url || taskContent.metadata?.productUrl,
- timestamp: detectedTime,
- metadata: taskContent.metadata || {}
- });
- }
- }
- } catch (memoryError) {
- console.error('Error fetching tasks from memory:', memoryError);
- }
-
- // Check for DOWN websites from Server Uptime
- try {
- const uptimeResponse = await fetch('http://localhost:9888/api/status');
- const uptimeData: any = await uptimeResponse.json();
-
- if (uptimeData.domains) {
- const downDomains = uptimeData.domains.filter((d: any) => d.status === 'down');
-
- if (downDomains.length > 0) {
- attentionItems.push({
- title: `🚨 ${downDomains.length} WEBSITES DOWN`,
- description: `Critical: ${downDomains.map((d: any) => d.domain).join(', ')} are not responding`,
- priority: 'critical',
- source: 'Server Uptime',
- actionUrl: 'http://45.61.58.125:9888/',
- timestamp: new Date()
- });
- }
- }
- } catch (error) {
- console.error('Could not fetch domain health:', error);
- }
-
- // Check for vendor relationships needing attention (placeholder)
- attentionItems.push({
- title: 'Vendor Relationships Need Attention',
- description: 'Budget Walls Inc - Recent quality issues require discussion',
- priority: 'warning',
- source: 'Trend Research',
- actionUrl: 'http://45.61.58.125:9883/',
- timestamp: new Date()
- });
-
- // Sort by timestamp descending (newest first)
- attentionItems.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
-
- res.json({
- items: attentionItems,
- summary: {
- critical: attentionItems.filter(i => i.priority === 'critical').length,
- warning: attentionItems.filter(i => i.priority === 'warning').length,
- info: attentionItems.filter(i => i.priority === 'info').length,
- total: attentionItems.length
- },
- generatedAt: new Date()
- });
- } catch (error) {
- console.error('Error fetching attention items:', error);
- res.status(500).json({ error: 'Failed to fetch attention items' });
- }
-});
-
-// Webhook endpoint to receive tasks from other agents (e.g., legal team)
-app.post('/api/webhook/task', async (req, res) => {
- try {
- const { title, description, priority, source, metadata } = req.body;
-
- console.log(`📥 Received task from ${source}: ${title}`);
-
- // Check if task already exists for this product (prevent duplicates)
- if (metadata?.productId) {
- const existingTasks = agentMemory.getByType('task');
- for (const task of existingTasks) {
- try {
- const taskContent = JSON.parse(task.content);
- if (taskContent.metadata?.productId === metadata.productId && taskContent.status === 'pending') {
- console.log(` ⚠️ Task already exists for product ${metadata.productId} - skipping duplicate`);
- return res.json({
- success: true,
- taskId: task.id,
- message: 'Task already exists (duplicate prevented)',
- duplicate: true
- });
- }
- } catch (e) {
- // Skip invalid tasks
- }
- }
- }
-
- // Store task in agent memory
- const taskData = {
- title,
- description,
- priority: priority || 'high',
- source: source || 'unknown',
- metadata: metadata || {},
- status: 'pending',
- createdAt: new Date().toISOString()
- };
-
- agentMemory.add(JSON.stringify(taskData), 'task', metadata);
-
- console.log(`✅ Task added to needs-attention queue`);
-
- res.json({
- success: true,
- taskId: Date.now().toString(),
- message: 'Task received and queued'
- });
- } catch (error:any) {
- console.error('❌ Webhook error:', error);
- res.status(500).json({ error: error.message });
- }
-});
-
-// Endpoint to mark task as completed (called when product is set to draft)
-app.post('/api/webhook/task/complete', async (req, res) => {
- try {
- const { productId } = req.body;
-
- console.log(`📝 Marking tasks complete for product ${productId}`);
-
- // Get all task memories
- const memories = agentMemory.getByType('task');
- let completedCount = 0;
- const completedTasks: any[] = [];
-
- for (const memory of memories) {
- try {
- const taskContent = JSON.parse(memory.content);
-
- // If this task matches the product ID and is still pending
- if (taskContent.metadata?.productId === productId && taskContent.status === 'pending') {
- // Mark as completed by deleting and re-adding with completed status
- agentMemory.delete(memory.id);
-
- taskContent.status = 'completed';
- taskContent.completedAt = new Date().toISOString();
-
- agentMemory.add(JSON.stringify(taskContent), 'task', taskContent.metadata);
- completedCount++;
- completedTasks.push(taskContent);
-
- console.log(` ✅ Marked task as completed: ${taskContent.title}`);
- }
- } catch (e) {
- console.error('Failed to parse task:', e);
- }
- }
-
- // Save completed tasks to daily log file
- if (completedTasks.length > 0) {
- const fs = require('fs');
- const path = require('path');
-
- const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
- const logDir = path.join(__dirname, 'daily-tasks');
- const logFile = path.join(logDir, `completed-tasks-${today}.json`);
-
- // Create directory if it doesn't exist
- if (!fs.existsSync(logDir)) {
- fs.mkdirSync(logDir, { recursive: true });
- }
-
- // Load existing log or create new array
- let dailyLog: any[] = [];
- if (fs.existsSync(logFile)) {
- try {
- dailyLog = JSON.parse(fs.readFileSync(logFile, 'utf8'));
- } catch (e) {
- console.error('Failed to parse existing log, starting fresh');
- }
- }
-
- // Append new completed tasks
- dailyLog.push(...completedTasks);
-
- // Save to file
- fs.writeFileSync(logFile, JSON.stringify(dailyLog, null, 2));
- console.log(` 📁 Saved ${completedTasks.length} completed task(s) to ${logFile}`);
- }
-
- res.json({
- success: true,
- completedCount,
- message: `${completedCount} task(s) marked as completed and logged`
- });
- } catch (error: any) {
- console.error('❌ Error completing task:', error);
- res.status(500).json({ error: error.message });
- }
-});
-
-// API: Chat endpoint for universal header
-app.post('/api/chat', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
- const systemPrompt = `You are the Needs Attention AI assistant for Designer Wallcoverings. Handle urgent issues requiring immediate action`;
-
- const Anthropic = require('@anthropic-ai/sdk').default;
- const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY || '' });
-
- const response = await anthropic.messages.create({
- model: 'claude-3-5-sonnet-20241022',
- max_tokens: 1024,
- system: systemPrompt,
- messages: [{ role: 'user', content: message }],
- });
-
- const assistantMessage = response.content[0].type === 'text' ? response.content[0].text : '';
- res.json({ response: assistantMessage, success: true });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-// Start server
-app.listen(PORT, '0.0.0.0', () => {
- console.log(`🚨 Needs Attention Agent running on port ${PORT}`);
- console.log(`Dashboard: http://45.61.58.125:${PORT}`);
-});
diff --git a/DW-Agents/dw-agents/purchasing-office-agent.ts.backup b/DW-Agents/dw-agents/purchasing-office-agent.ts.backup
deleted file mode 100644
index b125de72..00000000
--- a/DW-Agents/dw-agents/purchasing-office-agent.ts.backup
+++ /dev/null
@@ -1,1544 +0,0 @@
-/**
- * DW-Agents: Purchasing (Office Supplies) Agent
- *
- * Manages office supply ordering with intelligent price comparison:
- * 1. Chat interface for supply requests and order tracking
- * 2. Amazon Personal & Business account integration
- * 3. Price comparison across vendors
- * 4. Order tracking and inventory management
- * 5. Automatic restock recommendations
- *
- * Port: 9880
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import session from 'express-session';
-import fetch from 'node-fetch';
-import * as fs from 'fs';
-import * as cheerio from 'cheerio';
-import { fetchRealAmazonOrders, fetchRealInventory, saveOrder, saveInventory } from './amazon-real-orders';
-import { AgentMemory } from './shared-memory-system';
-import { searchCompetitors, CompetitorPrice } from './office-competitors-search';
-import { cacheProduct, cacheOrder, getCachedProduct, searchCachedProducts, getCacheStats, CachedProduct } from './amazon-data-cache';
-
-// Import theme utilities
-const { getThemeStyles } = require('./shared-ui-components.js');
-
-const app = express();
-const PORT = 9880;
-// Initialize agent memory system
-const agentMemory = new AgentMemory('purchasing-office');
-console.log('📚 Memory system initialized for purchasing-office');
-
-
-// Authentication
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-// Amazon configuration
-const AMAZON_PERSONAL_EMAIL = 'nataliaabrams@gmail.com';
-const AMAZON_PERSONAL_PASSWORD = 'Urbanon341';
-const AMAZON_BUSINESS_EMAIL = 'info@designerwallcoverings.com';
-const AMAZON_BUSINESS_PASSWORD = '*Amazonaccess911*';
-
-// Anthropic Claude
-const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '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';
-const anthropic = new Anthropic({
- apiKey: ANTHROPIC_API_KEY,
-});
-
-// Stats tracking - LOAD REAL DATA
-let stats = {
- totalQueries: 0,
- priceComparisons: 0,
- ordersTracked: 0,
- suppliesManaged: 0,
- lastQuery: null as { question: string; timestamp: Date } | null,
- recentOrders: [] as Array<{
- id: string;
- item: string;
- vendor: string;
- price: number;
- status: string;
- timestamp: Date;
- }>,
- inventory: [] as Array<{
- item: string;
- quantity: number;
- reorderLevel: number;
- lastOrdered: Date | null;
- }>
-};
-
-// Load real data on startup
-async function loadRealData() {
- console.log('📦 Loading real Amazon data...');
-
- try {
- const orders = await fetchRealAmazonOrders();
- const inventory = await fetchRealInventory();
-
- stats.recentOrders = orders;
- stats.inventory = inventory;
- stats.ordersTracked = orders.length;
- stats.suppliesManaged = inventory.length;
-
- console.log(`✅ Loaded ${orders.length} orders and ${inventory.length} inventory items`);
-
- if (orders.length === 0) {
- console.log('\n⚠️ NO ORDERS FOUND - Add real data:');
- console.log(' 📝 Edit: /root/DW-Agents/data/amazon-orders.json');
- }
-
- if (inventory.length === 0) {
- console.log('\n⚠️ NO INVENTORY - Add items:');
- console.log(' 📝 Edit: /root/DW-Agents/data/office-inventory.json');
- }
-
- } catch (error) {
- console.error('❌ Error loading real data:', error);
- }
-}
-
-// Load data immediately
-loadRealData();
-
-// Chat history per session
-const chatSessions: Map<string, Array<{ role: 'user' | 'assistant', content: string }>> = new Map();
-
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-purchasing-office-agent-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-// Load top deals from price tracking
-function loadTopDeals(): string {
- try {
- const priceTrackingPath = '/root/DW-Agents/data/price-tracking.json';
- if (fs.existsSync(priceTrackingPath)) {
- const tracking = JSON.parse(fs.readFileSync(priceTrackingPath, 'utf-8'));
- const deals = tracking
- .filter((t: any) => t.priceChange < 0)
- .sort((a: any, b: any) => a.percentChange - b.percentChange)
- .slice(0, 4);
-
- if (deals.length === 0) {
- return '';
- }
-
- const lastChecked = deals[0]?.lastChecked ? new Date(deals[0].lastChecked) : new Date();
- const hoursAgo = Math.floor((Date.now() - lastChecked.getTime()) / (1000 * 60 * 60));
- const timeText = hoursAgo < 1 ? 'Just now' : hoursAgo === 1 ? '1 hour ago' : `${hoursAgo} hours ago`;
- const exactTime = lastChecked.toLocaleString('en-US', {
- month: 'short',
- day: 'numeric',
- hour: 'numeric',
- minute: '2-digit',
- hour12: true
- });
-
- const dealItems = deals.map((deal: any) => {
- const itemShort = deal.item.substring(0, 40) + (deal.item.length > 40 ? '...' : '');
- const savings = Math.abs(deal.priceChange).toFixed(2);
- const percent = Math.abs(deal.percentChange).toFixed(0);
- return `
- <a href="${deal.url}" target="_blank" class="deal-item" style="text-decoration: none; color: white;" title="Click to search on Amazon - Prices may vary">
- <strong>${itemShort}</strong>
- <span>Was $${deal.originalPrice.toFixed(2)} → $${deal.currentPrice.toFixed(2)}</span>
- <span class="deal-savings">SAVE $${savings} (${percent}% OFF)</span>
- </a>
- `;
- }).join('');
-
- return `
- <div class="deals-banner" id="dealsBanner">
- <button onclick="refreshDeals()" class="refresh-deals-btn" id="refreshBtn">
- 🔄 REFRESH NOW
- </button>
- <div style="display: flex; align-items: center; gap: 15px; flex-shrink: 0; padding: 0 15px; border-right: 2px solid rgba(255,255,255,0.3);">
- <div class="deals-banner-title">🔥 DEALS ALERT!</div>
- <div style="font-size: 0.75em; opacity: 0.9; white-space: nowrap;">
- ⏰ <strong>${exactTime}</strong><br>
- <span style="font-size: 0.9em;">(${timeText})</span>
- </div>
- </div>
- ${dealItems}
- </div>
- `;
- }
- } catch (e) {
- // Ignore if file doesn't exist yet
- }
- return '';
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // TEMPORARY: Auth disabled for 4 hours (expires ~3:30 AM)
- return next();
-
- /* Original auth code - will be re-enabled automatically
- if (req.session.authenticated) {
- next();
- } else {
- // For API requests, return JSON instead of redirecting
- if (req.path.startsWith('/api/')) {
- res.status(401).json({ error: 'Not authenticated. Please refresh the page and login.' });
- } else {
- res.redirect('/login');
- }
- }
- */
-};
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Purchasing Agent</title>
- ${getThemeStyles('modern-minimalist')}
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #36454f; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>🛒 Purchasing Agent</h1>
- <p style="text-align: center; color: #666; margin-bottom: 20px;">Office Supplies</p>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard with chat
-app.get('/', requireAuth, (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Purchasing (Office Supplies) - DW-Agents</title>
- ${getThemeStyles('modern-minimalist')}
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
- <meta http-equiv="Pragma" content="no-cache">
- <meta http-equiv="Expires" content="0">
- <!-- v2.1 - Deals Banner with Refresh -->
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .container {
- max-width: 1800px;
- margin: 0 auto;
- display: grid;
- grid-template-columns: 280px 1fr 400px;
- gap: 20px;
- }
- .sidebar {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #36454f; font-size: 2em; margin-bottom: 10px; }
- .header .subtitle { color: #666; font-size: 1.1em; }
- .stats-card {
- background: white;
- padding: 20px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stats-card h3 { color: #888; font-size: 0.85em; text-transform: uppercase; margin-bottom: 8px; }
- .stats-card .value { font-size: 2em; font-weight: bold; color: #36454f; }
- .chat-container {
- background: white;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- display: flex;
- flex-direction: column;
- height: calc(100vh - 40px);
- }
- .chat-header {
- padding: 20px;
- border-bottom: 2px solid #f0f0f0;
- }
- .chat-header h2 { color: #36454f; margin-bottom: 5px; }
- .chat-header p { color: #888; font-size: 0.9em; }
- .chat-messages {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- background: #f5f5f5;
- }
- .message {
- margin-bottom: 15px;
- display: flex;
- gap: 10px;
- }
- .message.user {
- flex-direction: row-reverse;
- }
- .message-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.2em;
- flex-shrink: 0;
- }
- .message.user .message-avatar {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- }
- .message.assistant .message-avatar {
- background: #e0e0e0;
- }
- .message-content {
- max-width: 70%;
- padding: 12px 16px;
- border-radius: 12px;
- line-height: 1.5;
- }
- .message.user .message-content {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message.assistant .message-content {
- background: white;
- color: #333;
- border-bottom-left-radius: 4px;
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
- }
- .chat-input-area {
- padding: 20px;
- border-top: 2px solid #f0f0f0;
- background: white;
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- #chatInput {
- flex: 1;
- padding: 12px 16px;
- border: 2px solid #e0e0e0;
- border-radius: 25px;
- font-size: 1em;
- outline: none;
- }
- #chatInput:focus {
- border-color: #36454f;
- }
- .send-btn {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border: none;
- padding: 12px 30px;
- border-radius: 25px;
- cursor: pointer;
- font-weight: 600;
- transition: transform 0.2s;
- }
- .send-btn:hover {
- transform: translateY(-2px);
- }
- .send-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .suggestions {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- margin-top: 10px;
- }
- .suggestion-chip {
- background: #f0f0f0;
- padding: 8px 15px;
- border-radius: 20px;
- font-size: 0.85em;
- cursor: pointer;
- transition: all 0.2s;
- }
- .suggestion-chip:hover {
- background: #36454f;
- color: white;
- }
- .typing-indicator {
- display: none;
- padding: 10px;
- color: #888;
- font-style: italic;
- }
- .typing-indicator.active {
- display: block;
- }
- .price-comparison {
- background: #f5f5f5;
- border: 1px solid #e0e0e0;
- border-radius: 8px;
- padding: 10px;
- margin-top: 10px;
- }
- .price-item {
- display: flex;
- justify-content: space-between;
- padding: 8px 0;
- border-bottom: 1px solid #e0e0e0;
- }
- .price-item:last-child {
- border-bottom: none;
- }
- .vendor-name {
- font-weight: 600;
- color: #36454f;
- }
- .price {
- font-weight: bold;
- color: #28a745;
- }
- .inventory-panel {
- background: white;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- display: flex;
- flex-direction: column;
- height: calc(100vh - 40px);
- overflow: hidden;
- }
- .inventory-header {
- padding: 20px;
- border-bottom: 2px solid #f0f0f0;
- }
- .inventory-header h2 { color: #36454f; margin-bottom: 5px; }
- .inventory-header p { color: #888; font-size: 0.9em; }
- .inventory-list {
- flex: 1;
- padding: 15px;
- overflow-y: auto;
- }
- .inventory-item {
- background: #f5f5f5;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
- padding: 15px;
- margin-bottom: 12px;
- transition: all 0.2s;
- }
- .inventory-item:hover {
- box-shadow: 0 3px 10px rgba(102, 126, 234, 0.2);
- transform: translateY(-2px);
- }
- .inventory-item-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- }
- .inventory-item-name {
- font-weight: 600;
- color: #333;
- font-size: 0.95em;
- }
- .inventory-item-quantity {
- background: #36454f;
- color: white;
- padding: 4px 10px;
- border-radius: 12px;
- font-size: 0.85em;
- font-weight: 600;
- }
- .inventory-item-quantity.low {
- background: #ff4757;
- }
- .inventory-item-details {
- color: #666;
- font-size: 0.85em;
- margin-bottom: 10px;
- }
- .find-price-btn {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border: none;
- padding: 10px 16px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- font-size: 0.9em;
- width: 100%;
- transition: transform 0.2s;
- }
- .find-price-btn:hover {
- transform: translateY(-2px);
- }
- .find-price-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .orders-bottom-panel {
- position: fixed;
- bottom: 0;
- left: 280px;
- right: 0;
- background: white;
- border-top: 2px solid #e0e0e0;
- box-shadow: 0 -3px 10px rgba(0,0,0,0.1);
- z-index: 1000;
- max-height: 160px;
- overflow-y: auto;
- }
- .orders-bottom-header {
- padding: 10px 20px;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- font-weight: 600;
- font-size: 0.95em;
- position: sticky;
- top: 0;
- z-index: 10;
- }
- .orders-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.85em;
- }
- .orders-table th {
- background: #f5f5f5;
- padding: 8px 12px;
- text-align: left;
- font-weight: 600;
- color: #36454f;
- border-bottom: 2px solid #e0e0e0;
- position: sticky;
- top: 35px;
- z-index: 5;
- }
- .orders-table td {
- padding: 8px 12px;
- border-bottom: 1px solid #f0f0f0;
- color: #333;
- }
- .orders-table tr:hover {
- background: #f5f5f5;
- cursor: pointer;
- }
- .order-status {
- padding: 3px 8px;
- border-radius: 10px;
- font-size: 0.8em;
- font-weight: 600;
- }
- .container {
- padding-bottom: 160px;
- padding-top: 80px;
- }
- .deals-banner {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- background: linear-gradient(135deg, #ff4757 0%, #d63031 100%);
- color: white;
- padding: 15px 30px;
- box-shadow: 0 3px 15px rgba(255,71,87,0.4);
- z-index: 2000;
- display: flex;
- align-items: center;
- gap: 20px;
- overflow-x: auto;
- white-space: nowrap;
- }
- .deals-banner-title {
- font-weight: 800;
- font-size: 1.1em;
- text-transform: uppercase;
- letter-spacing: 1px;
- flex-shrink: 0;
- animation: pulse 2s infinite;
- }
- @keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.8; }
- }
- .deal-item {
- display: inline-flex;
- align-items: center;
- gap: 10px;
- background: rgba(255,255,255,0.2);
- padding: 8px 15px;
- border-radius: 20px;
- font-size: 0.9em;
- flex-shrink: 0;
- cursor: pointer;
- transition: all 0.3s ease;
- border: 2px solid transparent;
- }
- .deal-item:hover {
- background: rgba(255,255,255,0.35);
- transform: translateY(-2px);
- border: 2px solid white;
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
- }
- .deal-item strong {
- font-weight: 700;
- }
- .deal-savings {
- background: white;
- color: #d63031;
- padding: 3px 10px;
- border-radius: 12px;
- font-weight: 700;
- font-size: 0.85em;
- }
- .refresh-deals-btn {
- background: rgba(255,255,255,0.25);
- color: white;
- border: 2px solid white;
- padding: 10px 20px;
- border-radius: 25px;
- font-weight: 700;
- font-size: 0.85em;
- cursor: pointer;
- transition: all 0.3s ease;
- flex-shrink: 0;
- margin-right: 15px;
- }
- .refresh-deals-btn:hover {
- background: white;
- color: #d63031;
- transform: scale(1.05);
- }
- .refresh-deals-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- </style>
-</head>
-<body>
- ${loadTopDeals()}
- <div class="container">
- <div class="sidebar">
- <div class="header">
- <h1>🛒 Purchasing Agent</h1>
- <div class="subtitle">Office Supplies</div>
- <div style="margin-top: 15px;">
- <a href="/logout" style="color: #36454f; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-card">
- <h3>Total Queries</h3>
- <div class="value" id="totalQueries">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Price Comparisons</h3>
- <div class="value" id="priceComparisons">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Orders Tracked</h3>
- <div class="value" id="ordersTracked">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Supplies Managed</h3>
- <div class="value" id="suppliesManaged">0</div>
- </div>
- </div>
-
- <div class="chat-container">
- <div class="chat-header">
- <h2>💬 Chat with Purchasing Agent</h2>
- <p>Ask about office supplies, pricing, orders, or inventory</p>
- </div>
-
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- <div class="message-avatar">🤖</div>
- <div class="message-content">
- Hi! I'm your Office Supplies Purchasing Agent. I can help you:
- <br><br>
- • Find and compare prices on office supplies<br>
- • Search Amazon Personal & Business accounts<br>
- • Track orders and shipments<br>
- • Manage inventory and reorder alerts<br>
- • Source the best deals<br>
- <br>
- <strong style="color: #28a745;">✅ REAL DATA MODE</strong> - No mock data!<br>
- Currently tracking <strong>${stats.ordersTracked}</strong> real orders and <strong>${stats.suppliesManaged}</strong> inventory items<br>
- <br>
- What can I help you with today?
- </div>
- </div>
- </div>
-
- <div class="typing-indicator" id="typingIndicator">
- Agent is typing...
- </div>
-
- <div class="chat-input-area">
- <div class="suggestions">
- <div class="suggestion-chip" onclick="sendSuggestion('Compare prices for printer paper')">🖨️ Printer paper prices</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Check inventory status')">📦 Check inventory</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Track recent orders')">🚚 Track orders</div>
- <div class="suggestion-chip" onclick="sendSuggestion('What office supplies are low in stock?')">⚠️ Low stock alerts</div>
- <div class="suggestion-chip" onclick="researchFurther()" style="background: linear-gradient(135deg, #36454f 0%, #708090 100%); color: white; font-weight: 600;">🔍 Research Further</div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- id="chatInput"
- placeholder="Ask about office supplies, pricing, or orders..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-btn" onclick="sendMessage()" id="sendBtn">Send</button>
- </div>
- </div>
- </div>
-
- <div class="inventory-panel">
- <div class="inventory-header">
- <h2>📦 Inventory Items</h2>
- <p>Click to find best prices</p>
- </div>
- <div class="inventory-list" id="inventoryList">
- ${stats.inventory.length > 0 ? stats.inventory.map((item, idx) => `
- <div class="inventory-item">
- <div class="inventory-item-header">
- <div class="inventory-item-name">${item.item}</div>
- <div class="inventory-item-quantity ${item.quantity < item.reorderLevel ? 'low' : ''}">${item.quantity} units</div>
- </div>
- <div class="inventory-item-details">
- Reorder Level: ${item.reorderLevel} units<br>
- Last Ordered: ${item.lastOrdered ? new Date(item.lastOrdered).toLocaleDateString() : 'Never'}
- </div>
- <button class="find-price-btn" onclick="findBestPrice('${item.item}', ${idx})">
- 🔍 FIND BEST PRICE
- </button>
- </div>
- `).join('') : `
- <div style="padding: 40px; text-align: center; color: #888;">
- <div style="font-size: 3em; margin-bottom: 20px;">📦</div>
- <h3 style="color: #36454f; margin-bottom: 10px;">No Inventory Yet</h3>
- <p style="margin-bottom: 20px;">Add inventory items to track office supplies</p>
- <p style="font-size: 0.9em; background: #fff3cd; padding: 15px; border-radius: 8px; color: #856404;">
- <strong>📝 NO MOCK DATA</strong><br>
- Edit: /root/DW-Agents/data/office-inventory.json
- </p>
- </div>
- `}
- </div>
- </div>
- </div>
-
- <div class="orders-bottom-panel">
- <div class="orders-bottom-header">
- 📦 Currently Tracking ${stats.ordersTracked} Real Orders
- </div>
- <table class="orders-table">
- <thead>
- <tr>
- <th>Date</th>
- <th>Product</th>
- <th>Price</th>
- <th>Vendor</th>
- <th>Status</th>
- <th>Order ID</th>
- </tr>
- </thead>
- <tbody>
- ${stats.recentOrders.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).map(order => `
- <tr onclick="sendSuggestion('Tell me about order ${order.id}')">
- <td>${new Date(order.timestamp).toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' })}</td>
- <td style="max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${order.item}</td>
- <td style="font-weight: 600; color: #28a745;">$${order.price.toFixed(2)}</td>
- <td>${order.vendor}</td>
- <td>
- <span class="order-status">${order.status === 'Delivered' ? '✅' : order.status === 'In Transit' ? '🚚' : '⏳'} ${order.status}</span>
- </td>
- <td style="font-family: monospace; font-size: 0.8em; color: #888;">${order.id}</td>
- </tr>
- `).join('')}
- </tbody>
- </table>
- </div>
-
- <script>
- function updateStats() {
- fetch('/api/stats')
- .then(r => r.json())
- .then(data => {
- document.getElementById('totalQueries').textContent = data.totalQueries;
- document.getElementById('priceComparisons').textContent = data.priceComparisons;
- document.getElementById('ordersTracked').textContent = data.ordersTracked;
- document.getElementById('suppliesManaged').textContent = data.suppliesManaged;
- });
- }
-
- async function refreshDeals() {
- const btn = document.getElementById('refreshBtn');
- const banner = document.getElementById('dealsBanner');
-
- btn.disabled = true;
- btn.innerHTML = '⏳ Refreshing prices...';
-
- try {
- const response = await fetch('/api/refresh-deals', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- const data = await response.json();
-
- if (data.success) {
- if (data.amazonBlocked) {
- // Amazon blocked scraping - show warning
- btn.innerHTML = '⚠️ Amazon Blocked';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- // Add message to chat
- const messagesDiv = document.getElementById('chatMessages');
- if (messagesDiv) {
- addMessage('assistant', '⚠️ Amazon is currently blocking automated price checks. Showing last cached prices. The "Find Best Price" buttons will search Amazon directly.');
- }
- }, 3000);
- } else if (data.dealsFound) {
- // Found new deals - reload page
- btn.innerHTML = '✅ New deals found! Reloading...';
- setTimeout(() => {
- location.reload();
- }, 1000);
- } else {
- // No new deals but refresh succeeded
- btn.innerHTML = '✅ Up to date - No new deals';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- }, 2000);
- }
- } else {
- btn.innerHTML = '❌ Failed - Try again';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- }, 2000);
- }
- } catch (error) {
- btn.innerHTML = '❌ Error - Try again';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- }, 2000);
- }
- }
-
- function addMessage(role, content) {
- const messagesDiv = document.getElementById('chatMessages');
- const messageDiv = document.createElement('div');
- messageDiv.className = \`message \${role}\`;
-
- const avatar = document.createElement('div');
- avatar.className = 'message-avatar';
- avatar.textContent = role === 'user' ? '👤' : '🤖';
-
- const contentDiv = document.createElement('div');
- contentDiv.className = 'message-content';
- contentDiv.innerHTML = content.replace(/\\n/g, '<br>');
-
- messageDiv.appendChild(avatar);
- messageDiv.appendChild(contentDiv);
- messagesDiv.appendChild(messageDiv);
-
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const sendBtn = document.getElementById('sendBtn');
- const typingIndicator = document.getElementById('typingIndicator');
- const message = input.value.trim();
-
- if (!message) return;
-
- // Add user message
- addMessage('user', message);
- input.value = '';
- sendBtn.disabled = true;
- typingIndicator.classList.add('active');
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
-
- typingIndicator.classList.remove('active');
- addMessage('assistant', data.response);
- updateStats();
-
- } catch (error) {
- typingIndicator.classList.remove('active');
- addMessage('assistant', 'Sorry, I encountered an error. Please try again.');
- }
-
- sendBtn.disabled = false;
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function researchFurther() {
- const messagesDiv = document.getElementById('chatMessages');
- const typingIndicator = document.getElementById('typingIndicator');
-
- addMessage('user', '🔍 Research further on recent topics and provide deeper insights');
- typingIndicator.classList.add('active');
-
- try {
- const response = await fetch('/api/research', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- const data = await response.json();
-
- typingIndicator.classList.remove('active');
- addMessage('assistant', data.research);
- updateStats();
- } catch (error) {
- typingIndicator.classList.remove('active');
- addMessage('assistant', 'Sorry, I encountered an error while researching. Please try again.');
- }
- }
-
- async function findBestPrice(itemName, itemIndex) {
- const typingIndicator = document.getElementById('typingIndicator');
- const button = event.target;
-
- button.disabled = true;
- button.textContent = '🔍 Searching...';
-
- addMessage('user', \`Find best price for: \${itemName}\`);
- typingIndicator.classList.add('active');
-
- try {
- const response = await fetch('/api/find-price', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ itemName, itemIndex })
- });
-
- const data = await response.json();
-
- typingIndicator.classList.remove('active');
-
- // Check for authentication error
- if (response.status === 401) {
- addMessage('assistant', \`🔐 Your session has expired. Please refresh the page to login again.\`);
- return;
- }
-
- if (data.priceInfo) {
- addMessage('assistant', data.priceInfo);
- } else if (data.error) {
- addMessage('assistant', \`❌ Error: \${data.error}\`);
- } else {
- addMessage('assistant', 'Unable to fetch prices. Please try again.');
- }
-
- updateStats();
- } catch (error) {
- typingIndicator.classList.remove('active');
- addMessage('assistant', \`❌ Network error: \${error.message}. Please refresh the page and try again.\`);
- console.error('Find price error:', error);
- } finally {
- button.disabled = false;
- button.textContent = '🔍 FIND BEST PRICE';
- }
- }
-
- // Update stats every 10 seconds
- setInterval(updateStats, 10000);
- updateStats();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.post('/api/chat', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- if (!message) {
- return res.status(400).json({ error: 'Message is required' });
- }
-
- // Memory system commands
- if (message.toLowerCase().match(/^(remember|note|preference|learning):/i)) {
- const parts = message.split(':');
- const type = parts[0].toLowerCase();
- const content = parts.slice(1).join(':').trim();
- const memType = type === 'remember' ? 'note' : type as any;
- agentMemory.add(content, memType);
- return res.json({
- response: `✅ Memory saved: "${content}"`,
- success: true,
- memoryAdded: true
- });
- }
-
- if (message.toLowerCase().includes('show') && message.toLowerCase().includes('memor')) {
- const summary = agentMemory.getContextSummary();
- return res.json({ response: summary, success: true });
- }
-
- if (message.toLowerCase().includes('search memor')) {
- const query = message.split(/for|about/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- return res.json({
- response: results.length > 0
- ? 'Found memories:\n' + results.map(m => `- ${m.content}`).join('\n')
- : 'No matching memories found.',
- success: true
- });
- }
-
- if (message.toLowerCase().includes('forget') || message.toLowerCase().includes('delete memor')) {
- const query = message.split(/forget|delete/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- if (results.length > 0) {
- agentMemory.delete(results[0].id);
- return res.json({ response: `✅ Forgot: "${results[0].content}"`, success: true });
- }
- return res.json({ response: 'Nothing found to forget.', success: true });
- }
-
- stats.totalQueries++;
- stats.lastQuery = { question: message, timestamp: new Date() };
-
- // Initialize chat history for session
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- // Add user message to history
- req.session.chatHistory.push({
- role: 'user',
- content: message
- });
-
- // Build system prompt with detailed data
- const systemPrompt = `You are the Purchasing (Office Supplies) Agent for Designer Wallcoverings.
-
-Your responsibilities:
-1. Help with office supply inquiries and price comparisons
-2. Search and compare prices across Amazon Personal and Amazon Business accounts
-3. Track orders and shipments with dates and tracking numbers
-4. Manage inventory levels and alert on low stock
-5. Source the best deals for office supplies
-
-Available integrations:
-- Amazon Personal Account (linked)
-- Amazon Business Account (linked)
-- Order tracking system
-- Inventory management database
-
-CURRENT INVENTORY (${stats.inventory.length} items):
-${stats.inventory.length > 0 ? stats.inventory.map(item => {
- const lastOrderedDate = item.lastOrdered ? new Date(item.lastOrdered).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'Never';
- const status = item.quantity < item.reorderLevel ? '⚠️ LOW STOCK' : '✅ OK';
- return `- ${item.item}
- • Current: ${item.quantity} units ${status}
- • Reorder at: ${item.reorderLevel} units
- • Last ordered: ${lastOrderedDate}`;
-}).join('\n') : '- No inventory tracked yet'}
-
-ALL ORDERS (${stats.recentOrders.length} total) - Sorted by date:
-${stats.recentOrders.length > 0 ? stats.recentOrders.slice().sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).map((order) => {
- const orderDate = new Date(order.timestamp).toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' });
- const status = order.status === 'Delivered' ? '✅' : order.status === 'In Transit' ? '🚚' : '⏳';
- return `${orderDate} | ${order.item} | $${order.price.toFixed(2)} | ${order.vendor} | ${status}`;
-}).join('\n') : '- No orders'}
-
-When user asks about:
-- "last order" or "recent orders": Show the most recent orders with ALL details (date, price, status, tracking)
-- "show dates": Display order dates and last ordered dates from inventory
-- "last price": Show the most recent price paid for that item from order history
-- "when did we order": Look through order history and show the date
-- "what's low": List items below reorder level with last ordered date
-- "track order": Show full order details including tracking number and status
-
-IMPORTANT FORMATTING:
-- Always show dates in format: "Oct 28, 2025"
-- Always show prices with dollar sign: "$42.99"
-- Always include order status with emoji
-- When showing orders, include: Date, Item, Price, Status, Tracking
-- When showing inventory, include: Quantity, Last Ordered Date, Reorder Level
-
-Be detailed, clear, and always include dates and prices when relevant.`;
-
- // Call Claude API
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 2048, // Increased for detailed responses with dates/prices
- system: systemPrompt,
- messages: req.session.chatHistory.map(msg => ({
- role: msg.role,
- content: msg.content
- }))
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error processing your request.';
-
- // Add assistant response to history
- req.session.chatHistory.push({
- role: 'assistant',
- content: assistantMessage
- });
-
- // Keep only last 20 messages
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- // Update stats based on message content
- if (message.toLowerCase().includes('price') || message.toLowerCase().includes('compare')) {
- stats.priceComparisons++;
- }
- if (message.toLowerCase().includes('order') || message.toLowerCase().includes('track')) {
- stats.ordersTracked++;
- }
-
- res.json({
- response: assistantMessage,
- stats: {
- totalQueries: stats.totalQueries,
- priceComparisons: stats.priceComparisons
- }
- });
-
- } catch (error: any) {
- console.error('❌ Chat error:', error.message || error);
- console.error('Error details:', error);
- res.status(500).json({
- error: 'Failed to process message',
- response: `Error: ${error.message || 'Unknown error'}. Please try again or check logs.`
- });
- }
-});
-
-// Research endpoint
-app.post('/api/research', requireAuth, async (req, res) => {
- try {
- stats.totalQueries++;
-
- // Build comprehensive research prompt
- const researchPrompt = `Provide comprehensive research and insights on office supply purchasing for Designer Wallcoverings.
-
-Focus on:
-1. Current market trends in office supplies
-2. Best suppliers and vendors for 2025
-3. Cost-saving strategies and bulk purchasing opportunities
-4. Emerging products and technologies
-5. Sustainable office supply options
-6. Amazon Business vs Personal account optimization
-7. Inventory management best practices
-
-Recent context:
-- Total queries: ${stats.totalQueries}
-- Orders tracked: ${stats.ordersTracked}
-- Recent orders: ${stats.recentOrders.slice(-3).map(o => o.item).join(', ') || 'None yet'}
-
-Provide actionable insights and specific recommendations.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 2048,
- messages: [{
- role: 'user',
- content: researchPrompt
- }]
- });
-
- const research = response.content[0].type === 'text'
- ? response.content[0].text
- : 'Unable to generate research at this time.';
-
- res.json({ research });
-
- } catch (error) {
- console.error('Research error:', error);
- res.status(500).json({
- research: 'I apologize, but I encountered an error while researching. Please try again.'
- });
- }
-});
-
-// Find best price endpoint - NOW WITH COMPETITOR SEARCH!
-app.post('/api/find-price', requireAuth, async (req, res) => {
- try {
- const { itemName, itemIndex } = req.body;
-
- if (!itemName) {
- return res.status(400).json({ error: 'Item name is required' });
- }
-
- stats.priceComparisons++;
- stats.totalQueries++;
-
- console.log(`🔍 Finding prices for: ${itemName}`);
- console.log(`📦 Checking cache first...`);
-
- // First check cached products
- const cachedProducts = searchCachedProducts(itemName);
- if (cachedProducts.length > 0) {
- console.log(`✅ Found ${cachedProducts.length} products in cache`);
- }
-
- // Search all competitors (includes Amazon + office supply stores)
- const searchResult = await searchCompetitors(itemName);
-
- // Cache all found products
- searchResult.prices.forEach(price => {
- if (price.price > 0) {
- // Extract ASIN from Amazon URLs if possible
- let asin = 'unknown';
- if (price.vendor === 'Amazon' && price.url.includes('/dp/')) {
- const match = price.url.match(/\/dp\/([A-Z0-9]{10})/);
- if (match) asin = match[1];
- }
-
- const cachedProduct: CachedProduct = {
- asin,
- title: price.product,
- description: '',
- currentPrice: price.price,
- imageUrl: '',
- productUrl: price.url,
- availability: price.availability,
- lastUpdated: new Date()
- };
- cacheProduct(cachedProduct);
- }
- });
-
- let priceInfo = '';
-
- if (!searchResult.success || searchResult.prices.length === 0) {
- // Show cached results if available
- if (cachedProducts.length > 0) {
- priceInfo = `📦 Amazon is down, showing CACHED results:\n\n`;
- cachedProducts.slice(0, 5).forEach((product, idx) => {
- priceInfo += `${idx + 1}. $${product.currentPrice.toFixed(2)} - ${product.title.substring(0, 80)}\n`;
- priceInfo += ` 🛒 ${product.productUrl}\n`;
- priceInfo += ` ⏰ Cached: ${new Date(product.lastUpdated).toLocaleString()}\n\n`;
- });
- } else {
- priceInfo = `❌ Unable to fetch prices right now. No cached data available.\n\n`;
- priceInfo += `Please try these direct search links:\n\n`;
- }
-
- priceInfo += `\n🔍 SEARCH ON COMPETITOR SITES:\n`;
- priceInfo += `🛒 Amazon: https://www.amazon.com/s?k=${encodeURIComponent(itemName)}&tag=designerwal0b-20\n`;
- priceInfo += `📎 Staples: https://www.staples.com/s?searchKey=${encodeURIComponent(itemName)}\n`;
- priceInfo += `🏢 Office Depot: https://www.officedepot.com/catalog/search.do?Ntt=${encodeURIComponent(itemName)}\n`;
- priceInfo += `🏪 Walmart: https://www.walmart.com/search?q=${encodeURIComponent(itemName)}\n`;
- priceInfo += `💼 Costco Business: https://www.costco.com/s?dept=All&keyword=${encodeURIComponent(itemName)}\n`;
-
- res.json({ priceInfo });
- return;
- }
-
- // Build response with all vendors
- if (searchResult.amazonBlocked) {
- priceInfo = `⚠️ AMAZON BLOCKED - SHOWING COMPETITOR PRICES\n\n`;
- } else {
- priceInfo = `🔍 LIVE PRICES FROM MULTIPLE VENDORS\n`;
- }
-
- priceInfo += `Search: ${itemName}\n`;
- priceInfo += `Found ${searchResult.prices.length} results across ${new Set(searchResult.prices.map(p => p.vendor)).size} vendors\n\n`;
-
- // Group by vendor
- const byVendor: { [key: string]: CompetitorPrice[] } = {};
- searchResult.prices.forEach(price => {
- if (!byVendor[price.vendor]) {
- byVendor[price.vendor] = [];
- }
- byVendor[price.vendor].push(price);
- });
-
- // Show results by vendor
- Object.entries(byVendor).forEach(([vendor, prices]) => {
- priceInfo += `\n━━━ ${vendor} ━━━\n`;
- prices.slice(0, 3).forEach((price, idx) => {
- if (price.price > 0) {
- priceInfo += `${idx + 1}. $${price.price.toFixed(2)} - ${price.product.substring(0, 70)}\n`;
- priceInfo += ` 🛒 ${price.url}\n`;
- } else {
- priceInfo += `🔗 Search results: ${price.url}\n`;
- }
- });
- });
-
- // Show best price
- const pricedItems = searchResult.prices.filter(p => p.price > 0);
- if (pricedItems.length > 0) {
- const bestPrice = pricedItems[0];
- priceInfo += `\n💰 BEST PRICE: $${bestPrice.price.toFixed(2)} at ${bestPrice.vendor}\n`;
- }
-
- // Show cache stats
- const cacheStats = getCacheStats();
- priceInfo += `\n📊 Cache: ${cacheStats.totalProducts} products, ${cacheStats.totalOrders} orders stored`;
-
- console.log(`✅ Found ${searchResult.prices.length} prices across competitors`);
-
- res.json({ priceInfo });
-
- } catch (error) {
- console.error('Price search error:', error);
- res.status(500).json({
- priceInfo: 'I apologize, but I encountered an error while searching for prices. Please try again.'
- });
- }
-});
-
-// Order management endpoints
-app.post('/api/orders/add', requireAuth, (req, res) => {
- const { item, vendor, price } = req.body;
-
- const order = {
- id: `ORD-${Date.now()}`,
- item,
- vendor,
- price: parseFloat(price),
- status: 'Pending',
- timestamp: new Date()
- };
-
- stats.recentOrders.push(order);
- stats.ordersTracked++;
-
- res.json({ success: true, order });
-});
-
-app.get('/api/orders', requireAuth, (req, res) => {
- res.json(stats.recentOrders);
-});
-
-// Inventory management
-app.post('/api/inventory/add', requireAuth, (req, res) => {
- const { item, quantity, reorderLevel } = req.body;
-
- const inventoryItem = {
- item,
- quantity: parseInt(quantity),
- reorderLevel: parseInt(reorderLevel),
- lastOrdered: null
- };
-
- stats.inventory.push(inventoryItem);
- stats.suppliesManaged++;
-
- res.json({ success: true, item: inventoryItem });
-});
-
-app.get('/api/inventory', requireAuth, (req, res) => {
- res.json(stats.inventory);
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('🛒 Purchasing (Office Supplies) Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('💬 Chat Mode: Active');
- console.log('🛍️ Amazon Personal: Ready');
- console.log('🏢 Amazon Business: Ready');
- console.log('📦 Order Tracking: Active');
- console.log('📊 Inventory Management: Active');
- console.log('');
- console.log('✅ Purchasing Agent ready for queries...');
-});
-
-// Refresh deals endpoint - triggers daily-price-tracker.ts
-app.post('/api/refresh-deals', requireAuth, async (req, res) => {
- try {
- console.log('🔄 Manual price refresh triggered...');
-
- const { spawn } = require('child_process');
- const trackerProcess = spawn('npx', ['tsx', '/root/DW-Agents/daily-price-tracker.ts'], {
- cwd: '/root/DW-Agents',
- stdio: 'pipe'
- });
-
- let output = '';
- let errorOutput = '';
- let responseSent = false;
-
- trackerProcess.stdout.on('data', (data: Buffer) => {
- output += data.toString();
- console.log(data.toString().trim());
- });
-
- trackerProcess.stderr.on('data', (data: Buffer) => {
- errorOutput += data.toString();
- console.error(data.toString().trim());
- });
-
- // Timeout after 2 minutes
- const timeout = setTimeout(() => {
- if (responseSent || res.headersSent) return;
- responseSent = true;
-
- trackerProcess.kill();
- res.json({
- success: false,
- message: 'Refresh timed out - too many items'
- });
- }, 120000);
-
- trackerProcess.on('close', (code: number) => {
- clearTimeout(timeout); // Clear timeout FIRST to prevent double response
- if (responseSent || res.headersSent) return;
- responseSent = true;
-
- if (code === 0) {
- console.log('✅ Price refresh completed successfully');
-
- // Check if Amazon blocked all requests
- const amazonBlocked = output.includes('HTTP 503') || output.includes('No prices tracked');
- const dealsFound = output.includes('PRICE DROPS');
-
- res.json({
- success: true,
- message: amazonBlocked ? 'Amazon blocked scraping - showing cached prices' : 'Prices refreshed successfully',
- dealsFound: dealsFound,
- amazonBlocked: amazonBlocked
- });
- } else {
- console.error('❌ Price refresh failed with code:', code);
- res.json({
- success: false,
- message: 'Failed to refresh prices',
- error: errorOutput
- });
- }
- });
-
- } catch (error: any) {
- console.error('❌ Refresh error:', error);
- res.status(500).json({
- success: false,
- message: error.message
- });
- }
-});
diff --git a/DW-Agents/dw-agents/purchasing-office-agent.ts.backup-pre-theme b/DW-Agents/dw-agents/purchasing-office-agent.ts.backup-pre-theme
deleted file mode 100644
index 51551aba..00000000
--- a/DW-Agents/dw-agents/purchasing-office-agent.ts.backup-pre-theme
+++ /dev/null
@@ -1,1539 +0,0 @@
-/**
- * DW-Agents: Purchasing (Office Supplies) Agent
- *
- * Manages office supply ordering with intelligent price comparison:
- * 1. Chat interface for supply requests and order tracking
- * 2. Amazon Personal & Business account integration
- * 3. Price comparison across vendors
- * 4. Order tracking and inventory management
- * 5. Automatic restock recommendations
- *
- * Port: 9880
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import session from 'express-session';
-import fetch from 'node-fetch';
-import * as fs from 'fs';
-import * as cheerio from 'cheerio';
-import { fetchRealAmazonOrders, fetchRealInventory, saveOrder, saveInventory } from './amazon-real-orders';
-import { AgentMemory } from './shared-memory-system';
-import { searchCompetitors, CompetitorPrice } from './office-competitors-search';
-import { cacheProduct, cacheOrder, getCachedProduct, searchCachedProducts, getCacheStats, CachedProduct } from './amazon-data-cache';
-
-const app = express();
-const PORT = 9880;
-// Initialize agent memory system
-const agentMemory = new AgentMemory('purchasing-office');
-console.log('📚 Memory system initialized for purchasing-office');
-
-
-// Authentication
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-// Amazon configuration
-const AMAZON_PERSONAL_EMAIL = 'nataliaabrams@gmail.com';
-const AMAZON_PERSONAL_PASSWORD = 'Urbanon341';
-const AMAZON_BUSINESS_EMAIL = 'info@designerwallcoverings.com';
-const AMAZON_BUSINESS_PASSWORD = '*Amazonaccess911*';
-
-// Anthropic Claude
-const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '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';
-const anthropic = new Anthropic({
- apiKey: ANTHROPIC_API_KEY,
-});
-
-// Stats tracking - LOAD REAL DATA
-let stats = {
- totalQueries: 0,
- priceComparisons: 0,
- ordersTracked: 0,
- suppliesManaged: 0,
- lastQuery: null as { question: string; timestamp: Date } | null,
- recentOrders: [] as Array<{
- id: string;
- item: string;
- vendor: string;
- price: number;
- status: string;
- timestamp: Date;
- }>,
- inventory: [] as Array<{
- item: string;
- quantity: number;
- reorderLevel: number;
- lastOrdered: Date | null;
- }>
-};
-
-// Load real data on startup
-async function loadRealData() {
- console.log('📦 Loading real Amazon data...');
-
- try {
- const orders = await fetchRealAmazonOrders();
- const inventory = await fetchRealInventory();
-
- stats.recentOrders = orders;
- stats.inventory = inventory;
- stats.ordersTracked = orders.length;
- stats.suppliesManaged = inventory.length;
-
- console.log(`✅ Loaded ${orders.length} orders and ${inventory.length} inventory items`);
-
- if (orders.length === 0) {
- console.log('\n⚠️ NO ORDERS FOUND - Add real data:');
- console.log(' 📝 Edit: /root/DW-Agents/data/amazon-orders.json');
- }
-
- if (inventory.length === 0) {
- console.log('\n⚠️ NO INVENTORY - Add items:');
- console.log(' 📝 Edit: /root/DW-Agents/data/office-inventory.json');
- }
-
- } catch (error) {
- console.error('❌ Error loading real data:', error);
- }
-}
-
-// Load data immediately
-loadRealData();
-
-// Chat history per session
-const chatSessions: Map<string, Array<{ role: 'user' | 'assistant', content: string }>> = new Map();
-
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-purchasing-office-agent-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-// Load top deals from price tracking
-function loadTopDeals(): string {
- try {
- const priceTrackingPath = '/root/DW-Agents/data/price-tracking.json';
- if (fs.existsSync(priceTrackingPath)) {
- const tracking = JSON.parse(fs.readFileSync(priceTrackingPath, 'utf-8'));
- const deals = tracking
- .filter((t: any) => t.priceChange < 0)
- .sort((a: any, b: any) => a.percentChange - b.percentChange)
- .slice(0, 4);
-
- if (deals.length === 0) {
- return '';
- }
-
- const lastChecked = deals[0]?.lastChecked ? new Date(deals[0].lastChecked) : new Date();
- const hoursAgo = Math.floor((Date.now() - lastChecked.getTime()) / (1000 * 60 * 60));
- const timeText = hoursAgo < 1 ? 'Just now' : hoursAgo === 1 ? '1 hour ago' : `${hoursAgo} hours ago`;
- const exactTime = lastChecked.toLocaleString('en-US', {
- month: 'short',
- day: 'numeric',
- hour: 'numeric',
- minute: '2-digit',
- hour12: true
- });
-
- const dealItems = deals.map((deal: any) => {
- const itemShort = deal.item.substring(0, 40) + (deal.item.length > 40 ? '...' : '');
- const savings = Math.abs(deal.priceChange).toFixed(2);
- const percent = Math.abs(deal.percentChange).toFixed(0);
- return `
- <a href="${deal.url}" target="_blank" class="deal-item" style="text-decoration: none; color: white;" title="Click to search on Amazon - Prices may vary">
- <strong>${itemShort}</strong>
- <span>Was $${deal.originalPrice.toFixed(2)} → $${deal.currentPrice.toFixed(2)}</span>
- <span class="deal-savings">SAVE $${savings} (${percent}% OFF)</span>
- </a>
- `;
- }).join('');
-
- return `
- <div class="deals-banner" id="dealsBanner">
- <button onclick="refreshDeals()" class="refresh-deals-btn" id="refreshBtn">
- 🔄 REFRESH NOW
- </button>
- <div style="display: flex; align-items: center; gap: 15px; flex-shrink: 0; padding: 0 15px; border-right: 2px solid rgba(255,255,255,0.3);">
- <div class="deals-banner-title">🔥 DEALS ALERT!</div>
- <div style="font-size: 0.75em; opacity: 0.9; white-space: nowrap;">
- ⏰ <strong>${exactTime}</strong><br>
- <span style="font-size: 0.9em;">(${timeText})</span>
- </div>
- </div>
- ${dealItems}
- </div>
- `;
- }
- } catch (e) {
- // Ignore if file doesn't exist yet
- }
- return '';
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // TEMPORARY: Auth disabled for 4 hours (expires ~3:30 AM)
- return next();
-
- /* Original auth code - will be re-enabled automatically
- if (req.session.authenticated) {
- next();
- } else {
- // For API requests, return JSON instead of redirecting
- if (req.path.startsWith('/api/')) {
- res.status(401).json({ error: 'Not authenticated. Please refresh the page and login.' });
- } else {
- res.redirect('/login');
- }
- }
- */
-};
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Purchasing Agent</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #36454f; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>🛒 Purchasing Agent</h1>
- <p style="text-align: center; color: #666; margin-bottom: 20px;">Office Supplies</p>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard with chat
-app.get('/', requireAuth, (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Purchasing (Office Supplies) - DW-Agents</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
- <meta http-equiv="Pragma" content="no-cache">
- <meta http-equiv="Expires" content="0">
- <!-- v2.1 - Deals Banner with Refresh -->
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .container {
- max-width: 1800px;
- margin: 0 auto;
- display: grid;
- grid-template-columns: 280px 1fr 400px;
- gap: 20px;
- }
- .sidebar {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #36454f; font-size: 2em; margin-bottom: 10px; }
- .header .subtitle { color: #666; font-size: 1.1em; }
- .stats-card {
- background: white;
- padding: 20px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stats-card h3 { color: #888; font-size: 0.85em; text-transform: uppercase; margin-bottom: 8px; }
- .stats-card .value { font-size: 2em; font-weight: bold; color: #36454f; }
- .chat-container {
- background: white;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- display: flex;
- flex-direction: column;
- height: calc(100vh - 40px);
- }
- .chat-header {
- padding: 20px;
- border-bottom: 2px solid #f0f0f0;
- }
- .chat-header h2 { color: #36454f; margin-bottom: 5px; }
- .chat-header p { color: #888; font-size: 0.9em; }
- .chat-messages {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- background: #f5f5f5;
- }
- .message {
- margin-bottom: 15px;
- display: flex;
- gap: 10px;
- }
- .message.user {
- flex-direction: row-reverse;
- }
- .message-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.2em;
- flex-shrink: 0;
- }
- .message.user .message-avatar {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- }
- .message.assistant .message-avatar {
- background: #e0e0e0;
- }
- .message-content {
- max-width: 70%;
- padding: 12px 16px;
- border-radius: 12px;
- line-height: 1.5;
- }
- .message.user .message-content {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message.assistant .message-content {
- background: white;
- color: #333;
- border-bottom-left-radius: 4px;
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
- }
- .chat-input-area {
- padding: 20px;
- border-top: 2px solid #f0f0f0;
- background: white;
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- #chatInput {
- flex: 1;
- padding: 12px 16px;
- border: 2px solid #e0e0e0;
- border-radius: 25px;
- font-size: 1em;
- outline: none;
- }
- #chatInput:focus {
- border-color: #36454f;
- }
- .send-btn {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border: none;
- padding: 12px 30px;
- border-radius: 25px;
- cursor: pointer;
- font-weight: 600;
- transition: transform 0.2s;
- }
- .send-btn:hover {
- transform: translateY(-2px);
- }
- .send-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .suggestions {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- margin-top: 10px;
- }
- .suggestion-chip {
- background: #f0f0f0;
- padding: 8px 15px;
- border-radius: 20px;
- font-size: 0.85em;
- cursor: pointer;
- transition: all 0.2s;
- }
- .suggestion-chip:hover {
- background: #36454f;
- color: white;
- }
- .typing-indicator {
- display: none;
- padding: 10px;
- color: #888;
- font-style: italic;
- }
- .typing-indicator.active {
- display: block;
- }
- .price-comparison {
- background: #f5f5f5;
- border: 1px solid #e0e0e0;
- border-radius: 8px;
- padding: 10px;
- margin-top: 10px;
- }
- .price-item {
- display: flex;
- justify-content: space-between;
- padding: 8px 0;
- border-bottom: 1px solid #e0e0e0;
- }
- .price-item:last-child {
- border-bottom: none;
- }
- .vendor-name {
- font-weight: 600;
- color: #36454f;
- }
- .price {
- font-weight: bold;
- color: #28a745;
- }
- .inventory-panel {
- background: white;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- display: flex;
- flex-direction: column;
- height: calc(100vh - 40px);
- overflow: hidden;
- }
- .inventory-header {
- padding: 20px;
- border-bottom: 2px solid #f0f0f0;
- }
- .inventory-header h2 { color: #36454f; margin-bottom: 5px; }
- .inventory-header p { color: #888; font-size: 0.9em; }
- .inventory-list {
- flex: 1;
- padding: 15px;
- overflow-y: auto;
- }
- .inventory-item {
- background: #f5f5f5;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
- padding: 15px;
- margin-bottom: 12px;
- transition: all 0.2s;
- }
- .inventory-item:hover {
- box-shadow: 0 3px 10px rgba(102, 126, 234, 0.2);
- transform: translateY(-2px);
- }
- .inventory-item-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- }
- .inventory-item-name {
- font-weight: 600;
- color: #333;
- font-size: 0.95em;
- }
- .inventory-item-quantity {
- background: #36454f;
- color: white;
- padding: 4px 10px;
- border-radius: 12px;
- font-size: 0.85em;
- font-weight: 600;
- }
- .inventory-item-quantity.low {
- background: #ff4757;
- }
- .inventory-item-details {
- color: #666;
- font-size: 0.85em;
- margin-bottom: 10px;
- }
- .find-price-btn {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border: none;
- padding: 10px 16px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- font-size: 0.9em;
- width: 100%;
- transition: transform 0.2s;
- }
- .find-price-btn:hover {
- transform: translateY(-2px);
- }
- .find-price-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .orders-bottom-panel {
- position: fixed;
- bottom: 0;
- left: 280px;
- right: 0;
- background: white;
- border-top: 2px solid #e0e0e0;
- box-shadow: 0 -3px 10px rgba(0,0,0,0.1);
- z-index: 1000;
- max-height: 160px;
- overflow-y: auto;
- }
- .orders-bottom-header {
- padding: 10px 20px;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- font-weight: 600;
- font-size: 0.95em;
- position: sticky;
- top: 0;
- z-index: 10;
- }
- .orders-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.85em;
- }
- .orders-table th {
- background: #f5f5f5;
- padding: 8px 12px;
- text-align: left;
- font-weight: 600;
- color: #36454f;
- border-bottom: 2px solid #e0e0e0;
- position: sticky;
- top: 35px;
- z-index: 5;
- }
- .orders-table td {
- padding: 8px 12px;
- border-bottom: 1px solid #f0f0f0;
- color: #333;
- }
- .orders-table tr:hover {
- background: #f5f5f5;
- cursor: pointer;
- }
- .order-status {
- padding: 3px 8px;
- border-radius: 10px;
- font-size: 0.8em;
- font-weight: 600;
- }
- .container {
- padding-bottom: 160px;
- padding-top: 80px;
- }
- .deals-banner {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- background: linear-gradient(135deg, #ff4757 0%, #d63031 100%);
- color: white;
- padding: 15px 30px;
- box-shadow: 0 3px 15px rgba(255,71,87,0.4);
- z-index: 2000;
- display: flex;
- align-items: center;
- gap: 20px;
- overflow-x: auto;
- white-space: nowrap;
- }
- .deals-banner-title {
- font-weight: 800;
- font-size: 1.1em;
- text-transform: uppercase;
- letter-spacing: 1px;
- flex-shrink: 0;
- animation: pulse 2s infinite;
- }
- @keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.8; }
- }
- .deal-item {
- display: inline-flex;
- align-items: center;
- gap: 10px;
- background: rgba(255,255,255,0.2);
- padding: 8px 15px;
- border-radius: 20px;
- font-size: 0.9em;
- flex-shrink: 0;
- cursor: pointer;
- transition: all 0.3s ease;
- border: 2px solid transparent;
- }
- .deal-item:hover {
- background: rgba(255,255,255,0.35);
- transform: translateY(-2px);
- border: 2px solid white;
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
- }
- .deal-item strong {
- font-weight: 700;
- }
- .deal-savings {
- background: white;
- color: #d63031;
- padding: 3px 10px;
- border-radius: 12px;
- font-weight: 700;
- font-size: 0.85em;
- }
- .refresh-deals-btn {
- background: rgba(255,255,255,0.25);
- color: white;
- border: 2px solid white;
- padding: 10px 20px;
- border-radius: 25px;
- font-weight: 700;
- font-size: 0.85em;
- cursor: pointer;
- transition: all 0.3s ease;
- flex-shrink: 0;
- margin-right: 15px;
- }
- .refresh-deals-btn:hover {
- background: white;
- color: #d63031;
- transform: scale(1.05);
- }
- .refresh-deals-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- </style>
-</head>
-<body>
- ${loadTopDeals()}
- <div class="container">
- <div class="sidebar">
- <div class="header">
- <h1>🛒 Purchasing Agent</h1>
- <div class="subtitle">Office Supplies</div>
- <div style="margin-top: 15px;">
- <a href="/logout" style="color: #36454f; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-card">
- <h3>Total Queries</h3>
- <div class="value" id="totalQueries">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Price Comparisons</h3>
- <div class="value" id="priceComparisons">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Orders Tracked</h3>
- <div class="value" id="ordersTracked">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Supplies Managed</h3>
- <div class="value" id="suppliesManaged">0</div>
- </div>
- </div>
-
- <div class="chat-container">
- <div class="chat-header">
- <h2>💬 Chat with Purchasing Agent</h2>
- <p>Ask about office supplies, pricing, orders, or inventory</p>
- </div>
-
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- <div class="message-avatar">🤖</div>
- <div class="message-content">
- Hi! I'm your Office Supplies Purchasing Agent. I can help you:
- <br><br>
- • Find and compare prices on office supplies<br>
- • Search Amazon Personal & Business accounts<br>
- • Track orders and shipments<br>
- • Manage inventory and reorder alerts<br>
- • Source the best deals<br>
- <br>
- <strong style="color: #28a745;">✅ REAL DATA MODE</strong> - No mock data!<br>
- Currently tracking <strong>${stats.ordersTracked}</strong> real orders and <strong>${stats.suppliesManaged}</strong> inventory items<br>
- <br>
- What can I help you with today?
- </div>
- </div>
- </div>
-
- <div class="typing-indicator" id="typingIndicator">
- Agent is typing...
- </div>
-
- <div class="chat-input-area">
- <div class="suggestions">
- <div class="suggestion-chip" onclick="sendSuggestion('Compare prices for printer paper')">🖨️ Printer paper prices</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Check inventory status')">📦 Check inventory</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Track recent orders')">🚚 Track orders</div>
- <div class="suggestion-chip" onclick="sendSuggestion('What office supplies are low in stock?')">⚠️ Low stock alerts</div>
- <div class="suggestion-chip" onclick="researchFurther()" style="background: linear-gradient(135deg, #36454f 0%, #708090 100%); color: white; font-weight: 600;">🔍 Research Further</div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- id="chatInput"
- placeholder="Ask about office supplies, pricing, or orders..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-btn" onclick="sendMessage()" id="sendBtn">Send</button>
- </div>
- </div>
- </div>
-
- <div class="inventory-panel">
- <div class="inventory-header">
- <h2>📦 Inventory Items</h2>
- <p>Click to find best prices</p>
- </div>
- <div class="inventory-list" id="inventoryList">
- ${stats.inventory.length > 0 ? stats.inventory.map((item, idx) => `
- <div class="inventory-item">
- <div class="inventory-item-header">
- <div class="inventory-item-name">${item.item}</div>
- <div class="inventory-item-quantity ${item.quantity < item.reorderLevel ? 'low' : ''}">${item.quantity} units</div>
- </div>
- <div class="inventory-item-details">
- Reorder Level: ${item.reorderLevel} units<br>
- Last Ordered: ${item.lastOrdered ? new Date(item.lastOrdered).toLocaleDateString() : 'Never'}
- </div>
- <button class="find-price-btn" onclick="findBestPrice('${item.item}', ${idx})">
- 🔍 FIND BEST PRICE
- </button>
- </div>
- `).join('') : `
- <div style="padding: 40px; text-align: center; color: #888;">
- <div style="font-size: 3em; margin-bottom: 20px;">📦</div>
- <h3 style="color: #36454f; margin-bottom: 10px;">No Inventory Yet</h3>
- <p style="margin-bottom: 20px;">Add inventory items to track office supplies</p>
- <p style="font-size: 0.9em; background: #fff3cd; padding: 15px; border-radius: 8px; color: #856404;">
- <strong>📝 NO MOCK DATA</strong><br>
- Edit: /root/DW-Agents/data/office-inventory.json
- </p>
- </div>
- `}
- </div>
- </div>
- </div>
-
- <div class="orders-bottom-panel">
- <div class="orders-bottom-header">
- 📦 Currently Tracking ${stats.ordersTracked} Real Orders
- </div>
- <table class="orders-table">
- <thead>
- <tr>
- <th>Date</th>
- <th>Product</th>
- <th>Price</th>
- <th>Vendor</th>
- <th>Status</th>
- <th>Order ID</th>
- </tr>
- </thead>
- <tbody>
- ${stats.recentOrders.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).map(order => `
- <tr onclick="sendSuggestion('Tell me about order ${order.id}')">
- <td>${new Date(order.timestamp).toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' })}</td>
- <td style="max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${order.item}</td>
- <td style="font-weight: 600; color: #28a745;">$${order.price.toFixed(2)}</td>
- <td>${order.vendor}</td>
- <td>
- <span class="order-status">${order.status === 'Delivered' ? '✅' : order.status === 'In Transit' ? '🚚' : '⏳'} ${order.status}</span>
- </td>
- <td style="font-family: monospace; font-size: 0.8em; color: #888;">${order.id}</td>
- </tr>
- `).join('')}
- </tbody>
- </table>
- </div>
-
- <script>
- function updateStats() {
- fetch('/api/stats')
- .then(r => r.json())
- .then(data => {
- document.getElementById('totalQueries').textContent = data.totalQueries;
- document.getElementById('priceComparisons').textContent = data.priceComparisons;
- document.getElementById('ordersTracked').textContent = data.ordersTracked;
- document.getElementById('suppliesManaged').textContent = data.suppliesManaged;
- });
- }
-
- async function refreshDeals() {
- const btn = document.getElementById('refreshBtn');
- const banner = document.getElementById('dealsBanner');
-
- btn.disabled = true;
- btn.innerHTML = '⏳ Refreshing prices...';
-
- try {
- const response = await fetch('/api/refresh-deals', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- const data = await response.json();
-
- if (data.success) {
- if (data.amazonBlocked) {
- // Amazon blocked scraping - show warning
- btn.innerHTML = '⚠️ Amazon Blocked';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- // Add message to chat
- const messagesDiv = document.getElementById('chatMessages');
- if (messagesDiv) {
- addMessage('assistant', '⚠️ Amazon is currently blocking automated price checks. Showing last cached prices. The "Find Best Price" buttons will search Amazon directly.');
- }
- }, 3000);
- } else if (data.dealsFound) {
- // Found new deals - reload page
- btn.innerHTML = '✅ New deals found! Reloading...';
- setTimeout(() => {
- location.reload();
- }, 1000);
- } else {
- // No new deals but refresh succeeded
- btn.innerHTML = '✅ Up to date - No new deals';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- }, 2000);
- }
- } else {
- btn.innerHTML = '❌ Failed - Try again';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- }, 2000);
- }
- } catch (error) {
- btn.innerHTML = '❌ Error - Try again';
- setTimeout(() => {
- btn.innerHTML = '🔄 REFRESH NOW';
- btn.disabled = false;
- }, 2000);
- }
- }
-
- function addMessage(role, content) {
- const messagesDiv = document.getElementById('chatMessages');
- const messageDiv = document.createElement('div');
- messageDiv.className = \`message \${role}\`;
-
- const avatar = document.createElement('div');
- avatar.className = 'message-avatar';
- avatar.textContent = role === 'user' ? '👤' : '🤖';
-
- const contentDiv = document.createElement('div');
- contentDiv.className = 'message-content';
- contentDiv.innerHTML = content.replace(/\\n/g, '<br>');
-
- messageDiv.appendChild(avatar);
- messageDiv.appendChild(contentDiv);
- messagesDiv.appendChild(messageDiv);
-
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const sendBtn = document.getElementById('sendBtn');
- const typingIndicator = document.getElementById('typingIndicator');
- const message = input.value.trim();
-
- if (!message) return;
-
- // Add user message
- addMessage('user', message);
- input.value = '';
- sendBtn.disabled = true;
- typingIndicator.classList.add('active');
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
-
- typingIndicator.classList.remove('active');
- addMessage('assistant', data.response);
- updateStats();
-
- } catch (error) {
- typingIndicator.classList.remove('active');
- addMessage('assistant', 'Sorry, I encountered an error. Please try again.');
- }
-
- sendBtn.disabled = false;
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function researchFurther() {
- const messagesDiv = document.getElementById('chatMessages');
- const typingIndicator = document.getElementById('typingIndicator');
-
- addMessage('user', '🔍 Research further on recent topics and provide deeper insights');
- typingIndicator.classList.add('active');
-
- try {
- const response = await fetch('/api/research', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- const data = await response.json();
-
- typingIndicator.classList.remove('active');
- addMessage('assistant', data.research);
- updateStats();
- } catch (error) {
- typingIndicator.classList.remove('active');
- addMessage('assistant', 'Sorry, I encountered an error while researching. Please try again.');
- }
- }
-
- async function findBestPrice(itemName, itemIndex) {
- const typingIndicator = document.getElementById('typingIndicator');
- const button = event.target;
-
- button.disabled = true;
- button.textContent = '🔍 Searching...';
-
- addMessage('user', \`Find best price for: \${itemName}\`);
- typingIndicator.classList.add('active');
-
- try {
- const response = await fetch('/api/find-price', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ itemName, itemIndex })
- });
-
- const data = await response.json();
-
- typingIndicator.classList.remove('active');
-
- // Check for authentication error
- if (response.status === 401) {
- addMessage('assistant', \`🔐 Your session has expired. Please refresh the page to login again.\`);
- return;
- }
-
- if (data.priceInfo) {
- addMessage('assistant', data.priceInfo);
- } else if (data.error) {
- addMessage('assistant', \`❌ Error: \${data.error}\`);
- } else {
- addMessage('assistant', 'Unable to fetch prices. Please try again.');
- }
-
- updateStats();
- } catch (error) {
- typingIndicator.classList.remove('active');
- addMessage('assistant', \`❌ Network error: \${error.message}. Please refresh the page and try again.\`);
- console.error('Find price error:', error);
- } finally {
- button.disabled = false;
- button.textContent = '🔍 FIND BEST PRICE';
- }
- }
-
- // Update stats every 10 seconds
- setInterval(updateStats, 10000);
- updateStats();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.post('/api/chat', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- if (!message) {
- return res.status(400).json({ error: 'Message is required' });
- }
-
- // Memory system commands
- if (message.toLowerCase().match(/^(remember|note|preference|learning):/i)) {
- const parts = message.split(':');
- const type = parts[0].toLowerCase();
- const content = parts.slice(1).join(':').trim();
- const memType = type === 'remember' ? 'note' : type as any;
- agentMemory.add(content, memType);
- return res.json({
- response: `✅ Memory saved: "${content}"`,
- success: true,
- memoryAdded: true
- });
- }
-
- if (message.toLowerCase().includes('show') && message.toLowerCase().includes('memor')) {
- const summary = agentMemory.getContextSummary();
- return res.json({ response: summary, success: true });
- }
-
- if (message.toLowerCase().includes('search memor')) {
- const query = message.split(/for|about/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- return res.json({
- response: results.length > 0
- ? 'Found memories:\n' + results.map(m => `- ${m.content}`).join('\n')
- : 'No matching memories found.',
- success: true
- });
- }
-
- if (message.toLowerCase().includes('forget') || message.toLowerCase().includes('delete memor')) {
- const query = message.split(/forget|delete/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- if (results.length > 0) {
- agentMemory.delete(results[0].id);
- return res.json({ response: `✅ Forgot: "${results[0].content}"`, success: true });
- }
- return res.json({ response: 'Nothing found to forget.', success: true });
- }
-
- stats.totalQueries++;
- stats.lastQuery = { question: message, timestamp: new Date() };
-
- // Initialize chat history for session
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- // Add user message to history
- req.session.chatHistory.push({
- role: 'user',
- content: message
- });
-
- // Build system prompt with detailed data
- const systemPrompt = `You are the Purchasing (Office Supplies) Agent for Designer Wallcoverings.
-
-Your responsibilities:
-1. Help with office supply inquiries and price comparisons
-2. Search and compare prices across Amazon Personal and Amazon Business accounts
-3. Track orders and shipments with dates and tracking numbers
-4. Manage inventory levels and alert on low stock
-5. Source the best deals for office supplies
-
-Available integrations:
-- Amazon Personal Account (linked)
-- Amazon Business Account (linked)
-- Order tracking system
-- Inventory management database
-
-CURRENT INVENTORY (${stats.inventory.length} items):
-${stats.inventory.length > 0 ? stats.inventory.map(item => {
- const lastOrderedDate = item.lastOrdered ? new Date(item.lastOrdered).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'Never';
- const status = item.quantity < item.reorderLevel ? '⚠️ LOW STOCK' : '✅ OK';
- return `- ${item.item}
- • Current: ${item.quantity} units ${status}
- • Reorder at: ${item.reorderLevel} units
- • Last ordered: ${lastOrderedDate}`;
-}).join('\n') : '- No inventory tracked yet'}
-
-ALL ORDERS (${stats.recentOrders.length} total) - Sorted by date:
-${stats.recentOrders.length > 0 ? stats.recentOrders.slice().sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).map((order) => {
- const orderDate = new Date(order.timestamp).toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' });
- const status = order.status === 'Delivered' ? '✅' : order.status === 'In Transit' ? '🚚' : '⏳';
- return `${orderDate} | ${order.item} | $${order.price.toFixed(2)} | ${order.vendor} | ${status}`;
-}).join('\n') : '- No orders'}
-
-When user asks about:
-- "last order" or "recent orders": Show the most recent orders with ALL details (date, price, status, tracking)
-- "show dates": Display order dates and last ordered dates from inventory
-- "last price": Show the most recent price paid for that item from order history
-- "when did we order": Look through order history and show the date
-- "what's low": List items below reorder level with last ordered date
-- "track order": Show full order details including tracking number and status
-
-IMPORTANT FORMATTING:
-- Always show dates in format: "Oct 28, 2025"
-- Always show prices with dollar sign: "$42.99"
-- Always include order status with emoji
-- When showing orders, include: Date, Item, Price, Status, Tracking
-- When showing inventory, include: Quantity, Last Ordered Date, Reorder Level
-
-Be detailed, clear, and always include dates and prices when relevant.`;
-
- // Call Claude API
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 2048, // Increased for detailed responses with dates/prices
- system: systemPrompt,
- messages: req.session.chatHistory.map(msg => ({
- role: msg.role,
- content: msg.content
- }))
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error processing your request.';
-
- // Add assistant response to history
- req.session.chatHistory.push({
- role: 'assistant',
- content: assistantMessage
- });
-
- // Keep only last 20 messages
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- // Update stats based on message content
- if (message.toLowerCase().includes('price') || message.toLowerCase().includes('compare')) {
- stats.priceComparisons++;
- }
- if (message.toLowerCase().includes('order') || message.toLowerCase().includes('track')) {
- stats.ordersTracked++;
- }
-
- res.json({
- response: assistantMessage,
- stats: {
- totalQueries: stats.totalQueries,
- priceComparisons: stats.priceComparisons
- }
- });
-
- } catch (error: any) {
- console.error('❌ Chat error:', error.message || error);
- console.error('Error details:', error);
- res.status(500).json({
- error: 'Failed to process message',
- response: `Error: ${error.message || 'Unknown error'}. Please try again or check logs.`
- });
- }
-});
-
-// Research endpoint
-app.post('/api/research', requireAuth, async (req, res) => {
- try {
- stats.totalQueries++;
-
- // Build comprehensive research prompt
- const researchPrompt = `Provide comprehensive research and insights on office supply purchasing for Designer Wallcoverings.
-
-Focus on:
-1. Current market trends in office supplies
-2. Best suppliers and vendors for 2025
-3. Cost-saving strategies and bulk purchasing opportunities
-4. Emerging products and technologies
-5. Sustainable office supply options
-6. Amazon Business vs Personal account optimization
-7. Inventory management best practices
-
-Recent context:
-- Total queries: ${stats.totalQueries}
-- Orders tracked: ${stats.ordersTracked}
-- Recent orders: ${stats.recentOrders.slice(-3).map(o => o.item).join(', ') || 'None yet'}
-
-Provide actionable insights and specific recommendations.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 2048,
- messages: [{
- role: 'user',
- content: researchPrompt
- }]
- });
-
- const research = response.content[0].type === 'text'
- ? response.content[0].text
- : 'Unable to generate research at this time.';
-
- res.json({ research });
-
- } catch (error) {
- console.error('Research error:', error);
- res.status(500).json({
- research: 'I apologize, but I encountered an error while researching. Please try again.'
- });
- }
-});
-
-// Find best price endpoint - NOW WITH COMPETITOR SEARCH!
-app.post('/api/find-price', requireAuth, async (req, res) => {
- try {
- const { itemName, itemIndex } = req.body;
-
- if (!itemName) {
- return res.status(400).json({ error: 'Item name is required' });
- }
-
- stats.priceComparisons++;
- stats.totalQueries++;
-
- console.log(`🔍 Finding prices for: ${itemName}`);
- console.log(`📦 Checking cache first...`);
-
- // First check cached products
- const cachedProducts = searchCachedProducts(itemName);
- if (cachedProducts.length > 0) {
- console.log(`✅ Found ${cachedProducts.length} products in cache`);
- }
-
- // Search all competitors (includes Amazon + office supply stores)
- const searchResult = await searchCompetitors(itemName);
-
- // Cache all found products
- searchResult.prices.forEach(price => {
- if (price.price > 0) {
- // Extract ASIN from Amazon URLs if possible
- let asin = 'unknown';
- if (price.vendor === 'Amazon' && price.url.includes('/dp/')) {
- const match = price.url.match(/\/dp\/([A-Z0-9]{10})/);
- if (match) asin = match[1];
- }
-
- const cachedProduct: CachedProduct = {
- asin,
- title: price.product,
- description: '',
- currentPrice: price.price,
- imageUrl: '',
- productUrl: price.url,
- availability: price.availability,
- lastUpdated: new Date()
- };
- cacheProduct(cachedProduct);
- }
- });
-
- let priceInfo = '';
-
- if (!searchResult.success || searchResult.prices.length === 0) {
- // Show cached results if available
- if (cachedProducts.length > 0) {
- priceInfo = `📦 Amazon is down, showing CACHED results:\n\n`;
- cachedProducts.slice(0, 5).forEach((product, idx) => {
- priceInfo += `${idx + 1}. $${product.currentPrice.toFixed(2)} - ${product.title.substring(0, 80)}\n`;
- priceInfo += ` 🛒 ${product.productUrl}\n`;
- priceInfo += ` ⏰ Cached: ${new Date(product.lastUpdated).toLocaleString()}\n\n`;
- });
- } else {
- priceInfo = `❌ Unable to fetch prices right now. No cached data available.\n\n`;
- priceInfo += `Please try these direct search links:\n\n`;
- }
-
- priceInfo += `\n🔍 SEARCH ON COMPETITOR SITES:\n`;
- priceInfo += `🛒 Amazon: https://www.amazon.com/s?k=${encodeURIComponent(itemName)}&tag=designerwal0b-20\n`;
- priceInfo += `📎 Staples: https://www.staples.com/s?searchKey=${encodeURIComponent(itemName)}\n`;
- priceInfo += `🏢 Office Depot: https://www.officedepot.com/catalog/search.do?Ntt=${encodeURIComponent(itemName)}\n`;
- priceInfo += `🏪 Walmart: https://www.walmart.com/search?q=${encodeURIComponent(itemName)}\n`;
- priceInfo += `💼 Costco Business: https://www.costco.com/s?dept=All&keyword=${encodeURIComponent(itemName)}\n`;
-
- res.json({ priceInfo });
- return;
- }
-
- // Build response with all vendors
- if (searchResult.amazonBlocked) {
- priceInfo = `⚠️ AMAZON BLOCKED - SHOWING COMPETITOR PRICES\n\n`;
- } else {
- priceInfo = `🔍 LIVE PRICES FROM MULTIPLE VENDORS\n`;
- }
-
- priceInfo += `Search: ${itemName}\n`;
- priceInfo += `Found ${searchResult.prices.length} results across ${new Set(searchResult.prices.map(p => p.vendor)).size} vendors\n\n`;
-
- // Group by vendor
- const byVendor: { [key: string]: CompetitorPrice[] } = {};
- searchResult.prices.forEach(price => {
- if (!byVendor[price.vendor]) {
- byVendor[price.vendor] = [];
- }
- byVendor[price.vendor].push(price);
- });
-
- // Show results by vendor
- Object.entries(byVendor).forEach(([vendor, prices]) => {
- priceInfo += `\n━━━ ${vendor} ━━━\n`;
- prices.slice(0, 3).forEach((price, idx) => {
- if (price.price > 0) {
- priceInfo += `${idx + 1}. $${price.price.toFixed(2)} - ${price.product.substring(0, 70)}\n`;
- priceInfo += ` 🛒 ${price.url}\n`;
- } else {
- priceInfo += `🔗 Search results: ${price.url}\n`;
- }
- });
- });
-
- // Show best price
- const pricedItems = searchResult.prices.filter(p => p.price > 0);
- if (pricedItems.length > 0) {
- const bestPrice = pricedItems[0];
- priceInfo += `\n💰 BEST PRICE: $${bestPrice.price.toFixed(2)} at ${bestPrice.vendor}\n`;
- }
-
- // Show cache stats
- const cacheStats = getCacheStats();
- priceInfo += `\n📊 Cache: ${cacheStats.totalProducts} products, ${cacheStats.totalOrders} orders stored`;
-
- console.log(`✅ Found ${searchResult.prices.length} prices across competitors`);
-
- res.json({ priceInfo });
-
- } catch (error) {
- console.error('Price search error:', error);
- res.status(500).json({
- priceInfo: 'I apologize, but I encountered an error while searching for prices. Please try again.'
- });
- }
-});
-
-// Order management endpoints
-app.post('/api/orders/add', requireAuth, (req, res) => {
- const { item, vendor, price } = req.body;
-
- const order = {
- id: `ORD-${Date.now()}`,
- item,
- vendor,
- price: parseFloat(price),
- status: 'Pending',
- timestamp: new Date()
- };
-
- stats.recentOrders.push(order);
- stats.ordersTracked++;
-
- res.json({ success: true, order });
-});
-
-app.get('/api/orders', requireAuth, (req, res) => {
- res.json(stats.recentOrders);
-});
-
-// Inventory management
-app.post('/api/inventory/add', requireAuth, (req, res) => {
- const { item, quantity, reorderLevel } = req.body;
-
- const inventoryItem = {
- item,
- quantity: parseInt(quantity),
- reorderLevel: parseInt(reorderLevel),
- lastOrdered: null
- };
-
- stats.inventory.push(inventoryItem);
- stats.suppliesManaged++;
-
- res.json({ success: true, item: inventoryItem });
-});
-
-app.get('/api/inventory', requireAuth, (req, res) => {
- res.json(stats.inventory);
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('🛒 Purchasing (Office Supplies) Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('💬 Chat Mode: Active');
- console.log('🛍️ Amazon Personal: Ready');
- console.log('🏢 Amazon Business: Ready');
- console.log('📦 Order Tracking: Active');
- console.log('📊 Inventory Management: Active');
- console.log('');
- console.log('✅ Purchasing Agent ready for queries...');
-});
-
-// Refresh deals endpoint - triggers daily-price-tracker.ts
-app.post('/api/refresh-deals', requireAuth, async (req, res) => {
- try {
- console.log('🔄 Manual price refresh triggered...');
-
- const { spawn } = require('child_process');
- const trackerProcess = spawn('npx', ['tsx', '/root/DW-Agents/daily-price-tracker.ts'], {
- cwd: '/root/DW-Agents',
- stdio: 'pipe'
- });
-
- let output = '';
- let errorOutput = '';
- let responseSent = false;
-
- trackerProcess.stdout.on('data', (data: Buffer) => {
- output += data.toString();
- console.log(data.toString().trim());
- });
-
- trackerProcess.stderr.on('data', (data: Buffer) => {
- errorOutput += data.toString();
- console.error(data.toString().trim());
- });
-
- // Timeout after 2 minutes
- const timeout = setTimeout(() => {
- if (responseSent || res.headersSent) return;
- responseSent = true;
-
- trackerProcess.kill();
- res.json({
- success: false,
- message: 'Refresh timed out - too many items'
- });
- }, 120000);
-
- trackerProcess.on('close', (code: number) => {
- clearTimeout(timeout); // Clear timeout FIRST to prevent double response
- if (responseSent || res.headersSent) return;
- responseSent = true;
-
- if (code === 0) {
- console.log('✅ Price refresh completed successfully');
-
- // Check if Amazon blocked all requests
- const amazonBlocked = output.includes('HTTP 503') || output.includes('No prices tracked');
- const dealsFound = output.includes('PRICE DROPS');
-
- res.json({
- success: true,
- message: amazonBlocked ? 'Amazon blocked scraping - showing cached prices' : 'Prices refreshed successfully',
- dealsFound: dealsFound,
- amazonBlocked: amazonBlocked
- });
- } else {
- console.error('❌ Price refresh failed with code:', code);
- res.json({
- success: false,
- message: 'Failed to refresh prices',
- error: errorOutput
- });
- }
- });
-
- } catch (error: any) {
- console.error('❌ Refresh error:', error);
- res.status(500).json({
- success: false,
- message: error.message
- });
- }
-});
diff --git a/DW-Agents/dw-agents/task-orchestrator-agent.ts.backup b/DW-Agents/dw-agents/task-orchestrator-agent.ts.backup
deleted file mode 100644
index 793f939f..00000000
--- a/DW-Agents/dw-agents/task-orchestrator-agent.ts.backup
+++ /dev/null
@@ -1,1723 +0,0 @@
-/**
- * DW-Agents: Task Orchestrator Agent
- *
- * Master task management system that:
- * - Auto-tracks every action as a task
- * - Routes tasks to appropriate specialized agents
- * - Maintains full audit trail with timestamps
- * - Self-healing capabilities for server errors
- * - Memory system for context persistence
- * - Interactive mode for clarifications
- *
- * Port: 9900
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express, { Request, Response } from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import fs from 'fs';
-import path from 'path';
-
-// Import theme utilities
-const { getThemeStyles } = require('./shared-ui-components.js');
-
-const app = express();
-const PORT = 9900;
-
-// Task database file
-const TASKS_DB_FILE = path.join(__dirname, 'tasks-database.json');
-const MEMORY_FILE = path.join(__dirname, 'task-memory.json');
-
-// Task status types
-type TaskStatus = 'pending' | 'assigned' | 'in_progress' | 'blocked' | 'completed' | 'failed';
-type TaskPriority = 'low' | 'medium' | 'high' | 'critical';
-
-interface Task {
- id: string;
- title: string;
- description: string;
- status: TaskStatus;
- priority: TaskPriority;
- assignedAgent?: string;
- createdAt: string;
- assignedAt?: string;
- startedAt?: string;
- completedAt?: string;
- blockedAt?: string;
- blockReason?: string;
- estimatedDuration?: number; // minutes
- actualDuration?: number; // minutes
- autoGenerated: boolean;
- parentTaskId?: string;
- subtasks: string[]; // task IDs
- tags: string[];
- metadata: Record<string, any>;
- actionLog: TaskAction[];
-}
-
-interface TaskAction {
- timestamp: string;
- action: string;
- agent?: string;
- details?: string;
- success: boolean;
-}
-
-interface Memory {
- context: Record<string, any>;
- recentTasks: string[];
- agentPerformance: Record<string, {
- tasksCompleted: number;
- averageDuration: number;
- successRate: number;
- }>;
- commonPatterns: string[];
- lastUpdated: string;
-}
-
-// Agent registry for routing (alphabetical order)
-const AGENT_REGISTRY = {
- 'accounting': { port: 9882, capabilities: ['invoices', 'payments', 'financial-reports'] },
- 'digital-samples': { port: 9879, capabilities: ['sample-orders', 'dig-series', 'file-delivery'] },
- 'in-parallel': { port: 9891, capabilities: ['batch-processing', 'bulk-operations'] },
- 'legal': { port: 9878, capabilities: ['settlement', 'compliance', 'legal-docs'] },
- 'marketing': { port: 9881, capabilities: ['content', 'campaigns', 'social-media'] },
- 'needs-attention': { port: 9886, capabilities: ['urgent-issues', 'customer-complaints'] },
- 'purchasing': { port: 9880, capabilities: ['inventory', 'vendors', 'purchase-orders', 'office-supplies', 'amazon-personal', 'amazon-business', 'price-comparison', 'order-tracking', 'price-tracking', 'deals'] },
- 'server-uptime': { port: 9888, capabilities: ['monitoring', 'health-checks', 'auto-restart'] },
- 'shopify-store': { port: 7238, capabilities: ['shopify', 'products', 'orders', 'customers', 'transactions', 'store-management', 'new-products'] },
- 'york': { port: 7234, capabilities: ['york-products', 'dwjs-updater', 'product-sync'] },
- 'zendesk-chat': { port: 9884, capabilities: ['customer-support', 'live-chat'] }
-};
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- }
-}
-
-app.use(
- session({
- secret: 'dw-task-orchestrator-2025',
- resave: false,
- saveUninitialized: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 7 * 24 * 60 * 60 * 1000,
- },
- })
-);
-
-// Initialize databases
-function initializeDatabases() {
- if (!fs.existsSync(TASKS_DB_FILE)) {
- fs.writeFileSync(TASKS_DB_FILE, JSON.stringify({ tasks: [] }, null, 2));
- console.log('✅ Initialized tasks database');
- }
-
- if (!fs.existsSync(MEMORY_FILE)) {
- const initialMemory: Memory = {
- context: {},
- recentTasks: [],
- agentPerformance: {},
- commonPatterns: [],
- lastUpdated: new Date().toISOString()
- };
- fs.writeFileSync(MEMORY_FILE, JSON.stringify(initialMemory, null, 2));
- console.log('✅ Initialized memory system');
- }
-}
-
-// Load tasks from database
-function loadTasks(): Task[] {
- try {
- const fileContent = fs.readFileSync(TASKS_DB_FILE, 'utf-8');
-
- // Check if file is empty or truncated
- if (!fileContent || fileContent.trim().length === 0) {
- console.warn('⚠️ Tasks database is empty, initializing...');
- return [];
- }
-
- const data = JSON.parse(fileContent);
- return data.tasks || [];
- } catch (error: any) {
- console.error('❌ Error loading tasks:', error.message);
-
- // Try to recover from backup if exists
- const backupFile = TASKS_DB_FILE + '.backup';
- if (fs.existsSync(backupFile)) {
- try {
- console.log('🔄 Attempting to restore from backup...');
- const backupContent = fs.readFileSync(backupFile, 'utf-8');
- const backupData = JSON.parse(backupContent);
- fs.copyFileSync(backupFile, TASKS_DB_FILE);
- console.log('✅ Restored from backup successfully');
- return backupData.tasks || [];
- } catch (backupError) {
- console.error('❌ Backup restore failed:', backupError);
- }
- }
-
- return [];
- }
-}
-
-// Save tasks to database (atomic write to prevent corruption)
-function saveTasks(tasks: Task[]) {
- try {
- const tempFile = TASKS_DB_FILE + '.tmp';
- const backupFile = TASKS_DB_FILE + '.backup';
- const content = JSON.stringify({ tasks }, null, 2);
-
- // Create backup of current file if it exists
- if (fs.existsSync(TASKS_DB_FILE)) {
- try {
- fs.copyFileSync(TASKS_DB_FILE, backupFile);
- } catch (backupError) {
- console.error('⚠️ Failed to create backup:', backupError);
- }
- }
-
- // Write to temp file first
- fs.writeFileSync(tempFile, content, 'utf-8');
-
- // Verify temp file is valid JSON before replacing
- const verification = JSON.parse(fs.readFileSync(tempFile, 'utf-8'));
- if (!verification.tasks) {
- throw new Error('Invalid task data structure');
- }
-
- // Atomic rename (replaces old file)
- fs.renameSync(tempFile, TASKS_DB_FILE);
- } catch (error) {
- console.error('❌ Error saving tasks:', error);
- // Clean up temp file if it exists
- const tempFile = TASKS_DB_FILE + '.tmp';
- if (fs.existsSync(tempFile)) {
- fs.unlinkSync(tempFile);
- }
- }
-}
-
-// Load memory
-function loadMemory(): Memory {
- try {
- return JSON.parse(fs.readFileSync(MEMORY_FILE, 'utf-8'));
- } catch (error) {
- console.error('Error loading memory:', error);
- return {
- context: {},
- recentTasks: [],
- agentPerformance: {},
- commonPatterns: [],
- lastUpdated: new Date().toISOString()
- };
- }
-}
-
-// Save memory
-function saveMemory(memory: Memory) {
- try {
- memory.lastUpdated = new Date().toISOString();
- fs.writeFileSync(MEMORY_FILE, JSON.stringify(memory, null, 2));
- } catch (error) {
- console.error('Error saving memory:', error);
- }
-}
-
-// Generate unique task ID
-function generateTaskId(): string {
- const timestamp = Date.now();
- const random = Math.random().toString(36).substr(2, 9);
- return `TASK-${timestamp}-${random}`;
-}
-
-// Create a new task
-function createTask(
- title: string,
- description: string,
- priority: TaskPriority = 'medium',
- autoGenerated: boolean = false,
- metadata: Record<string, any> = {}
-): Task {
- const task: Task = {
- id: generateTaskId(),
- title,
- description,
- status: 'pending',
- priority,
- createdAt: new Date().toISOString(),
- autoGenerated,
- subtasks: [],
- tags: [],
- metadata,
- actionLog: [{
- timestamp: new Date().toISOString(),
- action: 'created',
- details: `Task created: ${title}`,
- success: true
- }]
- };
-
- const tasks = loadTasks();
- tasks.push(task);
- saveTasks(tasks);
-
- // Update memory
- const memory = loadMemory();
- memory.recentTasks.unshift(task.id);
- if (memory.recentTasks.length > 100) {
- memory.recentTasks = memory.recentTasks.slice(0, 100);
- }
- saveMemory(memory);
-
- console.log(`✅ Created task: ${task.id} - ${title}`);
- return task;
-}
-
-// Route task to appropriate agent
-function routeTask(task: Task): string | null {
- const keywords = (task.title + ' ' + task.description).toLowerCase();
-
- for (const [agentName, agent] of Object.entries(AGENT_REGISTRY)) {
- for (const capability of agent.capabilities) {
- if (keywords.includes(capability.toLowerCase())) {
- return agentName;
- }
- }
- }
-
- // Default routing logic
- if (keywords.includes('urgent') || keywords.includes('critical')) {
- return 'needs-attention';
- }
- if (keywords.includes('customer') || keywords.includes('support')) {
- return 'zendesk-chat';
- }
- if (keywords.includes('order') || keywords.includes('sample')) {
- return 'digital-samples';
- }
- if (keywords.includes('server') || keywords.includes('error') || keywords.includes('down')) {
- return 'server-uptime';
- }
-
- return null;
-}
-
-// Assign task to agent
-function assignTask(taskId: string, agentName: string): boolean {
- const tasks = loadTasks();
- const task = tasks.find(t => t.id === taskId);
-
- if (!task) return false;
-
- task.assignedAgent = agentName;
- task.status = 'assigned';
- task.assignedAt = new Date().toISOString();
- task.actionLog.push({
- timestamp: new Date().toISOString(),
- action: 'assigned',
- agent: agentName,
- details: `Assigned to ${agentName} agent`,
- success: true
- });
-
- saveTasks(tasks);
- console.log(`📋 Assigned task ${taskId} to ${agentName}`);
- return true;
-}
-
-// Update task status
-function updateTaskStatus(taskId: string, status: TaskStatus, details?: string): boolean {
- const tasks = loadTasks();
- const task = tasks.find(t => t.id === taskId);
-
- if (!task) return false;
-
- task.status = status;
-
- if (status === 'in_progress' && !task.startedAt) {
- task.startedAt = new Date().toISOString();
- } else if (status === 'completed' && !task.completedAt) {
- task.completedAt = new Date().toISOString();
-
- // Calculate actual duration
- if (task.startedAt) {
- const start = new Date(task.startedAt).getTime();
- const end = new Date(task.completedAt).getTime();
- task.actualDuration = Math.round((end - start) / 60000); // minutes
- }
-
- // Update agent performance in memory
- if (task.assignedAgent) {
- const memory = loadMemory();
- if (!memory.agentPerformance[task.assignedAgent]) {
- memory.agentPerformance[task.assignedAgent] = {
- tasksCompleted: 0,
- averageDuration: 0,
- successRate: 0
- };
- }
- memory.agentPerformance[task.assignedAgent].tasksCompleted++;
- saveMemory(memory);
- }
- } else if (status === 'blocked') {
- task.blockedAt = new Date().toISOString();
- }
-
- task.actionLog.push({
- timestamp: new Date().toISOString(),
- action: 'status_change',
- details: details || `Status changed to ${status}`,
- success: true
- });
-
- saveTasks(tasks);
- console.log(`📝 Updated task ${task.id} status to ${status}`);
- return true;
-}
-
-// Auto-detect and create tasks from system events
-async function autoDetectTasks() {
- console.log('🔍 Auto-detecting tasks from system events...');
-
- // 1. Sync TodoWrite tasks
- try {
- const { execSync } = require('child_process');
- const syncScript = require('path').join(__dirname, 'sync-todos-to-orchestrator.js');
- execSync(`node "${syncScript}"`, { cwd: __dirname, stdio: 'pipe' });
- console.log('✅ TodoWrite sync completed');
- } catch (error: any) {
- if (error.message && !error.message.includes('No changes')) {
- console.error('❌ TodoWrite sync error:', error.message);
- }
- }
-
- // 2. Monitor for new Shopify orders (last 5 minutes)
- try {
- const https = require('https');
- const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
-
- const options = {
- hostname: 'designer-laboratory-sandbox.myshopify.com',
- path: `/admin/api/2024-01/orders.json?status=any&created_at_min=${fiveMinutesAgo}&limit=10`,
- headers: { 'X-Shopify-Access-Token': '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' }
- };
-
- https.get(options, (res: any) => {
- let data = '';
- res.on('data', (chunk: any) => data += chunk);
- res.on('end', () => {
- try {
- const { orders } = JSON.parse(data);
- if (orders && orders.length > 0) {
- orders.forEach((order: any) => {
- const tasks = loadTasks();
- const exists = tasks.tasks.some((t: any) =>
- t.metadata?.orderId === order.id.toString()
- );
-
- if (!exists) {
- const newTask = {
- id: generateTaskId(),
- title: `New Order: ${order.name} - ${order.customer?.first_name || 'Customer'}`,
- description: `Order from ${order.customer?.email || 'unknown'} - ${order.line_items?.length || 0} items - $${order.total_price}`,
- status: 'pending',
- priority: parseFloat(order.total_price) > 500 ? 'high' : 'medium',
- createdAt: order.created_at,
- assignedAgent: 'purchasing-office',
- metadata: {
- orderId: order.id.toString(),
- orderName: order.name,
- source: 'Shopify',
- autoDetected: true
- },
- tags: ['order', 'shopify', 'auto-detected'],
- actionLog: [{
- timestamp: new Date().toISOString(),
- action: 'created',
- agent: 'task-orchestrator',
- details: 'Auto-detected from Shopify',
- success: true
- }]
- };
- tasks.tasks.push(newTask);
- saveTasks(tasks);
- console.log(`📦 New order detected: ${order.name}`);
- }
- });
- }
- } catch (e) { /* Silent fail */ }
- });
- }).on('error', () => { /* Silent fail */ });
- } catch (error) { /* Silent fail */ }
-
- // 3. Monitor for new client signups (check agent status)
- try {
- const http = require('http');
- const req = http.get('http://localhost:9885/api/stats', (res: any) => {
- let data = '';
- res.on('data', (chunk: any) => data += chunk);
- res.on('end', () => {
- try {
- const stats = JSON.parse(data);
- if (stats.pendingSignups && stats.pendingSignups > 0) {
- const tasks = loadTasks();
- const exists = tasks.tasks.some((t: any) =>
- t.title.includes('New Client Signup') && t.status === 'pending'
- );
- if (!exists) {
- const newTask = {
- id: generateTaskId(),
- title: `New Client Signup - ${stats.pendingSignups} pending`,
- description: 'Review and approve new client registrations',
- status: 'pending',
- priority: 'high',
- createdAt: new Date().toISOString(),
- assignedAgent: 'marketing',
- metadata: { source: 'ClientSignup', autoDetected: true },
- tags: ['client', 'signup', 'auto-detected'],
- actionLog: [{
- timestamp: new Date().toISOString(),
- action: 'created',
- agent: 'task-orchestrator',
- details: 'Auto-detected pending signups',
- success: true
- }]
- };
- tasks.tasks.push(newTask);
- saveTasks(tasks);
- console.log(`👤 New client signup detected`);
- }
- }
- } catch (e) { /* Silent fail */ }
- });
- });
- req.on('error', () => { /* Silent fail */ });
- } catch (error) { /* Silent fail */ }
-
- // 4. Monitor PM2 processes for failures
- try {
- const { execSync } = require('child_process');
- const pm2List = execSync('pm2 jlist', { encoding: 'utf8' });
- const processes = JSON.parse(pm2List);
-
- processes.forEach((proc: any) => {
- if (proc.pm2_env?.status === 'errored' || proc.pm2_env?.restart_time > 5) {
- const tasks = loadTasks();
- const exists = tasks.tasks.some((t: any) =>
- t.metadata?.processName === proc.name &&
- t.status !== 'completed' &&
- t.createdAt > new Date(Date.now() - 10 * 60 * 1000).toISOString()
- );
-
- if (!exists) {
- const newTask = {
- id: generateTaskId(),
- title: `Process Issue: ${proc.name}`,
- description: `Process ${proc.name} has ${proc.pm2_env.restart_time} restarts. Status: ${proc.pm2_env.status}`,
- status: 'pending',
- priority: 'critical',
- createdAt: new Date().toISOString(),
- assignedAgent: 'server-uptime',
- metadata: {
- processName: proc.name,
- pid: proc.pid,
- restarts: proc.pm2_env.restart_time,
- source: 'PM2',
- autoDetected: true
- },
- tags: ['server', 'pm2', 'auto-detected', 'critical'],
- actionLog: [{
- timestamp: new Date().toISOString(),
- action: 'created',
- agent: 'task-orchestrator',
- details: 'Auto-detected from PM2 monitoring',
- success: true
- }]
- };
- tasks.tasks.push(newTask);
- saveTasks(tasks);
- console.log(`🚨 Process issue detected: ${proc.name}`);
- }
- }
- });
- } catch (error) { /* Silent fail */ }
-}
-
-// SSO Authentication
-app.get('/login', handleLogin);
-
-app.get('/sso/callback', (req, res) => {
- setSSOToken();
- res.redirect('/');
-});
-
-// Dashboard
-app.get('/', ssoAuth, (req: Request, res: Response) => {
- const tasks = loadTasks();
- const memory = loadMemory();
-
- const tasksByStatus = {
- pending: tasks.filter(t => t.status === 'pending').length,
- assigned: tasks.filter(t => t.status === 'assigned').length,
- in_progress: tasks.filter(t => t.status === 'in_progress').length,
- blocked: tasks.filter(t => t.status === 'blocked').length,
- completed: tasks.filter(t => t.status === 'completed').length,
- failed: tasks.filter(t => t.status === 'failed').length
- };
-
- // Sort tasks: in_progress first, then blocked, then pending, then completed
- const statusPriority = {
- 'in_progress': 1,
- 'blocked': 2,
- 'pending': 3,
- 'assigned': 4,
- 'completed': 5,
- 'failed': 6
- };
-
- // Sort all tasks (no limit - will use infinite scroll on frontend)
- const recentTasks = tasks
- .slice()
- .sort((a, b) => {
- const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99);
- if (priorityDiff !== 0) return priorityDiff;
- // Within same status, sort by most recent first
- return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
- });
- const taskListHtml = recentTasks.length > 0 ? recentTasks.map(task => {
- const createdDate = new Date(task.createdAt).toLocaleString();
- const startedDate = task.startedAt ? new Date(task.startedAt).toLocaleString() : '-';
- const completedDate = task.completedAt ? new Date(task.completedAt).toLocaleString() : '-';
-
- // Calculate duration
- let duration = '-';
- if (task.status === 'completed' && task.actualDuration) {
- duration = `${task.actualDuration} min`;
- } else if (task.startedAt) {
- const start = new Date(task.startedAt).getTime();
- const now = Date.now();
- const minutes = Math.round((now - start) / 60000);
- duration = `${minutes} min (ongoing)`;
- }
-
- // Progress indicator for in-progress tasks
- let progressHtml = '';
- if (task.status === 'in_progress') {
- // Prioritize work progress (e.g., "44/2000 products") over action log progress
- if (task.metadata && task.metadata.workProgress) {
- const { current, total } = task.metadata.workProgress;
- const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
- progressHtml = `
- <div style="margin: 10px 0; padding: 12px; background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1)); border-left: 4px solid #667eea; border-radius: 8px;">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
- <span style="font-weight: 700; color: #36454f; font-size: 16px;">⚙️ Work Progress: ${current.toLocaleString()}/${total.toLocaleString()}</span>
- <span style="font-weight: 700; color: #36454f; font-size: 18px;">${percentage}%</span>
- </div>
- <div style="background: #e0e0e0; height: 12px; border-radius: 6px; overflow: hidden;">
- <div style="background: linear-gradient(90deg, #667eea, #764ba2); height: 100%; width: ${percentage}%; transition: width 0.3s ease;"></div>
- </div>
- </div>
- `;
- } else if (task.metadata && task.metadata.progress) {
- const { completed, total } = task.metadata.progress;
- const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
- progressHtml = `
- <div style="margin: 10px 0;">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
- <span style="font-weight: 600; color: #36454f;">📊 Progress: ${completed}/${total} steps</span>
- <span style="font-weight: 600; color: #36454f;">${percentage}%</span>
- </div>
- <div style="background: #e0e0e0; height: 8px; border-radius: 4px; overflow: hidden;">
- <div style="background: linear-gradient(90deg, #667eea, #764ba2); height: 100%; width: ${percentage}%; transition: width 0.3s ease;"></div>
- </div>
- </div>
- `;
- }
- }
-
- // Needs intervention alert for blocked tasks
- const interventionBanner = task.status === 'blocked' ? `
- <div style="background: #ff6b6b; color: white; padding: 8px 12px; border-radius: 6px; font-weight: 700; display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
- <span style="font-size: 18px;">⚠️</span>
- <span style="font-size: 13px;">NEEDS INTERVENTION</span>
- ${task.blockReason ? `<span style="font-size: 12px; font-weight: 400; opacity: 0.9;">- ${task.blockReason}</span>` : ''}
- </div>
- ` : '';
-
- // Get agent display name and suggestion
- let agentDisplay = task.assignedAgent || 'unassigned';
- const agentIcon = task.assignedAgent ? '🤖' : '⏳';
-
- // Suggest agent if unassigned
- let suggestedAgent = '';
- if (!task.assignedAgent) {
- const keywords = (task.title + ' ' + task.description).toLowerCase();
- // Check capabilities
- for (const [agentName, agent] of Object.entries(AGENT_REGISTRY)) {
- for (const capability of agent.capabilities) {
- if (keywords.includes(capability.toLowerCase())) {
- suggestedAgent = agentName;
- agentDisplay = `unassigned <span style="color: #28a745; font-size: 11px;">(💡 suggest: ${agentName})</span>`;
- break;
- }
- }
- if (suggestedAgent) break;
- }
- }
-
- // Agent dropdown options
- const agentOptions = Object.keys(AGENT_REGISTRY).map(agentName => {
- const selected = task.assignedAgent === agentName ? 'selected' : '';
- return `<option value="${agentName}" ${selected}>${agentName}</option>`;
- }).join('');
-
- // Action buttons
- const actionButtons = `
- <span class="task-actions">
- ${task.status !== 'completed' && task.status !== 'failed' ? `
- <select class="agent-dropdown" onchange="assignTaskToAgent('${task.id}', this.value)" style="padding: 4px 8px; font-size: 11px; border: 2px solid #667eea; border-radius: 6px; cursor: pointer; font-weight: 600; background: white; color: #36454f;">
- <option value="">-- Reassign --</option>
- ${agentOptions}
- </select>
- ` : ''}
- ${task.status === 'pending' || task.status === 'assigned' ? `<button class="action-btn start-btn" onclick="updateStatus('${task.id}', 'in_progress')">▶️ Start</button>` : ''}
- ${task.status === 'in_progress' ? `
- <button class="action-btn stop-btn" onclick="stopTask('${task.id}')">⏸️ Stop</button>
- <button class="action-btn complete-btn" onclick="updateStatus('${task.id}', 'completed')">✅ Complete</button>
- ` : ''}
- ${task.status === 'blocked' || task.status === 'failed' ? `<button class="action-btn restart-btn" onclick="restartTask('${task.id}')">🔄 Restart</button>` : ''}
- ${task.status !== 'blocked' && task.status !== 'completed' && task.status !== 'failed' ? `<button class="action-btn block-btn" onclick="blockTask('${task.id}')">⚠️ Block</button>` : ''}
- </span>
- `;
-
- // Agent link and live metrics
- let agentLink = '';
- let agentPort = task.metadata?.agentPort || (task.assignedAgent && AGENT_REGISTRY[task.assignedAgent] ? AGENT_REGISTRY[task.assignedAgent].port : null);
- if (agentPort) {
- agentLink = `<div style="margin: 10px 0;"><a href="http://45.61.58.125:${agentPort}" target="_blank" style="color: #36454f; text-decoration: none; font-weight: 600; background: rgba(102, 126, 234, 0.1); padding: 8px 12px; border-radius: 6px; display: inline-block;">🔗 Agent Dashboard: http://45.61.58.125:${agentPort}</a></div>`;
- }
-
- // Live metrics container (will be populated via JavaScript)
- let liveMetricsHtml = '';
- if (task.status === 'in_progress' && task.assignedAgent) {
- liveMetricsHtml = `
- <div id="live-metrics-${task.id}" class="live-metrics" style="margin: 10px 0; padding: 12px; background: linear-gradient(135deg, rgba(0, 212, 255, 0.1), rgba(40, 167, 69, 0.1)); border-left: 4px solid #00d4ff; border-radius: 8px;">
- <div style="font-weight: 700; color: #00d4ff; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
- <span>📊 Live Metrics</span>
- <span style="font-size: 12px; color: #999;">Updating...</span>
- </div>
- <div class="metrics-content" style="font-size: 14px; color: #666;">
- Loading live data...
- </div>
- </div>
- `;
- }
-
- return `
- <div class="task-item" data-status="${task.status}" data-task-id="${task.id}">
- ${interventionBanner}
- <div class="task-header">
- <div class="task-title" title="${task.title}">${task.title}</div>
- <div class="task-status status-${task.status}">${task.status.replace('_', ' ')}</div>
- </div>
- <div class="task-description">${task.description}</div>
- ${progressHtml}
- ${liveMetricsHtml}
- ${agentLink}
- ${task.metadata && task.metadata.url ? `<div style="margin: 10px 0;"><a href="${task.metadata.url}" target="_blank" style="color: #36454f; text-decoration: none; font-weight: 600;">🔗 ${task.metadata.url}</a></div>` : ''}
- <div class="task-meta">
- <span class="task-agent">${agentIcon} ${agentDisplay}</span>
- <span class="task-priority">⚡ ${task.priority}</span>
- <span class="task-created">📅 ${createdDate}</span>
- <span class="task-started">▶️ ${startedDate}</span>
- <span class="task-completed">✅ ${completedDate}</span>
- <span class="task-duration">⏱️ ${duration}</span>
- ${actionButtons}
- </div>
- </div>
- `;
- }).join('') : '<div style="padding: 20px; text-align: center; color: #999;">No tasks yet. Create your first task below!</div>';
-
- res.send(`
- <!DOCTYPE html>
- <html>
- <head>
- <title>Task Orchestrator - DW Agents</title>
- ${getThemeStyles('modern-minimalist')}
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- min-height: 100vh;
- padding: 20px;
- }
- .container { max-width: 1400px; margin: 0 auto; }
- .header {
- background: white;
- padding: 30px;
- border-radius: 20px;
- margin-bottom: 30px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.1);
- }
- .header h1 {
- font-size: 42px;
- color: #1a1a1a;
- margin-bottom: 10px;
- display: flex;
- align-items: center;
- gap: 15px;
- }
- .header p { color: #666; font-size: 18px; }
-
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stat-card {
- background: white;
- padding: 25px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stat-card h3 {
- font-size: 14px;
- color: #888;
- text-transform: uppercase;
- letter-spacing: 1px;
- margin-bottom: 10px;
- }
- .stat-card .number {
- font-size: 36px;
- font-weight: 700;
- color: #36454f;
- }
-
- .main-content {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 30px;
- }
-
- .panel {
- background: white;
- padding: 30px;
- border-radius: 20px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.1);
- }
- .panel h2 {
- font-size: 24px;
- margin-bottom: 20px;
- color: #1a1a1a;
- display: flex;
- align-items: center;
- gap: 10px;
- }
-
- .task-list { max-height: 600px; overflow-y: auto; }
- .task-item {
- padding: 15px;
- border: 2px solid #f0f0f0;
- border-radius: 10px;
- margin-bottom: 15px;
- transition: all 0.3s;
- }
- .task-item:hover {
- border-color: #36454f;
- box-shadow: 0 3px 10px rgba(102, 126, 234, 0.1);
- }
- .task-header {
- display: flex;
- justify-content: space-between;
- align-items: start;
- margin-bottom: 10px;
- }
- .task-title {
- font-weight: 600;
- font-size: 16px;
- color: #1a1a1a;
- }
- .task-status {
- padding: 5px 12px;
- border-radius: 20px;
- font-size: 12px;
- font-weight: 600;
- text-transform: uppercase;
- }
- .status-pending { background: #fff3cd; color: #856404; }
- .status-assigned { background: #cce5ff; color: #004085; }
- .status-in_progress { background: #d1ecf1; color: #0c5460; }
- .status-blocked { background: #f8d7da; color: #721c24; }
- .status-completed { background: #d4edda; color: #155724; }
- .task-meta {
- display: flex;
- gap: 15px;
- font-size: 13px;
- color: #888;
- margin-top: 8px;
- flex-wrap: wrap;
- }
-
- /* List view styles - spreadsheet layout */
- .view-toggle {
- display: flex;
- gap: 10px;
- margin-bottom: 15px;
- }
- .view-btn {
- padding: 8px 16px;
- background: #f0f0f0;
- border: 2px solid transparent;
- border-radius: 8px;
- cursor: pointer;
- font-size: 14px;
- font-weight: 600;
- color: #666;
- transition: all 0.3s;
- }
- .view-btn:hover {
- background: #e0e0e0;
- }
- .view-btn.active {
- background: #36454f;
- color: white;
- border-color: #36454f;
- }
-
- /* Spreadsheet-style table for list view */
- .task-list.list-view {
- display: table;
- width: 100%;
- border-collapse: collapse;
- border: 1px solid #e0e0e0;
- }
- .task-list.list-view .task-item {
- display: table-row;
- padding: 0;
- margin: 0;
- border: none;
- border-radius: 0;
- }
- .task-list.list-view .task-item:hover {
- background: #f0f7ff;
- }
- .task-list.list-view .task-item > div {
- display: table-cell;
- padding: 10px 8px;
- vertical-align: middle;
- font-size: 12px;
- border-bottom: 1px solid #e0e0e0;
- }
-
- /* Hide description in list view */
- .task-list.list-view .task-description {
- display: none;
- }
-
- .task-list.list-view .task-header {
- display: contents;
- }
- .task-list.list-view .task-title {
- font-size: 13px;
- font-weight: 600;
- max-width: 250px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .task-list.list-view .task-status {
- padding: 4px 8px;
- border-radius: 10px;
- font-size: 10px;
- text-align: center;
- white-space: nowrap;
- }
- .task-list.list-view .task-meta {
- display: contents;
- }
- .task-list.list-view .task-meta span {
- display: table-cell;
- padding: 10px 8px;
- font-size: 11px;
- white-space: nowrap;
- border-bottom: 1px solid #e0e0e0;
- }
-
- /* Blocked row highlight */
- .task-list.list-view .task-item[data-status="blocked"] {
- background: #fff5f5;
- }
- .task-list.list-view .task-item[data-status="blocked"]:hover {
- background: #ffe5e5;
- }
-
- /* Agent column styling */
- .task-agent {
- font-weight: 600;
- color: #667eea !important;
- background: #f0f4ff;
- padding: 6px 10px !important;
- border-radius: 6px;
- }
- .task-list.list-view .task-agent {
- background: transparent;
- font-weight: 600;
- color: #667eea !important;
- }
-
- /* Action buttons */
- .task-actions {
- display: inline-flex;
- gap: 5px;
- margin-left: 10px;
- align-items: center;
- }
- .agent-dropdown {
- padding: 4px 8px;
- font-size: 11px;
- border: 2px solid #667eea;
- border-radius: 6px;
- cursor: pointer;
- font-weight: 600;
- background: white;
- color: #36454f;
- transition: all 0.2s;
- }
- .agent-dropdown:hover {
- background: #f0f4ff;
- transform: translateY(-1px);
- box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
- }
- .agent-dropdown:focus {
- outline: none;
- border-color: #5568d3;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
- }
- .action-btn {
- padding: 4px 10px;
- font-size: 11px;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-weight: 600;
- transition: all 0.2s;
- }
- .action-btn:hover {
- transform: translateY(-1px);
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
- }
- .assign-btn {
- background: #36454f;
- color: white;
- }
- .start-btn {
- background: #00d4ff;
- color: white;
- }
- .stop-btn {
- background: #ff9800;
- color: white;
- }
- .restart-btn {
- background: #9c27b0;
- color: white;
- }
- .complete-btn {
- background: #28a745;
- color: white;
- }
- .block-btn {
- background: #ff6b6b;
- color: white;
- }
- .task-list.list-view .task-actions {
- display: table-cell;
- padding: 10px 8px;
- border-bottom: 1px solid #e0e0e0;
- }
-
- .btn {
- display: inline-block;
- padding: 12px 24px;
- background: #36454f;
- color: white;
- text-decoration: none;
- border-radius: 8px;
- font-weight: 600;
- transition: all 0.3s;
- border: none;
- cursor: pointer;
- width: 100%;
- }
- .btn:hover {
- background: #5568d3;
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
- }
-
- .create-task-form {
- display: grid;
- gap: 15px;
- margin-top: 20px;
- }
- .form-group label {
- display: block;
- margin-bottom: 5px;
- font-weight: 600;
- color: #333;
- }
- .form-group input,
- .form-group textarea,
- .form-group select {
- width: 100%;
- padding: 12px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- font-size: 14px;
- font-family: inherit;
- }
- .form-group textarea { resize: vertical; min-height: 100px; }
-
- @media (max-width: 968px) {
- .main-content { grid-template-columns: 1fr; }
- }
- </style>
- </head>
- <body>
- <div class="container">
-
- <div class="header">
- <h1>🎯 Task Orchestrator</h1>
- <p>Master task management & auto-routing system for all DW agents</p>
- </div>
-
- <div class="stats-grid">
- <div class="stat-card">
- <h3>Pending</h3>
- <div class="number">${tasksByStatus.pending}</div>
- </div>
- <div class="stat-card">
- <h3>In Progress</h3>
- <div class="number">${tasksByStatus.in_progress}</div>
- </div>
- <div class="stat-card">
- <h3>Completed</h3>
- <div class="number">${tasksByStatus.completed}</div>
- </div>
- <div class="stat-card">
- <h3>Blocked</h3>
- <div class="number">${tasksByStatus.blocked}</div>
- </div>
- <div class="stat-card">
- <h3>Total Tasks</h3>
- <div class="number">${tasks.length}</div>
- </div>
- <div class="stat-card">
- <h3>Active Agents</h3>
- <div class="number">${Object.keys(AGENT_REGISTRY).length}</div>
- </div>
- </div>
-
- <div class="main-content">
-
- <div class="panel">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
- <h2 style="margin: 0;">📋 Recent Tasks</h2>
- <button class="btn" onclick="togglePanel()" style="width: auto; padding: 10px 20px;" id="toggleBtn">➕ Create Task</button>
- </div>
-
- <div id="taskListPanel">
- <div class="view-toggle">
- <button class="view-btn" onclick="setView('card')">🎴 Card View</button>
- <button class="view-btn active" onclick="setView('list')">📋 List View</button>
- </div>
- <div class="task-list list-view" id="taskList">
- ${taskListHtml}
- </div>
- </div>
-
- <div id="createFormPanel" style="display: none;">
- <h3 style="font-size: 20px; margin-bottom: 20px; color: #333;">➕ Create New Task</h3>
- <form class="create-task-form" action="/api/tasks" method="POST">
- <div class="form-group">
- <label>Task Title</label>
- <input type="text" name="title" placeholder="Brief description of the task" required>
- </div>
- <div class="form-group">
- <label>Description</label>
- <textarea name="description" placeholder="Detailed task description" required></textarea>
- </div>
- <div class="form-group">
- <label>Priority</label>
- <select name="priority">
- <option value="low">Low</option>
- <option value="medium" selected>Medium</option>
- <option value="high">High</option>
- <option value="critical">Critical</option>
- </select>
- </div>
- <button type="submit" class="btn">Create Task</button>
- </form>
- </div>
- </div>
-
- </div>
-
- <div class="panel" style="margin-top: 20px;">
- <h2>📊 System Memory</h2>
- <div style="background: #f5f5f5; padding: 20px; border-radius: 10px; font-size: 14px; color: #555;">
- <p style="margin-bottom: 10px;"><strong>Recent Tasks Tracked:</strong> ${memory.recentTasks.length}</p>
- <p style="margin-bottom: 10px;"><strong>Last Updated:</strong> ${new Date(memory.lastUpdated).toLocaleString()}</p>
- <p><strong>Agents Monitored:</strong> ${Object.keys(memory.agentPerformance).length}</p>
- </div>
- </div>
-
- </div>
-
- <script>
- let showingForm = false;
-
- function togglePanel() {
- const taskListPanel = document.getElementById('taskListPanel');
- const createFormPanel = document.getElementById('createFormPanel');
- const toggleBtn = document.getElementById('toggleBtn');
-
- showingForm = !showingForm;
-
- if (showingForm) {
- taskListPanel.style.display = 'none';
- createFormPanel.style.display = 'block';
- toggleBtn.innerHTML = '📋 View Tasks';
- } else {
- taskListPanel.style.display = 'block';
- createFormPanel.style.display = 'none';
- toggleBtn.innerHTML = '➕ Create Task';
- }
- }
-
- function setView(viewType) {
- const taskList = document.getElementById('taskList');
- const buttons = document.querySelectorAll('.view-btn');
-
- buttons.forEach(btn => btn.classList.remove('active'));
-
- if (viewType === 'list') {
- taskList.classList.add('list-view');
- buttons[1].classList.add('active');
- } else {
- taskList.classList.remove('list-view');
- buttons[0].classList.add('active');
- }
-
- // Save preference to localStorage
- localStorage.setItem('taskViewPreference', viewType);
- }
-
- // Restore saved view preference on load
- window.addEventListener('DOMContentLoaded', () => {
- const savedView = localStorage.getItem('taskViewPreference') || 'list';
- setView(savedView);
-
- // Start live metrics polling for in_progress tasks
- startLiveMetricsPolling();
- });
-
- // Live metrics polling for in_progress tasks
- async function startLiveMetricsPolling() {
- const liveMetricElements = document.querySelectorAll('.live-metrics');
- if (liveMetricElements.length === 0) return;
-
- // Poll every 5 seconds
- setInterval(async () => {
- for (const element of liveMetricElements) {
- const taskId = element.id.replace('live-metrics-', '');
- await updateLiveMetrics(taskId);
- }
- }, 5000);
-
- // Initial update
- for (const element of liveMetricElements) {
- const taskId = element.id.replace('live-metrics-', '');
- await updateLiveMetrics(taskId);
- }
- }
-
- async function updateLiveMetrics(taskId) {
- try {
- const response = await fetch(\`/api/agent-metrics/\${taskId}\`);
- const data = await response.json();
-
- const metricsElement = document.getElementById(\`live-metrics-\${taskId}\`);
- if (!metricsElement) return;
-
- const contentDiv = metricsElement.querySelector('.metrics-content');
- if (!contentDiv) return;
-
- // Update timestamp
- const timestampSpan = metricsElement.querySelector('span:last-child');
- if (timestampSpan) {
- const now = new Date();
- timestampSpan.textContent = \`Updated \${now.toLocaleTimeString()}\`;
- timestampSpan.style.color = '#28a745';
- }
-
- // Parse and display metrics
- if (data.error) {
- contentDiv.innerHTML = \`<span style="color: #999;">⚠️ \${data.error}</span>\`;
- } else if (data.metrics && data.metrics.job) {
- const job = data.metrics.job;
- const percentage = job.progress.total > 0
- ? Math.round((job.progress.processed / job.progress.total) * 100)
- : 0;
-
- let statusColor = '#00d4ff';
- if (job.status === 'completed') statusColor = '#28a745';
- if (job.status === 'failed') statusColor = '#ff6b6b';
-
- // Extract current SKU from logs
- let currentSku = 'Unknown';
- if (job.logs && job.logs.length > 0) {
- const lastLog = job.logs[job.logs.length - 1];
- const skuMatch = lastLog.match(/DWJS-\\d+/);
- if (skuMatch) currentSku = skuMatch[0];
- }
-
- // Get latest logs (last 10)
- const latestLogs = job.logs && job.logs.length > 0
- ? job.logs.slice(-10).map(log => {
- let color = '#f0f0f0';
- let bgColor = 'transparent';
- if (log.includes('✅') || log.includes('Successfully')) {
- color = '#4ade80';
- bgColor = 'rgba(74, 222, 128, 0.1)';
- }
- if (log.includes('❌') || log.includes('Failed')) {
- color = '#ff6b6b';
- bgColor = 'rgba(255, 107, 107, 0.1)';
- }
- if (log.includes('📄 Page')) {
- color = '#60a5fa';
- bgColor = 'rgba(96, 165, 250, 0.1)';
- }
- return \`<div style="font-size: 11px; padding: 4px 6px; margin: 2px 0; color: \${color}; background: \${bgColor}; border-radius: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">\${log}</div>\`;
- }).join('')
- : '<div style="color: #999; font-size: 11px;">No logs yet</div>';
-
- contentDiv.innerHTML = \`
- <div style="display: grid; gap: 8px;">
- <div style="display: flex; justify-content: space-between;">
- <span style="font-weight: 600;">Status:</span>
- <span style="color: \${statusColor}; font-weight: 700;">\${job.status.toUpperCase()}</span>
- </div>
- <div style="display: flex; justify-content: space-between;">
- <span style="font-weight: 600;">Processed:</span>
- <span style="font-weight: 700; color: #36454f;">\${job.progress.processed.toLocaleString()}\${job.progress.total > 0 ? ' / ' + job.progress.total.toLocaleString() : ''}</span>
- </div>
- <div style="display: flex; justify-content: space-between;">
- <span style="font-weight: 600;">Success Rate:</span>
- <span style="font-weight: 700; color: #28a745;">\${job.progress.successful.toLocaleString()} ✓</span>
- </div>
- \${job.progress.failed > 0 ? \`
- <div style="display: flex; justify-content: space-between;">
- <span style="font-weight: 600;">Failed:</span>
- <span style="font-weight: 700; color: #ff6b6b;">\${job.progress.failed} ✗</span>
- </div>
- \` : ''}
- <div style="display: flex; justify-content: space-between;">
- <span style="font-weight: 600;">Current SKU:</span>
- <span style="font-weight: 700; color: #764ba2;">\${currentSku}</span>
- </div>
- \${job.progress.total > 0 ? \`
- <div style="margin-top: 8px;">
- <div style="background: #e0e0e0; height: 6px; border-radius: 3px; overflow: hidden;">
- <div style="background: linear-gradient(90deg, #00d4ff, #28a745); height: 100%; width: \${percentage}%; transition: width 0.3s ease;"></div>
- </div>
- <div style="text-align: center; margin-top: 4px; font-size: 12px; color: #999;">\${percentage}% Complete</div>
- </div>
- \` : ''}
- <div style="margin-top: 12px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px;">
- <div style="font-weight: 600; margin-bottom: 6px; color: #ffffff; font-size: 12px;">📋 Live Logs:</div>
- <div style="max-height: 150px; overflow-y: auto; background: #1a1a2e; padding: 8px; border-radius: 6px; font-family: 'Courier New', monospace; border: 1px solid rgba(255,255,255,0.1);">
- \${latestLogs}
- </div>
- </div>
- </div>
- \`;
- } else {
- contentDiv.innerHTML = '<span style="color: #999;">No metrics available</span>';
- }
- } catch (error) {
- console.error('Failed to update live metrics:', error);
- }
- }
-
- // Auto-refresh every 30 seconds (increased from 15 to avoid interrupting live updates)
- setInterval(() => {
- // Only refresh if not showing the form
- if (!showingForm) {
- location.reload();
- }
- }, 30000);
-
- // Task action functions
- async function assignTaskToAgent(taskId, agentName) {
- if (!agentName) return; // Don't do anything if empty option selected
-
- try {
- const response = await fetch(\`/api/tasks/\${taskId}/assign\`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ agent: agentName })
- });
- if (response.ok) {
- // Update the task item to show the Start button
- const taskItem = document.querySelector(\`[data-task-id="\${taskId}"]\`);
- if (taskItem) {
- const actionsSpan = taskItem.querySelector('.task-actions');
- if (actionsSpan) {
- // Add Start button if it doesn't exist
- if (!actionsSpan.querySelector('.start-btn')) {
- const startBtn = document.createElement('button');
- startBtn.className = 'action-btn start-btn';
- startBtn.innerHTML = '▶️ Start';
- startBtn.onclick = () => updateStatus(taskId, 'in_progress');
- actionsSpan.appendChild(startBtn);
- }
-
- // Update agent display in task-meta
- const agentSpan = taskItem.querySelector('.task-agent');
- if (agentSpan) {
- agentSpan.innerHTML = \`🤖 \${agentName}\`;
- }
-
- // Update task status badge
- const statusBadge = taskItem.querySelector('.task-status');
- if (statusBadge) {
- statusBadge.className = 'task-status status-assigned';
- statusBadge.textContent = 'assigned';
- }
- }
- }
- }
- } catch (error) {
- alert('Failed to assign task');
- }
- }
-
- async function updateStatus(taskId, status) {
- try {
- const response = await fetch(\`/api/tasks/\${taskId}/status\`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ status })
- });
- if (response.ok) {
- location.reload();
- }
- } catch (error) {
- alert('Failed to update status');
- }
- }
-
- async function blockTask(taskId) {
- const reason = prompt('Why is this task blocked?');
- if (reason) {
- try {
- const response = await fetch(\`/api/tasks/\${taskId}/status\`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ status: 'blocked', details: reason })
- });
- if (response.ok) {
- location.reload();
- }
- } catch (error) {
- alert('Failed to block task');
- }
- }
- }
-
- async function stopTask(taskId) {
- if (confirm('Stop this task? You can restart it later.')) {
- try {
- const response = await fetch(\`/api/tasks/\${taskId}/status\`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ status: 'pending', details: 'Task stopped by user' })
- });
- if (response.ok) {
- location.reload();
- }
- } catch (error) {
- alert('Failed to stop task');
- }
- }
- }
-
- async function restartTask(taskId) {
- if (confirm('Restart this task?')) {
- try {
- const response = await fetch(\`/api/tasks/\${taskId}/status\`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ status: 'in_progress', details: 'Task restarted by user' })
- });
- if (response.ok) {
- location.reload();
- }
- } catch (error) {
- alert('Failed to restart task');
- }
- }
- }
- </script>
- </body>
- </html>
- `);
-});
-
-// API: Create task
-app.post('/api/tasks', ssoAuth, (req: Request, res: Response) => {
- const { title, description, priority } = req.body;
-
- if (!title || !description) {
- return res.status(400).json({ error: 'Title and description required' });
- }
-
- const task = createTask(title, description, priority as TaskPriority, false);
-
- // Auto-route to appropriate agent
- const agent = routeTask(task);
- if (agent) {
- assignTask(task.id, agent);
- }
-
- res.redirect('/');
-});
-
-// API: Get all tasks
-app.get('/api/tasks', ssoAuth, (req: Request, res: Response) => {
- const tasks = loadTasks();
- res.json({ tasks });
-});
-
-// API: Get task by ID
-app.get('/api/tasks/:id', ssoAuth, (req: Request, res: Response) => {
- const tasks = loadTasks();
- const task = tasks.find(t => t.id === req.params.id);
-
- if (!task) {
- return res.status(404).json({ error: 'Task not found' });
- }
-
- res.json({ task });
-});
-
-// API: Update task status
-app.patch('/api/tasks/:id/status', ssoAuth, (req: Request, res: Response) => {
- const { status, details } = req.body;
-
- if (!status) {
- return res.status(400).json({ error: 'Status required' });
- }
-
- const success = updateTaskStatus(req.params.id, status as TaskStatus, details);
-
- if (!success) {
- return res.status(404).json({ error: 'Task not found' });
- }
-
- res.json({ success: true });
-});
-
-// API: Assign task to agent
-app.post('/api/tasks/:id/assign', ssoAuth, (req: Request, res: Response) => {
- const { agent } = req.body;
-
- if (!agent) {
- return res.status(400).json({ error: 'Agent name required' });
- }
-
- const success = assignTask(req.params.id, agent);
-
- if (!success) {
- return res.status(404).json({ error: 'Task not found' });
- }
-
- res.json({ success: true });
-});
-
-// API: Get memory/context
-app.get('/api/memory', ssoAuth, (req: Request, res: Response) => {
- const memory = loadMemory();
- res.json({ memory });
-});
-
-// API: Get live agent metrics
-app.get('/api/agent-metrics/:taskId', async (req: Request, res: Response) => {
- const { taskId } = req.params;
- const tasks = loadTasks();
- const task = tasks.find(t => t.id === taskId);
-
- if (!task) {
- return res.json({ error: 'Task not found' });
- }
-
- // Check if task has an agent and port metadata
- const agentName = task.assignedAgent;
- let agentPort = task.metadata?.agentPort;
-
- // If no port in metadata, look up in registry
- if (!agentPort && agentName && AGENT_REGISTRY[agentName]) {
- agentPort = AGENT_REGISTRY[agentName].port;
- }
-
- // Try to fetch live metrics from the agent
- if (agentPort) {
- try {
- const http = require('http');
- const response = await new Promise<any>((resolve, reject) => {
- const req = http.get(`http://localhost:${agentPort}/api/status`, (res) => {
- let data = '';
- res.on('data', chunk => data += chunk);
- res.on('end', () => {
- try {
- resolve(JSON.parse(data));
- } catch (e) {
- resolve({ error: 'Invalid JSON response' });
- }
- });
- });
- req.on('error', () => resolve({ error: 'Agent not responding' }));
- req.setTimeout(2000, () => {
- req.destroy();
- resolve({ error: 'Timeout' });
- });
- });
-
- return res.json({
- taskId,
- agentName,
- agentPort,
- agentUrl: `http://45.61.58.125:${agentPort}`,
- metrics: response,
- timestamp: new Date().toISOString()
- });
- } catch (error) {
- return res.json({
- taskId,
- agentName,
- agentPort,
- error: 'Failed to fetch metrics'
- });
- }
- }
-
- return res.json({
- taskId,
- agentName,
- error: 'No agent port available'
- });
-});
-
-// Webhook endpoint for other agents to submit tasks (no auth required)
-app.post('/api/webhook/task', (req: Request, res: Response) => {
- try {
- const { title, description, priority, source, metadata } = req.body;
-
- if (!title || !description) {
- return res.status(400).json({ error: 'Title and description required' });
- }
-
- console.log(`📥 Webhook task received from: ${source || 'unknown'}`);
- console.log(` Title: ${title}`);
-
- const task = createTask(
- title,
- description,
- priority as TaskPriority || 'medium',
- true // auto-generated
- );
-
- // Add source metadata
- if (source) {
- task.metadata.source = source;
- }
- if (metadata) {
- task.metadata = { ...task.metadata, ...metadata };
- }
-
- // Save the updated task
- const tasks = loadTasks();
- const index = tasks.findIndex(t => t.id === task.id);
- if (index !== -1) {
- tasks[index] = task;
- saveTasks(tasks);
- }
-
- // Auto-route to appropriate agent
- const agent = routeTask(task);
- if (agent) {
- assignTask(task.id, agent);
- console.log(` ✓ Auto-assigned to: ${agent}`);
- }
-
- res.json({
- success: true,
- taskId: task.id,
- assignedAgent: agent
- });
- } catch (error) {
- console.error('Webhook error:', error);
- res.status(500).json({
- error: error instanceof Error ? error.message : 'Unknown error'
- });
- }
-});
-
-// Health check
-app.get('/health', (req: Request, res: Response) => {
- res.json({
- status: 'ok',
- totalTasks: loadTasks().length,
- timestamp: new Date().toISOString()
- });
-});
-
-// Start server
-app.listen(PORT, () => {
- initializeDatabases();
- console.log(`
-╔════════════════════════════════════════════════════════════╗
-║ 🎯 Task Orchestrator Agent ║
-╠════════════════════════════════════════════════════════════╣
-║ Status: RUNNING ║
-║ Port: ${PORT} ║
-║ Dashboard: http://45.61.58.125:${PORT}/ ║
-║ ║
-║ Features: ║
-║ ✓ Auto task tracking ║
-║ ✓ Smart agent routing ║
-║ ✓ Full audit trail ║
-║ ✓ Memory system ║
-║ ✓ Self-healing detection ║
-╚════════════════════════════════════════════════════════════╝
- `);
-
- // Start auto-detection
- setInterval(autoDetectTasks, 60000); // Every minute
-});
diff --git a/DW-Agents/dw-agents/todays-highlights-agent.ts.backup b/DW-Agents/dw-agents/todays-highlights-agent.ts.backup
deleted file mode 100644
index b03b69c3..00000000
--- a/DW-Agents/dw-agents/todays-highlights-agent.ts.backup
+++ /dev/null
@@ -1,942 +0,0 @@
-/**
- * DW-Agents: Today's Highlights Agent
- *
- * Aggregates notable activities across all DW-Agents throughout the day
- * - New orders and sales
- * - Digital samples processed
- * - Legal compliance updates
- * - Marketing campaigns
- * - Trend research insights
- * - Vendor updates
- *
- * Port: 9885
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express, { Request, Response } from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import fetch from 'node-fetch';
-import { AgentMemory } from './shared-memory-system';
-
-// Import theme utilities
-const { getThemeStyles } = require('./shared-ui-components.js');
-
-const app = express();
-const PORT = 9885;
-// Initialize agent memory system
-const agentMemory = new AgentMemory('todays-highlights');
-console.log('📚 Memory system initialized for todays-highlights');
-
-
-// Shopify configuration - DW-MCP App with read_orders scope
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || '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';
-const API_VERSION = '2024-07';
-
-// Anthropic AI
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY,
-});
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- }
-}
-
-app.use(
- session({
- secret: 'dw-highlights-2025',
- resave: false,
- saveUninitialized: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 7 * 24 * 60 * 60 * 1000,
- },
- })
-);
-
-const requireAuth = (req: Request, res: Response, next: any) => {
- // TEMPORARY: Auth disabled for 4 hours
- return next();
-
- /* Original auth:
-
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
- */
-};
-
-// Login page
-app.get('/login', (req: Request, res: Response) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Today's Highlights - Login</title>
- ${getThemeStyles('modern-minimalist')}
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #FFA500; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>✨ Today's Highlights</h1>
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req: Request, res: Response) => {
- const { username, password } = req.body;
- if (username === 'admin' && password === '2025') {
- req.session.authenticated = true;
-
- // Set SSO cookie for cross-agent authentication
- setSSOToken(res);
-
- res.redirect('/');
- } else {
- res.redirect('/login');
- }
-});
-
-// Main dashboard
-app.get('/', requireAuth, (req: Request, res: Response) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Today's Highlights - DW-Agents</title>
- ${getThemeStyles('modern-minimalist')}
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #FFA500; font-size: 2.5em; margin-bottom: 10px; }
- .header .subtitle { color: #666; font-size: 1.1em; }
- .refresh-bar {
- background: white;
- padding: 15px 30px;
- border-radius: 15px;
- margin-bottom: 20px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .refresh-bar button {
- background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
- color: white;
- border: none;
- padding: 10px 20px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- }
- .highlights-container {
- display: grid;
- gap: 20px;
- }
- .highlight-section {
- background: white;
- padding: 25px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .highlight-section h2 {
- color: #FFA500;
- margin-bottom: 15px;
- font-size: 1.5em;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .highlight-item {
- background: #f5f5f5;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 4px solid #FFA500;
- }
- .highlight-item strong { color: #333; }
- .highlight-item .time { color: #888; font-size: 0.9em; }
- .stat-badge {
- display: inline-block;
- background: #FFD700;
- color: #333;
- padding: 4px 12px;
- border-radius: 12px;
- font-size: 0.9em;
- font-weight: 600;
- margin-left: 10px;
- }
- .loading { color: #888; text-align: center; padding: 20px; }
- .metrics-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 15px;
- margin-bottom: 20px;
- }
- .metric-card {
- background: white;
- padding: 20px;
- border-radius: 10px;
- border: 2px solid #FFD700;
- text-align: center;
- }
- .metric-value {
- font-size: 2em;
- font-weight: bold;
- color: #FFA500;
- margin: 10px 0;
- }
- .metric-label {
- color: #666;
- font-size: 0.9em;
- text-transform: uppercase;
- letter-spacing: 1px;
- }
- .product-table {
- width: 100%;
- border-collapse: collapse;
- margin-top: 15px;
- }
- .product-table th {
- background: #FFA500;
- color: white;
- padding: 12px;
- text-align: left;
- font-size: 0.9em;
- position: sticky;
- top: 0;
- z-index: 10;
- }
- .product-table td {
- padding: 10px;
- border-bottom: 1px solid #eee;
- }
- .product-table tr:hover {
- background: #fffaec;
- }
- </style>
-</head>
-<body>
- <div class="header">
- <h1>✨ Today's Highlights</h1>
- <div class="subtitle">Notable activities across all DW-Agents today</div>
- </div>
-
- <div class="refresh-bar">
- <div style="display: flex; gap: 10px; align-items: center;">
- <button onclick="loadDate('today')" id="todayBtn" style="background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);">📅 Today</button>
- <button onclick="loadDate('yesterday')" id="yesterdayBtn">📅 Yesterday</button>
- <span id="currentDate" style="color: #666; font-weight: 600; margin-left: 10px;"></span>
- </div>
- <div style="display: flex; gap: 10px; align-items: center;">
- <span id="lastUpdate" style="color: #888;">Last updated: --:--</span>
- <button onclick="refreshHighlights()">🔄 Refresh</button>
- </div>
- </div>
-
- <div class="highlights-container">
- <div class="highlight-section" style="background: linear-gradient(135deg, #fff 0%, #fffaec 100%);">
- <h2>💰 Sales Dashboard Today</h2>
- <div id="salesMetrics" class="loading">Loading...</div>
- </div>
-
- <div class="highlight-section">
- <h2>🚨 Critical Events & Recoveries <span class="stat-badge" id="criticalCount">0</span></h2>
- <div id="criticalHighlights" class="loading">Loading...</div>
- </div>
-
- <div class="highlight-section">
- <h2>🛒 Sales & Orders <span class="stat-badge" id="ordersCount">0</span></h2>
- <div id="ordersHighlights" class="loading">Loading...</div>
- </div>
-
- <div class="highlight-section">
- <h2>📦 Digital Samples <span class="stat-badge" id="samplesCount">0</span></h2>
- <div id="samplesHighlights" class="loading">Loading...</div>
- </div>
-
- <div class="highlight-section">
- <h2>⚖️ Legal & Compliance</h2>
- <div id="legalHighlights" class="loading">Loading...</div>
- </div>
-
- <div class="highlight-section">
- <h2>📊 Trends & Insights</h2>
- <div id="trendsHighlights" class="loading">Loading...</div>
- </div>
-
- <div class="highlight-section">
- <h2>💬 Customer Support</h2>
- <div id="supportHighlights" class="loading">Loading...</div>
- </div>
-
- <div class="highlight-section" style="background: linear-gradient(135deg, #fff 0%, #e8f5e9 100%);">
- <h2>✅ Highlighted Completed Tasks & Big Picture Wins <span class="stat-badge" id="completedCount">0</span></h2>
- <div id="completedHighlights" class="loading">Loading...</div>
- </div>
- </div>
-
- <script>
- let currentDateParam = '';
-
- function loadDate(date) {
- currentDateParam = date === 'today' ? '' : '?date=' + date;
-
- // Update button styles
- document.getElementById('todayBtn').style.background = date === 'today'
- ? 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)'
- : 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)';
- document.getElementById('yesterdayBtn').style.background = date === 'yesterday'
- ? 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)'
- : 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)';
-
- // Update date label
- const dateLabel = date === 'today' ? 'Today' : 'Yesterday';
- document.getElementById('currentDate').textContent = dateLabel;
-
- refreshHighlights();
- }
-
- async function refreshHighlights() {
- try {
- const data = await fetch('/api/highlights' + currentDateParam).then(r => r.json());
-
- // Sales Metrics Dashboard
- const salesDiv = document.getElementById('salesMetrics');
- if (data.salesMetrics) {
- const m = data.salesMetrics;
- salesDiv.innerHTML = \`
- <div class="metrics-grid">
- <div class="metric-card">
- <div class="metric-label">Total Orders</div>
- <div class="metric-value">\${m.totalOrders}</div>
- </div>
- <div class="metric-card">
- <div class="metric-label">Revenue Today</div>
- <div class="metric-value">$\${m.totalRevenue.toLocaleString()}</div>
- </div>
- <div class="metric-card">
- <div class="metric-label">Avg Order Value</div>
- <div class="metric-value">$\${m.averageOrderValue.toFixed(2)}</div>
- </div>
- <div class="metric-card">
- <div class="metric-label">Unique Customers</div>
- <div class="metric-value">\${m.uniqueCustomers}</div>
- </div>
- <div class="metric-card">
- <div class="metric-label">Repeat Customers</div>
- <div class="metric-value">\${m.repeatCustomers}</div>
- </div>
- <div class="metric-card">
- <div class="metric-label">Repeat Rate</div>
- <div class="metric-value">\${m.conversionRate}%</div>
- </div>
- </div>
-
- <h3 style="color: #FFA500; margin: 20px 0 10px 0;">🛍️ Latest Products Purchased</h3>
- <div style="max-height: 400px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 8px;">
- <table class="product-table">
- <thead>
- <tr>
- <th>Product</th>
- <th>SKU</th>
- <th>Qty Sold</th>
- <th>Revenue</th>
- </tr>
- </thead>
- <tbody>
- \${m.topProducts.slice(0, 10).map(p => \`
- <tr>
- <td><strong>\${p.name}</strong></td>
- <td style="color: #888; font-size: 0.9em;">\${p.sku}</td>
- <td>\${p.quantity}</td>
- <td style="color: #FFA500; font-weight: bold;">$\${p.revenue.toFixed(2)}</td>
- </tr>
- \`).join('')}
- \${m.topProducts.length === 0 ? '<tr><td colspan="4" style="text-align: center; color: #888;">No products sold yet today</td></tr>' : ''}
- </tbody>
- </table>
- </div>
-
- \${m.lowStockProducts && m.lowStockProducts.length > 0 ? \`
- <h3 style="color: #DC143C; margin: 30px 0 10px 0;">⚠️ Low Inventory Alert</h3>
- <table class="product-table">
- <thead>
- <tr>
- <th>Product</th>
- <th>SKU</th>
- <th>Stock</th>
- </tr>
- </thead>
- <tbody>
- \${m.lowStockProducts.map(p => \`
- <tr style="background: \${p.inventory <= 3 ? '#ffebee' : 'white'}">
- <td>
- <strong>\${p.title}</strong>
- \${p.variantTitle ? \`<br><span style="color: #888; font-size: 0.9em;">\${p.variantTitle}</span>\` : ''}
- </td>
- <td style="color: #888; font-size: 0.9em;">\${p.sku}</td>
- <td style="color: \${p.inventory <= 3 ? '#DC143C' : '#FFA500'}; font-weight: bold;">\${p.inventory} left</td>
- </tr>
- \`).join('')}
- </tbody>
- </table>
- \` : ''}
- \`;
- }
-
- // Critical Events
- const criticalDiv = document.getElementById('criticalHighlights');
- document.getElementById('criticalCount').textContent = data.critical.length;
- if (data.critical.length > 0) {
- criticalDiv.innerHTML = data.critical.map(c => \`
- <div class="highlight-item" style="border-left-color: #DC143C; background: #ffebee;">
- <strong>\${c.title}</strong><br>
- \${c.description}<br>
- <span class="time">\${new Date(c.timestamp).toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles' })}</span>
- </div>
- \`).join('');
- } else {
- criticalDiv.innerHTML = '<p style="color: #888;">✅ No critical events today</p>';
- }
-
- // Orders
- const ordersDiv = document.getElementById('ordersHighlights');
- document.getElementById('ordersCount').textContent = data.orders.length;
- if (data.orders.length > 0) {
- ordersDiv.innerHTML = data.orders.map(o => \`
- <div class="highlight-item">
- <strong>\${o.title}</strong><br>
- \${o.description}<br>
- <span class="time">\${new Date(o.timestamp).toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles' })}</span>
- </div>
- \`).join('');
- } else {
- ordersDiv.innerHTML = '<p style="color: #888;">No orders today</p>';
- }
-
- // Samples
- const samplesDiv = document.getElementById('samplesHighlights');
- document.getElementById('samplesCount').textContent = data.samples.length;
- if (data.samples.length > 0) {
- samplesDiv.innerHTML = data.samples.map(s => \`
- <div class="highlight-item">
- <strong>\${s.title}</strong><br>
- \${s.description}<br>
- <span class="time">\${new Date(s.timestamp).toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles' })}</span>
- </div>
- \`).join('');
- } else {
- samplesDiv.innerHTML = '<p style="color: #888;">No samples processed today</p>';
- }
-
- // Legal
- const legalDiv = document.getElementById('legalHighlights');
- if (data.legal.length > 0) {
- legalDiv.innerHTML = data.legal.map(l => \`
- <div class="highlight-item">
- <strong>\${l.title}</strong><br>
- \${l.description}
- </div>
- \`).join('');
- } else {
- legalDiv.innerHTML = '<p style="color: #888;">No legal updates today</p>';
- }
-
- // Trends
- const trendsDiv = document.getElementById('trendsHighlights');
- if (data.trends.length > 0) {
- trendsDiv.innerHTML = data.trends.map(t => \`
- <div class="highlight-item">
- <strong>\${t.title}</strong><br>
- \${t.description}
- </div>
- \`).join('');
- } else {
- trendsDiv.innerHTML = '<p style="color: #888;">No trend updates today</p>';
- }
-
- // Support
- const supportDiv = document.getElementById('supportHighlights');
- if (data.support.length > 0) {
- supportDiv.innerHTML = data.support.map(s => \`
- <div class="highlight-item">
- <strong>\${s.title}</strong><br>
- \${s.description}
- </div>
- \`).join('');
- } else {
- supportDiv.innerHTML = '<p style="color: #888;">No support activity today</p>';
- }
-
- // Completed Tasks & Big Picture Wins
- const completedDiv = document.getElementById('completedHighlights');
- document.getElementById('completedCount').textContent = data.completed ? data.completed.length : 0;
- if (data.completed && data.completed.length > 0) {
- completedDiv.innerHTML = data.completed.map(c => \`
- <div class="highlight-item" style="border-left-color: #4CAF50; background: #f1f8e9;">
- <strong>\${c.title}</strong><br>
- \${c.description}<br>
- \${c.timestamp ? '<span class="time">' + new Date(c.timestamp).toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles' }) + '</span>' : ''}
- </div>
- \`).join('');
- } else {
- completedDiv.innerHTML = '<p style="color: #888;">No major accomplishments logged yet today</p>';
- }
-
- document.getElementById('lastUpdate').textContent = \`Last updated: \${new Date().toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles' })}\`;
- } catch (error) {
- console.error('Error fetching highlights:', error);
- }
- }
-
- // Auto-refresh every 30 seconds
- setInterval(refreshHighlights, 30000);
-
- // Initialize with today's data
- loadDate('today');
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoint to get today's highlights
-app.get('/api/highlights', requireAuth, async (req: Request, res: Response) => {
- try {
- // Allow date parameter: ?date=yesterday or ?date=2025-11-06
- const dateParam = req.query.date as string;
-
- // Use Pacific Time (America/Los_Angeles)
- let targetDate = new Date(new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }));
-
- if (dateParam === 'yesterday') {
- targetDate.setDate(targetDate.getDate() - 1);
- } else if (dateParam) {
- targetDate = new Date(dateParam);
- }
-
- const today = new Date(targetDate);
- today.setHours(0, 0, 0, 0);
- const tomorrow = new Date(today);
- tomorrow.setDate(tomorrow.getDate() + 1);
-
- // Fetch critical events from Server Uptime Agent
- let criticalHighlights: any[] = [];
- try {
- const uptimeResponse = await fetch('http://localhost:9888/api/status');
- const uptimeData: any = await uptimeResponse.json();
-
- // Get today's activity from the log
- if (uptimeData.recentActivity) {
- const todayActivity = uptimeData.recentActivity.filter((activity: any) => {
- const activityDate = new Date(activity.timestamp);
- return activityDate >= today;
- });
-
- // Filter for critical events (restarts, crash loops, website recoveries)
- const criticalActivity = todayActivity.filter((activity: any) =>
- activity.type === 'RESTART' ||
- activity.type === 'CRASH LOOP FIX' ||
- activity.type === 'DOMAIN RECOVERY' ||
- activity.action.includes('restarted') ||
- activity.action.includes('recovered') ||
- activity.action.includes('crash loop')
- );
-
- criticalHighlights = criticalActivity.slice(0, 10).map((activity: any) => ({
- title: `${activity.type}: ${activity.target}`,
- description: activity.action,
- timestamp: new Date(activity.timestamp),
- type: 'critical'
- }));
- }
-
- // Check for any websites that recovered today
- if (uptimeData.domains) {
- const recoveredDomains = uptimeData.domains.filter((d: any) =>
- d.status === 'up' && d.lastCheck && new Date(d.lastCheck) >= today
- );
-
- if (recoveredDomains.length > 0 && criticalHighlights.length === 0) {
- criticalHighlights.push({
- title: '🚨 Websites Recovered',
- description: `${recoveredDomains.length} websites verified online and operational`,
- timestamp: new Date(),
- type: 'critical'
- });
- }
- }
- } catch (error) {
- console.error('Could not fetch critical events:', error);
- }
-
- // Fetch orders from Shopify for the target date
- const ordersUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/orders.json?status=any&created_at_min=${today.toISOString()}&created_at_max=${tomorrow.toISOString()}&limit=250`;
- console.log(`📊 Fetching orders from ${today.toLocaleDateString()} to ${tomorrow.toLocaleDateString()}`);
- const ordersResponse = await fetch(ordersUrl, {
- headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN },
- });
- const ordersData: any = await ordersResponse.json();
- const orders = ordersData.orders || [];
- console.log(`✅ Found ${orders.length} orders for ${today.toLocaleDateString()}`);
-
- // Calculate real-time sales metrics
- const totalOrders = orders.length;
- const totalRevenue = orders.reduce((sum: number, order: any) => sum + parseFloat(order.total_price || 0), 0);
- const averageOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0;
-
- // Top products sold today
- const productSales: { [key: string]: { name: string; quantity: number; revenue: number; sku: string } } = {};
- orders.forEach((order: any) => {
- order.line_items?.forEach((item: any) => {
- const productId = item.product_id || item.title;
- if (!productSales[productId]) {
- productSales[productId] = {
- name: item.title || item.name || 'Unknown Product',
- quantity: 0,
- revenue: 0,
- sku: item.sku || 'N/A'
- };
- }
- productSales[productId].quantity += item.quantity || 0;
- productSales[productId].revenue += parseFloat(item.price || 0) * (item.quantity || 0);
- });
- });
-
- const topProducts = Object.values(productSales)
- .sort((a, b) => b.revenue - a.revenue)
- .slice(0, 10);
-
- // Revenue by hour (for trend analysis)
- const revenueByHour: { [hour: string]: number } = {};
- orders.forEach((order: any) => {
- const hour = new Date(order.created_at).getHours();
- const hourKey = `${hour}:00`;
- if (!revenueByHour[hourKey]) revenueByHour[hourKey] = 0;
- revenueByHour[hourKey] += parseFloat(order.total_price || 0);
- });
-
- // Customer metrics
- const uniqueCustomers = new Set(orders.map((o: any) => o.customer?.id).filter(Boolean)).size;
- const repeatCustomers = orders.filter((order: any) => {
- return order.customer && parseInt(order.customer.orders_count || '0') > 1;
- }).length;
-
- // Fetch inventory alerts (low stock products)
- let lowStockProducts: any[] = [];
- try {
- const productsUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products.json?limit=250&fields=id,title,variants`;
- const productsResponse = await fetch(productsUrl, {
- headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN },
- });
- const productsData: any = await productsResponse.json();
- const products = productsData.products || [];
-
- // Find products with low inventory (less than 10 units)
- products.forEach((product: any) => {
- product.variants?.forEach((variant: any) => {
- if (variant.inventory_quantity !== null && variant.inventory_quantity < 10 && variant.inventory_quantity > 0) {
- lowStockProducts.push({
- title: product.title,
- variantTitle: variant.title !== 'Default Title' ? variant.title : '',
- sku: variant.sku || 'N/A',
- inventory: variant.inventory_quantity
- });
- }
- });
- });
-
- lowStockProducts.sort((a, b) => a.inventory - b.inventory);
- } catch (error) {
- console.error('Error fetching inventory:', error);
- }
-
- const orderHighlights = orders.slice(0, 10).map((order: any) => ({
- title: `Order ${order.name}`,
- description: `${order.customer?.first_name || 'Customer'} - $${order.total_price} (${order.line_items?.length || 0} items)`,
- timestamp: new Date(order.created_at),
- type: 'order'
- }));
-
- // DIG samples from today
- const digOrders = orders.filter((order: any) =>
- order.line_items?.some((item: any) => item.sku && item.sku.toUpperCase().startsWith('DIG-'))
- );
-
- const sampleHighlights = digOrders.slice(0, 10).map((order: any) => {
- const digItems = order.line_items.filter((item: any) => item.sku && item.sku.toUpperCase().startsWith('DIG-'));
- return {
- title: `Digital Sample Order ${order.name}`,
- description: `${order.customer?.first_name || 'Customer'} - ${digItems.length} DIG samples`,
- timestamp: new Date(order.created_at),
- type: 'sample'
- };
- });
-
- // Load today's real accomplishments
- const fs = require('fs');
- const path = require('path');
- let todaysAccomplishments: any = { accomplishments: [] };
-
- try {
- const dataPath = path.join(__dirname, 'todays-real-highlights.json');
- if (fs.existsSync(dataPath)) {
- todaysAccomplishments = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
- }
- } catch (error) {
- console.error('Error loading accomplishments:', error);
- }
-
- // Add accomplishments to critical if high impact
- todaysAccomplishments.accomplishments
- .filter((a: any) => a.category === 'critical' || a.impact === 'high')
- .forEach((a: any) => {
- if (!criticalHighlights.some((c: any) => c.title === a.title)) {
- criticalHighlights.push({
- title: a.title,
- description: a.description,
- timestamp: new Date(a.timestamp),
- type: 'critical'
- });
- }
- });
-
- // Legal highlights - check agent status
- const legalHighlights = [
- {
- title: 'Settlement Compliance',
- description: 'All products checked and compliant with settlement terms',
- type: 'legal'
- }
- ];
-
- // Trends highlights
- const trendsHighlights = todaysAccomplishments.accomplishments
- .filter((a: any) => a.category === 'trends')
- .map((a: any) => ({
- title: a.title,
- description: a.description,
- type: 'trend'
- }));
-
- if (trendsHighlights.length === 0) {
- trendsHighlights.push({
- title: 'Market Trends Monitored',
- description: 'Continuous monitoring of design trends and market opportunities',
- type: 'trend'
- });
- }
-
- // Support highlights - from Zendesk agent
- const supportHighlights = todaysAccomplishments.accomplishments
- .filter((a: any) => a.category === 'support' || a.title.includes('Zendesk'))
- .map((a: any) => ({
- title: a.title,
- description: a.description,
- type: 'support'
- }));
-
- if (supportHighlights.length === 0) {
- supportHighlights.push({
- title: 'Zendesk Chat Agent Active',
- description: 'Monitoring customer support conversations and providing AI-powered response suggestions',
- type: 'support'
- });
- }
-
- // Completed Tasks & Accomplishments
- const completedTasks = todaysAccomplishments.accomplishments
- .filter(a =>
- a.description &&
- (a.description.includes('completed') ||
- a.description.includes('fixed') ||
- a.description.includes('created') ||
- a.description.includes('implemented') ||
- a.description.includes('updated') ||
- a.description.includes('deployed') ||
- a.description.includes('Added') ||
- a.description.includes('Built') ||
- a.description.includes('Launched') ||
- a.description.toLowerCase().includes('gmail') ||
- a.description.toLowerCase().includes('email'))
- )
- .slice(0, 20)
- .map(a => ({
- title: a.agent || 'Task Completed',
- description: a.description,
- timestamp: a.timestamp
- }));
-
- res.json({
- critical: criticalHighlights,
- orders: orderHighlights,
- samples: sampleHighlights,
- legal: legalHighlights,
- trends: trendsHighlights,
- support: supportHighlights,
- completed: completedTasks,
- salesMetrics: {
- totalOrders,
- totalRevenue: parseFloat(totalRevenue.toFixed(2)),
- averageOrderValue: parseFloat(averageOrderValue.toFixed(2)),
- topProducts,
- revenueByHour,
- uniqueCustomers,
- repeatCustomers,
- conversionRate: totalOrders > 0 ? ((repeatCustomers / totalOrders) * 100).toFixed(1) : '0.0',
- lowStockProducts: lowStockProducts.slice(0, 15)
- },
- generatedAt: new Date()
- });
- } catch (error) {
- console.error('Error fetching highlights:', error);
- res.status(500).json({ error: 'Failed to fetch highlights' });
- }
-});
-
-
-// API: Chat endpoint for universal header
-app.post('/api/chat', requireAuth, async (req, res) => {
-
- // Memory system commands
- if (message.toLowerCase().match(/^(remember|note|preference|learning):/i)) {
- const parts = message.split(':');
- const type = parts[0].toLowerCase();
- const content = parts.slice(1).join(':').trim();
- const memType = type === 'remember' ? 'note' : type as any;
- agentMemory.add(content, memType);
- return res.json({
- response: `✅ Memory saved: "${content}"`,
- success: true,
- memoryAdded: true
- });
- }
-
- if (message.toLowerCase().includes('show') && message.toLowerCase().includes('memor')) {
- const summary = agentMemory.getContextSummary();
- return res.json({ response: summary, success: true });
- }
-
- if (message.toLowerCase().includes('search memor')) {
- const query = message.split(/for|about/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- return res.json({
- response: results.length > 0
- ? 'Found memories:\n' + results.map(m => `- ${m.content}`).join('\n')
- : 'No matching memories found.',
- success: true
- });
- }
-
- if (message.toLowerCase().includes('forget') || message.toLowerCase().includes('delete memor')) {
- const query = message.split(/forget|delete/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- if (results.length > 0) {
- agentMemory.delete(results[0].id);
- return res.json({ response: `✅ Forgot: "${results[0].content}"`, success: true });
- }
- return res.json({ response: 'Nothing found to forget.', success: true });
- }
-
- try {
- const { message } = req.body;
- const systemPrompt = `You are the Todays Highlights AI assistant for Designer Wallcoverings. Display key events and metrics for the day`;
-
- const Anthropic = require('@anthropic-ai/sdk').default;
- const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY || '' });
-
- const response = await anthropic.messages.create({
- model: 'claude-3-5-sonnet-20241022',
- max_tokens: 1024,
- system: systemPrompt,
- messages: [{ role: 'user', content: message }],
- });
-
- const assistantMessage = response.content[0].type === 'text' ? response.content[0].text : '';
- res.json({ response: assistantMessage, success: true });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-// Start server
-app.listen(PORT, '0.0.0.0', () => {
- console.log(`✨ Today's Highlights Agent running on port ${PORT}`);
- console.log(`Dashboard: http://45.61.58.125:${PORT}`);
-});
diff --git a/DW-Agents/dw-agents/zendesk-chat-agent.ts.backup b/DW-Agents/dw-agents/zendesk-chat-agent.ts.backup
deleted file mode 100644
index a8a0c57e..00000000
--- a/DW-Agents/dw-agents/zendesk-chat-agent.ts.backup
+++ /dev/null
@@ -1,1744 +0,0 @@
-/**
- * DW-Agents: Zendesk Chat Agent
- *
- * Customer support chat integration system
- * - Zendesk chat monitoring and management
- * - AI-powered response suggestions
- * - Customer conversation history
- * - Real-time chat analytics
- * - Chat interface for support queries
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express, { Request, Response } from 'express';
-import session from 'express-session';
-import cookieParser from 'cookie-parser';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import fetch from 'node-fetch';
-import { AgentMemory } from './shared-memory-system';
-
-// Import theme utilities
-const { getThemeStyles } = require('./shared-ui-components.js');
-
-const app = express();
-const PORT = 9884;
-
-// Anthropic AI client
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY || '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',
-});
-
-// Chat configuration (No paid services required - using local data)
-const ZENDESK_DOMAIN = 'designerwallcoverings.zendesk.com';
-const ZENDESK_EMAIL = process.env.ZENDESK_EMAIL || 'team@designerwallcoverings.com';
-const ZENDESK_PASSWORD = process.env.ZENDESK_PASSWORD || '';
-const ZENDESK_API_URL = `https://${ZENDESK_DOMAIN}/api/v2`;
-const ZENDESK_CHAT_URL = `https://${ZENDESK_DOMAIN}/chat/agent`;
-
-// Local chat data storage (replaces Zendesk API)
-const USE_DEMO_DATA = true; // Always use demo data since we're not paying for Zendesk
-
-// Middleware
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-// Session configuration with extended TypeScript types
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-app.use(
- session({
- secret: 'dw-agents-zendesk-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days - longer session to avoid re-login
- },
- })
-);
-
-// Authentication middleware with SSO support
-const requireAuth = (req: Request, res: Response, next: any) => {
- // TEMPORARY: Auth disabled for 4 hours
- return next();
-
- /* Original auth:
-
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
- */
-};
-
-// Statistics tracking
-const stats = {
- totalQueries: 0,
- activeChats: 0,
- resolvedChats: 0,
- avgResponseTime: '2.3 min',
- customerSatisfaction: '94%',
- lastActivity: new Date(),
-};
-
-// Sample chat data
-interface ChatMessage {
- sender: 'customer' | 'agent';
- message: string;
- timestamp: Date;
-}
-
-interface ChatConversation {
- id: string;
- zendeskId?: number;
- customerName: string;
- customerEmail: string;
- status: 'active' | 'pending' | 'resolved';
- priority: 'high' | 'normal' | 'low';
- subject: string;
- lastMessage: string;
- timestamp: Date;
- tags: string[];
- messages: ChatMessage[];
-}
-
-// Zendesk API helper functions
-async function fetchZendeskTickets(): Promise<ChatConversation[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?sort_by=updated_at&sort_order=desc&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk API error:', response.status, await response.text());
- return [];
- }
-
- const data: any = await response.json();
- const tickets = data.tickets || [];
-
- // Fetch users to get names and emails
- const userIds = [...new Set(tickets.map((t: any) => t.requester_id).filter((id: any) => typeof id === 'number'))] as number[];
- const users = await fetchZendeskUsers(userIds);
-
- const conversations: ChatConversation[] = tickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- zendeskId: ticket.id,
- customerName: user?.name || 'Unknown Customer',
- customerEmail: user?.email || 'unknown@email.com',
- status: ticket.status === 'new' || ticket.status === 'open' ? 'active' :
- ticket.status === 'pending' ? 'pending' : 'resolved',
- priority: ticket.priority === 'urgent' || ticket.priority === 'high' ? 'high' :
- ticket.priority === 'low' ? 'low' : 'normal',
- subject: ticket.subject || 'No Subject',
- lastMessage: ticket.description?.substring(0, 100) || 'No message',
- timestamp: new Date(ticket.updated_at),
- tags: ticket.tags || [],
- messages: [], // Will be loaded on demand
- };
- });
-
- return conversations;
- } catch (error) {
- console.error('Error fetching Zendesk tickets:', error);
- return [];
- }
-}
-
-async function fetchZendeskUsers(userIds: number[]): Promise<any[]> {
- try {
- if (userIds.length === 0) return [];
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/users/show_many.json?ids=${userIds.join(',')}`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk users API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- return data.users || [];
- } catch (error) {
- console.error('Error fetching Zendesk users:', error);
- return [];
- }
-}
-
-async function fetchTicketComments(ticketId: string): Promise<ChatMessage[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets/${ticketId}/comments.json`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk comments API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- const comments = data.comments || [];
-
- return comments.map((comment: any) => ({
- sender: comment.public ? 'agent' : 'customer',
- message: comment.body || '',
- timestamp: new Date(comment.created_at),
- }));
- } catch (error) {
- console.error('Error fetching ticket comments:', error);
- return [];
- }
-}
-
-// Fetch currently online visitors/chats from Zendesk Chat
-interface OnlineVisitor {
- id: string;
- name: string;
- email: string;
- status: 'browsing' | 'chatting' | 'waiting';
- pageUrl?: string;
- location?: string;
- timestamp: Date;
-}
-
-async function fetchOnlineVisitors(): Promise<OnlineVisitor[]> {
- try {
- // Zendesk Chat uses different auth - OAuth token
- // For now, we'll try to get active chats from the Support API
- // by looking for tickets created in the last hour with specific tags
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
-
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?created_at_gt=${oneHourAgo}&status=new,open&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk online visitors API error:', response.status);
- // Return demo data when API fails
- return [
- {
- id: 'demo-1',
- name: 'Sarah Johnson',
- email: 'sarah@example.com',
- status: 'chatting' as const,
- pageUrl: 'https://designerwallcoverings.com/wallpaper/floral',
- location: 'New York, USA',
- timestamp: new Date(Date.now() - 5 * 60 * 1000) // 5 min ago
- },
- {
- id: 'demo-2',
- name: 'Mike Chen',
- email: 'mike@example.com',
- status: 'waiting' as const,
- pageUrl: 'https://designerwallcoverings.com/samples',
- location: 'Los Angeles, USA',
- timestamp: new Date(Date.now() - 2 * 60 * 1000) // 2 min ago
- },
- {
- id: 'demo-3',
- name: 'Emily Rodriguez',
- email: 'emily@example.com',
- status: 'browsing' as const,
- pageUrl: 'https://designerwallcoverings.com/about',
- location: 'Chicago, USA',
- timestamp: new Date(Date.now() - 1 * 60 * 1000) // 1 min ago
- }
- ];
- }
-
- const data: any = await response.json();
- const recentTickets = data.tickets || [];
-
- // Fetch users for these tickets
- const userIds = [...new Set(recentTickets.map((t: any) => t.requester_id).filter((id: any) => typeof id === 'number'))] as number[];
- const users = await fetchZendeskUsers(userIds);
-
- const visitors: OnlineVisitor[] = recentTickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- name: user?.name || 'Anonymous Visitor',
- email: user?.email || 'visitor@unknown.com',
- status: ticket.status === 'new' ? 'waiting' : 'chatting',
- pageUrl: ticket.custom_fields?.find((f: any) => f.id === 'url')?.value || 'Unknown page',
- location: user?.time_zone || 'Unknown',
- timestamp: new Date(ticket.created_at)
- };
- });
-
- return visitors;
- } catch (error) {
- console.error('Error fetching online visitors:', error);
- return [];
- }
-}
-
-// VIP detection - Fortune 500 and high-value domains
-const VIP_INDICATORS = [
- // Fortune 500 Companies
- 'apple.com', 'microsoft.com', 'amazon.com', 'google.com', 'meta.com', 'facebook.com',
- 'walmart.com', 'exxonmobil.com', 'berkshirehathaway.com', 'unitedhealth.com',
- 'jpmorgan.com', 'jpmorganchase.com', 'cvs.com', 'tesla.com', 'alphabet.com',
- // Hospitality & Hotels
- 'marriott.com', 'hilton.com', 'hyatt.com', 'ihg.com', 'fourseasons.com', 'ritzcarlton.com',
- 'starwood.com', 'accor.com', 'wyndham.com', 'choicehotels.com',
- // Interior Design Firms
- 'gensler.com', 'perkinswill.com', 'hok.com', 'som.com', 'kpf.com',
- // Architecture Firms
- 'aecom.com', 'stantec.com', 'hdr.com', 'jacobs.com',
- // High-end Retailers
- 'nordstrom.com', 'bloomingdales.com', 'saks.com', 'neiman-marcus.com', 'bergdorf.com',
- // Commercial Real Estate
- 'cbre.com', 'jll.com', 'cushmanwakefield.com', 'colliers.com',
- // Restaurants & Hospitality
- 'mcdonalds.com', 'starbucks.com', 'chipotle.com', 'darden.com',
- // Government & Education
- '.gov', '.edu', 'harvard.edu', 'stanford.edu', 'mit.edu',
-];
-
-interface VIPAlert {
- company: string;
- email: string;
- name: string;
- reason: string;
- timestamp: Date;
-}
-
-let vipAlerts: VIPAlert[] = [];
-
-function detectVIP(email: string, name: string): VIPAlert | null {
- const emailLower = email.toLowerCase();
- const nameLower = name.toLowerCase();
-
- // Check email domain
- for (const indicator of VIP_INDICATORS) {
- if (emailLower.includes(indicator)) {
- const company = emailLower.split('@')[1] || indicator;
- return {
- company: company.replace(/\.(com|net|org|edu|gov)$/, ''),
- email,
- name,
- reason: `High-value domain detected: ${company}`,
- timestamp: new Date(),
- };
- }
- }
-
- // Check for corporate keywords in name or email
- const corporateKeywords = ['hotel', 'resort', 'design', 'architect', 'interior', 'commercial', 'property', 'development'];
- for (const keyword of corporateKeywords) {
- if (emailLower.includes(keyword) || nameLower.includes(keyword)) {
- return {
- company: email.split('@')[1] || 'Corporate',
- email,
- name,
- reason: `Corporate keyword detected: ${keyword}`,
- timestamp: new Date(),
- };
- }
- }
-
- return null;
-}
-
-// Cache for conversations
-let conversationsCache: ChatConversation[] = [];
-let lastFetchTime = 0;
-const CACHE_DURATION = 30000; // 30 seconds
-
-let onlineVisitorsCache: OnlineVisitor[] = [];
-let lastVisitorFetchTime = 0;
-
-async function getConversations(): Promise<ChatConversation[]> {
- const now = Date.now();
- if (now - lastFetchTime > CACHE_DURATION) {
- conversationsCache = await fetchZendeskTickets();
- lastFetchTime = now;
-
- // Check for VIP contacts
- conversationsCache.forEach(conv => {
- const vip = detectVIP(conv.customerEmail, conv.customerName);
- if (vip) {
- // Only add if not already in alerts
- if (!vipAlerts.find(a => a.email === vip.email && a.timestamp > new Date(Date.now() - 3600000))) {
- vipAlerts.unshift(vip);
- vipAlerts = vipAlerts.slice(0, 10); // Keep last 10
- }
- }
- });
- }
- return conversationsCache;
-}
-
-async function getOnlineVisitors(): Promise<OnlineVisitor[]> {
- const now = Date.now();
- if (now - lastVisitorFetchTime > CACHE_DURATION) {
- onlineVisitorsCache = await fetchOnlineVisitors();
- lastVisitorFetchTime = now;
- }
- return onlineVisitorsCache;
-}
-
-// NO MOCK DATA - All conversations are loaded from Zendesk API in real-time
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- action: string;
- details: string;
-}> = [];
-
-function logActivity(action: string, details: string) {
- activityLog.unshift({
- timestamp: new Date(),
- action,
- details,
- });
- if (activityLog.length > 50) {
- activityLog.pop();
- }
- stats.lastActivity = new Date();
-}
-
-// Routes
-
-// Login page
-app.get('/login', (req: Request, res: Response) => {
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Login</title>
- ${getThemeStyles('modern-minimalist')}
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 16px;
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
- width: 100%;
- max-width: 400px;
- }
- h1 {
- color: #333;
- font-size: 28px;
- margin-bottom: 10px;
- text-align: center;
- }
- .subtitle {
- color: #666;
- text-align: center;
- margin-bottom: 30px;
- font-size: 14px;
- }
- .form-group {
- margin-bottom: 20px;
- }
- label {
- display: block;
- color: #555;
- margin-bottom: 8px;
- font-weight: 500;
- }
- input {
- width: 100%;
- padding: 12px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- font-size: 16px;
- transition: border-color 0.3s;
- }
- input:focus {
- outline: none;
- border-color: #03a9f4;
- }
- button {
- width: 100%;
- padding: 14px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- button:hover {
- transform: translateY(-2px);
- }
- .agent-badge {
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 12px;
- text-align: center;
- margin-bottom: 20px;
- font-weight: 600;
- }
- </style>
- </head>
- <body>
- <div class="login-container">
- <div class="agent-badge">💬 ZENDESK CHAT AGENT</div>
- <h1>Customer Support</h1>
- <p class="subtitle">DW-Agents System</p>
- <form method="POST" action="/login">
- <div class="form-group">
- <label for="username">Username</label>
- <input type="text" id="username" name="username" required autocomplete="username">
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" id="password" name="password" required autocomplete="current-password">
- </div>
- <button type="submit">Access Dashboard</button>
- </form>
- </div>
- </body>
- </html>
- `);
-});
-
-// Login handler
-app.post('/login', (req: Request, res: Response) => {
- const { username, password } = req.body;
-
- if (username === 'admin' && password === '2025') {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- req.session.save((err) => {
- if (err) {
- console.error('Session save error:', err);
- }
- res.redirect('/');
- });
- logActivity('Login', 'User authenticated successfully');
- } else {
- res.redirect('/login');
- }
-});
-
-// Main dashboard
-app.get('/', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- const onlineVisitors = await getOnlineVisitors();
- const displayConversations = liveConversations; // NO MOCK DATA - only show real Zendesk tickets
- const isLiveData = liveConversations.length > 0;
-
- const activeChats = displayConversations.filter(c => c.status === 'active').length;
- const pendingChats = displayConversations.filter(c => c.status === 'pending').length;
- const resolvedChats = displayConversations.filter(c => c.status === 'resolved').length;
- const onlineCount = onlineVisitors.length;
-
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Dashboard</title>
- ${getThemeStyles('modern-minimalist')}
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- padding: 20px;
- }
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .header h1 {
- color: #333;
- font-size: 32px;
- margin-bottom: 10px;
- }
- .agent-status {
- display: inline-block;
- background: #10b981;
- color: white;
- padding: 6px 12px;
- border-radius: 20px;
- font-size: 14px;
- font-weight: 600;
- }
- .zendesk-link {
- display: inline-block;
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 8px;
- text-decoration: none;
- margin-left: 15px;
- font-size: 14px;
- transition: background 0.3s;
- }
- .zendesk-link:hover {
- background: #0288d1;
- }
- .tabs {
- display: flex;
- gap: 10px;
- margin-bottom: 20px;
- }
- .tab-button {
- background: white;
- color: #03a9f4;
- padding: 12px 24px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-size: 16px;
- font-weight: 600;
- transition: all 0.3s;
- }
- .tab-button:hover {
- background: #f3f4f6;
- }
- .tab-button.active {
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- }
- .tab-content {
- display: none;
- }
- .tab-content.active {
- display: block;
- }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stat-card {
- background: white;
- padding: 24px;
- border-radius: 12px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .stat-value {
- font-size: 32px;
- font-weight: 700;
- color: #03a9f4;
- margin-bottom: 5px;
- }
- .stat-label {
- color: #666;
- font-size: 14px;
- }
- .section {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .section h2 {
- color: #333;
- font-size: 24px;
- margin-bottom: 20px;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .chat-container {
- display: flex;
- flex-direction: column;
- height: 600px;
- }
- .chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 20px;
- background: #f9fafb;
- border-radius: 12px;
- margin-bottom: 20px;
- }
- .message {
- margin-bottom: 16px;
- padding: 12px 16px;
- border-radius: 12px;
- max-width: 80%;
- }
- .message.user {
- background: #03a9f4;
- color: white;
- margin-left: auto;
- }
- .message.assistant {
- background: white;
- color: #333;
- border: 1px solid #e0e0e0;
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- .chat-input {
- flex: 1;
- padding: 14px;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- font-size: 16px;
- font-family: inherit;
- }
- .send-button {
- padding: 14px 28px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 12px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- .send-button:hover {
- transform: translateY(-2px);
- }
- .suggestion-chips {
- display: flex;
- gap: 10px;
- margin-bottom: 15px;
- flex-wrap: wrap;
- }
- .chip {
- background: #f3f4f6;
- color: #03a9f4;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 14px;
- cursor: pointer;
- border: 2px solid transparent;
- transition: all 0.3s;
- }
- .chip:hover {
- background: #03a9f4;
- color: white;
- border-color: #03a9f4;
- }
- .conversation-card {
- background: white;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 15px;
- transition: all 0.3s;
- }
- .conversation-card:hover {
- border-color: #03a9f4;
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(3, 169, 244, 0.1);
- }
- .conversation-header {
- display: flex;
- justify-content: space-between;
- align-items: start;
- margin-bottom: 10px;
- }
- .customer-name {
- font-size: 18px;
- font-weight: 600;
- color: #333;
- }
- .status-badge {
- padding: 4px 12px;
- border-radius: 12px;
- font-size: 12px;
- font-weight: 600;
- }
- .status-active { background: #d1fae5; color: #065f46; }
- .status-pending { background: #fef3c7; color: #92400e; }
- .status-resolved { background: #dbeafe; color: #1e40af; }
- .priority-high { background: #fee2e2; color: #991b1b; }
- .priority-normal { background: #e0e0e0; color: #666; }
- .priority-low { background: #f3f4f6; color: #888; }
- .conversation-subject {
- color: #03a9f4;
- font-size: 14px;
- font-weight: 500;
- margin-bottom: 8px;
- }
- .last-message {
- color: #666;
- font-size: 14px;
- line-height: 1.5;
- margin-bottom: 10px;
- }
- .conversation-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 10px;
- border-top: 1px solid #e0e0e0;
- }
- .timestamp {
- font-size: 12px;
- color: #888;
- }
- .action-button {
- padding: 6px 16px;
- background: #03a9f4;
- color: white;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 14px;
- transition: background 0.3s;
- }
- .action-button:hover {
- background: #0288d1;
- }
- .modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0,0,0,0.7);
- z-index: 1000;
- align-items: center;
- justify-content: center;
- }
- .modal.active {
- display: flex;
- }
- .modal-content {
- background: white;
- border-radius: 16px;
- width: 90%;
- max-width: 800px;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 25px 50px rgba(0,0,0,0.5);
- }
- .modal-header {
- padding: 25px 30px;
- border-bottom: 2px solid #e0e0e0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .modal-header h2 {
- color: #03a9f4;
- margin: 0;
- font-size: 22px;
- }
- .modal-close {
- background: #ef4444;
- color: white;
- border: none;
- width: 36px;
- height: 36px;
- border-radius: 50%;
- cursor: pointer;
- font-size: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.3s;
- }
- .modal-close:hover {
- background: #dc2626;
- }
- .modal-body {
- padding: 25px 30px;
- overflow-y: auto;
- flex: 1;
- }
- .chat-message {
- margin-bottom: 20px;
- display: flex;
- flex-direction: column;
- }
- .chat-message.customer {
- align-items: flex-start;
- }
- .chat-message.agent {
- align-items: flex-end;
- }
- .message-sender {
- font-size: 12px;
- font-weight: 600;
- color: #666;
- margin-bottom: 6px;
- text-transform: uppercase;
- }
- .message-bubble {
- padding: 12px 18px;
- border-radius: 12px;
- max-width: 70%;
- word-wrap: break-word;
- line-height: 1.5;
- }
- .chat-message.customer .message-bubble {
- background: #f3f4f6;
- color: #333;
- border-bottom-left-radius: 4px;
- }
- .chat-message.agent .message-bubble {
- background: #03a9f4;
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message-time {
- font-size: 11px;
- color: #888;
- margin-top: 4px;
- }
- @keyframes pulse {
- 0%, 100% { transform: scale(1); box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); }
- 50% { transform: scale(1.02); box-shadow: 0 15px 50px rgba(255, 107, 107, 0.5); }
- }
- .zendesk-modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.85);
- z-index: 2000;
- align-items: center;
- justify-content: center;
- animation: fadeIn 0.3s ease-out;
- }
- .zendesk-modal.active {
- display: flex;
- }
- .zendesk-modal-content {
- background: white;
- border-radius: 16px;
- width: 95%;
- height: 95%;
- max-width: 1600px;
- max-height: 95vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
- position: relative;
- overflow: hidden;
- }
- .zendesk-modal-header {
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- padding: 20px 30px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 2px solid #0288d1;
- }
- .zendesk-modal-header h2 {
- color: white;
- margin: 0;
- font-size: 22px;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .zendesk-modal-close {
- background: #ef4444;
- color: white;
- border: none;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- cursor: pointer;
- font-size: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s;
- font-weight: bold;
- }
- .zendesk-modal-close:hover {
- background: #dc2626;
- transform: rotate(90deg);
- }
- .zendesk-modal-body {
- flex: 1;
- overflow: hidden;
- position: relative;
- background: #f5f5f5;
- }
- .zendesk-iframe-container {
- width: 100%;
- height: 100%;
- position: relative;
- }
- .zendesk-iframe {
- width: 100%;
- height: 100%;
- border: none;
- background: white;
- }
- .zendesk-loading {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- text-align: center;
- z-index: 1;
- }
- .zendesk-spinner {
- width: 60px;
- height: 60px;
- border: 4px solid #e0e0e0;
- border-top-color: #03a9f4;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto 20px;
- }
- .zendesk-error {
- display: none;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background: white;
- padding: 40px;
- border-radius: 16px;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
- max-width: 500px;
- text-align: center;
- z-index: 2;
- }
- .zendesk-error.show {
- display: block;
- }
- .zendesk-error h3 {
- color: #ef4444;
- margin-bottom: 15px;
- font-size: 20px;
- }
- .zendesk-error p {
- color: #666;
- margin-bottom: 20px;
- line-height: 1.6;
- }
- .zendesk-error-icon {
- font-size: 48px;
- margin-bottom: 15px;
- }
- @keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- @media (max-width: 768px) {
- .zendesk-modal-content {
- width: 100%;
- height: 100%;
- max-width: 100%;
- max-height: 100%;
- border-radius: 0;
- }
- .zendesk-modal-header {
- padding: 15px 20px;
- }
- .zendesk-modal-header h2 {
- font-size: 18px;
- }
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>💬 Customer Chat Monitor</h1>
- <p style="color: #666; margin-top: 5px;">Free Customer Support & Chat Management (No paid services required)</p>
- <div style="margin-top: 15px; display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
- <span class="agent-status">● ONLINE</span>
- <span style="color: #888; font-size: 14px;">Port 9884</span>
- <button onclick="openZendeskDashboard()" class="zendesk-link" style="cursor: pointer; border: none;">🔗 Open Zendesk Chat</button>
- <button onclick="location.reload()" class="zendesk-link" style="cursor: pointer; border: none;">🔄 Refresh</button>
- </div>
- </div>
-
- <!-- VIP Alerts Banner -->
- <div id="vipAlertsBanner" style="display: none; background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); border: 3px solid #ff6b6b; border-radius: 15px; padding: 20px; margin: 20px 0; box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); animation: pulse 2s infinite;">
- <div style="display: flex; align-items: center; gap: 15px;">
- <div style="font-size: 3em;">🚨</div>
- <div style="flex: 1;">
- <h2 style="color: #d32f2f; margin: 0 0 10px 0; font-size: 1.5em;">⚡ HIGH-VALUE OPPORTUNITY DETECTED</h2>
- <div id="vipAlertsContent"></div>
- </div>
- </div>
- </div>
-
- <div class="tabs">
- <button class="tab-button active" onclick="switchTab('overview')">📊 Overview</button>
- <button class="tab-button" onclick="switchTab('chat')">💬 AI Assistant</button>
- <button class="tab-button" onclick="switchTab('conversations')">💭 Active Chats</button>
- <button class="tab-button" onclick="switchTab('analytics')">📈 Analytics</button>
- </div>
-
- <!-- Overview Tab -->
- <div id="tab-overview" class="tab-content active">
- <div class="stats-grid">
- <div class="stat-card" style="border-left: 4px solid #10b981;">
- <div class="stat-value" style="color: #10b981;">${onlineCount}</div>
- <div class="stat-label">🟢 Currently Online</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${activeChats}</div>
- <div class="stat-label">Active Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${pendingChats}</div>
- <div class="stat-label">Pending Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${resolvedChats}</div>
- <div class="stat-label">Resolved Today</div>
- </div>
- </div>
-
- <div class="section" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; padding: 25px; border-radius: 16px; margin-bottom: 20px;">
- <h2 style="color: white; margin-bottom: 15px;">🟢 Currently Online (${onlineCount})</h2>
- ${onlineVisitors.length > 0 ? onlineVisitors.map(visitor => `
- <div style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; margin-bottom: 10px; border-left: 4px solid ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'};">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
- <strong style="font-size: 1.1em;">${visitor.name}</strong>
- <span style="background: ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'}; color: white; padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: 600;">
- ${visitor.status === 'waiting' ? '⏳ Waiting' : visitor.status === 'chatting' ? '💬 Chatting' : '👀 Browsing'}
- </span>
- </div>
- <div style="font-size: 0.9em; opacity: 0.95;">
- 📧 ${visitor.email}<br>
- ${visitor.pageUrl ? `📄 ${visitor.pageUrl}<br>` : ''}
- 🕐 ${new Date(visitor.timestamp).toLocaleTimeString()}
- </div>
- </div>
- `).join('') : '<p style="text-align: center; padding: 20px; opacity: 0.9;">No visitors currently online</p>'}
- </div>
-
- <div class="section">
- <h2>💭 Recent Conversations ${isLiveData ? '<span style="color: #28a745; font-size: 0.8em;">● LIVE</span>' : '<span style="color: #ffc107; font-size: 0.8em;">● DEMO</span>'}</h2>
- <div id="conversationsList">
- ${displayConversations.slice(0, 3).map(conv => `
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-${conv.status}">${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-${conv.priority}">${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">${conv.subject}</div>
- <div class="last-message">${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- `).join('')}
- </div>
- </div>
-
- <div class="section">
- <h2>🔗 Zendesk Integration</h2>
- <p style="color: #666; margin-bottom: 15px;">Connected to: <strong>${ZENDESK_DOMAIN}</strong></p>
- <p style="color: #666; margin-bottom: 15px;">Account: <strong>${ZENDESK_EMAIL}</strong></p>
- <button class="action-button" onclick="openZendeskDashboard()" style="cursor: pointer;">Open Zendesk Chat Dashboard</button>
- </div>
- </div>
-
- <!-- Chat Tab -->
- <div id="tab-chat" class="tab-content">
- <div class="section">
- <h2>💬 AI Support Assistant</h2>
- <p style="color: #666; margin-bottom: 20px;">Get AI-powered help with customer support, chat responses, and Zendesk management</p>
-
- <div class="suggestion-chips">
- <span class="chip" onclick="sendSuggestion('Suggest response for installation question')">Installation help</span>
- <span class="chip" onclick="sendSuggestion('How to handle product availability inquiry')">Product availability</span>
- <span class="chip" onclick="sendSuggestion('Best practices for customer support')">Support best practices</span>
- <span class="chip" onclick="sendSuggestion('Generate empathetic response template')">Response templates</span>
- </div>
-
- <div class="chat-container">
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- Hello! I'm your Zendesk Chat AI Assistant powered by Claude 4. I can help you with:
- <br><br>
- • Suggesting responses to customer inquiries<br>
- • Customer support best practices<br>
- • Chat management strategies<br>
- • Handling difficult conversations<br>
- • Zendesk tips and workflows<br>
- <br>
- How can I assist you with customer support today?
- </div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- class="chat-input"
- id="chatInput"
- placeholder="Ask about customer support, chat responses..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-button" onclick="sendMessage()">Send</button>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Conversations Tab -->
- <div id="tab-conversations" class="tab-content">
- <div class="section">
- <h2>💭 All Conversations</h2>
- <div id="allConversations">
- <p style="color: #888;">Loading conversations...</p>
- </div>
- </div>
- </div>
-
- <!-- Analytics Tab -->
- <div id="tab-analytics" class="tab-content">
- <div class="section">
- <h2>📈 Chat Analytics</h2>
- <div class="stats-grid">
- <div class="stat-card">
- <div class="stat-value">${stats.customerSatisfaction}</div>
- <div class="stat-label">Customer Satisfaction</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.totalQueries}</div>
- <div class="stat-label">Total AI Queries</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${displayConversations.length}</div>
- <div class="stat-label">Total Conversations</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.avgResponseTime}</div>
- <div class="stat-label">Avg Response Time</div>
- </div>
- </div>
- <div style="background: #f9fafb; padding: 20px; border-radius: 12px; margin-top: 20px;">
- <p style="color: #666;">Connect to Zendesk Analytics API for real-time reporting and advanced metrics.</p>
- </div>
- </div>
- </div>
- </div>
-
- <script>
- function switchTab(tabName) {
- document.querySelectorAll('.tab-content').forEach(tab => {
- tab.classList.remove('active');
- });
- document.querySelectorAll('.tab-button').forEach(btn => {
- btn.classList.remove('active');
- });
-
- document.getElementById('tab-' + tabName).classList.add('active');
- event.target.classList.add('active');
-
- if (tabName === 'conversations') {
- loadConversations();
- }
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const message = input.value.trim();
-
- if (!message) return;
-
- const chatMessages = document.getElementById('chatMessages');
- chatMessages.innerHTML += \`
- <div class="message user">\${message}</div>
- \`;
-
- input.value = '';
- chatMessages.scrollTop = chatMessages.scrollHeight;
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
-
- chatMessages.innerHTML += \`
- <div class="message assistant">\${data.response.replace(/\\n/g, '<br>')}</div>
- \`;
- chatMessages.scrollTop = chatMessages.scrollHeight;
- } catch (error) {
- console.error('Chat error:', error);
- chatMessages.innerHTML += \`
- <div class="message assistant">Sorry, I encountered an error. Please try again.</div>
- \`;
- }
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function loadVIPAlerts() {
- try {
- const response = await fetch('/api/vip-alerts');
- const data = await response.json();
-
- if (data.alerts && data.alerts.length > 0) {
- const banner = document.getElementById('vipAlertsBanner');
- const content = document.getElementById('vipAlertsContent');
-
- const html = data.alerts.map(alert => \`
- <div style="background: white; padding: 15px; border-radius: 10px; margin-bottom: 10px; border-left: 5px solid #ff6b6b;">
- <div style="font-weight: 700; font-size: 1.2em; color: #d32f2f; margin-bottom: 5px;">
- \${alert.name} - \${alert.company.toUpperCase()}
- </div>
- <div style="color: #666; margin-bottom: 5px;">
- 📧 \${alert.email}
- </div>
- <div style="color: #ff6b6b; font-weight: 600; font-size: 0.9em;">
- 🎯 \${alert.reason}
- </div>
- <div style="color: #888; font-size: 0.85em; margin-top: 5px;">
- ⏰ \${new Date(alert.timestamp).toLocaleString()}
- </div>
- </div>
- \`).join('');
-
- content.innerHTML = html;
- banner.style.display = 'block';
- } else {
- document.getElementById('vipAlertsBanner').style.display = 'none';
- }
- } catch (error) {
- console.error('Error loading VIP alerts:', error);
- }
- }
-
- async function loadConversations() {
- const container = document.getElementById('allConversations');
- try {
- const response = await fetch('/api/conversations');
- const data = await response.json();
-
- let html = '';
- data.conversations.forEach(conv => {
- html += \`
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">\${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">\${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-\${conv.status}">\${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-\${conv.priority}">\${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">\${conv.subject}</div>
- <div class="last-message">\${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">\${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('\${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/\${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- \`;
- });
-
- container.innerHTML = html;
- } catch (error) {
- console.error('Load conversations error:', error);
- container.innerHTML = '<p style="color: #ef4444;">Error loading conversations</p>';
- }
- }
-
- async function viewChat(chatId) {
- try {
- const response = await fetch(\`/api/conversation/\${chatId}\`);
- const data = await response.json();
-
- const modal = document.getElementById('chatModal');
- const modalTitle = document.getElementById('modalTitle');
- const modalBody = document.getElementById('modalBody');
-
- modalTitle.textContent = \`Chat with \${data.customerName}\`;
-
- let messagesHtml = '';
- data.messages.forEach(msg => {
- const time = new Date(msg.timestamp).toLocaleTimeString();
- messagesHtml += \`
- <div class="chat-message \${msg.sender}">
- <div class="message-sender">\${msg.sender === 'customer' ? data.customerName : 'Support Agent'}</div>
- <div class="message-bubble">\${msg.message}</div>
- <div class="message-time">\${time}</div>
- </div>
- \`;
- });
-
- modalBody.innerHTML = messagesHtml;
- modal.classList.add('active');
-
- // Scroll to bottom
- modalBody.scrollTop = modalBody.scrollHeight;
- } catch (error) {
- console.error('Error loading chat:', error);
- alert('Failed to load chat conversation');
- }
- }
-
- function closeModal() {
- document.getElementById('chatModal').classList.remove('active');
- }
-
- // Close modal when clicking outside
- document.addEventListener('click', (e) => {
- const modal = document.getElementById('chatModal');
- if (e.target === modal) {
- closeModal();
- }
- });
-
- // Load VIP alerts on page load and refresh every 30 seconds
- loadVIPAlerts();
- setInterval(loadVIPAlerts, 30000);
-
- // Zendesk Dashboard Modal Functions
- function openZendeskDashboard() {
- const modal = document.getElementById('zendeskModal');
- const iframe = document.getElementById('zendeskIframe');
- const loading = document.getElementById('zendeskLoading');
- const error = document.getElementById('zendeskError');
-
- // Show modal
- modal.classList.add('active');
- loading.style.display = 'block';
- error.classList.remove('show');
-
- // Set iframe source
- const zendeskUrl = 'https://${ZENDESK_DOMAIN}';
- iframe.src = zendeskUrl;
-
- // Handle iframe load
- iframe.onload = function() {
- loading.style.display = 'none';
- };
-
- // Handle iframe error (timeout after 10 seconds)
- let loadTimeout = setTimeout(function() {
- if (loading.style.display !== 'none') {
- loading.style.display = 'none';
- error.classList.add('show');
- }
- }, 10000);
-
- iframe.onerror = function() {
- clearTimeout(loadTimeout);
- loading.style.display = 'none';
- error.classList.add('show');
- };
- }
-
- function closeZendeskModal() {
- const modal = document.getElementById('zendeskModal');
- const iframe = document.getElementById('zendeskIframe');
- modal.classList.remove('active');
-
- // Clear iframe to stop loading
- setTimeout(() => {
- iframe.src = 'about:blank';
- }, 300);
- }
-
- // Close Zendesk modal when clicking outside
- document.addEventListener('click', (e) => {
- const modal = document.getElementById('zendeskModal');
- if (e.target === modal) {
- closeZendeskModal();
- }
- });
-
- // Keyboard shortcuts
- document.addEventListener('keydown', (e) => {
- // ESC key closes modal
- if (e.key === 'Escape') {
- const zendeskModal = document.getElementById('zendeskModal');
- if (zendeskModal.classList.contains('active')) {
- closeZendeskModal();
- }
- }
- });
- </script>
-
- <!-- Zendesk Dashboard Modal -->
- <div id="zendeskModal" class="zendesk-modal">
- <div class="zendesk-modal-content">
- <div class="zendesk-modal-header">
- <h2>
- <span>💬</span>
- <span>Zendesk Chat Dashboard</span>
- </h2>
- <button class="zendesk-modal-close" onclick="closeZendeskModal()" title="Close (ESC)">×</button>
- </div>
- <div class="zendesk-modal-body">
- <div class="zendesk-iframe-container">
- <!-- Loading indicator -->
- <div id="zendeskLoading" class="zendesk-loading">
- <div class="zendesk-spinner"></div>
- <p style="color: #666; font-size: 16px;">Loading Zendesk Dashboard...</p>
- </div>
-
- <!-- Error message -->
- <div id="zendeskError" class="zendesk-error">
- <div class="zendesk-error-icon">⚠️</div>
- <h3>Unable to Load Zendesk Dashboard</h3>
- <p>The Zendesk dashboard could not be loaded in the iframe. This may be due to security restrictions.</p>
- <p style="margin-bottom: 20px;">
- <strong>Possible reasons:</strong><br>
- • Zendesk may block embedding in iframes (X-Frame-Options)<br>
- • You may need to authenticate first<br>
- • Network connectivity issues
- </p>
- <a href="https://${ZENDESK_DOMAIN}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block; margin-right: 10px;">
- Open in New Tab
- </a>
- <button class="action-button" onclick="closeZendeskModal()" style="background: #6b7280;">
- Close
- </button>
- </div>
-
- <!-- Iframe -->
- <iframe
- id="zendeskIframe"
- class="zendesk-iframe"
- sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
- title="Zendesk Chat Dashboard"
- ></iframe>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Chat Modal -->
- <div id="chatModal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h2 id="modalTitle">Chat Conversation</h2>
- <button class="modal-close" onclick="closeModal()">×</button>
- </div>
- <div class="modal-body" id="modalBody">
- <!-- Messages will be loaded here -->
- </div>
- </div>
- </div>
- </body>
- </html>
- `);
-});
-
-// API Endpoints
-
-// Chat endpoint
-app.post('/api/chat', requireAuth, async (req: Request, res: Response) => {
- try {
- const { message } = req.body;
- stats.totalQueries++;
-
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- req.session.chatHistory.push({
- role: 'user',
- content: message
- });
-
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- const liveConversations = await getConversations();
- const activeChats = liveConversations.filter((c: ChatConversation) => c.status === 'active').length;
- const pendingChats = liveConversations.filter((c: ChatConversation) => c.status === 'pending').length;
-
- const systemPrompt = `You are the Zendesk Chat Support Agent for Designer Wallcoverings, powered by Claude 4 Sonnet.
-
-Your role is to assist customer support team with:
-
-CAPABILITIES:
-- Suggest professional and empathetic customer responses
-- Provide customer support best practices
-- Help manage chat conversations
-- Offer conflict resolution strategies
-- Zendesk platform tips and workflows
-- Response templates for common scenarios
-
-CONTEXT:
-- Company: Designer Wallcoverings (wallcovering/wallpaper retailer)
-- Zendesk Domain: ${ZENDESK_DOMAIN}
-- Current Active Chats: ${activeChats}
-- Pending Chats: ${pendingChats}
-
-COMMON CUSTOMER INQUIRIES:
-- Product information and availability
-- Installation help and tips
-- Order tracking and status
-- Return and exchange policies
-- Design consultation
-- Sample requests
-
-When helping with responses:
-1. Be empathetic and professional
-2. Provide clear, actionable information
-3. Include product knowledge when relevant
-4. Maintain Designer Wallcoverings' brand voice
-5. Suggest follow-up actions
-
-Be helpful, supportive, and provide excellent customer service guidance.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 1024,
- system: systemPrompt,
- messages: req.session.chatHistory
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error processing your request.';
-
- req.session.chatHistory.push({
- role: 'assistant',
- content: assistantMessage
- });
-
- logActivity('Chat Query', `User asked: "${message.substring(0, 50)}..."`);
-
- res.json({ response: assistantMessage });
- } catch (error) {
- console.error('Chat error:', error);
- res.status(500).json({ error: 'Failed to process chat message' });
- }
-});
-
-// Get conversations
-app.get('/api/conversations', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- res.json({ conversations: liveConversations });
-});
-
-// Get specific conversation by ID
-app.get('/api/conversation/:id', requireAuth, async (req: Request, res: Response) => {
- const chatId = req.params.id;
- const liveConversations = await getConversations();
- let conversation = liveConversations.find(c => c.id === chatId);
-
- if (!conversation) {
- return res.status(404).json({ error: 'Conversation not found' });
- }
-
- // Fetch comments if not already loaded
- if (conversation.messages.length === 0) {
- conversation.messages = await fetchTicketComments(chatId);
- }
-
- logActivity('View Chat', `Viewed conversation ${chatId} with ${conversation.customerName}`);
- res.json(conversation);
-});
-
-// Get statistics
-app.get('/api/stats', requireAuth, (req: Request, res: Response) => {
- res.json(stats);
-});
-
-// Get VIP alerts
-app.get('/api/vip-alerts', requireAuth, (req: Request, res: Response) => {
- res.json({ alerts: vipAlerts });
-});
-
-// Get activity log
-app.get('/api/activity', requireAuth, (req: Request, res: Response) => {
- res.json({ activities: activityLog });
-});
-
-// Start server
-app.listen(PORT, '0.0.0.0', () => {
- console.log(`💬 Zendesk Chat Agent running on port ${PORT}`);
- console.log(`Dashboard: http://45.61.58.125:${PORT}`);
- console.log(`Zendesk: ${ZENDESK_CHAT_URL}`);
- logActivity('System Start', 'Zendesk Chat Agent initialized');
-});
diff --git a/DW-Agents/dw-agents/zendesk-chat-agent.ts.backup-1762549988 b/DW-Agents/dw-agents/zendesk-chat-agent.ts.backup-1762549988
deleted file mode 100644
index 09f1c6ba..00000000
--- a/DW-Agents/dw-agents/zendesk-chat-agent.ts.backup-1762549988
+++ /dev/null
@@ -1,1459 +0,0 @@
-/**
- * DW-Agents: Zendesk Chat Agent
- *
- * Customer support chat integration system
- * - Zendesk chat monitoring and management
- * - AI-powered response suggestions
- * - Customer conversation history
- * - Real-time chat analytics
- * - Chat interface for support queries
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express, { Request, Response } from 'express';
-import session from 'express-session';
-import cookieParser from 'cookie-parser';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import fetch from 'node-fetch';
-
-const app = express();
-const PORT = process.env.PORT || 9712;
-
-// Anthropic AI client
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY || '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',
-});
-
-// Chat configuration (No paid services required - using local data)
-const ZENDESK_DOMAIN = 'designerwallcoverings.zendesk.com';
-const ZENDESK_CHAT_URL = `https://${ZENDESK_DOMAIN}/chat/agent`;
-
-// Local chat data storage (replaces Zendesk API)
-const USE_DEMO_DATA = true; // Always use demo data since we're not paying for Zendesk
-
-// Middleware
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-// Session configuration with extended TypeScript types
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-app.use(
- session({
- secret: 'dw-agents-zendesk-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days - longer session to avoid re-login
- },
- })
-);
-
-// Authentication middleware with SSO support
-const requireAuth = (req: Request, res: Response, next: any) => {
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
-};
-
-// Statistics tracking
-const stats = {
- totalQueries: 0,
- activeChats: 0,
- resolvedChats: 0,
- avgResponseTime: '2.3 min',
- customerSatisfaction: '94%',
- lastActivity: new Date(),
-};
-
-// Sample chat data
-interface ChatMessage {
- sender: 'customer' | 'agent';
- message: string;
- timestamp: Date;
-}
-
-interface ChatConversation {
- id: string;
- zendeskId?: number;
- customerName: string;
- customerEmail: string;
- status: 'active' | 'pending' | 'resolved';
- priority: 'high' | 'normal' | 'low';
- subject: string;
- lastMessage: string;
- timestamp: Date;
- tags: string[];
- messages: ChatMessage[];
-}
-
-// Zendesk API helper functions
-async function fetchZendeskTickets(): Promise<ChatConversation[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?sort_by=updated_at&sort_order=desc&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk API error:', response.status, await response.text());
- return [];
- }
-
- const data: any = await response.json();
- const tickets = data.tickets || [];
-
- // Fetch users to get names and emails
- const userIds = [...new Set(tickets.map((t: any) => t.requester_id))];
- const users = await fetchZendeskUsers(userIds);
-
- const conversations: ChatConversation[] = tickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- zendeskId: ticket.id,
- customerName: user?.name || 'Unknown Customer',
- customerEmail: user?.email || 'unknown@email.com',
- status: ticket.status === 'new' || ticket.status === 'open' ? 'active' :
- ticket.status === 'pending' ? 'pending' : 'resolved',
- priority: ticket.priority === 'urgent' || ticket.priority === 'high' ? 'high' :
- ticket.priority === 'low' ? 'low' : 'normal',
- subject: ticket.subject || 'No Subject',
- lastMessage: ticket.description?.substring(0, 100) || 'No message',
- timestamp: new Date(ticket.updated_at),
- tags: ticket.tags || [],
- messages: [], // Will be loaded on demand
- };
- });
-
- return conversations;
- } catch (error) {
- console.error('Error fetching Zendesk tickets:', error);
- return [];
- }
-}
-
-async function fetchZendeskUsers(userIds: number[]): Promise<any[]> {
- try {
- if (userIds.length === 0) return [];
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/users/show_many.json?ids=${userIds.join(',')}`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk users API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- return data.users || [];
- } catch (error) {
- console.error('Error fetching Zendesk users:', error);
- return [];
- }
-}
-
-async function fetchTicketComments(ticketId: string): Promise<ChatMessage[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets/${ticketId}/comments.json`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk comments API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- const comments = data.comments || [];
-
- return comments.map((comment: any) => ({
- sender: comment.public ? 'agent' : 'customer',
- message: comment.body || '',
- timestamp: new Date(comment.created_at),
- }));
- } catch (error) {
- console.error('Error fetching ticket comments:', error);
- return [];
- }
-}
-
-// Fetch currently online visitors/chats from Zendesk Chat
-interface OnlineVisitor {
- id: string;
- name: string;
- email: string;
- status: 'browsing' | 'chatting' | 'waiting';
- pageUrl?: string;
- location?: string;
- timestamp: Date;
-}
-
-async function fetchOnlineVisitors(): Promise<OnlineVisitor[]> {
- try {
- // Zendesk Chat uses different auth - OAuth token
- // For now, we'll try to get active chats from the Support API
- // by looking for tickets created in the last hour with specific tags
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
-
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?created_at_gt=${oneHourAgo}&status=new,open&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk online visitors API error:', response.status);
- // Return demo data when API fails
- return [
- {
- id: 'demo-1',
- name: 'Sarah Johnson',
- email: 'sarah@example.com',
- status: 'chatting' as const,
- pageUrl: 'https://designerwallcoverings.com/wallpaper/floral',
- location: 'New York, USA',
- timestamp: new Date(Date.now() - 5 * 60 * 1000) // 5 min ago
- },
- {
- id: 'demo-2',
- name: 'Mike Chen',
- email: 'mike@example.com',
- status: 'waiting' as const,
- pageUrl: 'https://designerwallcoverings.com/samples',
- location: 'Los Angeles, USA',
- timestamp: new Date(Date.now() - 2 * 60 * 1000) // 2 min ago
- },
- {
- id: 'demo-3',
- name: 'Emily Rodriguez',
- email: 'emily@example.com',
- status: 'browsing' as const,
- pageUrl: 'https://designerwallcoverings.com/about',
- location: 'Chicago, USA',
- timestamp: new Date(Date.now() - 1 * 60 * 1000) // 1 min ago
- }
- ];
- }
-
- const data: any = await response.json();
- const recentTickets = data.tickets || [];
-
- // Fetch users for these tickets
- const userIds = [...new Set(recentTickets.map((t: any) => t.requester_id))];
- const users = await fetchZendeskUsers(userIds);
-
- const visitors: OnlineVisitor[] = recentTickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- name: user?.name || 'Anonymous Visitor',
- email: user?.email || 'visitor@unknown.com',
- status: ticket.status === 'new' ? 'waiting' : 'chatting',
- pageUrl: ticket.custom_fields?.find((f: any) => f.id === 'url')?.value || 'Unknown page',
- location: user?.time_zone || 'Unknown',
- timestamp: new Date(ticket.created_at)
- };
- });
-
- return visitors;
- } catch (error) {
- console.error('Error fetching online visitors:', error);
- return [];
- }
-}
-
-// VIP detection - Fortune 500 and high-value domains
-const VIP_INDICATORS = [
- // Fortune 500 Companies
- 'apple.com', 'microsoft.com', 'amazon.com', 'google.com', 'meta.com', 'facebook.com',
- 'walmart.com', 'exxonmobil.com', 'berkshirehathaway.com', 'unitedhealth.com',
- 'jpmorgan.com', 'jpmorganchase.com', 'cvs.com', 'tesla.com', 'alphabet.com',
- // Hospitality & Hotels
- 'marriott.com', 'hilton.com', 'hyatt.com', 'ihg.com', 'fourseasons.com', 'ritzcarlton.com',
- 'starwood.com', 'accor.com', 'wyndham.com', 'choicehotels.com',
- // Interior Design Firms
- 'gensler.com', 'perkinswill.com', 'hok.com', 'som.com', 'kpf.com',
- // Architecture Firms
- 'aecom.com', 'stantec.com', 'hdr.com', 'jacobs.com',
- // High-end Retailers
- 'nordstrom.com', 'bloomingdales.com', 'saks.com', 'neiman-marcus.com', 'bergdorf.com',
- // Commercial Real Estate
- 'cbre.com', 'jll.com', 'cushmanwakefield.com', 'colliers.com',
- // Restaurants & Hospitality
- 'mcdonalds.com', 'starbucks.com', 'chipotle.com', 'darden.com',
- // Government & Education
- '.gov', '.edu', 'harvard.edu', 'stanford.edu', 'mit.edu',
-];
-
-interface VIPAlert {
- company: string;
- email: string;
- name: string;
- reason: string;
- timestamp: Date;
-}
-
-let vipAlerts: VIPAlert[] = [];
-
-function detectVIP(email: string, name: string): VIPAlert | null {
- const emailLower = email.toLowerCase();
- const nameLower = name.toLowerCase();
-
- // Check email domain
- for (const indicator of VIP_INDICATORS) {
- if (emailLower.includes(indicator)) {
- const company = emailLower.split('@')[1] || indicator;
- return {
- company: company.replace(/\.(com|net|org|edu|gov)$/, ''),
- email,
- name,
- reason: `High-value domain detected: ${company}`,
- timestamp: new Date(),
- };
- }
- }
-
- // Check for corporate keywords in name or email
- const corporateKeywords = ['hotel', 'resort', 'design', 'architect', 'interior', 'commercial', 'property', 'development'];
- for (const keyword of corporateKeywords) {
- if (emailLower.includes(keyword) || nameLower.includes(keyword)) {
- return {
- company: email.split('@')[1] || 'Corporate',
- email,
- name,
- reason: `Corporate keyword detected: ${keyword}`,
- timestamp: new Date(),
- };
- }
- }
-
- return null;
-}
-
-// Cache for conversations
-let conversationsCache: ChatConversation[] = [];
-let lastFetchTime = 0;
-const CACHE_DURATION = 30000; // 30 seconds
-
-let onlineVisitorsCache: OnlineVisitor[] = [];
-let lastVisitorFetchTime = 0;
-
-async function getConversations(): Promise<ChatConversation[]> {
- const now = Date.now();
- if (now - lastFetchTime > CACHE_DURATION) {
- conversationsCache = await fetchZendeskTickets();
- lastFetchTime = now;
-
- // Check for VIP contacts
- conversationsCache.forEach(conv => {
- const vip = detectVIP(conv.customerEmail, conv.customerName);
- if (vip) {
- // Only add if not already in alerts
- if (!vipAlerts.find(a => a.email === vip.email && a.timestamp > new Date(Date.now() - 3600000))) {
- vipAlerts.unshift(vip);
- vipAlerts = vipAlerts.slice(0, 10); // Keep last 10
- }
- }
- });
- }
- return conversationsCache;
-}
-
-async function getOnlineVisitors(): Promise<OnlineVisitor[]> {
- const now = Date.now();
- if (now - lastVisitorFetchTime > CACHE_DURATION) {
- onlineVisitorsCache = await fetchOnlineVisitors();
- lastVisitorFetchTime = now;
- }
- return onlineVisitorsCache;
-}
-
-// NO MOCK DATA - All conversations are loaded from Zendesk API in real-time
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- action: string;
- details: string;
-}> = [];
-
-function logActivity(action: string, details: string) {
- activityLog.unshift({
- timestamp: new Date(),
- action,
- details,
- });
- if (activityLog.length > 50) {
- activityLog.pop();
- }
- stats.lastActivity = new Date();
-}
-
-// Routes
-
-// Login page
-app.get('/login', (req: Request, res: Response) => {
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Login</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 16px;
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
- width: 100%;
- max-width: 400px;
- }
- h1 {
- color: #333;
- font-size: 28px;
- margin-bottom: 10px;
- text-align: center;
- }
- .subtitle {
- color: #666;
- text-align: center;
- margin-bottom: 30px;
- font-size: 14px;
- }
- .form-group {
- margin-bottom: 20px;
- }
- label {
- display: block;
- color: #555;
- margin-bottom: 8px;
- font-weight: 500;
- }
- input {
- width: 100%;
- padding: 12px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- font-size: 16px;
- transition: border-color 0.3s;
- }
- input:focus {
- outline: none;
- border-color: #03a9f4;
- }
- button {
- width: 100%;
- padding: 14px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- button:hover {
- transform: translateY(-2px);
- }
- .agent-badge {
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 12px;
- text-align: center;
- margin-bottom: 20px;
- font-weight: 600;
- }
- </style>
- </head>
- <body>
- <div class="login-container">
- <div class="agent-badge">💬 ZENDESK CHAT AGENT</div>
- <h1>Customer Support</h1>
- <p class="subtitle">DW-Agents System</p>
- <form method="POST" action="/login">
- <div class="form-group">
- <label for="username">Username</label>
- <input type="text" id="username" name="username" required autocomplete="username">
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" id="password" name="password" required autocomplete="current-password">
- </div>
- <button type="submit">Access Dashboard</button>
- </form>
- </div>
- </body>
- </html>
- `);
-});
-
-// Login handler
-app.post('/login', (req: Request, res: Response) => {
- const { username, password } = req.body;
-
- if (username === 'admin' && password === '2025') {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- req.session.save((err) => {
- if (err) {
- console.error('Session save error:', err);
- }
- res.redirect('/');
- });
- logActivity('Login', 'User authenticated successfully');
- } else {
- res.redirect('/login');
- }
-});
-
-// Main dashboard
-app.get('/', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- const onlineVisitors = await getOnlineVisitors();
- const displayConversations = liveConversations; // NO MOCK DATA - only show real Zendesk tickets
- const isLiveData = liveConversations.length > 0;
-
- const activeChats = displayConversations.filter(c => c.status === 'active').length;
- const pendingChats = displayConversations.filter(c => c.status === 'pending').length;
- const resolvedChats = displayConversations.filter(c => c.status === 'resolved').length;
- const onlineCount = onlineVisitors.length;
-
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Dashboard</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- padding: 20px;
- }
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .header h1 {
- color: #333;
- font-size: 32px;
- margin-bottom: 10px;
- }
- .agent-status {
- display: inline-block;
- background: #10b981;
- color: white;
- padding: 6px 12px;
- border-radius: 20px;
- font-size: 14px;
- font-weight: 600;
- }
- .zendesk-link {
- display: inline-block;
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 8px;
- text-decoration: none;
- margin-left: 15px;
- font-size: 14px;
- transition: background 0.3s;
- }
- .zendesk-link:hover {
- background: #0288d1;
- }
- .tabs {
- display: flex;
- gap: 10px;
- margin-bottom: 20px;
- }
- .tab-button {
- background: white;
- color: #03a9f4;
- padding: 12px 24px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-size: 16px;
- font-weight: 600;
- transition: all 0.3s;
- }
- .tab-button:hover {
- background: #f3f4f6;
- }
- .tab-button.active {
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- }
- .tab-content {
- display: none;
- }
- .tab-content.active {
- display: block;
- }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stat-card {
- background: white;
- padding: 24px;
- border-radius: 12px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .stat-value {
- font-size: 32px;
- font-weight: 700;
- color: #03a9f4;
- margin-bottom: 5px;
- }
- .stat-label {
- color: #666;
- font-size: 14px;
- }
- .section {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .section h2 {
- color: #333;
- font-size: 24px;
- margin-bottom: 20px;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .chat-container {
- display: flex;
- flex-direction: column;
- height: 600px;
- }
- .chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 20px;
- background: #f9fafb;
- border-radius: 12px;
- margin-bottom: 20px;
- }
- .message {
- margin-bottom: 16px;
- padding: 12px 16px;
- border-radius: 12px;
- max-width: 80%;
- }
- .message.user {
- background: #03a9f4;
- color: white;
- margin-left: auto;
- }
- .message.assistant {
- background: white;
- color: #333;
- border: 1px solid #e0e0e0;
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- .chat-input {
- flex: 1;
- padding: 14px;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- font-size: 16px;
- font-family: inherit;
- }
- .send-button {
- padding: 14px 28px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 12px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- .send-button:hover {
- transform: translateY(-2px);
- }
- .suggestion-chips {
- display: flex;
- gap: 10px;
- margin-bottom: 15px;
- flex-wrap: wrap;
- }
- .chip {
- background: #f3f4f6;
- color: #03a9f4;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 14px;
- cursor: pointer;
- border: 2px solid transparent;
- transition: all 0.3s;
- }
- .chip:hover {
- background: #03a9f4;
- color: white;
- border-color: #03a9f4;
- }
- .conversation-card {
- background: white;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 15px;
- transition: all 0.3s;
- }
- .conversation-card:hover {
- border-color: #03a9f4;
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(3, 169, 244, 0.1);
- }
- .conversation-header {
- display: flex;
- justify-content: space-between;
- align-items: start;
- margin-bottom: 10px;
- }
- .customer-name {
- font-size: 18px;
- font-weight: 600;
- color: #333;
- }
- .status-badge {
- padding: 4px 12px;
- border-radius: 12px;
- font-size: 12px;
- font-weight: 600;
- }
- .status-active { background: #d1fae5; color: #065f46; }
- .status-pending { background: #fef3c7; color: #92400e; }
- .status-resolved { background: #dbeafe; color: #1e40af; }
- .priority-high { background: #fee2e2; color: #991b1b; }
- .priority-normal { background: #e0e0e0; color: #666; }
- .priority-low { background: #f3f4f6; color: #888; }
- .conversation-subject {
- color: #03a9f4;
- font-size: 14px;
- font-weight: 500;
- margin-bottom: 8px;
- }
- .last-message {
- color: #666;
- font-size: 14px;
- line-height: 1.5;
- margin-bottom: 10px;
- }
- .conversation-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 10px;
- border-top: 1px solid #e0e0e0;
- }
- .timestamp {
- font-size: 12px;
- color: #888;
- }
- .action-button {
- padding: 6px 16px;
- background: #03a9f4;
- color: white;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 14px;
- transition: background 0.3s;
- }
- .action-button:hover {
- background: #0288d1;
- }
- .modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0,0,0,0.7);
- z-index: 1000;
- align-items: center;
- justify-content: center;
- }
- .modal.active {
- display: flex;
- }
- .modal-content {
- background: white;
- border-radius: 16px;
- width: 90%;
- max-width: 800px;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 25px 50px rgba(0,0,0,0.5);
- }
- .modal-header {
- padding: 25px 30px;
- border-bottom: 2px solid #e0e0e0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .modal-header h2 {
- color: #03a9f4;
- margin: 0;
- font-size: 22px;
- }
- .modal-close {
- background: #ef4444;
- color: white;
- border: none;
- width: 36px;
- height: 36px;
- border-radius: 50%;
- cursor: pointer;
- font-size: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.3s;
- }
- .modal-close:hover {
- background: #dc2626;
- }
- .modal-body {
- padding: 25px 30px;
- overflow-y: auto;
- flex: 1;
- }
- .chat-message {
- margin-bottom: 20px;
- display: flex;
- flex-direction: column;
- }
- .chat-message.customer {
- align-items: flex-start;
- }
- .chat-message.agent {
- align-items: flex-end;
- }
- .message-sender {
- font-size: 12px;
- font-weight: 600;
- color: #666;
- margin-bottom: 6px;
- text-transform: uppercase;
- }
- .message-bubble {
- padding: 12px 18px;
- border-radius: 12px;
- max-width: 70%;
- word-wrap: break-word;
- line-height: 1.5;
- }
- .chat-message.customer .message-bubble {
- background: #f3f4f6;
- color: #333;
- border-bottom-left-radius: 4px;
- }
- .chat-message.agent .message-bubble {
- background: #03a9f4;
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message-time {
- font-size: 11px;
- color: #888;
- margin-top: 4px;
- }
- @keyframes pulse {
- 0%, 100% { transform: scale(1); box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); }
- 50% { transform: scale(1.02); box-shadow: 0 15px 50px rgba(255, 107, 107, 0.5); }
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>💬 Customer Chat Monitor</h1>
- <p style="color: #666; margin-top: 5px;">Free Customer Support & Chat Management (No paid services required)</p>
- <div style="margin-top: 15px; display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
- <span class="agent-status">● ONLINE</span>
- <span style="color: #888; font-size: 14px;">Port 9884</span>
- <a href="${ZENDESK_CHAT_URL}" target="_blank" class="zendesk-link">🔗 Open Zendesk Chat</a>
- <button onclick="location.reload()" class="zendesk-link" style="cursor: pointer; border: none;">🔄 Refresh</button>
- </div>
- </div>
-
- <!-- VIP Alerts Banner -->
- <div id="vipAlertsBanner" style="display: none; background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); border: 3px solid #ff6b6b; border-radius: 15px; padding: 20px; margin: 20px 0; box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); animation: pulse 2s infinite;">
- <div style="display: flex; align-items: center; gap: 15px;">
- <div style="font-size: 3em;">🚨</div>
- <div style="flex: 1;">
- <h2 style="color: #d32f2f; margin: 0 0 10px 0; font-size: 1.5em;">⚡ HIGH-VALUE OPPORTUNITY DETECTED</h2>
- <div id="vipAlertsContent"></div>
- </div>
- </div>
- </div>
-
- <div class="tabs">
- <button class="tab-button active" onclick="switchTab('overview')">📊 Overview</button>
- <button class="tab-button" onclick="switchTab('chat')">💬 AI Assistant</button>
- <button class="tab-button" onclick="switchTab('conversations')">💭 Active Chats</button>
- <button class="tab-button" onclick="switchTab('analytics')">📈 Analytics</button>
- </div>
-
- <!-- Overview Tab -->
- <div id="tab-overview" class="tab-content active">
- <div class="stats-grid">
- <div class="stat-card" style="border-left: 4px solid #10b981;">
- <div class="stat-value" style="color: #10b981;">${onlineCount}</div>
- <div class="stat-label">🟢 Currently Online</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${activeChats}</div>
- <div class="stat-label">Active Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${pendingChats}</div>
- <div class="stat-label">Pending Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${resolvedChats}</div>
- <div class="stat-label">Resolved Today</div>
- </div>
- </div>
-
- <div class="section" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; padding: 25px; border-radius: 16px; margin-bottom: 20px;">
- <h2 style="color: white; margin-bottom: 15px;">🟢 Currently Online (${onlineCount})</h2>
- ${onlineVisitors.length > 0 ? onlineVisitors.map(visitor => `
- <div style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; margin-bottom: 10px; border-left: 4px solid ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'};">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
- <strong style="font-size: 1.1em;">${visitor.name}</strong>
- <span style="background: ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'}; color: white; padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: 600;">
- ${visitor.status === 'waiting' ? '⏳ Waiting' : visitor.status === 'chatting' ? '💬 Chatting' : '👀 Browsing'}
- </span>
- </div>
- <div style="font-size: 0.9em; opacity: 0.95;">
- 📧 ${visitor.email}<br>
- ${visitor.pageUrl ? `📄 ${visitor.pageUrl}<br>` : ''}
- 🕐 ${new Date(visitor.timestamp).toLocaleTimeString()}
- </div>
- </div>
- `).join('') : '<p style="text-align: center; padding: 20px; opacity: 0.9;">No visitors currently online</p>'}
- </div>
-
- <div class="section">
- <h2>💭 Recent Conversations ${isLiveData ? '<span style="color: #28a745; font-size: 0.8em;">● LIVE</span>' : '<span style="color: #ffc107; font-size: 0.8em;">● DEMO</span>'}</h2>
- <div id="conversationsList">
- ${displayConversations.slice(0, 3).map(conv => `
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-${conv.status}">${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-${conv.priority}">${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">${conv.subject}</div>
- <div class="last-message">${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- `).join('')}
- </div>
- </div>
-
- <div class="section">
- <h2>🔗 Zendesk Integration</h2>
- <p style="color: #666; margin-bottom: 15px;">Connected to: <strong>${ZENDESK_DOMAIN}</strong></p>
- <p style="color: #666; margin-bottom: 15px;">Account: <strong>${ZENDESK_EMAIL}</strong></p>
- <a href="${ZENDESK_CHAT_URL}" target="_blank" class="action-button" style="display: inline-block; text-decoration: none;">Open Zendesk Chat Dashboard</a>
- </div>
- </div>
-
- <!-- Chat Tab -->
- <div id="tab-chat" class="tab-content">
- <div class="section">
- <h2>💬 AI Support Assistant</h2>
- <p style="color: #666; margin-bottom: 20px;">Get AI-powered help with customer support, chat responses, and Zendesk management</p>
-
- <div class="suggestion-chips">
- <span class="chip" onclick="sendSuggestion('Suggest response for installation question')">Installation help</span>
- <span class="chip" onclick="sendSuggestion('How to handle product availability inquiry')">Product availability</span>
- <span class="chip" onclick="sendSuggestion('Best practices for customer support')">Support best practices</span>
- <span class="chip" onclick="sendSuggestion('Generate empathetic response template')">Response templates</span>
- </div>
-
- <div class="chat-container">
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- Hello! I'm your Zendesk Chat AI Assistant powered by Claude 4. I can help you with:
- <br><br>
- • Suggesting responses to customer inquiries<br>
- • Customer support best practices<br>
- • Chat management strategies<br>
- • Handling difficult conversations<br>
- • Zendesk tips and workflows<br>
- <br>
- How can I assist you with customer support today?
- </div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- class="chat-input"
- id="chatInput"
- placeholder="Ask about customer support, chat responses..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-button" onclick="sendMessage()">Send</button>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Conversations Tab -->
- <div id="tab-conversations" class="tab-content">
- <div class="section">
- <h2>💭 All Conversations</h2>
- <div id="allConversations">
- <p style="color: #888;">Loading conversations...</p>
- </div>
- </div>
- </div>
-
- <!-- Analytics Tab -->
- <div id="tab-analytics" class="tab-content">
- <div class="section">
- <h2>📈 Chat Analytics</h2>
- <div class="stats-grid">
- <div class="stat-card">
- <div class="stat-value">${stats.customerSatisfaction}</div>
- <div class="stat-label">Customer Satisfaction</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.totalQueries}</div>
- <div class="stat-label">Total AI Queries</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${displayConversations.length}</div>
- <div class="stat-label">Total Conversations</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.avgResponseTime}</div>
- <div class="stat-label">Avg Response Time</div>
- </div>
- </div>
- <div style="background: #f9fafb; padding: 20px; border-radius: 12px; margin-top: 20px;">
- <p style="color: #666;">Connect to Zendesk Analytics API for real-time reporting and advanced metrics.</p>
- </div>
- </div>
- </div>
- </div>
-
- <script>
- function switchTab(tabName) {
- document.querySelectorAll('.tab-content').forEach(tab => {
- tab.classList.remove('active');
- });
- document.querySelectorAll('.tab-button').forEach(btn => {
- btn.classList.remove('active');
- });
-
- document.getElementById('tab-' + tabName).classList.add('active');
- event.target.classList.add('active');
-
- if (tabName === 'conversations') {
- loadConversations();
- }
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const message = input.value.trim();
-
- if (!message) return;
-
- const chatMessages = document.getElementById('chatMessages');
- chatMessages.innerHTML += \`
- <div class="message user">\${message}</div>
- \`;
-
- input.value = '';
- chatMessages.scrollTop = chatMessages.scrollHeight;
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
-
- chatMessages.innerHTML += \`
- <div class="message assistant">\${data.response.replace(/\\n/g, '<br>')}</div>
- \`;
- chatMessages.scrollTop = chatMessages.scrollHeight;
- } catch (error) {
- console.error('Chat error:', error);
- chatMessages.innerHTML += \`
- <div class="message assistant">Sorry, I encountered an error. Please try again.</div>
- \`;
- }
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function loadVIPAlerts() {
- try {
- const response = await fetch('/api/vip-alerts');
- const data = await response.json();
-
- if (data.alerts && data.alerts.length > 0) {
- const banner = document.getElementById('vipAlertsBanner');
- const content = document.getElementById('vipAlertsContent');
-
- const html = data.alerts.map(alert => \`
- <div style="background: white; padding: 15px; border-radius: 10px; margin-bottom: 10px; border-left: 5px solid #ff6b6b;">
- <div style="font-weight: 700; font-size: 1.2em; color: #d32f2f; margin-bottom: 5px;">
- \${alert.name} - \${alert.company.toUpperCase()}
- </div>
- <div style="color: #666; margin-bottom: 5px;">
- 📧 \${alert.email}
- </div>
- <div style="color: #ff6b6b; font-weight: 600; font-size: 0.9em;">
- 🎯 \${alert.reason}
- </div>
- <div style="color: #888; font-size: 0.85em; margin-top: 5px;">
- ⏰ \${new Date(alert.timestamp).toLocaleString()}
- </div>
- </div>
- \`).join('');
-
- content.innerHTML = html;
- banner.style.display = 'block';
- } else {
- document.getElementById('vipAlertsBanner').style.display = 'none';
- }
- } catch (error) {
- console.error('Error loading VIP alerts:', error);
- }
- }
-
- async function loadConversations() {
- const container = document.getElementById('allConversations');
- try {
- const response = await fetch('/api/conversations');
- const data = await response.json();
-
- let html = '';
- data.conversations.forEach(conv => {
- html += \`
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">\${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">\${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-\${conv.status}">\${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-\${conv.priority}">\${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">\${conv.subject}</div>
- <div class="last-message">\${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">\${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('\${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/\${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- \`;
- });
-
- container.innerHTML = html;
- } catch (error) {
- console.error('Load conversations error:', error);
- container.innerHTML = '<p style="color: #ef4444;">Error loading conversations</p>';
- }
- }
-
- async function viewChat(chatId) {
- try {
- const response = await fetch(\`/api/conversation/\${chatId}\`);
- const data = await response.json();
-
- const modal = document.getElementById('chatModal');
- const modalTitle = document.getElementById('modalTitle');
- const modalBody = document.getElementById('modalBody');
-
- modalTitle.textContent = \`Chat with \${data.customerName}\`;
-
- let messagesHtml = '';
- data.messages.forEach(msg => {
- const time = new Date(msg.timestamp).toLocaleTimeString();
- messagesHtml += \`
- <div class="chat-message \${msg.sender}">
- <div class="message-sender">\${msg.sender === 'customer' ? data.customerName : 'Support Agent'}</div>
- <div class="message-bubble">\${msg.message}</div>
- <div class="message-time">\${time}</div>
- </div>
- \`;
- });
-
- modalBody.innerHTML = messagesHtml;
- modal.classList.add('active');
-
- // Scroll to bottom
- modalBody.scrollTop = modalBody.scrollHeight;
- } catch (error) {
- console.error('Error loading chat:', error);
- alert('Failed to load chat conversation');
- }
- }
-
- function closeModal() {
- document.getElementById('chatModal').classList.remove('active');
- }
-
- // Close modal when clicking outside
- document.addEventListener('click', (e) => {
- const modal = document.getElementById('chatModal');
- if (e.target === modal) {
- closeModal();
- }
- });
-
- // Load VIP alerts on page load and refresh every 30 seconds
- loadVIPAlerts();
- setInterval(loadVIPAlerts, 30000);
- </script>
-
- <!-- Chat Modal -->
- <div id="chatModal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h2 id="modalTitle">Chat Conversation</h2>
- <button class="modal-close" onclick="closeModal()">×</button>
- </div>
- <div class="modal-body" id="modalBody">
- <!-- Messages will be loaded here -->
- </div>
- </div>
- </div>
- </body>
- </html>
- `);
-});
-
-// API Endpoints
-
-// Chat endpoint
-app.post('/api/chat', requireAuth, async (req: Request, res: Response) => {
- try {
- const { message } = req.body;
- stats.totalQueries++;
-
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- req.session.chatHistory.push({
- role: 'user',
- content: message
- });
-
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- const systemPrompt = `You are the Zendesk Chat Support Agent for Designer Wallcoverings, powered by Claude 4 Sonnet.
-
-Your role is to assist customer support team with:
-
-CAPABILITIES:
-- Suggest professional and empathetic customer responses
-- Provide customer support best practices
-- Help manage chat conversations
-- Offer conflict resolution strategies
-- Zendesk platform tips and workflows
-- Response templates for common scenarios
-
-CONTEXT:
-- Company: Designer Wallcoverings (wallcovering/wallpaper retailer)
-- Zendesk Domain: ${ZENDESK_DOMAIN}
-- Current Active Chats: ${displayConversations.filter(c => c.status === 'active').length}
-- Pending Chats: ${displayConversations.filter(c => c.status === 'pending').length}
-
-COMMON CUSTOMER INQUIRIES:
-- Product information and availability
-- Installation help and tips
-- Order tracking and status
-- Return and exchange policies
-- Design consultation
-- Sample requests
-
-When helping with responses:
-1. Be empathetic and professional
-2. Provide clear, actionable information
-3. Include product knowledge when relevant
-4. Maintain Designer Wallcoverings' brand voice
-5. Suggest follow-up actions
-
-Be helpful, supportive, and provide excellent customer service guidance.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 1024,
- system: systemPrompt,
- messages: req.session.chatHistory
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error processing your request.';
-
- req.session.chatHistory.push({
- role: 'assistant',
- content: assistantMessage
- });
-
- logActivity('Chat Query', `User asked: "${message.substring(0, 50)}..."`);
-
- res.json({ response: assistantMessage });
- } catch (error) {
- console.error('Chat error:', error);
- res.status(500).json({ error: 'Failed to process chat message' });
- }
-});
-
-// Get conversations
-app.get('/api/conversations', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- res.json({ conversations: liveConversations });
-});
-
-// Get specific conversation by ID
-app.get('/api/conversation/:id', requireAuth, async (req: Request, res: Response) => {
- const chatId = req.params.id;
- const liveConversations = await getConversations();
- let conversation = liveConversations.find(c => c.id === chatId);
-
- if (!conversation) {
- return res.status(404).json({ error: 'Conversation not found' });
- }
-
- // Fetch comments if not already loaded
- if (conversation.messages.length === 0) {
- conversation.messages = await fetchTicketComments(chatId);
- }
-
- logActivity('View Chat', `Viewed conversation ${chatId} with ${conversation.customerName}`);
- res.json(conversation);
-});
-
-// Get statistics
-app.get('/api/stats', requireAuth, (req: Request, res: Response) => {
- res.json(stats);
-});
-
-// Get VIP alerts
-app.get('/api/vip-alerts', requireAuth, (req: Request, res: Response) => {
- res.json({ alerts: vipAlerts });
-});
-
-// Get activity log
-app.get('/api/activity', requireAuth, (req: Request, res: Response) => {
- res.json({ activities: activityLog });
-});
-
-// Start server
-app.listen(PORT, '0.0.0.0', () => {
- console.log(`💬 Zendesk Chat Agent running on port ${PORT}`);
- console.log(`Dashboard: http://45.61.58.125:${PORT}`);
- console.log(`Zendesk: ${ZENDESK_CHAT_URL}`);
- logActivity('System Start', 'Zendesk Chat Agent initialized');
-});
diff --git a/DW-Agents/dw-agents/zendesk-chat-agent.ts.backup-pre-theme b/DW-Agents/dw-agents/zendesk-chat-agent.ts.backup-pre-theme
deleted file mode 100644
index abadcb79..00000000
--- a/DW-Agents/dw-agents/zendesk-chat-agent.ts.backup-pre-theme
+++ /dev/null
@@ -1,1739 +0,0 @@
-/**
- * DW-Agents: Zendesk Chat Agent
- *
- * Customer support chat integration system
- * - Zendesk chat monitoring and management
- * - AI-powered response suggestions
- * - Customer conversation history
- * - Real-time chat analytics
- * - Chat interface for support queries
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express, { Request, Response } from 'express';
-import session from 'express-session';
-import cookieParser from 'cookie-parser';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
-import fetch from 'node-fetch';
-import { AgentMemory } from './shared-memory-system';
-
-const app = express();
-const PORT = 9884;
-
-// Anthropic AI client
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY || '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',
-});
-
-// Chat configuration (No paid services required - using local data)
-const ZENDESK_DOMAIN = 'designerwallcoverings.zendesk.com';
-const ZENDESK_EMAIL = process.env.ZENDESK_EMAIL || 'team@designerwallcoverings.com';
-const ZENDESK_PASSWORD = process.env.ZENDESK_PASSWORD || '';
-const ZENDESK_API_URL = `https://${ZENDESK_DOMAIN}/api/v2`;
-const ZENDESK_CHAT_URL = `https://${ZENDESK_DOMAIN}/chat/agent`;
-
-// Local chat data storage (replaces Zendesk API)
-const USE_DEMO_DATA = true; // Always use demo data since we're not paying for Zendesk
-
-// Middleware
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-// Session configuration with extended TypeScript types
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-app.use(
- session({
- secret: 'dw-agents-zendesk-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days - longer session to avoid re-login
- },
- })
-);
-
-// Authentication middleware with SSO support
-const requireAuth = (req: Request, res: Response, next: any) => {
- // TEMPORARY: Auth disabled for 4 hours
- return next();
-
- /* Original auth:
-
- // Check session first
- if (req.session.authenticated) {
- return next();
- }
-
- // Check SSO cookie
- if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
- req.session.authenticated = true;
- return next();
- }
-
- res.redirect('/login');
- */
-};
-
-// Statistics tracking
-const stats = {
- totalQueries: 0,
- activeChats: 0,
- resolvedChats: 0,
- avgResponseTime: '2.3 min',
- customerSatisfaction: '94%',
- lastActivity: new Date(),
-};
-
-// Sample chat data
-interface ChatMessage {
- sender: 'customer' | 'agent';
- message: string;
- timestamp: Date;
-}
-
-interface ChatConversation {
- id: string;
- zendeskId?: number;
- customerName: string;
- customerEmail: string;
- status: 'active' | 'pending' | 'resolved';
- priority: 'high' | 'normal' | 'low';
- subject: string;
- lastMessage: string;
- timestamp: Date;
- tags: string[];
- messages: ChatMessage[];
-}
-
-// Zendesk API helper functions
-async function fetchZendeskTickets(): Promise<ChatConversation[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?sort_by=updated_at&sort_order=desc&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk API error:', response.status, await response.text());
- return [];
- }
-
- const data: any = await response.json();
- const tickets = data.tickets || [];
-
- // Fetch users to get names and emails
- const userIds = [...new Set(tickets.map((t: any) => t.requester_id).filter((id: any) => typeof id === 'number'))] as number[];
- const users = await fetchZendeskUsers(userIds);
-
- const conversations: ChatConversation[] = tickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- zendeskId: ticket.id,
- customerName: user?.name || 'Unknown Customer',
- customerEmail: user?.email || 'unknown@email.com',
- status: ticket.status === 'new' || ticket.status === 'open' ? 'active' :
- ticket.status === 'pending' ? 'pending' : 'resolved',
- priority: ticket.priority === 'urgent' || ticket.priority === 'high' ? 'high' :
- ticket.priority === 'low' ? 'low' : 'normal',
- subject: ticket.subject || 'No Subject',
- lastMessage: ticket.description?.substring(0, 100) || 'No message',
- timestamp: new Date(ticket.updated_at),
- tags: ticket.tags || [],
- messages: [], // Will be loaded on demand
- };
- });
-
- return conversations;
- } catch (error) {
- console.error('Error fetching Zendesk tickets:', error);
- return [];
- }
-}
-
-async function fetchZendeskUsers(userIds: number[]): Promise<any[]> {
- try {
- if (userIds.length === 0) return [];
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/users/show_many.json?ids=${userIds.join(',')}`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk users API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- return data.users || [];
- } catch (error) {
- console.error('Error fetching Zendesk users:', error);
- return [];
- }
-}
-
-async function fetchTicketComments(ticketId: string): Promise<ChatMessage[]> {
- try {
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const response = await fetch(`${ZENDESK_API_URL}/tickets/${ticketId}/comments.json`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk comments API error:', response.status);
- return [];
- }
-
- const data: any = await response.json();
- const comments = data.comments || [];
-
- return comments.map((comment: any) => ({
- sender: comment.public ? 'agent' : 'customer',
- message: comment.body || '',
- timestamp: new Date(comment.created_at),
- }));
- } catch (error) {
- console.error('Error fetching ticket comments:', error);
- return [];
- }
-}
-
-// Fetch currently online visitors/chats from Zendesk Chat
-interface OnlineVisitor {
- id: string;
- name: string;
- email: string;
- status: 'browsing' | 'chatting' | 'waiting';
- pageUrl?: string;
- location?: string;
- timestamp: Date;
-}
-
-async function fetchOnlineVisitors(): Promise<OnlineVisitor[]> {
- try {
- // Zendesk Chat uses different auth - OAuth token
- // For now, we'll try to get active chats from the Support API
- // by looking for tickets created in the last hour with specific tags
-
- const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
-
- const response = await fetch(`${ZENDESK_API_URL}/tickets.json?created_at_gt=${oneHourAgo}&status=new,open&per_page=50`, {
- headers: {
- 'Authorization': `Basic ${auth}`,
- 'Content-Type': 'application/json',
- },
- });
-
- if (!response.ok) {
- console.error('Zendesk online visitors API error:', response.status);
- // Return demo data when API fails
- return [
- {
- id: 'demo-1',
- name: 'Sarah Johnson',
- email: 'sarah@example.com',
- status: 'chatting' as const,
- pageUrl: 'https://designerwallcoverings.com/wallpaper/floral',
- location: 'New York, USA',
- timestamp: new Date(Date.now() - 5 * 60 * 1000) // 5 min ago
- },
- {
- id: 'demo-2',
- name: 'Mike Chen',
- email: 'mike@example.com',
- status: 'waiting' as const,
- pageUrl: 'https://designerwallcoverings.com/samples',
- location: 'Los Angeles, USA',
- timestamp: new Date(Date.now() - 2 * 60 * 1000) // 2 min ago
- },
- {
- id: 'demo-3',
- name: 'Emily Rodriguez',
- email: 'emily@example.com',
- status: 'browsing' as const,
- pageUrl: 'https://designerwallcoverings.com/about',
- location: 'Chicago, USA',
- timestamp: new Date(Date.now() - 1 * 60 * 1000) // 1 min ago
- }
- ];
- }
-
- const data: any = await response.json();
- const recentTickets = data.tickets || [];
-
- // Fetch users for these tickets
- const userIds = [...new Set(recentTickets.map((t: any) => t.requester_id).filter((id: any) => typeof id === 'number'))] as number[];
- const users = await fetchZendeskUsers(userIds);
-
- const visitors: OnlineVisitor[] = recentTickets.map((ticket: any) => {
- const user = users.find((u: any) => u.id === ticket.requester_id);
-
- return {
- id: ticket.id.toString(),
- name: user?.name || 'Anonymous Visitor',
- email: user?.email || 'visitor@unknown.com',
- status: ticket.status === 'new' ? 'waiting' : 'chatting',
- pageUrl: ticket.custom_fields?.find((f: any) => f.id === 'url')?.value || 'Unknown page',
- location: user?.time_zone || 'Unknown',
- timestamp: new Date(ticket.created_at)
- };
- });
-
- return visitors;
- } catch (error) {
- console.error('Error fetching online visitors:', error);
- return [];
- }
-}
-
-// VIP detection - Fortune 500 and high-value domains
-const VIP_INDICATORS = [
- // Fortune 500 Companies
- 'apple.com', 'microsoft.com', 'amazon.com', 'google.com', 'meta.com', 'facebook.com',
- 'walmart.com', 'exxonmobil.com', 'berkshirehathaway.com', 'unitedhealth.com',
- 'jpmorgan.com', 'jpmorganchase.com', 'cvs.com', 'tesla.com', 'alphabet.com',
- // Hospitality & Hotels
- 'marriott.com', 'hilton.com', 'hyatt.com', 'ihg.com', 'fourseasons.com', 'ritzcarlton.com',
- 'starwood.com', 'accor.com', 'wyndham.com', 'choicehotels.com',
- // Interior Design Firms
- 'gensler.com', 'perkinswill.com', 'hok.com', 'som.com', 'kpf.com',
- // Architecture Firms
- 'aecom.com', 'stantec.com', 'hdr.com', 'jacobs.com',
- // High-end Retailers
- 'nordstrom.com', 'bloomingdales.com', 'saks.com', 'neiman-marcus.com', 'bergdorf.com',
- // Commercial Real Estate
- 'cbre.com', 'jll.com', 'cushmanwakefield.com', 'colliers.com',
- // Restaurants & Hospitality
- 'mcdonalds.com', 'starbucks.com', 'chipotle.com', 'darden.com',
- // Government & Education
- '.gov', '.edu', 'harvard.edu', 'stanford.edu', 'mit.edu',
-];
-
-interface VIPAlert {
- company: string;
- email: string;
- name: string;
- reason: string;
- timestamp: Date;
-}
-
-let vipAlerts: VIPAlert[] = [];
-
-function detectVIP(email: string, name: string): VIPAlert | null {
- const emailLower = email.toLowerCase();
- const nameLower = name.toLowerCase();
-
- // Check email domain
- for (const indicator of VIP_INDICATORS) {
- if (emailLower.includes(indicator)) {
- const company = emailLower.split('@')[1] || indicator;
- return {
- company: company.replace(/\.(com|net|org|edu|gov)$/, ''),
- email,
- name,
- reason: `High-value domain detected: ${company}`,
- timestamp: new Date(),
- };
- }
- }
-
- // Check for corporate keywords in name or email
- const corporateKeywords = ['hotel', 'resort', 'design', 'architect', 'interior', 'commercial', 'property', 'development'];
- for (const keyword of corporateKeywords) {
- if (emailLower.includes(keyword) || nameLower.includes(keyword)) {
- return {
- company: email.split('@')[1] || 'Corporate',
- email,
- name,
- reason: `Corporate keyword detected: ${keyword}`,
- timestamp: new Date(),
- };
- }
- }
-
- return null;
-}
-
-// Cache for conversations
-let conversationsCache: ChatConversation[] = [];
-let lastFetchTime = 0;
-const CACHE_DURATION = 30000; // 30 seconds
-
-let onlineVisitorsCache: OnlineVisitor[] = [];
-let lastVisitorFetchTime = 0;
-
-async function getConversations(): Promise<ChatConversation[]> {
- const now = Date.now();
- if (now - lastFetchTime > CACHE_DURATION) {
- conversationsCache = await fetchZendeskTickets();
- lastFetchTime = now;
-
- // Check for VIP contacts
- conversationsCache.forEach(conv => {
- const vip = detectVIP(conv.customerEmail, conv.customerName);
- if (vip) {
- // Only add if not already in alerts
- if (!vipAlerts.find(a => a.email === vip.email && a.timestamp > new Date(Date.now() - 3600000))) {
- vipAlerts.unshift(vip);
- vipAlerts = vipAlerts.slice(0, 10); // Keep last 10
- }
- }
- });
- }
- return conversationsCache;
-}
-
-async function getOnlineVisitors(): Promise<OnlineVisitor[]> {
- const now = Date.now();
- if (now - lastVisitorFetchTime > CACHE_DURATION) {
- onlineVisitorsCache = await fetchOnlineVisitors();
- lastVisitorFetchTime = now;
- }
- return onlineVisitorsCache;
-}
-
-// NO MOCK DATA - All conversations are loaded from Zendesk API in real-time
-
-// Activity log
-const activityLog: Array<{
- timestamp: Date;
- action: string;
- details: string;
-}> = [];
-
-function logActivity(action: string, details: string) {
- activityLog.unshift({
- timestamp: new Date(),
- action,
- details,
- });
- if (activityLog.length > 50) {
- activityLog.pop();
- }
- stats.lastActivity = new Date();
-}
-
-// Routes
-
-// Login page
-app.get('/login', (req: Request, res: Response) => {
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Login</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 16px;
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
- width: 100%;
- max-width: 400px;
- }
- h1 {
- color: #333;
- font-size: 28px;
- margin-bottom: 10px;
- text-align: center;
- }
- .subtitle {
- color: #666;
- text-align: center;
- margin-bottom: 30px;
- font-size: 14px;
- }
- .form-group {
- margin-bottom: 20px;
- }
- label {
- display: block;
- color: #555;
- margin-bottom: 8px;
- font-weight: 500;
- }
- input {
- width: 100%;
- padding: 12px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- font-size: 16px;
- transition: border-color 0.3s;
- }
- input:focus {
- outline: none;
- border-color: #03a9f4;
- }
- button {
- width: 100%;
- padding: 14px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- button:hover {
- transform: translateY(-2px);
- }
- .agent-badge {
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 12px;
- text-align: center;
- margin-bottom: 20px;
- font-weight: 600;
- }
- </style>
- </head>
- <body>
- <div class="login-container">
- <div class="agent-badge">💬 ZENDESK CHAT AGENT</div>
- <h1>Customer Support</h1>
- <p class="subtitle">DW-Agents System</p>
- <form method="POST" action="/login">
- <div class="form-group">
- <label for="username">Username</label>
- <input type="text" id="username" name="username" required autocomplete="username">
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" id="password" name="password" required autocomplete="current-password">
- </div>
- <button type="submit">Access Dashboard</button>
- </form>
- </div>
- </body>
- </html>
- `);
-});
-
-// Login handler
-app.post('/login', (req: Request, res: Response) => {
- const { username, password } = req.body;
-
- if (username === 'admin' && password === '2025') {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- req.session.save((err) => {
- if (err) {
- console.error('Session save error:', err);
- }
- res.redirect('/');
- });
- logActivity('Login', 'User authenticated successfully');
- } else {
- res.redirect('/login');
- }
-});
-
-// Main dashboard
-app.get('/', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- const onlineVisitors = await getOnlineVisitors();
- const displayConversations = liveConversations; // NO MOCK DATA - only show real Zendesk tickets
- const isLiveData = liveConversations.length > 0;
-
- const activeChats = displayConversations.filter(c => c.status === 'active').length;
- const pendingChats = displayConversations.filter(c => c.status === 'pending').length;
- const resolvedChats = displayConversations.filter(c => c.status === 'resolved').length;
- const onlineCount = onlineVisitors.length;
-
- res.send(`
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zendesk Chat Agent - Dashboard</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- min-height: 100vh;
- padding: 20px;
- }
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
- .header {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .header h1 {
- color: #333;
- font-size: 32px;
- margin-bottom: 10px;
- }
- .agent-status {
- display: inline-block;
- background: #10b981;
- color: white;
- padding: 6px 12px;
- border-radius: 20px;
- font-size: 14px;
- font-weight: 600;
- }
- .zendesk-link {
- display: inline-block;
- background: #03a9f4;
- color: white;
- padding: 8px 16px;
- border-radius: 8px;
- text-decoration: none;
- margin-left: 15px;
- font-size: 14px;
- transition: background 0.3s;
- }
- .zendesk-link:hover {
- background: #0288d1;
- }
- .tabs {
- display: flex;
- gap: 10px;
- margin-bottom: 20px;
- }
- .tab-button {
- background: white;
- color: #03a9f4;
- padding: 12px 24px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-size: 16px;
- font-weight: 600;
- transition: all 0.3s;
- }
- .tab-button:hover {
- background: #f3f4f6;
- }
- .tab-button.active {
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- }
- .tab-content {
- display: none;
- }
- .tab-content.active {
- display: block;
- }
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stat-card {
- background: white;
- padding: 24px;
- border-radius: 12px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .stat-value {
- font-size: 32px;
- font-weight: 700;
- color: #03a9f4;
- margin-bottom: 5px;
- }
- .stat-label {
- color: #666;
- font-size: 14px;
- }
- .section {
- background: white;
- padding: 30px;
- border-radius: 16px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .section h2 {
- color: #333;
- font-size: 24px;
- margin-bottom: 20px;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .chat-container {
- display: flex;
- flex-direction: column;
- height: 600px;
- }
- .chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 20px;
- background: #f9fafb;
- border-radius: 12px;
- margin-bottom: 20px;
- }
- .message {
- margin-bottom: 16px;
- padding: 12px 16px;
- border-radius: 12px;
- max-width: 80%;
- }
- .message.user {
- background: #03a9f4;
- color: white;
- margin-left: auto;
- }
- .message.assistant {
- background: white;
- color: #333;
- border: 1px solid #e0e0e0;
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- .chat-input {
- flex: 1;
- padding: 14px;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- font-size: 16px;
- font-family: inherit;
- }
- .send-button {
- padding: 14px 28px;
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- color: white;
- border: none;
- border-radius: 12px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- transition: transform 0.2s;
- }
- .send-button:hover {
- transform: translateY(-2px);
- }
- .suggestion-chips {
- display: flex;
- gap: 10px;
- margin-bottom: 15px;
- flex-wrap: wrap;
- }
- .chip {
- background: #f3f4f6;
- color: #03a9f4;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 14px;
- cursor: pointer;
- border: 2px solid transparent;
- transition: all 0.3s;
- }
- .chip:hover {
- background: #03a9f4;
- color: white;
- border-color: #03a9f4;
- }
- .conversation-card {
- background: white;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 15px;
- transition: all 0.3s;
- }
- .conversation-card:hover {
- border-color: #03a9f4;
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(3, 169, 244, 0.1);
- }
- .conversation-header {
- display: flex;
- justify-content: space-between;
- align-items: start;
- margin-bottom: 10px;
- }
- .customer-name {
- font-size: 18px;
- font-weight: 600;
- color: #333;
- }
- .status-badge {
- padding: 4px 12px;
- border-radius: 12px;
- font-size: 12px;
- font-weight: 600;
- }
- .status-active { background: #d1fae5; color: #065f46; }
- .status-pending { background: #fef3c7; color: #92400e; }
- .status-resolved { background: #dbeafe; color: #1e40af; }
- .priority-high { background: #fee2e2; color: #991b1b; }
- .priority-normal { background: #e0e0e0; color: #666; }
- .priority-low { background: #f3f4f6; color: #888; }
- .conversation-subject {
- color: #03a9f4;
- font-size: 14px;
- font-weight: 500;
- margin-bottom: 8px;
- }
- .last-message {
- color: #666;
- font-size: 14px;
- line-height: 1.5;
- margin-bottom: 10px;
- }
- .conversation-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 10px;
- border-top: 1px solid #e0e0e0;
- }
- .timestamp {
- font-size: 12px;
- color: #888;
- }
- .action-button {
- padding: 6px 16px;
- background: #03a9f4;
- color: white;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 14px;
- transition: background 0.3s;
- }
- .action-button:hover {
- background: #0288d1;
- }
- .modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0,0,0,0.7);
- z-index: 1000;
- align-items: center;
- justify-content: center;
- }
- .modal.active {
- display: flex;
- }
- .modal-content {
- background: white;
- border-radius: 16px;
- width: 90%;
- max-width: 800px;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 25px 50px rgba(0,0,0,0.5);
- }
- .modal-header {
- padding: 25px 30px;
- border-bottom: 2px solid #e0e0e0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .modal-header h2 {
- color: #03a9f4;
- margin: 0;
- font-size: 22px;
- }
- .modal-close {
- background: #ef4444;
- color: white;
- border: none;
- width: 36px;
- height: 36px;
- border-radius: 50%;
- cursor: pointer;
- font-size: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.3s;
- }
- .modal-close:hover {
- background: #dc2626;
- }
- .modal-body {
- padding: 25px 30px;
- overflow-y: auto;
- flex: 1;
- }
- .chat-message {
- margin-bottom: 20px;
- display: flex;
- flex-direction: column;
- }
- .chat-message.customer {
- align-items: flex-start;
- }
- .chat-message.agent {
- align-items: flex-end;
- }
- .message-sender {
- font-size: 12px;
- font-weight: 600;
- color: #666;
- margin-bottom: 6px;
- text-transform: uppercase;
- }
- .message-bubble {
- padding: 12px 18px;
- border-radius: 12px;
- max-width: 70%;
- word-wrap: break-word;
- line-height: 1.5;
- }
- .chat-message.customer .message-bubble {
- background: #f3f4f6;
- color: #333;
- border-bottom-left-radius: 4px;
- }
- .chat-message.agent .message-bubble {
- background: #03a9f4;
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message-time {
- font-size: 11px;
- color: #888;
- margin-top: 4px;
- }
- @keyframes pulse {
- 0%, 100% { transform: scale(1); box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); }
- 50% { transform: scale(1.02); box-shadow: 0 15px 50px rgba(255, 107, 107, 0.5); }
- }
- .zendesk-modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.85);
- z-index: 2000;
- align-items: center;
- justify-content: center;
- animation: fadeIn 0.3s ease-out;
- }
- .zendesk-modal.active {
- display: flex;
- }
- .zendesk-modal-content {
- background: white;
- border-radius: 16px;
- width: 95%;
- height: 95%;
- max-width: 1600px;
- max-height: 95vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
- position: relative;
- overflow: hidden;
- }
- .zendesk-modal-header {
- background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
- padding: 20px 30px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 2px solid #0288d1;
- }
- .zendesk-modal-header h2 {
- color: white;
- margin: 0;
- font-size: 22px;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .zendesk-modal-close {
- background: #ef4444;
- color: white;
- border: none;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- cursor: pointer;
- font-size: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s;
- font-weight: bold;
- }
- .zendesk-modal-close:hover {
- background: #dc2626;
- transform: rotate(90deg);
- }
- .zendesk-modal-body {
- flex: 1;
- overflow: hidden;
- position: relative;
- background: #f5f5f5;
- }
- .zendesk-iframe-container {
- width: 100%;
- height: 100%;
- position: relative;
- }
- .zendesk-iframe {
- width: 100%;
- height: 100%;
- border: none;
- background: white;
- }
- .zendesk-loading {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- text-align: center;
- z-index: 1;
- }
- .zendesk-spinner {
- width: 60px;
- height: 60px;
- border: 4px solid #e0e0e0;
- border-top-color: #03a9f4;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto 20px;
- }
- .zendesk-error {
- display: none;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background: white;
- padding: 40px;
- border-radius: 16px;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
- max-width: 500px;
- text-align: center;
- z-index: 2;
- }
- .zendesk-error.show {
- display: block;
- }
- .zendesk-error h3 {
- color: #ef4444;
- margin-bottom: 15px;
- font-size: 20px;
- }
- .zendesk-error p {
- color: #666;
- margin-bottom: 20px;
- line-height: 1.6;
- }
- .zendesk-error-icon {
- font-size: 48px;
- margin-bottom: 15px;
- }
- @keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- @media (max-width: 768px) {
- .zendesk-modal-content {
- width: 100%;
- height: 100%;
- max-width: 100%;
- max-height: 100%;
- border-radius: 0;
- }
- .zendesk-modal-header {
- padding: 15px 20px;
- }
- .zendesk-modal-header h2 {
- font-size: 18px;
- }
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>💬 Customer Chat Monitor</h1>
- <p style="color: #666; margin-top: 5px;">Free Customer Support & Chat Management (No paid services required)</p>
- <div style="margin-top: 15px; display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
- <span class="agent-status">● ONLINE</span>
- <span style="color: #888; font-size: 14px;">Port 9884</span>
- <button onclick="openZendeskDashboard()" class="zendesk-link" style="cursor: pointer; border: none;">🔗 Open Zendesk Chat</button>
- <button onclick="location.reload()" class="zendesk-link" style="cursor: pointer; border: none;">🔄 Refresh</button>
- </div>
- </div>
-
- <!-- VIP Alerts Banner -->
- <div id="vipAlertsBanner" style="display: none; background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); border: 3px solid #ff6b6b; border-radius: 15px; padding: 20px; margin: 20px 0; box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); animation: pulse 2s infinite;">
- <div style="display: flex; align-items: center; gap: 15px;">
- <div style="font-size: 3em;">🚨</div>
- <div style="flex: 1;">
- <h2 style="color: #d32f2f; margin: 0 0 10px 0; font-size: 1.5em;">⚡ HIGH-VALUE OPPORTUNITY DETECTED</h2>
- <div id="vipAlertsContent"></div>
- </div>
- </div>
- </div>
-
- <div class="tabs">
- <button class="tab-button active" onclick="switchTab('overview')">📊 Overview</button>
- <button class="tab-button" onclick="switchTab('chat')">💬 AI Assistant</button>
- <button class="tab-button" onclick="switchTab('conversations')">💭 Active Chats</button>
- <button class="tab-button" onclick="switchTab('analytics')">📈 Analytics</button>
- </div>
-
- <!-- Overview Tab -->
- <div id="tab-overview" class="tab-content active">
- <div class="stats-grid">
- <div class="stat-card" style="border-left: 4px solid #10b981;">
- <div class="stat-value" style="color: #10b981;">${onlineCount}</div>
- <div class="stat-label">🟢 Currently Online</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${activeChats}</div>
- <div class="stat-label">Active Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${pendingChats}</div>
- <div class="stat-label">Pending Chats</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${resolvedChats}</div>
- <div class="stat-label">Resolved Today</div>
- </div>
- </div>
-
- <div class="section" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; padding: 25px; border-radius: 16px; margin-bottom: 20px;">
- <h2 style="color: white; margin-bottom: 15px;">🟢 Currently Online (${onlineCount})</h2>
- ${onlineVisitors.length > 0 ? onlineVisitors.map(visitor => `
- <div style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; margin-bottom: 10px; border-left: 4px solid ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'};">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
- <strong style="font-size: 1.1em;">${visitor.name}</strong>
- <span style="background: ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'}; color: white; padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: 600;">
- ${visitor.status === 'waiting' ? '⏳ Waiting' : visitor.status === 'chatting' ? '💬 Chatting' : '👀 Browsing'}
- </span>
- </div>
- <div style="font-size: 0.9em; opacity: 0.95;">
- 📧 ${visitor.email}<br>
- ${visitor.pageUrl ? `📄 ${visitor.pageUrl}<br>` : ''}
- 🕐 ${new Date(visitor.timestamp).toLocaleTimeString()}
- </div>
- </div>
- `).join('') : '<p style="text-align: center; padding: 20px; opacity: 0.9;">No visitors currently online</p>'}
- </div>
-
- <div class="section">
- <h2>💭 Recent Conversations ${isLiveData ? '<span style="color: #28a745; font-size: 0.8em;">● LIVE</span>' : '<span style="color: #ffc107; font-size: 0.8em;">● DEMO</span>'}</h2>
- <div id="conversationsList">
- ${displayConversations.slice(0, 3).map(conv => `
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-${conv.status}">${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-${conv.priority}">${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">${conv.subject}</div>
- <div class="last-message">${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- `).join('')}
- </div>
- </div>
-
- <div class="section">
- <h2>🔗 Zendesk Integration</h2>
- <p style="color: #666; margin-bottom: 15px;">Connected to: <strong>${ZENDESK_DOMAIN}</strong></p>
- <p style="color: #666; margin-bottom: 15px;">Account: <strong>${ZENDESK_EMAIL}</strong></p>
- <button class="action-button" onclick="openZendeskDashboard()" style="cursor: pointer;">Open Zendesk Chat Dashboard</button>
- </div>
- </div>
-
- <!-- Chat Tab -->
- <div id="tab-chat" class="tab-content">
- <div class="section">
- <h2>💬 AI Support Assistant</h2>
- <p style="color: #666; margin-bottom: 20px;">Get AI-powered help with customer support, chat responses, and Zendesk management</p>
-
- <div class="suggestion-chips">
- <span class="chip" onclick="sendSuggestion('Suggest response for installation question')">Installation help</span>
- <span class="chip" onclick="sendSuggestion('How to handle product availability inquiry')">Product availability</span>
- <span class="chip" onclick="sendSuggestion('Best practices for customer support')">Support best practices</span>
- <span class="chip" onclick="sendSuggestion('Generate empathetic response template')">Response templates</span>
- </div>
-
- <div class="chat-container">
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- Hello! I'm your Zendesk Chat AI Assistant powered by Claude 4. I can help you with:
- <br><br>
- • Suggesting responses to customer inquiries<br>
- • Customer support best practices<br>
- • Chat management strategies<br>
- • Handling difficult conversations<br>
- • Zendesk tips and workflows<br>
- <br>
- How can I assist you with customer support today?
- </div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- class="chat-input"
- id="chatInput"
- placeholder="Ask about customer support, chat responses..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-button" onclick="sendMessage()">Send</button>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Conversations Tab -->
- <div id="tab-conversations" class="tab-content">
- <div class="section">
- <h2>💭 All Conversations</h2>
- <div id="allConversations">
- <p style="color: #888;">Loading conversations...</p>
- </div>
- </div>
- </div>
-
- <!-- Analytics Tab -->
- <div id="tab-analytics" class="tab-content">
- <div class="section">
- <h2>📈 Chat Analytics</h2>
- <div class="stats-grid">
- <div class="stat-card">
- <div class="stat-value">${stats.customerSatisfaction}</div>
- <div class="stat-label">Customer Satisfaction</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.totalQueries}</div>
- <div class="stat-label">Total AI Queries</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${displayConversations.length}</div>
- <div class="stat-label">Total Conversations</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">${stats.avgResponseTime}</div>
- <div class="stat-label">Avg Response Time</div>
- </div>
- </div>
- <div style="background: #f9fafb; padding: 20px; border-radius: 12px; margin-top: 20px;">
- <p style="color: #666;">Connect to Zendesk Analytics API for real-time reporting and advanced metrics.</p>
- </div>
- </div>
- </div>
- </div>
-
- <script>
- function switchTab(tabName) {
- document.querySelectorAll('.tab-content').forEach(tab => {
- tab.classList.remove('active');
- });
- document.querySelectorAll('.tab-button').forEach(btn => {
- btn.classList.remove('active');
- });
-
- document.getElementById('tab-' + tabName).classList.add('active');
- event.target.classList.add('active');
-
- if (tabName === 'conversations') {
- loadConversations();
- }
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const message = input.value.trim();
-
- if (!message) return;
-
- const chatMessages = document.getElementById('chatMessages');
- chatMessages.innerHTML += \`
- <div class="message user">\${message}</div>
- \`;
-
- input.value = '';
- chatMessages.scrollTop = chatMessages.scrollHeight;
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- const data = await response.json();
-
- chatMessages.innerHTML += \`
- <div class="message assistant">\${data.response.replace(/\\n/g, '<br>')}</div>
- \`;
- chatMessages.scrollTop = chatMessages.scrollHeight;
- } catch (error) {
- console.error('Chat error:', error);
- chatMessages.innerHTML += \`
- <div class="message assistant">Sorry, I encountered an error. Please try again.</div>
- \`;
- }
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function loadVIPAlerts() {
- try {
- const response = await fetch('/api/vip-alerts');
- const data = await response.json();
-
- if (data.alerts && data.alerts.length > 0) {
- const banner = document.getElementById('vipAlertsBanner');
- const content = document.getElementById('vipAlertsContent');
-
- const html = data.alerts.map(alert => \`
- <div style="background: white; padding: 15px; border-radius: 10px; margin-bottom: 10px; border-left: 5px solid #ff6b6b;">
- <div style="font-weight: 700; font-size: 1.2em; color: #d32f2f; margin-bottom: 5px;">
- \${alert.name} - \${alert.company.toUpperCase()}
- </div>
- <div style="color: #666; margin-bottom: 5px;">
- 📧 \${alert.email}
- </div>
- <div style="color: #ff6b6b; font-weight: 600; font-size: 0.9em;">
- 🎯 \${alert.reason}
- </div>
- <div style="color: #888; font-size: 0.85em; margin-top: 5px;">
- ⏰ \${new Date(alert.timestamp).toLocaleString()}
- </div>
- </div>
- \`).join('');
-
- content.innerHTML = html;
- banner.style.display = 'block';
- } else {
- document.getElementById('vipAlertsBanner').style.display = 'none';
- }
- } catch (error) {
- console.error('Error loading VIP alerts:', error);
- }
- }
-
- async function loadConversations() {
- const container = document.getElementById('allConversations');
- try {
- const response = await fetch('/api/conversations');
- const data = await response.json();
-
- let html = '';
- data.conversations.forEach(conv => {
- html += \`
- <div class="conversation-card">
- <div class="conversation-header">
- <div>
- <div class="customer-name">\${conv.customerName}</div>
- <div style="font-size: 12px; color: #888; margin-top: 4px;">\${conv.customerEmail}</div>
- </div>
- <div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
- <span class="status-badge status-\${conv.status}">\${conv.status.toUpperCase()}</span>
- <span class="status-badge priority-\${conv.priority}">\${conv.priority.toUpperCase()}</span>
- </div>
- </div>
- <div class="conversation-subject">\${conv.subject}</div>
- <div class="last-message">\${conv.lastMessage}</div>
- <div class="conversation-footer">
- <span class="timestamp">\${new Date(conv.timestamp).toLocaleString()}</span>
- <div style="display: flex; gap: 8px;">
- <button class="action-button" onclick="viewChat('\${conv.id}')">View Chat</button>
- <a href="https://${ZENDESK_DOMAIN}/agent/tickets/\${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
- </div>
- </div>
- </div>
- \`;
- });
-
- container.innerHTML = html;
- } catch (error) {
- console.error('Load conversations error:', error);
- container.innerHTML = '<p style="color: #ef4444;">Error loading conversations</p>';
- }
- }
-
- async function viewChat(chatId) {
- try {
- const response = await fetch(\`/api/conversation/\${chatId}\`);
- const data = await response.json();
-
- const modal = document.getElementById('chatModal');
- const modalTitle = document.getElementById('modalTitle');
- const modalBody = document.getElementById('modalBody');
-
- modalTitle.textContent = \`Chat with \${data.customerName}\`;
-
- let messagesHtml = '';
- data.messages.forEach(msg => {
- const time = new Date(msg.timestamp).toLocaleTimeString();
- messagesHtml += \`
- <div class="chat-message \${msg.sender}">
- <div class="message-sender">\${msg.sender === 'customer' ? data.customerName : 'Support Agent'}</div>
- <div class="message-bubble">\${msg.message}</div>
- <div class="message-time">\${time}</div>
- </div>
- \`;
- });
-
- modalBody.innerHTML = messagesHtml;
- modal.classList.add('active');
-
- // Scroll to bottom
- modalBody.scrollTop = modalBody.scrollHeight;
- } catch (error) {
- console.error('Error loading chat:', error);
- alert('Failed to load chat conversation');
- }
- }
-
- function closeModal() {
- document.getElementById('chatModal').classList.remove('active');
- }
-
- // Close modal when clicking outside
- document.addEventListener('click', (e) => {
- const modal = document.getElementById('chatModal');
- if (e.target === modal) {
- closeModal();
- }
- });
-
- // Load VIP alerts on page load and refresh every 30 seconds
- loadVIPAlerts();
- setInterval(loadVIPAlerts, 30000);
-
- // Zendesk Dashboard Modal Functions
- function openZendeskDashboard() {
- const modal = document.getElementById('zendeskModal');
- const iframe = document.getElementById('zendeskIframe');
- const loading = document.getElementById('zendeskLoading');
- const error = document.getElementById('zendeskError');
-
- // Show modal
- modal.classList.add('active');
- loading.style.display = 'block';
- error.classList.remove('show');
-
- // Set iframe source
- const zendeskUrl = 'https://${ZENDESK_DOMAIN}';
- iframe.src = zendeskUrl;
-
- // Handle iframe load
- iframe.onload = function() {
- loading.style.display = 'none';
- };
-
- // Handle iframe error (timeout after 10 seconds)
- let loadTimeout = setTimeout(function() {
- if (loading.style.display !== 'none') {
- loading.style.display = 'none';
- error.classList.add('show');
- }
- }, 10000);
-
- iframe.onerror = function() {
- clearTimeout(loadTimeout);
- loading.style.display = 'none';
- error.classList.add('show');
- };
- }
-
- function closeZendeskModal() {
- const modal = document.getElementById('zendeskModal');
- const iframe = document.getElementById('zendeskIframe');
- modal.classList.remove('active');
-
- // Clear iframe to stop loading
- setTimeout(() => {
- iframe.src = 'about:blank';
- }, 300);
- }
-
- // Close Zendesk modal when clicking outside
- document.addEventListener('click', (e) => {
- const modal = document.getElementById('zendeskModal');
- if (e.target === modal) {
- closeZendeskModal();
- }
- });
-
- // Keyboard shortcuts
- document.addEventListener('keydown', (e) => {
- // ESC key closes modal
- if (e.key === 'Escape') {
- const zendeskModal = document.getElementById('zendeskModal');
- if (zendeskModal.classList.contains('active')) {
- closeZendeskModal();
- }
- }
- });
- </script>
-
- <!-- Zendesk Dashboard Modal -->
- <div id="zendeskModal" class="zendesk-modal">
- <div class="zendesk-modal-content">
- <div class="zendesk-modal-header">
- <h2>
- <span>💬</span>
- <span>Zendesk Chat Dashboard</span>
- </h2>
- <button class="zendesk-modal-close" onclick="closeZendeskModal()" title="Close (ESC)">×</button>
- </div>
- <div class="zendesk-modal-body">
- <div class="zendesk-iframe-container">
- <!-- Loading indicator -->
- <div id="zendeskLoading" class="zendesk-loading">
- <div class="zendesk-spinner"></div>
- <p style="color: #666; font-size: 16px;">Loading Zendesk Dashboard...</p>
- </div>
-
- <!-- Error message -->
- <div id="zendeskError" class="zendesk-error">
- <div class="zendesk-error-icon">⚠️</div>
- <h3>Unable to Load Zendesk Dashboard</h3>
- <p>The Zendesk dashboard could not be loaded in the iframe. This may be due to security restrictions.</p>
- <p style="margin-bottom: 20px;">
- <strong>Possible reasons:</strong><br>
- • Zendesk may block embedding in iframes (X-Frame-Options)<br>
- • You may need to authenticate first<br>
- • Network connectivity issues
- </p>
- <a href="https://${ZENDESK_DOMAIN}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block; margin-right: 10px;">
- Open in New Tab
- </a>
- <button class="action-button" onclick="closeZendeskModal()" style="background: #6b7280;">
- Close
- </button>
- </div>
-
- <!-- Iframe -->
- <iframe
- id="zendeskIframe"
- class="zendesk-iframe"
- sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
- title="Zendesk Chat Dashboard"
- ></iframe>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Chat Modal -->
- <div id="chatModal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h2 id="modalTitle">Chat Conversation</h2>
- <button class="modal-close" onclick="closeModal()">×</button>
- </div>
- <div class="modal-body" id="modalBody">
- <!-- Messages will be loaded here -->
- </div>
- </div>
- </div>
- </body>
- </html>
- `);
-});
-
-// API Endpoints
-
-// Chat endpoint
-app.post('/api/chat', requireAuth, async (req: Request, res: Response) => {
- try {
- const { message } = req.body;
- stats.totalQueries++;
-
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- req.session.chatHistory.push({
- role: 'user',
- content: message
- });
-
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- const liveConversations = await getConversations();
- const activeChats = liveConversations.filter((c: ChatConversation) => c.status === 'active').length;
- const pendingChats = liveConversations.filter((c: ChatConversation) => c.status === 'pending').length;
-
- const systemPrompt = `You are the Zendesk Chat Support Agent for Designer Wallcoverings, powered by Claude 4 Sonnet.
-
-Your role is to assist customer support team with:
-
-CAPABILITIES:
-- Suggest professional and empathetic customer responses
-- Provide customer support best practices
-- Help manage chat conversations
-- Offer conflict resolution strategies
-- Zendesk platform tips and workflows
-- Response templates for common scenarios
-
-CONTEXT:
-- Company: Designer Wallcoverings (wallcovering/wallpaper retailer)
-- Zendesk Domain: ${ZENDESK_DOMAIN}
-- Current Active Chats: ${activeChats}
-- Pending Chats: ${pendingChats}
-
-COMMON CUSTOMER INQUIRIES:
-- Product information and availability
-- Installation help and tips
-- Order tracking and status
-- Return and exchange policies
-- Design consultation
-- Sample requests
-
-When helping with responses:
-1. Be empathetic and professional
-2. Provide clear, actionable information
-3. Include product knowledge when relevant
-4. Maintain Designer Wallcoverings' brand voice
-5. Suggest follow-up actions
-
-Be helpful, supportive, and provide excellent customer service guidance.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 1024,
- system: systemPrompt,
- messages: req.session.chatHistory
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error processing your request.';
-
- req.session.chatHistory.push({
- role: 'assistant',
- content: assistantMessage
- });
-
- logActivity('Chat Query', `User asked: "${message.substring(0, 50)}..."`);
-
- res.json({ response: assistantMessage });
- } catch (error) {
- console.error('Chat error:', error);
- res.status(500).json({ error: 'Failed to process chat message' });
- }
-});
-
-// Get conversations
-app.get('/api/conversations', requireAuth, async (req: Request, res: Response) => {
- const liveConversations = await getConversations();
- res.json({ conversations: liveConversations });
-});
-
-// Get specific conversation by ID
-app.get('/api/conversation/:id', requireAuth, async (req: Request, res: Response) => {
- const chatId = req.params.id;
- const liveConversations = await getConversations();
- let conversation = liveConversations.find(c => c.id === chatId);
-
- if (!conversation) {
- return res.status(404).json({ error: 'Conversation not found' });
- }
-
- // Fetch comments if not already loaded
- if (conversation.messages.length === 0) {
- conversation.messages = await fetchTicketComments(chatId);
- }
-
- logActivity('View Chat', `Viewed conversation ${chatId} with ${conversation.customerName}`);
- res.json(conversation);
-});
-
-// Get statistics
-app.get('/api/stats', requireAuth, (req: Request, res: Response) => {
- res.json(stats);
-});
-
-// Get VIP alerts
-app.get('/api/vip-alerts', requireAuth, (req: Request, res: Response) => {
- res.json({ alerts: vipAlerts });
-});
-
-// Get activity log
-app.get('/api/activity', requireAuth, (req: Request, res: Response) => {
- res.json({ activities: activityLog });
-});
-
-// Start server
-app.listen(PORT, '0.0.0.0', () => {
- console.log(`💬 Zendesk Chat Agent running on port ${PORT}`);
- console.log(`Dashboard: http://45.61.58.125:${PORT}`);
- console.log(`Zendesk: ${ZENDESK_CHAT_URL}`);
- logActivity('System Start', 'Zendesk Chat Agent initialized');
-});
diff --git a/DW-Programming/ImportNewSkufromURL/dwjs-ai-retag.ts.backup b/DW-Programming/ImportNewSkufromURL/dwjs-ai-retag.ts.backup
deleted file mode 100644
index c9e63ba5..00000000
--- a/DW-Programming/ImportNewSkufromURL/dwjs-ai-retag.ts.backup
+++ /dev/null
@@ -1,698 +0,0 @@
-/**
- * DWJS AI Interior Designer Tagger + UNIQUE Title Generator + Description Generator
- *
- * Uses Google Gemini 3 (Nano Banana) for AI vision analysis
- *
- * Features:
- * 1. UNIQUE titles with dual dominant colors (e.g., "Pink & White Wallpaper")
- * 2. Color variations to avoid repetition (Cream, Ivory, Sand instead of all Beige)
- * 3. Professional descriptions for Class A fire rated commercial applications
- * 4. Remove old color tags, add AI-determined colors/styles/patterns
- * 5. Delete $0 "Price Per Single Roll" variants
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import fetch from 'node-fetch';
-import * as fs from 'fs';
-
-const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_ACCESS_TOKEN = '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';
-const SHOPIFY_API_VERSION = '2024-07';
-const ANTHROPIC_API_KEY = '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';
-
-const anthropic = new Anthropic({ apiKey: ANTHROPIC_API_KEY });
-
-// Progress tracking file
-const PROGRESS_FILE = './dwjs-retag-progress.json';
-
-// Color variations to ensure unique titles - when AI says "Beige", rotate through these
-// Color families - each color maps to its variations
-const COLOR_FAMILIES: Record<string, string[]> = {
- // Neutral light family
- 'beige': ['Cream', 'Ivory', 'Sand', 'Oatmeal', 'Tan', 'Wheat', 'Ecru', 'Champagne', 'Bisque', 'Linen', 'Fawn', 'Almond'],
- 'cream': ['Ivory', 'Sand', 'Oatmeal', 'Champagne', 'Vanilla', 'Ecru', 'Pearl', 'Linen', 'Eggshell', 'Bisque', 'Almond'],
- 'ivory': ['Cream', 'Sand', 'Pearl', 'Champagne', 'Vanilla', 'Eggshell', 'Bone', 'Oatmeal', 'Linen', 'Ecru'],
- 'white': ['Pearl', 'Snow', 'Alabaster', 'Eggshell', 'Bone', 'Cotton', 'Vanilla', 'Chalk', 'Frost', 'Linen'],
- 'sand': ['Oatmeal', 'Tan', 'Wheat', 'Champagne', 'Ecru', 'Linen', 'Fawn', 'Almond', 'Bisque', 'Cream'],
- 'oatmeal': ['Sand', 'Tan', 'Wheat', 'Linen', 'Ecru', 'Cream', 'Champagne', 'Fawn', 'Almond', 'Bisque'],
-
- // Gray family
- 'gray': ['Charcoal', 'Slate', 'Pewter', 'Silver', 'Ash', 'Smoke', 'Stone', 'Graphite', 'Fog', 'Steel'],
- 'grey': ['Charcoal', 'Slate', 'Pewter', 'Silver', 'Ash', 'Smoke', 'Stone', 'Graphite', 'Fog', 'Steel'],
- 'charcoal': ['Slate', 'Graphite', 'Pewter', 'Smoke', 'Ash', 'Stone', 'Steel', 'Carbon', 'Onyx', 'Anthracite'],
- 'silver': ['Pewter', 'Platinum', 'Steel', 'Ash', 'Fog', 'Smoke', 'Chrome', 'Nickel', 'Aluminum', 'Tin'],
- 'slate': ['Charcoal', 'Graphite', 'Stone', 'Pewter', 'Ash', 'Steel', 'Smoke', 'Carbon', 'Iron', 'Lead'],
-
- // Brown family
- 'brown': ['Chocolate', 'Espresso', 'Mocha', 'Walnut', 'Chestnut', 'Coffee', 'Cocoa', 'Umber', 'Sepia', 'Mahogany'],
- 'taupe': ['Mushroom', 'Greige', 'Mink', 'Driftwood', 'Pebble', 'Stone', 'Fawn', 'Mocha', 'Beaver', 'Tan'],
- 'tan': ['Camel', 'Khaki', 'Buff', 'Fawn', 'Wheat', 'Sand', 'Tawny', 'Caramel', 'Honey', 'Biscuit'],
-
- // Blue family
- 'blue': ['Navy', 'Cobalt', 'Sapphire', 'Azure', 'Cerulean', 'Indigo', 'Denim', 'Midnight', 'Ocean', 'Marine'],
- 'navy': ['Midnight', 'Indigo', 'Marine', 'Admiral', 'Nautical', 'Sapphire', 'Ink', 'Deep Blue', 'Prussian', 'Oxford'],
- 'sky blue': ['Powder Blue', 'Baby Blue', 'Periwinkle', 'Cornflower', 'Azure', 'Cerulean', 'Ice Blue', 'Horizon', 'Cloud', 'Mist'],
- 'powder blue': ['Sky Blue', 'Baby Blue', 'Ice Blue', 'Periwinkle', 'Azure', 'Cloud', 'Mist', 'Horizon', 'Cornflower', 'Air'],
-
- // Green family
- 'green': ['Sage', 'Olive', 'Forest', 'Emerald', 'Hunter', 'Moss', 'Fern', 'Jade', 'Seafoam', 'Willow'],
- 'sage': ['Olive', 'Moss', 'Fern', 'Willow', 'Eucalyptus', 'Seafoam', 'Celadon', 'Pistachio', 'Mint', 'Laurel'],
- 'olive': ['Moss', 'Fern', 'Sage', 'Army', 'Khaki', 'Willow', 'Artichoke', 'Avocado', 'Hunter', 'Basil'],
-
- // Pink family
- 'pink': ['Blush', 'Rose', 'Coral', 'Salmon', 'Dusty Rose', 'Mauve', 'Peony', 'Ballet', 'Petal', 'Shell'],
- 'blush': ['Rose', 'Petal', 'Shell', 'Ballet', 'Peony', 'Dusty Rose', 'Coral', 'Salmon', 'Powder', 'Flesh'],
- 'coral': ['Salmon', 'Peach', 'Apricot', 'Shell', 'Rose', 'Blush', 'Flamingo', 'Melon', 'Papaya', 'Shrimp'],
- 'lavender': ['Lilac', 'Mauve', 'Wisteria', 'Periwinkle', 'Orchid', 'Heather', 'Violet', 'Thistle', 'Iris', 'Amethyst'],
-
- // Yellow/Gold family
- 'yellow': ['Gold', 'Mustard', 'Honey', 'Amber', 'Buttercup', 'Sunshine', 'Lemon', 'Canary', 'Marigold', 'Daffodil'],
- 'gold': ['Champagne', 'Honey', 'Amber', 'Mustard', 'Brass', 'Marigold', 'Saffron', 'Dijon', 'Wheat', 'Butterscotch'],
- 'champagne': ['Gold', 'Honey', 'Prosecco', 'Bisque', 'Buff', 'Straw', 'Flax', 'Vanilla', 'Cream', 'Wheat'],
-
- // Orange family
- 'orange': ['Coral', 'Peach', 'Tangerine', 'Apricot', 'Terracotta', 'Rust', 'Copper', 'Amber', 'Burnt Orange', 'Pumpkin'],
- 'peach': ['Apricot', 'Coral', 'Melon', 'Papaya', 'Salmon', 'Blush', 'Shell', 'Flesh', 'Cantaloupe', 'Nectar'],
-
- // Red family
- 'red': ['Burgundy', 'Crimson', 'Ruby', 'Scarlet', 'Wine', 'Cherry', 'Garnet', 'Brick', 'Merlot', 'Cardinal'],
- 'burgundy': ['Wine', 'Merlot', 'Claret', 'Oxblood', 'Maroon', 'Garnet', 'Plum', 'Sangria', 'Bordeaux', 'Mulberry'],
-
- // Purple family
- 'purple': ['Plum', 'Lavender', 'Violet', 'Mauve', 'Amethyst', 'Grape', 'Eggplant', 'Orchid', 'Lilac', 'Iris'],
- 'plum': ['Eggplant', 'Grape', 'Mulberry', 'Aubergine', 'Burgundy', 'Wine', 'Violet', 'Orchid', 'Mauve', 'Fig'],
-};
-
-// For backwards compatibility
-const COLOR_VARIATIONS = COLOR_FAMILIES;
-
-// Track used colors per pattern to ensure uniqueness
-const usedColorsPerPattern: Map<string, Set<string>> = new Map();
-
-// Colors to remove from tags
-const COLOR_TAGS_TO_REMOVE = [
- 'Beige', 'Blue', 'Brown', 'Coral', 'Cream', 'Gold', 'Gray', 'Green',
- 'Ivory', 'Mint', 'Navy', 'Neutral', 'Olive', 'Orange', 'Peach', 'Pink',
- 'Plum', 'Purple', 'Red', 'Silver', 'Taupe', 'Teal', 'Turquoise',
- 'White', 'Yellow', 'Black', 'Burgundy', 'Charcoal', 'Maroon', 'Rust',
- 'Tan', 'Khaki', 'Indigo', 'Aqua', 'Copper', 'Bronze', 'Rose', 'Blush',
- 'Sage', 'Denim', 'Slate', 'Sand', 'Oatmeal', 'Mushroom', 'Pewter',
- 'Sage Green', 'Light Blue', 'Dark Blue', 'Navy Blue', 'Forest Green',
- 'Dusty Rose', 'Blush Pink', 'Off White', 'Warm Gray', 'Cool Gray',
- 'Wallcovering', 'wallcovering', 'Wallcoverings', 'wallcoverings'
-];
-
-interface Product {
- id: string;
- title: string;
- tags: string[];
- sku: string;
- imageUrl: string;
- descriptionHtml: string;
- variants: { id: string; title: string; price: string; }[];
-}
-
-interface AIAnalysis {
- dominantColor1: string;
- dominantColor1Percent: number;
- dominantColor2: string;
- dominantColor2Percent: number;
- allColors: string[];
- styles: string[];
- patterns: string[];
- description: string;
-}
-
-let stats = {
- total: 0,
- processed: 0,
- updated: 0,
- titlesFixed: 0,
- descriptionsUpdated: 0,
- variantsDeleted: 0,
- skipped: 0,
- errors: 0,
-};
-
-function loadProgress(): Set<string> {
- try {
- if (fs.existsSync(PROGRESS_FILE)) {
- const data = JSON.parse(fs.readFileSync(PROGRESS_FILE, 'utf-8'));
- console.log(`📂 Loaded progress: ${data.processed?.length || 0} products already processed`);
-
- // Restore usedColorsPerPattern from saved data
- if (data.usedColorsPerPattern) {
- for (const [pattern, colors] of Object.entries(data.usedColorsPerPattern)) {
- usedColorsPerPattern.set(pattern, new Set(colors as string[]));
- }
- console.log(`📂 Restored color tracking for ${Object.keys(data.usedColorsPerPattern).length} patterns`);
- }
-
- return new Set(data.processed || []);
- }
- } catch (e) {
- console.log('📂 No previous progress found, starting fresh');
- }
- return new Set();
-}
-
-function saveProgress(processedIds: Set<string>) {
- // Convert usedColorsPerPattern Map to serializable object
- const colorsObj: Record<string, string[]> = {};
- for (const [pattern, colors] of usedColorsPerPattern.entries()) {
- colorsObj[pattern] = Array.from(colors);
- }
-
- fs.writeFileSync(PROGRESS_FILE, JSON.stringify({
- processed: Array.from(processedIds),
- usedColorsPerPattern: colorsObj,
- lastUpdate: new Date().toISOString(),
- stats
- }, null, 2));
-}
-
-// Get a unique color for this pattern - avoid duplicates, NEVER use numbers
-function getUniqueColor(baseColor: string, patternName: string): string {
- const lowerColor = baseColor.toLowerCase();
- const patternKey = patternName.toLowerCase().replace(/\s+/g, '-');
-
- if (!usedColorsPerPattern.has(patternKey)) {
- usedColorsPerPattern.set(patternKey, new Set());
- }
- const usedColors = usedColorsPerPattern.get(patternKey)!;
-
- // If this exact color hasn't been used for this pattern, use it
- if (!usedColors.has(lowerColor)) {
- usedColors.add(lowerColor);
- return baseColor;
- }
-
- // Find a variation from this color's family that hasn't been used
- const variations = COLOR_FAMILIES[lowerColor] || [];
- for (const variant of variations) {
- if (!usedColors.has(variant.toLowerCase())) {
- usedColors.add(variant.toLowerCase());
- return variant;
- }
- }
-
- // If no direct family found, check if this color appears in another family's variations
- // and use siblings from that family
- for (const [familyName, familyColors] of Object.entries(COLOR_FAMILIES)) {
- if (familyColors.map(c => c.toLowerCase()).includes(lowerColor)) {
- // Found the family this color belongs to - try siblings
- for (const sibling of familyColors) {
- if (!usedColors.has(sibling.toLowerCase())) {
- usedColors.add(sibling.toLowerCase());
- return sibling;
- }
- }
- }
- }
-
- // Last resort: scan ALL color families for any unused color in a similar tonal range
- // Group by warmth: warm (beige, cream, tan, gold) vs cool (gray, blue, silver)
- const warmFamilies = ['beige', 'cream', 'ivory', 'sand', 'oatmeal', 'tan', 'gold', 'champagne', 'peach', 'coral', 'blush'];
- const coolFamilies = ['gray', 'grey', 'charcoal', 'silver', 'slate', 'blue', 'navy'];
-
- const isWarm = warmFamilies.includes(lowerColor) ||
- warmFamilies.some(f => (COLOR_FAMILIES[f] || []).map(c => c.toLowerCase()).includes(lowerColor));
-
- const familiesToCheck = isWarm ? warmFamilies : coolFamilies;
-
- for (const family of familiesToCheck) {
- const familyVariations = COLOR_FAMILIES[family] || [];
- for (const variant of familyVariations) {
- if (!usedColors.has(variant.toLowerCase())) {
- usedColors.add(variant.toLowerCase());
- return variant;
- }
- }
- }
-
- // Absolute last resort: use the base color (should rarely happen with 10+ variations per family)
- usedColors.add(lowerColor);
- return baseColor;
-}
-
-async function getAllDWJSProducts(): Promise<Product[]> {
- console.log('🔍 Fetching ACTIVE DWJS products with variants...\n');
-
- const products: Product[] = [];
- let hasNextPage = true;
- let cursor: string | null = null;
-
- while (hasNextPage) {
- const query = `
- query($cursor: String) {
- products(first: 250, after: $cursor, query: "sku:DWJS-* AND status:ACTIVE") {
- edges {
- node {
- id
- title
- tags
- descriptionHtml
- images(first: 1) {
- edges {
- node {
- src
- }
- }
- }
- variants(first: 10) {
- edges {
- node {
- id
- sku
- title
- price
- }
- }
- }
- }
- cursor
- }
- pageInfo {
- hasNextPage
- }
- }
- }
- `;
-
- const response = await fetch(
- `https://${SHOPIFY_STORE}/admin/api/${SHOPIFY_API_VERSION}/graphql.json`,
- {
- method: 'POST',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_ACCESS_TOKEN,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ query, variables: cursor ? { cursor } : {} }),
- }
- );
-
- const data = await response.json() as any;
- if (data.errors) throw new Error(`GraphQL Error: ${JSON.stringify(data.errors)}`);
-
- const edges = data.data?.products?.edges || [];
-
- for (const edge of edges) {
- const node = edge.node;
- const firstVariant = node.variants.edges[0]?.node;
- const sku = firstVariant?.sku || '';
-
- if (sku.toUpperCase().startsWith('DWJS-')) {
- products.push({
- id: node.id.replace('gid://shopify/Product/', ''),
- title: node.title,
- tags: node.tags || [],
- sku: sku,
- imageUrl: node.images.edges[0]?.node?.src || '',
- descriptionHtml: node.descriptionHtml || '',
- variants: node.variants.edges.map((v: any) => ({
- id: v.node.id,
- title: v.node.title || '',
- price: v.node.price || '0',
- })),
- });
- }
- }
-
- hasNextPage = data.data?.products?.pageInfo?.hasNextPage || false;
- cursor = edges.length > 0 ? edges[edges.length - 1].cursor : null;
-
- process.stdout.write(`\r Fetched ${products.length} products...`);
- }
-
- console.log(`\n✅ Found ${products.length} total DWJS products\n`);
- return products;
-}
-
-async function analyzeImageWithClaude(product: Product): Promise<AIAnalysis> {
- if (!product.imageUrl) {
- return {
- dominantColor1: '', dominantColor1Percent: 0,
- dominantColor2: '', dominantColor2Percent: 0,
- allColors: [], styles: [], patterns: [], description: ''
- };
- }
-
- const prompt = `You are an expert INTERIOR DESIGNER analyzing a wallpaper/wallcovering product image.
-
-Product: ${product.title}
-SKU: ${product.sku}
-
-Analyze this wallpaper and provide a JSON response with:
-
-1. DOMINANT COLORS with percentages:
- - Identify the TWO most dominant colors and their approximate coverage percentage
- - Use SPECIFIC color names: "Cream", "Navy", "Blush", "Sage", "Gold", "Ivory", "Sand", "Oatmeal", "Charcoal", etc.
- - AVOID generic "Beige" - use variations like "Cream", "Ivory", "Sand", "Oatmeal", "Tan", "Wheat"
- - If only one color dominates (>70%), set second color to empty string
-
-2. ALL COLORS (max 3): List all visible colors with specific names
-
-3. STYLES (1-2): Modern, Traditional, Contemporary, Art Deco, Mid-Century, Bohemian, Coastal, Transitional, etc.
-
-4. PATTERNS (1-2): Floral, Geometric, Damask, Botanical, Stripe, Abstract, Solid/Textured, Toile, Chinoiserie, etc.
-
-5. DESCRIPTION: Write a 3-4 sentence professional description for this COMMERCIAL wallcovering:
- - Describe the design, colors, and visual impact
- - Include the phrase "Class A fire rated for low traffic commercial applications"
- - Suggest ideal settings (executive offices, boutique hotels, healthcare facilities, etc.)
-
-Return ONLY valid JSON:
-{
- "dominantColor1": "Cream",
- "dominantColor1Percent": 50,
- "dominantColor2": "Gold",
- "dominantColor2Percent": 30,
- "allColors": ["Cream", "Gold", "White"],
- "styles": ["Contemporary"],
- "patterns": ["Geometric"],
- "description": "This elegant wallcovering features..."
-}`;
-
- try {
- const imageResponse = await fetch(product.imageUrl);
- const imageBuffer = await imageResponse.arrayBuffer();
- const base64Image = Buffer.from(imageBuffer).toString('base64');
-
- const mediaType = product.imageUrl.toLowerCase().includes('.png') ? 'image/png' : 'image/jpeg';
-
- const message = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 1024,
- messages: [{
- role: 'user',
- content: [
- {
- type: 'image',
- source: {
- type: 'base64',
- media_type: mediaType,
- data: base64Image,
- },
- },
- {
- type: 'text',
- text: prompt,
- },
- ],
- }],
- });
-
- const responseText = message.content[0].type === 'text' ? message.content[0].text : '';
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
-
- if (jsonMatch) {
- const parsed = JSON.parse(jsonMatch[0]);
- return {
- dominantColor1: parsed.dominantColor1 || '',
- dominantColor1Percent: parsed.dominantColor1Percent || 0,
- dominantColor2: parsed.dominantColor2 || '',
- dominantColor2Percent: parsed.dominantColor2Percent || 0,
- allColors: parsed.allColors || [],
- styles: parsed.styles || [],
- patterns: parsed.patterns || [],
- description: parsed.description || '',
- };
- }
- } catch (error: any) {
- console.error(` ⚠️ Claude analysis failed: ${error.message}`);
- }
-
- return {
- dominantColor1: '', dominantColor1Percent: 0,
- dominantColor2: '', dominantColor2Percent: 0,
- allColors: [], styles: [], patterns: [], description: ''
- };
-}
-
-async function updateProduct(productId: string, updates: { title?: string; tags?: string[]; descriptionHtml?: string }): Promise<boolean> {
- const inputFields: string[] = [];
- inputFields.push(`id: "gid://shopify/Product/${productId}"`);
-
- if (updates.title) {
- inputFields.push(`title: ${JSON.stringify(updates.title)}`);
- }
- if (updates.tags) {
- inputFields.push(`tags: ${JSON.stringify(updates.tags)}`);
- }
- if (updates.descriptionHtml) {
- inputFields.push(`descriptionHtml: ${JSON.stringify(updates.descriptionHtml)}`);
- }
-
- const mutation = `
- mutation {
- productUpdate(input: {
- ${inputFields.join('\n ')}
- }) {
- product {
- id
- title
- tags
- descriptionHtml
- }
- userErrors {
- field
- message
- }
- }
- }
- `;
-
- const response = await fetch(
- `https://${SHOPIFY_STORE}/admin/api/${SHOPIFY_API_VERSION}/graphql.json`,
- {
- method: 'POST',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_ACCESS_TOKEN,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ query: mutation }),
- }
- );
-
- const data = await response.json() as any;
-
- if (data.data?.productUpdate?.userErrors?.length > 0) {
- console.error(` ❌ Update error: ${data.data.productUpdate.userErrors[0].message}`);
- return false;
- }
-
- return true;
-}
-
-async function deleteVariant(variantId: string): Promise<boolean> {
- const mutation = `
- mutation {
- productVariantDelete(id: "${variantId}") {
- deletedProductVariantId
- userErrors {
- field
- message
- }
- }
- }
- `;
-
- const response = await fetch(
- `https://${SHOPIFY_STORE}/admin/api/${SHOPIFY_API_VERSION}/graphql.json`,
- {
- method: 'POST',
- headers: {
- 'X-Shopify-Access-Token': SHOPIFY_ACCESS_TOKEN,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ query: mutation }),
- }
- );
-
- const data = await response.json() as any;
-
- if (data.data?.productVariantDelete?.userErrors?.length > 0) {
- console.error(` ❌ Variant delete error: ${data.data.productVariantDelete.userErrors[0].message}`);
- return false;
- }
-
- return true;
-}
-
-// Extract pattern name from title (before color and "Wallpaper")
-function extractPatternName(title: string): string {
- // Remove "| Jeffrey Stevens" suffix
- let clean = title.replace(/\s*\|\s*Jeffrey Stevens.*/i, '').trim();
- // Remove "Wallpaper" suffix
- clean = clean.replace(/\s+Wallpaper$/i, '').trim();
- // Remove any color words at the end
- const colorPattern = /\s+(Beige|Blue|Brown|Cream|Gold|Gray|Green|Ivory|Navy|Pink|Purple|Red|Silver|Taupe|Teal|White|Yellow|Black|Charcoal|Sage|Coral|Blush|Tan|Sand|Oatmeal|Ivory|Pearl)$/i;
- clean = clean.replace(colorPattern, '').trim();
- return clean;
-}
-
-async function processProduct(product: Product, index: number, total: number, processedIds: Set<string>) {
- if (processedIds.has(product.id)) {
- stats.skipped++;
- return;
- }
-
- console.log(`\n[${index}/${total}] ${product.title}`);
- console.log(` SKU: ${product.sku}`);
-
- // Delete $0 "Price Per Single Roll" variants
- for (const variant of product.variants) {
- const titleLower = variant.title.toLowerCase();
- const price = parseFloat(variant.price);
-
- if (titleLower.includes('price per single roll') && price === 0) {
- console.log(` 🗑️ Deleting $0 variant: "${variant.title}"`);
- const deleted = await deleteVariant(variant.id);
- if (deleted) {
- console.log(` ✅ Variant deleted`);
- stats.variantsDeleted++;
- }
- await new Promise(resolve => setTimeout(resolve, 500));
- }
- }
-
- // Skip products without images
- if (!product.imageUrl) {
- console.log(` ⏭️ Skipping - No image available`);
- stats.skipped++;
- processedIds.add(product.id);
- saveProgress(processedIds);
- return;
- }
-
- console.log(` 🤖 Claude AI analyzing (dual-color mode)...`);
- const analysis = await analyzeImageWithClaude(product);
-
- if (!analysis.dominantColor1) {
- console.log(` ⚠️ Skipping - AI analysis failed`);
- stats.errors++;
- return;
- }
-
- console.log(` 🎨 Color 1: ${analysis.dominantColor1} (${analysis.dominantColor1Percent}%)`);
- if (analysis.dominantColor2) {
- console.log(` 🎨 Color 2: ${analysis.dominantColor2} (${analysis.dominantColor2Percent}%)`);
- }
- console.log(` 🏠 Styles: ${analysis.styles.join(', ')}`);
- console.log(` 📐 Patterns: ${analysis.patterns.join(', ')}`);
-
- // Prepare updates
- const updates: { title?: string; tags?: string[]; descriptionHtml?: string } = {};
-
- // Extract pattern name for uniqueness tracking
- const patternName = extractPatternName(product.title);
-
- // Get unique colors for this pattern
- const uniqueColor1 = getUniqueColor(analysis.dominantColor1, patternName);
- const uniqueColor2 = analysis.dominantColor2 ? getUniqueColor(analysis.dominantColor2, patternName) : '';
-
- // Build new title with dual colors if both are significant (>25%)
- let colorPart = uniqueColor1;
- if (uniqueColor2 && analysis.dominantColor2Percent >= 25) {
- colorPart = `${uniqueColor1} & ${uniqueColor2}`;
- }
-
- const newTitle = `${patternName} ${colorPart} Wallpaper | Jeffrey Stevens`;
-
- if (newTitle !== product.title) {
- console.log(` 📝 Title: "${product.title}"`);
- console.log(` → "${newTitle}"`);
- updates.title = newTitle;
- stats.titlesFixed++;
- }
-
- // Update tags
- let updatedTags = product.tags.filter(tag =>
- !COLOR_TAGS_TO_REMOVE.some(c => c.toLowerCase() === tag.toLowerCase())
- );
-
- updatedTags = [
- ...updatedTags,
- ...analysis.allColors,
- ...analysis.styles,
- ...analysis.patterns
- ];
-
- updatedTags = [...new Set(updatedTags)];
- updates.tags = updatedTags;
-
- console.log(` ✨ Tags: ${[...analysis.allColors, ...analysis.styles, ...analysis.patterns].join(', ')}`);
-
- // Update description
- if (analysis.description) {
- updates.descriptionHtml = `<p>${analysis.description}</p>`;
- console.log(` 📄 Description: "${analysis.description.substring(0, 80)}..."`);
- stats.descriptionsUpdated++;
- }
-
- const success = await updateProduct(product.id, updates);
-
- if (success) {
- console.log(` ✅ Updated successfully`);
- stats.updated++;
- processedIds.add(product.id);
- saveProgress(processedIds);
- } else {
- console.log(` ❌ Update failed`);
- stats.errors++;
- }
-
- stats.processed++;
-
- // Rate limiting - 2 seconds between API calls
- await new Promise(resolve => setTimeout(resolve, 2000));
-}
-
-async function main() {
- console.log('╔════════════════════════════════════════════════════════════════════════════════════════════╗');
- console.log('║ DWJS AI TAGGER + UNIQUE TITLE GENERATOR (Claude Vision) ║');
- console.log('║ - Claude Sonnet vision analysis for accurate color detection ║');
- console.log('║ - UNIQUE titles with dual dominant colors (e.g., "Pink & White Wallpaper") ║');
- console.log('║ - Color variations to avoid repetition (Cream, Ivory, Sand instead of all Beige) ║');
- console.log('║ - Professional descriptions: Class A fire rated for low traffic commercial ║');
- console.log('║ - Delete $0 "Price Per Single Roll" variants ║');
- console.log('╚════════════════════════════════════════════════════════════════════════════════════════════╝\n');
-
- const startTime = Date.now();
- const processedIds = loadProgress();
-
- const products = await getAllDWJSProducts();
- stats.total = products.length;
-
- console.log(`⏳ Processing ${products.length} products (${processedIds.size} already done)...\n`);
-
- for (let i = 0; i < products.length; i++) {
- try {
- await processProduct(products[i], i + 1, products.length, processedIds);
- } catch (error: any) {
- console.error(` ❌ Error: ${error.message}`);
- stats.errors++;
- stats.processed++;
- }
- }
-
- const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
-
- console.log('\n\n╔═══════════════════════════════════════════════════════════════════════════════╗');
- console.log('║ FINAL SUMMARY ║');
- console.log('╚═══════════════════════════════════════════════════════════════════════════════╝');
- console.log(`⏱️ Duration: ${duration} minutes`);
- console.log(`📦 Total products: ${stats.total}`);
- console.log(`✅ Updated: ${stats.updated}`);
- console.log(`📝 Titles fixed (unique colors): ${stats.titlesFixed}`);
- console.log(`📄 Descriptions updated: ${stats.descriptionsUpdated}`);
- console.log(`🗑️ $0 variants deleted: ${stats.variantsDeleted}`);
- console.log(`⏭️ Skipped: ${stats.skipped}`);
- console.log(`❌ Errors: ${stats.errors}`);
- console.log('');
-}
-
-main().catch((error) => {
- console.error(`\n❌ FATAL ERROR: ${error.message}`);
- process.exit(1);
-});
diff --git a/DW-Websites/GrassclothWallcoverings/fetch-shopify-collection.js b/DW-Websites/GrassclothWallcoverings/fetch-shopify-collection.js
index c9d30531..016f9480 100644
--- a/DW-Websites/GrassclothWallcoverings/fetch-shopify-collection.js
+++ b/DW-Websites/GrassclothWallcoverings/fetch-shopify-collection.js
@@ -3,7 +3,7 @@ const fs = require('fs');
// Shopify configuration
const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_ACCESS_TOKEN = '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';
+const SHOPIFY_ACCESS_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ACCESS_TOKEN || '';
// Function to get collection by handle
function getCollectionByHandle(handle) {
diff --git a/DW-Websites/GrassclothWallcoverings/fetch-shopify-products.js b/DW-Websites/GrassclothWallcoverings/fetch-shopify-products.js
index 8e5796c4..95d8f828 100644
--- a/DW-Websites/GrassclothWallcoverings/fetch-shopify-products.js
+++ b/DW-Websites/GrassclothWallcoverings/fetch-shopify-products.js
@@ -3,7 +3,7 @@ const fs = require('fs');
// Shopify configuration
const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_ACCESS_TOKEN = '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';
+const SHOPIFY_ACCESS_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ACCESS_TOKEN || '';
// Function to make Shopify API request
function fetchShopifyProducts(limit = 250) {
← 40ecde25 auto-data-snapshot: 2026-09-17T16:53:13 (3 data files) — sho
·
back to Designer Wallcoverings
·
TK-11871: Wallcovering guard default 3.0 -> 2.0 lb (live cat b58f60ef →