607 lines
18 KiB
HTML
607 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>James Dashboard</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: #F8F7F6;
|
|
--bg-card: #FFFFFF;
|
|
--border: #E5E2DE;
|
|
--text: #1C1917;
|
|
--text-muted: #78716C;
|
|
--accent: #B45309;
|
|
--accent-light: #FEF3C7;
|
|
--success: #059669;
|
|
--success-light: #ECFDF5;
|
|
--danger: #DC2626;
|
|
--danger-light: #FEF2F2;
|
|
--info: #6366f1;
|
|
--info-light: #EEF2FF;
|
|
--james: #B45309;
|
|
--johan: #2563EB;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: "Sora", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
line-height: 1.5;
|
|
padding: 24px;
|
|
}
|
|
|
|
.container { max-width: 1400px; margin: 0 auto; }
|
|
|
|
.three-column {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
gap: 24px;
|
|
}
|
|
|
|
@media (max-width: 1100px) {
|
|
.three-column { grid-template-columns: 1fr 1fr; }
|
|
.column-done { grid-column: span 2; }
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
.three-column { grid-template-columns: 1fr; }
|
|
.column-done { grid-column: span 1; }
|
|
}
|
|
|
|
header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
margin-bottom: 24px;
|
|
padding-bottom: 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
h1 { font-size: 1.75rem; font-weight: 700; }
|
|
h1 .accent { color: var(--accent); }
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.updated { font-size: 0.8rem; color: var(--text-muted); }
|
|
|
|
/* Agents Bar */
|
|
.agents-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
padding: 12px 16px;
|
|
background: var(--bg-card);
|
|
border-radius: 10px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.agents-label {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.agents-links {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.agent-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 12px;
|
|
background: var(--bg);
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
color: var(--text);
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
transition: background 0.15s, transform 0.1s;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.agent-link:hover {
|
|
background: var(--accent-light);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.agent-link.default {
|
|
background: var(--accent-light);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.agent-emoji { font-size: 1rem; }
|
|
|
|
.section { margin-bottom: 20px; }
|
|
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.section-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.section-indicator {
|
|
width: 4px;
|
|
height: 20px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.section-indicator.alerts { background: var(--danger); }
|
|
.section-indicator.james { background: var(--james); }
|
|
.section-indicator.johan { background: var(--johan); }
|
|
.section-indicator.progress { background: var(--info); }
|
|
.section-indicator.done { background: var(--success); }
|
|
|
|
.section-title {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.section-count {
|
|
font-size: 0.7rem;
|
|
color: var(--text-muted);
|
|
background: var(--bg);
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.card {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
border: 1px solid var(--border);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-collapsed .card-content { display: none; }
|
|
.card-collapsed .collapse-toggle::after { content: '▶'; }
|
|
|
|
.collapse-toggle {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
font-size: 0.65rem;
|
|
color: var(--text-muted);
|
|
}
|
|
.collapse-toggle::after { content: '▼'; margin-left: 4px; }
|
|
|
|
/* Alerts/News */
|
|
.alert-item {
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.alert-item:last-child { border-bottom: none; }
|
|
|
|
.alert-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.alert-title {
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.alert-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.alert-dot.error { background: var(--danger); }
|
|
.alert-dot.warning { background: var(--accent); }
|
|
.alert-dot.success { background: var(--success); }
|
|
.alert-dot.info { background: var(--info); }
|
|
|
|
.alert-time {
|
|
font-size: 0.75rem;
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.alert-body {
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
margin-left: 16px;
|
|
}
|
|
|
|
.alert-source {
|
|
font-size: 0.7rem;
|
|
color: var(--text-muted);
|
|
margin-left: 16px;
|
|
margin-top: 4px;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Tasks */
|
|
.task-item {
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.task-item:last-child { border-bottom: none; }
|
|
|
|
.task-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
}
|
|
|
|
.task-title {
|
|
font-weight: 500;
|
|
font-size: 0.85rem;
|
|
flex: 1;
|
|
}
|
|
|
|
.task-domain {
|
|
font-size: 0.7rem;
|
|
color: var(--text-muted);
|
|
background: var(--bg);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.task-notes {
|
|
font-size: 0.75rem;
|
|
color: var(--text-muted);
|
|
margin-top: 6px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.task-meta {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.65rem;
|
|
font-weight: 500;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.badge-high { background: var(--danger-light); color: var(--danger); }
|
|
.badge-medium { background: var(--accent-light); color: var(--accent); }
|
|
.badge-low { background: var(--bg); color: var(--text-muted); }
|
|
.badge-done { background: var(--success-light); color: var(--success); }
|
|
.badge-in-progress { background: var(--info-light); color: var(--info); }
|
|
.badge-pending { background: var(--bg); color: var(--text-muted); }
|
|
|
|
.empty {
|
|
padding: 24px;
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
/* Latest Briefing Banner */
|
|
.briefing-banner {
|
|
background: linear-gradient(135deg, #8B5CF6 0%, #6366f1 100%);
|
|
color: white;
|
|
padding: 16px 20px;
|
|
border-radius: 12px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.briefing-banner-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.briefing-banner-title {
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.briefing-banner-time {
|
|
font-size: 0.75rem;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.briefing-banner-summary {
|
|
font-size: 0.8rem;
|
|
opacity: 0.9;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
body { padding: 16px; }
|
|
h1 { font-size: 1.25rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1><span class="accent">James</span> Dashboard</h1>
|
|
<div class="header-right">
|
|
<span class="updated" id="last-updated">Loading...</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Latest Briefing Banner -->
|
|
<div class="briefing-banner" id="briefing-banner">
|
|
<div class="briefing-banner-header">
|
|
<span class="briefing-banner-title">Loading briefing...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agents Quick Links -->
|
|
<div class="agents-bar" id="agents-bar">
|
|
<span class="agents-label">Agents:</span>
|
|
<div class="agents-links" id="agents-links">
|
|
<span class="loading">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="three-column">
|
|
<!-- Column 1: Alerts + James Tasks -->
|
|
<div class="column">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<div class="section-header-left">
|
|
<div class="section-indicator alerts"></div>
|
|
<span class="section-title">Alerts</span>
|
|
</div>
|
|
<span class="section-count" id="alerts-count">0</span>
|
|
</div>
|
|
<div class="card" id="alerts-container">
|
|
<div class="empty">No alerts</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<div class="section-header-left">
|
|
<div class="section-indicator james"></div>
|
|
<span class="section-title">James — Pending</span>
|
|
</div>
|
|
<span class="section-count" id="james-pending-count">0</span>
|
|
</div>
|
|
<div class="card" id="james-pending-container">
|
|
<div class="empty">No pending tasks</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Column 2: Johan Tasks + In Progress -->
|
|
<div class="column">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<div class="section-header-left">
|
|
<div class="section-indicator johan"></div>
|
|
<span class="section-title">Johan — Pending</span>
|
|
</div>
|
|
<span class="section-count" id="johan-pending-count">0</span>
|
|
</div>
|
|
<div class="card" id="johan-pending-container">
|
|
<div class="empty">No pending tasks</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<div class="section-header-left">
|
|
<div class="section-indicator progress"></div>
|
|
<span class="section-title">In Progress</span>
|
|
</div>
|
|
<span class="section-count" id="progress-count">0</span>
|
|
</div>
|
|
<div class="card" id="progress-container">
|
|
<div class="empty">Nothing in progress</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Column 3: Done -->
|
|
<div class="column column-done">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<div class="section-header-left">
|
|
<div class="section-indicator done"></div>
|
|
<span class="section-title">Done</span>
|
|
<span class="collapse-toggle" onclick="toggleDone()"></span>
|
|
</div>
|
|
<span class="section-count" id="done-count">0</span>
|
|
</div>
|
|
<div class="card" id="done-container">
|
|
<div class="empty">No completed tasks</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let doneCollapsed = false;
|
|
|
|
function toggleDone() {
|
|
doneCollapsed = !doneCollapsed;
|
|
document.getElementById('done-container').parentElement.classList.toggle('card-collapsed', doneCollapsed);
|
|
}
|
|
|
|
function formatTime(iso) {
|
|
const d = new Date(iso);
|
|
const now = new Date();
|
|
const diff = now - d;
|
|
|
|
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
|
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
|
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
}
|
|
|
|
function priorityBadge(priority) {
|
|
return `<span class="badge badge-${priority}">${priority}</span>`;
|
|
}
|
|
|
|
function ownerBadge(owner) {
|
|
const color = owner === 'james' ? 'var(--james)' : 'var(--johan)';
|
|
return `<span class="badge" style="background: ${color}20; color: ${color}">${owner}</span>`;
|
|
}
|
|
|
|
function renderTask(t, showOwner = false) {
|
|
return `
|
|
<div class="task-item">
|
|
<div class="task-header">
|
|
<div class="task-title">${t.title}</div>
|
|
${t.domain ? `<span class="task-domain">${t.domain}</span>` : ''}
|
|
</div>
|
|
${t.notes ? `<div class="task-notes">${t.notes}</div>` : ''}
|
|
<div class="task-meta">
|
|
${priorityBadge(t.priority)}
|
|
${showOwner ? ownerBadge(t.owner) : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderAlert(n) {
|
|
return `
|
|
<div class="alert-item">
|
|
<div class="alert-header">
|
|
<div class="alert-title">
|
|
<span class="alert-dot ${n.type}"></span>
|
|
${n.title}
|
|
</div>
|
|
<span class="alert-time">${formatTime(n.timestamp)}</span>
|
|
</div>
|
|
<div class="alert-body">${n.body}</div>
|
|
${n.source ? `<div class="alert-source">— ${n.source}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function loadData() {
|
|
try {
|
|
const [tasksRes, newsRes, briefingsRes] = await Promise.all([
|
|
fetch('/api/tasks'),
|
|
fetch('/api/news'),
|
|
fetch('/api/briefings')
|
|
]);
|
|
|
|
const tasksData = await tasksRes.json();
|
|
const newsData = await newsRes.json();
|
|
const briefingsData = await briefingsRes.json();
|
|
|
|
const tasks = tasksData.tasks || [];
|
|
const news = newsData.items || [];
|
|
const briefings = briefingsData.briefings || [];
|
|
|
|
// Filter tasks by owner and status
|
|
const jamesPending = tasks.filter(t => t.owner === 'james' && t.status === 'pending');
|
|
const johanPending = tasks.filter(t => t.owner === 'johan' && t.status === 'pending');
|
|
const inProgress = tasks.filter(t => t.status === 'in-progress');
|
|
const done = tasks.filter(t => t.status === 'done').slice(0, 10); // Last 10
|
|
|
|
// Filter news to last 24h only
|
|
const oneDayAgo = Date.now() - 86400000;
|
|
const recentNews = news.filter(n => new Date(n.timestamp).getTime() > oneDayAgo);
|
|
|
|
// Render alerts
|
|
document.getElementById('alerts-count').textContent = recentNews.length;
|
|
document.getElementById('alerts-container').innerHTML = recentNews.length
|
|
? recentNews.map(renderAlert).join('')
|
|
: '<div class="empty">No alerts in last 24h</div>';
|
|
|
|
// Render task groups
|
|
document.getElementById('james-pending-count').textContent = jamesPending.length;
|
|
document.getElementById('james-pending-container').innerHTML = jamesPending.length
|
|
? jamesPending.map(t => renderTask(t)).join('')
|
|
: '<div class="empty">No pending tasks</div>';
|
|
|
|
document.getElementById('johan-pending-count').textContent = johanPending.length;
|
|
document.getElementById('johan-pending-container').innerHTML = johanPending.length
|
|
? johanPending.map(t => renderTask(t)).join('')
|
|
: '<div class="empty">No pending tasks</div>';
|
|
|
|
document.getElementById('progress-count').textContent = inProgress.length;
|
|
document.getElementById('progress-container').innerHTML = inProgress.length
|
|
? inProgress.map(t => renderTask(t, true)).join('')
|
|
: '<div class="empty">Nothing in progress</div>';
|
|
|
|
document.getElementById('done-count').textContent = done.length;
|
|
document.getElementById('done-container').innerHTML = done.length
|
|
? done.map(t => renderTask(t, true)).join('')
|
|
: '<div class="empty">No completed tasks</div>';
|
|
|
|
// Render latest briefing banner
|
|
if (briefings.length > 0) {
|
|
const latest = briefings[0];
|
|
document.getElementById('briefing-banner').innerHTML = `
|
|
<div class="briefing-banner-header">
|
|
<span class="briefing-banner-title">📋 ${latest.title}</span>
|
|
<span class="briefing-banner-time">${formatTime(latest.created_at)}</span>
|
|
</div>
|
|
<div class="briefing-banner-summary">${latest.summary || latest.weather + ' • ' + (latest.markets || '').substring(0, 80) + '...'}</div>
|
|
`;
|
|
}
|
|
|
|
document.getElementById('last-updated').textContent = 'Updated ' + formatTime(new Date().toISOString());
|
|
|
|
} catch (e) {
|
|
console.error('Failed to load data:', e);
|
|
}
|
|
}
|
|
|
|
async function loadAgents() {
|
|
try {
|
|
const res = await fetch('/api/agents');
|
|
const data = await res.json();
|
|
const agents = data.agents || [];
|
|
|
|
if (agents.length === 0) {
|
|
document.getElementById('agents-links').innerHTML = '<span class="loading">No agents configured</span>';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('agents-links').innerHTML = agents.map(a => `
|
|
<a href="${a.url}" target="_blank" class="agent-link ${a.default ? 'default' : ''}">
|
|
<span class="agent-emoji">${a.emoji}</span>
|
|
<span>${a.name}</span>
|
|
</a>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load agents:', e);
|
|
document.getElementById('agents-links').innerHTML = '<span class="loading">Failed to load</span>';
|
|
}
|
|
}
|
|
|
|
loadAgents();
|
|
loadData();
|
|
setInterval(loadData, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|