← 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}`);
});