nothumanallowed 13.5.34 → 13.5.36

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.34",
3
+ "version": "13.5.36",
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": {
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.34';
8
+ export const VERSION = '13.5.36';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -257,10 +257,12 @@ function switchView(v) {
257
257
  function openSidebar() {
258
258
  document.getElementById('sidebar').classList.add('sidebar--open');
259
259
  document.getElementById('overlay').classList.add('sidebar__overlay--open');
260
+ var mb = document.getElementById('mobileBurger'); if (mb) mb.style.display = 'none';
260
261
  }
261
262
  function closeSidebar() {
262
263
  document.getElementById('sidebar').classList.remove('sidebar--open');
263
264
  document.getElementById('overlay').classList.remove('sidebar__overlay--open');
265
+ var mb = document.getElementById('mobileBurger'); if (mb) mb.style.display = '';
264
266
  }
265
267
  function toggleSidebar() {
266
268
  var sb = document.getElementById('sidebar');
@@ -324,6 +326,7 @@ function render(){
324
326
  case 'birthdays':renderBirthdays(el);break;
325
327
  case 'agents':renderAgents(el);break;
326
328
  case 'studio':renderStudio(el);break;
329
+ case 'webcraft':renderWebCraft(el);break;
327
330
  case 'collab':renderCollab(el);break;
328
331
  case 'settings':renderSettings(el);break;
329
332
  }
@@ -3305,6 +3308,10 @@ function renderSidebar() {
3305
3308
  \x27<span class="nav-item__icon">&#9881;</span> \x27+t(\x27nav_studio\x27)+
3306
3309
  \x27<span style="font-size:8px;padding:1px 5px;border-radius:4px;background:rgba(99,102,241,.25);color:var(--green);margin-left:4px;font-weight:700">NEW</span>\x27+
3307
3310
  \x27</div>\x27+
3311
+ \x27<div class="nav-item\x27+(activeView===\x27webcraft\x27?\x27 nav-item--active\x27:\x27\x27)+\x27" data-view="webcraft" onclick="switchView(\\\x27webcraft\\\x27)">\x27+
3312
+ \x27<span class="nav-item__icon">&#128736;</span> WebCraft\x27+
3313
+ \x27<span style="font-size:8px;padding:1px 5px;border-radius:4px;background:rgba(99,102,241,.25);color:var(--green);margin-left:4px;font-weight:700">NEW</span>\x27+
3314
+ \x27</div>\x27+
3308
3315
  \x27<div class="nav-item\x27+(activeView===\x27collab\x27?\x27 nav-item--active\x27:\x27\x27)+\x27" data-view="collab" onclick="switchView(\\\x27collab\\\x27)">\x27+
3309
3316
  \x27<span class="nav-item__icon">&#128274;</span> \x27+t(\x27nav_collab\x27)+
3310
3317
  \x27<span id="collabBadge" style="display:none;background:var(--red);color:#fff;font-size:9px;padding:1px 5px;border-radius:8px;margin-left:4px;font-family:var(--mono)">0</span>\x27+
@@ -3338,7 +3345,7 @@ var studioAbortController = null;
3338
3345
  var parlActiveAgent = null; // active agent label during parliament streaming
3339
3346
  var parlDoneAgents = {}; // set of completed agent labels during parliament
3340
3347
  var _parlPersistHtml = null; // persists parliament block HTML across tab navigations
3341
- var _PARL_STAMP = '<!--nha-parl-v13.5.34-->';
3348
+ var _PARL_STAMP = '<!--nha-parl-v13.5.36-->';
3342
3349
 
3343
3350
  function stopStudio() {
3344
3351
  if (!studioState.running) return;
@@ -5863,8 +5870,9 @@ function renderStudio(el) {
5863
5870
  '<h2>&#9881; NHA Studio</h2>' +
5864
5871
  '<p>Build a pipeline manually — click agents to add them in order — or describe your task in natural language and let Studio plan it automatically.</p>' +
5865
5872
  '</div>' +
5866
- '<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start">' +
5873
+ '<div style="display:flex;gap:16px;align-items:flex-start" id="studioMainRow">' +
5867
5874
  '<div style="flex:1;min-width:0">' +
5875
+ '<button class="studio-sidebar-toggle" onclick="(function(){var sb=document.getElementById(\\x27studioSidebar\\x27);sb.classList.toggle(\\x27studio-sidebar--open\\x27)})()" title="Tools &amp; Agents">&#128295; Tools &amp; Agents</button>' +
5868
5876
 
5869
5877
  // ── MODE TABS ──
5870
5878
  '<div style="display:flex;gap:0;margin-bottom:14px;border:1px solid var(--border);border-radius:8px;overflow:hidden">' +
@@ -5921,7 +5929,7 @@ function renderStudio(el) {
5921
5929
  '</div>' +
5922
5930
 
5923
5931
  // ── AGENT SIDEBAR ──
5924
- '<div style="display:flex;flex-direction:column;gap:12px;width:220px;flex-shrink:0;position:sticky;top:16px;align-self:flex-start">' +
5932
+ '<div id="studioSidebar" class="studio-sidebar">' +
5925
5933
  '<div class="studio-tools-panel">' +
5926
5934
  // Tab bar
5927
5935
  '<div style="display:flex;gap:0;margin-bottom:10px;border:1px solid var(--border);border-radius:6px;overflow:hidden">' +
@@ -6147,6 +6155,238 @@ function init(){
6147
6155
  },3000);
6148
6156
  }
6149
6157
  init();
6158
+
6159
+ // ---- WEBCRAFT ----
6160
+ var wcState = {
6161
+ description: '',
6162
+ authFields: [{name:'firstName',label:'First name',type:'text',required:true},{name:'lastName',label:'Last name',type:'text',required:true},{name:'email',label:'Email',type:'email',required:true},{name:'password',label:'Password',type:'password',required:true}],
6163
+ blocks: {auth:true, cookieBanner:true, securityMiddleware:true, emailVerification:true},
6164
+ generatedFiles: [], // [{name, content, lang}]
6165
+ activeFile: 0,
6166
+ running: false,
6167
+ projectName: ''
6168
+ };
6169
+
6170
+ function wcEsc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):''}
6171
+
6172
+ function renderWebCraft(el) {
6173
+ var fileTabsHtml = wcState.generatedFiles.length > 0
6174
+ ? '<div style="display:flex;gap:0;overflow-x:auto;border-bottom:1px solid var(--border);margin-bottom:0;flex-shrink:0">' +
6175
+ wcState.generatedFiles.map(function(f,i){
6176
+ var active = i === wcState.activeFile;
6177
+ return '<button onclick="wcSetFile('+i+')" style="padding:6px 14px;font-size:11px;font-family:var(--mono);font-weight:'+(active?'700':'400')+';background:'+(active?'var(--bg3)':'transparent')+';border:none;border-bottom:2px solid '+(active?'var(--green3)':'transparent')+';color:'+(active?'var(--green)':'var(--dim)')+';cursor:pointer;white-space:nowrap;flex-shrink:0">'+wcEsc(f.name)+'</button>';
6178
+ }).join('') +
6179
+ '</div>'
6180
+ : '';
6181
+
6182
+ var codeHtml = wcState.generatedFiles.length > 0 && wcState.generatedFiles[wcState.activeFile]
6183
+ ? '<div id="wcCodeWrap" style="flex:1;overflow:auto;background:var(--bg3);border-radius:0 0 8px 8px">' +
6184
+ '<pre style="margin:0;padding:14px 16px;font-size:11px;line-height:1.6;color:var(--text);font-family:var(--mono);white-space:pre-wrap;word-break:break-all">'+wcEsc(wcState.generatedFiles[wcState.activeFile].content)+'</pre>' +
6185
+ '</div>'
6186
+ : '<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--dim);font-size:12px;flex-direction:column;gap:8px">' +
6187
+ '<span style="font-size:36px;opacity:.25">&#128736;</span>' +
6188
+ '<span>Describe your project and click Generate</span>' +
6189
+ '</div>';
6190
+
6191
+ var authFieldsHtml = wcState.authFields.map(function(f,i){
6192
+ return '<div style="display:flex;align-items:center;gap:6px;padding:5px 8px;background:var(--bg3);border-radius:6px;margin-bottom:4px">' +
6193
+ '<input value="'+wcEsc(f.label)+'" onchange="wcUpdateField('+i+',this.value)" style="flex:1;background:transparent;border:none;color:var(--text);font-size:11px;font-family:var(--mono)" />' +
6194
+ '<select onchange="wcUpdateFieldType('+i+',this.value)" style="background:var(--bg2);border:1px solid var(--border);border-radius:4px;color:var(--dim);font-size:10px;padding:2px 4px">' +
6195
+ ['text','email','password','tel','date','number'].map(function(t){return '<option value="'+t+'"'+(f.type===t?' selected':'')+'>'+t+'</option>'}).join('') +
6196
+ '</select>' +
6197
+ '<input type="checkbox"'+(f.required?' checked':'')+' onchange="wcToggleRequired('+i+',this.checked)" title="Required" style="accent-color:var(--green3)">' +
6198
+ '<button onclick="wcRemoveField('+i+')" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:13px;line-height:1;padding:0 2px">&times;</button>' +
6199
+ '</div>';
6200
+ }).join('');
6201
+
6202
+ el.innerHTML =
6203
+ '<div style="max-width:1100px;margin:0 auto;padding:0 8px">' +
6204
+ '<div style="margin-bottom:18px">' +
6205
+ '<h2 style="font-size:15px;color:var(--green);margin-bottom:4px">&#128736; WebCraft</h2>' +
6206
+ '<p style="font-size:11px;color:var(--dim);line-height:1.5">Generate enterprise-grade web projects — security headers A+, BEM CSS, PostgreSQL pool, Auth, GDPR cookie banner.</p>' +
6207
+ '</div>' +
6208
+
6209
+ '<div style="display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap">' +
6210
+
6211
+ // LEFT: config panel
6212
+ '<div style="width:300px;flex-shrink:0;display:flex;flex-direction:column;gap:12px">' +
6213
+
6214
+ // Project name + description
6215
+ '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px">' +
6216
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px">Project</div>' +
6217
+ '<input id="wcProjectName" placeholder="Project name (e.g. MySaaS)" 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">' +
6218
+ '<textarea id="wcDesc" placeholder="Describe your project... 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>' +
6219
+ '</div>' +
6220
+
6221
+ // Blocks
6222
+ '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px">' +
6223
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px;margin-bottom:10px">Included Blocks</div>' +
6224
+ ['auth','cookieBanner','securityMiddleware','emailVerification'].map(function(b){
6225
+ var labels = {auth:'Auth (register/login/JWT)',cookieBanner:'GDPR Cookie Banner',securityMiddleware:'Security Middleware',emailVerification:'Email Verification'};
6226
+ var icons = {auth:'&#128274;',cookieBanner:'&#127850;',securityMiddleware:'&#128737;',emailVerification:'&#9993;'};
6227
+ return '<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;font-size:11px;color:var(--text)">' +
6228
+ '<input type="checkbox"'+(wcState.blocks[b]?' checked':'')+' onchange="wcState.blocks['+JSON.stringify(b)+']=this.checked" style="accent-color:var(--green3);width:14px;height:14px">' +
6229
+ '<span>'+icons[b]+'</span><span>'+labels[b]+'</span>' +
6230
+ '</label>';
6231
+ }).join('') +
6232
+ '</div>' +
6233
+
6234
+ // Auth fields configurator
6235
+ '<div id="wcAuthFieldsPanel" style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px;'+(wcState.blocks.auth?'':'display:none')+'">' +
6236
+ '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
6237
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px">Auth Fields</div>' +
6238
+ '<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">+ Add field</button>' +
6239
+ '</div>' +
6240
+ '<div id="wcFieldsList">'+authFieldsHtml+'</div>' +
6241
+ '<div style="font-size:9px;color:var(--dim);margin-top:4px">&#9745; = Required &nbsp;|&nbsp; Edit label &amp; type inline</div>' +
6242
+ '</div>' +
6243
+
6244
+ // Generate button
6245
+ '<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':'')+'>'+
6246
+ (wcState.running ? '&#9203; Generating...' : '&#9654; Generate Project') +
6247
+ '</button>' +
6248
+
6249
+ (wcState.generatedFiles.length > 0 ?
6250
+ '<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; Download ZIP</button>' : '') +
6251
+
6252
+ '</div>' +
6253
+
6254
+ // RIGHT: file viewer
6255
+ '<div style="flex:1;min-width:0;background:var(--bg2);border:1px solid var(--border);border-radius:10px;display:flex;flex-direction:column;min-height:520px;overflow:hidden">' +
6256
+ fileTabsHtml +
6257
+ codeHtml +
6258
+ '</div>' +
6259
+
6260
+ '</div>' +
6261
+ '</div>';
6262
+ }
6263
+
6264
+ function wcUpdateField(i, val) { wcState.authFields[i].label = val; }
6265
+ function wcUpdateFieldType(i, t) { wcState.authFields[i].type = t; }
6266
+ function wcToggleRequired(i, v) { wcState.authFields[i].required = v; }
6267
+ function wcRemoveField(i) { wcState.authFields.splice(i,1); renderWebCraft(document.getElementById('content')); }
6268
+ function wcAddField() {
6269
+ wcState.authFields.push({name:'field'+wcState.authFields.length,label:'New field',type:'text',required:false});
6270
+ renderWebCraft(document.getElementById('content'));
6271
+ }
6272
+ function wcSetFile(i) { wcState.activeFile = i; renderWebCraft(document.getElementById('content')); }
6273
+
6274
+ async function wcGenerate() {
6275
+ if (wcState.running) return;
6276
+ var desc = (document.getElementById('wcDesc')||{}).value || wcState.description;
6277
+ var projName = (document.getElementById('wcProjectName')||{}).value || wcState.projectName || 'myproject';
6278
+ if (!desc || desc.length < 10) { alert('Please describe your project first.'); return; }
6279
+ wcState.description = desc;
6280
+ wcState.projectName = projName;
6281
+ wcState.running = true;
6282
+ wcState.generatedFiles = [];
6283
+ wcState.activeFile = 0;
6284
+ renderWebCraft(document.getElementById('content'));
6285
+
6286
+ // Security rules always injected
6287
+ var SECURITY_RULES = [
6288
+ 'ALWAYS use security headers: X-Content-Type-Options, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy: geolocation=(), microphone=(), camera=(), Strict-Transport-Security: max-age=31536000; includeSubDomains; preload.',
6289
+ 'NEVER put secrets, API keys, or DB credentials in frontend code. Only in .env server-side.',
6290
+ 'ALWAYS use prepared statements / parameterized queries. NEVER string-concatenate SQL.',
6291
+ 'ALWAYS hash passwords with bcrypt (cost factor 12+). NEVER store plain passwords.',
6292
+ 'ALWAYS validate and sanitize all user inputs server-side.',
6293
+ 'ALWAYS use httpOnly, secure, sameSite=Strict cookies for session tokens.',
6294
+ 'ALWAYS rate-limit auth endpoints (max 5 attempts / 15min per IP).',
6295
+ 'CSS MUST follow BEM naming: block__element--modifier. No inline styles except dynamic values.',
6296
+ 'PostgreSQL: use pg.Pool (max:10, idleTimeoutMillis:30000). Export singleton. Always use parameterized queries.',
6297
+ 'JWT: access token 15min, refresh token 7 days with rotation. Store refresh in httpOnly cookie.',
6298
+ ].join('\n');
6299
+
6300
+ var authFieldsDef = wcState.authFields.map(function(f){ return f.label+' ('+f.type+(f.required?', required':'')+')'; }).join(', ');
6301
+ var blocksEnabled = Object.keys(wcState.blocks).filter(function(b){ return wcState.blocks[b]; }).join(', ');
6302
+
6303
+ // File plan — always this structure
6304
+ var filePlan = [
6305
+ { name: 'package.json', lang: 'json', prompt: 'Generate package.json for an Express/PostgreSQL project named "'+projName+'". Dependencies: express, pg, bcryptjs, jsonwebtoken, nodemailer, helmet, express-rate-limit, cors, dotenv, express-validator, ioredis. DevDependencies: nodemon. Scripts: start, dev.' },
6306
+ { name: '.env.example', lang: 'bash', prompt: 'Generate .env.example with all required env vars: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS, JWT_SECRET, JWT_REFRESH_SECRET, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM, SENDGRID_API_KEY (commented as optional fallback), REDIS_URL (default redis://localhost:6379, works with Dragonfly too), PORT, NODE_ENV, CORS_ORIGIN, BASE_URL. Add helpful comments for each. Note that REDIS_URL is optional — app falls back to in-memory LRU if Redis unavailable.' },
6307
+ { name: 'server/db.js', lang: 'javascript', prompt: 'Generate server/db.js: pg.Pool singleton (max:10, idleTimeoutMillis:30000, connectionTimeoutMillis:2000). Circuit breaker: if DB fails 5+ times in 60s, open circuit (throw immediately) for 30s, then half-open (try one query). Export pool, query(text, params) with circuit breaker, transaction(callback) helper (BEGIN/COMMIT/ROLLBACK). Graceful shutdown on SIGTERM/SIGINT.' },
6308
+ { name: 'server/middleware/security.js', lang: 'javascript', prompt: 'Generate server/middleware/security.js: helmet with full CSP (defaultSrc self, scriptSrc self, styleSrc self unsafe-inline, imgSrc self data:, connectSrc self, frameSrc none, objectSrc none), HSTS, X-Frame-Options DENY, Referrer-Policy. Add express-rate-limit for general routes (100/15min) and strict limiter for auth (5/15min). Export { applySecurityMiddleware, authLimiter }.' },
6309
+ { name: 'server/middleware/validate.js', lang: 'javascript', prompt: 'Generate server/middleware/validate.js using express-validator. Export handleValidationErrors middleware. Export auth field validators: registerValidator (fields: '+authFieldsDef+'), loginValidator (email + password).' },
6310
+ { name: 'server/services/email.js', lang: 'javascript', prompt: 'Generate server/services/email.js: Nodemailer transporter using SMTP from env. Function sendVerificationEmail(to, token, baseUrl): sends HTML email with verification link. Function sendPasswordResetEmail(to, token, baseUrl). Add SendGrid fallback (commented out, predisposed with transporter swap). Never expose credentials.' },
6311
+ { name: 'server/routes/auth.js', lang: 'javascript', prompt: 'Generate server/routes/auth.js: POST /register (validate fields: '+authFieldsDef+', check duplicate email, bcrypt hash password cost 12, insert user, send verification email, return 201), POST /login (validate, check email verified, compare bcrypt, issue JWT access 15min + refresh 7d httpOnly cookie), POST /logout (clear refresh cookie), POST /refresh-token (validate refresh from httpOnly cookie, rotate token), GET /verify-email/:token (mark email verified). Use parameterized queries only. Apply authLimiter to register and login.' },
6312
+ { name: 'server/routes/api.js', lang: 'javascript', prompt: 'Generate server/routes/api.js: Express router with a verifyToken middleware (validates JWT Bearer). GET /api/me returns authenticated user profile (no password hash). GET /api/health returns {status: ok, timestamp}. Structure ready for adding more routes.' },
6313
+ { name: 'server/index.js', lang: 'javascript', prompt: 'Generate server/index.js: Express app entry point. Apply applySecurityMiddleware first. Then apply sentinelMiddleware (import from ./middleware/sentinel.js). Use CORS with env CORS_ORIGIN. Parse JSON body (limit 10kb). Mount /api/auth → auth.js, /api → api.js. Serve public/ as static. 404 handler and global error handler (never leak stack traces in production). Start on PORT from env.' },
6314
+ { name: 'db/migrations/001_init.sql', lang: 'sql', prompt: 'Generate PostgreSQL migration 001_init.sql: CREATE TABLE users with id UUID default gen_random_uuid(), fields for '+authFieldsDef+', email_verified BOOLEAN default false, verification_token VARCHAR, reset_token VARCHAR, reset_token_expires TIMESTAMPTZ, refresh_token_hash VARCHAR, created_at TIMESTAMPTZ default now(), updated_at TIMESTAMPTZ default now(). CREATE INDEX on email. CREATE TABLE refresh_tokens (id, user_id FK, token_hash, expires_at, created_at). Add updated_at trigger function.' },
6315
+ { name: 'public/css/base.css', lang: 'css', prompt: 'Generate public/css/base.css: CSS custom properties (color palette, spacing scale, font scale, border-radius, shadows, transitions). CSS reset (*, box-sizing). Base typography (Inter or system-ui). Utility classes using BEM where applicable. Dark/light mode via prefers-color-scheme.' },
6316
+ { name: 'public/css/components.css', lang: 'css', prompt: 'Generate public/css/components.css following STRICT BEM (block__element--modifier). Components: .btn (--primary, --secondary, --danger, --ghost), .form (form__field, form__label, form__input, form__error, form__hint), .card (card__header, card__body, card__footer), .nav (nav__brand, nav__links, nav__link--active), .alert (--success, --error, --warning, --info), .spinner, .badge, .modal (modal__overlay, modal__content, modal__header, modal__body, modal__footer). Fully accessible (focus states, aria).' },
6317
+ { name: 'public/css/pages.css', lang: 'css', prompt: 'Generate public/css/pages.css: page-level layout classes using BEM. .page-auth (centered card layout for login/register), .page-dashboard (sidebar + content grid), .page-landing (hero section, features grid, pricing cards). Responsive at 768px and 480px breakpoints.' },
6318
+ { name: 'public/js/main.js', lang: 'javascript', prompt: 'Generate public/js/main.js: vanilla JS, no dependencies. authAPI object with methods register(data), login(data), logout(), refreshToken(), getMe(). Cookie banner controller: reads localStorage consent, shows banner if not set, sets consent by category (necessary/analytics/marketing). Form handlers for register and login pages. Global error display utility. Export nothing (IIFE).' },
6319
+ { name: 'public/index.html', lang: 'html', prompt: 'Generate public/index.html for "'+projName+'": '+desc+'. Full HTML5. Security meta tags (CSP via meta as fallback, X-UA-Compatible). Include base.css, components.css, pages.css. GDPR cookie banner HTML (class .cookie-banner, .cookie-banner__text, .cookie-banner__actions, .cookie-banner__btn--accept, .cookie-banner__btn--reject). Navigation. Hero section. Include main.js at end of body. Semantic HTML, ARIA roles, lang attribute.' },
6320
+ { name: 'public/login.html', lang: 'html', prompt: 'Generate public/login.html: login page for "'+projName+'". Form with email + password fields using .form BEM classes. Link to register.html. Error display area. Include same CSS files. ARIA labels, autocomplete attributes.' },
6321
+ { name: 'public/register.html', lang: 'html', prompt: 'Generate public/register.html: registration page for "'+projName+'". Form fields: '+authFieldsDef+'. Use .form BEM classes. Client-side validation hints. Link to login.html. Error/success display. Include same CSS files. ARIA labels, autocomplete attributes.' },
6322
+ { 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).' },
6323
+ { 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.' },
6324
+ { 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.' },
6325
+ ];
6326
+
6327
+ // Filter by selected blocks
6328
+ if (!wcState.blocks.auth) {
6329
+ filePlan = filePlan.filter(function(f){ return !['server/routes/auth.js','public/login.html','public/register.html','server/middleware/validate.js'].includes(f.name); });
6330
+ }
6331
+ if (!wcState.blocks.cookieBanner) {
6332
+ // keep index.html but note no cookie banner
6333
+ }
6334
+ if (!wcState.blocks.emailVerification) {
6335
+ // mark in prompt
6336
+ filePlan = filePlan.map(function(f){ return f.name === 'server/services/email.js' ? Object.assign({},f,{prompt:f.prompt+' (Skip email verification — not enabled)'}) : f; });
6337
+ }
6338
+
6339
+ var sysPreamble = 'You are an expert full-stack engineer generating production-quality code.\n\nSECURITY RULES (non-negotiable):\n'+SECURITY_RULES+'\n\nProject: '+projName+'\nDescription: '+desc+'\nEnabled blocks: '+blocksEnabled+'\n\nGenerate ONLY the file content requested. No explanations, no markdown code fences, no comments like "here is the file". Output raw file content only.';
6340
+
6341
+ for (var fi = 0; fi < filePlan.length; fi++) {
6342
+ var fp = filePlan[fi];
6343
+ var runBtn = document.getElementById('wcRunBtn');
6344
+ if (runBtn) runBtn.textContent = '&#9203; Generating ' + fp.name + ' (' + (fi+1) + '/' + filePlan.length + ')...';
6345
+ try {
6346
+ var content = await wcCallLLM(sysPreamble, fp.prompt + '\n\nFile to generate: ' + fp.name);
6347
+ // Strip markdown code fences if model added them anyway
6348
+ content = content.replace(/^```[a-z]*\n?/i,'').replace(/\n?```$/,'').trim();
6349
+ wcState.generatedFiles.push({ name: fp.name, content: content, lang: fp.lang });
6350
+ if (fi === 0) wcState.activeFile = 0;
6351
+ renderWebCraft(document.getElementById('content'));
6352
+ } catch(e) {
6353
+ wcState.generatedFiles.push({ name: fp.name, content: '// Error generating this file: ' + e.message, lang: fp.lang });
6354
+ }
6355
+ }
6356
+
6357
+ wcState.running = false;
6358
+ renderWebCraft(document.getElementById('content'));
6359
+ }
6360
+
6361
+ async function wcCallLLM(sys, user) {
6362
+ var r = await fetch(API + '/studio/step', {
6363
+ method: 'POST',
6364
+ headers: {'Content-Type':'application/json'},
6365
+ body: JSON.stringify({system: sys, user: user, max_tokens: 4096, stream: false})
6366
+ });
6367
+ if (!r.ok) throw new Error('LLM error ' + r.status);
6368
+ var d = await r.json();
6369
+ return (d && (d.text || d.content || d.result)) || '';
6370
+ }
6371
+
6372
+ function wcDownloadZip() {
6373
+ if (!wcState.generatedFiles.length) return;
6374
+ // Build a simple ZIP using JSZip-free approach: store as individual downloads or use blob trick
6375
+ // Since we have no JSZip, we offer each file as a combined text archive (tar-like) with clear separators
6376
+ var lines = ['# ' + (wcState.projectName || 'project') + ' — Generated by NHA WebCraft', '# Extract manually or use: csplit or awk to split by === FILE: markers', ''];
6377
+ wcState.generatedFiles.forEach(function(f) {
6378
+ lines.push('=== FILE: ' + f.name + ' ===');
6379
+ lines.push(f.content);
6380
+ lines.push('=== END: ' + f.name + ' ===');
6381
+ lines.push('');
6382
+ });
6383
+ var blob = new Blob([lines.join('\n')], {type:'text/plain'});
6384
+ var a = document.createElement('a');
6385
+ a.href = URL.createObjectURL(blob);
6386
+ a.download = (wcState.projectName || 'project') + '-webcraft.txt';
6387
+ a.click();
6388
+ URL.revokeObjectURL(a.href);
6389
+ }
6150
6390
  `;
6151
6391
 
6152
6392
  export function getHTML(port) {
@@ -6215,6 +6455,7 @@ input:focus,textarea:focus{border-color:var(--green3)}
6215
6455
 
6216
6456
  /* Mobile burger button */
6217
6457
  #mobileBurger{display:block}
6458
+ .sidebar--open~* #mobileBurger,.sidebar--open+* #mobileBurger{opacity:0;pointer-events:none}
6218
6459
  .sidebar__close{position:absolute;top:12px;right:12px;background:none;border:none;color:var(--dim);font-size:20px;cursor:pointer;padding:4px 8px;z-index:10;line-height:1}
6219
6460
  .sidebar__close:hover{color:var(--bright)}
6220
6461
  .sidebar__brand{position:relative}
@@ -6268,6 +6509,7 @@ input:focus,textarea:focus{border-color:var(--green3)}
6268
6509
  #canvasPanel .cvs-header span{font-family:var(--mono);color:var(--green);font-size:12px}
6269
6510
  #canvasPanel .cvs-header button{background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px;margin-left:8px}
6270
6511
  #canvasPanel iframe{flex:1;border:none;background:#fff;min-height:350px;width:100%}
6512
+ @media(max-width:600px){#canvasPanel{top:0;right:0;left:0;width:100%;max-width:100%;height:100dvh;max-height:100dvh;border-radius:0;border-left:none;border-right:none}}
6271
6513
  .msg--thinking{color:var(--dim);font-style:italic}
6272
6514
  .tool-indicator{display:inline-block;padding:2px 8px;margin:2px 0;border-radius:4px;font-size:11px;background:var(--bg3);border:1px solid var(--border)}
6273
6515
  .tool-indicator--browser{border-color:#9c27b0;color:#ce93d8}
@@ -6440,6 +6682,17 @@ input:focus,textarea:focus{border-color:var(--green3)}
6440
6682
  .studio-header{margin-bottom:20px}
6441
6683
  .studio-header h2{font-size:15px;color:var(--green);margin-bottom:4px}
6442
6684
  .studio-header p{font-size:11px;color:var(--dim);line-height:1.5}
6685
+ .studio-sidebar{display:flex;flex-direction:column;gap:12px;width:220px;flex-shrink:0;position:sticky;top:16px;align-self:flex-start}
6686
+ .studio-sidebar-toggle{display:none}
6687
+ @media(max-width:600px){
6688
+ #studioMainRow{flex-direction:column}
6689
+ .studio-sidebar{position:fixed;top:0;right:0;bottom:0;width:260px;max-width:85vw;background:var(--bg2);border-left:1px solid var(--border);z-index:500;padding:16px 12px;overflow-y:auto;transform:translateX(110%);transition:transform .3s cubic-bezier(.4,0,.2,1);box-shadow:-4px 0 24px rgba(0,0,0,.4);flex-shrink:0}
6690
+ .studio-sidebar--open{transform:translateX(0)}
6691
+ .studio-sidebar-toggle{display:flex;align-items:center;gap:6px;margin-bottom:12px;padding:8px 14px;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;color:var(--cyan);font-size:12px;font-weight:600;cursor:pointer;width:100%;justify-content:center}
6692
+ .studio-header p{display:none}
6693
+ .studio-input-row{flex-direction:column}
6694
+ .studio-input-row textarea{min-height:72px;font-size:13px}
6695
+ }
6443
6696
  .studio-input-row{display:flex;gap:8px;margin-bottom:16px;align-items:flex-start}
6444
6697
  .studio-input-row textarea{flex:1;resize:vertical;min-height:90px;max-height:200px;padding:10px 14px;font-size:13px;border-radius:var(--r);border:1px solid var(--border2);line-height:1.5}
6445
6698
  .studio-input-row textarea:focus{border-color:var(--green3)}
@@ -6756,7 +7009,7 @@ input:focus,textarea:focus{border-color:var(--green3)}
6756
7009
  <div class="app">
6757
7010
  <nav class="sidebar" id="sidebar"></nav>
6758
7011
 
6759
- <button onclick="openSidebar()" style="position:fixed;top:6px;left:6px;z-index:100;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);color:var(--green);font-size:16px;padding:4px 8px;cursor:pointer;line-height:1;opacity:0.85" id="mobileBurger">&#9776;</button>
7012
+ <button onclick="openSidebar()" style="position:fixed;bottom:16px;left:50%;transform:translateX(-50%);z-index:100;background:var(--bg2);border:1px solid var(--green3);border-radius:24px;color:var(--green);font-size:13px;font-weight:700;padding:8px 20px;cursor:pointer;line-height:1;box-shadow:0 2px 12px rgba(0,0,0,.5);letter-spacing:.3px" id="mobileBurger">&#9776; Menu</button>
6760
7013
 
6761
7014
  <div class="content" id="content"></div>
6762
7015