codex-stats 1.4.2__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_stats-1.5.0/PKG-INFO +111 -0
- codex_stats-1.5.0/README.md +84 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/pyproject.toml +1 -1
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/__init__.py +1 -1
- codex_stats-1.5.0/src/codex_stats/cli.py +263 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/display.py +757 -41
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/models.py +44 -0
- codex_stats-1.5.0/src/codex_stats.egg-info/PKG-INFO +111 -0
- codex_stats-1.5.0/tests/test_cli.py +50 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/tests/test_metrics.py +84 -4
- codex_stats-1.4.2/PKG-INFO +0 -308
- codex_stats-1.4.2/README.md +0 -281
- codex_stats-1.4.2/src/codex_stats/cli.py +0 -667
- codex_stats-1.4.2/src/codex_stats.egg-info/PKG-INFO +0 -308
- codex_stats-1.4.2/tests/test_cli.py +0 -227
- {codex_stats-1.4.2 → codex_stats-1.5.0}/LICENSE +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/setup.cfg +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/__main__.py +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/completions.py +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/config.py +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/ingest.py +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/metrics.py +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/otel.py +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/transfer.py +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/watch_state.py +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/SOURCES.txt +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/dependency_links.txt +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/entry_points.txt +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/requires.txt +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/top_level.txt +0 -0
- {codex_stats-1.4.2 → codex_stats-1.5.0}/tests/test_config.py +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codex-stats
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Local AI usage observability CLI for Codex sessions.
|
|
5
|
+
Author: Vivek
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vivek378521/codex-stats
|
|
8
|
+
Project-URL: Repository, https://github.com/vivek378521/codex-stats
|
|
9
|
+
Project-URL: Issues, https://github.com/vivek378521/codex-stats/issues
|
|
10
|
+
Keywords: codex,cli,analytics,sqlite,usage
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# codex-stats
|
|
29
|
+
|
|
30
|
+
`codex-stats` is a local analytics tool for Codex usage.
|
|
31
|
+
|
|
32
|
+
It reads your local Codex state from `~/.codex` and surfaces:
|
|
33
|
+
|
|
34
|
+
- a browser dashboard with day, week, month, and all-time tabs
|
|
35
|
+
- model and project breakdowns
|
|
36
|
+
- recent session history
|
|
37
|
+
- estimated token-based cost
|
|
38
|
+
- anomaly-aware usage insights and recommendations
|
|
39
|
+
- export for cross-device snapshots
|
|
40
|
+
- shareable SVG cards and browser PDF export from the dashboard
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pipx install codex-stats
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or with `pip`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python3 -m pip install codex-stats
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Command Reference
|
|
55
|
+
|
|
56
|
+
- `codex-stats`
|
|
57
|
+
Generate a standalone dashboard HTML file and open it in the default browser.
|
|
58
|
+
- `codex-stats --output codex-stats-dashboard.html`
|
|
59
|
+
Write the dashboard HTML to a fixed path.
|
|
60
|
+
- `codex-stats --output codex-stats-dashboard.html --no-open`
|
|
61
|
+
Write the dashboard HTML without opening the browser.
|
|
62
|
+
- `codex-stats export codex-stats-export.json`
|
|
63
|
+
Export normalized local stats to JSON.
|
|
64
|
+
- `codex-stats export codex-stats-export.json --since 30d`
|
|
65
|
+
Export only a rolling window of recent sessions.
|
|
66
|
+
|
|
67
|
+
Inside the dashboard, use the action bar to:
|
|
68
|
+
|
|
69
|
+
- switch between `Day`, `Week`, `Month`, and `All Time`
|
|
70
|
+
- print the active tab to PDF
|
|
71
|
+
- download shareable SVG cards for summary, cost, focus, and project share
|
|
72
|
+
|
|
73
|
+
## How It Works
|
|
74
|
+
|
|
75
|
+
`codex-stats` does not proxy or intercept Codex API traffic.
|
|
76
|
+
|
|
77
|
+
It reads local Codex artifacts, including:
|
|
78
|
+
|
|
79
|
+
- `state_5.sqlite` for session metadata
|
|
80
|
+
- rollout JSONL files for request and token snapshots
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
- Costs are estimates, not billing values.
|
|
85
|
+
- Output depends on local Codex file formats remaining compatible.
|
|
86
|
+
- `export --since Nd` limits snapshots to a rolling window before sharing.
|
|
87
|
+
|
|
88
|
+
## Roadmap
|
|
89
|
+
|
|
90
|
+
The current priority list lives in [docs/roadmap.md](/Users/vivek/Desktop/Salad/codex_stats/docs/roadmap.md).
|
|
91
|
+
|
|
92
|
+
## Shareable Assets
|
|
93
|
+
|
|
94
|
+
Share-ready assets now live under [docs/assets/codex-stats-share-card.svg](/Users/vivek/Desktop/Salad/codex_stats/docs/assets/codex-stats-share-card.svg), and the dashboard can generate a fresh SVG download set directly from your local usage data.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
For local development from the repo:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python3 -m venv .venv
|
|
102
|
+
source .venv/bin/activate
|
|
103
|
+
python -m pip install -U pip setuptools
|
|
104
|
+
python -m pip install -e .
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run without installing:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
PYTHONPATH=src python3 -m codex_stats
|
|
111
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# codex-stats
|
|
2
|
+
|
|
3
|
+
`codex-stats` is a local analytics tool for Codex usage.
|
|
4
|
+
|
|
5
|
+
It reads your local Codex state from `~/.codex` and surfaces:
|
|
6
|
+
|
|
7
|
+
- a browser dashboard with day, week, month, and all-time tabs
|
|
8
|
+
- model and project breakdowns
|
|
9
|
+
- recent session history
|
|
10
|
+
- estimated token-based cost
|
|
11
|
+
- anomaly-aware usage insights and recommendations
|
|
12
|
+
- export for cross-device snapshots
|
|
13
|
+
- shareable SVG cards and browser PDF export from the dashboard
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pipx install codex-stats
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or with `pip`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python3 -m pip install codex-stats
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Command Reference
|
|
28
|
+
|
|
29
|
+
- `codex-stats`
|
|
30
|
+
Generate a standalone dashboard HTML file and open it in the default browser.
|
|
31
|
+
- `codex-stats --output codex-stats-dashboard.html`
|
|
32
|
+
Write the dashboard HTML to a fixed path.
|
|
33
|
+
- `codex-stats --output codex-stats-dashboard.html --no-open`
|
|
34
|
+
Write the dashboard HTML without opening the browser.
|
|
35
|
+
- `codex-stats export codex-stats-export.json`
|
|
36
|
+
Export normalized local stats to JSON.
|
|
37
|
+
- `codex-stats export codex-stats-export.json --since 30d`
|
|
38
|
+
Export only a rolling window of recent sessions.
|
|
39
|
+
|
|
40
|
+
Inside the dashboard, use the action bar to:
|
|
41
|
+
|
|
42
|
+
- switch between `Day`, `Week`, `Month`, and `All Time`
|
|
43
|
+
- print the active tab to PDF
|
|
44
|
+
- download shareable SVG cards for summary, cost, focus, and project share
|
|
45
|
+
|
|
46
|
+
## How It Works
|
|
47
|
+
|
|
48
|
+
`codex-stats` does not proxy or intercept Codex API traffic.
|
|
49
|
+
|
|
50
|
+
It reads local Codex artifacts, including:
|
|
51
|
+
|
|
52
|
+
- `state_5.sqlite` for session metadata
|
|
53
|
+
- rollout JSONL files for request and token snapshots
|
|
54
|
+
|
|
55
|
+
## Notes
|
|
56
|
+
|
|
57
|
+
- Costs are estimates, not billing values.
|
|
58
|
+
- Output depends on local Codex file formats remaining compatible.
|
|
59
|
+
- `export --since Nd` limits snapshots to a rolling window before sharing.
|
|
60
|
+
|
|
61
|
+
## Roadmap
|
|
62
|
+
|
|
63
|
+
The current priority list lives in [docs/roadmap.md](/Users/vivek/Desktop/Salad/codex_stats/docs/roadmap.md).
|
|
64
|
+
|
|
65
|
+
## Shareable Assets
|
|
66
|
+
|
|
67
|
+
Share-ready assets now live under [docs/assets/codex-stats-share-card.svg](/Users/vivek/Desktop/Salad/codex_stats/docs/assets/codex-stats-share-card.svg), and the dashboard can generate a fresh SVG download set directly from your local usage data.
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
For local development from the repo:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python3 -m venv .venv
|
|
75
|
+
source .venv/bin/activate
|
|
76
|
+
python -m pip install -U pip setuptools
|
|
77
|
+
python -m pip install -e .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Run without installing:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
PYTHONPATH=src python3 -m codex_stats
|
|
84
|
+
```
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import tempfile
|
|
5
|
+
import webbrowser
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .config import Paths, PricingConfig, load_pricing_config
|
|
10
|
+
from .display import format_dashboard_html
|
|
11
|
+
from .metrics import (
|
|
12
|
+
local_date,
|
|
13
|
+
summarize_compare_from_details,
|
|
14
|
+
summarize_costs_from_details,
|
|
15
|
+
summarize_daily_from_details,
|
|
16
|
+
summarize_details,
|
|
17
|
+
summarize_history_from_details,
|
|
18
|
+
summarize_insights_from_details,
|
|
19
|
+
summarize_projects_from_details,
|
|
20
|
+
summarize_top_sessions_from_details,
|
|
21
|
+
)
|
|
22
|
+
from .ingest import iter_session_details
|
|
23
|
+
from .models import DashboardData, DashboardWindow, SessionDetails
|
|
24
|
+
from .transfer import write_export
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
28
|
+
parser = argparse.ArgumentParser(
|
|
29
|
+
prog="codex-stats",
|
|
30
|
+
description="Open a local Codex usage dashboard in the browser or export normalized stats.",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--output",
|
|
34
|
+
dest="dashboard_output",
|
|
35
|
+
help="Write dashboard HTML to a file instead of a temp file.",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument("--no-open", action="store_true", help="Write the dashboard without opening the browser.")
|
|
38
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
39
|
+
|
|
40
|
+
export_parser = subparsers.add_parser("export", help="Export normalized local stats to JSON.")
|
|
41
|
+
export_parser.add_argument("output", help="Output JSON file.")
|
|
42
|
+
export_parser.add_argument("--since", help="Only export the last Nd of sessions, for example 30d.")
|
|
43
|
+
return parser
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv: list[str] | None = None) -> int:
|
|
47
|
+
parser = build_parser()
|
|
48
|
+
args = parser.parse_args(argv)
|
|
49
|
+
paths = Paths.discover()
|
|
50
|
+
|
|
51
|
+
if args.command == "export":
|
|
52
|
+
output_path = write_export(paths, Path(args.output).expanduser(), since=args.since)
|
|
53
|
+
print(f"Exported stats to {output_path}")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
if args.command is not None:
|
|
57
|
+
parser.print_help()
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
dashboard = _build_dashboard(paths)
|
|
61
|
+
output_path = _write_dashboard_output(format_dashboard_html(dashboard), args.dashboard_output)
|
|
62
|
+
print(f"Wrote dashboard to {output_path}")
|
|
63
|
+
if not args.no_open:
|
|
64
|
+
_open_report_in_browser(output_path)
|
|
65
|
+
print(f"Opened dashboard in browser: {output_path}")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _build_dashboard(paths: Paths, now: datetime | None = None) -> DashboardData:
|
|
70
|
+
current_time = now or datetime.now().astimezone()
|
|
71
|
+
pricing = load_pricing_config(paths)
|
|
72
|
+
all_details = iter_session_details(paths)
|
|
73
|
+
|
|
74
|
+
today_details = _details_for_last_days(all_details, 1, current_time)
|
|
75
|
+
week_details = _details_for_last_days(all_details, 7, current_time)
|
|
76
|
+
month_details = _details_for_last_days(all_details, 30, current_time)
|
|
77
|
+
|
|
78
|
+
windows = [
|
|
79
|
+
_build_window(
|
|
80
|
+
key="day",
|
|
81
|
+
label="Day",
|
|
82
|
+
description="Today’s usage with a direct comparison to yesterday.",
|
|
83
|
+
current_details=today_details,
|
|
84
|
+
previous_details=_details_for_previous_window(all_details, 1, current_time),
|
|
85
|
+
current_label="today",
|
|
86
|
+
previous_label="yesterday",
|
|
87
|
+
trend_days=1,
|
|
88
|
+
all_details=all_details,
|
|
89
|
+
pricing=pricing,
|
|
90
|
+
now=current_time,
|
|
91
|
+
),
|
|
92
|
+
_build_window(
|
|
93
|
+
key="week",
|
|
94
|
+
label="Week",
|
|
95
|
+
description="Rolling 7-day totals, recent trend, and the busiest sessions this week.",
|
|
96
|
+
current_details=week_details,
|
|
97
|
+
previous_details=_details_for_previous_window(all_details, 7, current_time),
|
|
98
|
+
current_label="last 7 days",
|
|
99
|
+
previous_label="previous 7 days",
|
|
100
|
+
trend_days=7,
|
|
101
|
+
all_details=all_details,
|
|
102
|
+
pricing=pricing,
|
|
103
|
+
now=current_time,
|
|
104
|
+
),
|
|
105
|
+
_build_window(
|
|
106
|
+
key="month",
|
|
107
|
+
label="Month",
|
|
108
|
+
description="Rolling 30-day totals with project concentration and cost pressure.",
|
|
109
|
+
current_details=month_details,
|
|
110
|
+
previous_details=_details_for_previous_window(all_details, 30, current_time),
|
|
111
|
+
current_label="last 30 days",
|
|
112
|
+
previous_label="previous 30 days",
|
|
113
|
+
trend_days=30,
|
|
114
|
+
all_details=all_details,
|
|
115
|
+
pricing=pricing,
|
|
116
|
+
now=current_time,
|
|
117
|
+
),
|
|
118
|
+
_build_all_time_window(
|
|
119
|
+
all_details=all_details,
|
|
120
|
+
pricing=pricing,
|
|
121
|
+
now=current_time,
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
return DashboardData(generated_at=current_time, windows=windows)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_window(
|
|
128
|
+
*,
|
|
129
|
+
key: str,
|
|
130
|
+
label: str,
|
|
131
|
+
description: str,
|
|
132
|
+
current_details: list[SessionDetails],
|
|
133
|
+
previous_details: list[SessionDetails],
|
|
134
|
+
current_label: str,
|
|
135
|
+
previous_label: str,
|
|
136
|
+
trend_days: int,
|
|
137
|
+
all_details: list[SessionDetails],
|
|
138
|
+
pricing: PricingConfig,
|
|
139
|
+
now: datetime,
|
|
140
|
+
) -> DashboardWindow:
|
|
141
|
+
summary = summarize_details(current_label, current_details, pricing)
|
|
142
|
+
comparison = summarize_compare_from_details(
|
|
143
|
+
current_details,
|
|
144
|
+
previous_details,
|
|
145
|
+
current_label=current_label,
|
|
146
|
+
previous_label=previous_label,
|
|
147
|
+
pricing=pricing,
|
|
148
|
+
)
|
|
149
|
+
insights = summarize_insights_from_details(current_details, pricing=pricing, month=summary, now=now)
|
|
150
|
+
costs = summarize_costs_from_details(
|
|
151
|
+
current_details,
|
|
152
|
+
pricing=pricing,
|
|
153
|
+
today=summary,
|
|
154
|
+
week=summary,
|
|
155
|
+
month=summary,
|
|
156
|
+
now=now,
|
|
157
|
+
)
|
|
158
|
+
history_source = current_details if current_details else all_details
|
|
159
|
+
return DashboardWindow(
|
|
160
|
+
key=key,
|
|
161
|
+
label=label,
|
|
162
|
+
description=description,
|
|
163
|
+
comparison_label=f"{current_label} vs {previous_label}",
|
|
164
|
+
summary=summary,
|
|
165
|
+
comparison=comparison,
|
|
166
|
+
projects=summarize_projects_from_details(current_details, pricing)[:10],
|
|
167
|
+
top_sessions=summarize_top_sessions_from_details(current_details, pricing, limit=10),
|
|
168
|
+
history=summarize_history_from_details(history_source, pricing, limit=10),
|
|
169
|
+
daily_points=summarize_daily_from_details(current_details, days=max(trend_days, 1), now=now, pricing=pricing),
|
|
170
|
+
costs=costs,
|
|
171
|
+
insights=insights,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _build_all_time_window(
|
|
176
|
+
*,
|
|
177
|
+
all_details: list[SessionDetails],
|
|
178
|
+
pricing: PricingConfig,
|
|
179
|
+
now: datetime,
|
|
180
|
+
) -> DashboardWindow:
|
|
181
|
+
summary = summarize_details("all time", all_details, pricing)
|
|
182
|
+
recent_details = _details_for_last_days(all_details, 30, now)
|
|
183
|
+
previous_details = _details_for_previous_window(all_details, 30, now)
|
|
184
|
+
comparison = summarize_compare_from_details(
|
|
185
|
+
recent_details,
|
|
186
|
+
previous_details,
|
|
187
|
+
current_label="recent 30 days",
|
|
188
|
+
previous_label="prior 30 days",
|
|
189
|
+
pricing=pricing,
|
|
190
|
+
)
|
|
191
|
+
costs = summarize_costs_from_details(all_details, pricing=pricing, now=now)
|
|
192
|
+
trend_days = _all_time_trend_days(all_details, now)
|
|
193
|
+
trend_details = _details_for_last_days(all_details, trend_days, now)
|
|
194
|
+
return DashboardWindow(
|
|
195
|
+
key="all",
|
|
196
|
+
label="All Time",
|
|
197
|
+
description=f"All recorded sessions. Trend charts cover the last {trend_days} days so the page stays readable.",
|
|
198
|
+
comparison_label="recent 30 days vs prior 30 days",
|
|
199
|
+
summary=summary,
|
|
200
|
+
comparison=comparison,
|
|
201
|
+
projects=summarize_projects_from_details(all_details, pricing)[:10],
|
|
202
|
+
top_sessions=summarize_top_sessions_from_details(all_details, pricing, limit=10),
|
|
203
|
+
history=summarize_history_from_details(all_details, pricing, limit=10),
|
|
204
|
+
daily_points=summarize_daily_from_details(trend_details, days=trend_days, now=now, pricing=pricing),
|
|
205
|
+
costs=costs,
|
|
206
|
+
insights=summarize_insights_from_details(all_details, pricing=pricing, month=summary, now=now),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _details_for_last_days(details: list[SessionDetails], days: int, now: datetime) -> list[SessionDetails]:
|
|
211
|
+
safe_days = max(days, 1)
|
|
212
|
+
end_day = now.date()
|
|
213
|
+
start_day = end_day - timedelta(days=safe_days - 1)
|
|
214
|
+
return _filter_details_between(details, start_day, end_day, now)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _details_for_previous_window(details: list[SessionDetails], days: int, now: datetime) -> list[SessionDetails]:
|
|
218
|
+
safe_days = max(days, 1)
|
|
219
|
+
end_day = now.date() - timedelta(days=safe_days)
|
|
220
|
+
start_day = end_day - timedelta(days=safe_days - 1)
|
|
221
|
+
return _filter_details_between(details, start_day, end_day, now)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _filter_details_between(
|
|
225
|
+
details: list[SessionDetails],
|
|
226
|
+
start_day,
|
|
227
|
+
end_day,
|
|
228
|
+
now: datetime,
|
|
229
|
+
) -> list[SessionDetails]:
|
|
230
|
+
return [
|
|
231
|
+
detail
|
|
232
|
+
for detail in details
|
|
233
|
+
if start_day <= local_date(detail.session.created_at, now.tzinfo) <= end_day
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _all_time_trend_days(details: list[SessionDetails], now: datetime) -> int:
|
|
238
|
+
if not details:
|
|
239
|
+
return 30
|
|
240
|
+
newest_day = max(local_date(detail.session.created_at, now.tzinfo) for detail in details)
|
|
241
|
+
oldest_day = min(local_date(detail.session.created_at, now.tzinfo) for detail in details)
|
|
242
|
+
span_days = (newest_day - oldest_day).days + 1
|
|
243
|
+
return min(max(span_days, 1), 90)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _write_dashboard_output(content: str, output: str | None) -> Path:
|
|
247
|
+
if output:
|
|
248
|
+
output_path = Path(output).expanduser()
|
|
249
|
+
else:
|
|
250
|
+
handle = tempfile.NamedTemporaryFile(prefix="codex-stats-dashboard-", suffix=".html", delete=False)
|
|
251
|
+
handle.close()
|
|
252
|
+
output_path = Path(handle.name)
|
|
253
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
254
|
+
output_path.write_text(content + ("" if content.endswith("\n") else "\n"), encoding="utf-8")
|
|
255
|
+
return output_path
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _open_report_in_browser(path: Path) -> None:
|
|
259
|
+
webbrowser.open(path.resolve().as_uri())
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
raise SystemExit(main())
|