sync: auto-sync from HOWARD-HOME at 2026-07-18 11:26:31
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-07-18 11:26:31
This commit is contained in:
406
.claude/skills/bug-tracker/scripts/tracker.py
Normal file
406
.claude/skills/bug-tracker/scripts/tracker.py
Normal file
@@ -0,0 +1,406 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ACG Bug & Project Tracker — Gitea Issues CLI.
|
||||
|
||||
Usage:
|
||||
py tracker.py list [--repo REPO] [--type TYPE] [--state STATE] [--client CLIENT] [--json]
|
||||
py tracker.py create --repo REPO --title TITLE [--body BODY] --type TYPE --priority PRIO
|
||||
[--component COMP] [--client CLIENT] [--assign WHO] [--discovered DATE]
|
||||
py tracker.py show REPO NUMBER
|
||||
py tracker.py comment REPO NUMBER --body TEXT
|
||||
py tracker.py label REPO NUMBER --add LABEL
|
||||
py tracker.py label REPO NUMBER --remove LABEL
|
||||
py tracker.py priority REPO NUMBER --set PRIO
|
||||
py tracker.py assign REPO NUMBER --to WHO
|
||||
py tracker.py close REPO NUMBER [--comment TEXT]
|
||||
py tracker.py reopen REPO NUMBER
|
||||
py tracker.py status
|
||||
|
||||
Environment:
|
||||
Reads Gitea API token from the SOPS vault via vault.sh.
|
||||
Falls back to GITEA_TOKEN env var if vault is unavailable.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
API = "https://git.azcomputerguru.com/api/v1"
|
||||
OWNER = "azcomputerguru"
|
||||
TRACKED_REPOS = ["gururmm", "guru-rmm", "guru-connect", "claudetools"]
|
||||
PRIORITY_LABELS = ["P1-critical", "P2-high", "P3-normal", "P4-low"]
|
||||
ASSIGN_LABELS = ["assigned:howard", "assigned:mike"]
|
||||
|
||||
# Resolve repo root for vault.sh
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
REPO_ROOT = SCRIPT_DIR.parent.parent.parent.parent # .claude/skills/bug-tracker/scripts -> repo root
|
||||
|
||||
|
||||
def get_token():
|
||||
"""Get Gitea API token from vault or env."""
|
||||
tok = os.environ.get("GITEA_TOKEN")
|
||||
if tok:
|
||||
return tok
|
||||
vault_sh = REPO_ROOT / ".claude" / "scripts" / "vault.sh"
|
||||
if vault_sh.exists():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["bash", str(vault_sh), "get-field", "services/gitea", "credentials.api.api-token"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
pass
|
||||
print("[ERROR] No Gitea token found. Set GITEA_TOKEN or configure vault.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def api(method, path, token, data=None):
|
||||
"""Make a Gitea API call using urllib (no external deps)."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
url = f"{API}{path}"
|
||||
headers = {"Authorization": f"token {token}", "Accept": "application/json"}
|
||||
body = None
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
body = json.dumps(data).encode()
|
||||
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
raw = resp.read().decode()
|
||||
return resp.status, json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode() if e.fp else ""
|
||||
try:
|
||||
return e.code, json.loads(raw)
|
||||
except Exception:
|
||||
return e.code, {"error": raw}
|
||||
except Exception as e:
|
||||
return 0, {"error": str(e)}
|
||||
|
||||
|
||||
def get_label_ids(token, repo):
|
||||
"""Fetch label name->id map for a repo."""
|
||||
code, labels = api("GET", f"/repos/{OWNER}/{repo}/labels?limit=200", token)
|
||||
if code != 200:
|
||||
return {}
|
||||
return {l["name"]: l["id"] for l in labels}
|
||||
|
||||
|
||||
def cmd_status(args, token):
|
||||
"""Show issue counts across all tracked repos."""
|
||||
print(f"{'Repo':<20} {'Open':>6} {'Closed':>8}")
|
||||
print("-" * 36)
|
||||
total_open = 0
|
||||
total_closed = 0
|
||||
for repo in TRACKED_REPOS:
|
||||
code, issues = api("GET", f"/repos/{OWNER}/{repo}/issues?state=open&type=issues&limit=1", token)
|
||||
# Gitea doesn't return total count easily; fetch up to 50
|
||||
code, open_issues = api("GET", f"/repos/{OWNER}/{repo}/issues?state=open&type=issues&limit=50", token)
|
||||
code2, closed_issues = api("GET", f"/repos/{OWNER}/{repo}/issues?state=closed&type=issues&limit=50", token)
|
||||
o = len(open_issues) if code == 200 else 0
|
||||
c = len(closed_issues) if code2 == 200 else 0
|
||||
total_open += o
|
||||
total_closed += c
|
||||
print(f"{repo:<20} {o:>6} {c:>8}")
|
||||
print("-" * 36)
|
||||
print(f"{'Total':<20} {total_open:>6} {total_closed:>8}")
|
||||
|
||||
|
||||
def cmd_list(args, token):
|
||||
"""List issues with optional filters."""
|
||||
repos = [args.repo] if args.repo else TRACKED_REPOS
|
||||
state = args.state or "open"
|
||||
all_issues = []
|
||||
|
||||
for repo in repos:
|
||||
page = 1
|
||||
while True:
|
||||
code, issues = api("GET",
|
||||
f"/repos/{OWNER}/{repo}/issues?state={state}&type=issues&limit=50&page={page}",
|
||||
token)
|
||||
if code != 200 or not issues:
|
||||
break
|
||||
for i in issues:
|
||||
i["_repo"] = repo
|
||||
all_issues.extend(issues)
|
||||
page += 1
|
||||
if len(issues) < 50:
|
||||
break
|
||||
|
||||
# Filter by type
|
||||
if args.type:
|
||||
all_issues = [i for i in all_issues if any(l["name"] == args.type for l in i.get("labels", []))]
|
||||
|
||||
# Filter by client
|
||||
if args.client:
|
||||
tag = args.client if args.client.startswith("client:") else f"client:{args.client}"
|
||||
all_issues = [i for i in all_issues if any(l["name"] == tag for l in i.get("labels", []))]
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(all_issues, indent=2, default=str))
|
||||
return
|
||||
|
||||
if not all_issues:
|
||||
print("[INFO] No issues found.")
|
||||
return
|
||||
|
||||
for i in sorted(all_issues, key=lambda x: x["number"]):
|
||||
labels = ", ".join(l["name"] for l in i.get("labels", []))
|
||||
print(f" {i['_repo']:<16} #{i['number']:<4} {i['title'][:60]}")
|
||||
if labels:
|
||||
print(f" {'':16} [{labels}]")
|
||||
|
||||
|
||||
def cmd_create(args, token):
|
||||
"""Create a new issue."""
|
||||
repo = args.repo
|
||||
lbl_map = get_label_ids(token, repo)
|
||||
label_ids = []
|
||||
|
||||
# Type label
|
||||
if args.type in lbl_map:
|
||||
label_ids.append(lbl_map[args.type])
|
||||
else:
|
||||
print(f"[ERROR] Type label '{args.type}' not found in {repo}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Priority label
|
||||
if args.priority in lbl_map:
|
||||
label_ids.append(lbl_map[args.priority])
|
||||
else:
|
||||
print(f"[ERROR] Priority label '{args.priority}' not found in {repo}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Optional component
|
||||
if args.component and args.component in lbl_map:
|
||||
label_ids.append(lbl_map[args.component])
|
||||
|
||||
# Optional client
|
||||
if args.client:
|
||||
tag = args.client if args.client.startswith("client:") else f"client:{args.client}"
|
||||
if tag in lbl_map:
|
||||
label_ids.append(lbl_map[tag])
|
||||
|
||||
# Optional assignee
|
||||
if args.assign:
|
||||
atag = args.assign if args.assign.startswith("assigned:") else f"assigned:{args.assign}"
|
||||
if atag in lbl_map:
|
||||
label_ids.append(lbl_map[atag])
|
||||
|
||||
body = args.body or ""
|
||||
if args.discovered:
|
||||
body = f"**Discovered:** {args.discovered}\n\n{body}"
|
||||
|
||||
data = {"title": args.title, "body": body, "labels": label_ids}
|
||||
code, result = api("POST", f"/repos/{OWNER}/{repo}/issues", token, data)
|
||||
|
||||
if code == 201:
|
||||
print(f"[OK] Created #{result['number']}: {result['title']}")
|
||||
print(f" URL: https://git.azcomputerguru.com/{OWNER}/{repo}/issues/{result['number']}")
|
||||
else:
|
||||
print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_show(args, token):
|
||||
"""Show issue detail + comments."""
|
||||
code, issue = api("GET", f"/repos/{OWNER}/{args.repo}/issues/{args.number}", token)
|
||||
if code != 200:
|
||||
print(f"[ERROR] HTTP {code}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
labels = ", ".join(l["name"] for l in issue.get("labels", []))
|
||||
print(f"#{issue['number']} [{issue['state']}] {issue['title']}")
|
||||
print(f"Labels: {labels}")
|
||||
print(f"Created: {issue['created_at'][:10]} Updated: {issue['updated_at'][:10]}")
|
||||
if issue.get("body"):
|
||||
print(f"\n{issue['body']}")
|
||||
|
||||
code2, comments = api("GET", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/comments", token)
|
||||
if code2 == 200 and comments:
|
||||
print(f"\n--- {len(comments)} comment(s) ---")
|
||||
for c in comments:
|
||||
print(f"\n {c['user']['login']} ({c['created_at'][:10]}):")
|
||||
print(f" {c['body']}")
|
||||
|
||||
|
||||
def cmd_comment(args, token):
|
||||
"""Add a comment to an issue."""
|
||||
code, result = api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/comments",
|
||||
token, {"body": args.body})
|
||||
if code == 201:
|
||||
print(f"[OK] Comment added to {args.repo} #{args.number}")
|
||||
else:
|
||||
print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_priority(args, token):
|
||||
"""Change issue priority (swap labels)."""
|
||||
lbl_map = get_label_ids(token, args.repo)
|
||||
# Remove existing priority labels
|
||||
for p in PRIORITY_LABELS:
|
||||
pid = lbl_map.get(p)
|
||||
if pid:
|
||||
api("DELETE", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels/{pid}", token)
|
||||
# Add new
|
||||
new_id = lbl_map.get(args.set)
|
||||
if new_id:
|
||||
api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels", token, {"labels": [new_id]})
|
||||
print(f"[OK] {args.repo} #{args.number} priority set to {args.set}")
|
||||
else:
|
||||
print(f"[ERROR] Priority label '{args.set}' not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_assign(args, token):
|
||||
"""Change issue assignee (swap labels)."""
|
||||
lbl_map = get_label_ids(token, args.repo)
|
||||
# Remove existing assign labels
|
||||
for a in ASSIGN_LABELS:
|
||||
aid = lbl_map.get(a)
|
||||
if aid:
|
||||
api("DELETE", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels/{aid}", token)
|
||||
# Add new
|
||||
atag = args.to if args.to.startswith("assigned:") else f"assigned:{args.to}"
|
||||
new_id = lbl_map.get(atag)
|
||||
if new_id:
|
||||
api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels", token, {"labels": [new_id]})
|
||||
print(f"[OK] {args.repo} #{args.number} assigned to {args.to}")
|
||||
else:
|
||||
print(f"[ERROR] Assignee label '{atag}' not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_close(args, token):
|
||||
"""Close an issue, optionally with a comment."""
|
||||
if args.comment:
|
||||
api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/comments",
|
||||
token, {"body": args.comment})
|
||||
code, result = api("PATCH", f"/repos/{OWNER}/{args.repo}/issues/{args.number}",
|
||||
token, {"state": "closed"})
|
||||
if code == 201:
|
||||
print(f"[OK] Closed {args.repo} #{args.number}")
|
||||
else:
|
||||
print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
|
||||
|
||||
|
||||
def cmd_reopen(args, token):
|
||||
"""Reopen an issue."""
|
||||
code, result = api("PATCH", f"/repos/{OWNER}/{args.repo}/issues/{args.number}",
|
||||
token, {"state": "open"})
|
||||
if code == 201:
|
||||
print(f"[OK] Reopened {args.repo} #{args.number}")
|
||||
else:
|
||||
print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
|
||||
|
||||
|
||||
def cmd_label(args, token):
|
||||
"""Add or remove a label."""
|
||||
lbl_map = get_label_ids(token, args.repo)
|
||||
if args.add:
|
||||
lid = lbl_map.get(args.add)
|
||||
if not lid:
|
||||
print(f"[ERROR] Label '{args.add}' not found in {args.repo}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels", token, {"labels": [lid]})
|
||||
print(f"[OK] Added '{args.add}' to {args.repo} #{args.number}")
|
||||
elif args.remove:
|
||||
lid = lbl_map.get(args.remove)
|
||||
if not lid:
|
||||
print(f"[ERROR] Label '{args.remove}' not found in {args.repo}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
api("DELETE", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels/{lid}", token)
|
||||
print(f"[OK] Removed '{args.remove}' from {args.repo} #{args.number}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="ACG Bug & Project Tracker")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# status
|
||||
sub.add_parser("status", help="Show issue counts")
|
||||
|
||||
# list
|
||||
p_list = sub.add_parser("list", help="List issues")
|
||||
p_list.add_argument("--repo", choices=TRACKED_REPOS)
|
||||
p_list.add_argument("--type", choices=["bug", "feature", "skill", "project", "security", "client-project"])
|
||||
p_list.add_argument("--state", choices=["open", "closed", "all"], default="open")
|
||||
p_list.add_argument("--client")
|
||||
p_list.add_argument("--json", action="store_true")
|
||||
|
||||
# create
|
||||
p_create = sub.add_parser("create", help="Create an issue")
|
||||
p_create.add_argument("--repo", required=True, choices=TRACKED_REPOS)
|
||||
p_create.add_argument("--title", required=True)
|
||||
p_create.add_argument("--body", default="")
|
||||
p_create.add_argument("--type", required=True, choices=["bug", "feature", "skill", "project", "security", "client-project"])
|
||||
p_create.add_argument("--priority", required=True, choices=PRIORITY_LABELS)
|
||||
p_create.add_argument("--component")
|
||||
p_create.add_argument("--client")
|
||||
p_create.add_argument("--assign", choices=["howard", "mike"])
|
||||
p_create.add_argument("--discovered")
|
||||
|
||||
# show
|
||||
p_show = sub.add_parser("show", help="Show issue detail")
|
||||
p_show.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_show.add_argument("number", type=int)
|
||||
|
||||
# comment
|
||||
p_comment = sub.add_parser("comment", help="Add a comment")
|
||||
p_comment.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_comment.add_argument("number", type=int)
|
||||
p_comment.add_argument("--body", required=True)
|
||||
|
||||
# priority
|
||||
p_prio = sub.add_parser("priority", help="Change priority")
|
||||
p_prio.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_prio.add_argument("number", type=int)
|
||||
p_prio.add_argument("--set", required=True, choices=PRIORITY_LABELS)
|
||||
|
||||
# assign
|
||||
p_assign = sub.add_parser("assign", help="Change assignee")
|
||||
p_assign.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_assign.add_argument("number", type=int)
|
||||
p_assign.add_argument("--to", required=True, choices=["howard", "mike"])
|
||||
|
||||
# close
|
||||
p_close = sub.add_parser("close", help="Close an issue")
|
||||
p_close.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_close.add_argument("number", type=int)
|
||||
p_close.add_argument("--comment")
|
||||
|
||||
# reopen
|
||||
p_reopen = sub.add_parser("reopen", help="Reopen an issue")
|
||||
p_reopen.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_reopen.add_argument("number", type=int)
|
||||
|
||||
# label
|
||||
p_label = sub.add_parser("label", help="Add/remove a label")
|
||||
p_label.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_label.add_argument("number", type=int)
|
||||
grp = p_label.add_mutually_exclusive_group(required=True)
|
||||
grp.add_argument("--add")
|
||||
grp.add_argument("--remove")
|
||||
|
||||
args = parser.parse_args()
|
||||
token = get_token()
|
||||
|
||||
commands = {
|
||||
"status": cmd_status, "list": cmd_list, "create": cmd_create,
|
||||
"show": cmd_show, "comment": cmd_comment, "priority": cmd_priority,
|
||||
"assign": cmd_assign, "close": cmd_close, "reopen": cmd_reopen,
|
||||
"label": cmd_label,
|
||||
}
|
||||
commands[args.command](args, token)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,90 +1,110 @@
|
||||
---
|
||||
name: bug-tracker
|
||||
description: "File, list, update, and close bugs/features/tasks in the Gitea issue tracker across GuruRMM, GuruConnect, and ClaudeTools repos. Visual dashboard at projects/msp-tools/bug-tracker/dashboard.html."
|
||||
triggers:
|
||||
- "file a bug"
|
||||
- "track a bug"
|
||||
- "open an issue"
|
||||
- "bug tracker"
|
||||
- "what bugs are open"
|
||||
- "list bugs"
|
||||
- "close issue"
|
||||
- "update issue"
|
||||
- "mark fixed"
|
||||
- "what's being worked on"
|
||||
description: "File, list, update, and close bugs/features/tasks/client-projects in the Gitea issue tracker across GuruRMM, GuruConnect, and ClaudeTools repos. Live dashboard at tracker.azcomputerguru.com. Triggers: file a bug, track a bug, open an issue, bug tracker, what bugs are open, list bugs, close issue, update issue, mark fixed, what is being worked on."
|
||||
---
|
||||
|
||||
# Bug & Project Tracker — Gitea Issues
|
||||
|
||||
Manage the ACG bug/feature/skill/project tracker via Gitea Issues API.
|
||||
Dashboard: `projects/msp-tools/bug-tracker/dashboard.html`
|
||||
Live client for the ACG bug/feature/skill/project/client-project tracker. All data lives in
|
||||
Gitea Issues — no separate database. The web dashboard and this skill are two interfaces
|
||||
to the same data.
|
||||
|
||||
## Setup
|
||||
- **Dashboard:** https://tracker.azcomputerguru.com/dashboard.html
|
||||
- **Direct:** http://172.16.3.20:8089/dashboard.html
|
||||
|
||||
- Gitea: `https://git.azcomputerguru.com/api/v1`
|
||||
- Owner: `azcomputerguru`
|
||||
- Token: vault `services/gitea` → `credentials.api.api-token`
|
||||
- Tracked repos: `gururmm`, `guru-connect`, `claudetools`
|
||||
## Running
|
||||
|
||||
```bash
|
||||
BT=".claude/skills/bug-tracker/scripts/tracker.py"
|
||||
|
||||
# --- reads ---
|
||||
py "$BT" status # issue counts across all repos
|
||||
py "$BT" list # all open issues, all repos
|
||||
py "$BT" list --repo gururmm --type bug # gururmm bugs only
|
||||
py "$BT" list --type client-project --client cascades # Cascades client projects
|
||||
py "$BT" list --state closed --json # closed issues as JSON
|
||||
py "$BT" show gururmm 39 # detail + comments for gururmm #39
|
||||
|
||||
# --- writes ---
|
||||
py "$BT" create --repo claudetools --title "vault: X fails" \
|
||||
--body "Error detail here" --type bug --priority P2-high \
|
||||
--component component:harness --discovered 2026-07-18
|
||||
|
||||
py "$BT" create --repo claudetools --title "Cascades: new project" \
|
||||
--body "Description" --type client-project --priority P3-normal \
|
||||
--client cascades --assign howard --discovered 2026-07-18
|
||||
|
||||
py "$BT" comment gururmm 39 --body "Tried X, still reproduces"
|
||||
py "$BT" priority gururmm 39 --set P1-critical
|
||||
py "$BT" assign claudetools 5 --to mike
|
||||
py "$BT" label gururmm 39 --add in-progress
|
||||
py "$BT" label gururmm 39 --remove blocked
|
||||
py "$BT" close claudetools 7 --comment "Fixed in commit abc123"
|
||||
py "$BT" reopen claudetools 7
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
Token loaded at runtime from the SOPS vault (never hardcoded):
|
||||
**`services/gitea`** -> `credentials.api.api-token`.
|
||||
Falls back to `GITEA_TOKEN` env var if vault is unavailable.
|
||||
|
||||
## Tracked repos
|
||||
|
||||
| Repo | What it tracks |
|
||||
|------|---------------|
|
||||
| `gururmm` | RMM agent/server/dashboard bugs, features, security |
|
||||
| `guru-rmm` | RMM code review tracking |
|
||||
| `guru-connect` | GuruConnect bugs, security audit findings |
|
||||
| `claudetools` | Harness/skill bugs, skill work, projects, **all client projects** |
|
||||
|
||||
## Label scheme
|
||||
|
||||
| Label | Purpose |
|
||||
|-------|---------|
|
||||
| `bug` | Something broken |
|
||||
| `feature` | New capability |
|
||||
| `skill` | Skill work |
|
||||
| `project` | Project-level |
|
||||
| `P1-critical` / `P2-high` / `P3-normal` / `P4-low` | Priority |
|
||||
| `in-progress` | Being worked |
|
||||
| `blocked` | Blocked |
|
||||
| `dropped` | Stalled |
|
||||
| `fixed` | Fix applied |
|
||||
| `verified` | Confirmed working |
|
||||
| `security` | Security-related |
|
||||
| `component:rmm-agent` / `rmm-server` / `rmm-dashboard` / `guruconnect` / `harness` | Component |
|
||||
| Category | Labels |
|
||||
|----------|--------|
|
||||
| Type | `bug` `feature` `skill` `project` `security` `client-project` |
|
||||
| Priority | `P1-critical` `P2-high` `P3-normal` `P4-low` |
|
||||
| Status | `in-progress` `blocked` `dropped` `fixed` `verified` `wontfix` |
|
||||
| Component | `component:rmm-agent` `component:rmm-server` `component:rmm-dashboard` `component:guruconnect` `component:harness` |
|
||||
| Client | `client:<slug>` (e.g. `client:cascades`, `client:dataforth`) |
|
||||
| Assignee | `assigned:howard` `assigned:mike` |
|
||||
|
||||
## Operations
|
||||
|
||||
### File a new issue
|
||||
Read the token from vault. POST to `/repos/{owner}/{repo}/issues`:
|
||||
```json
|
||||
{
|
||||
"title": "...",
|
||||
"body": "## Problem\n...\n\n## Steps to reproduce\n...\n\n## Expected behavior\n...\n\n## Session context\nFiled from session on MACHINE at DATE",
|
||||
"labels": [label_id, label_id, ...]
|
||||
}
|
||||
```
|
||||
Always include: one type label (bug/feature/skill/project), one priority label, component label(s) if applicable.
|
||||
|
||||
### List open issues
|
||||
GET `/repos/{owner}/{repo}/issues?state=open&type=issues&limit=50`
|
||||
For cross-repo view, query all tracked repos and merge results.
|
||||
|
||||
### Update an issue (add progress, change status)
|
||||
- Add a comment: POST `/repos/{owner}/{repo}/issues/{number}/comments` with `{"body": "..."}`
|
||||
- Change labels: PATCH `/repos/{owner}/{repo}/issues/{number}` or use label endpoints
|
||||
- Close: PATCH with `{"state": "closed"}`
|
||||
|
||||
### Mark fixed
|
||||
Add `fixed` label, add a comment describing the fix, reference the commit if available.
|
||||
|
||||
### Mark verified
|
||||
Add `verified` label, close the issue.
|
||||
Always include: one type label + one priority label. Add component/client/assignee as applicable.
|
||||
|
||||
## Auto-filing (IMPORTANT)
|
||||
|
||||
When a bug surfaces during a Claude session — a command fails unexpectedly, a skill hits an error that needs follow-up, or the user reports something broken — **file it as an issue automatically** (with user confirmation for the title/priority). This is the whole point: bugs should not require someone to remember them.
|
||||
When a bug surfaces during a Claude session — a command fails unexpectedly, a skill
|
||||
hits an error that needs follow-up, or the user reports something broken — **file it
|
||||
as an issue** using this script (with user confirmation for title/priority). This is
|
||||
the whole point: bugs should not require someone to remember them.
|
||||
|
||||
The issue body should include:
|
||||
- What happened (error message, unexpected behavior)
|
||||
- Context (what was being attempted, which machine, which session)
|
||||
- Any workaround applied
|
||||
- Links to relevant session logs if available
|
||||
- A `**Discovered:** YYYY-MM-DD` line so the dashboard shows accurate age
|
||||
|
||||
## Dashboard
|
||||
Do NOT file:
|
||||
- Client machine problems (PacketDial 404s, Intune device metrics, client network issues)
|
||||
- Vendor API limitations we cannot fix
|
||||
- One-off env/friction issues that are already resolved
|
||||
|
||||
Open `projects/msp-tools/bug-tracker/dashboard.html` in a browser.
|
||||
Pass the Gitea token as a URL parameter: `?token=xxx`
|
||||
It shows a kanban board (Open / In Progress / Blocked / Done) with filtering by repo and type.
|
||||
## Error logging
|
||||
|
||||
On any failure (API error, label not found, vault unavailable), log it:
|
||||
```bash
|
||||
bash .claude/scripts/log-skill-error.sh "bug-tracker" "brief error description" --context "cmd=create repo=claudetools"
|
||||
```
|
||||
|
||||
## Relationship to the dashboard
|
||||
|
||||
The dashboard at tracker.azcomputerguru.com is a single-page HTML app that makes the
|
||||
same Gitea API calls this script does. There is no sync, no cache, no separate database.
|
||||
An issue created by this script appears on the dashboard immediately. An issue edited
|
||||
on the dashboard is visible to this script immediately. They are the same data.
|
||||
|
||||
Dashboard infrastructure: Docker container `acg-tracker` on Jupiter (172.16.3.20:8089),
|
||||
proxied through NPM with Let's Encrypt SSL. Files at `/mnt/user/appdata/acg-tracker/`.
|
||||
To update the dashboard: upload new `dashboard.html` via SFTP, `docker restart acg-tracker`.
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
@@ -562,9 +562,19 @@ async function showDetail(repo, num) {
|
||||
m.innerHTML = `
|
||||
<button class="xbtn" onclick="closeDetail()">×</button>
|
||||
<div class="m-repo">${REPO_NAMES[repo]||repo} #${num} <span class="sbadge s-${st}" style="margin-left:6px">${st}</span></div>
|
||||
<h2>${esc(issue.title)}</h2>
|
||||
<div class="m-labels">${labels.map(l=>`<span class="tag" style="${tagStyle(l)}">${esc(l.name)}</span>`).join('')}</div>
|
||||
<div class="m-body">${body}</div>
|
||||
<div id="detail-view-mode">
|
||||
<h2>${esc(issue.title)} <span style="font-size:12px;color:var(--muted);cursor:pointer;margin-left:8px;font-weight:400" onclick="enterEditMode('${repo}',${num})">[edit]</span></h2>
|
||||
<div class="m-labels">${labels.map(l=>`<span class="tag" style="${tagStyle(l)}">${esc(l.name)}</span>`).join('')}</div>
|
||||
<div class="m-body">${body}</div>
|
||||
</div>
|
||||
<div id="detail-edit-mode" style="display:none">
|
||||
<div class="fg"><label>Title</label><input type="text" id="edit-title" value="${esc(issue.title).replace(/"/g,'"')}"></div>
|
||||
<div class="fg"><label>Description</label><textarea id="edit-body" style="min-height:150px">${esc(issue.body||'')}</textarea></div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn btn-success" style="font-size:12px" onclick="saveEdit('${repo}',${num})">Save</button>
|
||||
<button class="btn" style="font-size:12px" onclick="cancelEdit()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-meta">
|
||||
Created ${new Date(issue.created_at).toLocaleString()} ·
|
||||
Updated ${new Date(issue.updated_at).toLocaleString()} ·
|
||||
@@ -610,6 +620,26 @@ async function showDetail(repo, num) {
|
||||
}
|
||||
function closeDetail() { document.getElementById('detail-overlay').classList.remove('open'); }
|
||||
|
||||
function enterEditMode() {
|
||||
document.getElementById('detail-view-mode').style.display='none';
|
||||
document.getElementById('detail-edit-mode').style.display='block';
|
||||
document.getElementById('edit-title').focus();
|
||||
}
|
||||
function cancelEdit() {
|
||||
document.getElementById('detail-view-mode').style.display='block';
|
||||
document.getElementById('detail-edit-mode').style.display='none';
|
||||
}
|
||||
async function saveEdit(repo, num) {
|
||||
const title = document.getElementById('edit-title').value.trim();
|
||||
const body = document.getElementById('edit-body').value;
|
||||
if (!title) return;
|
||||
await fetch(`${API}/repos/${OWNER}/${repo}/issues/${num}`, {
|
||||
method:'PATCH', headers:{...headers(),'Content-Type':'application/json'},
|
||||
body: JSON.stringify({title, body})
|
||||
});
|
||||
showDetail(repo, num); loadIssues();
|
||||
}
|
||||
|
||||
async function addComment(repo,num) {
|
||||
const t=document.getElementById('cmt-text').value.trim(); if(!t) return;
|
||||
await fetch(`${API}/repos/${OWNER}/${repo}/issues/${num}/comments`, {method:'POST', headers:{...headers(),'Content-Type':'application/json'}, body:JSON.stringify({body:t})});
|
||||
|
||||
Reference in New Issue
Block a user