metallkart-erp/screenshot-api.js
Louis-Andre 52e59a13ac feat: add screenshot-api.js tool for visual validation from claude.ai
Puppeteer-based screenshot tool with JWT auth, base64/file output modes,
CSS animation disabling, and 1920x1080 viewport for ERP frontend captures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:10:02 +00:00

146 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
// Screenshot API tool for visual validation from claude.ai
// Usage: NODE_PATH=/usr/lib/node_modules node /opt/erp/screenshot-api.js <url> [base64|file]
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs');
const LOGIN_URL = 'http://localhost:5173/login';
const LOGIN_EMAIL = 'louis@metallcart.ru';
const LOGIN_PASSWORD = 'MetallKart2026!';
const API_LOGIN_URL = 'http://localhost:3001/api/v1/auth/login';
const VIEWPORT = { width: 1920, height: 1080 };
const GLOBAL_TIMEOUT = 30000;
const log = (...args) => console.error('[screenshot-api]', ...args);
async function main() {
const url = process.argv[2];
const outputFormat = process.argv[3] || 'base64';
if (!url) {
log('Usage: node screenshot-api.js <url> [base64|file]');
process.exit(1);
}
if (!['base64', 'file'].includes(outputFormat)) {
log('Invalid output format. Use "base64" or "file".');
process.exit(1);
}
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
],
timeout: GLOBAL_TIMEOUT,
});
const page = await browser.newPage();
await page.setViewport(VIEWPORT);
// Disable animations for stable screenshots
await page.evaluateOnNewDocument(() => {
const style = document.createElement('style');
style.textContent = `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
`;
document.head.appendChild(style);
});
// Login via API to get JWT tokens
log('Logging in via API...');
const http = require('http');
const tokens = await new Promise((resolve, reject) => {
const body = JSON.stringify({ email: LOGIN_EMAIL, password: LOGIN_PASSWORD });
const req = http.request(API_LOGIN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
timeout: 10000,
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode !== 200) {
reject(new Error(`Login failed: ${res.statusCode} ${data}`));
return;
}
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error(`Login response parse error: ${data}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('Login request timeout')); });
req.write(body);
req.end();
});
log('Login successful, injecting tokens...');
// Navigate to the app first to set localStorage on the correct origin
await page.goto('http://localhost:5173', { waitUntil: 'domcontentloaded', timeout: 15000 });
// Inject tokens into localStorage
await page.evaluate((t) => {
localStorage.setItem('accessToken', t.accessToken);
localStorage.setItem('refreshToken', t.refreshToken);
if (t.user) localStorage.setItem('user', JSON.stringify(t.user));
}, tokens);
// Navigate to the target URL
log(`Navigating to ${url}...`);
try {
await page.goto(url, { waitUntil: 'networkidle0', timeout: 20000 });
} catch (e) {
log(`networkidle0 timeout, falling back to load...`);
await page.goto(url, { waitUntil: 'load', timeout: 15000 });
}
// Wait a bit for Ant Design components to finish rendering
await new Promise(r => setTimeout(r, 800));
// Take screenshot
const screenshotBuffer = await page.screenshot({ fullPage: false, type: 'png' });
if (outputFormat === 'base64') {
// Output ONLY base64 to stdout
process.stdout.write(screenshotBuffer.toString('base64'));
} else {
// Save to file
const screenshotsDir = '/opt/erp/screenshots';
fs.mkdirSync(screenshotsDir, { recursive: true });
const urlSlug = url.replace(/https?:\/\//, '').replace(/[^a-zA-Z0-9]/g, '_').substring(0, 60);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
const filename = `${urlSlug}_${timestamp}.png`;
const filepath = path.join(screenshotsDir, filename);
fs.writeFileSync(filepath, screenshotBuffer);
process.stdout.write(filepath);
}
log('Screenshot captured successfully.');
} catch (err) {
log('Error:', err.message);
process.exit(1);
} finally {
if (browser) {
await browser.close().catch(() => {});
}
}
}
main();