nothumanallowed 13.5.43 → 13.5.45

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.43",
3
+ "version": "13.5.45",
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,81 @@ ${completedHeadings ? `## SECTIONS ALREADY WRITTEN (headings only):\n${completed
3865
3865
  return;
3866
3866
  }
3867
3867
 
3868
+ // ── WebCraft Projects — list and delete saved projects ──────────────────
3869
+ // GET /api/studio/webcraft/projects → { projects: [{name, description, fileCount, createdAt, dir}] }
3870
+ // DELETE /api/studio/webcraft/projects/:name → { ok }
3871
+ // POST /api/studio/webcraft/projects/save → saves meta+files, { ok, dir }
3872
+ if (pathname === '/api/studio/webcraft/projects' && method === 'GET') {
3873
+ const wcBaseDir = path.join(os.homedir(), '.nha', 'webcraft');
3874
+ const projects = [];
3875
+ if (fs.existsSync(wcBaseDir)) {
3876
+ for (const entry of fs.readdirSync(wcBaseDir, { withFileTypes: true })) {
3877
+ if (!entry.isDirectory()) continue;
3878
+ const metaPath = path.join(wcBaseDir, entry.name, 'webcraft-meta.json');
3879
+ if (!fs.existsSync(metaPath)) continue;
3880
+ try {
3881
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
3882
+ projects.push({ name: entry.name, description: meta.description || '', fileCount: (meta.files || []).length, createdAt: meta.createdAt || '', dir: path.join(wcBaseDir, entry.name) });
3883
+ } catch(_) {}
3884
+ }
3885
+ }
3886
+ projects.sort((a, b) => (b.createdAt > a.createdAt ? 1 : -1));
3887
+ sendJSON(res, 200, { projects });
3888
+ logRequest(method, pathname, 200, Date.now() - start);
3889
+ return;
3890
+ }
3891
+
3892
+ if (pathname.startsWith('/api/studio/webcraft/projects/') && method === 'DELETE') {
3893
+ const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/projects/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
3894
+ if (!projName) { sendJSON(res, 400, { error: 'invalid name' }); return; }
3895
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
3896
+ if (fs.existsSync(projDir)) fs.rmSync(projDir, { recursive: true, force: true });
3897
+ sendJSON(res, 200, { ok: true });
3898
+ logRequest(method, pathname, 200, Date.now() - start);
3899
+ return;
3900
+ }
3901
+
3902
+ if (pathname === '/api/studio/webcraft/projects/save' && method === 'POST') {
3903
+ const body = await parseBody(req, 16 * 1024 * 1024); // 16MB
3904
+ const projName = (body.projectName || 'webcraft').replace(/[^a-zA-Z0-9_-]/g, '-');
3905
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
3906
+ fs.mkdirSync(projDir, { recursive: true });
3907
+ // Write each file with full folder structure
3908
+ for (const f of (body.files || [])) {
3909
+ const fp = path.join(projDir, f.name);
3910
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
3911
+ fs.writeFileSync(fp, f.content, 'utf8');
3912
+ }
3913
+ // Write meta
3914
+ const meta = { projectName: projName, description: body.description || '', files: (body.files || []).map(f => f.name), createdAt: new Date().toISOString() };
3915
+ fs.writeFileSync(path.join(projDir, 'webcraft-meta.json'), JSON.stringify(meta, null, 2), 'utf8');
3916
+ sendJSON(res, 200, { ok: true, dir: projDir });
3917
+ logRequest(method, pathname, 200, Date.now() - start);
3918
+ return;
3919
+ }
3920
+
3921
+ // GET /api/studio/webcraft/projects/load/:name → { projectName, description, files[] }
3922
+ if (pathname.startsWith('/api/studio/webcraft/projects/load/') && method === 'GET') {
3923
+ const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/projects/load/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
3924
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
3925
+ const metaPath = path.join(projDir, 'webcraft-meta.json');
3926
+ if (!fs.existsSync(metaPath)) { sendJSON(res, 404, { error: 'not found' }); return; }
3927
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
3928
+ const files = [];
3929
+ for (const fname of (meta.files || [])) {
3930
+ const fp = path.join(projDir, fname);
3931
+ if (fs.existsSync(fp)) {
3932
+ const content = fs.readFileSync(fp, 'utf8');
3933
+ const ext = fname.split('.').pop();
3934
+ const langMap = { js:'javascript', mjs:'javascript', ts:'typescript', json:'json', html:'html', css:'css', sql:'sql', md:'markdown', sh:'bash', env:'bash', conf:'nginx' };
3935
+ files.push({ name: fname, content, lang: langMap[ext] || 'text' });
3936
+ }
3937
+ }
3938
+ sendJSON(res, 200, { projectName: meta.projectName, description: meta.description, files });
3939
+ logRequest(method, pathname, 200, Date.now() - start);
3940
+ return;
3941
+ }
3942
+
3868
3943
  // ── WebCraft Sandbox — fullstack preview in ~/.nha/webcraft/<project> ──
3869
3944
  // POST /api/studio/webcraft/sandbox/start { projectName, files[] }
3870
3945
  // → SSE stream with log lines, ends with { type:'ready', port, dir }
@@ -4052,8 +4127,9 @@ module.exports = { pool, query, transaction };
4052
4127
  sendLog('✅ npm install completato');
4053
4128
 
4054
4129
  // Find free port
4130
+ const { default: netMod } = await import('net');
4055
4131
  const freePort = await new Promise(resolve => {
4056
- const srv = (await import('net')).default.createServer();
4132
+ const srv = netMod.createServer();
4057
4133
  srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => resolve(p)); });
4058
4134
  });
4059
4135
 
@@ -4080,15 +4156,13 @@ module.exports = { pool, query, transaction };
4080
4156
  await new Promise((resolve, reject) => {
4081
4157
  let attempts = 0;
4082
4158
  const tryConnect = () => {
4083
- import('net').then(({ default: net }) => {
4084
- const s = net.createConnection(freePort, '127.0.0.1');
4159
+ const s = netMod.createConnection(freePort, '127.0.0.1');
4085
4160
  s.on('connect', () => { s.destroy(); resolve(); });
4086
4161
  s.on('error', () => {
4087
4162
  s.destroy();
4088
4163
  if (++attempts > 20) reject(new Error('Server non risponde dopo 10s'));
4089
4164
  else setTimeout(tryConnect, 500);
4090
4165
  });
4091
- });
4092
4166
  };
4093
4167
  setTimeout(tryConnect, 1000);
4094
4168
  });
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.43';
8
+ export const VERSION = '13.5.45';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -3149,6 +3149,9 @@ var I18N = {
3149
3149
  wc_no_files:'Describe your project and click Generate',
3150
3150
  wc_examples_label:'Examples',
3151
3151
  wc_sandbox_start:'Launch Sandbox',
3152
+ wc_projects:'Projects',
3153
+ wc_no_projects:'No saved projects yet',
3154
+ wc_no_projects_hint:'Generate a project and it will be saved automatically',
3152
3155
  },
3153
3156
  it: {
3154
3157
  chat:'Chat', studio:'Studio', settings:'Impostazioni', agents:'Agenti',
@@ -3186,6 +3189,9 @@ var I18N = {
3186
3189
  wc_no_files:'Descrivi il progetto e clicca Genera',
3187
3190
  wc_examples_label:'Esempi',
3188
3191
  wc_sandbox_start:'Avvia Sandbox',
3192
+ wc_projects:'Progetti',
3193
+ wc_no_projects:'Nessun progetto salvato',
3194
+ wc_no_projects_hint:'Genera un progetto e verr\u00e0 salvato automaticamente',
3189
3195
  },
3190
3196
  es: {
3191
3197
  chat:'Chat', studio:'Studio', settings:'Configuración', agents:'Agentes',
@@ -3222,6 +3228,9 @@ var I18N = {
3222
3228
  wc_no_files:'Describe el proyecto y haz clic en Generar',
3223
3229
  wc_examples_label:'Ejemplos',
3224
3230
  wc_sandbox_start:'Iniciar Sandbox',
3231
+ wc_projects:'Proyectos',
3232
+ wc_no_projects:'No hay proyectos guardados',
3233
+ wc_no_projects_hint:'Genera un proyecto y se guardar\u00e1 autom\u00e1ticamente',
3225
3234
  },
3226
3235
  fr: {
3227
3236
  chat:'Chat', studio:'Studio', settings:'Paramètres', agents:'Agents',
@@ -3258,6 +3267,9 @@ var I18N = {
3258
3267
  wc_no_files:'D\u00e9crivez le projet et cliquez sur G\u00e9n\u00e9rer',
3259
3268
  wc_examples_label:'Exemples',
3260
3269
  wc_sandbox_start:'Lancer Sandbox',
3270
+ wc_projects:'Projets',
3271
+ wc_no_projects:'Aucun projet sauvegard\u00e9',
3272
+ wc_no_projects_hint:'G\u00e9n\u00e9rez un projet et il sera sauvegard\u00e9 automatiquement',
3261
3273
  },
3262
3274
  de: {
3263
3275
  chat:'Chat', studio:'Studio', settings:'Einstellungen', agents:'Agenten',
@@ -3294,6 +3306,9 @@ var I18N = {
3294
3306
  wc_no_files:'Beschreibe das Projekt und klicke auf Generieren',
3295
3307
  wc_examples_label:'Beispiele',
3296
3308
  wc_sandbox_start:'Sandbox starten',
3309
+ wc_projects:'Projekte',
3310
+ wc_no_projects:'Keine gespeicherten Projekte',
3311
+ wc_no_projects_hint:'Generiere ein Projekt und es wird automatisch gespeichert',
3297
3312
  },
3298
3313
  };
3299
3314
  // Fallback to 'en' for unmapped languages
@@ -6228,6 +6243,8 @@ var wcState = {
6228
6243
  }
6229
6244
  };
6230
6245
  var wcRightTab = 'files';
6246
+ var wcMainTab = 'new'; // 'new' | 'projects'
6247
+ var wcProjectsList = []; // cached list from server
6231
6248
 
6232
6249
  function wcEsc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):''}
6233
6250
 
@@ -6276,74 +6293,68 @@ function renderWebCraft(el) {
6276
6293
  }).join('') +
6277
6294
  '</div></div>';
6278
6295
 
6279
- el.innerHTML =
6280
- '<div style="display:flex;flex-direction:column;height:100%;min-height:0;padding:0 4px">' +
6281
- '<div style="margin-bottom:10px;flex-shrink:0">' +
6282
- '<h2 style="font-size:15px;color:var(--green);margin-bottom:4px">&#128736; '+t('wc_title')+'</h2>' +
6296
+ var headerHtml =
6297
+ '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;flex-shrink:0">' +
6298
+ '<div>' +
6299
+ '<h2 style="font-size:15px;color:var(--green);margin-bottom:2px">&#128736; '+t('wc_title')+'</h2>' +
6283
6300
  '<p style="font-size:11px;color:var(--dim);line-height:1.5">'+t('wc_subtitle')+'</p>' +
6284
6301
  '</div>' +
6302
+ '<div style="display:flex;gap:6px;flex-shrink:0">' +
6303
+ '<button onclick="wcMainTabNew()" style="padding:5px 14px;border-radius:6px;border:1px solid var(--border2);background:'+(wcMainTab==='new'?'var(--green3)':'var(--bg3)')+';color:'+(wcMainTab==='new'?'var(--bg)':'var(--dim)')+';font-size:11px;font-weight:600;cursor:pointer">+ Nuovo</button>' +
6304
+ '<button onclick="wcMainTabProjects()" style="padding:5px 14px;border-radius:6px;border:1px solid var(--border2);background:'+(wcMainTab==='projects'?'var(--green3)':'var(--bg3)')+';color:'+(wcMainTab==='projects'?'var(--bg)':'var(--dim)')+';font-size:11px;font-weight:600;cursor:pointer">&#128193; '+t('wc_projects')+'</button>' +
6305
+ '</div>' +
6306
+ '</div>';
6285
6307
 
6286
- wcExHtml +
6287
-
6288
- '<div style="display:flex;gap:14px;align-items:flex-start;flex:1;min-height:0">' +
6289
-
6290
- // LEFT: config panel
6291
- '<div style="width:280px;flex-shrink:0;display:flex;flex-direction:column;gap:10px;overflow-y:auto;max-height:calc(100vh - 120px)">' +
6292
-
6293
- // Project name + description
6294
- '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px">' +
6295
- '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px">'+t('wc_project')+'</div>' +
6296
- '<input id="wcProjectName" placeholder="'+t('wc_project_name')+'" value="'+wcEsc(wcState.projectName)+'" oninput="wcState.projectName=this.value" style="width:100%;padding:8px 10px;font-size:12px;border-radius:6px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);margin-bottom:8px;box-sizing:border-box">' +
6297
- '<textarea id="wcDesc" placeholder="'+t('wc_desc')+' e.g. SaaS landing page with user registration, dashboard, Stripe-ready pricing section" rows="4" oninput="wcState.description=this.value" style="width:100%;padding:8px 10px;font-size:12px;border-radius:6px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);resize:vertical;box-sizing:border-box;line-height:1.5">'+wcEsc(wcState.description)+'</textarea>' +
6298
- '</div>' +
6299
-
6300
- // Blocks
6301
- '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px">' +
6302
- '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px;margin-bottom:10px">'+t('wc_blocks')+'</div>' +
6303
- ['auth','cookieBanner','securityMiddleware','emailVerification'].map(function(b){
6304
- var labels = {auth:'Auth (register/login/JWT)',cookieBanner:'GDPR Cookie Banner',securityMiddleware:'Security Middleware',emailVerification:'Email Verification'};
6305
- var icons = {auth:'&#128274;',cookieBanner:'&#127850;',securityMiddleware:'&#128737;',emailVerification:'&#9993;'};
6306
- return '<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;font-size:11px;color:var(--text)">' +
6307
- '<input type="checkbox"'+(wcState.blocks[b]?' checked':'')+' onchange="wcState.blocks['+JSON.stringify(b)+']=this.checked" style="accent-color:var(--green3);width:14px;height:14px">' +
6308
- '<span>'+icons[b]+'</span><span>'+labels[b]+'</span>' +
6309
- '</label>';
6310
- }).join('') +
6311
- '</div>' +
6312
-
6313
- // Auth fields configurator
6314
- '<div id="wcAuthFieldsPanel" style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px;'+(wcState.blocks.auth?'':'display:none')+'">' +
6315
- '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
6316
- '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px">'+t('wc_auth_fields')+'</div>' +
6317
- '<button onclick="wcAddField()" style="font-size:10px;padding:3px 8px;background:var(--bg3);border:1px solid var(--border2);border-radius:5px;color:var(--green);cursor:pointer">'+t('wc_add_field')+'</button>' +
6318
- '</div>' +
6319
- '<div id="wcFieldsList">'+authFieldsHtml+'</div>' +
6320
- '<div style="font-size:9px;color:var(--dim);margin-top:4px">'+t('wc_required_hint')+'</div>' +
6321
- '</div>' +
6322
-
6323
- // Generate button
6324
- '<button id="wcRunBtn" onclick="wcGenerate()" style="width:100%;padding:12px;background:var(--green3);border:none;border-radius:8px;color:var(--bg);font-size:13px;font-weight:700;cursor:pointer;letter-spacing:.2px"'+(wcState.running?' disabled':'')+'>'+
6325
- (wcState.running ? '&#9203; '+t('wc_generating')+'...' : '&#9654; '+t('wc_generate')) +
6326
- '</button>' +
6327
-
6328
- (wcState.generatedFiles.length > 0 ?
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
- : '') +
6332
-
6308
+ var editorHtml =
6309
+ wcExHtml +
6310
+ '<div style="display:flex;gap:14px;align-items:flex-start;flex:1;min-height:0">' +
6311
+ '<div style="width:280px;flex-shrink:0;display:flex;flex-direction:column;gap:10px;overflow-y:auto;max-height:calc(100vh - 120px)">' +
6312
+ '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px">' +
6313
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px">'+t('wc_project')+'</div>' +
6314
+ '<input id="wcProjectName" placeholder="'+t('wc_project_name')+'" value="'+wcEsc(wcState.projectName)+'" oninput="wcState.projectName=this.value" style="width:100%;padding:8px 10px;font-size:12px;border-radius:6px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);margin-bottom:8px;box-sizing:border-box">' +
6315
+ '<textarea id="wcDesc" placeholder="'+t('wc_desc')+'" rows="4" oninput="wcState.description=this.value" style="width:100%;padding:8px 10px;font-size:12px;border-radius:6px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);resize:vertical;box-sizing:border-box;line-height:1.5">'+wcEsc(wcState.description)+'</textarea>' +
6333
6316
  '</div>' +
6334
-
6335
- // RIGHT: tabs — Files | Preview
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">' +
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>' +
6317
+ '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px">' +
6318
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px;margin-bottom:10px">'+t('wc_blocks')+'</div>' +
6319
+ ['auth','cookieBanner','securityMiddleware','emailVerification'].map(function(b){
6320
+ var labels = {auth:'Auth (register/login/JWT)',cookieBanner:'GDPR Cookie Banner',securityMiddleware:'Security Middleware',emailVerification:'Email Verification'};
6321
+ var icons = {auth:'&#128274;',cookieBanner:'&#127850;',securityMiddleware:'&#128737;',emailVerification:'&#9993;'};
6322
+ return '<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;font-size:11px;color:var(--text)">' +
6323
+ '<input type="checkbox"'+(wcState.blocks[b]?' checked':'')+' onchange="wcState.blocks['+JSON.stringify(b)+']=this.checked" style="accent-color:var(--green3);width:14px;height:14px">' +
6324
+ '<span>'+icons[b]+'</span><span>'+labels[b]+'</span>' +
6325
+ '</label>';
6326
+ }).join('') +
6327
+ '</div>' +
6328
+ '<div id="wcAuthFieldsPanel" style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px;'+(wcState.blocks.auth?'':'display:none')+'">' +
6329
+ '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
6330
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px">'+t('wc_auth_fields')+'</div>' +
6331
+ '<button onclick="wcAddField()" style="font-size:10px;padding:3px 8px;background:var(--bg3);border:1px solid var(--border2);border-radius:5px;color:var(--green);cursor:pointer">'+t('wc_add_field')+'</button>' +
6341
6332
  '</div>' +
6342
- (wcRightTab === 'preview' ? wcSandboxPanelHtml() : (fileTabsHtml + codeHtml)) +
6333
+ '<div id="wcFieldsList">'+authFieldsHtml+'</div>' +
6334
+ '<div style="font-size:9px;color:var(--dim);margin-top:4px">'+t('wc_required_hint')+'</div>' +
6343
6335
  '</div>' +
6344
-
6336
+ '<button id="wcRunBtn" onclick="wcGenerate()" style="width:100%;padding:12px;background:var(--green3);border:none;border-radius:8px;color:var(--bg);font-size:13px;font-weight:700;cursor:pointer;letter-spacing:.2px"'+(wcState.running?' disabled':'')+'>'+
6337
+ (wcState.running ? '&#9203; '+t('wc_generating')+'...' : '&#9654; '+t('wc_generate')) +
6338
+ '</button>' +
6339
+ (wcState.generatedFiles.length > 0 ?
6340
+ '<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>' +
6341
+ '<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>'
6342
+ : '') +
6343
+ '</div>' +
6344
+ '<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">' +
6345
+ '<div style="display:flex;border-bottom:1px solid var(--border);flex-shrink:0">' +
6346
+ '<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>' +
6347
+ '<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>' +
6348
+ '</div>' +
6349
+ (wcRightTab === 'preview' ? wcSandboxPanelHtml() : (fileTabsHtml + codeHtml)) +
6345
6350
  '</div>' +
6346
6351
  '</div>';
6352
+
6353
+ el.innerHTML =
6354
+ '<div style="display:flex;flex-direction:column;height:100%;min-height:0;padding:0 4px">' +
6355
+ headerHtml +
6356
+ (wcMainTab === 'projects' ? wcProjectsPanelHtml() : editorHtml) +
6357
+ '</div>';
6347
6358
  }
6348
6359
 
6349
6360
  function wcPickExample(i) {
@@ -6364,6 +6375,69 @@ function wcPickExample(i) {
6364
6375
  function wcTabFiles() { wcRightTab = 'files'; renderWebCraft(document.getElementById('content')); }
6365
6376
  function wcTabPreview() { wcRightTab = 'preview'; renderWebCraft(document.getElementById('content')); }
6366
6377
  function wcOpenSandbox() { if (wcState.sandbox.port) window.open('http://127.0.0.1:' + wcState.sandbox.port, '_blank'); }
6378
+
6379
+ function wcMainTabNew() { wcMainTab = 'new'; renderWebCraft(document.getElementById('content')); }
6380
+ function wcMainTabProjects() {
6381
+ wcMainTab = 'projects';
6382
+ renderWebCraft(document.getElementById('content'));
6383
+ // Load projects list from server
6384
+ fetch(API + '/api/studio/webcraft/projects').then(function(r){ return r.json(); }).then(function(d){
6385
+ wcProjectsList = d.projects || [];
6386
+ renderWebCraft(document.getElementById('content'));
6387
+ }).catch(function(){});
6388
+ }
6389
+
6390
+ function wcProjectsPanelHtml() {
6391
+ if (!wcProjectsList.length) {
6392
+ return '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;gap:10px;padding:40px;color:var(--dim)">' +
6393
+ '<div style="font-size:32px">&#128193;</div>' +
6394
+ '<div style="font-size:13px">'+t('wc_no_projects')+'</div>' +
6395
+ '<div style="font-size:11px">'+t('wc_no_projects_hint')+'</div>' +
6396
+ '</div>';
6397
+ }
6398
+ return '<div style="flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding:4px 0">' +
6399
+ wcProjectsList.map(function(p, pi){
6400
+ var date = p.createdAt ? new Date(p.createdAt).toLocaleString() : '';
6401
+ return '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px 16px;display:flex;align-items:center;gap:12px">' +
6402
+ '<div style="flex:1;min-width:0">' +
6403
+ '<div style="font-size:13px;font-weight:700;color:var(--text);margin-bottom:2px">'+wcEsc(p.name)+'</div>' +
6404
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+wcEsc(p.description||'')+'</div>' +
6405
+ '<div style="display:flex;gap:10px;font-size:10px;color:var(--dim)">' +
6406
+ '<span>&#128196; '+p.fileCount+' file</span>' +
6407
+ '<span>&#128197; '+date+'</span>' +
6408
+ '<span style="font-family:var(--mono);font-size:9px">'+wcEsc(p.dir||'')+'</span>' +
6409
+ '</div>' +
6410
+ '</div>' +
6411
+ '<button onclick="wcLoadProject('+pi+')" style="padding:6px 14px;background:var(--green3);border:none;border-radius:6px;color:var(--bg);font-size:11px;font-weight:700;cursor:pointer;flex-shrink:0">&#8599; Apri</button>' +
6412
+ '<button onclick="wcDeleteProject('+pi+')" style="padding:6px 10px;background:var(--bg3);border:1px solid var(--border2);border-radius:6px;color:var(--red);font-size:11px;cursor:pointer;flex-shrink:0">&#128465;</button>' +
6413
+ '</div>';
6414
+ }).join('') +
6415
+ '</div>';
6416
+ }
6417
+
6418
+ async function wcLoadProject(pi) {
6419
+ var p = wcProjectsList[pi];
6420
+ if (!p) return;
6421
+ var r = await fetch(API + '/api/studio/webcraft/projects/load/' + encodeURIComponent(p.name));
6422
+ if (!r.ok) return;
6423
+ var d = await r.json();
6424
+ wcState.projectName = d.projectName || p.name;
6425
+ wcState.description = d.description || '';
6426
+ wcState.generatedFiles = d.files || [];
6427
+ wcState.activeFile = 0;
6428
+ wcMainTab = 'new';
6429
+ wcRightTab = 'files';
6430
+ renderWebCraft(document.getElementById('content'));
6431
+ }
6432
+
6433
+ async function wcDeleteProject(pi) {
6434
+ var p = wcProjectsList[pi];
6435
+ if (!p) return;
6436
+ if (!confirm('Eliminare: ' + p.name + ' - ' + p.dir + ' ?')) return;
6437
+ await fetch(API + '/api/studio/webcraft/projects/' + encodeURIComponent(p.name), {method:'DELETE'});
6438
+ wcProjectsList.splice(pi, 1);
6439
+ renderWebCraft(document.getElementById('content'));
6440
+ }
6367
6441
  function wcUpdateField(i, val) { wcState.authFields[i].label = val; }
6368
6442
  function wcUpdateFieldType(i, t) { wcState.authFields[i].type = t; }
6369
6443
  function wcToggleRequired(i, v) { wcState.authFields[i].required = v; }
@@ -6465,6 +6539,16 @@ async function wcGenerate() {
6465
6539
  }
6466
6540
 
6467
6541
  wcState.running = false;
6542
+
6543
+ // Auto-save project to ~/.nha/webcraft/<projectName>/
6544
+ try {
6545
+ await fetch(API + '/api/studio/webcraft/projects/save', {
6546
+ method: 'POST',
6547
+ headers: {'Content-Type':'application/json'},
6548
+ body: JSON.stringify({ projectName: wcState.projectName, description: wcState.description, files: wcState.generatedFiles })
6549
+ });
6550
+ } catch(_) {}
6551
+
6468
6552
  renderWebCraft(document.getElementById('content'));
6469
6553
  }
6470
6554