Add global report generation command #24

Merged
wagesj45 merged 1 commit from codex/add-global-report-command-and-tests into main 2025-07-19 00:10:30 -05:00
3 changed files with 83 additions and 0 deletions

View file

@ -24,6 +24,7 @@ python scripts/generate_reports.py hourly
python scripts/generate_reports.py daily
python scripts/generate_reports.py weekly
python scripts/generate_reports.py monthly
python scripts/generate_reports.py global
# Generate reports for each individual domain
echo "[INFO] Generating per-domain reports..."

View file

@ -101,6 +101,10 @@ def _generate_interval(interval: str, domain: Optional[str] = None) -> None:
report_list = []
for definition in cfg:
if "{bucket}" not in definition["query"] or definition.get("global"):
# Global reports are generated separately
continue
name = definition["name"]
query = definition["query"].replace("{bucket}", bucket)
query = query.replace("FROM logs", "FROM logs_view")
@ -154,6 +158,50 @@ def _generate_root_index() -> None:
typer.echo(f"Generated root index at {out_path}")
def _generate_global() -> None:
"""Generate reports that do not depend on an interval."""
cfg = _load_config()
if not cfg:
typer.echo("No report definitions found")
return
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
out_dir = OUTPUT_DIR / "global"
out_dir.mkdir(parents=True, exist_ok=True)
report_list = []
for definition in cfg:
if "{bucket}" in definition["query"] and not definition.get("global"):
continue
name = definition["name"]
query = definition["query"]
cur.execute(query)
rows = cur.fetchall()
headers = [c[0] for c in cur.description]
data = [dict(zip(headers, row)) for row in rows]
json_path = out_dir / f"{name}.json"
_save_json(json_path, data)
entry = {
"name": name,
"label": definition.get("label", name.title()),
"chart": definition.get("chart", "line"),
"json": f"{name}.json",
"html": f"{name}.html",
}
if "color" in definition:
entry["color"] = definition["color"]
if "colors" in definition:
entry["colors"] = definition["colors"]
_render_snippet(entry, out_dir)
report_list.append(entry)
_save_json(out_dir / "reports.json", report_list)
typer.echo("Generated global reports")
@app.command()
def hourly(
domain: Optional[str] = typer.Option(
@ -218,6 +266,12 @@ def monthly(
_generate_interval("monthly", domain)
@app.command("global")
def global_reports() -> None:
"""Generate global reports."""
_generate_global()
@app.command()
def index() -> None:
"""Generate the root index page linking all reports."""

View file

@ -80,6 +80,14 @@ def sample_reports(tmp_path):
FROM logs
GROUP BY bucket
ORDER BY bucket
- name: domain_totals
global: true
query: |
SELECT host AS bucket,
COUNT(*) AS value
FROM logs
GROUP BY host
ORDER BY value DESC
"""
)
return cfg
@ -161,3 +169,23 @@ def test_generate_root_index(tmp_path, sample_reports, monkeypatch):
# check for domain options
assert '<option value="foo.com">' in content
assert '<option value="bar.com">' in content
def test_global_reports_once(tmp_path, sample_reports, monkeypatch):
db_path = tmp_path / "database" / "ngxstat.db"
setup_db(db_path)
monkeypatch.setattr(gr, "DB_PATH", db_path)
monkeypatch.setattr(gr, "OUTPUT_DIR", tmp_path / "output")
monkeypatch.setattr(gr, "REPORT_CONFIG", sample_reports)
monkeypatch.setattr(
gr, "TEMPLATE_DIR", Path(__file__).resolve().parents[1] / "templates"
)
gr._generate_global()
gr._generate_interval("hourly")
global_snippet = tmp_path / "output" / "global" / "domain_totals.html"
assert global_snippet.exists()
assert not (tmp_path / "output" / "hourly" / "domain_totals.html").exists()