diff --git a/bin/argus b/bin/argus index 550eafa..1186c49 100755 --- a/bin/argus +++ b/bin/argus @@ -705,7 +705,7 @@ def cmd_notif_list(args) -> None: params = [] if args.pending: - conditions.append("dismissed = 0 AND acted_at IS NULL") + conditions.append("dismissed = 0 AND acted_at IS NULL AND processing = 0") if getattr(args, 'dismissed', False): conditions.append("dismissed = 1") if getattr(args, 'own', False): @@ -726,7 +726,11 @@ def cmd_notif_list(args) -> None: return for n in notifications: - status = "✓" if n["acted_at"] else ("✗" if n["dismissed"] else "○") + try: + locked = n["processing"] + except (IndexError, KeyError): + locked = 0 + status = "✓" if n["acted_at"] else ("✗" if n["dismissed"] else ("🔒" if locked else "○")) own_tag = " 🔵" if n["is_own_pr"] else "" ci_tag = f" [CI:{n['ci_status']}]" if n["ci_status"] else "" print(f"{status}{own_tag}{ci_tag} [{n['id'][:8]}] {n['title'][:60]}") @@ -773,6 +777,41 @@ def cmd_notif_act(args) -> None: print(f"✓ Marked notification as acted: {action}") +def cmd_notif_lock(args) -> None: + """Lock notification to prevent duplicate processing.""" + conn = get_db() + notif = conn.execute( + "SELECT * FROM notifications WHERE id LIKE ?", + (f"{args.id}%",) + ).fetchone() + if not notif: + print(f"Error: Notification not found: {args.id}", file=sys.stderr) + sys.exit(1) + if notif["processing"]: + print(f"⚠ Already locked: {notif['id'][:8]}") + sys.exit(1) + conn.execute("UPDATE notifications SET processing = 1 WHERE id = ?", (notif["id"],)) + conn.commit() + conn.close() + print(f"🔒 Locked: {notif['id'][:8]}") + + +def cmd_notif_unlock(args) -> None: + """Unlock notification after processing completes.""" + conn = get_db() + notif = conn.execute( + "SELECT * FROM notifications WHERE id LIKE ?", + (f"{args.id}%",) + ).fetchone() + if not notif: + print(f"Error: Notification not found: {args.id}", file=sys.stderr) + sys.exit(1) + conn.execute("UPDATE notifications SET processing = 0 WHERE id = ?", (notif["id"],)) + conn.commit() + conn.close() + print(f"🔓 Unlocked: {notif['id'][:8]}") + + def cmd_notif_dismiss(args) -> None: """Dismiss notification.""" conn = get_db() @@ -795,6 +834,14 @@ def cmd_notif_dismiss(args) -> None: WHERE id = ? """, (f"dismissed: {reason}", notif["id"])) + # Auto-log activity + title = notif["title"] or "-" + detail = f"Dismissed: {reason} — {title}" + conn.execute( + "INSERT INTO activity_log (issue_id, action, detail, github_url) VALUES (?, ?, ?, ?)", + (notif["repo_id"], "dismiss", detail, notif["url"]) + ) + conn.commit() conn.close() @@ -1146,6 +1193,12 @@ def main(): dismiss_parser.add_argument("id", help="Notification ID (prefix match)") dismiss_parser.add_argument("reason", nargs="?", help="Dismissal reason") + lock_parser = notif_sub.add_parser("lock", help="Lock notification for processing") + lock_parser.add_argument("id", help="Notification ID (prefix match)") + + unlock_parser = notif_sub.add_parser("unlock", help="Unlock notification after processing") + unlock_parser.add_argument("id", help="Notification ID (prefix match)") + audit_parser = notif_sub.add_parser("audit", help="Audit stale notifications") audit_parser.add_argument("--stale-hours", type=int, help="Hours to consider stale (default: 48)") @@ -1217,6 +1270,10 @@ def main(): cmd_notif_act(args) elif args.notif_cmd == "dismiss": cmd_notif_dismiss(args) + elif args.notif_cmd == "lock": + cmd_notif_lock(args) + elif args.notif_cmd == "unlock": + cmd_notif_unlock(args) elif args.notif_cmd == "audit": cmd_notif_audit(args) elif args.notif_cmd == "filter": diff --git a/dashboard/index.html b/dashboard/index.html index 224b68b..ceb1c44 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -289,7 +289,7 @@ // Notifications table — show pending (non-dismissed) by default const notifications = await fetchData('notifications', - 'SELECT * FROM notifications WHERE dismissed = 0 ORDER BY seen_at DESC LIMIT 30'); + 'SELECT * FROM notifications WHERE dismissed = 0 AND processing = 0 ORDER BY seen_at DESC LIMIT 30'); if (notifications.length === 0) { document.getElementById('notifications-table').innerHTML = @@ -302,7 +302,15 @@ const ownTag = n.is_own_pr ? '🔵 ' : ''; const ciTag = n.ci_status ? ` [CI:${n.ci_status}]` : ''; const repo_short = n.repo_id ? n.repo_id.split('/').pop() : '-'; - const issue = n.url ? `${repo_short}` : repo_short; + let issueLabel = repo_short; + if (n.url) { + const m = n.url.match(/\/(pull|issues)\/(\d+)/); + if (m) { + const prefix = m[1] === 'pull' ? 'PR' : 'IS'; + issueLabel = `${repo_short} ${prefix} #${m[2]}`; + } + } + const issue = n.url ? `${issueLabel}` : issueLabel; const reasonClass = n.reason === 'mention' ? 'accent' : n.reason === 'author' ? 'success' : n.reason === 'review_requested' ? 'warning' : '';