fb_scraper / index.html
sonuprasad23's picture
Project Uploaded
a00882d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Hillside Medical Group - Social Media Monitor</title>
<style>
:root{
--bg:#f6f7fb; --ink:#0f172a; --muted:#475569;
--card:#ffffff; --border:#e5e7eb; --shadow:0 6px 24px rgba(15,23,42,.06);
--grad1:#2c5aa0; --grad2:#1e3c72; /* Blue theme */
--ok:#22c55e; --warn:#f59e0b; --err:#ef4444;
--term-bg:#0b1221; --term-ink:#22c55e; --term-hdr:#07101d; --term-border:#1f2937; --term-meta:#86efac;
--badge:#e2e8f0; --badge-ink:#0f172a;
--kw:#f59e0b; --kw-ink:#1f2937;
--pill:#dcfce7; --pill-ink:#166534;
}
*{box-sizing:border-box}
body{margin:0;font-family:Inter,system-ui,Arial,sans-serif;background:var(--bg);color:var(--ink)}
.wrap{max-width:1400px;margin:24px auto;padding:0 16px}
.header{background:linear-gradient(135deg,var(--grad1),var(--grad2));color:#fff;border-radius:14px;padding:18px;box-shadow:0 8px 28px rgba(15,23,42,.18); text-align: center;} /* Centered header text */
h1{margin:0 0 6px 0;font-size:22px;font-weight:600}
.grid{display:grid;gap:16px;grid-template-columns:1fr 420px}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;box-shadow:var(--shadow);padding:18px}
.btn{background:var(--ok);color:#fff;border:none;border-radius:10px;padding:10px 14px;font-weight:600;cursor:pointer}
.btn.red{background:var(--err)} .btn.gray{background:#64748b}
.btn[disabled]{opacity:.5;cursor:not-allowed}
input, select{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:10px} /* Added select style */
label{font-weight:600; display: block; margin-bottom: 6px;} /* Improved label spacing */
.muted{color:var(--muted);font-size:13px}
.bar{height:12px;background:var(--border);border-radius:999px;overflow:hidden}
.fill{height:100%;background:linear-gradient(90deg,var(--grad1),#667eea);width:0%}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:#334155;word-break:break-all}
/* Terminal */
.terminal{background:var(--term-bg);border-radius:14px;border:1px solid var(--term-border);height:520px;display:flex;flex-direction:column}
.term-header{color:#a7f3d0;background:var(--term-hdr);border-radius:14px 14px 0 0;padding:10px 12px;font-weight:700;font-size:13px;border-bottom:1px solid var(--term-border)}
.term-body{flex:1;overflow:auto;padding:10px 12px}
.term-line{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:var(--term-ink);white-space:pre-wrap;word-break:break-word;margin:0}
.term-meta{color:var(--term-meta)}
.term-warn{color:#facc15}
.term-err{color:#f87171}
.term-footer{padding:8px 12px;border-top:1px solid var(--term-border);display:flex;gap:8px}
/* Analysis */
.tabs{display:flex;gap:10px;margin-bottom:10px}
.tab{background:#e2e8f0;color:#0f172a;border:none;border-radius:10px;padding:8px 12px;font-weight:600;cursor:pointer}
.tab.active{background:#1e293b;color:#fff}
.flex{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.pill{display:inline-block;background:var(--pill);color:var(--pill-ink);padding:2px 10px;border-radius:999px;font-size:12px;font-weight:700}
.stat{display:grid;grid-template-columns:repeat(4,minmax(120px,1fr));gap:10px;margin-top:10px}
.stat .box{background:#f8fafc;border:1px solid var(--border);border-radius:12px;padding:10px;text-align:center}
.stat .num{font-weight:800;font-size:22px}
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:10px}
.filter{background:#e5e7eb;border:none;border-radius:999px;padding:6px 10px;font-weight:600;cursor:pointer}
.filter.active{background:#1e293b;color:#fff}
.search{flex:1;min-width:240px}
.posts{margin-top:12px;display:grid;gap:10px}
.post{background:#ffffff;border:1px solid var(--border);border-radius:12px;padding:12px}
.post-hdr{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap}
.badge{display:inline-block;background:var(--badge);color:var(--badge-ink);padding:2px 8px;border-radius:999px;font-size:12px;font-weight:700}
.kw{display:inline-block;background:#fff3cd;color:var(--kw-ink);border:1px solid #fde68a;padding:2px 8px;border-radius:999px;font-size:12px;margin:2px 4px 0 0}
.ai{margin-top:8px;background:#f1f5f9;border:1px solid var(--border);border-radius:10px;padding:10px}
.ai-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px}
.ai-pill{display:inline-block;border-radius:999px;padding:2px 10px;font-size:12px;font-weight:800}
.ai-ok{background:#dcfce7;color:#166534}
.ai-mid{background:#fef9c3;color:#92400e}
.ai-low{background:#fee2e2;color:#991b1b}
.email{font-size:12px;font-weight:700}
.email.ok{color:#166534} .email.no{color:#991b1b}
.reason{margin-top:6px;background:#fff;border:1px dashed var(--border);border-radius:10px;padding:8px;display:none}
.reason.show{display:block}
/* Recipient Dropdown */
.recipient-container { margin-top: 10px; position: relative; } /* Container for dropdown and custom input */
.recipient-select { margin-bottom: 10px; } /* Space below select if custom input appears */
#custom-recipient { margin-top: 6px; display: none; } /* Initially hidden custom input */
</style>
</head>
<body>
<div class="wrap">
<div class="header">
<h1>🩺 Hillside Medical Group - Social Media Monitor</h1>
<div class="muted">Automated monitoring and analysis of medical help requests in Facebook groups.</div>
</div>
<div class="grid" style="margin-top:16px">
<div class="left">
<div class="card">
<h3>System Status</h3>
<div id="sys"></div>
<button class="btn" style="margin-top:8px" onclick="refreshSystem()">Refresh Status</button>
</div>
<div class="card" style="margin-top:16px">
<h3>Start Monitoring Process</h3>
<label for="recipient-select">Report Recipients</label>
<div class="recipient-container">
<select id="recipient-select" class="recipient-select" onchange="handleRecipientChange()">
<option value="">-- Loading Recipients --</option>
</select>
<input type="email" id="custom-recipient" placeholder="Enter custom email address..." />
</div>
<div class="muted" style="margin:6px 0 10px">
Select a recipient from the list or choose 'Custom' to enter an email address.
The report summary will be sent to the selected address(es) after processing.
</div>
<div class="flex">
<button id="start" class="btn" onclick="startProcess()">Start Monitoring</button>
<button class="btn gray" onclick="refreshLive()">Refresh Live View</button>
</div>
<div style="margin-top:12px">
<div style="display:flex;justify-content:space-between"><b>Overall Progress</b><span id="pct" class="muted">0%</span></div>
<div class="bar"><div id="fill" class="fill"></div></div>
<div id="msg" class="muted" style="margin-top:6px">idle</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="tabs">
<button id="tab-groups" class="tab active" onclick="switchSection('groups')">Groups</button>
<button id="tab-analysis" class="tab" onclick="switchSection('analysis')">Analysis (Live)</button>
<button id="tab-summary" class="tab" onclick="switchSection('summary')">Summary</button>
</div>
<!-- Groups -->
<div id="section-groups">
<h3>Configured Groups (from groups.txt)</h3>
<div id="groups"></div>
</div>
<!-- Analysis Live -->
<div id="section-analysis" style="display:none">
<div class="flex">
<div class="pill" id="live-group">Group: –</div>
</div>
<div class="stat">
<div class="box"><div class="num" id="cnt-total">0</div><div class="muted">Total Posts</div></div>
<div class="box"><div class="num" id="cnt-kw">0</div><div class="muted">Keyword Hits</div></div>
<div class="box"><div class="num" id="cnt-ai">0</div><div class="muted">AI Analyzed</div></div>
<div class="box"><div class="num" id="cnt-confirmed">0</div><div class="muted">Confirmed</div></div>
</div>
<div class="filters">
<button class="filter active" id="flt-all" onclick="setFilter('all')">All</button>
<button class="filter" id="flt-kw" onclick="setFilter('kw')">Keyword</button>
<button class="filter" id="flt-confirmed" onclick="setFilter('confirmed')">Confirmed</button>
<input id="search" class="search" placeholder="Search post text..."/>
</div>
<div class="posts" id="posts"></div>
</div>
<!-- Summary -->
<div id="section-summary" style="display:none">
<h3>Last Run Summary</h3>
<div id="summary" class="muted">No summary available yet.</div>
</div>
</div>
</div>
<div class="right">
<div class="terminal">
<div class="term-header">Process Logs (Live)</div>
<div id="term" class="term-body"></div>
<div class="term-footer">
<button class="btn" onclick="scrollBottom()">Scroll to Bottom</button>
<button class="btn red" onclick="clearLogs()">Clear Logs</button>
</div>
</div>
</div>
</div>
</div>
<script>
const API = "/api"; //Server re babu
async function refreshSystem(){
try {
const r = await fetch(`${API}/system/status`);
const j = await r.json();
const chip = (b)=>`<span style="padding:2px 8px;border-radius:999px;font-size:12px;font-weight:700;background:${b?'#dcfce7':'#fee2e2'};color:${b?'#166534':'#991b1b'}">${b?'OK':'Missing'}</span>`;
document.getElementById('sys').innerHTML = `
<table>
<tr><td>Gmail Service</td><td>${chip(j.gmail)}</td></tr>
<tr><td>Groups File (groups.txt)</td><td>${chip(j.groups_file_exists)} • Count: ${j.groups_count}</td></tr>
<tr><td>Processing Script (final5.py)</td><td>${chip(j.final5_exists)}</td></tr>
<tr><td>Default Sender</td><td class="mono">${j.sender_email}</td></tr>
<tr><td>Data Folders</td><td class="mono">${j.scrape_outdir}${j.analysis_outdir}</td></tr>
</table>`;
} catch (error) {
console.error("Error refreshing system status:", error);
document.getElementById('sys').innerHTML = `<div class="muted">Error loading system status.</div>`;
}
}
async function loadGroups(){
try {
const r = await fetch(`${API}/groups`);
const j = await r.json();
const list = j.groups || [];
if(!list.length){
document.getElementById('groups').innerHTML = `<div class="muted">Please add Facebook group links to 'groups.txt' (one per line).</div>`;
return;
}
document.getElementById('groups').innerHTML = list.map((g,i)=>`
<div style="border:1px solid var(--border);border-radius:12px;padding:10px;margin:6px 0;background:#fff">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><b>#${i+1}</b><span class="mono">${g}</span></div>
<div id="g-${i}" class="muted">Status: Pending</div>
</div>
`).join("");
} catch (error) {
console.error("Error loading groups:", error);
document.getElementById('groups').innerHTML = `<div class="muted">Error loading groups list.</div>`;
}
}
// ---------------- Recipient Management ----------------
let recipientList = []; // Store fetched recipients
async function loadRecipients() {
const selectElement = document.getElementById('recipient-select');
selectElement.innerHTML = '<option value="">-- Loading Recipients --</option>';
try {
const response = await fetch(`${API}/recipients`);
const result = await response.json();
if (result.success && Array.isArray(result.data)) {
recipientList = result.data;
populateRecipientDropdown();
} else {
console.warn("API did not return a successful recipient list:", result);
selectElement.innerHTML = '<option value="">-- Error Loading --</option>';
}
} catch (error) {
console.error("Error fetching recipients:", error);
selectElement.innerHTML = '<option value="">-- Network Error --</option>';
}
}
function populateRecipientDropdown() {
const selectElement = document.getElementById('recipient-select');
selectElement.innerHTML = ''; // Clear loading option
// Add default recipient first
const defaultRecipient = "smahato@hillsidemedicalgroup.com";
let defaultOptionFound = false;
recipientList.forEach(recipient => {
const option = document.createElement('option');
option.value = recipient.email;
option.textContent = `${recipient.name} (${recipient.email})`;
if (recipient.email === defaultRecipient) {
option.selected = true;
defaultOptionFound = true;
}
selectElement.appendChild(option);
});
// Add Custom option
const customOption = document.createElement('option');
customOption.value = "custom";
customOption.textContent = "-- Custom Email --";
selectElement.appendChild(customOption);
// If default wasn't in the list, add it and select it
if (!defaultOptionFound) {
const defaultOption = document.createElement('option');
defaultOption.value = defaultRecipient;
defaultOption.textContent = `Subash Mahato (Default) (${defaultRecipient})`;
defaultOption.selected = true;
selectElement.insertBefore(defaultOption, selectElement.firstChild); // Add to top
}
}
function handleRecipientChange() {
const selectElement = document.getElementById('recipient-select');
const customInput = document.getElementById('custom-recipient');
if (selectElement.value === 'custom') {
customInput.style.display = 'block';
customInput.focus();
} else {
customInput.style.display = 'none';
customInput.value = ''; // Clear if deselected
}
}
function getSelectedRecipients() {
const selectElement = document.getElementById('recipient-select');
const customInput = document.getElementById('custom-recipient');
let emails = [];
if (selectElement.value === 'custom' && customInput.value.trim() !== '') {
// Validate custom email (basic check)
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailRegex.test(customInput.value.trim())) {
emails.push(customInput.value.trim());
} else {
alert("Please enter a valid custom email address.");
return null; // Signal error
}
} else if (selectElement.value) {
emails.push(selectElement.value);
} else {
// No recipient selected (shouldn't happen with default)
alert("Please select a recipient.");
return null;
}
return emails;
}
// ---------------- Process control ----------------
async function startProcess(){
document.getElementById('start').disabled = true;
const recipients = getSelectedRecipients();
if (!recipients) {
// Error message already shown in getSelectedRecipients
document.getElementById('start').disabled = false;
return;
}
try {
await fetch(`${API}/process/clear-logs`, {method:'POST'}); // Clear previous logs
const r = await fetch(`${API}/process/start`, {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({recipients: recipients}) // Send selected recipients
});
const j = await r.json();
if(!j.success){
alert(j.message||'Failed to start the process.');
document.getElementById('start').disabled = false;
return;
}
pollStatus(); // Start polling for status updates
} catch (error) {
console.error("Error starting process:", error);
alert("An error occurred while trying to start the process.");
document.getElementById('start').disabled = false;
}
}
async function pollStatus(){
try {
const r = await fetch(`${API}/process/status`);
const j = await r.json();
document.getElementById('msg').innerText = j.message || 'Idle';
document.getElementById('pct').innerText = `${j.progress||0}%`;
document.getElementById('fill').style.width = `${j.progress||0}%`;
(j.groups||[]).forEach((g,idx)=>{
const el = document.getElementById(`g-${idx}`);
if(!el) return;
el.innerHTML = `Status: ${g.stage} • Scraped: ${g.scraped_posts} • Confirmed: ${g.detected_posts}${g.error?(' • Error: '+g.error):''}`;
});
if(j.running){
setTimeout(pollStatus, 1200); // Poll every 1.2 seconds while running
} else {
document.getElementById('start').disabled = false;
loadSummary(); // Load the final summary when done
}
} catch (error) {
console.error("Error polling status:", error);
// Optionally, retry or show error state
setTimeout(pollStatus, 3000); // Retry after a longer delay on error
}
}
// ---------------- Logs (terminal) ----------------
let lastLogId = 0;
function appendLogs(entries){
const term = document.getElementById('term');
const nearBottom = term.scrollTop + term.clientHeight >= term.scrollHeight - 40;
entries.forEach(e=>{
const div = document.createElement('div');
div.className = 'term-line';
const color = e.level === 'error' ? 'term-err' : e.level === 'warn' ? 'term-warn' : '';
div.innerHTML = `<span class="term-meta">[${e.ts}] [${e.source}]</span> <span class="${color}">${escapeHtml(e.msg)}</span>`;
term.appendChild(div);
});
if(nearBottom || entries.length){ term.scrollTop = term.scrollHeight; }
}
async function pollLogs(){
try{
const r = await fetch(`${API}/process/logs?after=${lastLogId}&limit=500`);
const j = await r.json();
if(j.entries && j.entries.length){
appendLogs(j.entries);
lastLogId = j.last || lastLogId;
}
}catch(e){
console.error("Error polling logs:", e);
// Continue polling even on error
}
setTimeout(pollLogs, 900); // Poll logs every 0.9 seconds
}
function scrollBottom(){
const term = document.getElementById('term');
term.scrollTop = term.scrollHeight;
}
async function clearLogs(){
try {
await fetch(`${API}/process/clear-logs`, {method:'POST'});
document.getElementById('term').innerHTML = '';
lastLogId = 0;
} catch (error) {
console.error("Error clearing logs:", error);
// Optionally inform user
}
}
function escapeHtml(s){
return (s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'<','>':'>','"':'&quot;',"'":'&#039;'}[c]));
}
// ---------------- Live Analysis ----------------
let liveFilter = 'all'; // 'all' | 'kw' | 'confirmed'
let liveSearch = '';
function setFilter(f){
liveFilter = f;
document.getElementById('flt-all').classList.toggle('active', f==='all');
document.getElementById('flt-kw').classList.toggle('active', f==='kw');
document.getElementById('flt-confirmed').classList.toggle('active', f==='confirmed');
refreshLive();
}
document.getElementById('search').addEventListener('input', e=>{
liveSearch = e.target.value.toLowerCase();
refreshLive();
});
async function refreshLive(){
try {
const r = await fetch(`${API}/live/state`);
const j = await r.json();
if(!j.success) {
console.warn("Live state API returned not success:", j);
return;
}
const data = j.data || {};
renderLive(data);
} catch (error) {
console.error("Error refreshing live state:", error);
// Optionally update UI to show error
}
}
function renderLive(data){
// header + counts
document.getElementById('live-group').innerText = `Current Group: ${data.group || '–'}`;
const c = data.counts || {};
document.getElementById('cnt-total').innerText = c.total_posts || 0;
document.getElementById('cnt-kw').innerText = c.kw_hits || 0;
document.getElementById('cnt-ai').innerText = c.ai_done || 0;
document.getElementById('cnt-confirmed').innerText = c.confirmed || 0;
const posts = Array.isArray(data.posts) ? data.posts : [];
// filter/search
const filtered = posts.filter(p=>{
const hasKW = Array.isArray(p.found_keywords) && p.found_keywords.length>0;
const isConfirmed = p.ai && p.ai.is_medical_seeking;
if(liveFilter==='kw' && !hasKW) return false;
if(liveFilter==='confirmed' && !isConfirmed) return false;
if(liveSearch && !(p.text||'').toLowerCase().includes(liveSearch)) return false;
return true;
});
const html = filtered.map(p=>{
const hasKW = Array.isArray(p.found_keywords) && p.found_keywords.length>0;
const ai = p.ai || null;
const confirm = !!(ai && ai.is_medical_seeking);
const conf = ai ? (ai.confidence||'').toLowerCase() : '';
const urg = ai ? (ai.urgency_level||'').toLowerCase() : '';
const confClass = conf==='high' ? 'ai-ok' : conf==='medium' ? 'ai-mid' : 'ai-low';
const urgClass = urg==='high' ? 'ai-low' : urg==='medium' ? 'ai-mid' : 'ai-ok';
const emailTxt = p.email_sent ? '<span class="email ok">Email Sent</span>' : '<span class="email no">No Email</span>';
return `
<div class="post">
<div class="post-hdr">
<div class="flex">
<span class="badge">Post #${p.id || '-'}</span>
${hasKW ? '<span class="badge">Keyword Hit</span>' : ''}
${confirm ? '<span class="badge" style="background-color: #bfdbfe; color: #1e40af;">CONFIRMED</span>' : ''} <!-- Blue badge for confirmed -->
</div>
<div class="muted mono">Group: ${escapeHtml(p.group_link || 'N/A')}</div>
</div>
<div style="margin-top:6px; white-space: pre-wrap;">${escapeHtml(p.text || '')}</div> <!-- pre-wrap for text formatting -->
${hasKW ? `
<div style="margin-top:6px">${(p.found_keywords||[]).map(k=>`<span class="kw">${escapeHtml(k)}</span>`).join('')}</div>
` : ''}
${ai ? `
<div class="ai">
<div class="ai-row">
<span class="ai-pill ${confClass}">Confidence: ${(ai.confidence||'').toUpperCase()}</span>
<span class="ai-pill ${urgClass}">Urgency: ${(ai.urgency_level||'').toUpperCase()}</span>
${emailTxt}
</div>
<div><b>Summary:</b> ${escapeHtml(ai.medical_summary || '')}</div>
<div style="margin-top:4px"><b>Analysis:</b> ${escapeHtml(ai.analysis || '')}</div>
${Array.isArray(ai.suggested_services) && ai.suggested_services.length ? `
<div style="margin-top:4px"><b>Suggested Services:</b> ${(ai.suggested_services||[]).map(s=>`<span class="badge" style="margin-right:6px">${escapeHtml(s)}</span>`).join('')}</div>
`:''}
<div style="margin-top:6px">
<button class="btn gray" onclick="toggleReason(${p.id})">Show Reasoning</button>
<div id="reason-${p.id}" class="reason">${escapeHtml(ai.reasoning || 'No reasoning provided.')}</div>
</div>
</div>
` : '<div class="muted">Awaiting AI analysis...</div>'}
</div>
`;
}).join('');
document.getElementById('posts').innerHTML = html || `<div class="muted">No posts match the current filters.</div>`;
}
function toggleReason(id){
const el = document.getElementById(`reason-${id}`);
if(!el) return;
el.classList.toggle('show');
}
// auto poll live state
async function pollLive(){
await refreshLive();
setTimeout(pollLive, 1200); // Poll live view every 1.2 seconds
}
// ---------------- Summary ----------------
async function loadSummary(){
try {
const r = await fetch(`${API}/results/summary`);
const el = document.getElementById('summary');
if(r.status!==200){
el.innerHTML = `<div class="muted">Processing complete. Summary will appear here shortly.</div>`;
// Retry once after a short delay if not found immediately
setTimeout(async () => {
const retry = await fetch(`${API}/results/summary`);
if(retry.status === 200) {
const j = await retry.json();
el.innerHTML = `<pre class="mono">${JSON.stringify(j.data,null,2)}</pre>`;
}
}, 2000);
return;
}
const j = await r.json();
// Check if data exists and format nicely
if (j.success && j.data) {
// You can create a more user-friendly summary view here instead of raw JSON
// For now, we'll keep the JSON view
el.innerHTML = `<pre class="mono">${JSON.stringify(j.data,null,2)}</pre>`;
} else {
el.innerHTML = `<div class="muted">Summary data unavailable.</div>`;
}
} catch (error) {
console.error("Error loading summary:", error);
document.getElementById('summary').innerHTML = `<div class="muted">Error loading summary.</div>`;
}
}
// ---------------- Tabs ----------------
function switchSection(name){
const sec = (id,show)=>document.getElementById(id).style.display = show?'block':'none';
document.getElementById('tab-groups').classList.toggle('active', name==='groups');
document.getElementById('tab-analysis').classList.toggle('active', name==='analysis');
document.getElementById('tab-summary').classList.toggle('active', name==='summary');
sec('section-groups', name==='groups');
sec('section-analysis', name==='analysis');
sec('section-summary', name==='summary');
if(name==='analysis') refreshLive();
if(name==='summary') loadSummary();
}
// ---------------- Boot ----------------
// Load initial data and start polling
refreshSystem();
loadGroups();
loadRecipients(); // Load recipients on startup
loadSummary();
pollStatus(); // Start status polling
pollLogs(); // Start log polling
pollLive(); // Start live analysis polling
</script>
</body>
</html>