← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-legal-team/legal-agent.ts.backup
2323 lines
/**
* 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);
});