gator-command 1.2.0__tar.gz → 1.2.1__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.
- {gator_command-1.2.0/src/gator_command.egg-info → gator_command-1.2.1}/PKG-INFO +1 -1
- {gator_command-1.2.0 → gator_command-1.2.1}/pyproject.toml +1 -1
- gator_command-1.2.1/src/gator_command/scripts/gator-audit-renderers.py +361 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-audit.py +28 -348
- {gator_command-1.2.0 → gator_command-1.2.1/src/gator_command.egg-info}/PKG-INFO +1 -1
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command.egg-info/SOURCES.txt +1 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_audit.py +180 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/LICENSE +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/PYPI_README.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/README.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/setup.cfg +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/__init__.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/cli.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/crawler.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/dashboard.css +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/dashboard.html +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/dashboard.js +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/audit.js +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/fleet.js +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/repo.js +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/settings.js +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/updates.js +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/enforcer-prompt.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/extract-claude-sessions.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/extract-codex-sessions.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/extract-gemini-sessions.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-charter-draft.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-charter-lint.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-charter-verify.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-dashboard.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-deploy.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-drift.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-enforce.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-fleet-intel.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-fleet-report.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-init-command-post.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-init.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-machine-id.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-policy-status.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-pulse.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-repo-status.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-session-aggregator.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-session-common.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-session-sink.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-sessions.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-update.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-version.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator_core.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator_remote.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator_runtime.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize-actions.sh +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize-lib.sh +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize-post.sh +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize.sh +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/generate_markdown.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/generate_wiki.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/graph_health.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex-lint.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex-lint.sh +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex_formatters.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex_state.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/spawn.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/blueprints/README.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/charterignore +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/charters/README.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/charters/_template.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/commands/commit.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/commands/init.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/commands/update.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/constitution.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/field-guides/README.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/gator-start-up.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/charter-alignment.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/enforcer-review.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/field-guide-generation.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/knowledge-capture.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/significance-check.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/concierge-responses.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/example-project.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/git-workflow.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/refactor-approach.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/enforcer-prompt.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/enforcer-review.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-approve.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-enforce.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-init.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-pre-commit.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-pulse.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-update.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-version.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator_core.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/commit-msg +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/post-commit +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/pre-commit +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/sessions/.gitignore +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/vault/.gitkeep +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/whiteboard.md +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command.egg-info/dependency_links.txt +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command.egg-info/entry_points.txt +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command.egg-info/top_level.txt +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_audit_integration.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_charter_lint.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_dashboard_sessions.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_deploy.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_drift.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_fleet_intel.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_fleet_report.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_gator_core.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_gatorize.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_hooks.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_init.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_memex_state.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_packaging.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_pre_commit.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_remote.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_session_aggregator.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_session_sink.py +0 -0
- {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_sessions.py +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator-audit-renderers.py — Output renderers for gator audit.
|
|
4
|
+
|
|
5
|
+
Pure rendering functions: take an assembled audit data dict, return a string.
|
|
6
|
+
No data collection, no side effects, no shared state.
|
|
7
|
+
|
|
8
|
+
Consumers:
|
|
9
|
+
from gator_audit_renderers import render_text, render_html
|
|
10
|
+
|
|
11
|
+
@reads: nothing (pure functions)
|
|
12
|
+
@writes: nothing (returns strings)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Text Output
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
def render_text(data):
|
|
21
|
+
"""Render audit data as terminal text."""
|
|
22
|
+
lines = []
|
|
23
|
+
|
|
24
|
+
lines.append("")
|
|
25
|
+
lines.append(f" gator audit")
|
|
26
|
+
lines.append(f" {data['generated_local']} | {data['version']} | {data.get('machine', {}).get('label', '?')}")
|
|
27
|
+
lines.append("")
|
|
28
|
+
|
|
29
|
+
# Build drift lookup for fleet indicator
|
|
30
|
+
drift_by_name = {}
|
|
31
|
+
for d in data.get("drift", []):
|
|
32
|
+
drift_by_name[d.get("name", "")] = d.get("severity", "ok")
|
|
33
|
+
|
|
34
|
+
# Fleet Status
|
|
35
|
+
fleet = data.get("fleet_status", [])
|
|
36
|
+
if fleet:
|
|
37
|
+
lines.append(f" FLEET STATUS ({len(fleet)} repos)")
|
|
38
|
+
lines.append("")
|
|
39
|
+
for r in fleet:
|
|
40
|
+
if not r.get("accessible"):
|
|
41
|
+
lines.append(f" ✗ {r.get('name', '?')} — NOT ACCESSIBLE")
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
hooks = "✓" if r.get("has_hooks") else "✗"
|
|
45
|
+
trailers = "✓" if r.get("trailers") else "·"
|
|
46
|
+
|
|
47
|
+
# Indicator reflects governance health, not just issues
|
|
48
|
+
repo_drift = drift_by_name.get(r.get("name", ""), "ok")
|
|
49
|
+
if repo_drift == "drift":
|
|
50
|
+
indicator = "✗"
|
|
51
|
+
elif repo_drift == "warn" or not r.get("has_hooks"):
|
|
52
|
+
indicator = "⚠"
|
|
53
|
+
else:
|
|
54
|
+
indicator = "✓"
|
|
55
|
+
|
|
56
|
+
charters = r.get("charters", 0)
|
|
57
|
+
functions = r.get("functions", 0)
|
|
58
|
+
|
|
59
|
+
commit = r.get("last_commit")
|
|
60
|
+
commit_str = ""
|
|
61
|
+
if commit and not commit.get("error"):
|
|
62
|
+
commit_str = f" last: {commit['age']}"
|
|
63
|
+
|
|
64
|
+
lines.append(
|
|
65
|
+
f" {indicator} {r.get('name', '?'):<20}"
|
|
66
|
+
f" charters: {charters:>2} ({functions:>2} fn)"
|
|
67
|
+
f" hooks: {hooks} trailers: {trailers}"
|
|
68
|
+
f"{commit_str}"
|
|
69
|
+
)
|
|
70
|
+
lines.append("")
|
|
71
|
+
|
|
72
|
+
# Drift
|
|
73
|
+
drift_list = data.get("drift", [])
|
|
74
|
+
drift_sum = data.get("drift_summary", {})
|
|
75
|
+
if drift_list:
|
|
76
|
+
ok = drift_sum.get("ok", 0)
|
|
77
|
+
warn = drift_sum.get("warn", 0)
|
|
78
|
+
drifted = drift_sum.get("drift", 0)
|
|
79
|
+
|
|
80
|
+
# Surface command-post git failure once, not per-repo
|
|
81
|
+
cp_state = drift_sum.get("command_post", {})
|
|
82
|
+
if cp_state.get("git_failed"):
|
|
83
|
+
lines.append(f" DRIFT (command post git access failed — policy comparison skipped)")
|
|
84
|
+
else:
|
|
85
|
+
lines.append(f" DRIFT ({ok} current, {warn} warnings, {drifted} drifted)")
|
|
86
|
+
lines.append("")
|
|
87
|
+
|
|
88
|
+
seen_cp_warn = False
|
|
89
|
+
for r in drift_list:
|
|
90
|
+
if r.get("severity") == "ok":
|
|
91
|
+
continue
|
|
92
|
+
name = r.get("name", "?")
|
|
93
|
+
sev = "✗" if r["severity"] == "drift" else "⚠"
|
|
94
|
+
lines.append(f" {sev} {name}")
|
|
95
|
+
for f in r.get("findings", []):
|
|
96
|
+
# Deduplicate command-post git failure across repos
|
|
97
|
+
if f["check"] == "policy-version" and "git failed" in f.get("message", ""):
|
|
98
|
+
if not seen_cp_warn:
|
|
99
|
+
seen_cp_warn = True
|
|
100
|
+
marker = "⚠"
|
|
101
|
+
lines.append(f" {marker} {f['check']}: {f['message']}")
|
|
102
|
+
continue
|
|
103
|
+
marker = "✗" if f["severity"] == "drift" else "⚠"
|
|
104
|
+
lines.append(f" {marker} {f['check']}: {f['message']}")
|
|
105
|
+
if drifted == 0 and warn == 0:
|
|
106
|
+
lines.append(f" All repos current.")
|
|
107
|
+
lines.append("")
|
|
108
|
+
|
|
109
|
+
# Sessions
|
|
110
|
+
sessions = data.get("sessions", {})
|
|
111
|
+
if sessions and not sessions.get("error"):
|
|
112
|
+
since = sessions.get("since_days", 7)
|
|
113
|
+
recent = sessions.get("recent", 0)
|
|
114
|
+
total = sessions.get("total", 0)
|
|
115
|
+
lines.append(f" SESSIONS (last {since} days: {recent} of {total} total)")
|
|
116
|
+
lines.append("")
|
|
117
|
+
|
|
118
|
+
by_vendor = sessions.get("by_vendor", {})
|
|
119
|
+
if by_vendor:
|
|
120
|
+
vendor_parts = [f"{v}: {c}" for v, c in sorted(by_vendor.items())]
|
|
121
|
+
lines.append(f" Vendors: {' | '.join(vendor_parts)}")
|
|
122
|
+
|
|
123
|
+
by_repo = sessions.get("by_repo", {})
|
|
124
|
+
if by_repo:
|
|
125
|
+
repo_parts = [f"{r}({c})" for r, c in list(by_repo.items())[:6]]
|
|
126
|
+
lines.append(f" Repos: {', '.join(repo_parts)}")
|
|
127
|
+
|
|
128
|
+
pending = sessions.get("pending_export", 0)
|
|
129
|
+
exported = sessions.get("exported", 0)
|
|
130
|
+
lines.append(f" Export: {exported} exported, {pending} pending")
|
|
131
|
+
lines.append("")
|
|
132
|
+
|
|
133
|
+
# Governance Coverage
|
|
134
|
+
gov = data.get("governance", {})
|
|
135
|
+
if gov:
|
|
136
|
+
lines.append(f" GOVERNANCE COVERAGE")
|
|
137
|
+
lines.append("")
|
|
138
|
+
lines.append(f" Charters: {gov.get('charters', 0)} across {gov.get('repos', 0)} repos ({gov.get('functions', 0)} functions)")
|
|
139
|
+
lines.append(f" Hooks: {gov.get('hooks_installed', 0)}/{gov.get('repos', 0)} repos")
|
|
140
|
+
lines.append(f" Trailers: {gov.get('trailers_flowing', 0)}/{gov.get('repos', 0)} repos")
|
|
141
|
+
lines.append(f" Issues: {gov.get('issues', 0)} open")
|
|
142
|
+
lines.append("")
|
|
143
|
+
|
|
144
|
+
# Recent Decisions
|
|
145
|
+
decisions = data.get("decisions", [])
|
|
146
|
+
if decisions:
|
|
147
|
+
lines.append(f" RECENT DECISIONS ({len(decisions)})")
|
|
148
|
+
lines.append("")
|
|
149
|
+
for d in decisions[:10]:
|
|
150
|
+
ts = d.get("timestamp", "")[:10]
|
|
151
|
+
repo = d.get("repo", "")
|
|
152
|
+
repo_str = f" [{repo}]" if repo else ""
|
|
153
|
+
text = d.get("text", "")[:80]
|
|
154
|
+
lines.append(f" [{ts}]{repo_str} {text}")
|
|
155
|
+
lines.append("")
|
|
156
|
+
|
|
157
|
+
lines.append(f" Generated: {data['generated_local']} | Machine: {data.get('machine', {}).get('label', '?')}")
|
|
158
|
+
lines.append("")
|
|
159
|
+
|
|
160
|
+
return "\n".join(lines)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# HTML Output
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def render_html(data):
|
|
168
|
+
"""Render audit data as a self-contained HTML file.
|
|
169
|
+
|
|
170
|
+
Single file, inline CSS, no JavaScript dependencies, no external
|
|
171
|
+
resources. Opens in any browser. Can be saved as PDF, emailed,
|
|
172
|
+
attached to compliance reports. Air-gapped compatible.
|
|
173
|
+
"""
|
|
174
|
+
fleet = data.get("fleet_status", [])
|
|
175
|
+
drift = data.get("drift", [])
|
|
176
|
+
drift_sum = data.get("drift_summary", {})
|
|
177
|
+
sessions = data.get("sessions", {})
|
|
178
|
+
gov = data.get("governance", {})
|
|
179
|
+
decisions = data.get("decisions", [])
|
|
180
|
+
machine = data.get("machine", {})
|
|
181
|
+
|
|
182
|
+
# Build drift lookup for fleet indicator
|
|
183
|
+
drift_by_name = {}
|
|
184
|
+
for d in drift:
|
|
185
|
+
drift_by_name[d.get("name", "")] = d.get("severity", "ok")
|
|
186
|
+
|
|
187
|
+
# Fleet rows
|
|
188
|
+
fleet_rows = ""
|
|
189
|
+
for r in fleet:
|
|
190
|
+
if not r.get("accessible"):
|
|
191
|
+
fleet_rows += f'<tr class="drift"><td>{r.get("name","?")}</td><td colspan="5">NOT ACCESSIBLE</td></tr>\n'
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
repo_drift = drift_by_name.get(r.get("name", ""), "ok")
|
|
195
|
+
if repo_drift == "drift":
|
|
196
|
+
status_class = "drift"
|
|
197
|
+
elif repo_drift == "warn" or not r.get("has_hooks"):
|
|
198
|
+
status_class = "warn"
|
|
199
|
+
else:
|
|
200
|
+
status_class = "ok"
|
|
201
|
+
hooks = "✓" if r.get("has_hooks") else "✗"
|
|
202
|
+
hooks_class = "ok" if r.get("has_hooks") else "drift"
|
|
203
|
+
trailers = "✓" if r.get("trailers") else "·"
|
|
204
|
+
trailers_class = "ok" if r.get("trailers") else "muted"
|
|
205
|
+
|
|
206
|
+
commit = r.get("last_commit")
|
|
207
|
+
age = commit.get("age", "?") if commit and not commit.get("error") else "?"
|
|
208
|
+
|
|
209
|
+
fleet_rows += f'''<tr class="{status_class}">
|
|
210
|
+
<td><strong>{r.get("name","?")}</strong></td>
|
|
211
|
+
<td>{r.get("charters",0)} ({r.get("functions",0)} fn)</td>
|
|
212
|
+
<td class="{hooks_class}">{hooks}</td>
|
|
213
|
+
<td class="{trailers_class}">{trailers}</td>
|
|
214
|
+
<td>{r.get("issues",0)}</td>
|
|
215
|
+
<td>{age}</td>
|
|
216
|
+
</tr>\n'''
|
|
217
|
+
|
|
218
|
+
# Drift rows — deduplicate command-post git failure (same as text view)
|
|
219
|
+
drift_rows = ""
|
|
220
|
+
cp_state = drift_sum.get("command_post", {})
|
|
221
|
+
cp_git_failed = cp_state.get("git_failed", False)
|
|
222
|
+
seen_cp_warn = False
|
|
223
|
+
|
|
224
|
+
if cp_git_failed:
|
|
225
|
+
drift_rows += '<tr class="warn"><td colspan="3">Command post git access failed — policy comparison skipped for all repos</td></tr>\n'
|
|
226
|
+
|
|
227
|
+
for r in drift:
|
|
228
|
+
if r.get("severity") == "ok":
|
|
229
|
+
continue
|
|
230
|
+
for f in r.get("findings", []):
|
|
231
|
+
# Deduplicate command-post git failure
|
|
232
|
+
if f["check"] == "policy-version" and "git failed" in f.get("message", ""):
|
|
233
|
+
continue
|
|
234
|
+
sev_class = "drift" if f["severity"] == "drift" else "warn"
|
|
235
|
+
drift_rows += f'<tr class="{sev_class}"><td>{r.get("name","?")}</td><td>{f["check"]}</td><td>{f["message"]}</td></tr>\n'
|
|
236
|
+
|
|
237
|
+
if not drift_rows:
|
|
238
|
+
drift_rows = '<tr class="ok"><td colspan="3">All repos current. No drift detected.</td></tr>'
|
|
239
|
+
|
|
240
|
+
# Session stats
|
|
241
|
+
by_vendor_html = ""
|
|
242
|
+
for v, c in sorted(sessions.get("by_vendor", {}).items()):
|
|
243
|
+
by_vendor_html += f'<span class="tag">{v}: {c}</span> '
|
|
244
|
+
|
|
245
|
+
# Decision rows
|
|
246
|
+
decision_rows = ""
|
|
247
|
+
for d in decisions[:10]:
|
|
248
|
+
ts = d.get("timestamp", "")[:10]
|
|
249
|
+
repo = d.get("repo", "")
|
|
250
|
+
text = d.get("text", "")[:100]
|
|
251
|
+
decision_rows += f'<tr><td>{ts}</td><td>{repo}</td><td>{text}</td></tr>\n'
|
|
252
|
+
|
|
253
|
+
if not decision_rows:
|
|
254
|
+
decision_rows = '<tr><td colspan="3">No decisions extracted from recent sessions.</td></tr>'
|
|
255
|
+
|
|
256
|
+
ok_count = drift_sum.get("ok", 0)
|
|
257
|
+
warn_count = drift_sum.get("warn", 0)
|
|
258
|
+
drift_count = drift_sum.get("drift", 0)
|
|
259
|
+
since_days = sessions.get("since_days", 7)
|
|
260
|
+
recent_sessions = sessions.get("recent", 0)
|
|
261
|
+
total_sessions = sessions.get("total", 0)
|
|
262
|
+
pending = sessions.get("pending_export", 0)
|
|
263
|
+
exported = sessions.get("exported", 0)
|
|
264
|
+
|
|
265
|
+
html = f'''<!DOCTYPE html>
|
|
266
|
+
<html lang="en">
|
|
267
|
+
<head>
|
|
268
|
+
<meta charset="UTF-8">
|
|
269
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
270
|
+
<title>Gator Audit — {data["generated_local"]}</title>
|
|
271
|
+
<style>
|
|
272
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
273
|
+
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
|
274
|
+
background: #0d1117; color: #c9d1d9; padding: 24px; line-height: 1.5; }}
|
|
275
|
+
.container {{ max-width: 960px; margin: 0 auto; }}
|
|
276
|
+
h1 {{ color: #58a6ff; font-size: 1.4em; margin-bottom: 4px; }}
|
|
277
|
+
h2 {{ color: #8b949e; font-size: 1.1em; margin: 24px 0 12px 0;
|
|
278
|
+
border-bottom: 1px solid #21262d; padding-bottom: 6px; }}
|
|
279
|
+
.subtitle {{ color: #8b949e; font-size: 0.85em; margin-bottom: 20px; }}
|
|
280
|
+
table {{ width: 100%; border-collapse: collapse; margin: 8px 0 16px 0; font-size: 0.85em; }}
|
|
281
|
+
th {{ text-align: left; color: #8b949e; padding: 6px 10px; border-bottom: 1px solid #21262d; }}
|
|
282
|
+
td {{ padding: 6px 10px; border-bottom: 1px solid #161b22; }}
|
|
283
|
+
tr:hover {{ background: #161b22; }}
|
|
284
|
+
.ok {{ color: #3fb950; }}
|
|
285
|
+
.warn {{ color: #d29922; }}
|
|
286
|
+
.drift {{ color: #f85149; }}
|
|
287
|
+
.muted {{ color: #484f58; }}
|
|
288
|
+
.stats {{ display: flex; gap: 24px; flex-wrap: wrap; margin: 12px 0; }}
|
|
289
|
+
.stat {{ background: #161b22; border: 1px solid #21262d; border-radius: 6px;
|
|
290
|
+
padding: 12px 16px; min-width: 140px; }}
|
|
291
|
+
.stat-value {{ font-size: 1.6em; font-weight: bold; color: #58a6ff; }}
|
|
292
|
+
.stat-label {{ font-size: 0.8em; color: #8b949e; }}
|
|
293
|
+
.tag {{ background: #21262d; padding: 2px 8px; border-radius: 3px;
|
|
294
|
+
font-size: 0.8em; margin-right: 4px; }}
|
|
295
|
+
.footer {{ margin-top: 32px; padding-top: 12px; border-top: 1px solid #21262d;
|
|
296
|
+
color: #484f58; font-size: 0.75em; }}
|
|
297
|
+
@media print {{
|
|
298
|
+
body {{ background: white; color: #1f2328; }}
|
|
299
|
+
.stat {{ border-color: #d0d7de; }}
|
|
300
|
+
tr:hover {{ background: transparent; }}
|
|
301
|
+
}}
|
|
302
|
+
</style>
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
<div class="container">
|
|
306
|
+
|
|
307
|
+
<h1>Gator Audit Dashboard</h1>
|
|
308
|
+
<div class="subtitle">{data["generated_local"]} · {data["version"]} · {machine.get("label", "?")}</div>
|
|
309
|
+
|
|
310
|
+
<div class="stats">
|
|
311
|
+
<div class="stat"><div class="stat-value">{gov.get("repos", 0)}</div><div class="stat-label">Repos</div></div>
|
|
312
|
+
<div class="stat"><div class="stat-value">{gov.get("charters", 0)}</div><div class="stat-label">Charters</div></div>
|
|
313
|
+
<div class="stat"><div class="stat-value">{gov.get("functions", 0)}</div><div class="stat-label">Functions</div></div>
|
|
314
|
+
<div class="stat"><div class="stat-value {("ok" if drift_count == 0 else "drift")}">{drift_count}</div><div class="stat-label">Drifted</div></div>
|
|
315
|
+
<div class="stat"><div class="stat-value">{recent_sessions}</div><div class="stat-label">Sessions ({since_days}d)</div></div>
|
|
316
|
+
<div class="stat"><div class="stat-value">{gov.get("issues", 0)}</div><div class="stat-label">Issues</div></div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<h2>Fleet Status</h2>
|
|
320
|
+
<table>
|
|
321
|
+
<tr><th>Repo</th><th>Charters</th><th>Hooks</th><th>Trailers</th><th>Issues</th><th>Last Commit</th></tr>
|
|
322
|
+
{fleet_rows}
|
|
323
|
+
</table>
|
|
324
|
+
|
|
325
|
+
<h2>Drift Findings</h2>
|
|
326
|
+
<div class="subtitle">{ok_count} current, {warn_count} warnings, {drift_count} drifted</div>
|
|
327
|
+
<table>
|
|
328
|
+
<tr><th>Repo</th><th>Check</th><th>Finding</th></tr>
|
|
329
|
+
{drift_rows}
|
|
330
|
+
</table>
|
|
331
|
+
|
|
332
|
+
<h2>Sessions (last {since_days} days)</h2>
|
|
333
|
+
<div class="subtitle">{recent_sessions} of {total_sessions} total · {exported} exported, {pending} pending</div>
|
|
334
|
+
<div style="margin: 8px 0;">{by_vendor_html}</div>
|
|
335
|
+
|
|
336
|
+
<h2>Recent Decisions</h2>
|
|
337
|
+
<table>
|
|
338
|
+
<tr><th>Date</th><th>Repo</th><th>Decision</th></tr>
|
|
339
|
+
{decision_rows}
|
|
340
|
+
</table>
|
|
341
|
+
|
|
342
|
+
<h2>Governance Coverage</h2>
|
|
343
|
+
<table>
|
|
344
|
+
<tr><th>Metric</th><th>Value</th></tr>
|
|
345
|
+
<tr><td>Charters</td><td>{gov.get("charters",0)} across {gov.get("repos",0)} repos ({gov.get("functions",0)} functions documented)</td></tr>
|
|
346
|
+
<tr><td>Hooks Installed</td><td class="{"ok" if gov.get("hooks_installed",0) == gov.get("repos",0) else "warn"}">{gov.get("hooks_installed",0)} / {gov.get("repos",0)} repos</td></tr>
|
|
347
|
+
<tr><td>Trailers Flowing</td><td class="{"ok" if gov.get("trailers_flowing",0) == gov.get("repos",0) else "warn"}">{gov.get("trailers_flowing",0)} / {gov.get("repos",0)} repos</td></tr>
|
|
348
|
+
<tr><td>Open Issues</td><td>{gov.get("issues",0)}</td></tr>
|
|
349
|
+
</table>
|
|
350
|
+
|
|
351
|
+
<div class="footer">
|
|
352
|
+
Generated by Gator {data["version"]} · {data["generated_at"]} ·
|
|
353
|
+
Machine: {machine.get("label", "?")} ({machine.get("id", "?")[:8]}) ·
|
|
354
|
+
<a href="https://github.com/cumberland-laboratories/gator" style="color: #484f58;">cumberland-laboratories/gator</a>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
</div>
|
|
358
|
+
</body>
|
|
359
|
+
</html>'''
|
|
360
|
+
|
|
361
|
+
return html
|