Skip to content

Commit 7ace481

Browse files
Merge pull request #99 from mrlitong/feature/daily-monthly-token-stats
Add: Daily and monthly token usage views
2 parents 5a19040 + 73c5be4 commit 7ace481

File tree

11 files changed

+1971
-35
lines changed

11 files changed

+1971
-35
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,7 @@ logs/
212212
/src/Functionality_Coverage_Claude-Code-Usage-Monitor_Missing_From_claude_monitor.md
213213
/TODO.md
214214
*Zone.Identifier
215+
216+
# Local linting scripts
217+
lint*.py
218+
lint*.sh

src/claude_monitor/cli/main.py

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
import argparse
44
import contextlib
55
import logging
6+
import signal
67
import sys
8+
import time
79
import traceback
810
from pathlib import Path
911
from typing import Any, Callable, Dict, List, NoReturn, Optional, Union
1012

13+
from rich.console import Console
14+
1115
from claude_monitor import __version__
1216
from claude_monitor.cli.bootstrap import (
1317
ensure_directories,
@@ -17,6 +21,7 @@
1721
)
1822
from claude_monitor.core.plans import Plans, PlanType, get_token_limit
1923
from claude_monitor.core.settings import Settings
24+
from claude_monitor.data.aggregator import UsageAggregator
2025
from claude_monitor.data.analysis import analyze_usage
2126
from claude_monitor.error_handling import report_error
2227
from claude_monitor.monitoring.orchestrator import MonitoringOrchestrator
@@ -29,6 +34,7 @@
2934
)
3035
from claude_monitor.terminal.themes import get_themed_console, print_themed
3136
from claude_monitor.ui.display_controller import DisplayController
37+
from claude_monitor.ui.table_views import TableViewsController
3238

3339
# Type aliases for CLI callbacks
3440
DataUpdateCallback = Callable[[Dict[str, Any]], None]
@@ -103,6 +109,8 @@ def main(argv: Optional[List[str]] = None) -> int:
103109

104110
def _run_monitoring(args: argparse.Namespace) -> None:
105111
"""Main monitoring implementation without facade."""
112+
view_mode = getattr(args, "view", "realtime")
113+
106114
if hasattr(args, "theme") and args.theme:
107115
console = get_themed_console(force_theme=args.theme.lower())
108116
else:
@@ -121,6 +129,11 @@ def _run_monitoring(args: argparse.Namespace) -> None:
121129
logger = logging.getLogger(__name__)
122130
logger.info(f"Using data path: {data_path}")
123131

132+
# Handle different view modes
133+
if view_mode in ["daily", "monthly"]:
134+
_run_table_view(args, data_path, view_mode, console)
135+
return
136+
124137
token_limit: int = _get_initial_token_limit(args, str(data_path))
125138

126139
display_controller = DisplayController()
@@ -151,9 +164,9 @@ def _run_monitoring(args: argparse.Namespace) -> None:
151164
live_display.update(loading_display)
152165

153166
orchestrator = MonitoringOrchestrator(
154-
update_interval=args.refresh_rate
155-
if hasattr(args, "refresh_rate")
156-
else 10,
167+
update_interval=(
168+
args.refresh_rate if hasattr(args, "refresh_rate") else 10
169+
),
157170
data_path=str(data_path),
158171
)
159172
orchestrator.set_args(args)
@@ -214,10 +227,13 @@ def on_session_change(
214227
logger.warning("Timeout waiting for initial data")
215228

216229
# Main loop - live display is already active
217-
while True:
218-
import time
219-
220-
time.sleep(1)
230+
# Use signal.pause() for more efficient waiting
231+
try:
232+
signal.pause()
233+
except AttributeError:
234+
# Fallback for Windows which doesn't support signal.pause()
235+
while True:
236+
time.sleep(1)
221237
finally:
222238
# Stop monitoring first
223239
if "orchestrator" in locals():
@@ -362,5 +378,57 @@ def validate_cli_environment() -> Optional[str]:
362378
return f"Environment validation failed: {e}"
363379

364380

381+
def _run_table_view(
382+
args: argparse.Namespace, data_path: Path, view_mode: str, console: Console
383+
) -> None:
384+
"""Run table view mode (daily/monthly)."""
385+
logger = logging.getLogger(__name__)
386+
387+
try:
388+
# Create aggregator with appropriate mode
389+
aggregator = UsageAggregator(
390+
data_path=str(data_path),
391+
aggregation_mode=view_mode,
392+
timezone=args.timezone,
393+
)
394+
395+
# Create table controller
396+
controller = TableViewsController(console=console)
397+
398+
# Get aggregated data
399+
logger.info(f"Loading {view_mode} usage data...")
400+
aggregated_data = aggregator.aggregate()
401+
402+
if not aggregated_data:
403+
print_themed(f"No usage data found for {view_mode} view", style="warning")
404+
return
405+
406+
# Display the table
407+
controller.display_aggregated_view(
408+
data=aggregated_data,
409+
view_mode=view_mode,
410+
timezone=args.timezone,
411+
plan=args.plan,
412+
token_limit=_get_initial_token_limit(args, data_path),
413+
)
414+
415+
# Wait for user to press Ctrl+C
416+
print_themed("\nPress Ctrl+C to exit", style="info")
417+
try:
418+
# Use signal.pause() for more efficient waiting
419+
try:
420+
signal.pause()
421+
except AttributeError:
422+
# Fallback for Windows which doesn't support signal.pause()
423+
while True:
424+
time.sleep(1)
425+
except KeyboardInterrupt:
426+
print_themed("\nExiting...", style="info")
427+
428+
except Exception as e:
429+
logger.error(f"Error in table view: {e}", exc_info=True)
430+
print_themed(f"Error displaying {view_mode} view: {e}", style="error")
431+
432+
365433
if __name__ == "__main__":
366434
sys.exit(main())

src/claude_monitor/core/settings.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def save(self, settings: "Settings") -> None:
3333
"time_format": settings.time_format,
3434
"refresh_rate": settings.refresh_rate,
3535
"reset_hour": settings.reset_hour,
36+
"view": settings.view,
3637
"timestamp": datetime.now().isoformat(),
3738
}
3839

@@ -103,6 +104,11 @@ class Settings(BaseSettings):
103104
description="Plan type (pro, max5, max20, custom)",
104105
)
105106

107+
view: Literal["realtime", "daily", "monthly", "session"] = Field(
108+
default="realtime",
109+
description="View mode (realtime, daily, monthly, session)",
110+
)
111+
106112
@staticmethod
107113
def _get_system_timezone() -> str:
108114
"""Lazy import to avoid circular dependencies."""
@@ -178,6 +184,20 @@ def validate_plan(cls, v: Any) -> str:
178184
)
179185
return v
180186

187+
@field_validator("view", mode="before")
188+
@classmethod
189+
def validate_view(cls, v: Any) -> str:
190+
"""Validate and normalize view value."""
191+
if isinstance(v, str):
192+
v_lower = v.lower()
193+
valid_views = ["realtime", "daily", "monthly", "session"]
194+
if v_lower in valid_views:
195+
return v_lower
196+
raise ValueError(
197+
f"Invalid view: {v}. Must be one of: {', '.join(valid_views)}"
198+
)
199+
return v
200+
181201
@field_validator("theme", mode="before")
182202
@classmethod
183203
def validate_theme(cls, v: Any) -> str:
@@ -319,6 +339,7 @@ def to_namespace(self) -> argparse.Namespace:
319339
args = argparse.Namespace()
320340

321341
args.plan = self.plan
342+
args.view = self.view
322343
args.timezone = self.timezone
323344
args.theme = self.theme
324345
args.refresh_rate = self.refresh_rate

0 commit comments

Comments
 (0)