日別集計に期間の任意指定とプロジェクト除外を追加
/api/daily に from / to / exclude を追加。from/to があれば days より優先し、 その範囲を 0 埋めで返す。exclude のプロジェクトは集計対象から外す (横断モードのみ送信)。CSV も同じ条件で出力される。 UI は日数ボタンの隣に日付入力を置き、凡例のクリックで除外・「除外中」から クリックで復帰できるようにした。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+46
-15
@@ -25,7 +25,7 @@ import socket
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
@@ -495,10 +495,12 @@ class Store:
|
||||
out.sort(key=lambda s: s["startEpoch"] or 0, reverse=True)
|
||||
return out
|
||||
|
||||
def daily_stats(self, project_id=None, days=30):
|
||||
def daily_stats(self, project_id=None, days=30, start=None, end=None, exclude=()):
|
||||
"""ターンをローカル日付で集計する。project_id=None なら全プロジェクト。
|
||||
|
||||
days>0 なら「今日から遡って days 日」、それ以外は全期間。
|
||||
期間は start/end (date) を渡せばその範囲、無ければ days>0 で「今日から
|
||||
遡って days 日」、days<=0 なら全期間。exclude に入れたプロジェクトは
|
||||
集計に含めない (横断モード用)。
|
||||
活動のない日も 0 の行として埋めるので、そのまま横軸に使える。
|
||||
"""
|
||||
paths = {p["id"]: p["path"] for p in self.projects()}
|
||||
@@ -507,10 +509,16 @@ class Store:
|
||||
raise FileNotFoundError(project_id)
|
||||
targets = [project_id]
|
||||
else:
|
||||
targets = list(paths)
|
||||
targets = [pid for pid in paths if pid not in exclude]
|
||||
|
||||
today = datetime.now().date()
|
||||
cutoff = today - timedelta(days=days - 1) if days and days > 0 else None
|
||||
if start or end:
|
||||
lo, hi = start, end or today
|
||||
if lo and hi < lo:
|
||||
lo, hi = hi, lo
|
||||
else:
|
||||
lo = today - timedelta(days=days - 1) if days and days > 0 else None
|
||||
hi = None # 上限なし (今日 かデータの末尾まで)
|
||||
|
||||
per_day = {}
|
||||
per_project = {}
|
||||
@@ -522,7 +530,7 @@ class Store:
|
||||
if turn["ts"] is None:
|
||||
continue
|
||||
day = datetime.fromtimestamp(turn["ts"]).date()
|
||||
if cutoff and day < cutoff:
|
||||
if (lo and day < lo) or (hi and day > hi):
|
||||
continue
|
||||
dur = turn["durMs"] or 0
|
||||
counted = 1 if turn["kind"] in ("prompt", "command") else 0
|
||||
@@ -538,16 +546,16 @@ class Store:
|
||||
seen.add(session["id"])
|
||||
|
||||
if per_day:
|
||||
start = cutoff or min(per_day)
|
||||
end = max(max(per_day), today)
|
||||
elif cutoff:
|
||||
start, end = cutoff, today # データなしでも指定範囲は 0 で埋める
|
||||
first = lo or min(per_day)
|
||||
last = hi or max(max(per_day), today)
|
||||
elif lo or hi:
|
||||
first, last = (lo or hi), (hi or today) # データなしでも指定範囲は 0 で埋める
|
||||
else:
|
||||
start = end = None
|
||||
first = last = None
|
||||
|
||||
rows = []
|
||||
cur = start
|
||||
while cur and cur <= end:
|
||||
cur = first
|
||||
while cur and cur <= last:
|
||||
bucket = per_day.get(cur)
|
||||
rows.append(
|
||||
{
|
||||
@@ -577,7 +585,11 @@ class Store:
|
||||
busiest = max(rows, key=lambda r: r["activeMs"], default=None)
|
||||
return {
|
||||
"scope": project_id or "",
|
||||
"rangeDays": days if days and days > 0 else 0,
|
||||
"rangeDays": 0 if (start or end) else (days if days and days > 0 else 0),
|
||||
"custom": bool(start or end),
|
||||
"from": first.isoformat() if first else None,
|
||||
"to": last.isoformat() if last else None,
|
||||
"excluded": sorted(exclude),
|
||||
"days": rows,
|
||||
"projects": projects,
|
||||
"totals": {
|
||||
@@ -794,6 +806,19 @@ def parse_days(value):
|
||||
return max(0, min(days, 3650))
|
||||
|
||||
|
||||
def parse_date(value):
|
||||
"""?from= / ?to= の解釈。'YYYY-MM-DD' 以外は None (指定なし扱い)。"""
|
||||
try:
|
||||
return date.fromisoformat(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def parse_ids(value):
|
||||
"""?exclude= の解釈。カンマ区切りのプロジェクト ID。"""
|
||||
return {part for part in (value or "").split(",") if part}
|
||||
|
||||
|
||||
def sessions_csv(sessions, with_project=False):
|
||||
buf = io.StringIO(newline="")
|
||||
writer = csv.writer(buf)
|
||||
@@ -890,7 +915,13 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return self.send_json({"project": "", "sessions": sessions})
|
||||
if route == "/api/daily":
|
||||
project = query.get("project", [""])[0]
|
||||
data = store.daily_stats(project or None, parse_days(query.get("days", ["30"])[0]))
|
||||
data = store.daily_stats(
|
||||
project or None,
|
||||
parse_days(query.get("days", ["30"])[0]),
|
||||
parse_date(query.get("from", [""])[0]),
|
||||
parse_date(query.get("to", [""])[0]),
|
||||
parse_ids(query.get("exclude", [""])[0]),
|
||||
)
|
||||
if query.get("format", [""])[0] == "csv":
|
||||
return self.send_text(daily_csv(data), "text/csv", f"{project or 'all'}-daily.csv")
|
||||
return self.send_json(data)
|
||||
|
||||
Reference in New Issue
Block a user