diff --git a/agents/social-listening/dashboard/server.ts b/agents/social-listening/dashboard/server.ts
index 9ec9e9c..dbe9407 100644
--- a/agents/social-listening/dashboard/server.ts
+++ b/agents/social-listening/dashboard/server.ts
@@ -168,7 +168,54 @@ const server = createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
- // ─── Login routes ───
+ // ─── Auth API (JSON-based, for static frontend) ───
+
+ if (url.pathname === '/api/auth' && req.method === 'GET') {
+ if (isAuthenticated(req)) {
+ sendJSON(res, 200, { ok: true });
+ } else {
+ sendJSON(res, 401, { ok: false, error: 'Not authenticated' });
+ }
+ return;
+ }
+
+ if (url.pathname === '/api/login' && req.method === 'POST') {
+ const body = await parseBody(req);
+ let username = '', password = '';
+ try {
+ const json = JSON.parse(body);
+ username = json.username || '';
+ password = json.password || '';
+ } catch {
+ const params = new URLSearchParams(body);
+ username = params.get('username') || '';
+ password = params.get('password') || '';
+ }
+
+ if (username === DASH_USER && password === DASH_PASS) {
+ const payload = JSON.stringify({ user: username, exp: Date.now() + SESSION_MAX_AGE * 1000 });
+ const token = signSession(payload);
+ res.writeHead(200, {
+ 'Content-Type': 'application/json',
+ 'Set-Cookie': `sl_session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE}`,
+ });
+ res.end(JSON.stringify({ ok: true }));
+ } else {
+ sendJSON(res, 401, { ok: false, error: 'Invalid username or password' });
+ }
+ return;
+ }
+
+ if (url.pathname === '/api/logout' && req.method === 'GET') {
+ res.writeHead(200, {
+ 'Content-Type': 'application/json',
+ 'Set-Cookie': 'sl_session=; Path=/; HttpOnly; Max-Age=0',
+ });
+ res.end(JSON.stringify({ ok: true }));
+ return;
+ }
+
+ // ─── Legacy form login (backward compat for standalone Docker mode) ───
if (url.pathname === '/login' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/html' });
@@ -186,7 +233,7 @@ const server = createServer(async (req, res) => {
const payload = JSON.stringify({ user: username, exp: Date.now() + SESSION_MAX_AGE * 1000 });
const token = signSession(payload);
res.writeHead(302, {
- 'Set-Cookie': `sl_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${SESSION_MAX_AGE}`,
+ 'Set-Cookie': `sl_session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE}`,
'Location': '/',
});
res.end();
@@ -208,8 +255,12 @@ const server = createServer(async (req, res) => {
// ─── Auth gate (everything below requires login) ───
if (!isAuthenticated(req)) {
- res.writeHead(302, { 'Location': '/login' });
- res.end();
+ if (req.headers.accept?.includes('application/json') || url.pathname.startsWith('/api/')) {
+ sendJSON(res, 401, { error: 'Not authenticated' });
+ } else {
+ res.writeHead(302, { 'Location': '/login' });
+ res.end();
+ }
return;
}
diff --git a/deploy/apache-social-reports.conf b/deploy/apache-social-reports.conf
new file mode 100644
index 0000000..daf2fc6
--- /dev/null
+++ b/deploy/apache-social-reports.conf
@@ -0,0 +1,59 @@
+# Social Reporting — Apache config
+# Add this inside your existing VirtualHost for optical-dev.oliver.solutions
+# or include it via: Include /opt/social-reporting/deploy/apache-social-reports.conf
+
+# Enable required modules (run once):
+# sudo a2enmod proxy proxy_http proxy_wstunnel headers rewrite
+
+# ─── Static frontend ───
+Alias /social-reports /var/www/html/social-reporting
+
+ Options -Indexes
+ AllowOverride None
+ Require all granted
+
+ # SPA fallback — serve index.html for unknown paths
+ RewriteEngine On
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^ /social-reports/index.html [L]
+
+
+# ─── Proxy API + SSE + dynamic routes to Node backend ───
+ProxyPreserveHost On
+ProxyTimeout 600
+
+# Auth API
+ProxyPass /social-reports/api/ http://127.0.0.1:3456/api/
+ProxyPassReverse /social-reports/api/ http://127.0.0.1:3456/api/
+
+# SSE (long-lived connection — needs no buffering)
+ProxyPass /social-reports/events http://127.0.0.1:3456/events
+ProxyPassReverse /social-reports/events http://127.0.0.1:3456/events
+
+ # Disable buffering for SSE
+ SetEnv proxy-sendcl 1
+ SetEnv proxy-nokeepalive 1
+ Header set Cache-Control "no-cache"
+ Header set X-Accel-Buffering "no"
+
+
+# Pipeline run trigger
+ProxyPass /social-reports/run http://127.0.0.1:3456/run
+ProxyPassReverse /social-reports/run http://127.0.0.1:3456/run
+
+# Status check
+ProxyPass /social-reports/status http://127.0.0.1:3456/status
+ProxyPassReverse /social-reports/status http://127.0.0.1:3456/status
+
+# Legacy form login (standalone mode fallback)
+ProxyPass /social-reports/login http://127.0.0.1:3456/login
+ProxyPassReverse /social-reports/login http://127.0.0.1:3456/login
+
+# Legacy logout
+ProxyPass /social-reports/logout http://127.0.0.1:3456/logout
+ProxyPassReverse /social-reports/logout http://127.0.0.1:3456/logout
+
+# Report viewer
+ProxyPassMatch ^/social-reports/report/(.*)$ http://127.0.0.1:3456/report/$1
+ProxyPassReverse /social-reports/report/ http://127.0.0.1:3456/report/
diff --git a/deploy/setup.sh b/deploy/setup.sh
new file mode 100755
index 0000000..51ecdbd
--- /dev/null
+++ b/deploy/setup.sh
@@ -0,0 +1,144 @@
+#!/bin/bash
+set -euo pipefail
+
+# ═══════════════════════════════════════════════════════
+# Social Reporting — Server Deployment Script
+# Target: Ubuntu + Apache + Docker
+# URL: https://optical-dev.oliver.solutions/social-reports
+# ═══════════════════════════════════════════════════════
+
+REPO_URL="${REPO_URL:-}" # Set before running: export REPO_URL="https://x-token-auth:TOKEN@bitbucket.org/zlalani/social-reporting-tool.git"
+BACKEND_DIR="/opt/social-reporting"
+FRONTEND_DIR="/var/www/html/social-reporting"
+APACHE_CONF="/etc/apache2/conf-available/social-reports.conf"
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+log() { echo -e "${GREEN}[+]${NC} $1"; }
+warn() { echo -e "${YELLOW}[!]${NC} $1"; }
+err() { echo -e "${RED}[x]${NC} $1"; exit 1; }
+
+# ─── Pre-checks ───
+[[ -z "$REPO_URL" ]] && err "REPO_URL not set. Run: export REPO_URL='https://x-token-auth:YOUR_TOKEN@bitbucket.org/zlalani/social-reporting-tool.git'"
+command -v docker >/dev/null || err "Docker not installed"
+command -v docker compose >/dev/null 2>&1 || command -v docker-compose >/dev/null || err "Docker Compose not installed"
+command -v apache2ctl >/dev/null || err "Apache not installed"
+
+# ─── 1. Clone or pull repo ───
+if [[ -d "$BACKEND_DIR/.git" ]]; then
+ log "Updating existing repo at $BACKEND_DIR..."
+ cd "$BACKEND_DIR"
+ git remote set-url origin "$REPO_URL"
+ git pull origin main
+else
+ log "Cloning repo to $BACKEND_DIR..."
+ sudo mkdir -p "$BACKEND_DIR"
+ sudo chown "$(whoami):$(whoami)" "$BACKEND_DIR"
+ git clone "$REPO_URL" "$BACKEND_DIR"
+fi
+
+cd "$BACKEND_DIR"
+
+# ─── 2. Create .env if missing ───
+if [[ ! -f "$BACKEND_DIR/.env" ]]; then
+ warn ".env file not found — creating template"
+ cat > "$BACKEND_DIR/.env" << 'ENVEOF'
+APIFY_TOKEN=your_apify_token_here
+ANTHROPIC_API_KEY=your_anthropic_key_here
+APIFY_LIVE_APPROVED=true
+TEST_MODE=false
+DASHBOARD_PORT=3456
+DATABASE_URL=postgresql://sl_user:sl_pass@db:5432/social_listening
+APIFY_COST_LIMIT=5
+DASH_USER=admin
+DASH_PASS=changeme
+SESSION_SECRET=
+ENVEOF
+ # Generate a random session secret
+ SESSION_SECRET=$(openssl rand -hex 32)
+ sed -i "s/^SESSION_SECRET=$/SESSION_SECRET=${SESSION_SECRET}/" "$BACKEND_DIR/.env"
+ warn "Edit $BACKEND_DIR/.env with your API keys and credentials!"
+ warn " APIFY_TOKEN, ANTHROPIC_API_KEY, DASH_USER, DASH_PASS"
+fi
+
+# ─── 3. Deploy frontend ───
+log "Deploying frontend to $FRONTEND_DIR..."
+sudo mkdir -p "$FRONTEND_DIR"
+sudo cp "$BACKEND_DIR/frontend/index.html" "$FRONTEND_DIR/"
+sudo cp "$BACKEND_DIR/frontend/login.html" "$FRONTEND_DIR/"
+sudo cp "$BACKEND_DIR/frontend/config.js" "$FRONTEND_DIR/"
+sudo chown -R www-data:www-data "$FRONTEND_DIR"
+log "Frontend deployed: index.html, login.html, config.js"
+
+# ─── 4. Apache config ───
+log "Setting up Apache config..."
+sudo cp "$BACKEND_DIR/deploy/apache-social-reports.conf" "$APACHE_CONF"
+
+# Enable required modules
+for mod in proxy proxy_http headers rewrite; do
+ if ! apache2ctl -M 2>/dev/null | grep -q "${mod}_module"; then
+ log "Enabling Apache module: $mod"
+ sudo a2enmod "$mod"
+ fi
+done
+
+# Enable the config
+sudo a2enconf social-reports 2>/dev/null || true
+
+# Test Apache config
+log "Testing Apache config..."
+if sudo apache2ctl configtest 2>&1; then
+ log "Apache config OK"
+else
+ err "Apache config test failed — check $APACHE_CONF"
+fi
+
+# ─── 5. Docker Compose ───
+log "Starting Docker containers..."
+cd "$BACKEND_DIR"
+
+# Use the correct docker compose command
+if command -v "docker compose" >/dev/null 2>&1; then
+ COMPOSE="docker compose"
+else
+ COMPOSE="docker-compose"
+fi
+
+$COMPOSE -f docker-compose.yml -f docker-compose.prod.yml build
+$COMPOSE -f docker-compose.yml -f docker-compose.prod.yml up -d
+
+# Wait for health
+log "Waiting for services to be healthy..."
+sleep 5
+if curl -sf http://127.0.0.1:3456/status > /dev/null 2>&1; then
+ log "Backend is running on port 3456"
+else
+ warn "Backend not responding yet — check: $COMPOSE logs social-listening"
+fi
+
+# ─── 6. Reload Apache ───
+log "Reloading Apache..."
+sudo systemctl reload apache2
+
+# ─── Done ───
+echo ""
+echo "════════════════════════════════════════════════════"
+echo -e "${GREEN} Deployment complete!${NC}"
+echo ""
+echo " Frontend: https://optical-dev.oliver.solutions/social-reports/"
+echo " Backend: http://127.0.0.1:3456 (Docker)"
+echo " Login: https://optical-dev.oliver.solutions/social-reports/login.html"
+echo ""
+echo " Backend dir: $BACKEND_DIR"
+echo " Frontend dir: $FRONTEND_DIR"
+echo " Apache conf: $APACHE_CONF"
+echo ""
+echo " To update later:"
+echo " cd $BACKEND_DIR && git pull"
+echo " $COMPOSE -f docker-compose.yml -f docker-compose.prod.yml build && $COMPOSE -f docker-compose.yml -f docker-compose.prod.yml up -d"
+echo " sudo cp frontend/* $FRONTEND_DIR/ && sudo systemctl reload apache2"
+echo ""
+echo "════════════════════════════════════════════════════"
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..5f499a3
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,13 @@
+# Production overrides — use with: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
+services:
+ db:
+ ports:
+ - "127.0.0.1:5435:5432"
+ restart: unless-stopped
+
+ social-listening:
+ ports:
+ - "127.0.0.1:3456:3456"
+ restart: unless-stopped
+ environment:
+ - SESSION_SECRET=${SESSION_SECRET:-}
diff --git a/frontend/config.js b/frontend/config.js
new file mode 100644
index 0000000..2df75fe
--- /dev/null
+++ b/frontend/config.js
@@ -0,0 +1,4 @@
+// ─── Frontend config (injected before app scripts) ───
+// API base points to the proxied backend path
+window.__API_BASE = '/social-reports';
+window.__SSE_BASE = '/social-reports';
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..9062427
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,456 @@
+
+
+
+
+
+Social Listening Pipeline
+
+
+
+
+
+
+
+
Social Listening Pipeline
+
Automated social media research → client-ready reports
+
+
Sign Out
+
+
+
+
Pipeline
+
Run History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/login.html b/frontend/login.html
new file mode 100644
index 0000000..1bb86a1
--- /dev/null
+++ b/frontend/login.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+Login — Social Listening
+
+
+
+
+
+
Social Listening
+
Sign in to access the dashboard
+
+
+
+
+
+
+