@agenticmail/enterprise 0.5.80 → 0.5.81

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.
@@ -3993,12 +3993,14 @@ function BrowserConfigCard(props) {
3993
3993
  var _d = useApp(); var toast = _d.toast;
3994
3994
  var _cfg = useState(null); var cfg = _cfg[0]; var setCfg = _cfg[1];
3995
3995
  var _saving = useState(false); var saving = _saving[0]; var setSaving = _saving[1];
3996
- var _collapsed = useState(true); var collapsed = _collapsed[0]; var setCollapsed = _collapsed[1];
3996
+ var _testing = useState(false); var testing = _testing[0]; var setTesting = _testing[1];
3997
+ var _testResult = useState(null); var testResult = _testResult[0]; var setTestResult = _testResult[1];
3998
+ var _collapsed = useState(false); var collapsed = _collapsed[0]; var setCollapsed = _collapsed[1];
3997
3999
 
3998
4000
  function load() {
3999
4001
  engineCall('/bridge/agents/' + agentId + '/browser-config')
4000
- .then(function(d) { setCfg(d.config || {}); })
4001
- .catch(function() { setCfg({}); });
4002
+ .then(function(d) { setCfg(d.config || { provider: 'local' }); })
4003
+ .catch(function() { setCfg({ provider: 'local' }); });
4002
4004
  }
4003
4005
 
4004
4006
  useEffect(function() { load(); }, [agentId]);
@@ -4012,14 +4014,37 @@ function BrowserConfigCard(props) {
4012
4014
  .catch(function(e) { toast(e.message, 'error'); setSaving(false); });
4013
4015
  }
4014
4016
 
4017
+ function testConnection() {
4018
+ setTesting(true); setTestResult(null);
4019
+ engineCall('/bridge/agents/' + agentId + '/browser-config/test', { method: 'POST' })
4020
+ .then(function(d) { setTestResult(d); setTesting(false); })
4021
+ .catch(function(e) { setTestResult({ error: e.message }); setTesting(false); });
4022
+ }
4023
+
4015
4024
  function update(key, value) {
4016
4025
  setCfg(function(prev) { var n = Object.assign({}, prev); n[key] = value; return n; });
4017
4026
  }
4018
4027
 
4019
4028
  if (!cfg) return null;
4020
4029
 
4030
+ var provider = cfg.provider || 'local';
4021
4031
  var labelStyle = { display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 4 };
4022
4032
  var helpStyle = { fontSize: 11, color: 'var(--text-muted)', marginTop: 2 };
4033
+ var sectionStyle = { padding: '12px 0', borderBottom: '1px solid var(--border)' };
4034
+ var sectionTitle = function(icon, text) {
4035
+ return h('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 12, display: 'flex', alignItems: 'center', gap: 6 } },
4036
+ h('span', null, icon), text);
4037
+ };
4038
+
4039
+ // Provider descriptions
4040
+ var providers = [
4041
+ { id: 'local', name: 'Local Chromium', icon: '\uD83D\uDCBB', desc: 'Built-in headless Chromium on this server. Best for web automation, scraping, screenshots, form filling.' },
4042
+ { id: 'remote-cdp', name: 'Remote Browser (CDP)', icon: '\uD83C\uDF10', desc: 'Connect to a Chrome/Chromium instance via Chrome DevTools Protocol. Required for headed mode, video calls, persistent sessions.' },
4043
+ { id: 'browserless', name: 'Browserless.io', icon: '\u2601\uFE0F', desc: 'Cloud browser service. Scalable, managed infrastructure. Supports stealth mode, residential proxies, and concurrent sessions.' },
4044
+ { id: 'browserbase', name: 'Browserbase', icon: '\uD83D\uDE80', desc: 'AI-native cloud browser. Built for agent automation with session replay, anti-detection, and managed infrastructure.' },
4045
+ { id: 'steel', name: 'Steel.dev', icon: '\u26A1', desc: 'Open-source browser API designed for AI agents. Self-hostable, session management, built-in stealth.' },
4046
+ { id: 'scrapingbee', name: 'ScrapingBee', icon: '\uD83D\uDC1D', desc: 'Web scraping API with browser rendering, proxy rotation, and CAPTCHA solving.' },
4047
+ ];
4023
4048
 
4024
4049
  return h('div', { className: 'card', style: { marginTop: 16 } },
4025
4050
  h('div', {
@@ -4027,118 +4052,461 @@ function BrowserConfigCard(props) {
4027
4052
  style: { cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' },
4028
4053
  onClick: function() { setCollapsed(!collapsed); }
4029
4054
  },
4030
- h('span', null, '\uD83C\uDF10 Browser Configuration'),
4055
+ h('span', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
4056
+ '\uD83C\uDF10 Browser & Web Automation',
4057
+ cfg.provider && cfg.provider !== 'local' && h('span', { className: 'badge', style: { fontSize: 10, padding: '1px 6px', background: 'var(--accent-soft)', color: 'var(--accent)' } },
4058
+ providers.find(function(p) { return p.id === cfg.provider; })?.name || cfg.provider
4059
+ )
4060
+ ),
4031
4061
  h('span', { style: { fontSize: 12, color: 'var(--text-muted)' } }, collapsed ? '\u25BC' : '\u25B2')
4032
4062
  ),
4033
- !collapsed && h('div', { style: { padding: 16, display: 'grid', gap: 16 } },
4034
- // Headless mode
4035
- h('div', { className: 'form-group' },
4036
- h('label', { style: labelStyle }, 'Headless Mode'),
4037
- h('select', {
4038
- className: 'input', value: cfg.headless !== false ? 'true' : 'false',
4039
- onChange: function(e) { update('headless', e.target.value === 'true'); }
4040
- },
4041
- h('option', { value: 'true' }, 'Headless (no visible browser window)'),
4042
- h('option', { value: 'false' }, 'Headed (visible browser — needed for Meet/Teams)')
4043
- ),
4044
- h('div', { style: helpStyle }, 'Use headed mode for video calls (Google Meet, Teams) or visual debugging.')
4063
+ !collapsed && h('div', { style: { padding: 16 } },
4064
+
4065
+ // ─── Section 1: Browser Provider ─────────────────
4066
+ h('div', { style: sectionStyle },
4067
+ sectionTitle('\uD83D\uDD27', 'Browser Provider'),
4068
+ h('div', { style: { display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))' } },
4069
+ providers.map(function(p) {
4070
+ var selected = provider === p.id;
4071
+ return h('div', {
4072
+ key: p.id,
4073
+ onClick: function() { update('provider', p.id); },
4074
+ style: {
4075
+ padding: '12px 14px', borderRadius: 'var(--radius)', cursor: 'pointer',
4076
+ border: '2px solid ' + (selected ? 'var(--accent)' : 'var(--border)'),
4077
+ background: selected ? 'var(--accent-soft)' : 'var(--bg-secondary)',
4078
+ transition: 'all 0.15s',
4079
+ }
4080
+ },
4081
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 } },
4082
+ h('span', { style: { fontSize: 18 } }, p.icon),
4083
+ h('span', { style: { fontWeight: 600, fontSize: 13 } }, p.name),
4084
+ selected && h('span', { style: { marginLeft: 'auto', color: 'var(--accent)', fontSize: 14 } }, '\u2713')
4085
+ ),
4086
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.4 } }, p.desc)
4087
+ );
4088
+ })
4089
+ )
4045
4090
  ),
4046
4091
 
4047
- // SSRF Protection
4048
- h('div', { className: 'form-group' },
4049
- h('label', { style: labelStyle }, 'URL Protection'),
4050
- h('select', {
4051
- className: 'input', value: cfg.ssrfProtection || 'permissive',
4052
- onChange: function(e) { update('ssrfProtection', e.target.value); }
4053
- },
4054
- h('option', { value: 'off' }, 'Off — No restrictions (full internet access)'),
4055
- h('option', { value: 'permissive' }, 'Permissive — Block known dangerous URLs only'),
4056
- h('option', { value: 'strict' }, 'Strict Block private IPs, require allowlist')
4092
+ // ─── Section 2: Provider-Specific Config ─────────
4093
+ h('div', { style: sectionStyle },
4094
+
4095
+ // Local Chromium
4096
+ provider === 'local' && h(Fragment, null,
4097
+ sectionTitle('\uD83D\uDCBB', 'Local Chromium Settings'),
4098
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4099
+ h('div', { className: 'form-group' },
4100
+ h('label', { style: labelStyle }, 'Display Mode'),
4101
+ h('select', { className: 'input', value: cfg.headless !== false ? 'true' : 'false',
4102
+ onChange: function(e) { update('headless', e.target.value === 'true'); }
4103
+ },
4104
+ h('option', { value: 'true' }, 'Headless (no window)'),
4105
+ h('option', { value: 'false' }, 'Headed (visible window)')
4106
+ ),
4107
+ h('div', { style: helpStyle }, 'Headed mode requires a display server (X11/Wayland).')
4108
+ ),
4109
+ h('div', { className: 'form-group' },
4110
+ h('label', { style: labelStyle }, 'Executable Path'),
4111
+ h('input', { className: 'input', placeholder: 'Auto-detect (recommended)',
4112
+ value: cfg.executablePath || '',
4113
+ onChange: function(e) { update('executablePath', e.target.value || undefined); }
4114
+ }),
4115
+ h('div', { style: helpStyle }, 'Leave empty to use bundled Chromium.')
4116
+ ),
4117
+ h('div', { className: 'form-group' },
4118
+ h('label', { style: labelStyle }, 'User Data Directory'),
4119
+ h('input', { className: 'input', placeholder: 'Temporary (new profile each session)',
4120
+ value: cfg.userDataDir || '',
4121
+ onChange: function(e) { update('userDataDir', e.target.value || undefined); }
4122
+ }),
4123
+ h('div', { style: helpStyle }, 'Persist cookies, logins, and extensions across sessions.')
4124
+ ),
4125
+ h('div', { className: 'form-group' },
4126
+ h('label', { style: labelStyle }, 'Extra Chrome Args'),
4127
+ h('input', { className: 'input', placeholder: '--no-sandbox, --disable-gpu',
4128
+ value: (cfg.extraArgs || []).join(', '),
4129
+ onChange: function(e) { update('extraArgs', e.target.value.split(',').map(function(s) { return s.trim(); }).filter(Boolean)); }
4130
+ }),
4131
+ h('div', { style: helpStyle }, 'Additional Chromium launch arguments.')
4132
+ )
4133
+ )
4057
4134
  ),
4058
- h('div', { style: helpStyle }, 'Controls which URLs the agent can navigate to.')
4059
- ),
4060
4135
 
4061
- // Allow JS Evaluation
4062
- h('div', { className: 'form-group' },
4063
- h('label', { style: labelStyle }, 'JavaScript Evaluation'),
4064
- h('select', {
4065
- className: 'input', value: cfg.allowEvaluate !== false ? 'true' : 'false',
4066
- onChange: function(e) { update('allowEvaluate', e.target.value === 'true'); }
4067
- },
4068
- h('option', { value: 'true' }, 'Allowed — Agent can run JS in page context'),
4069
- h('option', { value: 'false' }, 'Blocked — No arbitrary JS execution')
4070
- )
4071
- ),
4136
+ // Remote CDP
4137
+ provider === 'remote-cdp' && h(Fragment, null,
4138
+ sectionTitle('\uD83C\uDF10', 'Remote Browser Connection'),
4139
+ h('div', { style: { padding: '10px 14px', background: 'var(--info-soft)', borderRadius: 'var(--radius)', marginBottom: 12, fontSize: 12, lineHeight: 1.5 } },
4140
+ h('strong', null, 'How it works: '),
4141
+ 'The agent connects to a Chrome/Chromium browser running on another machine via the Chrome DevTools Protocol (CDP). ',
4142
+ 'This is required for video calls (Google Meet, Teams, Zoom) where the browser needs a camera, microphone, and display. ',
4143
+ h('br', null), h('br', null),
4144
+ h('strong', null, 'Setup options:'), h('br', null),
4145
+ '\u2022 Run Chrome with --remote-debugging-port=9222 on a VM/desktop', h('br', null),
4146
+ '\u2022 Use a cloud desktop (AWS WorkSpaces, Azure Virtual Desktop, Hetzner)', h('br', null),
4147
+ '\u2022 Set up a dedicated browser VM with virtual camera/audio for meetings', h('br', null),
4148
+ '\u2022 Use SSH tunneling to expose Chrome DevTools securely'
4149
+ ),
4150
+ h('div', { style: { display: 'grid', gap: 12 } },
4151
+ h('div', { className: 'form-group' },
4152
+ h('label', { style: labelStyle }, 'CDP WebSocket URL *'),
4153
+ h('input', { className: 'input', placeholder: 'ws://192.168.1.100:9222/devtools/browser/...',
4154
+ value: cfg.cdpUrl || '',
4155
+ onChange: function(e) { update('cdpUrl', e.target.value); }
4156
+ }),
4157
+ h('div', { style: helpStyle }, 'WebSocket URL from chrome://inspect or --remote-debugging-port output. Format: ws://host:port/devtools/browser/<id>')
4158
+ ),
4159
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4160
+ h('div', { className: 'form-group' },
4161
+ h('label', { style: labelStyle }, 'Auth Token'),
4162
+ h('input', { className: 'input', type: 'password', placeholder: 'Optional — for authenticated CDP endpoints',
4163
+ value: cfg.cdpAuthToken || '',
4164
+ onChange: function(e) { update('cdpAuthToken', e.target.value || undefined); }
4165
+ })
4166
+ ),
4167
+ h('div', { className: 'form-group' },
4168
+ h('label', { style: labelStyle }, 'Connection Timeout (ms)'),
4169
+ h('input', { className: 'input', type: 'number', min: 5000, max: 60000,
4170
+ value: cfg.cdpTimeout || 30000,
4171
+ onChange: function(e) { update('cdpTimeout', parseInt(e.target.value) || 30000); }
4172
+ })
4173
+ )
4174
+ ),
4175
+ h('div', { className: 'form-group' },
4176
+ h('label', { style: labelStyle }, 'SSH Tunnel (auto-connect)'),
4177
+ h('input', { className: 'input', placeholder: 'ssh -L 9222:localhost:9222 user@remote-host (optional)',
4178
+ value: cfg.sshTunnel || '',
4179
+ onChange: function(e) { update('sshTunnel', e.target.value || undefined); }
4180
+ }),
4181
+ h('div', { style: helpStyle }, 'SSH command to establish tunnel before connecting. Agent will run this automatically.')
4182
+ )
4183
+ )
4184
+ ),
4072
4185
 
4073
- // Allow File URLs
4074
- h('div', { className: 'form-group' },
4075
- h('label', { style: labelStyle }, 'File URLs (file://)'),
4076
- h('select', {
4077
- className: 'input', value: cfg.allowFileUrls ? 'true' : 'false',
4078
- onChange: function(e) { update('allowFileUrls', e.target.value === 'true'); }
4079
- },
4080
- h('option', { value: 'false' }, 'Blocked — Cannot access local files'),
4081
- h('option', { value: 'true' }, 'Allowed — Can open local file:// URLs')
4082
- )
4083
- ),
4186
+ // Browserless
4187
+ provider === 'browserless' && h(Fragment, null,
4188
+ sectionTitle('\u2601\uFE0F', 'Browserless.io Configuration'),
4189
+ h('div', { style: { display: 'grid', gap: 12 } },
4190
+ h('div', { className: 'form-group' },
4191
+ h('label', { style: labelStyle }, 'API Token *'),
4192
+ h('input', { className: 'input', type: 'password', placeholder: 'Your Browserless API token',
4193
+ value: cfg.browserlessToken || '',
4194
+ onChange: function(e) { update('browserlessToken', e.target.value); }
4195
+ }),
4196
+ h('div', { style: helpStyle }, h('a', { href: 'https://www.browserless.io/dashboard', target: '_blank', style: { color: 'var(--accent)' } }, 'Get your API token'), ' from the Browserless dashboard.')
4197
+ ),
4198
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4199
+ h('div', { className: 'form-group' },
4200
+ h('label', { style: labelStyle }, 'Endpoint'),
4201
+ h('input', { className: 'input', placeholder: 'wss://chrome.browserless.io (default)',
4202
+ value: cfg.browserlessEndpoint || '',
4203
+ onChange: function(e) { update('browserlessEndpoint', e.target.value || undefined); }
4204
+ }),
4205
+ h('div', { style: helpStyle }, 'Custom endpoint for self-hosted or enterprise plans.')
4206
+ ),
4207
+ h('div', { className: 'form-group' },
4208
+ h('label', { style: labelStyle }, 'Concurrent Sessions'),
4209
+ h('input', { className: 'input', type: 'number', min: 1, max: 100,
4210
+ value: cfg.browserlessConcurrency || 5,
4211
+ onChange: function(e) { update('browserlessConcurrency', parseInt(e.target.value) || 5); }
4212
+ })
4213
+ )
4214
+ ),
4215
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4216
+ h('div', { className: 'form-group' },
4217
+ h('label', { style: labelStyle }, 'Stealth Mode'),
4218
+ h('select', { className: 'input', value: cfg.browserlessStealth ? 'true' : 'false',
4219
+ onChange: function(e) { update('browserlessStealth', e.target.value === 'true'); }
4220
+ },
4221
+ h('option', { value: 'false' }, 'Off'),
4222
+ h('option', { value: 'true' }, 'On — Evade bot detection')
4223
+ )
4224
+ ),
4225
+ h('div', { className: 'form-group' },
4226
+ h('label', { style: labelStyle }, 'Proxy'),
4227
+ h('input', { className: 'input', placeholder: 'Optional proxy URL',
4228
+ value: cfg.browserlessProxy || '',
4229
+ onChange: function(e) { update('browserlessProxy', e.target.value || undefined); }
4230
+ })
4231
+ )
4232
+ )
4233
+ )
4234
+ ),
4084
4235
 
4085
- // Max Contexts
4086
- h('div', { className: 'form-group' },
4087
- h('label', { style: labelStyle }, 'Max Browser Contexts'),
4088
- h('input', {
4089
- className: 'input', type: 'number', min: 1, max: 50,
4090
- value: cfg.maxContexts || 10,
4091
- onChange: function(e) { update('maxContexts', parseInt(e.target.value) || 10); }
4092
- }),
4093
- h('div', { style: helpStyle }, 'Maximum concurrent browser windows/tabs.')
4094
- ),
4236
+ // Browserbase
4237
+ provider === 'browserbase' && h(Fragment, null,
4238
+ sectionTitle('\uD83D\uDE80', 'Browserbase Configuration'),
4239
+ h('div', { style: { display: 'grid', gap: 12 } },
4240
+ h('div', { className: 'form-group' },
4241
+ h('label', { style: labelStyle }, 'API Key *'),
4242
+ h('input', { className: 'input', type: 'password', placeholder: 'Your Browserbase API key',
4243
+ value: cfg.browserbaseApiKey || '',
4244
+ onChange: function(e) { update('browserbaseApiKey', e.target.value); }
4245
+ }),
4246
+ h('div', { style: helpStyle }, h('a', { href: 'https://www.browserbase.com/settings', target: '_blank', style: { color: 'var(--accent)' } }, 'Get your API key'), ' from Browserbase settings.')
4247
+ ),
4248
+ h('div', { className: 'form-group' },
4249
+ h('label', { style: labelStyle }, 'Project ID *'),
4250
+ h('input', { className: 'input', placeholder: 'Your Browserbase project ID',
4251
+ value: cfg.browserbaseProjectId || '',
4252
+ onChange: function(e) { update('browserbaseProjectId', e.target.value); }
4253
+ })
4254
+ ),
4255
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4256
+ h('div', { className: 'form-group' },
4257
+ h('label', { style: labelStyle }, 'Session Recording'),
4258
+ h('select', { className: 'input', value: cfg.browserbaseRecording !== false ? 'true' : 'false',
4259
+ onChange: function(e) { update('browserbaseRecording', e.target.value === 'true'); }
4260
+ },
4261
+ h('option', { value: 'true' }, 'Enabled — Record sessions for replay'),
4262
+ h('option', { value: 'false' }, 'Disabled')
4263
+ )
4264
+ ),
4265
+ h('div', { className: 'form-group' },
4266
+ h('label', { style: labelStyle }, 'Keep Session Alive'),
4267
+ h('select', { className: 'input', value: cfg.browserbaseKeepAlive ? 'true' : 'false',
4268
+ onChange: function(e) { update('browserbaseKeepAlive', e.target.value === 'true'); }
4269
+ },
4270
+ h('option', { value: 'false' }, 'Close after task'),
4271
+ h('option', { value: 'true' }, 'Keep alive for reuse')
4272
+ )
4273
+ )
4274
+ )
4275
+ )
4276
+ ),
4095
4277
 
4096
- // Navigation Timeout
4097
- h('div', { className: 'form-group' },
4098
- h('label', { style: labelStyle }, 'Navigation Timeout (ms)'),
4099
- h('input', {
4100
- className: 'input', type: 'number', min: 5000, max: 120000, step: 1000,
4101
- value: cfg.navigationTimeoutMs || 30000,
4102
- onChange: function(e) { update('navigationTimeoutMs', parseInt(e.target.value) || 30000); }
4103
- }),
4104
- h('div', { style: helpStyle }, 'Maximum time to wait for page loads.')
4278
+ // Steel
4279
+ provider === 'steel' && h(Fragment, null,
4280
+ sectionTitle('\u26A1', 'Steel.dev Configuration'),
4281
+ h('div', { style: { display: 'grid', gap: 12 } },
4282
+ h('div', { className: 'form-group' },
4283
+ h('label', { style: labelStyle }, 'API Key *'),
4284
+ h('input', { className: 'input', type: 'password', placeholder: 'Your Steel API key',
4285
+ value: cfg.steelApiKey || '',
4286
+ onChange: function(e) { update('steelApiKey', e.target.value); }
4287
+ }),
4288
+ h('div', { style: helpStyle }, h('a', { href: 'https://app.steel.dev', target: '_blank', style: { color: 'var(--accent)' } }, 'Get your API key'), ' — or self-host Steel for free.')
4289
+ ),
4290
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4291
+ h('div', { className: 'form-group' },
4292
+ h('label', { style: labelStyle }, 'Endpoint'),
4293
+ h('input', { className: 'input', placeholder: 'https://api.steel.dev (default)',
4294
+ value: cfg.steelEndpoint || '',
4295
+ onChange: function(e) { update('steelEndpoint', e.target.value || undefined); }
4296
+ })
4297
+ ),
4298
+ h('div', { className: 'form-group' },
4299
+ h('label', { style: labelStyle }, 'Session Duration (min)'),
4300
+ h('input', { className: 'input', type: 'number', min: 1, max: 120,
4301
+ value: cfg.steelSessionDuration || 15,
4302
+ onChange: function(e) { update('steelSessionDuration', parseInt(e.target.value) || 15); }
4303
+ })
4304
+ )
4305
+ )
4306
+ )
4307
+ ),
4308
+
4309
+ // ScrapingBee
4310
+ provider === 'scrapingbee' && h(Fragment, null,
4311
+ sectionTitle('\uD83D\uDC1D', 'ScrapingBee Configuration'),
4312
+ h('div', { style: { display: 'grid', gap: 12 } },
4313
+ h('div', { className: 'form-group' },
4314
+ h('label', { style: labelStyle }, 'API Key *'),
4315
+ h('input', { className: 'input', type: 'password', placeholder: 'Your ScrapingBee API key',
4316
+ value: cfg.scrapingbeeApiKey || '',
4317
+ onChange: function(e) { update('scrapingbeeApiKey', e.target.value); }
4318
+ }),
4319
+ h('div', { style: helpStyle }, h('a', { href: 'https://www.scrapingbee.com/dashboard', target: '_blank', style: { color: 'var(--accent)' } }, 'Get your API key'), ' from ScrapingBee dashboard.')
4320
+ ),
4321
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr 1fr' } },
4322
+ h('div', { className: 'form-group' },
4323
+ h('label', { style: labelStyle }, 'JavaScript Rendering'),
4324
+ h('select', { className: 'input', value: cfg.scrapingbeeJsRendering !== false ? 'true' : 'false',
4325
+ onChange: function(e) { update('scrapingbeeJsRendering', e.target.value === 'true'); }
4326
+ },
4327
+ h('option', { value: 'true' }, 'Enabled'),
4328
+ h('option', { value: 'false' }, 'Disabled (faster)')
4329
+ )
4330
+ ),
4331
+ h('div', { className: 'form-group' },
4332
+ h('label', { style: labelStyle }, 'Premium Proxy'),
4333
+ h('select', { className: 'input', value: cfg.scrapingbeePremiumProxy ? 'true' : 'false',
4334
+ onChange: function(e) { update('scrapingbeePremiumProxy', e.target.value === 'true'); }
4335
+ },
4336
+ h('option', { value: 'false' }, 'Standard'),
4337
+ h('option', { value: 'true' }, 'Premium (residential IPs)')
4338
+ )
4339
+ ),
4340
+ h('div', { className: 'form-group' },
4341
+ h('label', { style: labelStyle }, 'Country'),
4342
+ h('input', { className: 'input', placeholder: 'us, gb, de...',
4343
+ value: cfg.scrapingbeeCountry || '',
4344
+ onChange: function(e) { update('scrapingbeeCountry', e.target.value || undefined); }
4345
+ })
4346
+ )
4347
+ )
4348
+ )
4349
+ )
4105
4350
  ),
4106
4351
 
4107
- // Idle Timeout
4108
- h('div', { className: 'form-group' },
4109
- h('label', { style: labelStyle }, 'Idle Timeout (minutes)'),
4110
- h('input', {
4111
- className: 'input', type: 'number', min: 1, max: 60,
4112
- value: Math.round((cfg.idleTimeoutMs || 300000) / 60000),
4113
- onChange: function(e) { update('idleTimeoutMs', (parseInt(e.target.value) || 5) * 60000); }
4114
- }),
4115
- h('div', { style: helpStyle }, 'Close browser after this many minutes of inactivity.')
4352
+ // ─── Section 3: Security & Limits ────────────────
4353
+ h('div', { style: sectionStyle },
4354
+ sectionTitle('\uD83D\uDD12', 'Security & Limits'),
4355
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4356
+ h('div', { className: 'form-group' },
4357
+ h('label', { style: labelStyle }, 'URL Protection'),
4358
+ h('select', { className: 'input', value: cfg.ssrfProtection || 'permissive',
4359
+ onChange: function(e) { update('ssrfProtection', e.target.value); }
4360
+ },
4361
+ h('option', { value: 'off' }, 'Off — No URL restrictions'),
4362
+ h('option', { value: 'permissive' }, 'Permissive — Block dangerous URLs'),
4363
+ h('option', { value: 'strict' }, 'Strict — Allowlist only')
4364
+ )
4365
+ ),
4366
+ h('div', { className: 'form-group' },
4367
+ h('label', { style: labelStyle }, 'JavaScript Evaluation'),
4368
+ h('select', { className: 'input', value: cfg.allowEvaluate !== false ? 'true' : 'false',
4369
+ onChange: function(e) { update('allowEvaluate', e.target.value === 'true'); }
4370
+ },
4371
+ h('option', { value: 'true' }, 'Allowed'),
4372
+ h('option', { value: 'false' }, 'Blocked')
4373
+ )
4374
+ ),
4375
+ h('div', { className: 'form-group' },
4376
+ h('label', { style: labelStyle }, 'File URLs (file://)'),
4377
+ h('select', { className: 'input', value: cfg.allowFileUrls ? 'true' : 'false',
4378
+ onChange: function(e) { update('allowFileUrls', e.target.value === 'true'); }
4379
+ },
4380
+ h('option', { value: 'false' }, 'Blocked'),
4381
+ h('option', { value: 'true' }, 'Allowed')
4382
+ )
4383
+ ),
4384
+ h('div', { className: 'form-group' },
4385
+ h('label', { style: labelStyle }, 'Max Concurrent Tabs'),
4386
+ h('input', { className: 'input', type: 'number', min: 1, max: 50,
4387
+ value: cfg.maxContexts || 10,
4388
+ onChange: function(e) { update('maxContexts', parseInt(e.target.value) || 10); }
4389
+ })
4390
+ ),
4391
+ h('div', { className: 'form-group' },
4392
+ h('label', { style: labelStyle }, 'Navigation Timeout (ms)'),
4393
+ h('input', { className: 'input', type: 'number', min: 5000, max: 120000, step: 1000,
4394
+ value: cfg.navigationTimeoutMs || 30000,
4395
+ onChange: function(e) { update('navigationTimeoutMs', parseInt(e.target.value) || 30000); }
4396
+ })
4397
+ ),
4398
+ h('div', { className: 'form-group' },
4399
+ h('label', { style: labelStyle }, 'Idle Timeout (min)'),
4400
+ h('input', { className: 'input', type: 'number', min: 1, max: 60,
4401
+ value: Math.round((cfg.idleTimeoutMs || 300000) / 60000),
4402
+ onChange: function(e) { update('idleTimeoutMs', (parseInt(e.target.value) || 5) * 60000); }
4403
+ })
4404
+ )
4405
+ ),
4406
+ h('div', { className: 'form-group', style: { marginTop: 12 } },
4407
+ h('label', { style: labelStyle }, 'Blocked URL Patterns'),
4408
+ h('input', { className: 'input', placeholder: '*://169.254.*, *://metadata.google.*',
4409
+ value: (cfg.blockedUrlPatterns || []).join(', '),
4410
+ onChange: function(e) { update('blockedUrlPatterns', e.target.value.split(',').map(function(s) { return s.trim(); }).filter(Boolean)); }
4411
+ })
4412
+ ),
4413
+ cfg.ssrfProtection === 'strict' && h('div', { className: 'form-group', style: { marginTop: 8 } },
4414
+ h('label', { style: labelStyle }, 'Allowed URL Patterns'),
4415
+ h('input', { className: 'input', placeholder: '*://example.com/*, *://app.service.com/*',
4416
+ value: (cfg.allowedUrlPatterns || []).join(', '),
4417
+ onChange: function(e) { update('allowedUrlPatterns', e.target.value.split(',').map(function(s) { return s.trim(); }).filter(Boolean)); }
4418
+ })
4419
+ )
4116
4420
  ),
4117
4421
 
4118
- // Blocked URL Patterns
4119
- h('div', { className: 'form-group' },
4120
- h('label', { style: labelStyle }, 'Blocked URL Patterns'),
4121
- h('input', {
4122
- className: 'input', placeholder: '*://169.254.*, *://metadata.google.*',
4123
- value: (cfg.blockedUrlPatterns || []).join(', '),
4124
- onChange: function(e) { update('blockedUrlPatterns', e.target.value.split(',').map(function(s) { return s.trim(); }).filter(Boolean)); }
4125
- }),
4126
- h('div', { style: helpStyle }, 'Comma-separated URL patterns to block (supports *).')
4422
+ // ─── Section 4: Meeting & Video Capabilities ─────
4423
+ h('div', { style: sectionStyle },
4424
+ sectionTitle('\uD83C\uDFA5', 'Meeting & Video Call Capabilities'),
4425
+ h('div', { style: { padding: '10px 14px', background: 'var(--bg-secondary)', borderRadius: 'var(--radius)', marginBottom: 12, fontSize: 12, lineHeight: 1.6 } },
4426
+ h('div', { style: { fontWeight: 600, marginBottom: 6 } }, 'For Google Meet, Teams, and Zoom:'),
4427
+ h('div', null, '\u2022 Use ', h('strong', null, 'Remote Browser (CDP)'), ' provider pointed at a VM with display + virtual camera'),
4428
+ h('div', null, '\u2022 The remote machine needs: X11/Wayland display, PulseAudio/PipeWire (audio), v4l2loopback (virtual camera)'),
4429
+ h('div', null, '\u2022 Agent will navigate to meeting URL, handle permissions, and interact with the meeting UI'),
4430
+ h('div', null, '\u2022 For voice participation: integrate with a Speech-to-Text / Text-to-Speech service'),
4431
+ h('div', { style: { marginTop: 8 } },
4432
+ h('strong', null, 'Quick VM Setup (Linux):'),
4433
+ h('pre', { style: { margin: '6px 0', padding: 8, background: 'var(--bg-tertiary)', borderRadius: 4, fontSize: 11, overflow: 'auto' } },
4434
+ '# Install deps\nsudo apt install -y xvfb pulseaudio chromium v4l2loopback-dkms\n\n# Start virtual display + audio\nXvfb :99 -screen 0 1920x1080x24 &\nexport DISPLAY=:99\npulseaudio --start\n\n# Launch Chrome with remote debugging\nchromium --remote-debugging-port=9222 \\\n --no-first-run --disable-gpu \\\n --use-fake-ui-for-media-stream \\\n --use-fake-device-for-media-stream \\\n --auto-accept-camera-and-microphone-capture'
4435
+ )
4436
+ )
4437
+ ),
4438
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4439
+ h('div', { className: 'form-group' },
4440
+ h('label', { style: labelStyle }, 'Auto-Accept Permissions'),
4441
+ h('select', { className: 'input', value: cfg.autoAcceptPermissions !== false ? 'true' : 'false',
4442
+ onChange: function(e) { update('autoAcceptPermissions', e.target.value === 'true'); }
4443
+ },
4444
+ h('option', { value: 'true' }, 'Yes — Auto-grant camera/microphone access'),
4445
+ h('option', { value: 'false' }, 'No — Require manual permission grants')
4446
+ )
4447
+ ),
4448
+ h('div', { className: 'form-group' },
4449
+ h('label', { style: labelStyle }, 'Virtual Camera Feed'),
4450
+ h('input', { className: 'input', placeholder: '/dev/video0 or URL to video feed',
4451
+ value: cfg.virtualCameraSource || '',
4452
+ onChange: function(e) { update('virtualCameraSource', e.target.value || undefined); }
4453
+ }),
4454
+ h('div', { style: helpStyle }, 'Video source for the agent\'s "camera" in meetings.')
4455
+ ),
4456
+ h('div', { className: 'form-group' },
4457
+ h('label', { style: labelStyle }, 'Audio Input'),
4458
+ h('input', { className: 'input', placeholder: 'PulseAudio source (e.g. virtual_mic)',
4459
+ value: cfg.audioInput || '',
4460
+ onChange: function(e) { update('audioInput', e.target.value || undefined); }
4461
+ }),
4462
+ h('div', { style: helpStyle }, 'Audio source for the agent to speak in meetings.')
4463
+ ),
4464
+ h('div', { className: 'form-group' },
4465
+ h('label', { style: labelStyle }, 'Audio Output'),
4466
+ h('input', { className: 'input', placeholder: 'PulseAudio sink (e.g. virtual_speaker)',
4467
+ value: cfg.audioOutput || '',
4468
+ onChange: function(e) { update('audioOutput', e.target.value || undefined); }
4469
+ }),
4470
+ h('div', { style: helpStyle }, 'Audio sink for the agent to hear meeting participants.')
4471
+ )
4472
+ )
4127
4473
  ),
4128
4474
 
4129
- // Allowed URL Patterns (only for strict mode)
4130
- cfg.ssrfProtection === 'strict' && h('div', { className: 'form-group' },
4131
- h('label', { style: labelStyle }, 'Allowed URL Patterns (Strict Mode)'),
4132
- h('input', {
4133
- className: 'input', placeholder: '*://example.com/*, *://api.service.com/*',
4134
- value: (cfg.allowedUrlPatterns || []).join(', '),
4135
- onChange: function(e) { update('allowedUrlPatterns', e.target.value.split(',').map(function(s) { return s.trim(); }).filter(Boolean)); }
4136
- }),
4137
- h('div', { style: helpStyle }, 'Only these URLs are allowed in strict mode.')
4475
+ // ─── Section 5: Persistent Sessions ──────────────
4476
+ h('div', { style: { paddingTop: 12 } },
4477
+ sectionTitle('\uD83D\uDD04', 'Session Persistence'),
4478
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4479
+ h('div', { className: 'form-group' },
4480
+ h('label', { style: labelStyle }, 'Persist Login Sessions'),
4481
+ h('select', { className: 'input', value: cfg.persistSessions ? 'true' : 'false',
4482
+ onChange: function(e) { update('persistSessions', e.target.value === 'true'); }
4483
+ },
4484
+ h('option', { value: 'false' }, 'No — Fresh session each time'),
4485
+ h('option', { value: 'true' }, 'Yes — Keep cookies, localStorage, logins')
4486
+ ),
4487
+ h('div', { style: helpStyle }, 'Persistent sessions let agents stay logged into web apps.')
4488
+ ),
4489
+ h('div', { className: 'form-group' },
4490
+ h('label', { style: labelStyle }, 'Session Storage Path'),
4491
+ h('input', { className: 'input', placeholder: '/data/browser-sessions/' + agentId.slice(0, 8),
4492
+ value: cfg.sessionStoragePath || '',
4493
+ onChange: function(e) { update('sessionStoragePath', e.target.value || undefined); }
4494
+ }),
4495
+ h('div', { style: helpStyle }, 'Directory to store persistent browser state.')
4496
+ )
4497
+ )
4138
4498
  ),
4139
4499
 
4140
- // Save button
4141
- h('div', { style: { display: 'flex', justifyContent: 'flex-end', paddingTop: 8 } },
4500
+ // ─── Actions Bar ─────────────────────────────────
4501
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingTop: 16, marginTop: 8, borderTop: '1px solid var(--border)' } },
4502
+ h('div', { style: { display: 'flex', gap: 8 } },
4503
+ h('button', { className: 'btn btn-sm', disabled: testing, onClick: testConnection },
4504
+ testing ? 'Testing...' : '\u{1F50C} Test Connection'
4505
+ ),
4506
+ testResult && h('span', { style: { fontSize: 12, color: testResult.error ? 'var(--danger)' : 'var(--success)', alignSelf: 'center' } },
4507
+ testResult.error ? '\u274C ' + testResult.error : '\u2705 Connected — ' + (testResult.browserVersion || 'OK')
4508
+ )
4509
+ ),
4142
4510
  h('button', { className: 'btn', disabled: saving, onClick: save }, saving ? 'Saving...' : 'Save Browser Config')
4143
4511
  )
4144
4512
  )