{
  "README.md": "# CloudBrowser VPS — Browser Control & \"Add as a Tool\" Session Sharing System\n\nRuns on Node.js + Playwright Real Chromium + WebSockets.\n\n## Quick Start\n```bash\nnpm install ws playwright\nnpx playwright install --with-deps chromium\nnode server.mjs\n```\n",
  "package.json": "{\n  \"name\": \"cloudbrowser\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"playwright\": \"^1.64.1\",\n    \"ws\": \"^8.22.0\"\n  }\n}\n",
  "server.mjs": "import http from 'node:http';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport crypto from 'node:crypto';\nimport { fileURLToPath } from 'node:url';\nimport { WebSocketServer } from 'ws';\nimport { chromium } from 'playwright';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst PORT = 4100;\nconst DATA_DIR = path.join(__dirname, 'data');\nconst PROFILES_DIR = path.join(__dirname, 'profiles');\nconst PUBLIC_DIR = path.join(__dirname, 'public');\nconst DB_FILE = path.join(DATA_DIR, 'state.json');\n\nfs.mkdirSync(DATA_DIR, { recursive: true });\nfs.mkdirSync(PROFILES_DIR, { recursive: true });\nfs.mkdirSync(PUBLIC_DIR, { recursive: true });\n\nfunction hashPassword(pw) {\n  return crypto.createHash('sha256').update(String(pw) + '::cloudbrowser-vps-salt').digest('hex');\n}\n\nfunction maskEmail(email) {\n  const str = String(email || 'shared@asacompany.com');\n  const [user, domain] = str.split('@');\n  if (!domain) return 'sh***@asacompany.com';\n  return `${user.slice(0, 3)}***@${domain}`;\n}\n\nfunction cleanToolTitle(rawTitle, urlStr) {\n  let t = String(rawTitle || '').trim();\n  if (!t || t === 'about:blank') {\n    try {\n      const u = new URL(urlStr);\n      return u.hostname.replace(/^www\\./, '');\n    } catch {\n      return 'Shared Web Tool';\n    }\n  }\n  return t.length > 36 ? t.slice(0, 36) + '…' : t;\n}\n\nfunction pickToolColor(urlStr) {\n  const lower = String(urlStr || '').toLowerCase();\n  if (lower.includes('canva.com')) return 'linear-gradient(135deg, #00C4CC 0%, #7D2AE8 100%)';\n  if (lower.includes('google.com')) return 'linear-gradient(135deg, #4285F4 0%, #34A853 100%)';\n  if (lower.includes('figma.com')) return 'linear-gradient(135deg, #F24E1E 0%, #A259FF 100%)';\n  if (lower.includes('photopea.com')) return 'linear-gradient(135deg, #10B981 0%, #047857 100%)';\n  return 'linear-gradient(135deg, #4F46E5 0%, #0D9488 100%)';\n}\n\nfunction defaultState() {\n  return {\n    users: [\n      {\n        id: 1,\n        name: 'Priya Nair (Company HR)',\n        email: 'hr@asacompany.com',\n        password_hash: hashPassword('HR@12345'),\n        role: 'hr',\n        department: 'Human Resources & Browsing Admin',\n        can_browse: true,\n        can_manage_access: true,\n        is_demo: true\n      },\n      {\n        id: 2,\n        name: 'Arjun Mehta (System Admin)',\n        email: 'admin@asacompany.com',\n        password_hash: hashPassword('Admin@12345'),\n        role: 'admin',\n        department: 'IT & Cloud Browser Admin',\n        can_browse: true,\n        can_manage_access: true,\n        is_demo: true\n      },\n      {\n        id: 3,\n        name: 'Rahul Sharma (Editor 1)',\n        email: 'editor1@asacompany.com',\n        password_hash: hashPassword('Editor@123'),\n        role: 'editor',\n        department: 'Creative Design Team',\n        can_browse: false,\n        can_manage_access: false,\n        is_demo: true\n      },\n      {\n        id: 4,\n        name: 'Ananya Verma (Editor 2)',\n        email: 'editor2@asacompany.com',\n        password_hash: hashPassword('Editor@123'),\n        role: 'editor',\n        department: 'Creative Design Team',\n        can_browse: false,\n        can_manage_access: false,\n        is_demo: true\n      },\n      {\n        id: 5,\n        name: 'Aman Gupta (Editor 3)',\n        email: 'editor3@asacompany.com',\n        password_hash: hashPassword('Editor@123'),\n        role: 'editor',\n        department: 'Social Media Graphics',\n        can_browse: false,\n        can_manage_access: false,\n        is_demo: true\n      },\n      {\n        id: 6,\n        name: 'Neha Kapoor (Editor 4)',\n        email: 'editor4@asacompany.com',\n        password_hash: hashPassword('Editor@123'),\n        role: 'editor',\n        department: 'Brand & Video Editing',\n        can_browse: false,\n        can_manage_access: false,\n        is_demo: true\n      }\n    ],\n    apps: [\n      {\n        id: 1,\n        slug: 'canva',\n        name: 'Canva (Shared Tool)',\n        category: 'Added Browser Tool',\n        target_url: 'https://www.canva.com/',\n        icon_bg: 'linear-gradient(135deg, #00C4CC 0%, #7D2AE8 100%)',\n        icon_letter: 'C',\n        account_email_masked: 'des***@asacompany.com',\n        session_status: 'active',\n        logged_in_by: 'Priya Nair (Company HR)',\n        logged_in_at: new Date().toISOString(),\n        assigned_user_ids: [1, 2, 3, 4, 5, 6],\n        description: 'Added as a tool from the Cloud Browser. Editors click to open Canva directly in the shared browser without email or password.'\n      }\n    ],\n    logs: [\n      {\n        id: 1,\n        time: new Date().toISOString().replace('T', ' ').slice(0, 19),\n        user_name: 'Priya Nair (Company HR)',\n        user_role: 'hr',\n        action_type: 'TOOL_ADDED',\n        target: 'Canva (https://www.canva.com/)',\n        details: 'Opened website in Cloud Browser, logged in, and clicked \"Add as a Tool\" for the 4 Team Editors'\n      }\n    ]\n  };\n}\n\nlet db = defaultState();\nif (fs.existsSync(DB_FILE)) {\n  try {\n    db = JSON.parse(fs.readFileSync(DB_FILE, 'utf8'));\n  } catch {\n    db = defaultState();\n  }\n} else {\n  fs.writeFileSync(DB_FILE, JSON.stringify(db, null, 2));\n}\n\nfunction saveDb() {\n  fs.writeFileSync(DB_FILE, JSON.stringify(db, null, 2));\n}\n\nfunction addLog(userName, userRole, actionType, target, details) {\n  db.logs.unshift({\n    id: Date.now() + Math.floor(Math.random() * 1000),\n    time: new Date().toISOString().replace('T', ' ').slice(0, 19),\n    user_name: userName,\n    user_role: userRole,\n    action_type: actionType,\n    target,\n    details\n  });\n  if (db.logs.length > 60) db.logs.length = 60;\n  saveDb();\n}\n\nconst sessions = new Map();\n\nfunction parseCookies(req) {\n  const list = {};\n  const rc = req.headers.cookie;\n  if (!rc) return list;\n  rc.split(';').forEach(cookie => {\n    const parts = cookie.split('=');\n    const k = parts.shift().trim();\n    const v = decodeURI(parts.join('='));\n    list[k] = v;\n  });\n  return list;\n}\n\nfunction getUserFromReq(req) {\n  const cookies = parseCookies(req);\n  const tok = cookies['cb_vps_token'];\n  if (!tok || !sessions.has(tok)) return null;\n  const uid = sessions.get(tok);\n  return db.users.find(u => u.id === uid) || null;\n}\n\n// ==================== REAL CHROMIUM CLOUD BROWSER ENGINE (ON VPS) ====================\nlet browserContext = null;\nlet activePage = null;\nlet cdpSession = null;\nlet lastFrameBase64 = null;\nlet currentBrowserUrl = 'https://www.google.com/?hl=en';\nlet currentBrowserTitle = 'Google';\nconst wsClients = new Set();\n\nfunction broadcastWs(msgObj) {\n  const raw = JSON.stringify(msgObj);\n  for (const client of wsClients) {\n    if (client.readyState === 1) {\n      client.send(raw);\n    }\n  }\n}\n\nfunction normalizeUrlOrSearch(rawInput) {\n  const s = String(rawInput || '').trim();\n  if (!s) return 'https://www.google.com/?hl=en';\n  if (/^https?:\\/\\//i.test(s)) return s;\n  // If it has spaces or no dot, treat it as a Google search!\n  if (/\\s/.test(s) || !s.includes('.')) {\n    return `https://www.google.com/search?q=${encodeURIComponent(s)}&hl=en`;\n  }\n  return 'https://' + s;\n}\n\nasync function ensureCloudBrowser() {\n  if (browserContext && activePage && !activePage.isClosed()) {\n    return { context: browserContext, page: activePage, cdp: cdpSession };\n  }\n\n  const profilePath = path.join(PROFILES_DIR, 'shared-workspace');\n  fs.mkdirSync(profilePath, { recursive: true });\n\n  browserContext = await chromium.launchPersistentContext(profilePath, {\n    headless: true,\n    viewport: { width: 1280, height: 720 },\n    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',\n    locale: 'en-US',\n    args: [\n      '--no-sandbox',\n      '--disable-setuid-sandbox',\n      '--disable-dev-shm-usage',\n      '--disable-blink-features=AutomationControlled'\n    ]\n  });\n\n  await browserContext.addInitScript(() => {\n    Object.defineProperty(navigator, 'webdriver', { get: () => undefined });\n  });\n\n  const pages = browserContext.pages();\n  activePage = pages.length > 0 ? pages[0] : await browserContext.newPage();\n\n  // Keep popups (like \"Continue with Google\" or target=\"_blank\" links) inside our visible streaming tab\n  browserContext.on('page', async newPage => {\n    try {\n      await newPage.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});\n      const u = newPage.url();\n      if (u && u !== 'about:blank') {\n        await newPage.close().catch(() => {});\n        await activePage.goto(u, { waitUntil: 'domcontentloaded', timeout: 20000 }).catch(() => {});\n      }\n    } catch {}\n  });\n\n  activePage.on('framenavigated', async frame => {\n    if (frame === activePage.mainFrame()) {\n      currentBrowserUrl = frame.url();\n      currentBrowserTitle = await activePage.title().catch(() => currentBrowserUrl);\n      broadcastWs({\n        type: 'url_changed',\n        url: currentBrowserUrl,\n        title: currentBrowserTitle\n      });\n    }\n  });\n\n  cdpSession = await browserContext.newCDPSession(activePage);\n  cdpSession.on('Page.screencastFrame', async event => {\n    lastFrameBase64 = event.data;\n    try {\n      await cdpSession.send('Page.screencastFrameAck', { sessionId: event.sessionId });\n    } catch {}\n    broadcastWs({\n      type: 'frame',\n      data: event.data,\n      url: currentBrowserUrl,\n      title: currentBrowserTitle\n    });\n  });\n\n  await cdpSession.send('Page.startScreencast', {\n    format: 'jpeg',\n    quality: 80,\n    maxWidth: 1280,\n    maxHeight: 720,\n    everyNthFrame: 1\n  });\n\n  // Start on Google so the user can immediately search Google, log into Google, or go to canva.com\n  await activePage.goto('https://www.google.com/?hl=en', { waitUntil: 'domcontentloaded', timeout: 25000 }).catch(() => {});\n  currentBrowserUrl = activePage.url();\n  currentBrowserTitle = await activePage.title().catch(() => 'Google');\n  return { context: browserContext, page: activePage, cdp: cdpSession };\n}\n\nensureCloudBrowser().then(() => {\n  console.log('✔ VPS Real Chromium Cloud Browser started on Google.com and streaming');\n}).catch(err => {\n  console.error('Chromium startup error:', err);\n});\n\nfunction readJsonBody(req) {\n  return new Promise(resolve => {\n    let body = '';\n    req.on('data', chunk => {\n      body += chunk;\n      if (body.length > 1e6) req.destroy();\n    });\n    req.on('end', () => {\n      try {\n        resolve(JSON.parse(body || '{}'));\n      } catch {\n        resolve({});\n      }\n    });\n  });\n}\n\nfunction sendJson(res, status, data, extraHeaders = {}) {\n  res.writeHead(status, {\n    'Content-Type': 'application/json; charset=utf-8',\n    'Cache-Control': 'no-store',\n    ...extraHeaders\n  });\n  res.end(JSON.stringify(data));\n}\n\nconst server = http.createServer(async (req, res) => {\n  const urlObj = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);\n  const pathname = urlObj.pathname;\n\n  if (pathname === '/api/action') {\n    const body = req.method === 'POST' ? await readJsonBody(req) : {};\n    const action = body.action || urlObj.searchParams.get('action') || '';\n\n    // 1. Login\n    if (action === 'login') {\n      const email = String(body.email || '').trim().toLowerCase();\n      const pass = String(body.password || '');\n      const u = db.users.find(x => x.email.toLowerCase() === email && x.password_hash === hashPassword(pass));\n      if (!u) {\n        return sendJson(res, 200, { ok: false, error: 'Invalid email or password. Use the 1-Click Login buttons below or Create an Account!' });\n      }\n      const tok = crypto.randomBytes(24).toString('hex');\n      sessions.set(tok, u.id);\n      addLog(u.name, u.role, 'PORTAL_LOGIN', 'CloudBrowser VPS', `Signed in as ${u.role.toUpperCase()}`);\n      return sendJson(res, 200, { ok: true, user: u }, {\n        'Set-Cookie': `cb_vps_token=${tok}; Path=/; HttpOnly; Max-Age=604800; SameSite=Lax`\n      });\n    }\n\n    // 2. Sign Up (Create Account directly on website — no extension needed!)\n    if (action === 'signup') {\n      const name = String(body.name || '').trim();\n      const email = String(body.email || '').trim().toLowerCase();\n      const pass = String(body.password || '');\n      const role = body.role === 'editor' ? 'editor' : 'hr';\n\n      if (!name || !email || pass.length < 4) {\n        return sendJson(res, 200, { ok: false, error: 'Please enter your Name, Email, and a Password (min 4 chars).' });\n      }\n      if (db.users.some(x => x.email.toLowerCase() === email)) {\n        return sendJson(res, 200, { ok: false, error: 'An account with this email already exists. Please log in!' });\n      }\n      const newId = db.users.reduce((m, x) => Math.max(m, x.id), 100) + 1;\n      const newUser = {\n        id: newId,\n        name: `${name} (${role === 'hr' ? 'Browser Admin / HR' : 'Team Editor'})`,\n        email,\n        password_hash: hashPassword(pass),\n        role,\n        department: role === 'hr' ? 'Browser Admin & HR' : 'Creative Design Team',\n        can_browse: role === 'hr',\n        can_manage_access: role === 'hr',\n        is_demo: false,\n        created_at: new Date().toISOString()\n      };\n      db.users.push(newUser);\n      for (const app of db.apps) {\n        if (!app.assigned_user_ids.includes(newId)) {\n          app.assigned_user_ids.push(newId);\n        }\n      }\n      saveDb();\n\n      const tok = crypto.randomBytes(24).toString('hex');\n      sessions.set(tok, newUser.id);\n      addLog(newUser.name, newUser.role, 'ACCOUNT_CREATED', 'CloudBrowser VPS', `Created new ${role.toUpperCase()} account`);\n      return sendJson(res, 200, { ok: true, user: newUser }, {\n        'Set-Cookie': `cb_vps_token=${tok}; Path=/; HttpOnly; Max-Age=604800; SameSite=Lax`\n      });\n    }\n\n    // 3. Quick Role Switch (1-Click Testing)\n    if (action === 'quick_switch') {\n      const email = String(body.email || '').trim().toLowerCase();\n      const u = db.users.find(x => x.email.toLowerCase() === email);\n      if (!u) return sendJson(res, 200, { ok: false, error: 'User not found' });\n      const tok = crypto.randomBytes(24).toString('hex');\n      sessions.set(tok, u.id);\n      addLog(u.name, u.role, 'ROLE_SWITCH', 'CloudBrowser VPS', `Switched active user to ${u.name}`);\n      return sendJson(res, 200, { ok: true, user: u }, {\n        'Set-Cookie': `cb_vps_token=${tok}; Path=/; HttpOnly; Max-Age=604800; SameSite=Lax`\n      });\n    }\n\n    // 4. Logout\n    if (action === 'logout') {\n      const cookies = parseCookies(req);\n      if (cookies['cb_vps_token']) sessions.delete(cookies['cb_vps_token']);\n      return sendJson(res, 200, { ok: true }, {\n        'Set-Cookie': 'cb_vps_token=; Path=/; HttpOnly; Max-Age=0'\n      });\n    }\n\n    const user = getUserFromReq(req);\n    if (!user) {\n      return sendJson(res, 401, { ok: false, error: 'Not authenticated' });\n    }\n    const isManager = Boolean(user.can_manage_access || user.role === 'admin' || user.role === 'hr');\n\n    // 5. Portal State\n    if (action === 'state') {\n      const appsWithAccess = db.apps.map(a => ({\n        ...a,\n        has_access: isManager || a.assigned_user_ids.includes(user.id)\n      }));\n      const visibleUsers = db.users\n        .filter(u => !u.hidden)\n        .map(({ password_hash, ...rest }) => rest);\n\n      return sendJson(res, 200, {\n        ok: true,\n        user: {\n          id: user.id,\n          name: user.name,\n          email: user.email,\n          role: user.role,\n          department: user.department,\n          can_browse: Boolean(user.can_browse),\n          can_manage_access: isManager\n        },\n        browser: {\n          current_url: currentBrowserUrl,\n          current_title: currentBrowserTitle\n        },\n        apps: appsWithAccess,\n        team: visibleUsers,\n        logs: db.logs\n      });\n    }\n\n    // 6. \"ADD AS A TOOL\" — Saves current browser URL/session (or custom URL) as a shared tool for Editors!\n    if (action === 'add_as_tool' || action === 'create_app') {\n      const rawUrl = String(body.target_url || currentBrowserUrl || 'https://www.canva.com/').trim();\n      const targetUrl = normalizeUrlOrSearch(rawUrl);\n      const rawName = String(body.name || '').trim();\n      const toolName = rawName || cleanToolTitle(currentBrowserTitle, targetUrl);\n      const email = String(body.account_email || user.email || 'hr@asacompany.com').trim();\n\n      const newId = db.apps.reduce((m, a) => Math.max(m, a.id), 0) + 1;\n      const allUserIds = db.users.map(u => u.id);\n      const newTool = {\n        id: newId,\n        slug: 'tool-' + newId,\n        name: toolName,\n        category: 'Shared Browser Tool',\n        target_url: targetUrl,\n        icon_bg: pickToolColor(targetUrl),\n        icon_letter: toolName.charAt(0).toUpperCase() || 'T',\n        account_email_masked: maskEmail(email),\n        session_status: 'active',\n        logged_in_by: user.name,\n        logged_in_at: new Date().toISOString(),\n        assigned_user_ids: allUserIds,\n        description: `Added via \"Add as a Tool\" by ${user.name}. Shared browser session ready for Team Editors.`\n      };\n      db.apps.push(newTool);\n      saveDb();\n\n      addLog(user.name, user.role, 'TOOL_ADDED', `${toolName} (${targetUrl})`, `Added \"${toolName}\" as a shared Tool for all Team Editors`);\n      broadcastWs({ type: 'tools_updated', tool: newTool });\n      return sendJson(res, 200, { ok: true, app: newTool });\n    }\n\n    // 7. Delete a Tool\n    if (action === 'delete_tool') {\n      if (!isManager) return sendJson(res, 200, { ok: false, error: 'Only HR/Admin can remove tools.' });\n      const toolId = Number(body.app_id || 0);\n      db.apps = db.apps.filter(a => a.id !== toolId);\n      saveDb();\n      broadcastWs({ type: 'tools_updated' });\n      return sendJson(res, 200, { ok: true });\n    }\n\n    // 8. Launch Added Tool in Live Cloud Browser\n    if (action === 'launch_app') {\n      const app = db.apps.find(a => a.id === Number(body.app_id || 1));\n      if (!app) return sendJson(res, 200, { ok: false, error: 'Tool not found' });\n      const hasAccess = isManager || app.assigned_user_ids.includes(user.id);\n      if (!hasAccess) {\n        addLog(user.name, user.role, 'SECURITY_BLOCK', app.name, `Blocked unauthorized tool launch by ${user.name}`);\n        return sendJson(res, 200, { ok: false, error: 'Access Denied: Company HR has not granted your Editor account permission for this tool.' });\n      }\n      addLog(user.name, user.role, 'TOOL_OPENED', app.name, `Clicked tool \"${app.name}\" -> opened ${app.target_url} in shared Cloud Browser`);\n\n      ensureCloudBrowser().then(({ page }) => {\n        page.goto(app.target_url, { waitUntil: 'domcontentloaded', timeout: 25000 }).catch(() => {});\n      }).catch(() => {});\n\n      return sendJson(res, 200, { ok: true, app });\n    }\n\n    // 9. Toggle Tool Permission for Editor (HR / Admin)\n    if (action === 'toggle_permission') {\n      if (!isManager) return sendJson(res, 200, { ok: false, error: 'Only HR or Admin can change permissions.' });\n      const appId = Number(body.app_id || 1);\n      const targetUserId = Number(body.user_id || 0);\n      const grant = Boolean(body.grant);\n      const app = db.apps.find(a => a.id === appId) || db.apps[0];\n      const targetUser = db.users.find(u => u.id === targetUserId);\n      if (!app || !targetUser) return sendJson(res, 200, { ok: false, error: 'Invalid tool or user' });\n\n      if (grant) {\n        if (!app.assigned_user_ids.includes(targetUserId)) app.assigned_user_ids.push(targetUserId);\n        addLog(user.name, user.role, 'ACCESS_GRANTED', app.name, `Granted tool access to ${targetUser.name}`);\n      } else {\n        app.assigned_user_ids = app.assigned_user_ids.filter(id => id !== targetUserId);\n        addLog(user.name, user.role, 'ACCESS_REVOKED', app.name, `Revoked tool access from ${targetUser.name}`);\n      }\n      saveDb();\n      return sendJson(res, 200, { ok: true });\n    }\n\n    // 10. Grant All Tools to All Editors (HR 1-Click)\n    if (action === 'grant_all_editors') {\n      if (!isManager) return sendJson(res, 200, { ok: false, error: 'Only HR or Admin can grant access.' });\n      for (const app of db.apps) {\n        for (const u of db.users) {\n          if (!app.assigned_user_ids.includes(u.id)) {\n            app.assigned_user_ids.push(u.id);\n          }\n        }\n      }\n      saveDb();\n      addLog(user.name, user.role, 'BULK_ACCESS_GRANTED', 'All Tools', 'Granted all shared tools to all 4 Team Editors');\n      return sendJson(res, 200, { ok: true });\n    }\n\n    // 11. Toggle Free Web Browsing Permission for User (HR / Admin)\n    if (action === 'toggle_browsing') {\n      if (!isManager) return sendJson(res, 200, { ok: false, error: 'Only HR or Admin can change browsing permission.' });\n      const targetUser = db.users.find(u => u.id === Number(body.user_id || 0));\n      if (!targetUser) return sendJson(res, 200, { ok: false, error: 'User not found' });\n      targetUser.can_browse = Boolean(body.can_browse);\n      saveDb();\n      addLog(user.name, user.role, 'BROWSING_POLICY', targetUser.name, `${targetUser.can_browse ? 'ENABLED Google & Free Web Browsing' : 'RESTRICTED to Added Tools Only'} for ${targetUser.name}`);\n      return sendJson(res, 200, { ok: true });\n    }\n\n    return sendJson(res, 400, { ok: false, error: 'Unknown action' });\n  }\n\n  // Download Open-Source Code Package\n  if (pathname === '/api/download-source') {\n    const readme = `# CloudBrowser VPS — Browser Control & \"Add as a Tool\" Session Sharing System\\n\\nRuns on Node.js + Playwright Real Chromium + WebSockets.\\n\\n## Quick Start\\n\\`\\`\\`bash\\nnpm install ws playwright\\nnpx playwright install --with-deps chromium\\nnode server.mjs\\n\\`\\`\\`\\n`;\n    const bundle = JSON.stringify({\n      'README.md': readme,\n      'package.json': fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'),\n      'server.mjs': fs.readFileSync(path.join(__dirname, 'server.mjs'), 'utf8'),\n      'public/index.html': fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8')\n    }, null, 2);\n    res.writeHead(200, {\n      'Content-Type': 'application/json; charset=utf-8',\n      'Content-Disposition': 'attachment; filename=\"cloudbrowser-vps-opensource.json\"'\n    });\n    return res.end(bundle);\n  }\n\n  // Serve Frontend UI\n  if (pathname === '/' || pathname === '/index.html') {\n    const html = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');\n    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });\n    return res.end(html);\n  }\n\n  res.writeHead(404, { 'Content-Type': 'text/plain' });\n  res.end('Not found');\n});\n\n// ==================== WEBSOCKET LIVE CHROMIUM STREAM & INPUT HANDLER ====================\nconst wss = new WebSocketServer({ server, path: '/ws' });\n\nwss.on('connection', async (ws, req) => {\n  const user = getUserFromReq(req);\n  if (!user) {\n    ws.close(1008, 'Unauthorized');\n    return;\n  }\n\n  wsClients.add(ws);\n  const isManager = Boolean(user.can_manage_access || user.role === 'admin' || user.role === 'hr');\n\n  if (lastFrameBase64) {\n    ws.send(JSON.stringify({\n      type: 'frame',\n      data: lastFrameBase64,\n      url: currentBrowserUrl,\n      title: currentBrowserTitle\n    }));\n  }\n\n  ws.on('message', async raw => {\n    try {\n      const msg = JSON.parse(String(raw));\n      const { page } = await ensureCloudBrowser();\n\n      if (msg.type === 'navigate') {\n        const rawInput = String(msg.url || '').trim();\n        if (!rawInput) return;\n        const targetUrl = normalizeUrlOrSearch(rawInput);\n\n        const lower = targetUrl.toLowerCase();\n        // Block /settings, /account, /password, /billing for Editor role\n        if (!isManager && (/\\/settings|\\/account|\\/billing|\\/password|\\/security/.test(lower))) {\n          addLog(user.name, user.role, 'SECURITY_BLOCK', targetUrl, 'Blocked Editor from opening account/password settings in Cloud Browser');\n          ws.send(JSON.stringify({\n            type: 'policy_block',\n            title: '🔒 Account & Password Settings Locked by HR',\n            message: 'Your Editor account can use shared tools (like Canva), but account email, password, and billing settings are protected by Company HR.'\n          }));\n          return;\n        }\n\n        // Check Browsing Permission for Editors\n        const freshUser = db.users.find(u => u.id === user.id) || user;\n        const canBrowse = Boolean(freshUser.can_browse || isManager);\n        if (!canBrowse) {\n          const allowedHosts = ['canva.com', 'www.canva.com'];\n          for (const a of db.apps) {\n            if (a.assigned_user_ids.includes(user.id)) {\n              try {\n                const h = new URL(a.target_url).hostname.toLowerCase();\n                allowedHosts.push(h, h.replace(/^www\\./, ''));\n              } catch {}\n            }\n          }\n          let reqHost = '';\n          try { reqHost = new URL(targetUrl).hostname.toLowerCase(); } catch {}\n          const hostAllowed = allowedHosts.some(ah => reqHost === ah || reqHost.endsWith('.' + ah));\n          if (!hostAllowed) {\n            addLog(user.name, user.role, 'BROWSING_BLOCKED', targetUrl, `Editor without Browsing Permission tried to visit ${targetUrl}`);\n            ws.send(JSON.stringify({\n              type: 'policy_block',\n              title: '🛡️ Free Browsing Disabled for Editor Role',\n              message: `You are signed in as ${user.name} (Free Browsing: OFF). Only HR/Admin can search Google freely and add new tools. Click one of the Added Tools above (like Canva) to start working!`\n            }));\n            return;\n          }\n        }\n\n        addLog(user.name, user.role, 'CLOUD_BROWSE', targetUrl, `Navigated Cloud Browser to ${targetUrl}`);\n        await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 25000 }).catch(() => {});\n        return;\n      }\n\n      if (msg.type === 'go_back') {\n        await page.goBack({ timeout: 10000 }).catch(() => {});\n        return;\n      }\n      if (msg.type === 'go_forward') {\n        await page.goForward({ timeout: 10000 }).catch(() => {});\n        return;\n      }\n      if (msg.type === 'reload') {\n        await page.reload({ timeout: 15000 }).catch(() => {});\n        return;\n      }\n\n      if (msg.type === 'mouse') {\n        const x = Math.max(0, Math.min(1280, Math.round(Number(msg.x || 0))));\n        const y = Math.max(0, Math.min(720, Math.round(Number(msg.y || 0))));\n        if (msg.eventType === 'mousePressed') {\n          await page.mouse.move(x, y).catch(() => {});\n          await page.mouse.down({ button: msg.button || 'left', clickCount: msg.clickCount || 1 }).catch(() => {});\n        } else if (msg.eventType === 'mouseReleased') {\n          await page.mouse.move(x, y).catch(() => {});\n          await page.mouse.up({ button: msg.button || 'left', clickCount: msg.clickCount || 1 }).catch(() => {});\n        } else if (msg.eventType === 'mouseMoved') {\n          await page.mouse.move(x, y).catch(() => {});\n        } else if (msg.eventType === 'mouseWheel') {\n          await page.mouse.wheel(Number(msg.deltaX || 0), Number(msg.deltaY || 0)).catch(() => {});\n        }\n        return;\n      }\n\n      if (msg.type === 'key') {\n        const k = String(msg.key || '');\n        if (!k) return;\n        if (k.length === 1) {\n          await page.keyboard.insertText(k).catch(() => {});\n        } else {\n          await page.keyboard.press(k).catch(() => {});\n        }\n        return;\n      }\n\n      if (msg.type === 'type_text') {\n        const text = String(msg.text || '');\n        if (text) {\n          await page.keyboard.insertText(text).catch(() => {});\n        }\n        if (msg.pressEnter) {\n          await page.keyboard.press('Enter').catch(() => {});\n        }\n        return;\n      }\n    } catch {}\n  });\n\n  ws.on('close', () => {\n    wsClients.delete(ws);\n  });\n});\n\nserver.listen(PORT, '127.0.0.1', () => {\n  console.log(`CloudBrowser VPS Server listening on http://127.0.0.1:${PORT}`);\n});\n",
  "public/index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title>CloudBrowser — Live Browser Control & \"Add as a Tool\" System</title>\n  <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n  <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n  <link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n  <style>\n    :root {\n      --bg-base: #090D16;\n      --bg-surface: #111827;\n      --bg-elevated: #1E293B;\n      --text-primary: #F8FAFC;\n      --text-secondary: #CBD5E1;\n      --text-muted: #94A3B8;\n      --accent-indigo: #4F46E5;\n      --accent-emerald: #10B981;\n      --accent-amber: #F59E0B;\n      --accent-rose: #EF4444;\n      --canva-grad: linear-gradient(135deg, #00C4CC 0%, #7D2AE8 100%);\n      --border: rgba(148, 163, 184, 0.18);\n    }\n\n    * { box-sizing: border-box; }\n    html, body {\n      margin: 0;\n      padding: 0;\n      background: var(--bg-base);\n      color: var(--text-primary);\n      font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;\n      font-size: 14px;\n      min-height: 100vh;\n    }\n\n    .mono { font-family: 'JetBrains Mono', monospace; font-size: 12px; }\n\n    /* ==================== AUTH SCREEN ==================== */\n    .auth-wrap {\n      min-height: 100vh;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 20px;\n      background: radial-gradient(circle at 20% 20%, rgba(79, 70, 229, 0.18), transparent 50%),\n                  radial-gradient(circle at 80% 80%, rgba(16, 185, 129, 0.14), transparent 50%);\n    }\n    .auth-card {\n      width: 100%;\n      max-width: 500px;\n      background: rgba(17, 24, 39, 0.95);\n      border: 1px solid var(--border);\n      border-radius: 18px;\n      padding: 26px;\n      box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);\n    }\n    .auth-tabs {\n      display: grid;\n      grid-template-columns: 1fr 1fr;\n      background: #0B0F19;\n      padding: 4px;\n      border-radius: 10px;\n      border: 1px solid var(--border);\n      margin-bottom: 18px;\n    }\n    .auth-tab {\n      padding: 9px 12px;\n      border: none;\n      background: transparent;\n      color: var(--text-muted);\n      font-weight: 700;\n      font-size: 13px;\n      border-radius: 8px;\n      cursor: pointer;\n    }\n    .auth-tab.active {\n      background: var(--accent-indigo);\n      color: #fff;\n    }\n    .form-group { margin-bottom: 12px; }\n    .form-label {\n      display: block;\n      font-size: 12px;\n      font-weight: 600;\n      color: var(--text-secondary);\n      margin-bottom: 5px;\n    }\n    .form-input {\n      width: 100%;\n      padding: 10px 12px;\n      border-radius: 9px;\n      border: 1px solid var(--border);\n      background: #0B0F19;\n      color: var(--text-primary);\n      font-size: 13.5px;\n      font-family: inherit;\n      outline: none;\n    }\n    .form-input:focus { border-color: #6366F1; }\n\n    .btn {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      gap: 6px;\n      padding: 9px 14px;\n      border-radius: 9px;\n      font-weight: 700;\n      font-size: 13px;\n      border: 1px solid transparent;\n      cursor: pointer;\n      transition: all 0.15s;\n      font-family: inherit;\n      text-decoration: none;\n      white-space: nowrap;\n    }\n    .btn-primary { background: var(--accent-indigo); color: #fff; }\n    .btn-primary:hover { background: #4338CA; }\n    .btn-add-tool {\n      background: linear-gradient(135deg, #10B981 0%, #059669 100%);\n      color: #fff;\n      box-shadow: 0 4px 16px rgba(16, 185, 129, 0.35);\n      font-size: 13.5px;\n      padding: 9px 16px;\n    }\n    .btn-add-tool:hover { filter: brightness(1.08); transform: translateY(-1px); }\n    .btn-canva { background: var(--canva-grad); color: #fff; }\n    .btn-outline {\n      background: rgba(30, 41, 59, 0.75);\n      border-color: var(--border);\n      color: var(--text-primary);\n    }\n    .btn-outline:hover { background: rgba(51, 65, 85, 0.9); }\n    .btn-sm { padding: 6px 11px; font-size: 12px; border-radius: 7px; }\n\n    .role-grid {\n      display: grid;\n      grid-template-columns: 1fr 1fr;\n      gap: 7px;\n      margin-top: 10px;\n    }\n    .role-btn {\n      text-align: left;\n      padding: 8px 10px;\n      border-radius: 9px;\n      background: rgba(30, 41, 59, 0.65);\n      border: 1px solid var(--border);\n      color: var(--text-primary);\n      cursor: pointer;\n    }\n    .role-btn:hover { border-color: #6366F1; background: rgba(79, 70, 229, 0.15); }\n    .role-btn strong { display: block; font-size: 12px; }\n    .role-btn span { font-size: 11px; color: var(--text-muted); }\n\n    /* ==================== SIMPLE BROWSER CONTROL WORKSPACE ==================== */\n    .workspace {\n      display: none;\n      flex-direction: column;\n      height: 100vh;\n      overflow: hidden;\n    }\n\n    /* Top Header Bar */\n    .top-header {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      padding: 8px 16px;\n      background: #0D1322;\n      border-bottom: 1px solid var(--border);\n      gap: 10px;\n      flex-wrap: wrap;\n    }\n    .brand-row {\n      display: flex;\n      align-items: center;\n      gap: 10px;\n    }\n    .brand-icon {\n      width: 30px;\n      height: 30px;\n      border-radius: 8px;\n      background: var(--canva-grad);\n      display: grid;\n      place-items: center;\n      font-weight: 800;\n      color: #fff;\n    }\n    .role-pill {\n      display: inline-flex;\n      align-items: center;\n      gap: 5px;\n      padding: 3px 9px;\n      border-radius: 99px;\n      font-size: 11px;\n      font-weight: 700;\n    }\n    .role-pill.hr, .role-pill.admin {\n      background: rgba(16, 185, 129, 0.16);\n      color: #34D399;\n      border: 1px solid rgba(16, 185, 129, 0.35);\n    }\n    .role-pill.editor {\n      background: rgba(245, 158, 11, 0.16);\n      color: #FBBF24;\n      border: 1px solid rgba(245, 158, 11, 0.35);\n    }\n\n    /* Saved Tools Bar (Added via \"Add as a Tool\") */\n    .tools-strip {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      padding: 8px 16px;\n      background: #111827;\n      border-bottom: 1px solid var(--border);\n      overflow-x: auto;\n    }\n    .tools-label {\n      font-size: 11.5px;\n      font-weight: 800;\n      text-transform: uppercase;\n      letter-spacing: 0.05em;\n      color: #38BDF8;\n      white-space: nowrap;\n      margin-right: 4px;\n    }\n    .tool-chip {\n      display: inline-flex;\n      align-items: center;\n      gap: 7px;\n      padding: 5px 12px;\n      border-radius: 99px;\n      background: rgba(30, 41, 59, 0.9);\n      border: 1px solid rgba(148, 163, 184, 0.28);\n      color: #F8FAFC;\n      font-weight: 700;\n      font-size: 12.5px;\n      cursor: pointer;\n      transition: all 0.15s;\n      white-space: nowrap;\n    }\n    .tool-chip:hover {\n      border-color: #38BDF8;\n      background: rgba(79, 70, 229, 0.25);\n      transform: translateY(-1px);\n    }\n    .tool-chip-dot {\n      width: 18px;\n      height: 18px;\n      border-radius: 50%;\n      display: inline-grid;\n      place-items: center;\n      font-size: 10px;\n      font-weight: 800;\n      color: #fff;\n    }\n    .tool-del {\n      margin-left: 4px;\n      color: #94A3B8;\n      font-size: 11px;\n      padding: 0 3px;\n      border-radius: 4px;\n    }\n    .tool-del:hover { color: #F87171; background: rgba(239, 68, 68, 0.2); }\n\n    /* Browser Chrome Toolbar (Back, Forward, Google, URL/Search Bar, ADD AS A TOOL) */\n    .browser-bar {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      padding: 9px 16px;\n      background: #1E293B;\n      border-bottom: 1px solid var(--border);\n      flex-wrap: wrap;\n    }\n    .url-box {\n      flex: 1;\n      min-width: 220px;\n      display: flex;\n      align-items: center;\n      background: #090D16;\n      border: 1px solid rgba(148, 163, 184, 0.35);\n      border-radius: 9px;\n      padding: 4px 10px;\n      gap: 8px;\n    }\n    .url-box:focus-within { border-color: #38BDF8; }\n    .url-input {\n      flex: 1;\n      background: transparent;\n      border: none;\n      color: #F8FAFC;\n      font-family: 'JetBrains Mono', monospace;\n      font-size: 13px;\n      outline: none;\n      padding: 4px 0;\n    }\n\n    /* Quick Keyboard / Helper Sub-bar */\n    .helper-bar {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      gap: 10px;\n      padding: 6px 16px;\n      background: #0F172A;\n      border-bottom: 1px solid var(--border);\n      font-size: 12px;\n      flex-wrap: wrap;\n    }\n\n    /* Live Browser Viewport */\n    .viewport-area {\n      flex: 1;\n      background: #050811;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      overflow: auto;\n      position: relative;\n      padding: 6px;\n    }\n    #liveBrowserCanvas {\n      height: 100%;\n      max-width: 100%;\n      aspect-ratio: 16 / 9;\n      background: #fff;\n      border-radius: 8px;\n      box-shadow: 0 12px 40px rgba(0, 0, 0, 0.65);\n      cursor: default;\n      outline: none;\n    }\n\n    /* Modals */\n    .modal-backdrop {\n      position: fixed;\n      inset: 0;\n      background: rgba(0, 0, 0, 0.75);\n      backdrop-filter: blur(4px);\n      display: none;\n      align-items: center;\n      justify-content: center;\n      z-index: 100;\n      padding: 16px;\n    }\n    .modal-card {\n      background: var(--bg-surface);\n      border: 1px solid var(--border);\n      border-radius: 16px;\n      max-width: 680px;\n      width: 100%;\n      padding: 22px;\n      box-shadow: 0 25px 60px rgba(0, 0, 0, 0.65);\n      max-height: 90vh;\n      overflow-y: auto;\n    }\n\n    table { width: 100%; border-collapse: collapse; margin-top: 10px; }\n    th, td {\n      text-align: left;\n      padding: 9px 10px;\n      border-bottom: 1px solid var(--border);\n      font-size: 12.5px;\n    }\n    th {\n      font-size: 11px;\n      text-transform: uppercase;\n      color: var(--text-muted);\n      background: rgba(15, 23, 42, 0.6);\n    }\n\n    .toast-box {\n      position: fixed;\n      bottom: 20px;\n      right: 20px;\n      background: #1E293B;\n      border: 1px solid #10B981;\n      color: #F8FAFC;\n      padding: 12px 18px;\n      border-radius: 12px;\n      box-shadow: 0 15px 35px rgba(0,0,0,0.5);\n      z-index: 200;\n      display: none;\n      font-weight: 700;\n    }\n  </style>\n</head>\n<body>\n\n  <!-- ==================== 1. LOGIN / SIGN UP SCREEN ==================== -->\n  <div id=\"authScreen\" class=\"auth-wrap\">\n    <div class=\"auth-card\">\n      <div style=\"display:flex;align-items:center;gap:10px;margin-bottom:14px\">\n        <div class=\"brand-icon\">C</div>\n        <div>\n          <div style=\"font-weight:800;font-size:17px\">CloudBrowser VPS</div>\n          <div style=\"font-size:12px;color:var(--text-muted)\">Live Google Browser + \"Add as a Tool\" Sharing</div>\n        </div>\n      </div>\n\n      <div class=\"auth-tabs\">\n        <button class=\"auth-tab active\" id=\"tabBtnLogin\" onclick=\"switchAuthMode('login')\">Log In</button>\n        <button class=\"auth-tab\" id=\"tabBtnSignup\" onclick=\"switchAuthMode('signup')\">Create Account (Sign Up)</button>\n      </div>\n\n      <div id=\"authErrorBox\" style=\"display:none;background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.4);color:#FCA5A5;padding:9px 12px;border-radius:8px;margin-bottom:12px;font-size:12.5px\"></div>\n\n      <!-- Log In Form -->\n      <form id=\"formLogin\" onsubmit=\"return submitLogin(event)\">\n        <div class=\"form-group\">\n          <label class=\"form-label\">Email Address</label>\n          <input type=\"email\" id=\"loginEmail\" class=\"form-input\" value=\"hr@asacompany.com\" required>\n        </div>\n        <div class=\"form-group\">\n          <label class=\"form-label\">Password</label>\n          <input type=\"password\" id=\"loginPass\" class=\"form-input\" value=\"HR@12345\" required>\n        </div>\n        <button type=\"submit\" class=\"btn btn-primary\" style=\"width:100%;padding:11px\">\n          Open Live Browser →\n        </button>\n      </form>\n\n      <!-- Sign Up Form -->\n      <form id=\"formSignup\" style=\"display:none\" onsubmit=\"return submitSignup(event)\">\n        <div class=\"form-group\">\n          <label class=\"form-label\">Your Name</label>\n          <input type=\"text\" id=\"signupName\" class=\"form-input\" placeholder=\"e.g. Ravi Kumar\" required>\n        </div>\n        <div class=\"form-group\">\n          <label class=\"form-label\">Email Address</label>\n          <input type=\"email\" id=\"signupEmail\" class=\"form-input\" placeholder=\"you@company.com\" required>\n        </div>\n        <div class=\"form-group\">\n          <label class=\"form-label\">Create Password</label>\n          <input type=\"password\" id=\"signupPass\" class=\"form-input\" placeholder=\"Min 4 characters\" required>\n        </div>\n        <div class=\"form-group\">\n          <label class=\"form-label\">Account Role</label>\n          <select id=\"signupRole\" class=\"form-input\">\n            <option value=\"hr\">Browser Admin / HR (Search Google + Add as a Tool)</option>\n            <option value=\"editor\">Team Editor (Click & Use Shared Tools Without Password)</option>\n          </select>\n        </div>\n        <button type=\"submit\" class=\"btn btn-add-tool\" style=\"width:100%;padding:11px\">\n          ✨ Create Account & Open Browser →\n        </button>\n      </form>\n\n      <div style=\"margin-top:16px;padding-top:14px;border-top:1px solid var(--border)\">\n        <div style=\"font-size:11px;font-weight:700;text-transform:uppercase;color:var(--text-muted);margin-bottom:6px\">\n          ⚡ 1-Click Instant Login (Demo Accounts)\n        </div>\n        <div class=\"role-grid\">\n          <button class=\"role-btn\" onclick=\"quickLogin('hr@asacompany.com','HR@12345')\">\n            <strong>👩‍💼 HR / Browser Admin</strong>\n            <span>Search Google + \"Add as a Tool\"</span>\n          </button>\n          <button class=\"role-btn\" onclick=\"quickLogin('editor1@asacompany.com','Editor@123')\">\n            <strong>🎨 Team Editor 1 (Rahul)</strong>\n            <span>Click Added Tools (Zero Password)</span>\n          </button>\n          <button class=\"role-btn\" onclick=\"quickLogin('editor2@asacompany.com','Editor@123')\">\n            <strong>🎨 Team Editor 2 (Ananya)</strong>\n            <span>Click Added Tools (Zero Password)</span>\n          </button>\n          <button class=\"role-btn\" onclick=\"quickLogin('editor3@asacompany.com','Editor@123')\">\n            <strong>🎨 Team Editor 3 (Aman)</strong>\n            <span>Click Added Tools (Zero Password)</span>\n          </button>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <!-- ==================== 2. SIMPLE LIVE BROWSER + \"ADD AS A TOOL\" WORKSPACE ==================== -->\n  <div id=\"portalScreen\" class=\"workspace\">\n\n    <!-- Top Header -->\n    <header class=\"top-header\">\n      <div class=\"brand-row\">\n        <div class=\"brand-icon\">C</div>\n        <div>\n          <strong style=\"font-size:14px\">CloudBrowser Control</strong>\n          <span id=\"topUserSub\" style=\"font-size:12px;color:var(--text-muted);margin-left:8px\"></span>\n        </div>\n        <span id=\"topRolePill\" class=\"role-pill hr\"></span>\n      </div>\n\n      <div style=\"display:flex;align-items:center;gap:8px;flex-wrap:wrap\">\n        <select class=\"form-input\" style=\"width:auto;padding:5px 10px;font-size:12px\" onchange=\"switchRoleInstant(this.value)\">\n          <option value=\"\">⚡ Switch User Role...</option>\n          <option value=\"hr@asacompany.com\">👩‍💼 HR (Google + Add as a Tool)</option>\n          <option value=\"editor1@asacompany.com\">🎨 Editor 1 — Rahul</option>\n          <option value=\"editor2@asacompany.com\">🎨 Editor 2 — Ananya</option>\n          <option value=\"editor3@asacompany.com\">🎨 Editor 3 — Aman</option>\n          <option value=\"editor4@asacompany.com\">🎨 Editor 4 — Neha</option>\n        </select>\n        <button class=\"btn btn-outline btn-sm\" onclick=\"openModal('teamModal')\">👥 4 Editors & Access</button>\n        <a href=\"/api/download-source\" class=\"btn btn-outline btn-sm\">⬇️ Code</a>\n        <button class=\"btn btn-outline btn-sm\" onclick=\"logoutPortal()\">Log Out</button>\n      </div>\n    </header>\n\n    <!-- Saved Shared Tools Strip (Tools added via \"Add as a Tool\") -->\n    <div class=\"tools-strip\">\n      <span class=\"tools-label\">🧰 Added Tools:</span>\n      <div id=\"toolsList\" style=\"display:flex;align-items:center;gap:8px\"></div>\n    </div>\n\n    <!-- Simple Browser Control Bar (Just like Chrome + \"➕ Add as a Tool\") -->\n    <div class=\"browser-bar\">\n      <button class=\"btn btn-outline btn-sm\" title=\"Back\" onclick=\"wsSend({type:'go_back'})\">←</button>\n      <button class=\"btn btn-outline btn-sm\" title=\"Forward\" onclick=\"wsSend({type:'go_forward'})\">→</button>\n      <button class=\"btn btn-outline btn-sm\" title=\"Reload\" onclick=\"wsSend({type:'reload'})\">↻</button>\n      <button class=\"btn btn-outline btn-sm\" onclick=\"navigateLive('https://www.google.com/?hl=en')\">🏠 Google</button>\n      <button class=\"btn btn-outline btn-sm\" onclick=\"navigateLive('https://www.canva.com/')\">🎨 Canva.com</button>\n\n      <div class=\"url-box\">\n        <span style=\"color:#38BDF8;font-size:12px\">🌐</span>\n        <input\n          type=\"text\"\n          id=\"browserUrlInput\"\n          class=\"url-input\"\n          placeholder=\"Search Google or type a URL (e.g. canva.com) and press Enter...\"\n          value=\"https://www.google.com/\"\n          onkeydown=\"if(event.key==='Enter') navigateFromBar()\"\n        />\n      </div>\n\n      <button class=\"btn btn-primary btn-sm\" onclick=\"navigateFromBar()\">🔍 Search / Go</button>\n\n      <!-- THE \"ADD AS A TOOL\" BUTTON -->\n      <button class=\"btn btn-add-tool\" onclick=\"openAddAsToolModal()\">\n        ➕ Add as a Tool\n      </button>\n    </div>\n\n    <!-- Quick Typing Helper Bar -->\n    <div class=\"helper-bar\">\n      <div style=\"color:var(--text-secondary)\">\n        💡 <strong>How to use:</strong> Click anywhere on the browser below and type directly on your keyboard — or use the quick-type box on the right. When you open any site (like <strong>canva.com</strong>), click <strong>“➕ Add as a Tool”</strong>!\n      </div>\n      <div style=\"display:flex;align-items:center;gap:6px\">\n        <input\n          type=\"text\"\n          id=\"quickTypeInput\"\n          class=\"form-input\"\n          style=\"width:240px;padding:5px 10px;font-size:12px\"\n          placeholder=\"Type text / email / password into browser...\"\n          onkeydown=\"if(event.key==='Enter') sendQuickText(true)\"\n        />\n        <button class=\"btn btn-primary btn-sm\" onclick=\"sendQuickText(true)\">Type + Enter ↵</button>\n      </div>\n    </div>\n\n    <!-- Real Live Browser Canvas -->\n    <div class=\"viewport-area\">\n      <canvas id=\"liveBrowserCanvas\" width=\"1280\" height=\"720\" tabindex=\"0\"></canvas>\n    </div>\n  </div>\n\n  <!-- ==================== MODAL 1: \"ADD AS A TOOL\" ==================== -->\n  <div class=\"modal-backdrop\" id=\"addToolModal\">\n    <div class=\"modal-card\" style=\"max-width:460px\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px\">\n        <h3 style=\"margin:0;font-size:17px\">➕ Add Current Website as a Tool</h3>\n        <button class=\"btn btn-outline btn-sm\" onclick=\"closeModal('addToolModal')\">✕</button>\n      </div>\n      <p style=\"color:var(--text-secondary);font-size:13px;margin:0 0 14px\">\n        This saves the current browser page and its logged-in session as a <strong>1-Click Tool</strong> in the top bar for your 4 Team Editors (without exposing your email or password).\n      </p>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Tool Name</label>\n        <input type=\"text\" id=\"toolNameInput\" class=\"form-input\" placeholder=\"e.g. Canva Pro\">\n      </div>\n      <div class=\"form-group\">\n        <label class=\"form-label\">Browser URL to Open</label>\n        <input type=\"text\" id=\"toolUrlInput\" class=\"form-input mono\">\n      </div>\n      <div style=\"display:flex;justify-content:flex-end;gap:8px;margin-top:16px\">\n        <button class=\"btn btn-outline\" onclick=\"closeModal('addToolModal')\">Cancel</button>\n        <button class=\"btn btn-add-tool\" onclick=\"confirmAddAsTool()\">✔ Add as a Tool Now</button>\n      </div>\n    </div>\n  </div>\n\n  <!-- ==================== MODAL 2: 4 EDITORS & PERMISSIONS ==================== -->\n  <div class=\"modal-backdrop\" id=\"teamModal\">\n    <div class=\"modal-card\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:10px\">\n        <div>\n          <h3 style=\"margin:0;font-size:17px\">👥 Company HR — 4 Team Editors & Tool Permissions</h3>\n          <p style=\"margin:4px 0 0;color:var(--text-muted);font-size:12.5px\">\n            Control which Editors can open Added Tools (like Canva) or browse Google freely.\n          </p>\n        </div>\n        <button class=\"btn btn-outline btn-sm\" onclick=\"closeModal('teamModal')\">✕</button>\n      </div>\n      <table>\n        <thead>\n          <tr>\n            <th>User</th>\n            <th>Role</th>\n            <th>Google / Free Browsing</th>\n            <th>Added Tools Access</th>\n            <th>Test Account</th>\n          </tr>\n        </thead>\n        <tbody id=\"permissionsBody\"></tbody>\n      </table>\n    </div>\n  </div>\n\n  <!-- ==================== MODAL 3: SECURITY SHIELD ==================== -->\n  <div class=\"modal-backdrop\" id=\"shieldModal\">\n    <div class=\"modal-card\" style=\"max-width:440px;border-color:#EF4444\">\n      <div style=\"font-size:32px;margin-bottom:6px\">🛡️</div>\n      <h3 style=\"margin:0 0 8px;color:#FCA5A5\" id=\"shieldTitle\">Restricted by HR Policy</h3>\n      <p style=\"color:var(--text-secondary);margin:0 0 16px\" id=\"shieldMsg\"></p>\n      <div style=\"display:flex;justify-content:flex-end\">\n        <button class=\"btn btn-outline\" onclick=\"closeModal('shieldModal')\">OK</button>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"toast-box\" id=\"toastBox\"></div>\n\n  <script>\n    let STATE = null;\n    let ws = null;\n    let latestPageTitle = 'Google';\n    const canvas = document.getElementById('liveBrowserCanvas');\n    const ctx = canvas.getContext('2d');\n\n    function showToast(msg) {\n      const t = document.getElementById('toastBox');\n      t.textContent = msg;\n      t.style.display = 'block';\n      clearTimeout(window._tt);\n      window._tt = setTimeout(() => { t.style.display = 'none'; }, 3500);\n    }\n\n    function openModal(id) { document.getElementById(id).style.display = 'flex'; }\n    function closeModal(id) { document.getElementById(id).style.display = 'none'; }\n\n    function switchAuthMode(mode) {\n      document.getElementById('formLogin').style.display = mode === 'login' ? 'block' : 'none';\n      document.getElementById('formSignup').style.display = mode === 'signup' ? 'block' : 'none';\n      document.getElementById('tabBtnLogin').classList.toggle('active', mode === 'login');\n      document.getElementById('tabBtnSignup').classList.toggle('active', mode === 'signup');\n      document.getElementById('authErrorBox').style.display = 'none';\n    }\n\n    async function apiCall(payload) {\n      const res = await fetch('/api/action', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(payload)\n      });\n      return res.json();\n    }\n\n    async function submitLogin(e) {\n      if (e) e.preventDefault();\n      const email = document.getElementById('loginEmail').value;\n      const password = document.getElementById('loginPass').value;\n      const res = await apiCall({ action: 'login', email, password });\n      if (res.ok) {\n        initPortal();\n      } else {\n        const eb = document.getElementById('authErrorBox');\n        eb.textContent = res.error || 'Login failed';\n        eb.style.display = 'block';\n      }\n      return false;\n    }\n\n    async function submitSignup(e) {\n      if (e) e.preventDefault();\n      const name = document.getElementById('signupName').value;\n      const email = document.getElementById('signupEmail').value;\n      const password = document.getElementById('signupPass').value;\n      const role = document.getElementById('signupRole').value;\n      const res = await apiCall({ action: 'signup', name, email, password, role });\n      if (res.ok) {\n        showToast('✨ Account created! Live browser is ready.');\n        initPortal();\n      } else {\n        const eb = document.getElementById('authErrorBox');\n        eb.textContent = res.error || 'Could not create account';\n        eb.style.display = 'block';\n      }\n      return false;\n    }\n\n    function quickLogin(email, pass) {\n      document.getElementById('loginEmail').value = email;\n      document.getElementById('loginPass').value = pass;\n      submitLogin();\n    }\n\n    async function switchRoleInstant(email) {\n      if (!email) return;\n      const res = await apiCall({ action: 'quick_switch', email });\n      if (res.ok) {\n        closeModal('teamModal');\n        showToast('Switched user to ' + res.user.name);\n        initPortal();\n      }\n    }\n\n    async function logoutPortal() {\n      await apiCall({ action: 'logout' });\n      if (ws) { ws.close(); ws = null; }\n      document.getElementById('portalScreen').style.display = 'none';\n      document.getElementById('authScreen').style.display = 'flex';\n    }\n\n    async function initPortal() {\n      const res = await fetch('/api/action?action=state').then(r => r.json()).catch(() => null);\n      if (!res || !res.ok) {\n        document.getElementById('authScreen').style.display = 'flex';\n        document.getElementById('portalScreen').style.display = 'none';\n        return;\n      }\n      STATE = res;\n      document.getElementById('authScreen').style.display = 'none';\n      document.getElementById('portalScreen').style.display = 'flex';\n      renderAll();\n      connectLiveBrowserWs();\n      setTimeout(() => canvas.focus(), 150);\n    }\n\n    function renderAll() {\n      const u = STATE.user;\n      document.getElementById('topUserSub').textContent = `${u.name} (${u.email})`;\n      const pill = document.getElementById('topRolePill');\n      pill.className = 'role-pill ' + u.role;\n      pill.textContent = u.can_browse ? '🌐 Full Browser + Add Tools' : '🔒 Editor Mode (Added Tools Only)';\n\n      if (STATE.browser && STATE.browser.current_url) {\n        document.getElementById('browserUrlInput').value = STATE.browser.current_url;\n        latestPageTitle = STATE.browser.current_title || 'Shared Tool';\n      }\n\n      // Render Saved Tools Strip\n      const toolsEl = document.getElementById('toolsList');\n      toolsEl.innerHTML = STATE.apps.map(a => `\n        <div class=\"tool-chip\" onclick=\"openTool(${a.id})\" title=\"Click to open ${esc(a.name)} (${esc(a.target_url)}) in the shared browser\">\n          <span class=\"tool-chip-dot\" style=\"background:${a.icon_bg}\">${esc(a.icon_letter || 'T')}</span>\n          <span>${esc(a.name)}</span>\n          ${u.can_manage_access && a.id !== 1 ? `<span class=\"tool-del\" onclick=\"event.stopPropagation(); deleteTool(${a.id})\" title=\"Remove tool\">✕</span>` : ''}\n        </div>\n      `).join('');\n\n      // Render Permissions Table in Modal\n      const firstTool = STATE.apps[0] || { id: 1, assigned_user_ids: [] };\n      document.getElementById('permissionsBody').innerHTML = STATE.team.map(m => {\n        const hasTool = firstTool.assigned_user_ids.includes(m.id) || m.role !== 'editor';\n        const isMgr = m.role === 'admin' || m.role === 'hr';\n        return `\n          <tr>\n            <td>\n              <strong>${esc(m.name)}</strong>\n              <div class=\"mono\" style=\"color:var(--text-muted)\">${esc(m.email)}</div>\n            </td>\n            <td><span class=\"role-pill ${m.role}\">${m.role.toUpperCase()}</span></td>\n            <td>\n              <input type=\"checkbox\" ${m.can_browse ? 'checked' : ''} ${(!u.can_manage_access || isMgr) ? 'disabled' : ''} onchange=\"toggleBrowse(${m.id}, this.checked)\">\n              <span style=\"margin-left:6px\">${m.can_browse ? 'Allowed' : 'Tools Only'}</span>\n            </td>\n            <td>\n              <input type=\"checkbox\" ${hasTool ? 'checked' : ''} ${(!u.can_manage_access || isMgr) ? 'disabled' : ''} onchange=\"togglePerm(${firstTool.id}, ${m.id}, this.checked)\">\n              <span style=\"margin-left:6px\">${hasTool ? '✔ Shared' : 'Blocked'}</span>\n            </td>\n            <td>\n              <button class=\"btn btn-outline btn-sm\" onclick=\"switchRoleInstant('${esc(m.email)}')\">Test as User →</button>\n            </td>\n          </tr>\n        `;\n      }).join('');\n    }\n\n    // ==================== \"ADD AS A TOOL\" FLOW ====================\n    function openAddAsToolModal() {\n      const curUrl = document.getElementById('browserUrlInput').value.trim() || 'https://www.canva.com/';\n      let defaultName = latestPageTitle || '';\n      try {\n        const host = new URL(curUrl).hostname.replace(/^www\\./, '');\n        if (!defaultName || defaultName === 'Google') {\n          defaultName = host.charAt(0).toUpperCase() + host.slice(1);\n        }\n      } catch {}\n      document.getElementById('toolNameInput').value = defaultName;\n      document.getElementById('toolUrlInput').value = curUrl;\n      openModal('addToolModal');\n    }\n\n    async function confirmAddAsTool() {\n      const name = document.getElementById('toolNameInput').value.trim();\n      const target_url = document.getElementById('toolUrlInput').value.trim();\n      const res = await apiCall({ action: 'add_as_tool', name, target_url });\n      if (res.ok) {\n        closeModal('addToolModal');\n        showToast(`✔ Added \"${res.app.name}\" as a Tool! Shared with all 4 Team Editors.`);\n        initPortal();\n      }\n    }\n\n    async function openTool(appId) {\n      const res = await apiCall({ action: 'launch_app', app_id: appId });\n      if (!res.ok) {\n        document.getElementById('shieldTitle').textContent = '🔒 Tool Access Not Granted';\n        document.getElementById('shieldMsg').textContent = res.error;\n        openModal('shieldModal');\n        return;\n      }\n      document.getElementById('browserUrlInput').value = res.app.target_url;\n      showToast(`🚀 Opening \"${res.app.name}\" in Shared Browser...`);\n      canvas.focus();\n    }\n\n    async function deleteTool(appId) {\n      await apiCall({ action: 'delete_tool', app_id: appId });\n      showToast('Removed tool from bar');\n      initPortal();\n    }\n\n    async function togglePerm(appId, userId, grant) {\n      await apiCall({ action: 'toggle_permission', app_id: appId, user_id: userId, grant });\n      showToast(grant ? '✔ Granted Tool Access' : '🔒 Revoked Tool Access');\n      initPortal();\n    }\n\n    async function toggleBrowse(userId, allow) {\n      await apiCall({ action: 'toggle_browsing', user_id: userId, can_browse: allow });\n      showToast(allow ? '🌐 Enabled Free Google Browsing' : '🔒 Restricted to Added Tools Only');\n      initPortal();\n    }\n\n    // ==================== LIVE CHROMIUM WEBSOCKET & INPUT ====================\n    function connectLiveBrowserWs() {\n      if (ws && (ws.readyState === 0 || ws.readyState === 1)) return;\n      const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';\n      ws = new WebSocket(proto + '//' + location.host + '/ws');\n      ws.onmessage = ev => {\n        try {\n          const msg = JSON.parse(ev.data);\n          if (msg.type === 'frame' && msg.data) {\n            const img = new Image();\n            img.onload = () => { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); };\n            img.src = 'data:image/jpeg;base64,' + msg.data;\n            if (msg.title) latestPageTitle = msg.title;\n          } else if (msg.type === 'url_changed') {\n            document.getElementById('browserUrlInput').value = msg.url;\n            if (msg.title) latestPageTitle = msg.title;\n          } else if (msg.type === 'tools_updated') {\n            initPortal();\n          } else if (msg.type === 'policy_block') {\n            document.getElementById('shieldTitle').textContent = msg.title;\n            document.getElementById('shieldMsg').textContent = msg.message;\n            openModal('shieldModal');\n          }\n        } catch {}\n      };\n      ws.onclose = () => {\n        ws = null;\n        setTimeout(() => {\n          if (document.getElementById('portalScreen').style.display !== 'none') {\n            connectLiveBrowserWs();\n          }\n        }, 1500);\n      };\n    }\n\n    function wsSend(obj) {\n      if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj));\n    }\n\n    function navigateFromBar() {\n      const val = document.getElementById('browserUrlInput').value.trim();\n      if (!val) return;\n      wsSend({ type: 'navigate', url: val });\n      canvas.focus();\n    }\n\n    function navigateLive(url) {\n      document.getElementById('browserUrlInput').value = url;\n      wsSend({ type: 'navigate', url });\n      canvas.focus();\n    }\n\n    function sendQuickText(pressEnter) {\n      const inp = document.getElementById('quickTypeInput');\n      if (!inp.value && !pressEnter) return;\n      wsSend({ type: 'type_text', text: inp.value, pressEnter: Boolean(pressEnter) });\n      inp.value = '';\n      canvas.focus();\n    }\n\n    function getCanvasCoords(e) {\n      const rect = canvas.getBoundingClientRect();\n      return {\n        x: (e.clientX - rect.left) * (canvas.width / rect.width),\n        y: (e.clientY - rect.top) * (canvas.height / rect.height)\n      };\n    }\n\n    canvas.addEventListener('mousedown', e => {\n      canvas.focus();\n      const { x, y } = getCanvasCoords(e);\n      wsSend({ type: 'mouse', eventType: 'mousePressed', x, y, button: e.button === 2 ? 'right' : 'left', clickCount: 1 });\n    });\n    canvas.addEventListener('mouseup', e => {\n      const { x, y } = getCanvasCoords(e);\n      wsSend({ type: 'mouse', eventType: 'mouseReleased', x, y, button: e.button === 2 ? 'right' : 'left', clickCount: 1 });\n    });\n    canvas.addEventListener('mousemove', e => {\n      const { x, y } = getCanvasCoords(e);\n      wsSend({ type: 'mouse', eventType: 'mouseMoved', x, y });\n    });\n    canvas.addEventListener('wheel', e => {\n      e.preventDefault();\n      const { x, y } = getCanvasCoords(e);\n      wsSend({ type: 'mouse', eventType: 'mouseWheel', x, y, deltaX: e.deltaX, deltaY: e.deltaY });\n    }, { passive: false });\n\n    canvas.addEventListener('keydown', e => {\n      // Allow Ctrl+V / Cmd+V paste to fire the paste event\n      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') return;\n      e.preventDefault();\n      wsSend({ type: 'key', key: e.key });\n    });\n\n    window.addEventListener('paste', e => {\n      if (document.activeElement === canvas) {\n        const text = (e.clipboardData || window.clipboardData).getData('text');\n        if (text) {\n          e.preventDefault();\n          wsSend({ type: 'type_text', text, pressEnter: false });\n        }\n      }\n    });\n\n    function esc(s) {\n      return String(s ?? '').replace(/[&<>\"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;' }[c]));\n    }\n\n    initPortal();\n  </script>\n</body>\n</html>\n"
}