# NotebookLM Per-User Google Account Linking - Complete Fix

## Problem
The per-user NotebookLM linking feature expects a browser to open locally (headed mode), but:
1. Server runs without a display server (`$DISPLAY` not set)
2. Users access the app from their own computers, not the server
3. The browser window would open on the server, not visible to users

## Solution Options

### Option A: Use noVNC (VNC in Browser) - RECOMMENDED
Provide a browser-based VNC client so users can see and interact with the login window.

**Pros:**
- Users can see the browser window in their web browser
- No software installation required for users
- Works with existing headed mode code

**Cons:**
- Requires installing noVNC and websockify
- More complex setup

### Option B: Google OAuth Flow Integration
Integrate with Google OAuth API for programmatic authentication.

**Pros:**
- No browser automation needed
- More reliable and secure
- Better UX

**Cons:**
- Requires Google Cloud project setup
- Needs user consent for NotebookLM API access
- NotebookLM API might not support this directly

### Option C: Ask Users to Provide Session Cookies
Users manually export their cookies from their browser.

**Pros:**
- Simple to implement
- No browser automation

**Cons:**
- Poor UX - users have to manually copy cookies
- Security concerns
- Cookies expire

---

## Recommended Implementation: Option A (noVNC)

### Step 1: Install Required Packages

```bash
# Install Xvfb (already done)
sudo yum install -y xorg-x11-server-Xvfb

# Install X11vnc for VNC server
sudo yum install -y tigervnc-server

# Install noVNC and websockify
cd /opt
git clone https://github.com/novnc/noVNC.git
cd noVNC
git clone https://github.com/novnc/websockify vnc_lite
```

### Step 2: Create VNC Wrapper Script

Create `/home/ashraffarid2010/jadwaai.com/app/ai/nlm_login_manager.py`:

```python
"""
NotebookLM Login Manager with VNC support.
Provides a web-accessible browser window for users to login.
"""
import subprocess
import os
import time
import socket
import fcntl
import struct
import json
import tempfile
from datetime import datetime, timedelta


class NLMLoginManager:
    """Manages browser sessions with VNC access for NotebookLM login."""

    def __init__(self):
        self.base_dir = "/tmp/nlm_sessions"
        os.makedirs(self.base_dir, exist_ok=True)
        self.vnc_port_range = (5900, 6000)
        self.novnc_port_range = (6080, 6100)

    def get_free_port(self, start, end):
        """Find a free port in the given range."""
        for port in range(start, end):
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            try:
                sock.bind(("0.0.0.0", port))
                sock.close()
                return port
            except OSError:
                continue
        raise Exception("No free ports available")

    def create_session(self, user_id):
        """Create a new login session with VNC access."""
        session_id = f"{user_id}_{int(datetime.now().timestamp())}"
        session_dir = os.path.join(self.base_dir, session_id)
        os.makedirs(session_dir, exist_ok=True)

        # Get free ports
        vnc_port = self.get_free_port(*self.vnc_port_range)
        display_num = vnc_port - 5900
        novnc_port = self.get_free_port(*self.novnc_port_range)

        # Storage path for Playwright
        storage_path = os.path.join(session_dir, "storage.json")

        # Start Xvfb
        xvfb_cmd = [
            "Xvfb", f":{display_num}",
            "-screen", "0", "1280x720x24",
            "-ac",  # Disable access control
        ]
        xvfb_proc = subprocess.Popen(
            xvfb_cmd,
            stdout=open(os.path.join(session_dir, "xvfb.log"), "w"),
            stderr=subprocess.STDOUT,
        )

        # Start X11vnc
        x11vnc_cmd = [
            "x11vnc",
            "-display", f":{display_num}",
            "-rfbport", str(vnc_port),
            "-forever",
            "-shared",
            "-nopw",
        ]
        x11vnc_proc = subprocess.Popen(
            x11vnc_cmd,
            stdout=open(os.path.join(session_dir, "vnc.log"), "w"),
            stderr=subprocess.STDOUT,
        )

        # Start websockify for noVNC
        websockify_cmd = [
            "python", "/opt/noVNC/vnc_lite/websockify",
            f"--web=/opt/noVNC",
            f"{novnc_port}",
            f"localhost:{vnc_port}"
        ]
        websockify_proc = subprocess.Popen(
            websockify_cmd,
            stdout=open(os.path.join(session_dir, "websockify.log"), "w"),
            stderr=subprocess.STDOUT,
        )

        # Save session info
        session_info = {
            "session_id": session_id,
            "user_id": user_id,
            "display": display_num,
            "vnc_port": vnc_port,
            "novnc_port": novnc_port,
            "novnc_url": f"https://jadwaai.com/novnc/{novnc_port}/vnc.html",
            "storage_path": storage_path,
            "created_at": datetime.now().isoformat(),
            "xvfb_pid": xvfb_proc.pid,
            "x11vnc_pid": x11vnc_proc.pid,
            "websockify_pid": websockify_proc.pid,
        }

        with open(os.path.join(session_dir, "session.json"), "w") as f:
            json.dump(session_info, f)

        # Wait a bit for services to start
        time.sleep(2)

        return session_info

    def start_browser(self, session_info):
        """Start Playwright browser in the session."""
        from playwright.sync_api import sync_playwright

        display = session_info["display"]
        storage_path = session_info["storage_path"]

        os.environ["DISPLAY"] = f":{display}"

        # Start Playwright worker
        worker_script = os.path.join(os.path.dirname(__file__), "_nlm_login_worker.py")

        proc = subprocess.Popen(
            ["python", worker_script, storage_path],
            env={**os.environ, "DISPLAY": f":{display}"},
        )

        return proc

    def cleanup_session(self, session_id):
        """Clean up a session and its processes."""
        session_file = os.path.join(self.base_dir, session_id, "session.json")
        if os.path.exists(session_file):
            with open(session_file) as f:
                info = json.load(f)

            # Kill processes
            for pid_key in ["xvfb_pid", "x11vnc_pid", "websockify_pid"]:
                pid = info.get(pid_key)
                if pid:
                    try:
                        os.kill(pid, 9)  # SIGKILL
                    except ProcessLookupError:
                        pass

            # Remove session directory
            import shutil
            shutil.rmtree(os.path.join(self.base_dir, session_id))


# Flask routes for noVNC access
"""
Add to app/routes/study.py:

@study_bp.route("/nlm/start-session", methods=["POST"])
@login_required
def start_nlm_session():
    '''Start a new NotebookLM login session with VNC access.'''
    from app.ai.nlm_login_manager import NLMLoginManager

    manager = NLMLoginManager()
    session_info = manager.create_session(current_user.id)

    return jsonify({
        "session_id": session_info["session_id"],
        "novnc_url": session_info["novnc_url"],
    })

@study_bp.route("/nlm/connect-browser/<session_id>", methods=["POST"])
@login_required
def connect_nlm_browser(session_id):
    '''Start the browser in an existing session.'''
    from app.ai.nlm_login_manager import NLMLoginManager

    manager = NLMLoginManager()
    session_file = os.path.join(manager.base_dir, session_id, "session.json")

    if not os.path.exists(session_file):
        return jsonify({"error": "Session not found"}), 404

    with open(session_file) as f:
        session_info = json.load(f)

    if session_info["user_id"] != current_user.id:
        return jsonify({"error": "Unauthorized"}), 403

    # Start browser
    proc = manager.start_browser(session_info)

    return jsonify({"status": "browser_started"})

@study_bp.route("/nlm/check-status/<session_id>", methods=["GET"])
@login_required
def check_nlm_status(session_id):
    '''Check if login is complete.'''
    from app.ai.nlm_login_manager import NLMLoginManager

    manager = NLMLoginManager()
    storage_path = os.path.join(manager.base_dir, session_id, "storage.json")

    if os.path.exists(storage_path):
        # Login complete - save to user profile
        current_user.google_nlm_session_path = storage_path
        current_user.google_nlm_connected_at = datetime.utcnow()
        db.session.commit()

        # Cleanup session
        manager.cleanup_session(session_id)

        return jsonify({"status": "connected"})

    return jsonify({"status": "pending"})
"""
```

### Step 3: Configure nginx for noVNC

Add to nginx config:

```nginx
location /novnc/ {
    proxy_pass http://localhost:6080/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;
}
```

### Step 4: Update Frontend JavaScript

```javascript
async function connectGoogle() {
    const btn = document.getElementById('connectBtn');
    const btnText = btn.querySelector('span');
    const btnLoad = btn.querySelector('svg.animate-spin');
    const errEl = document.getElementById('connectError');

    btn.disabled = true;
    btnText.classList.add('hidden');
    btnLoad.classList.remove('hidden');
    errEl.classList.add('hidden');

    try {
        // Start session
        const res = await fetch('/study/nlm/start-session', {
            method: 'POST',
            headers: {'Content-Type': 'application/json', 'X-CSRFToken': CSRF},
        });
        const data = await res.json();

        if (data.novnc_url) {
            // Show instructions with noVNC URL
            showVNCModal(data.novnc_url, data.session_id);
        }
    } catch (e) {
        errEl.textContent = 'خطأ: ' + e.message;
        errEl.classList.remove('hidden');
        btn.disabled = false;
    }
}

function showVNCModal(novncUrl, sessionId) {
    // Create modal with iframe to noVNC
    const modal = document.createElement('div');
    modal.className = 'fixed inset-0 bg-black/80 flex items-center justify-center z-50';
    modal.innerHTML = `
        <div class="glass rounded-2xl p-6 max-w-4xl w-full mx-4">
            <h3 class="text-xl font-bold text-white mb-4">ربط حساب Google</h3>
            <p class="text-slate-400 text-sm mb-4">
                افتح النافذة أدناه وسجّل الدخول بحساب Google الخاص بك
            </p>
            <div class="bg-black rounded-xl overflow-hidden" style="height: 500px;">
                <iframe src="${novncUrl}" class="w-full h-full" frameborder="0"></iframe>
            </div>
            <div class="flex justify-between items-center mt-4">
                <button onclick="startBrowserAndCheck('${sessionId}')" class="px-4 py-2 rounded-xl bg-gold-500 text-black font-bold">
                    بدء المتصفح
                </button>
                <button onclick="location.reload()" class="px-4 py-2 rounded-xl bg-slate-500 text-white">
                    إلغاء
                </button>
            </div>
        </div>
    `;
    document.body.appendChild(modal);
}
```

---

## Quick Alternative: Ask Users to Use Admin Shared Mode

If implementing noVNC is too complex, the simplest solution is:

1. Keep the "Admin Shared" mode as default
2. Admin connects their Google account once
3. All users use the admin's account
4. Add a note in the UI explaining this

Update the admin panel to show this message clearly.
