feat: complete calendar integration + planner widget + 298 new KB intents

- Add calendar sync route to api.php (/api/calendar → calendar_sync.php)
- Add CALENDAR SYNC tab to admin with feed CRUD (add/edit/delete Google/ICS feeds)
- Add cal_sync_now action to admin for on-demand iCloud/Google sync
- Add cron: calendar_sync.php every 15 min (iCloud CalDAV + ICS feeds)
- Add PLANNER mini panel to index.html (left panel, shows today tasks + appointments)
- Update loadPlannerSummary() to render tasks/appts with priority dots and times
- Seed 298 new KB intents across 37 categories: science, history, tech, geography,
  math, health, food, space, philosophy, psychology, sports, music, film, travel,
  language, literature, finance, productivity, nature, facts, home automation,
  architecture, geopolitics, and more — 543 total intents
This commit is contained in:
2026-05-31 22:47:35 +00:00
parent 1c7a42f68b
commit 5d5eb2fdac
4 changed files with 473 additions and 9 deletions
+64 -9
View File
@@ -388,6 +388,16 @@ body::after{
}
#contextClear:hover{color:var(--red)}
/* Planner mini panel */
#plannerMiniPanel .task-item{display:flex;align-items:center;gap:6px;padding:2px 0;border-bottom:1px solid rgba(0,212,255,0.06);cursor:default}
#plannerMiniPanel .task-item:last-child{border-bottom:none}
#plannerMiniPanel .pri-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
#plannerMiniPanel .pri-urgent{background:#f44}
#plannerMiniPanel .pri-high{background:#fa0}
#plannerMiniPanel .pri-normal{background:var(--cyan)}
#plannerMiniPanel .pri-low{background:var(--text-dim)}
#plannerMiniPanel .appt-row{color:var(--text-dim);font-size:0.58rem;padding:2px 0;display:flex;gap:6px;border-bottom:1px solid rgba(0,212,255,0.04)}
#plannerMiniPanel .appt-time{color:var(--cyan);min-width:42px}
/* Clickable panel items */
.vm-card{cursor:pointer;transition:background 0.15s,border-color 0.15s}
.vm-card:hover{background:rgba(0,212,255,0.07);border-color:rgba(0,212,255,0.4)}
@@ -718,6 +728,16 @@ body::after{
</div>
</div>
<!-- PLANNER MINI PANEL -->
<div id="plannerMiniPanel" class="panel" style="flex:0 0 auto;max-height:220px;overflow:hidden;display:flex;flex-direction:column">
<div class="panel-title" style="display:flex;align-items:center;gap:8px">
PLANNER
<span id="planner-badge" style="font-size:0.55rem;color:var(--cyan);margin-left:auto"></span>
<a href="/admin" target="_blank" style="font-size:0.55rem;color:var(--text-dim);text-decoration:none;letter-spacing:1px">ADMIN ↗</a>
</div>
<div id="planner-tasks" style="overflow-y:auto;flex:1;font-size:0.62rem;line-height:1.7"></div>
</div>
<!-- CENTER: Arc Reactor + Chat -->
<div id="centerPanel">
<div id="arcReactor">
@@ -1543,15 +1563,50 @@ async function loadPlannerSummary() {
const d = await api('planner/today');
const el = document.getElementById('tb-planner');
const tx = document.getElementById('tb-planner-text');
if (!el || !tx) return;
const tasksDue = (d.tasks_today || []).length + (d.tasks_overdue || []).length;
const appts = (d.appts_today || []).length;
if (!tasksDue && !appts) { el.style.display = 'none'; return; }
const parts = [];
if (tasksDue) parts.push(tasksDue + ' TASK' + (tasksDue > 1 ? 'S' : ''));
if (appts) parts.push(appts + ' APPT' + (appts > 1 ? 'S' : ''));
tx.textContent = parts.join(' · ');
el.style.display = '';
if (el && tx) {
const tasksDue = (d.tasks_today || []).length + (d.tasks_overdue || []).length;
const appts = (d.appts_today || []).length;
if (!tasksDue && !appts) { el.style.display = 'none'; }
else {
const parts = [];
if (tasksDue) parts.push(tasksDue + ' TASK' + (tasksDue > 1 ? 'S' : ''));
if (appts) parts.push(appts + ' APPT' + (appts > 1 ? 'S' : ''));
tx.textContent = parts.join(' · ');
el.style.display = '';
}
}
// Render planner mini panel
const pEl = document.getElementById('planner-tasks');
const badge = document.getElementById('planner-badge');
if (!pEl) return;
const priClass = {urgent:'pri-urgent',high:'pri-high',normal:'pri-normal',low:'pri-low'};
const fmtTime = s => { if(!s) return ''; const d=new Date(s); return d.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true}); };
const fmtDate = s => { if(!s) return ''; const d=new Date(s+'T00:00:00'); return d.toLocaleDateString('en-US',{month:'short',day:'numeric'}); };
const tasks = [...(d.tasks_overdue||[]).map(t=>({...t,_overdue:true})), ...(d.tasks_today||[])];
const appts = d.appts_today || [];
let html = '';
if (!tasks.length && !appts.length) {
html = '<div style="color:var(--text-dim);font-size:0.6rem;padding:4px 0">No tasks or appointments today.</div>';
} else {
if (appts.length) {
html += '<div style="color:var(--cyan);font-size:0.55rem;letter-spacing:2px;margin-bottom:3px">TODAY'S SCHEDULE</div>';
html += appts.map(a => `<div class="appt-row"><span class="appt-time">${fmtTime(a.start_at)}</span><span>${a.title}</span>${a.location?'<span style="color:var(--text-dim);font-size:0.55rem"> · '+a.location+'</span>':''}</div>`).join('');
}
if (tasks.length) {
html += '<div style="color:var(--cyan);font-size:0.55rem;letter-spacing:2px;margin:5px 0 3px">TASKS DUE</div>';
html += tasks.map(t => `<div class="task-item"><span class="pri-dot ${priClass[t.priority]||'pri-normal'}"></span><span style="flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${t.title}</span>${t._overdue?'<span style="color:#f66;font-size:0.55rem">OVERDUE</span>':''}</div>`).join('');
}
if (d.pending_count > tasks.length) {
html += `<div style="color:var(--text-dim);font-size:0.55rem;padding:3px 0">${d.pending_count} pending total</div>`;
}
}
pEl.innerHTML = html;
const total = tasks.length + appts.length;
if (badge) badge.textContent = total ? total + ' TODAY' : '';
}
// ── ALERTS ────────────────────────────────────────────────────────────