nothumanallowed 13.5.47 → 13.5.49
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 +1 -1
- package/src/commands/ui.mjs +77 -1
- package/src/constants.mjs +1 -1
- package/src/services/web-ui.mjs +114 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.5.
|
|
3
|
+
"version": "13.5.49",
|
|
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/commands/ui.mjs
CHANGED
|
@@ -4067,7 +4067,83 @@ async function transaction(cb) {
|
|
|
4067
4067
|
module.exports = { pool, query, transaction };
|
|
4068
4068
|
`;
|
|
4069
4069
|
fs.writeFileSync(path.join(sandboxDir, 'server', 'db.js'), dbShim, 'utf8');
|
|
4070
|
-
|
|
4070
|
+
|
|
4071
|
+
// Sentinel shim — zero external dependencies (no 'ip', 'net', etc.)
|
|
4072
|
+
const sentinelShim = `
|
|
4073
|
+
// NHA WebCraft Sandbox — Sentinel WAF shim (zero dependencies)
|
|
4074
|
+
const _ipWindows = {};
|
|
4075
|
+
function getIP(req) {
|
|
4076
|
+
return (req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket.remoteAddress || '127.0.0.1';
|
|
4077
|
+
}
|
|
4078
|
+
const SQL_RE = new RegExp('(union[\\\\s\\\\S]+select|drop[\\\\s]+table|insert[\\\\s]+into|delete[\\\\s]+from|update[\\\\s]+set|exec[\\\\s]*\\\\(|xp_cmdshell)', 'i');
|
|
4079
|
+
const XSS_RE = new RegExp('(<script|javascript:|onerror=|onload=|eval\\\\()', 'i');
|
|
4080
|
+
const PATH_RE = new RegExp('\\\\.\\\\./');
|
|
4081
|
+
function sentinelMiddleware(req, res, next) {
|
|
4082
|
+
var ip = getIP(req);
|
|
4083
|
+
var now = Date.now();
|
|
4084
|
+
if (!_ipWindows[ip]) _ipWindows[ip] = [];
|
|
4085
|
+
_ipWindows[ip] = _ipWindows[ip].filter(function(t){ return now - t < 60000; });
|
|
4086
|
+
_ipWindows[ip].push(now);
|
|
4087
|
+
if (_ipWindows[ip].length > 120) {
|
|
4088
|
+
process.stderr.write('[sentinel] rate-limit ' + ip + '\\n');
|
|
4089
|
+
return res.status(429).json({ error: 'Too many requests' });
|
|
4090
|
+
}
|
|
4091
|
+
var check = req.url + JSON.stringify(req.body || '');
|
|
4092
|
+
if (SQL_RE.test(check) || XSS_RE.test(check) || PATH_RE.test(check)) {
|
|
4093
|
+
process.stderr.write('[sentinel] blocked ' + ip + ' ' + req.method + ' ' + req.url + '\\n');
|
|
4094
|
+
return res.status(400).json({ error: 'Request blocked' });
|
|
4095
|
+
}
|
|
4096
|
+
next();
|
|
4097
|
+
}
|
|
4098
|
+
module.exports = { sentinelMiddleware };
|
|
4099
|
+
`;
|
|
4100
|
+
fs.mkdirSync(path.join(sandboxDir, 'server', 'middleware'), { recursive: true });
|
|
4101
|
+
fs.writeFileSync(path.join(sandboxDir, 'server', 'middleware', 'sentinel.js'), sentinelShim, 'utf8');
|
|
4102
|
+
|
|
4103
|
+
// Cache shim — zero dependencies (no ioredis), pure in-memory LRU
|
|
4104
|
+
const cacheShim = `
|
|
4105
|
+
// NHA WebCraft Sandbox — Cache shim (in-memory, no Redis required)
|
|
4106
|
+
const _store = new Map();
|
|
4107
|
+
const _MAX = 1000;
|
|
4108
|
+
function evict() { if (_store.size > _MAX) { _store.delete(_store.keys().next().value); } }
|
|
4109
|
+
async function get(key) { var e = _store.get(key); if (!e) return null; if (e.exp && Date.now() > e.exp) { _store.delete(key); return null; } return e.val; }
|
|
4110
|
+
async function set(key, value, ttlSeconds) { evict(); _store.set(key, { val: value, exp: ttlSeconds ? Date.now() + ttlSeconds * 1000 : null }); }
|
|
4111
|
+
async function del(key) { _store.delete(key); }
|
|
4112
|
+
async function exists(key) { return (await get(key)) !== null; }
|
|
4113
|
+
module.exports = { get, set, del, exists };
|
|
4114
|
+
`;
|
|
4115
|
+
fs.mkdirSync(path.join(sandboxDir, 'server', 'services'), { recursive: true });
|
|
4116
|
+
fs.writeFileSync(path.join(sandboxDir, 'server', 'services', 'cache.js'), cacheShim, 'utf8');
|
|
4117
|
+
|
|
4118
|
+
// Patch all generated JS files: fix known wrong require() names
|
|
4119
|
+
// The LLM often uses 'bcrypt' instead of 'bcryptjs', 'pg' instead of nothing, etc.
|
|
4120
|
+
const requireFixes = [
|
|
4121
|
+
[/require\(['"]bcrypt['"]\)/g, "require('bcryptjs')"],
|
|
4122
|
+
[/require\(['"]node-postgres['"]\)/g, "require('bcryptjs')"],
|
|
4123
|
+
[/require\(['"]pg['"]\)/g, "require('./db')"],
|
|
4124
|
+
[/require\(['"]ioredis['"]\)/g, "require('../services/cache')"],
|
|
4125
|
+
[/require\(['"]redis['"]\)/g, "require('../services/cache')"],
|
|
4126
|
+
[/require\(['"]ip['"]\)/g, "({address:()=>'127.0.0.1'})"],
|
|
4127
|
+
[/require\(['"]express-async-errors['"]\)/g, "{}"],
|
|
4128
|
+
[/require\(['"]multer['"]\)/g, "({single:()=>(r,s,n)=>n(),array:()=>(r,s,n)=>n()})"],
|
|
4129
|
+
];
|
|
4130
|
+
function patchJsFiles(dir) {
|
|
4131
|
+
if (!fs.existsSync(dir)) return;
|
|
4132
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
4133
|
+
const full = path.join(dir, entry.name);
|
|
4134
|
+
if (entry.isDirectory()) { patchJsFiles(full); continue; }
|
|
4135
|
+
if (!entry.name.endsWith('.js')) continue;
|
|
4136
|
+
let src = fs.readFileSync(full, 'utf8');
|
|
4137
|
+
let changed = false;
|
|
4138
|
+
for (const [pat, rep] of requireFixes) {
|
|
4139
|
+
const next = src.replace(pat, rep);
|
|
4140
|
+
if (next !== src) { src = next; changed = true; }
|
|
4141
|
+
}
|
|
4142
|
+
if (changed) fs.writeFileSync(full, src, 'utf8');
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
patchJsFiles(path.join(sandboxDir, 'server'));
|
|
4146
|
+
sendLog('🔧 Shim iniettati: DB (in-memory), Sentinel WAF, Cache — require() patchati');
|
|
4071
4147
|
|
|
4072
4148
|
// Patch package.json to remove pg, add only what's needed
|
|
4073
4149
|
const pkgPath = path.join(sandboxDir, 'package.json');
|
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.
|
|
8
|
+
export const VERSION = '13.5.49';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -6245,6 +6245,7 @@ var wcState = {
|
|
|
6245
6245
|
var wcRightTab = 'files';
|
|
6246
6246
|
var wcMainTab = 'new'; // 'new' | 'projects'
|
|
6247
6247
|
var wcProjectsList = []; // cached list from server
|
|
6248
|
+
var wcSandboxExpanded = {}; // { phaseKey: true/false }
|
|
6248
6249
|
|
|
6249
6250
|
function wcEsc(s){return s?String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'):''}
|
|
6250
6251
|
|
|
@@ -6375,6 +6376,7 @@ function wcPickExample(i) {
|
|
|
6375
6376
|
function wcTabFiles() { wcRightTab = 'files'; renderWebCraft(document.getElementById('content')); }
|
|
6376
6377
|
function wcTabPreview() { wcRightTab = 'preview'; renderWebCraft(document.getElementById('content')); }
|
|
6377
6378
|
function wcOpenSandbox() { if (wcState.sandbox.port) window.open('http://127.0.0.1:' + wcState.sandbox.port, '_blank'); }
|
|
6379
|
+
function wcTogglePhase(key) { wcSandboxExpanded[key] = !wcSandboxExpanded[key]; renderWebCraft(document.getElementById('content')); }
|
|
6378
6380
|
|
|
6379
6381
|
function wcMainTabNew() { wcMainTab = 'new'; renderWebCraft(document.getElementById('content')); }
|
|
6380
6382
|
function wcMainTabProjects() {
|
|
@@ -6568,38 +6570,127 @@ function wcSandboxPanelHtml() {
|
|
|
6568
6570
|
if (!wcState.generatedFiles.length) {
|
|
6569
6571
|
return '<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--dim);font-size:13px">Genera prima il progetto</div>';
|
|
6570
6572
|
}
|
|
6571
|
-
var logsHtml = sb.logs.length
|
|
6572
|
-
? '<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)">' +
|
|
6573
|
-
sb.logs.map(function(l){ return '<div>'+wcEsc(l)+'</div>'; }).join('') +
|
|
6574
|
-
'</div>'
|
|
6575
|
-
: '<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>';
|
|
6576
6573
|
|
|
6574
|
+
// Server ready — show iframe with top bar
|
|
6577
6575
|
if (sb.port && !sb.running) {
|
|
6578
|
-
// Server ready — show iframe
|
|
6579
6576
|
return '<div style="display:flex;flex-direction:column;flex:1;min-height:0">' +
|
|
6580
|
-
'<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0">' +
|
|
6581
|
-
'<span style="
|
|
6582
|
-
'<span style="font-size:
|
|
6583
|
-
'<
|
|
6584
|
-
'<button onclick="
|
|
6577
|
+
'<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0;background:var(--bg3)">' +
|
|
6578
|
+
'<span style="width:8px;height:8px;border-radius:50%;background:#22c55e;display:inline-block;flex-shrink:0"></span>' +
|
|
6579
|
+
'<span style="font-size:11px;color:var(--text);font-family:var(--mono);font-weight:600">http://127.0.0.1:'+sb.port+'</span>' +
|
|
6580
|
+
'<span style="font-size:10px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">'+wcEsc(sb.dir||'')+'</span>' +
|
|
6581
|
+
'<button onclick="wcStopSandbox()" style="padding:3px 10px;background:transparent;border:1px solid var(--border2);border-radius:5px;color:var(--dim);font-size:11px;cursor:pointer;flex-shrink:0">■ Ferma</button>' +
|
|
6582
|
+
'<button onclick="wcOpenSandbox()" style="padding:3px 12px;background:var(--green3);border:none;border-radius:5px;color:var(--bg);font-size:11px;cursor:pointer;font-weight:700;flex-shrink:0">↗ Apri nel browser</button>' +
|
|
6585
6583
|
'</div>' +
|
|
6586
6584
|
'<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>' +
|
|
6587
6585
|
'</div>';
|
|
6588
6586
|
}
|
|
6589
6587
|
|
|
6588
|
+
// Pre-launch info panel
|
|
6589
|
+
if (!sb.running && !sb.port && !sb.logs.length) {
|
|
6590
|
+
return '<div style="display:flex;flex-direction:column;flex:1;min-height:0;padding:20px">' +
|
|
6591
|
+
'<div style="background:var(--bg3);border:1px solid var(--border);border-radius:10px;padding:16px;max-width:480px">' +
|
|
6592
|
+
'<div style="font-size:12px;font-weight:700;color:var(--text);margin-bottom:12px">▶ Cosa succede quando avvii la sandbox:</div>' +
|
|
6593
|
+
'<div style="display:flex;flex-direction:column;gap:8px;font-size:11px;color:var(--dim)">' +
|
|
6594
|
+
'<div style="display:flex;gap:8px;align-items:flex-start"><span style="color:var(--green);flex-shrink:0">1.</span><span>I file vengono scritti in <span style="font-family:var(--mono);color:var(--green)">~/.nha/webcraft/'+wcEsc(wcState.projectName||'project')+'</span></span></div>' +
|
|
6595
|
+
'<div style="display:flex;gap:8px;align-items:flex-start"><span style="color:var(--green);flex-shrink:0">2.</span><span>npm install delle dipendenze (solo in quella cartella)</span></div>' +
|
|
6596
|
+
'<div style="display:flex;gap:8px;align-items:flex-start"><span style="color:var(--green);flex-shrink:0">3.</span><span>Il server Express parte su una porta locale casuale</span></div>' +
|
|
6597
|
+
'<div style="display:flex;gap:8px;align-items:flex-start"><span style="color:var(--green);flex-shrink:0">4.</span><span>DB in-memory (no PostgreSQL richiesto) — i dati si azzerano al riavvio</span></div>' +
|
|
6598
|
+
'</div>' +
|
|
6599
|
+
'<div style="margin-top:12px;padding:8px 10px;background:var(--amberdim);border:1px solid var(--amber3);border-radius:6px;font-size:10px;color:var(--amber)">⚠ Solo locale — nessun dato esce dal tuo Mac</div>' +
|
|
6600
|
+
'</div>' +
|
|
6601
|
+
'</div>';
|
|
6602
|
+
}
|
|
6603
|
+
|
|
6604
|
+
// Running — show structured phases log
|
|
6605
|
+
var phases = [
|
|
6606
|
+
{ key: 'files', label: 'Scrittura file', icon: '📄' },
|
|
6607
|
+
{ key: 'shims', label: 'Shim iniettati', icon: '🔧' },
|
|
6608
|
+
{ key: 'pkg', label: 'package.json', icon: '📦' },
|
|
6609
|
+
{ key: 'env', label: '.env sandbox', icon: '⚙' },
|
|
6610
|
+
{ key: 'deps', label: 'Dipendenze', icon: '📦' },
|
|
6611
|
+
{ key: 'install', label: 'npm install', icon: '⏳' },
|
|
6612
|
+
{ key: 'start', label: 'Avvio server', icon: '▶' },
|
|
6613
|
+
];
|
|
6614
|
+
var logs = sb.logs;
|
|
6615
|
+
// Classify each log line into a phase
|
|
6616
|
+
function phaseOf(l) {
|
|
6617
|
+
if (!l) return 'start';
|
|
6618
|
+
if (l.indexOf('Scrittura') !== -1 || l.indexOf('file...') !== -1 || l[0] === ' ' && l.indexOf('.') !== -1 && l.indexOf('✓') !== -1) return 'files';
|
|
6619
|
+
if (l.indexOf('Shim') !== -1 || l.indexOf('shim') !== -1 || l.indexOf('DB shim') !== -1) return 'shims';
|
|
6620
|
+
if (l.indexOf('package.json') !== -1) return 'pkg';
|
|
6621
|
+
if (l.indexOf('.env') !== -1) return 'env';
|
|
6622
|
+
if (l.indexOf('Dipendenze') !== -1 || l.indexOf('Percorso:') !== -1 || (l.indexOf('•') !== -1 && l.indexOf('@') !== -1)) return 'deps';
|
|
6623
|
+
if (l.indexOf('npm install') !== -1 || l.indexOf('added') !== -1 || l.indexOf('packages') !== -1 || l.indexOf('npm error') !== -1 || l.indexOf('audit') !== -1 || l.indexOf('funding') !== -1 || l.indexOf('vulnerability') !== -1) return 'install';
|
|
6624
|
+
return 'start';
|
|
6625
|
+
}
|
|
6626
|
+
var byPhase = {};
|
|
6627
|
+
logs.forEach(function(l){ var p = phaseOf(l); if (!byPhase[p]) byPhase[p] = []; byPhase[p].push(l); });
|
|
6628
|
+
|
|
6629
|
+
// Check if a phase is done (next phase has lines)
|
|
6630
|
+
var phaseKeys = phases.map(function(p){ return p.key; });
|
|
6631
|
+
function phaseStatus(pk) {
|
|
6632
|
+
var idx = phaseKeys.indexOf(pk);
|
|
6633
|
+
if (!byPhase[pk] || !byPhase[pk].length) return 'pending';
|
|
6634
|
+
// Done if next phase has started or if error
|
|
6635
|
+
for (var i = idx+1; i < phaseKeys.length; i++) {
|
|
6636
|
+
if (byPhase[phaseKeys[i]] && byPhase[phaseKeys[i]].length) return 'done';
|
|
6637
|
+
}
|
|
6638
|
+
if (sb.error) return 'error';
|
|
6639
|
+
return 'active';
|
|
6640
|
+
}
|
|
6641
|
+
|
|
6642
|
+
var statusColor = { done:'var(--green)', active:'var(--amber)', pending:'var(--dim)', error:'var(--red)' };
|
|
6643
|
+
var statusIcon = { done:'✓', active:'⏳', pending:'○', error:'❌' };
|
|
6644
|
+
|
|
6645
|
+
var phasesHtml = phases.map(function(ph){
|
|
6646
|
+
var st = phaseStatus(ph.key);
|
|
6647
|
+
var lines = byPhase[ph.key] || [];
|
|
6648
|
+
var clean = lines.filter(function(l){ return l.indexOf('npm fund') === -1 && l.indexOf('run ') === -1 && l.indexOf('npm audit') === -1; });
|
|
6649
|
+
var isOpen = !!wcSandboxExpanded[ph.key];
|
|
6650
|
+
var hasContent = clean.length > 0;
|
|
6651
|
+
|
|
6652
|
+
// Summary line (always visible)
|
|
6653
|
+
var summary = '';
|
|
6654
|
+
if (ph.key === 'files') {
|
|
6655
|
+
var cnt = clean.filter(function(l){ return l.indexOf('✓') !== -1; }).length;
|
|
6656
|
+
summary = cnt ? cnt + ' file scritti' : '';
|
|
6657
|
+
} else if (ph.key === 'deps') {
|
|
6658
|
+
var dcnt = clean.filter(function(l){ return l.indexOf('•') !== -1; }).length;
|
|
6659
|
+
summary = dcnt ? dcnt + ' dipendenze' : '';
|
|
6660
|
+
} else if (clean.length > 0) {
|
|
6661
|
+
var last = clean.filter(function(l){ return l.trim(); }).slice(-1)[0] || '';
|
|
6662
|
+
summary = wcEsc(last.trim().slice(0, 60));
|
|
6663
|
+
}
|
|
6664
|
+
|
|
6665
|
+
// Expanded detail — all lines
|
|
6666
|
+
var expandedHtml = '';
|
|
6667
|
+
if (isOpen && hasContent) {
|
|
6668
|
+
expandedHtml = '<div style="margin-top:6px;padding:8px;background:var(--bg);border-radius:6px;max-height:180px;overflow-y:auto">' +
|
|
6669
|
+
clean.map(function(l){
|
|
6670
|
+
var col = l.indexOf('❌') !== -1 || l.indexOf('Error') !== -1 ? 'var(--red)' : l.indexOf('✓') !== -1 || l.indexOf('✅') !== -1 ? 'var(--green)' : 'var(--dim)';
|
|
6671
|
+
return '<div style="font-size:10px;font-family:var(--mono);color:'+col+';line-height:1.6;white-space:pre-wrap;word-break:break-all">'+wcEsc(l)+'</div>';
|
|
6672
|
+
}).join('') +
|
|
6673
|
+
'</div>';
|
|
6674
|
+
}
|
|
6675
|
+
|
|
6676
|
+
var clickable = hasContent && st !== 'pending';
|
|
6677
|
+
return '<div style="border-bottom:1px solid var(--border)">' +
|
|
6678
|
+
'<div onclick="'+( clickable ? 'wcTogglePhase('+JSON.stringify(ph.key)+')' : '' )+'" style="display:flex;gap:10px;align-items:center;padding:9px 12px;cursor:'+(clickable?'pointer':'default')+'">' +
|
|
6679
|
+
'<span style="font-size:13px;flex-shrink:0">'+ph.icon+'</span>' +
|
|
6680
|
+
'<div style="flex:1;min-width:0">' +
|
|
6681
|
+
'<div style="font-size:11px;font-weight:600;color:'+(st==='pending'?'var(--dim)':'var(--text)')+'">'+ph.label+'</div>' +
|
|
6682
|
+
(summary && !isOpen ? '<div style="font-size:10px;color:var(--dim);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+summary+'</div>' : '') +
|
|
6683
|
+
'</div>' +
|
|
6684
|
+
(clickable ? '<span style="font-size:10px;color:var(--dim);flex-shrink:0">'+(isOpen?'▲':'▼')+'</span>' : '') +
|
|
6685
|
+
'<span style="font-size:13px;color:'+statusColor[st]+';flex-shrink:0;margin-left:4px">'+statusIcon[st]+'</span>' +
|
|
6686
|
+
'</div>' +
|
|
6687
|
+
(isOpen ? '<div style="padding:0 12px 10px">' + expandedHtml + '</div>' : '') +
|
|
6688
|
+
'</div>';
|
|
6689
|
+
}).join('');
|
|
6690
|
+
|
|
6590
6691
|
return '<div style="display:flex;flex-direction:column;flex:1;min-height:0">' +
|
|
6591
|
-
|
|
6592
|
-
(
|
|
6593
|
-
'<div style="font-weight:700;color:var(--text);margin-bottom:4px">Sandbox locale — cosa succede quando avvii:</div>' +
|
|
6594
|
-
'<div>• I file vengono scritti in <span style="font-family:var(--mono);color:var(--green)">~/.nha/webcraft/'+(wcState.projectName||'project')+'</span></div>' +
|
|
6595
|
-
'<div>• npm install delle dipendenze nella stessa cartella (nessuna installazione globale)</div>' +
|
|
6596
|
-
'<div>• Il server Express parte su una porta locale casuale (es. 45123)</div>' +
|
|
6597
|
-
'<div>• Il DB usa un in-memory store (nessun PostgreSQL richiesto)</div>' +
|
|
6598
|
-
'<div>• I dati sandbox sono temporanei e si azzerano al riavvio</div>' +
|
|
6599
|
-
'<div style="margin-top:6px;color:var(--amber)">⚠ Sandbox solo locale — nessun dato esce dal tuo Mac</div>' +
|
|
6600
|
-
'</div>' : '') +
|
|
6601
|
-
logsHtml +
|
|
6602
|
-
(sb.error ? '<div style="padding:8px 14px;color:var(--red);font-size:11px;font-family:var(--mono);flex-shrink:0">❌ '+wcEsc(sb.error)+'</div>' : '') +
|
|
6692
|
+
phasesHtml +
|
|
6693
|
+
(sb.error ? '<div style="padding:10px 14px;color:var(--red);font-size:11px;font-family:var(--mono);border-top:1px solid var(--border)">❌ '+wcEsc(sb.error)+'</div>' : '') +
|
|
6603
6694
|
'</div>';
|
|
6604
6695
|
}
|
|
6605
6696
|
|