← back to Designer Wallcoverings

DW-Agents/dw-agents/agent-needs-attention/needs-attention-agent.ts.backup-oauth-20251112

774 lines

import cookieParser from "cookie-parser";
/**
 * 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';
const SSO_TOKEN_NAME = 'sso_token';
const SSO_TOKEN_VALUE = 'dw-secure-token-2024';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import fetch from 'node-fetch';

import { chatMiddleware } from '../shared-chat-integration';

const app = express();

// Global Authentication
const PORT = 9886;

// Global Authentication - MUST come before any other middleware
app.use(cookieParser());

// Add inter-agent chat widget (after auth)
app.use(chatMiddleware({
  agentId: 'agent-needs-attention',
  agentName: 'Needs Attention',
  port: 9886,
  category: 'agent'
}));
 // Fixed: back to 9886 as 9891 is used by in-parallel agent

// 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';

app.use(express.json());
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) => {
  // 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>
  <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 === 'admin2025' && password === 'Otis') {
    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>
  <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%, #FF6B6B 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: #DC143C; 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, #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 {
      display: inline-block;
      color: #DC143C;
      text-decoration: none;
      font-weight: 600;
      margin-left: 10px;
      padding: 4px 12px;
      background: rgba(220, 20, 60, 0.1);
      border-radius: 4px;
      border: 1px solid #DC143C;
      transition: all 0.3s ease;
    }
    .action-link:hover {
      background: #DC143C;
      color: white;
      transform: translateX(3px);
    }
    .no-items { color: #888; text-align: center; padding: 20px; }
    .decision-btn {
      padding: 8px 16px;
      margin-right: 10px;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-weight: 600;
      font-size: 0.9em;
      transition: all 0.3s ease;
    }
    .agree-btn {
      background: #10b981;
      color: white;
    }
    .agree-btn:hover {
      background: #059669;
      transform: scale(1.05);
    }
    .disagree-btn {
      background: #6b7280;
      color: white;
    }
    .disagree-btn:hover {
      background: #4b5563;
      transform: scale(1.05);
    }
    .decision-btn:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
  </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>
    <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').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 => \`
            <div class="attention-item critical" id="item-\${item.metadata?.productId || Math.random()}">
              <span class="priority critical">\${item.priority.toUpperCase()}</span>
              <strong>\${item.title}</strong><br>
              \${item.description}
              \${item.actionUrl ? \`<a href="\${item.actionUrl}" target="_blank" class="action-link">→ View Product</a>\` : ''}
              \${item.metadata?.productId ? \`
                <div style="margin-top: 10px;">
                  <button onclick="handleDecision('\${item.metadata.productId}', 'agree', '\${item.metadata.sku || ''}')" class="decision-btn agree-btn">✓ Agree (Draft + Tag)</button>
                  <button onclick="handleDecision('\${item.metadata.productId}', 'disagree', '\${item.metadata.sku || ''}')" class="decision-btn disagree-btn">✗ Disagree (Keep + Note)</button>
                </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);
      }
    }

    // Handle decision buttons
    async function handleDecision(productId, decision, sku) {
      if (!confirm(\`Are you sure you want to \${decision === 'agree' ? 'AGREE (set to draft + add settlement tag)' : 'DISAGREE (keep status + add note)'} for product \${sku}?\`)) {
        return;
      }

      const itemDiv = document.getElementById(\`item-\${productId}\`);
      if (itemDiv) {
        itemDiv.style.opacity = '0.5';
      }

      try {
        const response = await fetch('/api/decision', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ productId, decision, sku })
        });

        const result = await response.json();

        if (result.success) {
          alert(\`✅ Success! Product \${sku} has been updated.\\n\\n\${result.message}\`);
          // Remove item from view
          if (itemDiv) {
            itemDiv.style.display = 'none';
          }
          // Refresh the list
          setTimeout(refreshAttention, 1000);
        } else {
          alert(\`❌ Error: \${result.error}\`);
          if (itemDiv) {
            itemDiv.style.opacity = '1';
          }
        }
      } catch (error) {
        alert(\`❌ Error processing decision: \${error.message}\`);
        if (itemDiv) {
          itemDiv.style.opacity = '1';
        }
      }
    }

    // Auto-refresh every 30 seconds
    setInterval(refreshAttention, 30000);
    refreshAttention();
  </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;
      metadata?: any;
    }> = [];

    // Read memory.json to get all stored critical items
    try {
      const fs = await import('fs/promises');
      const path = await import('path');
      const memoryPath = path.join(__dirname, 'memory.json');
      const memoryContent = await fs.readFile(memoryPath, 'utf-8');
      const memoryItems = JSON.parse(memoryContent);

      // Process memory items - parse the content field if it's a string
      for (const item of memoryItems) {
        try {
          let itemData = item;
          if (typeof item.content === 'string') {
            itemData = JSON.parse(item.content);
          }

          // Skip if already resolved
          if (item.resolved) {
            continue;
          }

          // Only include items with priority and not completed
          if (itemData.priority && itemData.status !== 'completed') {
            // Check if product has settlement_decision metafield (skip if it does)
            if (itemData.metadata?.productId) {
              try {
                const metafieldUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${itemData.metadata.productId}/metafields.json?namespace=legal_review&key=settlement_decision`;
                const metafieldResponse = await fetch(metafieldUrl, {
                  headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN },
                });
                const metafieldData: any = await metafieldResponse.json();

                // If metafield exists, skip this product
                if (metafieldData.metafields && metafieldData.metafields.length > 0) {
                  console.log(`Skipping product ${itemData.metadata.productId} - already has decision`);
                  continue;
                }
              } catch (metafieldError) {
                console.error('Error checking metafield:', metafieldError);
                // Continue anyway if metafield check fails
              }
            }

            attentionItems.push({
              title: itemData.title || 'Untitled Item',
              description: itemData.description || '',
              priority: itemData.priority,
              source: itemData.source || 'Unknown',
              actionUrl: itemData.metadata?.url || itemData.actionUrl,
              timestamp: new Date(item.timestamp || itemData.createdAt || Date.now()),
              metadata: itemData.metadata
            });
          }
        } catch (parseError) {
          console.error('Error parsing memory item:', parseError);
        }
      }
    } catch (error) {
      console.error('Error reading memory.json:', error);
    }

    // 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()
    });

    // 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()
    });

    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' });
  }
});

// API endpoint to handle Agree/Disagree decisions
app.post('/api/decision', requireAuth, async (req: Request, res: Response) => {
  try {
    const { productId, decision, sku } = req.body;

    if (!productId || !decision) {
      return res.status(400).json({ success: false, error: 'Missing productId or decision' });
    }

    // Fetch current product data
    const productUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${productId}.json`;
    const productResponse = await fetch(productUrl, {
      headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN },
    });
    const productData: any = await productResponse.json();
    const product = productData.product;

    if (!product) {
      return res.status(404).json({ success: false, error: 'Product not found' });
    }

    let message = '';

    if (decision === 'agree') {
      // AGREE: Set to draft and add settlement tag
      const currentTags = product.tags ? product.tags.split(',').map((t: string) => t.trim()) : [];
      if (!currentTags.includes('settlement')) {
        currentTags.push('settlement');
      }

      const updatePayload = {
        product: {
          id: productId,
          status: 'draft',
          tags: currentTags.join(', ')
        }
      };

      const updateResponse = await fetch(productUrl, {
        method: 'PUT',
        headers: {
          'X-Shopify-Access-Token': SHOPIFY_TOKEN,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(updatePayload)
      });

      if (!updateResponse.ok) {
        const errorText = await updateResponse.text();
        throw new Error(`Failed to update product: ${errorText}`);
      }

      message = `Product ${sku} set to DRAFT and tagged with "settlement"`;

    } else if (decision === 'disagree') {
      // DISAGREE: Add metafield to track disagreement
      const metafieldPayload = {
        metafield: {
          namespace: 'legal_review',
          key: 'settlement_decision',
          value: JSON.stringify({
            decision: 'disagree',
            timestamp: new Date().toISOString(),
            note: 'User disagreed with settlement violation flag'
          }),
          type: 'json'
        }
      };

      const metafieldUrl = `https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/products/${productId}/metafields.json`;
      const metafieldResponse = await fetch(metafieldUrl, {
        method: 'POST',
        headers: {
          'X-Shopify-Access-Token': SHOPIFY_TOKEN,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(metafieldPayload)
      });

      if (!metafieldResponse.ok) {
        const errorText = await metafieldResponse.text();
        throw new Error(`Failed to create metafield: ${errorText}`);
      }

      message = `Product ${sku} kept in current status with disagreement noted in metafield`;
    }

    // Update memory.json to mark this item as resolved
    try {
      const fs = await import('fs/promises');
      const path = await import('path');
      const memoryPath = path.join(__dirname, 'memory.json');
      const memoryContent = await fs.readFile(memoryPath, 'utf-8');
      const memoryItems = JSON.parse(memoryContent);

      // Find and update the item
      const updatedItems = memoryItems.map((item: any) => {
        try {
          const itemData = typeof item.content === 'string' ? JSON.parse(item.content) : item;
          if (itemData.metadata?.productId === productId) {
            return {
              ...item,
              resolved: true,
              resolvedAt: new Date().toISOString(),
              decision: decision
            };
          }
        } catch (e) {
          // Skip parsing errors
        }
        return item;
      });

      await fs.writeFile(memoryPath, JSON.stringify(updatedItems, null, 2));
    } catch (error) {
      console.error('Error updating memory.json:', error);
    }

    res.json({ success: true, message });

  } catch (error: any) {
    console.error('Error processing decision:', error);
    res.status(500).json({ success: false, error: error.message || 'Failed to process decision' });
  }
});


// 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(`🚨 Needs Attention Agent running on port ${PORT}`);
  console.log(`Dashboard: http://45.61.58.125:${PORT}`);
});