Filter LB status cards to relevant statuses and recent tasks

Only show To Do, In Progress, Error, Automation Underway counts.
Exclude tasks due more than 1 month ago. Add "Scheduled next month"
card showing upcoming work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cora-start
PeninsulaInd 2026-02-20 21:39:39 -06:00
parent f9142e6669
commit 6fd565734f
2 changed files with 87 additions and 1 deletions

View File

@ -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 ──

View File

@ -715,6 +715,7 @@ async function loadLinkBuilding() {
<div class="stat-card__detail">${data.companies?.map(c => c.name).join(', ') || 'None'}</div>
</div>
${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 `<div class="stat-card stat-card--blue">
<div class="stat-card__label">Scheduled ${label}</div>
<div class="stat-card__value">${count}</div>
<div class="stat-card__detail">Due next month</div>
</div>`;
}
function statusToCardColor(status) {
const s = (status || '').toLowerCase();
if (s.includes('complete') || s.includes('done') || s.includes('closed')) return 'green';