CheddahBot/dashboard/index.html

976 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CheddahBot Command Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Playfair+Display:wght@700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="dashboard">
<!-- ============ TOP BAR ============ -->
<header class="topbar">
<div class="topbar__brand">
<button class="mobile-toggle" onclick="toggleSidebar()" aria-label="Toggle navigation">&#9776;</button>
<div class="topbar__logo">C</div>
<span class="topbar__title">CheddahBot</span>
<span class="topbar__subtitle">Command Dashboard</span>
</div>
<div class="topbar__right">
<div class="topbar__status" id="system-status">
<span class="dot"></span>
<span>Loading...</span>
</div>
<button class="mobile-toggle" onclick="refreshAll()" aria-label="Refresh" style="display:inline-flex;font-size:0.85rem;" title="Refresh data">&#8635;</button>
<time class="topbar__time" id="clock"></time>
</div>
</header>
<!-- ============ SIDEBAR ============ -->
<nav class="sidebar" id="sidebar">
<div class="sidebar__section">
<div class="sidebar__label">Operations</div>
<button class="sidebar__link active" data-tab="overview">
<span class="icon">&#9788;</span>
Overview
</button>
<button class="sidebar__link" data-tab="linkbuilding">
<span class="icon">&#128279;</span>
Link Building
<span class="badge" id="badge-lb">-</span>
</button>
<button class="sidebar__link" data-tab="pressreleases">
<span class="icon">&#128240;</span>
Press Releases
<span class="badge" id="badge-pr">-</span>
</button>
<button class="sidebar__link" data-tab="bycompany">
<span class="icon">&#127970;</span>
By Company
</button>
</div>
<div class="sidebar__divider"></div>
<div class="sidebar__section">
<div class="sidebar__label">System</div>
<button class="sidebar__link" data-tab="health">
<span class="icon">&#9881;</span>
System Health
</button>
<button class="sidebar__link" data-tab="agents">
<span class="icon">&#129302;</span>
Agents
</button>
<button class="sidebar__link" data-tab="notifications">
<span class="icon">&#128220;</span>
Notifications
</button>
</div>
<div class="sidebar__divider"></div>
<div class="sidebar__section">
<div class="sidebar__label">Quick Links</div>
<a class="sidebar__link" href="/" target="_blank">
<span class="icon">&#128172;</span>
Open Chat
</a>
</div>
</nav>
<!-- ============ MAIN CONTENT ============ -->
<main class="main">
<!-- ========== TAB: OVERVIEW ========== -->
<div class="tab-panel active" id="tab-overview">
<div class="briefing-header">
<div class="briefing-header__left">
<h1 class="briefing-header__greeting" id="greeting-text">Good Morning, Bryan.</h1>
<p class="briefing-header__date" id="greeting-date"></p>
</div>
<blockquote class="briefing-header__quote">
&ldquo;Continuous effort &mdash; not strength or intelligence &mdash; is the key to unlocking our potential.&rdquo;
<cite>&mdash; Churchill</cite>
</blockquote>
</div>
<!-- Stats Row -->
<div class="stats-row stats-row--compact" id="overview-stats">
<div class="stat-card stat-card--gold stat-card--mini">
<div class="stat-card__value" id="stat-total">-</div>
<div class="stat-card__label">Total Tasks</div>
<div class="stat-card__detail" id="stat-total-detail">Loading...</div>
</div>
<div class="stat-card stat-card--blue stat-card--mini">
<div class="stat-card__value" id="stat-lb">-</div>
<div class="stat-card__label">Link Building</div>
<div class="stat-card__detail" id="stat-lb-detail">Loading...</div>
</div>
<div class="stat-card stat-card--green stat-card--mini">
<div class="stat-card__value" id="stat-pr">-</div>
<div class="stat-card__label">Press Releases</div>
<div class="stat-card__detail" id="stat-pr-detail">Loading...</div>
</div>
<div class="stat-card stat-card--amber stat-card--mini">
<div class="stat-card__value" id="stat-companies">-</div>
<div class="stat-card__label">Companies</div>
<div class="stat-card__detail" id="stat-companies-detail">Loading...</div>
</div>
<div class="stat-card stat-card--green stat-card--mini">
<div class="stat-card__value" id="stat-agents">-</div>
<div class="stat-card__label">Agents</div>
<div class="stat-card__detail" id="stat-agents-detail">Loading...</div>
</div>
</div>
<!-- Up Next (today/tomorrow, or next 5 by due date) -->
<div class="section section--tight">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#9200;</span> Up Next</h2>
<span class="section__badge" id="up-next-count">-</span>
</div>
<div class="task-table-wrap task-table-wrap--compact" id="overview-up-next">
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
</div>
</div>
<!-- This Month (tagged with current month) -->
<div class="section section--tight">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#128197;</span> This Month</h2>
<span class="section__badge" id="this-month-count">-</span>
</div>
<div class="task-table-wrap task-table-wrap--compact" id="overview-this-month">
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
</div>
</div>
<!-- Cora Reports Needed -->
<div class="section section--tight">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#128203;</span> Cora Reports Needed</h2>
<span onclick="navigator.clipboard.writeText('Z:\\cora-inbox');this.textContent='Copied!';setTimeout(()=>this.textContent='Z:\\cora-inbox',1500)" style="font-size:0.75rem;color:var(--gold-light);cursor:pointer;margin-left:0.5rem;" title="Click to copy path">Z:\cora-inbox</span>
<span class="section__badge"><a href="#" class="section__link" data-tab="linkbuilding">View LB</a></span>
</div>
<div class="task-table-wrap task-table-wrap--compact" id="overview-cora">
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
</div>
</div>
<!-- System Health Inline -->
<div class="section section--tight" id="briefing-health">
<div class="health-inline" id="overview-health">
<span class="health-inline__dot health-inline__dot--ok"></span>
<span class="health-inline__text">Loading system health...</span>
<span class="health-inline__detail"></span>
</div>
</div>
</div>
<!-- ========== TAB: LINK BUILDING ========== -->
<div class="tab-panel" id="tab-linkbuilding">
<div class="page-header">
<h1 class="page-header__greeting">Link Building Operations</h1>
<p class="page-header__date" id="lb-subtitle">Loading...</p>
</div>
<div class="stats-row" id="lb-stats"></div>
<!-- Recently Completed (Past 7 Days) -->
<div class="section">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#9989;</span> Recently Completed (Past 7 Days)</h2>
<span class="section__badge" id="lb-recent-count">-</span>
</div>
<div class="task-table-wrap" id="lb-recent-table">
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
</div>
</div>
<!-- In Progress - Not Started -->
<div class="section">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#9888;</span> In Progress &mdash; Not Started</h2>
<span class="section__badge" id="lb-not-started-count">-</span>
</div>
<div class="task-table-wrap" id="lb-not-started-table">
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
</div>
</div>
<!-- Company Breakdown -->
<div class="section">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#127970;</span> By Company</h2>
</div>
<div class="health-grid" id="lb-company-grid"></div>
</div>
<!-- Full task table -->
<div class="section">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#128203;</span> Full Task List</h2>
</div>
<div class="task-table-wrap" id="lb-full-table">
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
</div>
</div>
</div>
<!-- ========== TAB: PRESS RELEASES ========== -->
<div class="tab-panel" id="tab-pressreleases">
<div class="page-header">
<h1 class="page-header__greeting">Press Releases</h1>
<p class="page-header__date" id="pr-subtitle">Loading...</p>
</div>
<div class="stats-row" id="pr-stats"></div>
<div class="section">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#128240;</span> All Press Releases</h2>
</div>
<div id="pr-cards-container">
<p style="color:var(--text-muted);">Loading...</p>
</div>
</div>
</div>
<!-- ========== TAB: BY COMPANY ========== -->
<div class="tab-panel" id="tab-bycompany">
<div class="page-header">
<h1 class="page-header__greeting">Tasks by Company</h1>
<p class="page-header__date" id="company-subtitle">Loading...</p>
</div>
<div id="company-sections">
<p style="color:var(--text-muted);">Loading...</p>
</div>
</div>
<!-- ========== TAB: SYSTEM HEALTH ========== -->
<div class="tab-panel" id="tab-health">
<div class="page-header">
<h1 class="page-header__greeting">System Health</h1>
<p class="page-header__date" id="health-subtitle">Loading...</p>
</div>
<div class="stats-row" id="health-stats"></div>
<div class="section">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#128190;</span> Disk Space</h2>
</div>
<div class="health-grid" id="health-disks"></div>
</div>
</div>
<!-- ========== TAB: AGENTS ========== -->
<div class="tab-panel" id="tab-agents">
<div class="page-header">
<h1 class="page-header__greeting">Agent Configuration</h1>
<p class="page-header__date">Registered agents and their capabilities</p>
</div>
<div class="health-grid" id="agents-grid">
<p style="color:var(--text-muted);">Loading...</p>
</div>
</div>
<!-- ========== TAB: NOTIFICATIONS ========== -->
<div class="tab-panel" id="tab-notifications">
<div class="page-header">
<h1 class="page-header__greeting">Notifications</h1>
<p class="page-header__date">Recent system notifications and events</p>
</div>
<div class="task-table-wrap" id="notifications-list">
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
</div>
</div>
</main>
</div>
<script>
// ============================================================
// CheddahBot Dashboard - Data-Driven JS
// ============================================================
const API = '/api';
let _cache = {};
// --- Helpers ---
function esc(str) {
if (!str) return '';
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
function statusPillClass(status) {
if (!status) return 'status-pill--todo';
const s = status.toLowerCase();
if (s.includes('done') || s.includes('complete') || s.includes('closed')) return 'status-pill--done';
if (s.includes('progress') || s.includes('review') || s.includes('active')) return 'status-pill--progress';
return 'status-pill--todo';
}
function statusPill(status) {
return `<span class="status-pill ${statusPillClass(status)}">${esc(status || 'Unknown')}</span>`;
}
async function fetchJSON(path) {
try {
const res = await fetch(API + path);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (e) {
console.error(`Failed to fetch ${path}:`, e);
return null;
}
}
// --- Greeting ---
function updateGreeting() {
const now = new Date();
const hour = now.getHours();
let greeting = 'Good Evening';
if (hour < 12) greeting = 'Good Morning';
else if (hour < 17) greeting = 'Good Afternoon';
document.getElementById('greeting-text').textContent = `${greeting}, Bryan.`;
const opts = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
document.getElementById('greeting-date').textContent = now.toLocaleDateString('en-US', opts);
}
// --- Overview Tab ---
async function loadOverview() {
const [tasks, agents, health] = await Promise.all([
fetchJSON('/tasks'),
fetchJSON('/agents'),
fetchJSON('/system/health'),
]);
// Cache for other tabs
_cache.tasks = tasks;
_cache.agents = agents;
_cache.health = health;
// Stats — derive LB/PR counts from tasks data
if (tasks && tasks.tasks) {
document.getElementById('stat-total').textContent = tasks.count || 0;
document.getElementById('stat-total-detail').textContent = `ClickUp tasks`;
const lbTasks = tasks.tasks.filter(t => t.task_type === 'Link Building');
const prTasks = tasks.tasks.filter(t => t.task_type === 'Press Release');
document.getElementById('stat-lb').textContent = lbTasks.length;
const lbCompanies = new Set(lbTasks.map(t => t.custom_fields?.Customer || 'Unassigned'));
document.getElementById('stat-lb-detail').textContent = `${lbCompanies.size} companies`;
document.getElementById('badge-lb').textContent = lbTasks.length;
document.getElementById('stat-pr').textContent = prTasks.length;
const prCompanies = new Set(prTasks.map(t => t.custom_fields?.Customer || 'Unassigned'));
document.getElementById('stat-pr-detail').textContent = `${prCompanies.size} companies`;
document.getElementById('badge-pr').textContent = prTasks.length;
const companies = new Set(tasks.tasks.map(t => t.custom_fields?.Customer || 'Unassigned'));
document.getElementById('stat-companies').textContent = companies.size;
const names = [...companies].filter(c => c !== 'Unassigned').slice(0, 4).join(', ');
document.getElementById('stat-companies-detail').textContent = names || 'None';
}
if (agents) {
document.getElementById('stat-agents').textContent = agents.agents?.length || 0;
document.getElementById('stat-agents-detail').textContent = 'Configured';
}
// -- Up Next: tasks due today/tomorrow, or next 5 by due date --
if (tasks && tasks.tasks) {
const now = new Date();
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const endOfTomorrow = startOfToday + 2 * 24 * 60 * 60 * 1000;
// Only consider open tasks
const openTasks = tasks.tasks.filter(t => {
const s = (t.status || '').toLowerCase();
return !(s.includes('complete') || s.includes('done') || s.includes('closed'));
});
let upNext = openTasks
.filter(t => {
if (!t.due_date) return false;
const due = parseInt(t.due_date, 10);
return due >= startOfToday && due < endOfTomorrow;
})
.sort((a, b) => parseInt(a.due_date) - parseInt(b.due_date));
// Fallback: if nothing due today/tomorrow, show next 5 with a due date
if (upNext.length === 0) {
upNext = openTasks
.filter(t => t.due_date && parseInt(t.due_date, 10) >= startOfToday)
.sort((a, b) => parseInt(a.due_date) - parseInt(b.due_date))
.slice(0, 5);
}
document.getElementById('up-next-count').textContent = upNext.length;
renderOverviewTable('overview-up-next', upNext, true);
// -- This Month: tagged with month tag OR due date this month --
const monthNames = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'];
const monthTag = monthNames[now.getMonth()] + String(now.getFullYear()).slice(2);
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
const startOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1).getTime();
const thisMonthSet = new Set();
const thisMonth = [];
for (const t of openTasks) {
if (thisMonthSet.has(t.id)) continue;
const hasTag = (t.tags || []).some(tag => tag.toLowerCase() === monthTag);
const dueThisMonth = t.due_date &&
parseInt(t.due_date, 10) >= startOfMonth &&
parseInt(t.due_date, 10) < startOfNextMonth;
if (hasTag || dueThisMonth) {
thisMonth.push(t);
thisMonthSet.add(t.id);
}
}
document.getElementById('this-month-count').textContent = thisMonth.length;
renderOverviewTable('overview-this-month', thisMonth, true);
// -- Cora Reports Needed: LB tasks with LB Method = Cora Backlinks --
const needCora = openTasks.filter(t =>
t.task_type === 'Link Building'
&& t.custom_fields?.['LB Method'] === 'Cora Backlinks'
);
renderOverviewTable('overview-cora', needCora, false);
}
// Health inline
if (health) {
renderHealthInline(health);
}
// System status in topbar
updateSystemStatus(health);
}
function renderTaskTable(containerId, tasks, compact) {
const container = document.getElementById(containerId);
if (!tasks || tasks.length === 0) {
container.innerHTML = '<p style="padding:1rem;color:var(--text-muted);">No tasks found.</p>';
return;
}
const cls = compact ? 'task-table task-table--dense' : 'task-table';
let html = `<table class="${cls}">
<thead><tr>
<th>#</th><th>Task</th><th>Company</th><th>Keyword</th><th>Status</th>
</tr></thead><tbody>`;
tasks.forEach((t, i) => {
const company = t.custom_fields?.Customer || 'Unassigned';
const keyword = t.custom_fields?.Keyword || '';
const link = t.url ? `<a href="${esc(t.url)}" target="_blank" style="color:var(--cream-light);text-decoration:none;">${esc(t.name)}</a>` : esc(t.name);
html += `<tr>
<td class="task-table__num">${i + 1}</td>
<td class="task-table__title">${link}</td>
<td class="task-table__company">${esc(company)}</td>
<td class="task-table__keyword">${esc(keyword)}</td>
<td class="task-table__status">${statusPill(t.status)}</td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
function renderRecentTable(containerId, tasks) {
const container = document.getElementById(containerId);
if (!tasks || tasks.length === 0) {
container.innerHTML = '<p style="padding:1rem;color:var(--text-muted);">No recently completed tasks.</p>';
return;
}
let html = `<table class="task-table">
<thead><tr>
<th>#</th><th>Task</th><th>Company</th><th>Keyword</th><th>Completed</th>
</tr></thead><tbody>`;
tasks.forEach((t, i) => {
const company = t.custom_fields?.Customer || 'Unassigned';
const keyword = t.custom_fields?.Keyword || '';
const link = t.url ? `<a href="${esc(t.url)}" target="_blank" style="color:var(--cream-light);text-decoration:none;">${esc(t.name)}</a>` : esc(t.name);
let doneDate = '-';
if (t.date_done) {
const d = new Date(parseInt(t.date_done, 10));
doneDate = d.toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric'});
}
html += `<tr>
<td class="task-table__num">${i + 1}</td>
<td class="task-table__title">${link}</td>
<td class="task-table__company">${esc(company)}</td>
<td class="task-table__keyword">${esc(keyword)}</td>
<td style="white-space:nowrap;">${esc(doneDate)}</td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
function renderOverviewTable(containerId, tasks, showDueDate) {
const container = document.getElementById(containerId);
if (!tasks || tasks.length === 0) {
container.innerHTML = '<p style="padding:1rem;color:var(--text-muted);">None right now.</p>';
return;
}
const dueDateHeader = showDueDate ? '<th>Due</th>' : '';
let html = `<table class="task-table task-table--dense">
<thead><tr>
<th>#</th><th>Task</th><th>Company</th><th>Type</th><th>Status</th>${dueDateHeader}
</tr></thead><tbody>`;
tasks.forEach((t, i) => {
const company = t.custom_fields?.Customer || 'Unassigned';
const link = t.url ? `<a href="${esc(t.url)}" target="_blank" style="color:var(--cream-light);text-decoration:none;">${esc(t.name)}</a>` : esc(t.name);
let dueDateCell = '';
if (showDueDate && t.due_date) {
const d = new Date(parseInt(t.due_date, 10));
dueDateCell = `<td style="white-space:nowrap;">${d.toLocaleDateString('en-US', {month:'short', day:'numeric'})}</td>`;
} else if (showDueDate) {
dueDateCell = '<td>-</td>';
}
html += `<tr>
<td class="task-table__num">${i + 1}</td>
<td class="task-table__title">${link}</td>
<td class="task-table__company">${esc(company)}</td>
<td class="task-table__company">${esc(t.task_type || '-')}</td>
<td class="task-table__status">${statusPill(t.status)}</td>
${dueDateCell}
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
function renderPRCards(containerId, tasks) {
const container = document.getElementById(containerId);
if (!tasks || tasks.length === 0) {
container.innerHTML = '<p style="color:var(--text-muted);">No press release tasks found.</p>';
return;
}
let html = '';
tasks.forEach(t => {
const company = t.custom_fields?.Customer || 'Unassigned';
const link = t.url ? ` <a href="${esc(t.url)}" target="_blank" style="color:var(--gold-light);font-size:0.75rem;">[ClickUp]</a>` : '';
const kvState = t.kv_state;
let stateInfo = '';
if (kvState) {
stateInfo = `KV: ${kvState.state || 'unknown'}`;
if (kvState.completed_at) stateInfo += ` | Completed: ${kvState.completed_at}`;
}
html += `<div class="pr-card">
<div class="pr-card__header">
<div>
<div class="pr-card__company">${esc(company)}</div>
<div class="pr-card__headline">${esc(t.name)}${link}</div>
</div>
${statusPill(t.status)}
</div>
<div class="pr-card__detail">Task ID: ${esc(t.id)}${stateInfo ? ' | ' + esc(stateInfo) : ''}</div>
</div>`;
});
container.innerHTML = html;
}
function renderHealthInline(health) {
const el = document.getElementById('overview-health');
const diskInfo = health.disks?.map(d => `${d.drive} ${d.percent_free}% free`).join(' | ') || 'No disk info';
const brainOk = health.execution_brain;
const clickupOk = health.clickup_enabled;
const dotClass = (brainOk && clickupOk) ? 'health-inline__dot--ok' : 'health-inline__dot--warn';
const statusText = (brainOk && clickupOk) ? 'All systems nominal' : 'Some systems degraded';
el.innerHTML = `
<span class="health-inline__dot ${dotClass}"></span>
<span class="health-inline__text">${statusText}</span>
<span class="health-inline__detail">${esc(diskInfo)} | Brain: ${brainOk ? 'OK' : 'Missing'} | ClickUp: ${clickupOk ? 'OK' : 'Off'}</span>
`;
}
function updateSystemStatus(health) {
const el = document.getElementById('system-status');
if (!health) {
el.innerHTML = '<span class="dot" style="background:var(--red)"></span><span>API Error</span>';
return;
}
const ok = health.execution_brain && health.clickup_enabled;
el.innerHTML = `<span class="dot" style="background:${ok ? 'var(--green)' : 'var(--amber)'}"></span>
<span>${ok ? 'All Systems Operational' : 'Degraded'}</span>`;
}
// --- Link Building Tab ---
async function loadLinkBuilding() {
const data = _cache.lb || await fetchJSON('/tasks/link-building');
if (!data) return;
_cache.lb = data;
document.getElementById('lb-subtitle').textContent =
`${data.total || 0} tasks across ${data.companies?.length || 0} companies`;
// Stats
const statsHtml = `
<div class="stat-card stat-card--gold">
<div class="stat-card__label">Total Tasks</div>
<div class="stat-card__value">${data.total || 0}</div>
<div class="stat-card__detail">Work Category: Link Building</div>
</div>
<div class="stat-card stat-card--blue">
<div class="stat-card__label">Companies</div>
<div class="stat-card__value">${data.companies?.length || 0}</div>
<div class="stat-card__detail">${data.companies?.map(c => c.name).join(', ') || 'None'}</div>
</div>
${renderStatusCountCards(data.status_counts)}
`;
document.getElementById('lb-stats').innerHTML = statsHtml;
// Recently Completed
const recent = data.recently_completed || [];
document.getElementById('lb-recent-count').textContent = recent.length;
renderRecentTable('lb-recent-table', recent);
// In Progress - Not Started
const notStarted = data.in_progress_not_started || [];
document.getElementById('lb-not-started-count').textContent = notStarted.length;
renderTaskTable('lb-not-started-table', notStarted, false);
// Company breakdown
const grid = document.getElementById('lb-company-grid');
if (data.companies && data.companies.length > 0) {
let html = '';
data.companies.forEach(co => {
const done = co.tasks.filter(t => {
const s = (t.status || '').toLowerCase();
return s.includes('complete') || s.includes('done') || s.includes('closed');
}).length;
const pct = co.count > 0 ? Math.round((done / co.count) * 100) : 0;
const barColor = pct >= 100 ? 'green' : pct > 0 ? 'amber' : 'amber';
html += `<div class="health-card">
<div class="health-card__header">
<span class="health-card__title">${esc(co.name)}</span>
<span class="section__badge">${co.count} task${co.count !== 1 ? 's' : ''}</span>
</div>
<div class="health-bar"><div class="health-bar__fill health-bar__fill--${barColor}" style="width:${pct}%"></div></div>
<div class="health-card__meta"><span>${done} / ${co.count} completed</span><span>${pct}%</span></div>
</div>`;
});
grid.innerHTML = html;
} else {
grid.innerHTML = '<p style="color:var(--text-muted);">No link building tasks found.</p>';
}
// Full task table
const allTasks = data.companies?.flatMap(c => c.tasks) || [];
renderTaskTable('lb-full-table', allTasks, false);
}
function renderStatusCountCards(counts) {
if (!counts) return '';
let html = '';
for (const [status, count] of Object.entries(counts)) {
const color = statusToCardColor(status);
html += `<div class="stat-card stat-card--${color}">
<div class="stat-card__label">${esc(status)}</div>
<div class="stat-card__value">${count}</div>
</div>`;
}
return html;
}
function statusToCardColor(status) {
const s = (status || '').toLowerCase();
if (s.includes('complete') || s.includes('done') || s.includes('closed')) return 'green';
if (s.includes('progress') || s.includes('review') || s.includes('active')) return 'amber';
if (s.includes('fail') || s.includes('error') || s.includes('block')) return 'red';
return 'blue';
}
// --- Press Releases Tab ---
async function loadPressReleases() {
const data = _cache.pr || await fetchJSON('/tasks/press-releases');
if (!data) return;
_cache.pr = data;
document.getElementById('pr-subtitle').textContent =
`${data.total || 0} tasks across ${data.companies?.length || 0} companies`;
// Stats
let statsHtml = `
<div class="stat-card stat-card--gold">
<div class="stat-card__label">Total PRs</div>
<div class="stat-card__value">${data.total || 0}</div>
<div class="stat-card__detail">Work Category: Press Release</div>
</div>
${renderStatusCountCards(data.status_counts)}
`;
document.getElementById('pr-stats').innerHTML = statsHtml;
// PR cards
const allPR = data.companies?.flatMap(c => c.tasks) || [];
renderPRCards('pr-cards-container', allPR);
}
// --- By Company Tab ---
async function loadByCompany() {
const data = _cache.tasks ? { companies: groupByCompany(_cache.tasks.tasks || []) }
: await fetchJSON('/tasks/by-company');
if (!data) return;
const companies = data.companies || [];
document.getElementById('company-subtitle').textContent =
`${companies.length} companies with active tasks`;
let html = '';
companies.forEach(co => {
const coData = co.tasks || co;
const tasks = Array.isArray(coData) ? coData : (co.tasks || []);
const name = co.name || 'Unknown';
html += `<div class="section">
<div class="section__header">
<h2 class="section__title"><span class="icon">&#127970;</span> ${esc(name)}</h2>
<span class="section__badge">${tasks.length} task${tasks.length !== 1 ? 's' : ''}</span>
</div>`;
if (tasks.length > 0) {
html += '<div class="task-table-wrap"><table class="task-table"><thead><tr>';
html += '<th>#</th><th>Task</th><th>Type</th><th>Status</th>';
html += '</tr></thead><tbody>';
tasks.forEach((t, i) => {
const link = t.url ? `<a href="${esc(t.url)}" target="_blank" style="color:var(--cream-light);text-decoration:none;">${esc(t.name)}</a>` : esc(t.name);
html += `<tr>
<td class="task-table__num">${i + 1}</td>
<td class="task-table__title">${link}</td>
<td class="task-table__company">${esc(t.task_type || '-')}</td>
<td class="task-table__status">${statusPill(t.status)}</td>
</tr>`;
});
html += '</tbody></table></div>';
}
html += '</div>';
});
document.getElementById('company-sections').innerHTML = html || '<p style="color:var(--text-muted);">No tasks found.</p>';
}
function groupByCompany(tasks) {
const map = {};
tasks.forEach(t => {
const co = t.custom_fields?.Customer || 'Unassigned';
if (!map[co]) map[co] = { name: co, tasks: [], count: 0 };
map[co].tasks.push(t);
map[co].count++;
});
return Object.values(map).sort((a, b) => b.count - a.count);
}
// --- System Health Tab ---
async function loadHealth() {
const health = _cache.health || await fetchJSON('/system/health');
if (!health) return;
_cache.health = health;
document.getElementById('health-subtitle').textContent =
`Chat model: ${health.chat_model || 'unknown'} | Execution model: ${health.execution_model || 'unknown'}`;
// Stats
let statsHtml = `
<div class="stat-card stat-card--${health.execution_brain ? 'green' : 'red'}">
<div class="stat-card__label">Execution Brain</div>
<div class="stat-card__value">${health.execution_brain ? 'OK' : 'MISSING'}</div>
<div class="stat-card__detail">Claude Code CLI</div>
</div>
<div class="stat-card stat-card--${health.clickup_enabled ? 'green' : 'amber'}">
<div class="stat-card__label">ClickUp</div>
<div class="stat-card__value">${health.clickup_enabled ? 'ON' : 'OFF'}</div>
<div class="stat-card__detail">Task integration</div>
</div>
<div class="stat-card stat-card--green">
<div class="stat-card__label">Drives</div>
<div class="stat-card__value">${health.disks?.length || 0}</div>
<div class="stat-card__detail">Monitored</div>
</div>
`;
document.getElementById('health-stats').innerHTML = statsHtml;
// Disk cards
const diskGrid = document.getElementById('health-disks');
if (health.disks && health.disks.length > 0) {
let html = '';
health.disks.forEach(d => {
const pct = d.percent_free || 0;
const color = pct < 10 ? 'red' : pct < 25 ? 'amber' : 'green';
const statusCls = pct < 10 ? 'error' : pct < 25 ? 'warn' : 'ok';
html += `<div class="health-card">
<div class="health-card__header">
<span class="health-card__title">${esc(d.drive)}</span>
<span class="health-card__status health-card__status--${statusCls}">${pct}% FREE</span>
</div>
<div class="health-bar"><div class="health-bar__fill health-bar__fill--${color}" style="width:${pct}%"></div></div>
<div class="health-card__meta"><span>${d.free_gb} GB / ${d.total_gb} GB</span><span>Threshold: 10%</span></div>
</div>`;
});
diskGrid.innerHTML = html;
} else {
diskGrid.innerHTML = '<p style="color:var(--text-muted);">No disk data available.</p>';
}
}
// --- Agents Tab ---
async function loadAgents() {
const data = _cache.agents || await fetchJSON('/agents');
if (!data || !data.agents) return;
_cache.agents = data;
const grid = document.getElementById('agents-grid');
if (data.agents.length === 0) {
grid.innerHTML = '<p style="color:var(--text-muted);">No agents configured.</p>';
return;
}
let html = '';
data.agents.forEach(a => {
const toolList = a.tools ? a.tools.join(', ') : 'All tools';
html += `<div class="health-card">
<div class="health-card__header">
<span class="health-card__title">${esc(a.display_name || a.name)}</span>
<span class="health-card__status health-card__status--ok">READY</span>
</div>
<div class="health-card__meta" style="margin-top:0.5rem;">
<span>Model: ${esc(a.model || 'default')}</span>
<span>Scope: ${esc(a.memory_scope || 'shared')}</span>
</div>
<div style="margin-top:0.5rem;font-size:0.75rem;color:var(--text-muted);">
Tools: ${esc(toolList)}
</div>
</div>`;
});
grid.innerHTML = html;
}
// --- Notifications Tab ---
async function loadNotifications() {
const data = await fetchJSON('/system/notifications');
if (!data) return;
const container = document.getElementById('notifications-list');
const notes = data.notifications || [];
if (notes.length === 0) {
container.innerHTML = '<p style="padding:1rem;color:var(--text-muted);">No notifications yet.</p>';
return;
}
let html = '<ul class="log-list">';
notes.slice(0, 100).forEach(n => {
const time = n.created_at ? new Date(n.created_at * 1000).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) : '';
const level = (n.level || 'info').toLowerCase();
const typeCls = level === 'error' ? 'error' : level === 'warning' ? 'error' : 'heartbeat';
html += `<li class="log-item">
<span class="log-item__time">${esc(time)}</span>
<span class="log-item__type log-item__type--${typeCls}">${esc(n.level || 'info')}</span>
<span class="log-item__msg">${esc(n.message || '')}</span>
</li>`;
});
html += '</ul>';
container.innerHTML = html;
}
// --- Tab Navigation ---
function switchTab(tabId) {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.sidebar__link').forEach(l => l.classList.remove('active'));
const panel = document.getElementById('tab-' + tabId);
if (panel) panel.classList.add('active');
const btn = document.querySelector('.sidebar__link[data-tab="' + tabId + '"]');
if (btn) btn.classList.add('active');
document.getElementById('sidebar').classList.remove('open');
// Lazy-load tab data
const loaders = {
overview: loadOverview,
linkbuilding: loadLinkBuilding,
pressreleases: loadPressReleases,
bycompany: loadByCompany,
health: loadHealth,
agents: loadAgents,
notifications: loadNotifications,
};
if (loaders[tabId]) loaders[tabId]();
}
document.querySelectorAll('[data-tab]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
switchTab(btn.getAttribute('data-tab'));
});
});
// --- Mobile Sidebar ---
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
}
// --- Clock ---
function updateClock() {
const now = new Date();
const opts = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false };
document.getElementById('clock').textContent = now.toLocaleTimeString('en-GB', opts);
}
updateClock();
setInterval(updateClock, 1000);
// --- Refresh ---
async function refreshAll() {
_cache = {};
await fetchJSON('/cache/clear').catch(() => {});
const activeTab = document.querySelector('.tab-panel.active')?.id?.replace('tab-', '') || 'overview';
switchTab(activeTab);
}
// --- Init ---
updateGreeting();
loadOverview();
// Auto-refresh every 5 minutes
setInterval(() => {
_cache = {};
const activeTab = document.querySelector('.tab-panel.active')?.id?.replace('tab-', '') || 'overview';
switchTab(activeTab);
}, 300000);
</script>
</body>
</html>