Files
budget-tracker/penpot-template.mjs
T

193 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Affiche SOLDES -50% — version corrigée
* Leçons apprises :
* - create_text : paramètre `text` (pas `content`), `textAlign` (pas `align`)
* - create_text : parentId NON supporté → textes à coords absolues
* - create_rectangle : parentId OK
* - Frame à (0,0) → coordonnées absolues = relatives au frame
*/
import https from 'https';
import http from 'http';
const PORTAINER = { host: '192.168.1.150', port: 9443 };
const MCP = { host: '192.168.1.150', port: 9002 };
const API_KEY = process.env.PORTAINER_API_KEY;
// ─── PORTAINER ───────────────────────────────────────────────────────────────
function portainerReq(path, method = 'GET') {
return new Promise((resolve, reject) => {
const headers = { 'X-API-Key': API_KEY, 'Content-Length': 0 };
const req = https.request({ ...PORTAINER, path, method, rejectUnauthorized: false, headers }, res => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve(res.statusCode); } });
});
req.on('error', reject); req.end();
});
}
async function restartMCPContainer() {
const containers = await portainerReq('/api/endpoints/2/docker/containers/json?filters=%7B%22name%22%3A%5B%22penpot-penpot-mcp%22%5D%7D');
const id = containers[0]?.Id;
await portainerReq(`/api/endpoints/2/docker/containers/${id}/restart`, 'POST');
console.log('🔄 MCP restarted');
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ─── MCP CLIENT ──────────────────────────────────────────────────────────────
let sessionId = null;
let msgId = 1;
function postMCP(body) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify(body);
const headers = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
'Accept': 'application/json, text/event-stream',
};
if (sessionId) headers['mcp-session-id'] = sessionId;
const req = http.request({ ...MCP, path: '/mcp', method: 'POST', headers }, res => {
if (res.headers['mcp-session-id']) sessionId = res.headers['mcp-session-id'];
let data = ''; res.setEncoding('utf8');
res.on('data', c => data += c);
res.on('end', () => {
const ct = res.headers['content-type'] || '';
if (ct.includes('text/event-stream')) {
let result = null, error = null;
for (const line of data.split('\n')) {
if (line.startsWith('data:')) {
const raw = line.slice(5).trim();
if (!raw || raw === '[DONE]') continue;
try { const j = JSON.parse(raw); if (j.result !== undefined) result = j.result; if (j.error) error = j.error; } catch {}
}
}
if (error) reject(new Error(JSON.stringify(error)));
else resolve(result);
} else {
try { const j = JSON.parse(data); if (j.error) reject(new Error(JSON.stringify(j.error))); else resolve(j.result ?? j); }
catch { resolve(data || null); }
}
});
});
req.on('error', reject); req.write(payload); req.end();
});
}
async function initMCP() {
await postMCP({ jsonrpc: '2.0', id: msgId++, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'nox', version: '1.0' } } });
await postMCP({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }).catch(() => {});
console.log('✅ Session:', sessionId);
}
async function callTool(name, args = {}) {
const result = await postMCP({ jsonrpc: '2.0', id: msgId++, method: 'tools/call', params: { name, arguments: args } });
if (!result) return null;
if (result.content) {
for (const c of result.content) { try { return JSON.parse(c.text); } catch {} }
return result.content[0]?.text;
}
return result;
}
async function rect(args) {
const r = await callTool('create_rectangle', args);
console.log(` ▪ rect "${args.name}" ${r?.id?.slice(0,8) || JSON.stringify(r).slice(0,40)}`);
return r;
}
async function txt(args) {
const r = await callTool('create_text', args);
console.log(` ▪ text "${args.name}" ${r?.id?.slice(0,8) || JSON.stringify(r).slice(0,40)}`);
return r;
}
// ─── MAIN ────────────────────────────────────────────────────────────────────
console.log('🚀 Création affiche SOLDES (version corrigée)...');
await restartMCPContainer();
await sleep(10000);
await initMCP();
// Profil + IDs
const profile = await callTool('get_profile', {});
const projectId = profile?.defaultProjectId;
console.log('ProjectId:', projectId);
// Supprimer l'ancien fichier si présent
const existingFiles = await callTool('list_files', { projectId });
if (Array.isArray(existingFiles)) {
for (const f of existingFiles.filter(f => f.name?.includes('SOLDES'))) {
await callTool('delete_file', { fileId: f.id });
console.log('🗑 Supprimé ancien fichier:', f.name);
}
}
// Créer le fichier
const file = await callTool('create_file', { projectId, name: 'Affiche SOLDES -50%' });
const fileId = file?.id;
console.log('FileId:', fileId);
const pages = await callTool('list_pages', { fileId });
const pageId = Array.isArray(pages) ? pages[0]?.id : null;
console.log('PageId:', pageId);
if (!pageId) { console.error('❌ Pas de pageId'); process.exit(1); }
// ─── DESIGN 600×900 — frame à (0,0) ─────────────────────────────────────────
console.log('\n🎨 Construction...');
// Frame principale = fond rouge (à 0,0 → coords absolues = relatives)
const frame = await callTool('create_frame', {
fileId, pageId,
name: 'Affiche SOLDES', x: 0, y: 0, width: 600, height: 900,
fillColor: '#C0392B',
});
const frameId = frame?.id;
// ── Bandes jaunes haut / bas (parentId OK pour rect)
await rect({ fileId, pageId, parentId: frameId, name: 'Bande haut', x: 0, y: 0, width: 600, height: 14, fillColor: '#F1C40F' });
await rect({ fileId, pageId, parentId: frameId, name: 'Bande bas', x: 0, y: 886, width: 600, height: 14, fillColor: '#F1C40F' });
// ── Header sombre
await rect({ fileId, pageId, parentId: frameId, name: 'Header bg', x: 0, y: 14, width: 600, height: 88, fillColor: '#7B241C' });
// ── Séparateur milieu
await rect({ fileId, pageId, parentId: frameId, name: 'Sep1', x: 80, y: 430, width: 440, height: 3, fillColor: '#F1C40F' });
// ── Badge OFFRE LIMITÉE
await rect({ fileId, pageId, parentId: frameId, name: 'Badge bg', x: 115, y: 450, width: 370, height: 68, fillColor: '#F1C40F', r1: 34, r2: 34, r3: 34, r4: 34 });
// ── Séparateur bas
await rect({ fileId, pageId, parentId: frameId, name: 'Sep2', x: 80, y: 760, width: 440, height: 3, fillColor: '#F8C471' });
// ── TEXTES (coords absolues, pas de parentId)
// Collection header (petit texte dans le header sombre)
await txt({ fileId, pageId, name: 'Collection', text: '✦ Collection Printemps 2026 ✦', x: 0, y: 32, fontSize: 14, fontWeight: '700', fillColor: '#F1C40F', textAlign: 'center', letterSpacing: 4 });
// SOLDES (grand titre)
await txt({ fileId, pageId, name: 'Soldes', text: 'SOLDES', x: 0, y: 115, fontSize: 90, fontWeight: '900', fillColor: '#F1C40F', textAlign: 'center', letterSpacing: 10 });
// -50%
await txt({ fileId, pageId, name: 'Pct', text: '-50%', x: 0, y: 205, fontSize: 190, fontWeight: '900', fillColor: '#FFFFFF', textAlign: 'center' });
// Sous-titre
await txt({ fileId, pageId, name: 'Sous-titre', text: 'SUR TOUTES NOS CHAUSSURES', x: 0, y: 380, fontSize: 22, fontWeight: '700', fillColor: '#F1C40F', textAlign: 'center', letterSpacing: 3 });
// Offre limitée (sur le badge jaune)
await txt({ fileId, pageId, name: 'Offre', text: '⚡ OFFRE LIMITÉE ⚡', x: 115, y: 468, fontSize: 22, fontWeight: '900', fillColor: '#7B241C', textAlign: 'center' });
// Dates
await txt({ fileId, pageId, name: 'Dates', text: 'du 1er au 31 mars 2026', x: 0, y: 542, fontSize: 16, fontWeight: '400', fillColor: '#FADBD8', textAlign: 'center', letterSpacing: 2 });
// Emojis
await txt({ fileId, pageId, name: 'Chaussures', text: '👟 👠 👞', x: 0, y: 580, fontSize: 80, textAlign: 'center' });
// Site web
await txt({ fileId, pageId, name: 'Site web', text: 'www.votre-boutique.fr', x: 0, y: 780, fontSize: 14, fontWeight: '400', fillColor: '#F8C471', textAlign: 'center', letterSpacing: 3 });
// Étoiles
await txt({ fileId, pageId, name: 'Etoiles', text: '★ ★ ★ ★ ★', x: 0, y: 836, fontSize: 20, fillColor: '#F1C40F', textAlign: 'center' });
console.log('\n✅ Affiche recréée dans Penpot !');
console.log('🔗 http://192.168.1.150:9001 → Drafts → "Affiche SOLDES -50%"');