nothumanallowed 13.5.42 → 13.5.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "13.5.42",
3
+ "version": "13.5.43",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3865,6 +3865,264 @@ ${completedHeadings ? `## SECTIONS ALREADY WRITTEN (headings only):\n${completed
3865
3865
  return;
3866
3866
  }
3867
3867
 
3868
+ // ── WebCraft Sandbox — fullstack preview in ~/.nha/webcraft/<project> ──
3869
+ // POST /api/studio/webcraft/sandbox/start { projectName, files[] }
3870
+ // → SSE stream with log lines, ends with { type:'ready', port, dir }
3871
+ // DELETE /api/studio/webcraft/sandbox → kill running sandbox process
3872
+ if (pathname === '/api/studio/webcraft/sandbox/start' && method === 'POST') {
3873
+ const body = await parseBody(req, 8 * 1024 * 1024); // 8MB max
3874
+ const projName = (body.projectName || 'webcraft-sandbox').replace(/[^a-zA-Z0-9_-]/g, '-');
3875
+ const files = body.files || [];
3876
+ if (!files.length) { sendJSON(res, 400, { error: 'no files' }); return; }
3877
+
3878
+ res.writeHead(200, {
3879
+ 'Content-Type': 'text/event-stream',
3880
+ 'Cache-Control': 'no-cache',
3881
+ 'Connection': 'keep-alive',
3882
+ 'Access-Control-Allow-Origin': '*',
3883
+ });
3884
+ const sendLog = (msg) => res.write(`data: ${JSON.stringify({ type: 'log', msg })}\n\n`);
3885
+ const sendReady = (port, dir) => res.write(`data: ${JSON.stringify({ type: 'ready', port, dir })}\n\n`);
3886
+ const sendError = (msg) => res.write(`data: ${JSON.stringify({ type: 'error', msg })}\n\n`);
3887
+
3888
+ try {
3889
+ // Kill previous sandbox if running
3890
+ if (global._wcSandboxProc) {
3891
+ try { global._wcSandboxProc.kill('SIGTERM'); } catch(_) {}
3892
+ global._wcSandboxProc = null;
3893
+ }
3894
+
3895
+ const sandboxDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
3896
+ sendLog(`📁 Percorso sandbox: ${sandboxDir}`);
3897
+ fs.mkdirSync(sandboxDir, { recursive: true });
3898
+
3899
+ // Write all generated files
3900
+ sendLog(`📝 Scrittura di ${files.length} file...`);
3901
+ for (const f of files) {
3902
+ const filePath = path.join(sandboxDir, f.name);
3903
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
3904
+ fs.writeFileSync(filePath, f.content, 'utf8');
3905
+ sendLog(` ✓ ${f.name}`);
3906
+ }
3907
+
3908
+ // Inject sandbox db shim — replaces pg with in-memory SQLite-like store
3909
+ const dbShim = `
3910
+ // NHA WebCraft Sandbox DB Shim
3911
+ // Replaces pg.Pool with a simple in-memory store so the sandbox runs without PostgreSQL.
3912
+ // For production, delete this file and restore server/db.js to use pg.Pool.
3913
+ const crypto = require('crypto');
3914
+ const tables = {};
3915
+ function ensureTable(t) { if (!tables[t]) tables[t] = []; }
3916
+ function matchesWhere(row, where) {
3917
+ if (!where) return true;
3918
+ return Object.keys(where).every(k => row[k] == where[k]);
3919
+ }
3920
+ const pool = {
3921
+ query: async function(text, params) {
3922
+ // Parse very simple SQL patterns for auth routes
3923
+ const t = text.trim();
3924
+ const ins = t.match(/^INSERT INTO (\\w+)\\s*\\(([^)]+)\\)/i);
3925
+ if (ins) {
3926
+ const tbl = ins[1]; ensureTable(tbl);
3927
+ const cols = ins[2].split(',').map(c=>c.trim());
3928
+ const row = { id: crypto.randomUUID() };
3929
+ cols.forEach((c,i)=>{ row[c] = params?params[i]:null; });
3930
+ row.created_at = new Date().toISOString();
3931
+ row.updated_at = new Date().toISOString();
3932
+ tables[tbl].push(row);
3933
+ return { rows: [row], rowCount: 1 };
3934
+ }
3935
+ const sel = t.match(/^SELECT (.+) FROM (\\w+)/i);
3936
+ if (sel) {
3937
+ const tbl = sel[2]; ensureTable(tbl);
3938
+ const whereM = t.match(/WHERE (.+?)(?:LIMIT|ORDER|$)/i);
3939
+ let rows = tables[tbl];
3940
+ if (whereM && params) {
3941
+ // Simple $1 = value matching
3942
+ let idx = 0;
3943
+ const conds = whereM[1].split(/AND/i).map(c=>c.trim());
3944
+ const where = {};
3945
+ conds.forEach(c=>{
3946
+ const m = c.match(/(\\w+)\\s*=\\s*\\$\\d+/i);
3947
+ if (m) { where[m[1]] = params[idx++]; }
3948
+ });
3949
+ rows = rows.filter(r=>matchesWhere(r,where));
3950
+ }
3951
+ const lim = t.match(/LIMIT (\\d+)/i);
3952
+ if (lim) rows = rows.slice(0, parseInt(lim[1]));
3953
+ return { rows, rowCount: rows.length };
3954
+ }
3955
+ const upd = t.match(/^UPDATE (\\w+) SET (.+?) WHERE/i);
3956
+ if (upd) {
3957
+ const tbl = upd[1]; ensureTable(tbl);
3958
+ const whereM = t.match(/WHERE (.+?)(?:RETURNING|$)/i);
3959
+ let updated = [];
3960
+ tables[tbl] = tables[tbl].map(row=>{
3961
+ let match = true;
3962
+ if (whereM && params) {
3963
+ const conds = whereM[1].split(/AND/i).map(c=>c.trim());
3964
+ let idx = params.length - conds.length;
3965
+ conds.forEach(c=>{
3966
+ const m = c.match(/(\\w+)\\s*=\\s*\\$\\d+/i);
3967
+ if (m && params[idx] !== undefined) match = match && (row[m[1]] == params[idx++]);
3968
+ });
3969
+ }
3970
+ if (match) { row.updated_at = new Date().toISOString(); updated.push(row); }
3971
+ return row;
3972
+ });
3973
+ return { rows: updated, rowCount: updated.length };
3974
+ }
3975
+ const del = t.match(/^DELETE FROM (\\w+)/i);
3976
+ if (del) {
3977
+ const tbl = del[1]; ensureTable(tbl);
3978
+ tables[tbl] = [];
3979
+ return { rows: [], rowCount: 0 };
3980
+ }
3981
+ return { rows: [], rowCount: 0 };
3982
+ },
3983
+ connect: async function() { return { query: pool.query, release: ()=>{} }; }
3984
+ };
3985
+ pool.query.bind(pool);
3986
+ async function query(text, params) { return pool.query(text, params); }
3987
+ async function transaction(cb) {
3988
+ const client = await pool.connect();
3989
+ try { const r = await cb(client); client.release(); return r; }
3990
+ catch(e) { client.release(); throw e; }
3991
+ }
3992
+ module.exports = { pool, query, transaction };
3993
+ `;
3994
+ fs.writeFileSync(path.join(sandboxDir, 'server', 'db.js'), dbShim, 'utf8');
3995
+ sendLog('🔧 DB shim iniettato (in-memory, no PostgreSQL richiesto)');
3996
+
3997
+ // Patch package.json to remove pg, add only what's needed
3998
+ const pkgPath = path.join(sandboxDir, 'package.json');
3999
+ let pkg = {};
4000
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch(_) {}
4001
+ if (!pkg.dependencies) pkg.dependencies = {};
4002
+ delete pkg.dependencies.pg;
4003
+ // Ensure essentials
4004
+ pkg.dependencies.express = pkg.dependencies.express || '^4.19.0';
4005
+ pkg.dependencies.bcryptjs = pkg.dependencies.bcryptjs || '^2.4.3';
4006
+ pkg.dependencies.jsonwebtoken = pkg.dependencies.jsonwebtoken || '^9.0.0';
4007
+ pkg.dependencies.helmet = pkg.dependencies.helmet || '^7.0.0';
4008
+ pkg.dependencies['express-rate-limit'] = pkg.dependencies['express-rate-limit'] || '^7.0.0';
4009
+ pkg.dependencies.cors = pkg.dependencies.cors || '^2.8.5';
4010
+ pkg.dependencies.dotenv = pkg.dependencies.dotenv || '^16.0.0';
4011
+ pkg.dependencies.nodemailer = pkg.dependencies.nodemailer || '^6.9.0';
4012
+ pkg.dependencies['express-validator'] = pkg.dependencies['express-validator'] || '^7.0.0';
4013
+ delete pkg.dependencies.ioredis;
4014
+ pkg.scripts = pkg.scripts || {};
4015
+ pkg.scripts.start = 'node server/index.js';
4016
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
4017
+ sendLog('📦 package.json ottimizzato per sandbox (rimosso pg, redis)');
4018
+
4019
+ // Create minimal .env for sandbox
4020
+ const envContent = [
4021
+ 'PORT=0',
4022
+ 'NODE_ENV=development',
4023
+ `JWT_SECRET=nha-sandbox-${crypto.randomBytes(16).toString('hex')}`,
4024
+ `JWT_REFRESH_SECRET=nha-sandbox-ref-${crypto.randomBytes(16).toString('hex')}`,
4025
+ 'CORS_ORIGIN=*',
4026
+ `BASE_URL=http://localhost`,
4027
+ 'SMTP_HOST=localhost',
4028
+ 'SMTP_PORT=25',
4029
+ 'SMTP_USER=sandbox',
4030
+ 'SMTP_PASS=sandbox',
4031
+ 'SMTP_FROM=sandbox@localhost',
4032
+ ].join('\n');
4033
+ fs.writeFileSync(path.join(sandboxDir, '.env'), envContent, 'utf8');
4034
+ sendLog('⚙️ .env sandbox creato');
4035
+
4036
+ sendLog('');
4037
+ sendLog('📦 Dipendenze che verranno installate:');
4038
+ const deps = Object.keys(pkg.dependencies);
4039
+ deps.forEach(d => sendLog(` • ${d}@${pkg.dependencies[d]}`));
4040
+ sendLog(` Percorso: ${sandboxDir}/node_modules`);
4041
+ sendLog('');
4042
+ sendLog('⏳ npm install in corso...');
4043
+
4044
+ // npm install
4045
+ await new Promise((resolve, reject) => {
4046
+ const npm = exec(`npm install --prefer-offline 2>&1`, { cwd: sandboxDir, timeout: 120000 }, (err, stdout) => {
4047
+ if (err) { sendLog('❌ npm install fallito: ' + err.message); reject(err); }
4048
+ else { resolve(); }
4049
+ });
4050
+ npm.stdout && npm.stdout.on('data', d => { const line = d.toString().trim(); if (line) sendLog(' ' + line); });
4051
+ });
4052
+ sendLog('✅ npm install completato');
4053
+
4054
+ // Find free port
4055
+ const freePort = await new Promise(resolve => {
4056
+ const srv = (await import('net')).default.createServer();
4057
+ srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => resolve(p)); });
4058
+ });
4059
+
4060
+ // Patch PORT in .env
4061
+ fs.writeFileSync(path.join(sandboxDir, '.env'), envContent.replace('PORT=0', `PORT=${freePort}`), 'utf8');
4062
+
4063
+ sendLog(`🚀 Avvio server sandbox su porta ${freePort}...`);
4064
+
4065
+ // Spawn sandbox server
4066
+ const { spawn } = await import('child_process');
4067
+ const proc = spawn('node', ['server/index.js'], {
4068
+ cwd: sandboxDir,
4069
+ env: { ...process.env, PORT: String(freePort), NODE_ENV: 'development' },
4070
+ stdio: ['ignore', 'pipe', 'pipe'],
4071
+ });
4072
+ global._wcSandboxProc = proc;
4073
+ global._wcSandboxPort = freePort;
4074
+ global._wcSandboxDir = sandboxDir;
4075
+
4076
+ proc.stdout.on('data', d => { const l = d.toString().trim(); if (l) sendLog(' [server] ' + l); });
4077
+ proc.stderr.on('data', d => { const l = d.toString().trim(); if (l) sendLog(' [server] ' + l); });
4078
+
4079
+ // Wait for server to be ready (max 10s)
4080
+ await new Promise((resolve, reject) => {
4081
+ let attempts = 0;
4082
+ const tryConnect = () => {
4083
+ import('net').then(({ default: net }) => {
4084
+ const s = net.createConnection(freePort, '127.0.0.1');
4085
+ s.on('connect', () => { s.destroy(); resolve(); });
4086
+ s.on('error', () => {
4087
+ s.destroy();
4088
+ if (++attempts > 20) reject(new Error('Server non risponde dopo 10s'));
4089
+ else setTimeout(tryConnect, 500);
4090
+ });
4091
+ });
4092
+ };
4093
+ setTimeout(tryConnect, 1000);
4094
+ });
4095
+
4096
+ sendLog(`✅ Sandbox pronta!`);
4097
+ sendReady(freePort, sandboxDir);
4098
+ } catch (e) {
4099
+ sendError(e.message);
4100
+ }
4101
+ res.end();
4102
+ logRequest(method, pathname, 200, Date.now() - start);
4103
+ return;
4104
+ }
4105
+
4106
+ if (pathname === '/api/studio/webcraft/sandbox' && method === 'DELETE') {
4107
+ if (global._wcSandboxProc) {
4108
+ try { global._wcSandboxProc.kill('SIGTERM'); } catch(_) {}
4109
+ global._wcSandboxProc = null;
4110
+ }
4111
+ sendJSON(res, 200, { ok: true });
4112
+ logRequest(method, pathname, 200, Date.now() - start);
4113
+ return;
4114
+ }
4115
+
4116
+ if (pathname === '/api/studio/webcraft/sandbox/status' && method === 'GET') {
4117
+ sendJSON(res, 200, {
4118
+ running: !!global._wcSandboxProc,
4119
+ port: global._wcSandboxPort || null,
4120
+ dir: global._wcSandboxDir || null,
4121
+ });
4122
+ logRequest(method, pathname, 200, Date.now() - start);
4123
+ return;
4124
+ }
4125
+
3868
4126
  // ── Studio: Parliament deliberation (SSE streaming) ──────────────────
3869
4127
  // Implements the Legion DeliberationEngine protocol adapted for Studio:
3870
4128
  // Round 1 outputs already exist (from normal workflow steps).
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '13.5.42';
8
+ export const VERSION = '13.5.43';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -3148,6 +3148,7 @@ var I18N = {
3148
3148
  wc_download:'Download Archive', wc_describe_first:'Please describe your project first.',
3149
3149
  wc_no_files:'Describe your project and click Generate',
3150
3150
  wc_examples_label:'Examples',
3151
+ wc_sandbox_start:'Launch Sandbox',
3151
3152
  },
3152
3153
  it: {
3153
3154
  chat:'Chat', studio:'Studio', settings:'Impostazioni', agents:'Agenti',
@@ -3184,6 +3185,7 @@ var I18N = {
3184
3185
  wc_download:'Scarica archivio', wc_describe_first:'Descrivi prima il tuo progetto.',
3185
3186
  wc_no_files:'Descrivi il progetto e clicca Genera',
3186
3187
  wc_examples_label:'Esempi',
3188
+ wc_sandbox_start:'Avvia Sandbox',
3187
3189
  },
3188
3190
  es: {
3189
3191
  chat:'Chat', studio:'Studio', settings:'Configuración', agents:'Agentes',
@@ -3219,6 +3221,7 @@ var I18N = {
3219
3221
  wc_download:'Descargar archivo', wc_describe_first:'Por favor describe tu proyecto primero.',
3220
3222
  wc_no_files:'Describe el proyecto y haz clic en Generar',
3221
3223
  wc_examples_label:'Ejemplos',
3224
+ wc_sandbox_start:'Iniciar Sandbox',
3222
3225
  },
3223
3226
  fr: {
3224
3227
  chat:'Chat', studio:'Studio', settings:'Paramètres', agents:'Agents',
@@ -3254,6 +3257,7 @@ var I18N = {
3254
3257
  wc_download:'T\u00e9l\u00e9charger archive', wc_describe_first:'Veuillez d\u00e9crire votre projet.',
3255
3258
  wc_no_files:'D\u00e9crivez le projet et cliquez sur G\u00e9n\u00e9rer',
3256
3259
  wc_examples_label:'Exemples',
3260
+ wc_sandbox_start:'Lancer Sandbox',
3257
3261
  },
3258
3262
  de: {
3259
3263
  chat:'Chat', studio:'Studio', settings:'Einstellungen', agents:'Agenten',
@@ -3289,6 +3293,7 @@ var I18N = {
3289
3293
  wc_download:'Archiv herunterladen', wc_describe_first:'Bitte beschreibe zuerst dein Projekt.',
3290
3294
  wc_no_files:'Beschreibe das Projekt und klicke auf Generieren',
3291
3295
  wc_examples_label:'Beispiele',
3296
+ wc_sandbox_start:'Sandbox starten',
3292
3297
  },
3293
3298
  };
3294
3299
  // Fallback to 'en' for unmapped languages
@@ -6212,8 +6217,17 @@ var wcState = {
6212
6217
  generatedFiles: [], // [{name, content, lang}]
6213
6218
  activeFile: 0,
6214
6219
  running: false,
6215
- projectName: ''
6220
+ projectName: '',
6221
+ rightTab: 'files', // 'files' | 'preview'
6222
+ sandbox: {
6223
+ running: false,
6224
+ port: null,
6225
+ dir: null,
6226
+ logs: [],
6227
+ error: null
6228
+ }
6216
6229
  };
6230
+ var wcRightTab = 'files';
6217
6231
 
6218
6232
  function wcEsc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):''}
6219
6233
 
@@ -6312,14 +6326,20 @@ function renderWebCraft(el) {
6312
6326
  '</button>' +
6313
6327
 
6314
6328
  (wcState.generatedFiles.length > 0 ?
6315
- '<button onclick="wcDownloadZip()" style="width:100%;padding:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-size:12px;font-weight:600;cursor:pointer">&#8681; '+t('wc_download')+'</button>' : '') +
6329
+ '<button onclick="wcDownloadZip()" style="width:100%;padding:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-size:12px;font-weight:600;cursor:pointer">&#8681; '+t('wc_download')+'</button>' +
6330
+ '<button onclick="wcStartSandbox()" id="wcSandboxBtn" style="width:100%;padding:10px;background:var(--bg3);border:1px solid var(--green3);border-radius:8px;color:var(--green);font-size:12px;font-weight:600;cursor:pointer">&#9654; '+t('wc_sandbox_start')+'</button>'
6331
+ : '') +
6316
6332
 
6317
6333
  '</div>' +
6318
6334
 
6319
- // RIGHT: file viewer
6335
+ // RIGHT: tabs — Files | Preview
6320
6336
  '<div style="flex:1;min-width:0;background:var(--bg2);border:1px solid var(--border);border-radius:10px;display:flex;flex-direction:column;height:calc(100vh - 120px);overflow:hidden">' +
6321
- fileTabsHtml +
6322
- codeHtml +
6337
+ // Tab bar — use named functions to avoid quoting issues
6338
+ '<div style="display:flex;border-bottom:1px solid var(--border);flex-shrink:0">' +
6339
+ '<button onclick="wcTabFiles()" style="padding:8px 16px;background:'+(wcRightTab==='preview'?'transparent':'var(--bg3)')+';border:none;border-right:1px solid var(--border);color:'+(wcRightTab==='preview'?'var(--dim)':'var(--text)')+';font-size:11px;font-weight:600;cursor:pointer">&#128196; File</button>' +
6340
+ '<button onclick="wcTabPreview()" style="padding:8px 16px;background:'+(wcRightTab==='preview'?'var(--bg3)':'transparent')+';border:none;color:'+(wcRightTab==='preview'?'var(--text)':'var(--dim)')+';font-size:11px;font-weight:600;cursor:pointer">&#127760; Sandbox</button>' +
6341
+ '</div>' +
6342
+ (wcRightTab === 'preview' ? wcSandboxPanelHtml() : (fileTabsHtml + codeHtml)) +
6323
6343
  '</div>' +
6324
6344
 
6325
6345
  '</div>' +
@@ -6341,6 +6361,9 @@ function wcPickExample(i) {
6341
6361
  wcState.description = ex.desc;
6342
6362
  renderWebCraft(document.getElementById('content'));
6343
6363
  }
6364
+ function wcTabFiles() { wcRightTab = 'files'; renderWebCraft(document.getElementById('content')); }
6365
+ function wcTabPreview() { wcRightTab = 'preview'; renderWebCraft(document.getElementById('content')); }
6366
+ function wcOpenSandbox() { if (wcState.sandbox.port) window.open('http://127.0.0.1:' + wcState.sandbox.port, '_blank'); }
6344
6367
  function wcUpdateField(i, val) { wcState.authFields[i].label = val; }
6345
6368
  function wcUpdateFieldType(i, t) { wcState.authFields[i].type = t; }
6346
6369
  function wcToggleRequired(i, v) { wcState.authFields[i].required = v; }
@@ -6402,6 +6425,7 @@ async function wcGenerate() {
6402
6425
  { name: 'server/middleware/sentinel.js', lang: 'javascript', prompt: 'Generate server/middleware/sentinel.js: a lightweight WAF middleware for Express. Check request for: SQL injection patterns (UNION SELECT, DROP TABLE, etc.), XSS patterns (<script, javascript:, onerror=), path traversal (../), oversized payloads (>100KB body). Rate limit by IP using an in-memory sliding window (fallback when Redis unavailable). Log blocked requests with IP, method, path, reason to stderr. Export sentinelMiddleware(req, res, next).' },
6403
6426
  { name: 'server/services/cache.js', lang: 'javascript', prompt: 'Generate server/services/cache.js: Redis/Dragonfly client using ioredis. Connect to REDIS_URL from env. Export: get(key), set(key, value, ttlSeconds), del(key), exists(key). Add circuit breaker pattern: if Redis fails 3+ times in 30s, switch to in-memory LRU fallback (Map with max 1000 entries, LRU eviction). Reconnect Redis in background every 60s. Log circuit state changes. This makes the app resilient when Redis is down.' },
6404
6427
  { name: 'README.md', lang: 'markdown', prompt: 'Generate README.md for "'+projName+'": project description, tech stack (Express, PostgreSQL with circuit breaker, Redis/Dragonfly with LRU fallback, JWT auth, Nodemailer SMTP + SendGrid fallback, Sentinel WAF, BEM CSS), folder structure, setup instructions (clone, npm install, copy .env.example to .env, run migrations with psql, optional: start Redis/Dragonfly, npm run dev), environment variables table (including REDIS_URL), API endpoints table, security notes, email configuration guide.' },
6428
+ { name: 'nginx.conf', lang: 'nginx', prompt: 'Generate a production-ready nginx.conf for "'+projName+'" running as an Express app on localhost:3000. Include: HTTPS with TLS 1.2/1.3 only, strong cipher suites, HSTS (max-age=31536000; includeSubDomains; preload), X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy (geolocation=(), microphone=(), camera=()), Content-Security-Policy (default-src self, script-src self, style-src self unsafe-inline, img-src self data:, connect-src self, frame-src none, object-src none), rate limiting (limit_req_zone 10r/s burst 20), gzip compression, proxy_pass to http://127.0.0.1:3000 with correct headers (X-Real-IP, X-Forwarded-For, X-Forwarded-Proto), static files served directly from public/ with long cache headers, Certbot/ACME path. Use server_name example.com with a TODO comment. Security rating: A+ on securityheaders.com.' },
6405
6429
  ];
6406
6430
 
6407
6431
  // Filter by selected blocks
@@ -6455,6 +6479,116 @@ async function wcCallLLM(sys, user) {
6455
6479
  return (d && (d.text || d.content || d.result)) || '';
6456
6480
  }
6457
6481
 
6482
+ function wcSandboxPanelHtml() {
6483
+ var sb = wcState.sandbox;
6484
+ if (!wcState.generatedFiles.length) {
6485
+ return '<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--dim);font-size:13px">Genera prima il progetto</div>';
6486
+ }
6487
+ var logsHtml = sb.logs.length
6488
+ ? '<div id="wcSbLogs" style="flex:1;overflow-y:auto;padding:10px 14px;font-family:var(--mono);font-size:11px;line-height:1.7;color:var(--dim)">' +
6489
+ sb.logs.map(function(l){ return '<div>'+wcEsc(l)+'</div>'; }).join('') +
6490
+ '</div>'
6491
+ : '<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--dim);font-size:12px">Premi "Avvia Sandbox" per avviare il server locale</div>';
6492
+
6493
+ if (sb.port && !sb.running) {
6494
+ // Server ready — show iframe
6495
+ return '<div style="display:flex;flex-direction:column;flex:1;min-height:0">' +
6496
+ '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0">' +
6497
+ '<span style="font-size:11px;color:var(--dim);font-family:var(--mono)">http://127.0.0.1:'+sb.port+'</span>' +
6498
+ '<span style="font-size:10px;color:var(--dim)">&#8212; ' + wcEsc(sb.dir || '') + '</span>' +
6499
+ '<button onclick="wcStopSandbox()" style="margin-left:auto;padding:3px 10px;background:var(--bg3);border:1px solid var(--border2);border-radius:5px;color:var(--red);font-size:11px;cursor:pointer">&#9632; Ferma</button>' +
6500
+ '<button onclick="wcOpenSandbox()" style="padding:3px 10px;background:var(--green3);border:none;border-radius:5px;color:var(--bg);font-size:11px;cursor:pointer;font-weight:700">&#8599; Apri</button>' +
6501
+ '</div>' +
6502
+ '<iframe src="http://127.0.0.1:'+sb.port+'" style="flex:1;border:none;width:100%;background:#fff" sandbox="allow-scripts allow-forms allow-same-origin allow-popups"></iframe>' +
6503
+ '</div>';
6504
+ }
6505
+
6506
+ return '<div style="display:flex;flex-direction:column;flex:1;min-height:0">' +
6507
+ // Info bar
6508
+ (!sb.running && !sb.port ? '<div style="padding:10px 14px;border-bottom:1px solid var(--border);flex-shrink:0;font-size:11px;color:var(--dim);line-height:1.6">' +
6509
+ '<div style="font-weight:700;color:var(--text);margin-bottom:4px">Sandbox locale — cosa succede quando avvii:</div>' +
6510
+ '<div>&#8226; I file vengono scritti in <span style="font-family:var(--mono);color:var(--green)">~/.nha/webcraft/'+(wcState.projectName||'project')+'</span></div>' +
6511
+ '<div>&#8226; npm install delle dipendenze nella stessa cartella (nessuna installazione globale)</div>' +
6512
+ '<div>&#8226; Il server Express parte su una porta locale casuale (es. 45123)</div>' +
6513
+ '<div>&#8226; Il DB usa un in-memory store (nessun PostgreSQL richiesto)</div>' +
6514
+ '<div>&#8226; I dati sandbox sono temporanei e si azzerano al riavvio</div>' +
6515
+ '<div style="margin-top:6px;color:var(--amber)">&#9888; Sandbox solo locale — nessun dato esce dal tuo Mac</div>' +
6516
+ '</div>' : '') +
6517
+ logsHtml +
6518
+ (sb.error ? '<div style="padding:8px 14px;color:var(--red);font-size:11px;font-family:var(--mono);flex-shrink:0">&#10060; '+wcEsc(sb.error)+'</div>' : '') +
6519
+ '</div>';
6520
+ }
6521
+
6522
+ async function wcStartSandbox() {
6523
+ if (wcState.sandbox.running) return;
6524
+ wcState.sandbox = { running: true, port: null, dir: null, logs: [], error: null };
6525
+ wcState.rightTab = 'preview';
6526
+ wcRightTab = 'preview';
6527
+ renderWebCraft(document.getElementById('content'));
6528
+
6529
+ try {
6530
+ var r = await fetch(API + '/api/studio/webcraft/sandbox/start', {
6531
+ method: 'POST',
6532
+ headers: {'Content-Type':'application/json'},
6533
+ body: JSON.stringify({
6534
+ projectName: wcState.projectName || 'webcraft-sandbox',
6535
+ files: wcState.generatedFiles
6536
+ })
6537
+ });
6538
+ if (!r.ok || !r.body) throw new Error('Sandbox error ' + r.status);
6539
+ var reader = r.body.getReader();
6540
+ var dec = new TextDecoder();
6541
+ var buf = '';
6542
+ while (true) {
6543
+ var chunk = await reader.read();
6544
+ if (chunk.done) break;
6545
+ buf += dec.decode(chunk.value, {stream: true});
6546
+ var _dbl = String.fromCharCode(10)+String.fromCharCode(10);
6547
+ var parts = buf.split(_dbl);
6548
+ buf = parts.pop();
6549
+ for (var i = 0; i < parts.length; i++) {
6550
+ var line = parts[i].trim();
6551
+ if (!line.startsWith('data:')) continue;
6552
+ try {
6553
+ var evt = JSON.parse(line.slice(5).trim());
6554
+ if (evt.type === 'log') {
6555
+ wcState.sandbox.logs.push(evt.msg);
6556
+ // Live update logs only (avoid full re-render on every line)
6557
+ var logsEl = document.getElementById('wcSbLogs');
6558
+ if (logsEl) {
6559
+ var d = document.createElement('div');
6560
+ d.textContent = evt.msg;
6561
+ logsEl.appendChild(d);
6562
+ logsEl.scrollTop = logsEl.scrollHeight;
6563
+ } else {
6564
+ renderWebCraft(document.getElementById('content'));
6565
+ }
6566
+ } else if (evt.type === 'ready') {
6567
+ wcState.sandbox.running = false;
6568
+ wcState.sandbox.port = evt.port;
6569
+ wcState.sandbox.dir = evt.dir;
6570
+ renderWebCraft(document.getElementById('content'));
6571
+ } else if (evt.type === 'error') {
6572
+ wcState.sandbox.running = false;
6573
+ wcState.sandbox.error = evt.msg;
6574
+ renderWebCraft(document.getElementById('content'));
6575
+ }
6576
+ } catch(_) {}
6577
+ }
6578
+ }
6579
+ } catch(e) {
6580
+ wcState.sandbox.running = false;
6581
+ wcState.sandbox.error = e.message;
6582
+ renderWebCraft(document.getElementById('content'));
6583
+ }
6584
+ }
6585
+
6586
+ async function wcStopSandbox() {
6587
+ await fetch(API + '/api/studio/webcraft/sandbox', {method:'DELETE'});
6588
+ wcState.sandbox = { running: false, port: null, dir: null, logs: [], error: null };
6589
+ renderWebCraft(document.getElementById('content'));
6590
+ }
6591
+
6458
6592
  function wcDownloadZip() {
6459
6593
  if (!wcState.generatedFiles.length) return;
6460
6594
  // Build a real ZIP file (PKZIP format, stored/no compression) — zero dependencies