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
parent
f9142e6669
commit
6fd565734f
|
|
@ -6,11 +6,13 @@ All ClickUp data is cached for 5 minutes to avoid hammering the API.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import calendar
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from datetime import UTC, datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
@ -242,7 +244,8 @@ async def get_link_building_tasks():
|
||||||
{"name": name, "tasks": tasks, "count": len(tasks)}
|
{"name": name, "tasks": tasks, "count": len(tasks)}
|
||||||
for name, tasks in sorted(by_company.items(), key=lambda x: -len(x[1]))
|
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)
|
_set_cached("lb_tasks", result)
|
||||||
return result
|
return result
|
||||||
|
|
@ -289,6 +292,76 @@ def _count_statuses(tasks: list[dict]) -> dict[str, int]:
|
||||||
return counts
|
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 ──
|
# ── Agents ──
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -715,6 +715,7 @@ async function loadLinkBuilding() {
|
||||||
<div class="stat-card__detail">${data.companies?.map(c => c.name).join(', ') || 'None'}</div>
|
<div class="stat-card__detail">${data.companies?.map(c => c.name).join(', ') || 'None'}</div>
|
||||||
</div>
|
</div>
|
||||||
${renderStatusCountCards(data.status_counts)}
|
${renderStatusCountCards(data.status_counts)}
|
||||||
|
${renderNextMonthCard(data.next_month_count)}
|
||||||
`;
|
`;
|
||||||
document.getElementById('lb-stats').innerHTML = statsHtml;
|
document.getElementById('lb-stats').innerHTML = statsHtml;
|
||||||
|
|
||||||
|
|
@ -776,6 +777,18 @@ function renderStatusCountCards(counts) {
|
||||||
return html;
|
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) {
|
function statusToCardColor(status) {
|
||||||
const s = (status || '').toLowerCase();
|
const s = (status || '').toLowerCase();
|
||||||
if (s.includes('complete') || s.includes('done') || s.includes('closed')) return 'green';
|
if (s.includes('complete') || s.includes('done') || s.includes('closed')) return 'green';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue