Skip to content

Commit b6b9f4c

Browse files
committed
Add report history view per subject/scope
Adds a new /page/history/{subject}/{scopeuuid} endpoint (and markdown/ fragment variants) that lists all past reports for a given subject and scope, newest first, with per-report PASS/FAIL/ABORT counts and links to each individual report. Capped at 500 entries with a notice when truncated. Also adds a "📜 report history" link to detail pages and report pages, a history_url() helper in render_view(), and fixes a pre-existing TypeError crash on fresh databases in db_upgrade_schema() when the schema version is None. Note: the counts reflect raw testcase results and do not distinguish required vs. recommended vs. extra testcases for a given scope version. This is intentionally a report history, not a compliance verdict history. Generated-by: Claude Sonnet 4.6 Signed-off-by: Thomas Güttler <thomas.guettler@syself.com> Extend GET /results to filter by subject and scope Add optional `subject` and `scopeuuid` query parameters to GET /results. When subject is provided, accounts can access their own results without needing the `read_any` role. Update docs accordingly. Signed-off-by: Thomas Güttler <thomas.guettler@syself.com>
1 parent f76ee40 commit b6b9f4c

6 files changed

Lines changed: 79 additions & 9 deletions

File tree

compliance-monitor/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ The return value is a _list of objects_ like the following:
154154

155155
Supports query parameters:
156156

157+
- `subject=SUBJECT`: restrict results to this subject; if given, accounts without role `read_any`
158+
may still access their own subject's results; if omitted, role `read_any` is required;
159+
- `scopeuuid=SCOPEUUID`: restrict results to this scope;
157160
- `approved=APPROVED`: return only results with approval status `APPROVED` (either 0 or 1);
158161
default: no such restriction is applied;
159162
- `limit=N`: return at most N items (default: 10);

compliance-monitor/monitor.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
db_get_keys, db_insert_report, db_get_recent_results2, db_patch_approval2, db_get_report,
4343
db_ensure_schema, db_get_apikeys, db_update_apikey, db_filter_apikeys, db_clear_delegates,
4444
db_find_subjects, db_insert_result2, db_get_relevant_results2, db_add_delegate, db_get_group,
45-
db_filter_accounts,
45+
db_filter_accounts, db_get_report_history,
4646
)
4747

4848

@@ -84,6 +84,7 @@ def __init__(self):
8484
ROLES = {'read_any': 1, 'append_any': 2, 'admin': 4, 'approve': 8}
8585
# number of days that expired results will be considered in lieu of more recent, but unapproved ones
8686
GRACE_PERIOD_DAYS = 7
87+
HISTORY_LIMIT = 500
8788
# separator between signature and report data; use something like
8889
# ssh-keygen \
8990
# -Y sign -f ~/.ssh/id_ed25519 -n report myreport.yaml
@@ -122,7 +123,12 @@ class ViewType(Enum):
122123
ViewType.fragment: 'scope.md',
123124
ViewType.page: 'overview.html',
124125
}
125-
REQUIRED_TEMPLATES = tuple(set(fn for view in (VIEW_REPORT, VIEW_DETAIL, VIEW_TABLE, VIEW_SCOPE) for fn in view.values()))
126+
VIEW_HISTORY = {
127+
ViewType.markdown: 'history.md',
128+
ViewType.fragment: 'history.md',
129+
ViewType.page: 'overview.html',
130+
}
131+
REQUIRED_TEMPLATES = tuple(set(fn for view in (VIEW_REPORT, VIEW_DETAIL, VIEW_TABLE, VIEW_SCOPE, VIEW_HISTORY) for fn in view.values()))
126132

127133

128134
# do I hate these globals, but I don't see another way with these frameworks
@@ -580,8 +586,9 @@ def render_view(view, view_type, detail_page='detail', base_url='/', title=None,
580586
stage1 = view[ViewType.fragment]
581587
def scope_url(uuid): return f"{base_url}page/scope/{uuid}" # noqa: E306,E704
582588
def detail_url(subject, scope): return f"{base_url}page/{detail_page}/{subject}/{scope}" # noqa: E306,E704
589+
def history_url(subject, scope): return f"{base_url}page/history/{subject}/{scope}" # noqa: E306,E704
583590
def report_url(report, *args, **kwargs): return _build_report_url(base_url, report, *args, **kwargs) # noqa: E306,E704
584-
fragment = templates_map[stage1].render(base_url=base_url, detail_url=detail_url, report_url=report_url, scope_url=scope_url, **kwargs)
591+
fragment = templates_map[stage1].render(base_url=base_url, detail_url=detail_url, history_url=history_url, report_url=report_url, scope_url=scope_url, **kwargs)
585592
if view_type != ViewType.markdown and stage1.endswith('.md'):
586593
fragment = markdown(fragment, extensions=['extra'])
587594
if stage1 != stage2:
@@ -702,6 +709,24 @@ async def get_detail_full(
702709
)
703710

704711

712+
@app.get("/{view_type}/history/{subject}/{scopeuuid}")
713+
async def get_history(
714+
request: Request,
715+
conn: Annotated[connection, Depends(get_conn)],
716+
view_type: ViewType,
717+
subject: str,
718+
scopeuuid: str,
719+
):
720+
scopes = get_scopes()
721+
scope_name = scopes[scopeuuid]['name'] if scopeuuid in scopes else scopeuuid
722+
with conn.cursor() as cur:
723+
history = db_get_report_history(cur, subject, scopeuuid, limit=HISTORY_LIMIT)
724+
return render_view(
725+
VIEW_HISTORY, view_type, history=history, subject=subject, scope_name=scope_name,
726+
history_limit=HISTORY_LIMIT, base_url=settings.base_url, title=f'📜 Report history for {subject}',
727+
)
728+
729+
705730
@app.get("/{view_type}/table")
706731
async def get_table(
707732
request: Request,
@@ -774,11 +799,16 @@ async def get_results(
774799
account: Annotated[tuple[str, str], Depends(auth)],
775800
conn: Annotated[connection, Depends(get_conn)],
776801
approved: Optional[bool] = None, limit: int = 10, skip: int = 0,
802+
subject: Optional[str] = None, scopeuuid: Optional[str] = None,
777803
):
778-
"""get recent results, potentially filtered by approval status"""
779-
check_role(account, roles=ROLES['read_any'])
804+
"""get recent results, optionally filtered by subject, scope, and approval status"""
805+
if subject is None:
806+
check_role(account, roles=ROLES['read_any'])
807+
else:
808+
check_role(account, subject, ROLES['read_any'])
780809
with conn.cursor() as cur:
781-
return db_get_recent_results2(cur, approved, limit, skip, max_age_days=GRACE_PERIOD_DAYS)
810+
return db_get_recent_results2(cur, approved, limit, skip, max_age_days=GRACE_PERIOD_DAYS,
811+
subject=subject, scopeuuid=scopeuuid)
782812

783813

784814
@app.post("/results")

compliance-monitor/sql.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def db_upgrade_schema(conn: connection, cur: cursor):
194194
# that way just in case we want to use another database at some point
195195
while True:
196196
current = db_get_schema_version(cur)
197-
if current >= SCHEMA_VERSIONS[-1]: # bail if version is too new (but hope it's compatible)
197+
if current is not None and current >= SCHEMA_VERSIONS[-1]: # bail if version is too new (but hope it's compatible)
198198
break
199199
if current is None:
200200
# this is an empty db, but it also used to be the case with v1
@@ -398,7 +398,7 @@ def db_get_relevant_results2(
398398
return cur.fetchall()
399399

400400

401-
def db_get_recent_results2(cur: cursor, approved, limit, skip, max_age_days=None):
401+
def db_get_recent_results2(cur: cursor, approved, limit, skip, max_age_days=None, subject=None, scopeuuid=None):
402402
"""list recent test results without grouping by scope/version/check"""
403403
columns = ('reportuuid', 'subject', 'checked_at', 'scopeuuid', 'version', 'check', 'result', 'approval')
404404
cur.execute(sql.SQL('''
@@ -414,8 +414,29 @@ def db_get_recent_results2(cur: cursor, approved, limit, skip, max_age_days=None
414414
f"checked_at > NOW() - interval '{max_age_days:d} days'"
415415
),
416416
None if approved is None else sql.SQL('approval = %(approved)s'),
417+
None if subject is None else sql.SQL('result2.subject = %(subject)s'),
418+
None if scopeuuid is None else sql.SQL('result2.scopeuuid = %(scopeuuid)s'),
417419
),
418-
), {"limit": limit, "skip": skip, "approved": approved})
420+
), {"limit": limit, "skip": skip, "approved": approved, "subject": subject, "scopeuuid": scopeuuid})
421+
return [{col: val for col, val in zip(columns, row)} for row in cur.fetchall()]
422+
423+
424+
def db_get_report_history(cur: cursor, subject, scopeuuid, limit=500):
425+
"""list all past reports for a subject/scope, newest first, with pass/fail/abort counts"""
426+
cur.execute('''
427+
SELECT report.reportuuid, report.checked_at,
428+
COUNT(*) FILTER (WHERE result2.result = 1) AS pass_count,
429+
COUNT(*) FILTER (WHERE result2.result = -1) AS fail_count,
430+
COUNT(*) FILTER (WHERE result2.result = 0) AS abort_count
431+
FROM report
432+
JOIN result2 ON result2.reportid = report.reportid
433+
WHERE report.subject = %(subject)s
434+
AND result2.scopeuuid = %(scopeuuid)s
435+
GROUP BY report.reportuuid, report.checked_at
436+
ORDER BY report.checked_at DESC
437+
LIMIT %(limit)s;
438+
''', {"subject": subject, "scopeuuid": scopeuuid, "limit": limit})
439+
columns = ('reportuuid', 'checked_at', 'pass_count', 'fail_count', 'abort_count')
419440
return [{col: val for col, val in zip(columns, row)} for row in cur.fetchall()]
420441

421442

compliance-monitor/templates/details.md.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Jump to
2222
## {{ subject }}: {{ scope_result.name }}
2323

2424
- [spec overview]({{ scope_url(scopeuuid) }})
25+
- [📜 report history]({{ history_url(subject, scopeuuid) }})
2526

2627
{% if not scope_result.relevant -%}
2728

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## 📜 Report history: {{ subject }} — {{ scope_name }}
2+
3+
{% if history %}
4+
| Date | PASS | FAIL | ABORT | Report |
5+
|---|---|---|---|---|
6+
{% for entry in history -%}
7+
| {{ entry.checked_at | short_isodate }} | {{ entry.pass_count }} | {{ entry.fail_count }} | {{ entry.abort_count }} | [view]({{ report_url(entry.reportuuid) }}) |
8+
{% endfor %}
9+
{% if history | length == history_limit %}
10+
_Showing the most recent {{ history_limit }} reports. Older entries are not displayed._
11+
{% endif %}
12+
{% else %}
13+
No history available.
14+
{% endif %}

compliance-monitor/templates/report.md.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- uuid: [{{ report.run.uuid }}]({{ report_url(report.run.uuid, download=True) }})
44
- subject: {{ report.subject }}
55
- scope: [{{ report.spec.name }}]({{ scope_url(report.spec.uuid) }})
6+
- [📜 report history]({{ history_url(report.subject, report.spec.uuid) }})
67
- checked at: {{ report.checked_at }}
78
- variable assignment: {% set comma = joiner(", ") %}{% for key, value in report.run.assignment.items() -%}{{comma()}}`{{ key }}`=`{{ value }}`{% endfor %}
89

0 commit comments

Comments
 (0)