817 lines
29 KiB
HTML
817 lines
29 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">☰</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">↻</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">☼</span>
|
|
Overview
|
|
</button>
|
|
<button class="sidebar__link" data-tab="linkbuilding">
|
|
<span class="icon">🔗</span>
|
|
Link Building
|
|
<span class="badge" id="badge-lb">-</span>
|
|
</button>
|
|
<button class="sidebar__link" data-tab="pressreleases">
|
|
<span class="icon">📰</span>
|
|
Press Releases
|
|
<span class="badge" id="badge-pr">-</span>
|
|
</button>
|
|
<button class="sidebar__link" data-tab="bycompany">
|
|
<span class="icon">🏢</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">⚙</span>
|
|
System Health
|
|
</button>
|
|
<button class="sidebar__link" data-tab="agents">
|
|
<span class="icon">🤖</span>
|
|
Agents
|
|
</button>
|
|
<button class="sidebar__link" data-tab="notifications">
|
|
<span class="icon">📜</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">💬</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">
|
|
“Continuous effort — not strength or intelligence — is the key to unlocking our potential.”
|
|
<cite>— 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>
|
|
|
|
<!-- Top Link Building Tasks -->
|
|
<div class="section section--tight">
|
|
<div class="section__header">
|
|
<h2 class="section__title"><span class="icon">🔗</span> Link Building Tasks</h2>
|
|
<span class="section__badge"><a href="#" class="section__link" data-tab="linkbuilding">View all</a></span>
|
|
</div>
|
|
<div class="task-table-wrap task-table-wrap--compact" id="overview-lb-table">
|
|
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Press Release Tasks -->
|
|
<div class="section section--tight">
|
|
<div class="section__header">
|
|
<h2 class="section__title"><span class="icon">📰</span> Press Release Tasks</h2>
|
|
<span class="section__badge"><a href="#" class="section__link" data-tab="pressreleases">View all</a></span>
|
|
</div>
|
|
<div id="overview-pr-cards">
|
|
<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>
|
|
|
|
<!-- Company Breakdown -->
|
|
<div class="section">
|
|
<div class="section__header">
|
|
<h2 class="section__title"><span class="icon">🏢</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">📋</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">📰</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">💾</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, lb, pr, agents, health] = await Promise.all([
|
|
fetchJSON('/tasks'),
|
|
fetchJSON('/tasks/link-building'),
|
|
fetchJSON('/tasks/press-releases'),
|
|
fetchJSON('/agents'),
|
|
fetchJSON('/system/health'),
|
|
]);
|
|
|
|
// Cache for other tabs
|
|
_cache.tasks = tasks;
|
|
_cache.lb = lb;
|
|
_cache.pr = pr;
|
|
_cache.agents = agents;
|
|
_cache.health = health;
|
|
|
|
// Stats
|
|
if (tasks) {
|
|
document.getElementById('stat-total').textContent = tasks.count || 0;
|
|
document.getElementById('stat-total-detail').textContent = `ClickUp tasks`;
|
|
}
|
|
if (lb) {
|
|
document.getElementById('stat-lb').textContent = lb.total || 0;
|
|
document.getElementById('stat-lb-detail').textContent =
|
|
`${lb.companies ? lb.companies.length : 0} companies`;
|
|
document.getElementById('badge-lb').textContent = lb.total || 0;
|
|
}
|
|
if (pr) {
|
|
document.getElementById('stat-pr').textContent = pr.total || 0;
|
|
document.getElementById('stat-pr-detail').textContent =
|
|
`${pr.companies ? pr.companies.length : 0} companies`;
|
|
document.getElementById('badge-pr').textContent = pr.total || 0;
|
|
}
|
|
if (tasks && tasks.tasks) {
|
|
const companies = new Set(tasks.tasks.map(t => t.custom_fields?.Client || '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';
|
|
}
|
|
|
|
// Top LB tasks (first 5)
|
|
if (lb && lb.companies) {
|
|
const allTasks = lb.companies.flatMap(c => c.tasks);
|
|
renderTaskTable('overview-lb-table', allTasks.slice(0, 8), true);
|
|
}
|
|
|
|
// Top PR tasks
|
|
if (pr && pr.companies) {
|
|
const allPR = pr.companies.flatMap(c => c.tasks);
|
|
renderPRCards('overview-pr-cards', allPR.slice(0, 3));
|
|
}
|
|
|
|
// 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?.Client || '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 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?.Client || '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;
|
|
|
|
// 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">🏢</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?.Client || '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>
|