feat: initial Argus implementation
- CLI with notification, activity, and escalation commands - SQLite database with schema - Datasette + static HTML dashboard - Systemd service for background operation - Install/uninstall scripts - SKILL.md and procedures for sub-agent usage
This commit is contained in:
commit
90361fbe16
12 changed files with 1608 additions and 0 deletions
17
CHANGELOG.md
Normal file
17
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-02-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release
|
||||||
|
- CLI with notification, activity, and escalation commands
|
||||||
|
- SQLite database for persistent state
|
||||||
|
- Datasette integration for API access
|
||||||
|
- Static HTML dashboard
|
||||||
|
- Systemd service for background operation
|
||||||
|
- Install/uninstall scripts
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Zeus @ Olymp
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
148
README.md
Normal file
148
README.md
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Argus 👁️
|
||||||
|
|
||||||
|
CLI tool for GitHub/Forgejo community awareness — tracking notifications, logging activity, managing escalations.
|
||||||
|
|
||||||
|
Named after **Argus Panoptes**, the all-seeing giant of Greek mythology.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Notification triage** — Pull, act, or dismiss GitHub notifications
|
||||||
|
- **Activity logging** — Track community actions
|
||||||
|
- **Escalation management** — Flag and track issues needing attention
|
||||||
|
- **Dashboard** — Visual overview via Datasette + static HTML
|
||||||
|
- **Systemd service** — Run in background
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install
|
||||||
|
./scripts/install.sh
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
argus init
|
||||||
|
|
||||||
|
# Set up GitHub token
|
||||||
|
export GH_TOKEN=your_github_token
|
||||||
|
|
||||||
|
# Pull notifications
|
||||||
|
argus notif pull
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
argus status
|
||||||
|
|
||||||
|
# Start dashboard
|
||||||
|
systemctl start argus
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Core
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `argus init` | Create database |
|
||||||
|
| `argus version` | Show version |
|
||||||
|
| `argus status` | Summary overview |
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `argus notif pull [--dry-run]` | Fetch from GitHub |
|
||||||
|
| `argus notif list [--pending]` | List notifications |
|
||||||
|
| `argus notif act <id> <action>` | Mark as acted |
|
||||||
|
| `argus notif dismiss <id> [reason]` | Dismiss |
|
||||||
|
| `argus notif audit` | Auto-escalate stale |
|
||||||
|
|
||||||
|
### Activity
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `argus activity log <action> <detail>` | Log action |
|
||||||
|
| `argus activity list` | Show recent |
|
||||||
|
|
||||||
|
### Escalations
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `argus escalate create --category --title` | Create |
|
||||||
|
| `argus escalate list [--status]` | List |
|
||||||
|
| `argus escalate ack <id>` | Acknowledge |
|
||||||
|
| `argus escalate resolve <id> --by --resolution` | Resolve |
|
||||||
|
|
||||||
|
### Service
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `argus serve` | Start Datasette + dashboard |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `ARGUS_DB` | `~/.argus/argus.db` | Database path |
|
||||||
|
| `ARGUS_PORT` | `8100` | Datasette API port |
|
||||||
|
| `ARGUS_DASHBOARD_PORT` | `8101` | Dashboard port |
|
||||||
|
| `GH_TOKEN` | — | GitHub API token |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ argus CLI │
|
||||||
|
│ notif pull | activity log | escalate | serve │
|
||||||
|
└─────────────────┬───────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ ~/.argus/argus.db │
|
||||||
|
│ repos | issues | notifications | activity │
|
||||||
|
└─────────────────┬───────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────┴─────────┐
|
||||||
|
▼ ▼
|
||||||
|
┌───────────────┐ ┌───────────────┐
|
||||||
|
│ Datasette │ │ Dashboard │
|
||||||
|
│ :8100 (API) │ │ :8101 (HTML) │
|
||||||
|
└───────────────┘ └───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.9+
|
||||||
|
- pip
|
||||||
|
- gh CLI (for GitHub integration)
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://forgejo.tail593e12.ts.net/Zeus/argus.git /opt/argus
|
||||||
|
cd /opt/argus && ./scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/argus/scripts/uninstall.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Systemd Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl enable argus # Start on boot
|
||||||
|
systemctl start argus # Start now
|
||||||
|
systemctl status argus # Check status
|
||||||
|
journalctl -u argus -f # View logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Zeus @ Olymp
|
||||||
1
VERSION
Normal file
1
VERSION
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
0.1.0
|
||||||
635
bin/argus
Executable file
635
bin/argus
Executable file
|
|
@ -0,0 +1,635 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Argus - CLI tool for GitHub/Forgejo community awareness.
|
||||||
|
|
||||||
|
Named after Argus Panoptes, the all-seeing giant of Greek mythology.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
VERSION = "0.1.0"
|
||||||
|
DEFAULT_DB_PATH = Path.home() / ".argus" / "argus.db"
|
||||||
|
DEFAULT_PORT = 8100
|
||||||
|
DEFAULT_DASHBOARD_PORT = 8101
|
||||||
|
|
||||||
|
def get_db_path() -> Path:
|
||||||
|
return Path(os.environ.get("ARGUS_DB", DEFAULT_DB_PATH))
|
||||||
|
|
||||||
|
def get_schema_path() -> Path:
|
||||||
|
# Check relative to script first (for development)
|
||||||
|
script_dir = Path(__file__).parent.parent
|
||||||
|
schema_path = script_dir / "lib" / "schema.sql"
|
||||||
|
if schema_path.exists():
|
||||||
|
return schema_path
|
||||||
|
# Fall back to installed location
|
||||||
|
return Path("/opt/argus/lib/schema.sql")
|
||||||
|
|
||||||
|
def get_dashboard_path() -> Path:
|
||||||
|
script_dir = Path(__file__).parent.parent
|
||||||
|
dashboard_path = script_dir / "dashboard" / "index.html"
|
||||||
|
if dashboard_path.exists():
|
||||||
|
return dashboard_path
|
||||||
|
return Path("/opt/argus/dashboard/index.html")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Database
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_db() -> sqlite3.Connection:
|
||||||
|
db_path = get_db_path()
|
||||||
|
if not db_path.exists():
|
||||||
|
print(f"Error: Database not found at {db_path}", file=sys.stderr)
|
||||||
|
print("Run 'argus init' first.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def init_db() -> None:
|
||||||
|
db_path = get_db_path()
|
||||||
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
schema_path = get_schema_path()
|
||||||
|
if not schema_path.exists():
|
||||||
|
print(f"Error: Schema not found at {schema_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
with open(schema_path) as f:
|
||||||
|
conn.executescript(f.read())
|
||||||
|
conn.close()
|
||||||
|
print(f"✓ Initialized database at {db_path}")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# GitHub API
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_gh_token() -> Optional[str]:
|
||||||
|
return os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
||||||
|
|
||||||
|
def gh_api(endpoint: str) -> dict:
|
||||||
|
"""Call GitHub API via gh CLI."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["gh", "api", endpoint],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error calling GitHub API: {e.stderr}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"Error parsing GitHub API response", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Commands: Core
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def cmd_init(args) -> None:
|
||||||
|
"""Initialize database."""
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
def cmd_version(args) -> None:
|
||||||
|
"""Show version."""
|
||||||
|
print(f"argus {VERSION}")
|
||||||
|
|
||||||
|
def cmd_status(args) -> None:
|
||||||
|
"""Show status summary."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
# Pending notifications
|
||||||
|
pending = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM notifications WHERE dismissed = 0 AND acted_at IS NULL"
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
|
# Recent activity (last 24h)
|
||||||
|
yesterday = (datetime.now() - timedelta(days=1)).isoformat()
|
||||||
|
recent_activity = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM activity_log WHERE timestamp > ?",
|
||||||
|
(yesterday,)
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
|
# Open escalations
|
||||||
|
open_escalations = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM escalations WHERE status = 'open'"
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
|
# Last poll
|
||||||
|
last_poll = conn.execute(
|
||||||
|
"SELECT polled_at, new_count FROM notification_polls ORDER BY id DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
print("ARGUS STATUS")
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"Pending notifications: {pending}")
|
||||||
|
print(f"Activity (24h): {recent_activity}")
|
||||||
|
print(f"Open escalations: {open_escalations}")
|
||||||
|
if last_poll:
|
||||||
|
print(f"Last poll: {last_poll['polled_at']} ({last_poll['new_count']} new)")
|
||||||
|
else:
|
||||||
|
print("Last poll: Never")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Commands: Notifications
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def cmd_notif_pull(args) -> None:
|
||||||
|
"""Pull notifications from GitHub."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
# Fetch notifications from GitHub
|
||||||
|
notifications = gh_api("notifications")
|
||||||
|
|
||||||
|
new_count = 0
|
||||||
|
for notif in notifications:
|
||||||
|
notif_id = notif["id"]
|
||||||
|
|
||||||
|
# Check if already exists
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id FROM notifications WHERE id = ?",
|
||||||
|
(notif_id,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract repo info
|
||||||
|
repo_full = notif["repository"]["full_name"]
|
||||||
|
reason = notif["reason"]
|
||||||
|
title = notif["subject"]["title"]
|
||||||
|
url = notif["subject"]["url"] or ""
|
||||||
|
|
||||||
|
# Convert API URL to web URL
|
||||||
|
web_url = url.replace("api.github.com/repos", "github.com")
|
||||||
|
web_url = web_url.replace("/pulls/", "/pull/")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print(f"[DRY-RUN] Would add: {title} ({reason})")
|
||||||
|
new_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO notifications (id, repo_id, reason, title, url, seen_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""", (notif_id, repo_full, reason, title, web_url, datetime.now().isoformat()))
|
||||||
|
new_count += 1
|
||||||
|
|
||||||
|
if not args.dry_run:
|
||||||
|
# Record poll
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO notification_polls (new_count) VALUES (?)",
|
||||||
|
(new_count,)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print(f"\n[DRY-RUN] Would add {new_count} notifications")
|
||||||
|
else:
|
||||||
|
print(f"✓ Pulled {new_count} new notifications")
|
||||||
|
|
||||||
|
def cmd_notif_list(args) -> None:
|
||||||
|
"""List notifications."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
query = "SELECT * FROM notifications"
|
||||||
|
conditions = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if args.pending:
|
||||||
|
conditions.append("dismissed = 0 AND acted_at IS NULL")
|
||||||
|
|
||||||
|
if conditions:
|
||||||
|
query += " WHERE " + " AND ".join(conditions)
|
||||||
|
|
||||||
|
query += " ORDER BY seen_at DESC"
|
||||||
|
|
||||||
|
if args.limit:
|
||||||
|
query += f" LIMIT {args.limit}"
|
||||||
|
|
||||||
|
notifications = conn.execute(query, params).fetchall()
|
||||||
|
|
||||||
|
if not notifications:
|
||||||
|
print("No notifications found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for n in notifications:
|
||||||
|
status = "✓" if n["acted_at"] else ("✗" if n["dismissed"] else "○")
|
||||||
|
print(f"{status} [{n['id'][:8]}] {n['title'][:60]}")
|
||||||
|
print(f" Repo: {n['repo_id']} | Reason: {n['reason']}")
|
||||||
|
if n["url"]:
|
||||||
|
print(f" URL: {n['url']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def cmd_notif_act(args) -> None:
|
||||||
|
"""Mark notification as acted on."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
# Find notification by prefix match
|
||||||
|
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)
|
||||||
|
|
||||||
|
action = args.action
|
||||||
|
detail = args.detail or ""
|
||||||
|
|
||||||
|
conn.execute("""
|
||||||
|
UPDATE notifications
|
||||||
|
SET acted_at = ?, action_taken = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", (datetime.now().isoformat(), f"{action}: {detail}".strip(": "), notif["id"]))
|
||||||
|
|
||||||
|
# Log the activity
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO activity_log (issue_id, action, detail, github_url)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""", (notif.get("issue_id"), action, detail or notif["title"], notif.get("url")))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"✓ Marked notification as acted: {action}")
|
||||||
|
|
||||||
|
def cmd_notif_dismiss(args) -> None:
|
||||||
|
"""Dismiss notification."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
# Find notification by prefix match
|
||||||
|
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)
|
||||||
|
|
||||||
|
reason = args.reason or "dismissed"
|
||||||
|
|
||||||
|
conn.execute("""
|
||||||
|
UPDATE notifications
|
||||||
|
SET dismissed = 1, action_taken = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", (f"dismissed: {reason}", notif["id"]))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"✓ Dismissed notification: {reason}")
|
||||||
|
|
||||||
|
def cmd_notif_audit(args) -> None:
|
||||||
|
"""Audit stale notifications and create escalations."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
stale_hours = args.stale_hours or 48
|
||||||
|
cutoff = (datetime.now() - timedelta(hours=stale_hours)).isoformat()
|
||||||
|
|
||||||
|
stale = conn.execute("""
|
||||||
|
SELECT * FROM notifications
|
||||||
|
WHERE dismissed = 0 AND acted_at IS NULL AND seen_at < ?
|
||||||
|
""", (cutoff,)).fetchall()
|
||||||
|
|
||||||
|
if not stale:
|
||||||
|
print("✓ No stale notifications found")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(stale)} stale notifications (>{stale_hours}h):")
|
||||||
|
|
||||||
|
for n in stale:
|
||||||
|
print(f" - {n['title'][:50]}...")
|
||||||
|
|
||||||
|
# Create escalation
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO escalations (category, title, detail)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""", (
|
||||||
|
"stale-notification",
|
||||||
|
f"Stale: {n['title'][:100]}",
|
||||||
|
f"Notification {n['id']} has been pending for >{stale_hours}h.\nURL: {n.get('url', 'N/A')}"
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"\n✓ Created {len(stale)} escalations")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Commands: Activity
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def cmd_activity_log(args) -> None:
|
||||||
|
"""Log an activity."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO activity_log (issue_id, action, detail)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""", (args.issue, args.action, args.detail))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"✓ Logged: {args.action}")
|
||||||
|
|
||||||
|
def cmd_activity_list(args) -> None:
|
||||||
|
"""List recent activity."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
limit = args.limit or 20
|
||||||
|
|
||||||
|
activities = conn.execute("""
|
||||||
|
SELECT * FROM activity_log
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT ?
|
||||||
|
""", (limit,)).fetchall()
|
||||||
|
|
||||||
|
if not activities:
|
||||||
|
print("No activity found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for a in activities:
|
||||||
|
ts = a["timestamp"][:16].replace("T", " ")
|
||||||
|
issue = f"[{a['issue_id']}] " if a["issue_id"] else ""
|
||||||
|
print(f"{ts} | {issue}{a['action']}: {a['detail'] or ''}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Commands: Escalations
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def cmd_escalate_create(args) -> None:
|
||||||
|
"""Create an escalation."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO escalations (category, priority, title, detail)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""", (args.category, args.priority or "normal", args.title, args.detail))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"✓ Created escalation: {args.title}")
|
||||||
|
|
||||||
|
def cmd_escalate_list(args) -> None:
|
||||||
|
"""List escalations."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
query = "SELECT * FROM escalations"
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if args.status:
|
||||||
|
query += " WHERE status = ?"
|
||||||
|
params.append(args.status)
|
||||||
|
|
||||||
|
query += " ORDER BY created_at DESC"
|
||||||
|
|
||||||
|
escalations = conn.execute(query, params).fetchall()
|
||||||
|
|
||||||
|
if not escalations:
|
||||||
|
print("No escalations found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for e in escalations:
|
||||||
|
status_icon = "🔴" if e["status"] == "open" else "✅"
|
||||||
|
priority_icon = "⚠️" if e["priority"] == "high" else ""
|
||||||
|
print(f"{status_icon} [{e['id']}] {priority_icon}{e['title']}")
|
||||||
|
print(f" Category: {e['category']} | Created: {e['created_at'][:10]}")
|
||||||
|
if e["status"] == "resolved":
|
||||||
|
print(f" Resolved by: {e['resolved_by']} | {e['resolution']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def cmd_escalate_ack(args) -> None:
|
||||||
|
"""Acknowledge an escalation."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
conn.execute("""
|
||||||
|
UPDATE escalations
|
||||||
|
SET status = 'acknowledged'
|
||||||
|
WHERE id = ?
|
||||||
|
""", (args.id,))
|
||||||
|
|
||||||
|
if conn.total_changes == 0:
|
||||||
|
print(f"Error: Escalation not found: {args.id}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"✓ Acknowledged escalation #{args.id}")
|
||||||
|
|
||||||
|
def cmd_escalate_resolve(args) -> None:
|
||||||
|
"""Resolve an escalation."""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
conn.execute("""
|
||||||
|
UPDATE escalations
|
||||||
|
SET status = 'resolved', resolved_at = ?, resolved_by = ?, resolution = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", (datetime.now().isoformat(), args.by, args.resolution, args.id))
|
||||||
|
|
||||||
|
if conn.total_changes == 0:
|
||||||
|
print(f"Error: Escalation not found: {args.id}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"✓ Resolved escalation #{args.id}")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Commands: Serve
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def cmd_serve(args) -> None:
|
||||||
|
"""Start datasette + dashboard servers."""
|
||||||
|
import http.server
|
||||||
|
import threading
|
||||||
|
|
||||||
|
db_path = get_db_path()
|
||||||
|
dashboard_path = get_dashboard_path()
|
||||||
|
|
||||||
|
port = args.port or int(os.environ.get("ARGUS_PORT", DEFAULT_PORT))
|
||||||
|
dashboard_port = args.dashboard_port or int(os.environ.get("ARGUS_DASHBOARD_PORT", DEFAULT_DASHBOARD_PORT))
|
||||||
|
|
||||||
|
print(f"Starting Argus servers...")
|
||||||
|
print(f" Datasette: http://localhost:{port}")
|
||||||
|
print(f" Dashboard: http://localhost:{dashboard_port}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Start datasette in background
|
||||||
|
datasette_proc = subprocess.Popen(
|
||||||
|
["datasette", str(db_path), "-p", str(port), "--cors"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start simple HTTP server for dashboard
|
||||||
|
os.chdir(dashboard_path.parent)
|
||||||
|
|
||||||
|
handler = http.server.SimpleHTTPRequestHandler
|
||||||
|
with http.server.HTTPServer(("", dashboard_port), handler) as httpd:
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down...")
|
||||||
|
datasette_proc.terminate()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="argus",
|
||||||
|
description="CLI tool for GitHub/Forgejo community awareness"
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||||
|
|
||||||
|
# Core commands
|
||||||
|
subparsers.add_parser("init", help="Initialize database")
|
||||||
|
subparsers.add_parser("version", help="Show version")
|
||||||
|
subparsers.add_parser("status", help="Show status summary")
|
||||||
|
|
||||||
|
# Notification commands
|
||||||
|
notif_parser = subparsers.add_parser("notif", help="Notification management")
|
||||||
|
notif_sub = notif_parser.add_subparsers(dest="notif_cmd")
|
||||||
|
|
||||||
|
pull_parser = notif_sub.add_parser("pull", help="Pull notifications from GitHub")
|
||||||
|
pull_parser.add_argument("--dry-run", action="store_true", help="Show what would be pulled")
|
||||||
|
|
||||||
|
list_parser = notif_sub.add_parser("list", help="List notifications")
|
||||||
|
list_parser.add_argument("--pending", action="store_true", help="Show only pending")
|
||||||
|
list_parser.add_argument("--limit", type=int, help="Limit results")
|
||||||
|
|
||||||
|
act_parser = notif_sub.add_parser("act", help="Mark as acted on")
|
||||||
|
act_parser.add_argument("id", help="Notification ID (prefix match)")
|
||||||
|
act_parser.add_argument("action", help="Action taken")
|
||||||
|
act_parser.add_argument("detail", nargs="?", help="Additional detail")
|
||||||
|
|
||||||
|
dismiss_parser = notif_sub.add_parser("dismiss", help="Dismiss notification")
|
||||||
|
dismiss_parser.add_argument("id", help="Notification ID (prefix match)")
|
||||||
|
dismiss_parser.add_argument("reason", nargs="?", help="Dismissal reason")
|
||||||
|
|
||||||
|
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)")
|
||||||
|
|
||||||
|
# Activity commands
|
||||||
|
activity_parser = subparsers.add_parser("activity", help="Activity logging")
|
||||||
|
activity_sub = activity_parser.add_subparsers(dest="activity_cmd")
|
||||||
|
|
||||||
|
log_parser = activity_sub.add_parser("log", help="Log an activity")
|
||||||
|
log_parser.add_argument("action", help="Action type")
|
||||||
|
log_parser.add_argument("detail", help="Action detail")
|
||||||
|
log_parser.add_argument("--issue", help="Related issue ID")
|
||||||
|
|
||||||
|
list_activity_parser = activity_sub.add_parser("list", help="List recent activity")
|
||||||
|
list_activity_parser.add_argument("--limit", type=int, help="Limit results")
|
||||||
|
|
||||||
|
# Escalation commands
|
||||||
|
escalate_parser = subparsers.add_parser("escalate", help="Escalation management")
|
||||||
|
escalate_sub = escalate_parser.add_subparsers(dest="escalate_cmd")
|
||||||
|
|
||||||
|
create_parser = escalate_sub.add_parser("create", help="Create escalation")
|
||||||
|
create_parser.add_argument("--category", required=True, help="Escalation category")
|
||||||
|
create_parser.add_argument("--title", required=True, help="Escalation title")
|
||||||
|
create_parser.add_argument("--detail", help="Additional detail")
|
||||||
|
create_parser.add_argument("--priority", choices=["low", "normal", "high"], help="Priority")
|
||||||
|
|
||||||
|
list_esc_parser = escalate_sub.add_parser("list", help="List escalations")
|
||||||
|
list_esc_parser.add_argument("--status", choices=["open", "acknowledged", "resolved"], help="Filter by status")
|
||||||
|
|
||||||
|
ack_parser = escalate_sub.add_parser("ack", help="Acknowledge escalation")
|
||||||
|
ack_parser.add_argument("id", type=int, help="Escalation ID")
|
||||||
|
|
||||||
|
resolve_parser = escalate_sub.add_parser("resolve", help="Resolve escalation")
|
||||||
|
resolve_parser.add_argument("id", type=int, help="Escalation ID")
|
||||||
|
resolve_parser.add_argument("--by", required=True, help="Who resolved")
|
||||||
|
resolve_parser.add_argument("--resolution", required=True, help="Resolution description")
|
||||||
|
|
||||||
|
# Serve command
|
||||||
|
serve_parser = subparsers.add_parser("serve", help="Start servers")
|
||||||
|
serve_parser.add_argument("--port", type=int, help=f"Datasette port (default: {DEFAULT_PORT})")
|
||||||
|
serve_parser.add_argument("--dashboard-port", type=int, help=f"Dashboard port (default: {DEFAULT_DASHBOARD_PORT})")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Route to command handlers
|
||||||
|
if args.command == "init":
|
||||||
|
cmd_init(args)
|
||||||
|
elif args.command == "version":
|
||||||
|
cmd_version(args)
|
||||||
|
elif args.command == "status":
|
||||||
|
cmd_status(args)
|
||||||
|
elif args.command == "notif":
|
||||||
|
if args.notif_cmd == "pull":
|
||||||
|
cmd_notif_pull(args)
|
||||||
|
elif args.notif_cmd == "list":
|
||||||
|
cmd_notif_list(args)
|
||||||
|
elif args.notif_cmd == "act":
|
||||||
|
cmd_notif_act(args)
|
||||||
|
elif args.notif_cmd == "dismiss":
|
||||||
|
cmd_notif_dismiss(args)
|
||||||
|
elif args.notif_cmd == "audit":
|
||||||
|
cmd_notif_audit(args)
|
||||||
|
else:
|
||||||
|
notif_parser.print_help()
|
||||||
|
elif args.command == "activity":
|
||||||
|
if args.activity_cmd == "log":
|
||||||
|
cmd_activity_log(args)
|
||||||
|
elif args.activity_cmd == "list":
|
||||||
|
cmd_activity_list(args)
|
||||||
|
else:
|
||||||
|
activity_parser.print_help()
|
||||||
|
elif args.command == "escalate":
|
||||||
|
if args.escalate_cmd == "create":
|
||||||
|
cmd_escalate_create(args)
|
||||||
|
elif args.escalate_cmd == "list":
|
||||||
|
cmd_escalate_list(args)
|
||||||
|
elif args.escalate_cmd == "ack":
|
||||||
|
cmd_escalate_ack(args)
|
||||||
|
elif args.escalate_cmd == "resolve":
|
||||||
|
cmd_escalate_resolve(args)
|
||||||
|
else:
|
||||||
|
escalate_parser.print_help()
|
||||||
|
elif args.command == "serve":
|
||||||
|
cmd_serve(args)
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
349
dashboard/index.html
Normal file
349
dashboard/index.html
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Argus Dashboard</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #1a1a2e;
|
||||||
|
--card-bg: #16213e;
|
||||||
|
--accent: #e94560;
|
||||||
|
--text: #eee;
|
||||||
|
--text-dim: #888;
|
||||||
|
--success: #4ade80;
|
||||||
|
--warning: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 2rem;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 span {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat.warning { color: var(--warning); }
|
||||||
|
.stat.success { color: var(--success); }
|
||||||
|
.stat.accent { color: var(--accent); }
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending { background: var(--warning); color: #000; }
|
||||||
|
.status-acted { background: var(--success); color: #000; }
|
||||||
|
.status-dismissed { background: var(--text-dim); color: #000; }
|
||||||
|
.status-open { background: var(--accent); }
|
||||||
|
.status-resolved { background: var(--success); color: #000; }
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-dim);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: rgba(233, 69, 96, 0.2);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1><span>👁️</span> Argus Dashboard</h1>
|
||||||
|
|
||||||
|
<div id="error" class="error" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div class="grid" id="stats">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Pending Notifications</h2>
|
||||||
|
<div class="stat warning" id="pending-count">-</div>
|
||||||
|
<div class="stat-label">Awaiting action</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Activity (24h)</h2>
|
||||||
|
<div class="stat success" id="activity-count">-</div>
|
||||||
|
<div class="stat-label">Actions logged</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Open Escalations</h2>
|
||||||
|
<div class="stat accent" id="escalation-count">-</div>
|
||||||
|
<div class="stat-label">Need attention</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="card">
|
||||||
|
<h2>
|
||||||
|
Recent Notifications
|
||||||
|
<button class="refresh-btn" onclick="loadData()">↻ Refresh</button>
|
||||||
|
</h2>
|
||||||
|
<div id="notifications-table">
|
||||||
|
<div class="loading">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Recent Activity</h2>
|
||||||
|
<div id="activity-table">
|
||||||
|
<div class="loading">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Open Escalations</h2>
|
||||||
|
<div id="escalations-table">
|
||||||
|
<div class="loading">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = 'http://localhost:8100';
|
||||||
|
|
||||||
|
async function fetchData(table, sql) {
|
||||||
|
const url = `${API_BASE}/argus.json?sql=${encodeURIComponent(sql)}&_shape=array`;
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) throw new Error(`API error: ${resp.status}`);
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso) {
|
||||||
|
if (!iso) return '-';
|
||||||
|
const d = new Date(iso);
|
||||||
|
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNotificationStatus(row) {
|
||||||
|
if (row.dismissed) return { class: 'status-dismissed', text: 'Dismissed' };
|
||||||
|
if (row.acted_at) return { class: 'status-acted', text: 'Acted' };
|
||||||
|
return { class: 'status-pending', text: 'Pending' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const errorEl = document.getElementById('error');
|
||||||
|
errorEl.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stats
|
||||||
|
const pending = await fetchData('notifications',
|
||||||
|
'SELECT COUNT(*) as c FROM notifications WHERE dismissed = 0 AND acted_at IS NULL');
|
||||||
|
document.getElementById('pending-count').textContent = pending[0]?.c || 0;
|
||||||
|
|
||||||
|
const yesterday = new Date(Date.now() - 86400000).toISOString();
|
||||||
|
const activity = await fetchData('activity_log',
|
||||||
|
`SELECT COUNT(*) as c FROM activity_log WHERE timestamp > '${yesterday}'`);
|
||||||
|
document.getElementById('activity-count').textContent = activity[0]?.c || 0;
|
||||||
|
|
||||||
|
const escalations = await fetchData('escalations',
|
||||||
|
"SELECT COUNT(*) as c FROM escalations WHERE status = 'open'");
|
||||||
|
document.getElementById('escalation-count').textContent = escalations[0]?.c || 0;
|
||||||
|
|
||||||
|
// Notifications table
|
||||||
|
const notifications = await fetchData('notifications',
|
||||||
|
'SELECT * FROM notifications ORDER BY seen_at DESC LIMIT 10');
|
||||||
|
|
||||||
|
if (notifications.length === 0) {
|
||||||
|
document.getElementById('notifications-table').innerHTML =
|
||||||
|
'<div class="loading">No notifications yet.</div>';
|
||||||
|
} else {
|
||||||
|
let html = `<table>
|
||||||
|
<thead><tr><th>Status</th><th>Title</th><th>Repo</th><th>Seen</th></tr></thead>
|
||||||
|
<tbody>`;
|
||||||
|
for (const n of notifications) {
|
||||||
|
const status = getNotificationStatus(n);
|
||||||
|
const title = n.url ? `<a href="${n.url}" target="_blank">${n.title}</a>` : n.title;
|
||||||
|
html += `<tr>
|
||||||
|
<td><span class="status-badge ${status.class}">${status.text}</span></td>
|
||||||
|
<td>${title}</td>
|
||||||
|
<td>${n.repo_id || '-'}</td>
|
||||||
|
<td class="timestamp">${formatDate(n.seen_at)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
html += '</tbody></table>';
|
||||||
|
document.getElementById('notifications-table').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activity table
|
||||||
|
const activityLog = await fetchData('activity_log',
|
||||||
|
'SELECT * FROM activity_log ORDER BY timestamp DESC LIMIT 10');
|
||||||
|
|
||||||
|
if (activityLog.length === 0) {
|
||||||
|
document.getElementById('activity-table').innerHTML =
|
||||||
|
'<div class="loading">No activity yet.</div>';
|
||||||
|
} else {
|
||||||
|
let html = `<table>
|
||||||
|
<thead><tr><th>Time</th><th>Action</th><th>Detail</th></tr></thead>
|
||||||
|
<tbody>`;
|
||||||
|
for (const a of activityLog) {
|
||||||
|
const detail = a.github_url ?
|
||||||
|
`<a href="${a.github_url}" target="_blank">${a.detail || '-'}</a>` :
|
||||||
|
(a.detail || '-');
|
||||||
|
html += `<tr>
|
||||||
|
<td class="timestamp">${formatDate(a.timestamp)}</td>
|
||||||
|
<td>${a.action}</td>
|
||||||
|
<td>${detail}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
html += '</tbody></table>';
|
||||||
|
document.getElementById('activity-table').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escalations table
|
||||||
|
const escList = await fetchData('escalations',
|
||||||
|
"SELECT * FROM escalations WHERE status = 'open' ORDER BY created_at DESC LIMIT 10");
|
||||||
|
|
||||||
|
if (escList.length === 0) {
|
||||||
|
document.getElementById('escalations-table').innerHTML =
|
||||||
|
'<div class="loading">No open escalations. ✨</div>';
|
||||||
|
} else {
|
||||||
|
let html = `<table>
|
||||||
|
<thead><tr><th>ID</th><th>Category</th><th>Title</th><th>Created</th></tr></thead>
|
||||||
|
<tbody>`;
|
||||||
|
for (const e of escList) {
|
||||||
|
html += `<tr>
|
||||||
|
<td>#${e.id}</td>
|
||||||
|
<td>${e.category}</td>
|
||||||
|
<td>${e.title}</td>
|
||||||
|
<td class="timestamp">${formatDate(e.created_at)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
html += '</tbody></table>';
|
||||||
|
document.getElementById('escalations-table').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
errorEl.textContent = `Failed to load data: ${err.message}. Is the Datasette server running on port 8100?`;
|
||||||
|
errorEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load on page load
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
// Auto-refresh every 60 seconds
|
||||||
|
setInterval(loadData, 60000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
97
docs/SKILL.md
Normal file
97
docs/SKILL.md
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
---
|
||||||
|
name: argus
|
||||||
|
description: "GitHub/Forgejo community awareness tool. TRIGGERS: notifications, community triage, PR reviews, issue tracking, escalations. Use when managing GitHub notifications, logging community actions, or tracking escalations."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Argus Skill
|
||||||
|
|
||||||
|
CLI tool for GitHub/Forgejo community awareness — tracking notifications, logging activity, managing escalations.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize database
|
||||||
|
argus init
|
||||||
|
|
||||||
|
# Pull notifications from GitHub
|
||||||
|
argus notif pull
|
||||||
|
|
||||||
|
# View status
|
||||||
|
argus status
|
||||||
|
|
||||||
|
# Start dashboard
|
||||||
|
argus serve
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Status & Info
|
||||||
|
|
||||||
|
```bash
|
||||||
|
argus version # Show version
|
||||||
|
argus status # Summary: pending, activity, escalations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
```bash
|
||||||
|
argus notif pull [--dry-run] # Fetch from GitHub API
|
||||||
|
argus notif list [--pending] # List notifications
|
||||||
|
argus notif act <id> <action> # Mark as acted
|
||||||
|
argus notif dismiss <id> [reason] # Dismiss
|
||||||
|
argus notif audit [--stale-hours] # Auto-escalate stale
|
||||||
|
```
|
||||||
|
|
||||||
|
### Activity Logging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
argus activity log <action> <detail> [--issue ID]
|
||||||
|
argus activity list [--limit N]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Escalations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
argus escalate create --category CAT --title "..."
|
||||||
|
argus escalate list [--status open]
|
||||||
|
argus escalate ack <id>
|
||||||
|
argus escalate resolve <id> --by WHO --resolution "..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
argus serve [--port 8100] [--dashboard-port 8101]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `ARGUS_DB` | `~/.argus/argus.db` | Database path |
|
||||||
|
| `ARGUS_PORT` | `8100` | Datasette port |
|
||||||
|
| `ARGUS_DASHBOARD_PORT` | `8101` | Dashboard port |
|
||||||
|
| `GH_TOKEN` | — | GitHub API token |
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Pull notifications:** `argus notif pull`
|
||||||
|
2. **Review pending:** `argus notif list --pending`
|
||||||
|
3. **Act or dismiss:** `argus notif act <id> reviewed` or `argus notif dismiss <id> duplicate`
|
||||||
|
4. **Log activity:** `argus activity log commented "Answered question"`
|
||||||
|
5. **Check dashboard:** Open http://localhost:8101
|
||||||
|
|
||||||
|
## Sub-Agent Usage
|
||||||
|
|
||||||
|
For automated community management:
|
||||||
|
|
||||||
|
```
|
||||||
|
Read notifications with argus notif list --pending.
|
||||||
|
For each notification:
|
||||||
|
- If PR: review changes, comment if needed
|
||||||
|
- If issue: assess priority, respond or escalate
|
||||||
|
- Log action with argus activity log
|
||||||
|
After processing: argus notif audit
|
||||||
|
```
|
||||||
|
|
||||||
|
See `procedures/subagent-completion.md` for detailed patterns.
|
||||||
120
docs/procedures/subagent-completion.md
Normal file
120
docs/procedures/subagent-completion.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# Argus Sub-Agent Procedures
|
||||||
|
|
||||||
|
Standard procedures for AI agents performing community management tasks.
|
||||||
|
|
||||||
|
## Notification Triage
|
||||||
|
|
||||||
|
### Input
|
||||||
|
- Pending notifications from `argus notif list --pending`
|
||||||
|
|
||||||
|
### Process
|
||||||
|
|
||||||
|
1. **Classify** each notification:
|
||||||
|
- `review-request`: Someone requested your review
|
||||||
|
- `mention`: You were mentioned
|
||||||
|
- `assign`: Issue/PR was assigned to you
|
||||||
|
- `comment`: New comment on subscribed thread
|
||||||
|
- `ci-failure`: CI/CD failure notification
|
||||||
|
|
||||||
|
2. **Prioritize**:
|
||||||
|
- P1 (immediate): Security issues, prod failures
|
||||||
|
- P2 (same day): Review requests, direct mentions
|
||||||
|
- P3 (when possible): General comments, updates
|
||||||
|
|
||||||
|
3. **Act**:
|
||||||
|
- For each notification, either:
|
||||||
|
- Take action (review, respond, fix)
|
||||||
|
- Escalate (create escalation)
|
||||||
|
- Dismiss (with reason)
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After processing each notification
|
||||||
|
argus notif act <id> <action> "<brief detail>"
|
||||||
|
|
||||||
|
# Or dismiss
|
||||||
|
argus notif dismiss <id> "<reason>"
|
||||||
|
|
||||||
|
# Log activity
|
||||||
|
argus activity log <action> "<detail>" [--issue <id>]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pull Request Review
|
||||||
|
|
||||||
|
### Input
|
||||||
|
- PR notification or explicit request
|
||||||
|
|
||||||
|
### Process
|
||||||
|
|
||||||
|
1. **Read PR**: Check title, description, changes
|
||||||
|
2. **Assess scope**: Is this a minor fix or major change?
|
||||||
|
3. **Review code**: Look for issues, suggest improvements
|
||||||
|
4. **Leave feedback**: Comment on GitHub
|
||||||
|
5. **Log action**: Record what you did
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
argus notif act <id> reviewed "Approved with comments"
|
||||||
|
argus activity log pr-review "Reviewed PR #123: Added caching layer"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Escalation Handling
|
||||||
|
|
||||||
|
### When to Escalate
|
||||||
|
|
||||||
|
- Notification pending > 48 hours
|
||||||
|
- Security-related issues
|
||||||
|
- Unclear requirements needing human decision
|
||||||
|
- Permission/access issues
|
||||||
|
|
||||||
|
### Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create escalation
|
||||||
|
argus escalate create \
|
||||||
|
--category "stale-notification" \
|
||||||
|
--title "PR #123 needs maintainer input" \
|
||||||
|
--priority high
|
||||||
|
|
||||||
|
# After human acknowledges
|
||||||
|
argus escalate ack <id>
|
||||||
|
|
||||||
|
# After resolution
|
||||||
|
argus escalate resolve <id> \
|
||||||
|
--by "k9ert" \
|
||||||
|
--resolution "Merged after fixes"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Daily Routine
|
||||||
|
|
||||||
|
Suggested daily workflow for automated community management:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Morning
|
||||||
|
argus notif pull
|
||||||
|
argus status
|
||||||
|
argus notif list --pending --limit 20
|
||||||
|
|
||||||
|
# Process notifications (triage, respond, escalate)
|
||||||
|
# ... actions ...
|
||||||
|
|
||||||
|
# End of day
|
||||||
|
argus notif audit --stale-hours 48
|
||||||
|
argus activity list --limit 10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
For summaries:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Via datasette or direct SQL
|
||||||
|
SELECT action, COUNT(*)
|
||||||
|
FROM activity_log
|
||||||
|
WHERE timestamp > date('now', '-7 days')
|
||||||
|
GROUP BY action;
|
||||||
|
```
|
||||||
|
|
||||||
|
Dashboard at http://localhost:8101 provides visual overview.
|
||||||
81
lib/schema.sql
Normal file
81
lib/schema.sql
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
-- Argus Database Schema
|
||||||
|
-- Version: 0.1.0
|
||||||
|
|
||||||
|
-- Repositories being tracked
|
||||||
|
CREATE TABLE IF NOT EXISTS repos (
|
||||||
|
id TEXT PRIMARY KEY, -- owner/repo
|
||||||
|
description TEXT,
|
||||||
|
is_primary INTEGER DEFAULT 0,
|
||||||
|
last_synced_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Issues and PRs
|
||||||
|
CREATE TABLE IF NOT EXISTS issues (
|
||||||
|
id TEXT PRIMARY KEY, -- owner/repo#number
|
||||||
|
repo_id TEXT NOT NULL,
|
||||||
|
number INTEGER NOT NULL,
|
||||||
|
type TEXT NOT NULL, -- issue | pr
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL, -- open | closed | merged
|
||||||
|
author TEXT,
|
||||||
|
labels TEXT DEFAULT '[]',
|
||||||
|
created_at TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
FOREIGN KEY (repo_id) REFERENCES repos(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- GitHub notifications
|
||||||
|
CREATE TABLE IF NOT EXISTS notifications (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
issue_id TEXT,
|
||||||
|
repo_id TEXT,
|
||||||
|
reason TEXT,
|
||||||
|
title TEXT,
|
||||||
|
url TEXT,
|
||||||
|
seen_at TEXT,
|
||||||
|
acted_at TEXT,
|
||||||
|
action_taken TEXT,
|
||||||
|
dismissed INTEGER DEFAULT 0,
|
||||||
|
FOREIGN KEY (issue_id) REFERENCES issues(id),
|
||||||
|
FOREIGN KEY (repo_id) REFERENCES repos(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Activity log
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT DEFAULT (datetime('now')),
|
||||||
|
issue_id TEXT,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
detail TEXT,
|
||||||
|
github_url TEXT,
|
||||||
|
FOREIGN KEY (issue_id) REFERENCES issues(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Escalations
|
||||||
|
CREATE TABLE IF NOT EXISTS escalations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
priority TEXT DEFAULT 'normal',
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
detail TEXT,
|
||||||
|
status TEXT DEFAULT 'open',
|
||||||
|
resolved_at TEXT,
|
||||||
|
resolved_by TEXT,
|
||||||
|
resolution TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Poll history
|
||||||
|
CREATE TABLE IF NOT EXISTS notification_polls (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
polled_at TEXT DEFAULT (datetime('now')),
|
||||||
|
new_count INTEGER DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for common queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notifications_dismissed ON notifications(dismissed);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notifications_acted ON notifications(acted_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_timestamp ON activity_log(timestamp);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_escalations_status ON escalations(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_issues_state ON issues(state);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_issues_repo ON issues(repo_id);
|
||||||
80
scripts/install.sh
Executable file
80
scripts/install.sh
Executable file
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Argus Installation Script
|
||||||
|
#
|
||||||
|
set -e
|
||||||
|
|
||||||
|
INSTALL_DIR="/opt/argus"
|
||||||
|
SKILL_LINK="/olymp/shared/skills/argus"
|
||||||
|
|
||||||
|
echo "╔════════════════════════════════════════╗"
|
||||||
|
echo "║ Argus Installation ║"
|
||||||
|
echo "╚════════════════════════════════════════╝"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
echo "Checking dependencies..."
|
||||||
|
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
echo "Error: python3 is required but not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v gh &> /dev/null; then
|
||||||
|
echo "Warning: gh CLI not found. 'argus notif pull' will not work."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install datasette if not present
|
||||||
|
if ! command -v datasette &> /dev/null; then
|
||||||
|
echo "Installing datasette..."
|
||||||
|
pip3 install datasette --quiet
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clone or update repo
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
echo "Updating existing installation..."
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
git pull --quiet
|
||||||
|
else
|
||||||
|
echo "Installing to $INSTALL_DIR..."
|
||||||
|
# If running from repo directory
|
||||||
|
if [ -f "bin/argus" ]; then
|
||||||
|
cp -r . "$INSTALL_DIR"
|
||||||
|
else
|
||||||
|
echo "Error: Run this script from the argus repository directory."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create symlink for CLI
|
||||||
|
echo "Creating CLI symlink..."
|
||||||
|
ln -sf "$INSTALL_DIR/bin/argus" /usr/local/bin/argus
|
||||||
|
|
||||||
|
# Install systemd service
|
||||||
|
echo "Installing systemd service..."
|
||||||
|
cp "$INSTALL_DIR/systemd/argus.service" /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Create skill symlink if /olymp/shared/skills exists
|
||||||
|
if [ -d "/olymp/shared/skills" ]; then
|
||||||
|
echo "Creating skill symlink..."
|
||||||
|
ln -sf "$INSTALL_DIR/docs" "$SKILL_LINK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize database if not exists
|
||||||
|
if [ ! -f "$HOME/.argus/argus.db" ]; then
|
||||||
|
echo "Initializing database..."
|
||||||
|
argus init
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "✓ Installation complete!"
|
||||||
|
echo
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Set up GitHub token: export GH_TOKEN=your_token"
|
||||||
|
echo " 2. Pull notifications: argus notif pull"
|
||||||
|
echo " 3. Start dashboard: systemctl start argus"
|
||||||
|
echo
|
||||||
|
echo "Dashboard will be available at:"
|
||||||
|
echo " - Datasette API: http://localhost:8100"
|
||||||
|
echo " - Dashboard: http://localhost:8101"
|
||||||
44
scripts/uninstall.sh
Executable file
44
scripts/uninstall.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Argus Uninstallation Script
|
||||||
|
#
|
||||||
|
set -e
|
||||||
|
|
||||||
|
INSTALL_DIR="/opt/argus"
|
||||||
|
SKILL_LINK="/olymp/shared/skills/argus"
|
||||||
|
|
||||||
|
echo "╔════════════════════════════════════════╗"
|
||||||
|
echo "║ Argus Uninstallation ║"
|
||||||
|
echo "╚════════════════════════════════════════╝"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Stop and disable service
|
||||||
|
echo "Stopping argus service..."
|
||||||
|
systemctl stop argus 2>/dev/null || true
|
||||||
|
systemctl disable argus 2>/dev/null || true
|
||||||
|
|
||||||
|
# Remove systemd service
|
||||||
|
echo "Removing systemd service..."
|
||||||
|
rm -f /etc/systemd/system/argus.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Remove CLI symlink
|
||||||
|
echo "Removing CLI symlink..."
|
||||||
|
rm -f /usr/local/bin/argus
|
||||||
|
|
||||||
|
# Remove skill symlink
|
||||||
|
echo "Removing skill symlink..."
|
||||||
|
rm -f "$SKILL_LINK"
|
||||||
|
|
||||||
|
# Remove installation directory
|
||||||
|
echo "Removing installation directory..."
|
||||||
|
rm -rf "$INSTALL_DIR"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "✓ Uninstallation complete!"
|
||||||
|
echo
|
||||||
|
echo "Note: User data at ~/.argus/ was preserved."
|
||||||
|
echo "To remove it: rm -rf ~/.argus"
|
||||||
|
echo
|
||||||
|
echo "Datasette was not uninstalled."
|
||||||
|
echo "To remove it: pip3 uninstall datasette"
|
||||||
15
systemd/argus.service
Normal file
15
systemd/argus.service
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Argus Community Dashboard
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/argus serve
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
Environment=ARGUS_DB=/root/.argus/argus.db
|
||||||
|
Environment=ARGUS_PORT=8100
|
||||||
|
Environment=ARGUS_DASHBOARD_PORT=8101
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Loading…
Add table
Reference in a new issue