feat: augment outage report template with debugging context

- Expand service list: add Home Assistant, Actual Budget, Audiobookshelf,
  Linkwarden, Matrix, Paperless, Tandoor, FreshRSS, Frigate, HackMD,
  Excalidraw, Wealthfolio, Send, Stirling PDF
- Add structured debugging fields: error type, scope (just me vs others),
  when it started, URL accessed
- Fix user report parser to extract all form fields into status.json
- Show error type, scope, and start time in status page report cards

[ci skip]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-04-14 20:03:44 +00:00
parent c00a908610
commit 3e9231ae0d
3 changed files with 95 additions and 20 deletions

View file

@ -209,6 +209,9 @@ footer { color: var(--fg3); font-size: 11px; margin-top: 32px; padding-top: 16px
html+='<div class="inc-info"><div class="inc-title">'+esc(inc.title)+'</div>';
html+='<div class="inc-meta"><span>'+ago(created)+'</span>';
if(!isReport)html+='<span>'+dur(created,end)+'</span>';
if(isReport&&inc.error_type)html+='<span>'+esc(inc.error_type)+'</span>';
if(isReport&&inc.scope)html+='<span>'+esc(inc.scope)+'</span>';
if(isReport&&inc.when_started)html+='<span>Since: '+esc(inc.when_started)+'</span>';
if(resolved)html+='<span style="color:var(--green)">Resolved</span>';
html+='</div>';
if(inc.affected_services&&inc.affected_services.length){

View file

@ -385,22 +385,33 @@ ISSUES_REPO = "ViktorBarzin/infra"
def has_label(issue, name):
return any(l["name"].lower() == name.lower() for l in issue.get("labels", []))
def parse_user_report_service(body):
"""Extract service from GitHub Issue Form dropdown response."""
def parse_form_field(body, heading):
"""Extract value after a ### heading from GitHub Issue Form response."""
if not body:
return None
for line in body.split("\n"):
stripped = line.strip()
if stripped and not stripped.startswith("#") and not stripped.startswith("_") and not stripped.startswith("<!"):
prev_was_heading = False
for i, ln in enumerate(body.split("\n")):
if "affected service" in ln.lower():
prev_was_heading = True
continue
if prev_was_heading and ln.strip():
return ln.strip()
lines = body.split("\n")
for i, ln in enumerate(lines):
if heading.lower() in ln.lower() and ln.strip().startswith("#"):
for j in range(i + 1, len(lines)):
val = lines[j].strip()
if val and not val.startswith("#") and val != "_No response_":
return val
return None
def parse_user_report_context(body):
"""Extract structured fields from the issue form body."""
service = parse_form_field(body, "affected service")
# Strip parenthetical hints: "Nextcloud (files, calendar)" -> "Nextcloud"
if service and "(" in service:
service = service[:service.index("(")].strip()
return {
"service": service,
"error_type": parse_form_field(body, "what kind of error"),
"scope": parse_form_field(body, "is it just you"),
"when": parse_form_field(body, "when did it start"),
"url": parse_form_field(body, "url you were accessing"),
}
try:
issues_url = "https://api.github.com/repos/" + ISSUES_REPO + "/issues"
@ -435,7 +446,8 @@ try:
continue
if has_label(issue, "incident"):
continue # Already promoted to incident, skip duplicate
svc = parse_user_report_service(issue.get("body"))
ctx = parse_user_report_context(issue.get("body"))
svc = ctx["service"]
user_reports.append({
"id": issue["number"],
"title": issue["title"],
@ -443,6 +455,9 @@ try:
"status": "open",
"created_at": issue["created_at"],
"affected_services": [svc] if svc else [],
"error_type": ctx.get("error_type"),
"scope": ctx.get("scope"),
"when_started": ctx.get("when"),
"url": issue["html_url"],
})