diff --git a/cheddahbot/api.py b/cheddahbot/api.py index 0718aa2..b5c5d1f 100644 --- a/cheddahbot/api.py +++ b/cheddahbot/api.py @@ -6,11 +6,13 @@ All ClickUp data is cached for 5 minutes to avoid hammering the API. from __future__ import annotations +import calendar import json import logging import shutil import threading import time +from datetime import UTC, datetime from typing import TYPE_CHECKING from fastapi import APIRouter @@ -242,7 +244,8 @@ async def get_link_building_tasks(): {"name": name, "tasks": tasks, "count": len(tasks)} for name, tasks in sorted(by_company.items(), key=lambda x: -len(x[1])) ], - "status_counts": _count_statuses(active_lb), + "status_counts": _count_lb_statuses(active_lb), + "next_month_count": _count_next_month(active_lb), } _set_cached("lb_tasks", result) return result @@ -289,6 +292,76 @@ def _count_statuses(tasks: list[dict]) -> dict[str, int]: return counts +def _count_lb_statuses(tasks: list[dict]) -> dict[str, int]: + """Count only relevant statuses for tasks due in the current or previous month.""" + now = datetime.now(UTC) + # First day of current month + cur_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + # First day of previous month + if now.month == 1: + prev_start = cur_start.replace(year=now.year - 1, month=12) + else: + prev_start = cur_start.replace(month=now.month - 1) + + prev_start_ms = int(prev_start.timestamp() * 1000) + + allowed = {"to do", "in progress", "error", "automation underway"} + counts: dict[str, int] = {} + for t in tasks: + s = t.get("status", "unknown") + if s not in allowed: + continue + due = t.get("due_date") + if not due: + # No due date — still count it (it's active work) + counts[s] = counts.get(s, 0) + 1 + continue + try: + if int(due) >= prev_start_ms: + counts[s] = counts.get(s, 0) + 1 + except (ValueError, TypeError): + pass + return counts + + +def _count_next_month(tasks: list[dict]) -> int: + """Count tasks due next month.""" + now = datetime.now(UTC) + if now.month == 12: + next_start = now.replace( + year=now.year + 1, month=1, day=1, + hour=0, minute=0, second=0, microsecond=0, + ) + next_end_month = 1 + next_end_year = now.year + 1 + else: + next_start = now.replace( + month=now.month + 1, day=1, + hour=0, minute=0, second=0, microsecond=0, + ) + next_end_month = now.month + 1 + next_end_year = now.year + + last_day = calendar.monthrange(next_end_year, next_end_month)[1] + next_end = next_start.replace(day=last_day, hour=23, minute=59, second=59) + + start_ms = int(next_start.timestamp() * 1000) + end_ms = int(next_end.timestamp() * 1000) + + count = 0 + for t in tasks: + due = t.get("due_date") + if not due: + continue + try: + d = int(due) + if start_ms <= d <= end_ms: + count += 1 + except (ValueError, TypeError): + pass + return count + + # ── Agents ── diff --git a/dashboard/index.html b/dashboard/index.html index 29c334b..3ed3169 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -715,6 +715,7 @@ async function loadLinkBuilding() {
${data.companies?.map(c => c.name).join(', ') || 'None'}
${renderStatusCountCards(data.status_counts)} + ${renderNextMonthCard(data.next_month_count)} `; document.getElementById('lb-stats').innerHTML = statsHtml; @@ -776,6 +777,18 @@ function renderStatusCountCards(counts) { return html; } +function renderNextMonthCard(count) { + if (count == null) return ''; + const d = new Date(); + d.setMonth(d.getMonth() + 1); + const label = d.toLocaleDateString('en-US', {month:'short', year:'2-digit'}); + return `
+
Scheduled ${label}
+
${count}
+
Due next month
+
`; +} + function statusToCardColor(status) { const s = (status || '').toLowerCase(); if (s.includes('complete') || s.includes('done') || s.includes('closed')) return 'green';