Add bug status tracking with API and UI
Implements full bug lifecycle management (open → in_progress → resolved): Bug Watcher (testing/oversight/bug_watcher.py): - Add BugStatus enum with open/in_progress/resolved states - Add SQLite persistence with status tracking and indexes - New methods: update_bug_status(), get_bug(), log_bug() - Extended CLI: update, get, log commands with filters API Endpoints (ui/server.ts): - GET /api/bugs - List bugs with status/severity/phase filters - GET /api/bugs/summary - Bug statistics by status and severity - GET /api/bugs/:id - Single bug details - POST /api/bugs - Log new bug - PATCH /api/bugs/:id - Update bug status UI Dashboard: - New "Bugs" tab with summary cards (Total/Open/In Progress/Resolved) - Filter dropdowns for status and severity - Bug list with status badges and severity indicators - Detail panel with action buttons for status transitions - WebSocket broadcasts for real-time updates CLI Wrapper (bin/bugs): - bugs list [--status X] [--severity Y] - bugs get <id> - bugs log -m "message" [--severity high] - bugs update <id> <status> [--notes "..."] - bugs status Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ccc3b01609
commit
a304895249
81
bin/bugs
Executable file
81
bin/bugs
Executable file
@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Bug Tracking CLI
|
||||||
|
# Usage: bugs <command> [options]
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WATCHER_SCRIPT="/opt/agent-governance/testing/oversight/bug_watcher.py"
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo "Bug Tracking CLI"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: bugs <command> [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " list List all bugs"
|
||||||
|
echo " list --status open Filter by status (open/in_progress/resolved)"
|
||||||
|
echo " list --severity high Filter by severity (critical/high/medium/low)"
|
||||||
|
echo " get <id> Get details of a specific bug"
|
||||||
|
echo " log <message> Log a new bug"
|
||||||
|
echo " update <id> <status> Update bug status"
|
||||||
|
echo " scan Scan for anomalies"
|
||||||
|
echo " status Show bug summary"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " bugs list --status open"
|
||||||
|
echo " bugs log -m 'API timeout in pipeline' --severity high"
|
||||||
|
echo " bugs update anom-abc123 resolved --notes 'Fixed in commit xyz'"
|
||||||
|
echo " bugs get anom-abc123"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
shift
|
||||||
|
python3 "$WATCHER_SCRIPT" list "$@"
|
||||||
|
;;
|
||||||
|
get)
|
||||||
|
shift
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: Bug ID required"
|
||||||
|
echo "Usage: bugs get <bug-id>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
python3 "$WATCHER_SCRIPT" get --id "$1"
|
||||||
|
;;
|
||||||
|
log)
|
||||||
|
shift
|
||||||
|
python3 "$WATCHER_SCRIPT" log "$@"
|
||||||
|
;;
|
||||||
|
update)
|
||||||
|
shift
|
||||||
|
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||||
|
echo "Error: Bug ID and status required"
|
||||||
|
echo "Usage: bugs update <bug-id> <status> [--notes 'note']"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
BUG_ID="$1"
|
||||||
|
STATUS="$2"
|
||||||
|
shift 2
|
||||||
|
python3 "$WATCHER_SCRIPT" update --id "$BUG_ID" --set-status "$STATUS" "$@"
|
||||||
|
;;
|
||||||
|
scan)
|
||||||
|
shift
|
||||||
|
python3 "$WATCHER_SCRIPT" scan "$@"
|
||||||
|
;;
|
||||||
|
status|summary)
|
||||||
|
shift
|
||||||
|
python3 "$WATCHER_SCRIPT" status "$@"
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
show_help
|
||||||
|
else
|
||||||
|
echo "Unknown command: $1"
|
||||||
|
echo "Run 'bugs help' for usage"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -45,6 +45,13 @@ class Severity(str, Enum):
|
|||||||
INFO = "info" # Tracking only
|
INFO = "info" # Tracking only
|
||||||
|
|
||||||
|
|
||||||
|
class BugStatus(str, Enum):
|
||||||
|
"""Status tracking for bugs/anomalies"""
|
||||||
|
OPEN = "open" # Newly detected, not yet addressed
|
||||||
|
IN_PROGRESS = "in_progress" # Being worked on
|
||||||
|
RESOLVED = "resolved" # Fixed and verified
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Anomaly:
|
class Anomaly:
|
||||||
"""Represents a detected anomaly"""
|
"""Represents a detected anomaly"""
|
||||||
@ -60,14 +67,23 @@ class Anomaly:
|
|||||||
checkpoint_id: Optional[str] = None
|
checkpoint_id: Optional[str] = None
|
||||||
status_file: Optional[str] = None
|
status_file: Optional[str] = None
|
||||||
detected_at: str = ""
|
detected_at: str = ""
|
||||||
resolved: bool = False
|
# Status tracking
|
||||||
|
status: BugStatus = BugStatus.OPEN
|
||||||
|
resolved: bool = False # Kept for backwards compatibility
|
||||||
resolution_notes: Optional[str] = None
|
resolution_notes: Optional[str] = None
|
||||||
|
assigned_to: Optional[str] = None
|
||||||
|
updated_at: Optional[str] = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if not self.detected_at:
|
if not self.detected_at:
|
||||||
self.detected_at = datetime.now(timezone.utc).isoformat()
|
self.detected_at = datetime.now(timezone.utc).isoformat()
|
||||||
if not self.id:
|
if not self.id:
|
||||||
self.id = f"anom-{hashlib.sha256(f'{self.type}{self.phase}{self.message}{self.detected_at}'.encode()).hexdigest()[:12]}"
|
self.id = f"anom-{hashlib.sha256(f'{self.type}{self.phase}{self.message}{self.detected_at}'.encode()).hexdigest()[:12]}"
|
||||||
|
# Sync resolved with status for backwards compatibility
|
||||||
|
if self.resolved and self.status == BugStatus.OPEN:
|
||||||
|
self.status = BugStatus.RESOLVED
|
||||||
|
elif self.status == BugStatus.RESOLVED:
|
||||||
|
self.resolved = True
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -129,11 +145,13 @@ class BugWindowWatcher:
|
|||||||
def __init__(self, base_path: str = "/opt/agent-governance"):
|
def __init__(self, base_path: str = "/opt/agent-governance"):
|
||||||
self.base_path = Path(base_path)
|
self.base_path = Path(base_path)
|
||||||
self.ledger_db = self.base_path / "ledger" / "governance.db"
|
self.ledger_db = self.base_path / "ledger" / "governance.db"
|
||||||
|
self.bug_db = self.base_path / "testing" / "oversight" / "bug_watcher.db"
|
||||||
self.checkpoint_dir = self.base_path / "checkpoint" / "storage"
|
self.checkpoint_dir = self.base_path / "checkpoint" / "storage"
|
||||||
self.state = WatcherState()
|
self.state = WatcherState()
|
||||||
self.anomalies: list[Anomaly] = []
|
self.anomalies: list[Anomaly] = []
|
||||||
self._redis: Optional[redis.Redis] = None
|
self._redis: Optional[redis.Redis] = None
|
||||||
self._setup_redis()
|
self._setup_redis()
|
||||||
|
self._setup_bug_db()
|
||||||
|
|
||||||
def _setup_redis(self):
|
def _setup_redis(self):
|
||||||
"""Connect to DragonflyDB for real-time state"""
|
"""Connect to DragonflyDB for real-time state"""
|
||||||
@ -148,6 +166,43 @@ class BugWindowWatcher:
|
|||||||
except Exception:
|
except Exception:
|
||||||
self._redis = None
|
self._redis = None
|
||||||
|
|
||||||
|
def _setup_bug_db(self):
|
||||||
|
"""Initialize SQLite database for bug tracking"""
|
||||||
|
conn = sqlite3.connect(self.bug_db)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS bugs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
severity TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'open',
|
||||||
|
phase INTEGER NOT NULL,
|
||||||
|
phase_name TEXT NOT NULL,
|
||||||
|
directory TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
details TEXT,
|
||||||
|
stack_trace TEXT,
|
||||||
|
checkpoint_id TEXT,
|
||||||
|
status_file TEXT,
|
||||||
|
detected_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT,
|
||||||
|
resolved_at TEXT,
|
||||||
|
resolution_notes TEXT,
|
||||||
|
assigned_to TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bugs_status ON bugs(status)
|
||||||
|
""")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bugs_severity ON bugs(severity)
|
||||||
|
""")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bugs_phase ON bugs(phase)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def _now(self) -> str:
|
def _now(self) -> str:
|
||||||
return datetime.now(timezone.utc).isoformat()
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
@ -545,7 +600,46 @@ class BugWindowWatcher:
|
|||||||
return anomalies
|
return anomalies
|
||||||
|
|
||||||
def _persist_anomalies(self, anomalies: list[Anomaly]):
|
def _persist_anomalies(self, anomalies: list[Anomaly]):
|
||||||
"""Persist anomalies to storage"""
|
"""Persist anomalies to storage (Redis + SQLite)"""
|
||||||
|
# Persist to SQLite
|
||||||
|
conn = sqlite3.connect(self.bug_db)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
for anomaly in anomalies:
|
||||||
|
# Convert enum values to strings for storage
|
||||||
|
type_val = anomaly.type.value if hasattr(anomaly.type, 'value') else anomaly.type
|
||||||
|
sev_val = anomaly.severity.value if hasattr(anomaly.severity, 'value') else anomaly.severity
|
||||||
|
status_val = anomaly.status.value if hasattr(anomaly.status, 'value') else anomaly.status
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT OR REPLACE INTO bugs
|
||||||
|
(id, type, severity, status, phase, phase_name, directory, message,
|
||||||
|
details, stack_trace, checkpoint_id, status_file, detected_at,
|
||||||
|
updated_at, resolution_notes, assigned_to)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
anomaly.id,
|
||||||
|
type_val,
|
||||||
|
sev_val,
|
||||||
|
status_val,
|
||||||
|
anomaly.phase,
|
||||||
|
anomaly.phase_name,
|
||||||
|
anomaly.directory,
|
||||||
|
anomaly.message,
|
||||||
|
json.dumps(anomaly.details) if anomaly.details else None,
|
||||||
|
anomaly.stack_trace,
|
||||||
|
anomaly.checkpoint_id,
|
||||||
|
anomaly.status_file,
|
||||||
|
anomaly.detected_at,
|
||||||
|
anomaly.updated_at,
|
||||||
|
anomaly.resolution_notes,
|
||||||
|
anomaly.assigned_to
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Also persist to Redis for real-time access
|
||||||
if not self._redis:
|
if not self._redis:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -560,65 +654,201 @@ class BugWindowWatcher:
|
|||||||
self._redis.ltrim("oversight:anomalies", 0, 999)
|
self._redis.ltrim("oversight:anomalies", 0, 999)
|
||||||
|
|
||||||
# Index by severity
|
# Index by severity
|
||||||
self._redis.sadd(f"oversight:anomalies:{anomaly.severity.value}", anomaly.id)
|
sev_val = anomaly.severity.value if hasattr(anomaly.severity, 'value') else anomaly.severity
|
||||||
|
self._redis.sadd(f"oversight:anomalies:{sev_val}", anomaly.id)
|
||||||
|
|
||||||
# Index by phase
|
# Index by phase
|
||||||
self._redis.sadd(f"oversight:anomalies:phase:{anomaly.phase}", anomaly.id)
|
self._redis.sadd(f"oversight:anomalies:phase:{anomaly.phase}", anomaly.id)
|
||||||
|
|
||||||
|
# Index by status
|
||||||
|
status_val = anomaly.status.value if hasattr(anomaly.status, 'value') else anomaly.status
|
||||||
|
self._redis.sadd(f"oversight:anomalies:status:{status_val}", anomaly.id)
|
||||||
|
|
||||||
def get_anomalies(
|
def get_anomalies(
|
||||||
self,
|
self,
|
||||||
severity: Optional[Severity] = None,
|
severity: Optional[Severity] = None,
|
||||||
phase: Optional[int] = None,
|
phase: Optional[int] = None,
|
||||||
|
status: Optional[BugStatus] = None,
|
||||||
limit: int = 50
|
limit: int = 50
|
||||||
) -> list[Anomaly]:
|
) -> list[Anomaly]:
|
||||||
"""Retrieve anomalies with optional filters"""
|
"""Retrieve anomalies with optional filters from SQLite"""
|
||||||
if not self._redis:
|
conn = sqlite3.connect(self.bug_db)
|
||||||
# Return in-memory anomalies
|
conn.row_factory = sqlite3.Row
|
||||||
filtered = self.anomalies
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = "SELECT * FROM bugs WHERE 1=1"
|
||||||
|
params = []
|
||||||
|
|
||||||
if severity:
|
if severity:
|
||||||
filtered = [a for a in filtered if a.severity == severity]
|
sev_val = severity.value if hasattr(severity, 'value') else severity
|
||||||
|
query += " AND severity = ?"
|
||||||
|
params.append(sev_val)
|
||||||
|
|
||||||
if phase:
|
if phase:
|
||||||
filtered = [a for a in filtered if a.phase == phase]
|
query += " AND phase = ?"
|
||||||
return filtered[:limit]
|
params.append(phase)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
status_val = status.value if hasattr(status, 'value') else status
|
||||||
|
query += " AND status = ?"
|
||||||
|
params.append(status_val)
|
||||||
|
|
||||||
|
query += " ORDER BY detected_at DESC LIMIT ?"
|
||||||
|
params.append(limit)
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
# Get from Redis
|
|
||||||
raw = self._redis.lrange("oversight:anomalies", 0, limit - 1)
|
|
||||||
anomalies = []
|
anomalies = []
|
||||||
|
for row in rows:
|
||||||
for item in raw:
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(item)
|
anomaly = Anomaly(
|
||||||
anomaly = Anomaly(**data)
|
id=row['id'],
|
||||||
|
type=AnomalyType(row['type']),
|
||||||
if severity and anomaly.severity != severity:
|
severity=Severity(row['severity']),
|
||||||
continue
|
status=BugStatus(row['status']),
|
||||||
if phase and anomaly.phase != phase:
|
phase=row['phase'],
|
||||||
continue
|
phase_name=row['phase_name'],
|
||||||
|
directory=row['directory'],
|
||||||
|
message=row['message'],
|
||||||
|
details=json.loads(row['details']) if row['details'] else {},
|
||||||
|
stack_trace=row['stack_trace'],
|
||||||
|
checkpoint_id=row['checkpoint_id'],
|
||||||
|
status_file=row['status_file'],
|
||||||
|
detected_at=row['detected_at'],
|
||||||
|
updated_at=row['updated_at'],
|
||||||
|
resolution_notes=row['resolution_notes'],
|
||||||
|
assigned_to=row['assigned_to'],
|
||||||
|
resolved=row['status'] == 'resolved'
|
||||||
|
)
|
||||||
anomalies.append(anomaly)
|
anomalies.append(anomaly)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return anomalies
|
return anomalies
|
||||||
|
|
||||||
def acknowledge_anomaly(self, anomaly_id: str, notes: str = "") -> bool:
|
def update_bug_status(
|
||||||
"""Mark an anomaly as resolved"""
|
self,
|
||||||
if not self._redis:
|
bug_id: str,
|
||||||
for anomaly in self.anomalies:
|
new_status: BugStatus,
|
||||||
if anomaly.id == anomaly_id:
|
notes: Optional[str] = None,
|
||||||
anomaly.resolved = True
|
assigned_to: Optional[str] = None
|
||||||
anomaly.resolution_notes = notes
|
) -> bool:
|
||||||
return True
|
"""Update bug status with optional notes and assignment"""
|
||||||
return False
|
conn = sqlite3.connect(self.bug_db)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Update in Redis
|
now = self._now()
|
||||||
self._redis.hset(f"oversight:anomaly:{anomaly_id}", mapping={
|
status_val = new_status.value if hasattr(new_status, 'value') else new_status
|
||||||
"resolved": "true",
|
|
||||||
"resolution_notes": notes,
|
# Build update query
|
||||||
"resolved_at": self._now()
|
updates = ["status = ?", "updated_at = ?"]
|
||||||
|
params = [status_val, now]
|
||||||
|
|
||||||
|
if notes is not None:
|
||||||
|
updates.append("resolution_notes = ?")
|
||||||
|
params.append(notes)
|
||||||
|
|
||||||
|
if assigned_to is not None:
|
||||||
|
updates.append("assigned_to = ?")
|
||||||
|
params.append(assigned_to)
|
||||||
|
|
||||||
|
if new_status == BugStatus.RESOLVED:
|
||||||
|
updates.append("resolved_at = ?")
|
||||||
|
params.append(now)
|
||||||
|
|
||||||
|
params.append(bug_id)
|
||||||
|
|
||||||
|
cursor.execute(f"""
|
||||||
|
UPDATE bugs SET {', '.join(updates)} WHERE id = ?
|
||||||
|
""", params)
|
||||||
|
|
||||||
|
updated = cursor.rowcount > 0
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Update Redis index if available
|
||||||
|
if self._redis and updated:
|
||||||
|
# Remove from old status sets, add to new
|
||||||
|
for s in BugStatus:
|
||||||
|
self._redis.srem(f"oversight:anomalies:status:{s.value}", bug_id)
|
||||||
|
self._redis.sadd(f"oversight:anomalies:status:{status_val}", bug_id)
|
||||||
|
|
||||||
|
self._redis.hset(f"oversight:anomaly:{bug_id}", mapping={
|
||||||
|
"status": status_val,
|
||||||
|
"updated_at": now,
|
||||||
|
"resolution_notes": notes or "",
|
||||||
|
"assigned_to": assigned_to or ""
|
||||||
})
|
})
|
||||||
|
|
||||||
return True
|
return updated
|
||||||
|
|
||||||
|
def acknowledge_anomaly(self, anomaly_id: str, notes: str = "") -> bool:
|
||||||
|
"""Mark an anomaly as resolved (backwards compatible)"""
|
||||||
|
return self.update_bug_status(anomaly_id, BugStatus.RESOLVED, notes)
|
||||||
|
|
||||||
|
def get_bug(self, bug_id: str) -> Optional[Anomaly]:
|
||||||
|
"""Get a single bug by ID"""
|
||||||
|
conn = sqlite3.connect(self.bug_db)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM bugs WHERE id = ?", (bug_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return Anomaly(
|
||||||
|
id=row['id'],
|
||||||
|
type=AnomalyType(row['type']),
|
||||||
|
severity=Severity(row['severity']),
|
||||||
|
status=BugStatus(row['status']),
|
||||||
|
phase=row['phase'],
|
||||||
|
phase_name=row['phase_name'],
|
||||||
|
directory=row['directory'],
|
||||||
|
message=row['message'],
|
||||||
|
details=json.loads(row['details']) if row['details'] else {},
|
||||||
|
stack_trace=row['stack_trace'],
|
||||||
|
checkpoint_id=row['checkpoint_id'],
|
||||||
|
status_file=row['status_file'],
|
||||||
|
detected_at=row['detected_at'],
|
||||||
|
updated_at=row['updated_at'],
|
||||||
|
resolution_notes=row['resolution_notes'],
|
||||||
|
assigned_to=row['assigned_to'],
|
||||||
|
resolved=row['status'] == 'resolved'
|
||||||
|
)
|
||||||
|
|
||||||
|
def log_bug(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
severity: Severity = Severity.MEDIUM,
|
||||||
|
bug_type: AnomalyType = AnomalyType.UNHANDLED_ERROR,
|
||||||
|
phase: int = 0,
|
||||||
|
directory: str = "unknown",
|
||||||
|
details: Optional[dict] = None,
|
||||||
|
stack_trace: Optional[str] = None
|
||||||
|
) -> Anomaly:
|
||||||
|
"""Manually log a bug (for API/CLI use)"""
|
||||||
|
anomaly = Anomaly(
|
||||||
|
id="",
|
||||||
|
type=bug_type,
|
||||||
|
severity=severity,
|
||||||
|
status=BugStatus.OPEN,
|
||||||
|
phase=phase,
|
||||||
|
phase_name=self.PHASES.get(phase, f"Phase {phase}"),
|
||||||
|
directory=directory,
|
||||||
|
message=message,
|
||||||
|
details=details or {},
|
||||||
|
stack_trace=stack_trace
|
||||||
|
)
|
||||||
|
|
||||||
|
self._persist_anomalies([anomaly])
|
||||||
|
self.anomalies.append(anomaly)
|
||||||
|
|
||||||
|
return anomaly
|
||||||
|
|
||||||
def get_summary(self) -> dict:
|
def get_summary(self) -> dict:
|
||||||
"""Get summary of watcher state and anomalies"""
|
"""Get summary of watcher state and anomalies"""
|
||||||
@ -627,23 +857,29 @@ class BugWindowWatcher:
|
|||||||
by_severity = {s.value: 0 for s in Severity}
|
by_severity = {s.value: 0 for s in Severity}
|
||||||
by_phase = {p: 0 for p in self.PHASES}
|
by_phase = {p: 0 for p in self.PHASES}
|
||||||
by_type = {t.value: 0 for t in AnomalyType}
|
by_type = {t.value: 0 for t in AnomalyType}
|
||||||
|
by_status = {s.value: 0 for s in BugStatus}
|
||||||
|
|
||||||
for a in anomalies:
|
for a in anomalies:
|
||||||
# Handle both enum and string values
|
# Handle both enum and string values
|
||||||
sev_val = a.severity.value if hasattr(a.severity, 'value') else a.severity
|
sev_val = a.severity.value if hasattr(a.severity, 'value') else a.severity
|
||||||
type_val = a.type.value if hasattr(a.type, 'value') else a.type
|
type_val = a.type.value if hasattr(a.type, 'value') else a.type
|
||||||
|
status_val = a.status.value if hasattr(a.status, 'value') else a.status
|
||||||
|
|
||||||
by_severity[sev_val] = by_severity.get(sev_val, 0) + 1
|
by_severity[sev_val] = by_severity.get(sev_val, 0) + 1
|
||||||
by_phase[a.phase] = by_phase.get(a.phase, 0) + 1
|
by_phase[a.phase] = by_phase.get(a.phase, 0) + 1
|
||||||
by_type[type_val] = by_type.get(type_val, 0) + 1
|
by_type[type_val] = by_type.get(type_val, 0) + 1
|
||||||
|
by_status[status_val] = by_status.get(status_val, 0) + 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"state": asdict(self.state),
|
"state": asdict(self.state),
|
||||||
"total_anomalies": len(anomalies),
|
"total_anomalies": len(anomalies),
|
||||||
"unresolved": len([a for a in anomalies if not a.resolved]),
|
"open": by_status.get("open", 0),
|
||||||
|
"in_progress": by_status.get("in_progress", 0),
|
||||||
|
"resolved": by_status.get("resolved", 0),
|
||||||
"by_severity": by_severity,
|
"by_severity": by_severity,
|
||||||
"by_phase": by_phase,
|
"by_phase": by_phase,
|
||||||
"by_type": by_type,
|
"by_type": by_type,
|
||||||
|
"by_status": by_status,
|
||||||
"phases": self.PHASES
|
"phases": self.PHASES
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,10 +888,20 @@ if __name__ == "__main__":
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Bug Window Watcher")
|
parser = argparse.ArgumentParser(description="Bug Window Watcher")
|
||||||
parser.add_argument("command", choices=["scan", "status", "list"], help="Command to run")
|
parser.add_argument("command", choices=["scan", "status", "list", "update", "log", "get"], help="Command to run")
|
||||||
parser.add_argument("--phase", type=int, help="Specific phase to scan")
|
parser.add_argument("--phase", type=int, help="Specific phase to scan")
|
||||||
parser.add_argument("--severity", choices=["critical", "high", "medium", "low", "info"])
|
parser.add_argument("--severity", choices=["critical", "high", "medium", "low", "info"])
|
||||||
|
parser.add_argument("--bug-status", dest="bug_status", choices=["open", "in_progress", "resolved"], help="Filter by bug status")
|
||||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||||
|
# For update command
|
||||||
|
parser.add_argument("--id", help="Bug ID to update or get")
|
||||||
|
parser.add_argument("--set-status", dest="set_status", choices=["open", "in_progress", "resolved"], help="New status to set")
|
||||||
|
parser.add_argument("--notes", help="Resolution or status notes")
|
||||||
|
parser.add_argument("--assign", help="Assign bug to person/team")
|
||||||
|
# For log command
|
||||||
|
parser.add_argument("--message", "-m", help="Bug message (for log command)")
|
||||||
|
parser.add_argument("--directory", "-d", default="unknown", help="Directory (for log command)")
|
||||||
|
parser.add_argument("--type", dest="bug_type", choices=[t.value for t in AnomalyType], default="unhandled_error")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@ -678,8 +924,12 @@ if __name__ == "__main__":
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
for a in anomalies:
|
for a in anomalies:
|
||||||
icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵", "info": "⚪"}.get(a.severity.value, "⚪")
|
sev_val = a.severity.value if hasattr(a.severity, 'value') else a.severity
|
||||||
print(f"{icon} [{a.severity.value.upper()}] Phase {a.phase}: {a.message}")
|
status_val = a.status.value if hasattr(a.status, 'value') else a.status
|
||||||
|
icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵", "info": "⚪"}.get(sev_val, "⚪")
|
||||||
|
status_icon = {"open": "📋", "in_progress": "🔧", "resolved": "✅"}.get(status_val, "❓")
|
||||||
|
print(f"{icon} [{sev_val.upper()}] {status_icon} {status_val.upper()} | Phase {a.phase}: {a.message}")
|
||||||
|
print(f" ID: {a.id}")
|
||||||
print(f" Directory: {a.directory}")
|
print(f" Directory: {a.directory}")
|
||||||
if a.status_file:
|
if a.status_file:
|
||||||
print(f" Status: {a.status_file}")
|
print(f" Status: {a.status_file}")
|
||||||
@ -694,20 +944,123 @@ if __name__ == "__main__":
|
|||||||
print(f"BUG WINDOW WATCHER - Status")
|
print(f"BUG WINDOW WATCHER - Status")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
print(f"Active: {summary['state']['active']}")
|
print(f"Active: {summary['state']['active']}")
|
||||||
print(f"Total Anomalies: {summary['total_anomalies']}")
|
print(f"Total Bugs: {summary['total_anomalies']}")
|
||||||
print(f"Unresolved: {summary['unresolved']}")
|
print()
|
||||||
|
print("By Status:")
|
||||||
|
print(f" 📋 Open: {summary['open']}")
|
||||||
|
print(f" 🔧 In Progress: {summary['in_progress']}")
|
||||||
|
print(f" ✅ Resolved: {summary['resolved']}")
|
||||||
print()
|
print()
|
||||||
print("By Severity:")
|
print("By Severity:")
|
||||||
for sev, count in summary['by_severity'].items():
|
for sev, count in summary['by_severity'].items():
|
||||||
if count > 0:
|
if count > 0:
|
||||||
print(f" {sev}: {count}")
|
icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵", "info": "⚪"}.get(sev, "⚪")
|
||||||
|
print(f" {icon} {sev}: {count}")
|
||||||
|
|
||||||
elif args.command == "list":
|
elif args.command == "list":
|
||||||
severity = Severity(args.severity) if args.severity else None
|
severity = Severity(args.severity) if args.severity else None
|
||||||
anomalies = watcher.get_anomalies(severity=severity, phase=args.phase)
|
status = BugStatus(args.bug_status) if args.bug_status else None
|
||||||
|
anomalies = watcher.get_anomalies(severity=severity, phase=args.phase, status=status)
|
||||||
|
|
||||||
if args.json:
|
if args.json:
|
||||||
print(json.dumps([asdict(a) for a in anomalies], indent=2))
|
print(json.dumps([asdict(a) for a in anomalies], indent=2))
|
||||||
else:
|
else:
|
||||||
|
if not anomalies:
|
||||||
|
print("No bugs found matching criteria.")
|
||||||
|
else:
|
||||||
|
print(f"\n{'='*70}")
|
||||||
|
print(f"{'ID':<20} {'Status':<12} {'Severity':<10} {'Message'}")
|
||||||
|
print(f"{'='*70}")
|
||||||
for a in anomalies:
|
for a in anomalies:
|
||||||
print(f"[{a.id}] {a.severity.value}: {a.message}")
|
sev_val = a.severity.value if hasattr(a.severity, 'value') else a.severity
|
||||||
|
status_val = a.status.value if hasattr(a.status, 'value') else a.status
|
||||||
|
msg = a.message[:40] + "..." if len(a.message) > 40 else a.message
|
||||||
|
print(f"{a.id:<20} {status_val:<12} {sev_val:<10} {msg}")
|
||||||
|
|
||||||
|
elif args.command == "update":
|
||||||
|
if not args.id:
|
||||||
|
print("Error: --id is required for update command")
|
||||||
|
exit(1)
|
||||||
|
if not args.set_status:
|
||||||
|
print("Error: --set-status is required for update command")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
new_status = BugStatus(args.set_status)
|
||||||
|
success = watcher.update_bug_status(
|
||||||
|
args.id,
|
||||||
|
new_status,
|
||||||
|
notes=args.notes,
|
||||||
|
assigned_to=args.assign
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
bug = watcher.get_bug(args.id)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(asdict(bug), indent=2))
|
||||||
|
else:
|
||||||
|
print(f"✅ Bug {args.id} updated to {args.set_status}")
|
||||||
|
if args.notes:
|
||||||
|
print(f" Notes: {args.notes}")
|
||||||
|
if args.assign:
|
||||||
|
print(f" Assigned to: {args.assign}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to update bug {args.id} - not found")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
elif args.command == "get":
|
||||||
|
if not args.id:
|
||||||
|
print("Error: --id is required for get command")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
bug = watcher.get_bug(args.id)
|
||||||
|
if bug:
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(asdict(bug), indent=2))
|
||||||
|
else:
|
||||||
|
sev_val = bug.severity.value if hasattr(bug.severity, 'value') else bug.severity
|
||||||
|
status_val = bug.status.value if hasattr(bug.status, 'value') else bug.status
|
||||||
|
type_val = bug.type.value if hasattr(bug.type, 'value') else bug.type
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"Bug: {bug.id}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
print(f"Status: {status_val}")
|
||||||
|
print(f"Severity: {sev_val}")
|
||||||
|
print(f"Type: {type_val}")
|
||||||
|
print(f"Phase: {bug.phase} - {bug.phase_name}")
|
||||||
|
print(f"Directory: {bug.directory}")
|
||||||
|
print(f"Message: {bug.message}")
|
||||||
|
print(f"Detected: {bug.detected_at}")
|
||||||
|
if bug.updated_at:
|
||||||
|
print(f"Updated: {bug.updated_at}")
|
||||||
|
if bug.assigned_to:
|
||||||
|
print(f"Assigned to: {bug.assigned_to}")
|
||||||
|
if bug.resolution_notes:
|
||||||
|
print(f"Notes: {bug.resolution_notes}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Bug {args.id} not found")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
elif args.command == "log":
|
||||||
|
if not args.message:
|
||||||
|
print("Error: --message/-m is required for log command")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
severity = Severity(args.severity) if args.severity else Severity.MEDIUM
|
||||||
|
bug_type = AnomalyType(args.bug_type)
|
||||||
|
phase = args.phase or 0
|
||||||
|
|
||||||
|
bug = watcher.log_bug(
|
||||||
|
message=args.message,
|
||||||
|
severity=severity,
|
||||||
|
bug_type=bug_type,
|
||||||
|
phase=phase,
|
||||||
|
directory=args.directory
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(asdict(bug), indent=2))
|
||||||
|
else:
|
||||||
|
print(f"✅ Bug logged: {bug.id}")
|
||||||
|
print(f" Severity: {severity.value}")
|
||||||
|
print(f" Status: open")
|
||||||
|
print(f" Message: {args.message}")
|
||||||
|
|||||||
2963
ui/server.ts
2963
ui/server.ts
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user