kctl-rustdesk 0.6.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. kctl_rustdesk-0.6.3/.gitignore +33 -0
  2. kctl_rustdesk-0.6.3/PKG-INFO +16 -0
  3. kctl_rustdesk-0.6.3/README.md +72 -0
  4. kctl_rustdesk-0.6.3/pyproject.toml +44 -0
  5. kctl_rustdesk-0.6.3/skills/rustdesk-admin/SKILL.md +160 -0
  6. kctl_rustdesk-0.6.3/src/kctl_rustdesk/__init__.py +3 -0
  7. kctl_rustdesk-0.6.3/src/kctl_rustdesk/__main__.py +3 -0
  8. kctl_rustdesk-0.6.3/src/kctl_rustdesk/cli.py +133 -0
  9. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/__init__.py +0 -0
  10. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/audit.py +143 -0
  11. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/backup.py +151 -0
  12. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/config_cmd.py +173 -0
  13. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/dashboard.py +98 -0
  14. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/doctor_cmd.py +57 -0
  15. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/health.py +125 -0
  16. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/maintenance.py +170 -0
  17. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/peers.py +112 -0
  18. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/setup.py +114 -0
  19. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/skill_cmd.py +76 -0
  20. kctl_rustdesk-0.6.3/src/kctl_rustdesk/commands/users.py +115 -0
  21. kctl_rustdesk-0.6.3/src/kctl_rustdesk/core/__init__.py +0 -0
  22. kctl_rustdesk-0.6.3/src/kctl_rustdesk/core/callbacks.py +29 -0
  23. kctl_rustdesk-0.6.3/src/kctl_rustdesk/core/config.py +69 -0
  24. kctl_rustdesk-0.6.3/src/kctl_rustdesk/core/executor.py +162 -0
  25. kctl_rustdesk-0.6.3/src/kctl_rustdesk/core/plugins.py +39 -0
  26. kctl_rustdesk-0.6.3/tests/conftest.py +77 -0
  27. kctl_rustdesk-0.6.3/tests/test_core.py +318 -0
  28. kctl_rustdesk-0.6.3/tests/test_peers.py +122 -0
  29. kctl_rustdesk-0.6.3/tests/test_smoke.py +149 -0
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ dist/
7
+ build/
8
+ .eggs/
9
+
10
+ # Virtual environments
11
+ .venv/
12
+ venv/
13
+
14
+ # IDE
15
+ .idea/
16
+ .vscode/
17
+ *.swp
18
+ *.swo
19
+
20
+ # Testing
21
+ .pytest_cache/
22
+ .coverage
23
+ htmlcov/
24
+ .mypy_cache/
25
+ .ruff_cache/
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Environment
32
+ .env
33
+ .env.local
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: kctl-rustdesk
3
+ Version: 0.6.3
4
+ Summary: Kodemeio RustDesk CLI — manage RustDesk server infrastructure
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: kctl-lib>=0.8.0
7
+ Requires-Dist: pydantic>=2.10.0
8
+ Requires-Dist: pyyaml>=6.0.2
9
+ Requires-Dist: rich>=13.9.0
10
+ Requires-Dist: typer>=0.15.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: mypy>=1.14.0; extra == 'dev'
13
+ Requires-Dist: pytest-httpx>=0.35.0; extra == 'dev'
14
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
15
+ Requires-Dist: ruff>=0.9.0; extra == 'dev'
16
+ Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
@@ -0,0 +1,72 @@
1
+ # kctl-rustdesk
2
+
3
+ Kodemeio RustDesk CLI — manage RustDesk server infrastructure.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv tool install .
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ kctl-rustdesk config init
15
+ kctl-rustdesk health
16
+ kctl-rustdesk dashboard
17
+ kctl-rustdesk peers list
18
+ ```
19
+
20
+ ## Command Groups
21
+
22
+ | Group | Description |
23
+ |-------|-------------|
24
+ | `config` | Manage CLI configuration and profiles |
25
+ | `health` | Health checks for RustDesk server |
26
+ | `dashboard` | System overview dashboard |
27
+ | `peers` | Manage RustDesk peers (devices) |
28
+ | `users` | Manage RustDesk users |
29
+ | `audit` | Audit logs and connection history |
30
+ | `backup` | Backup and restore RustDesk server data |
31
+ | `setup` | Server setup and configuration |
32
+ | `maintenance` | Maintenance and operational tasks |
33
+
34
+ ## Global Options
35
+
36
+ | Option | Short | Description |
37
+ |--------|-------|-------------|
38
+ | `--json` | | Output as JSON |
39
+ | `--quiet` | `-q` | Suppress info messages |
40
+ | `--format` | `-f` | Output format: pretty, json, csv, yaml |
41
+ | `--no-header` | | Omit column headers |
42
+ | `--profile` | `-p` | Config profile |
43
+ | `--host` | | Server host override |
44
+ | `--version` | `-V` | Show version and exit |
45
+
46
+ ## Configuration
47
+
48
+ Config lives in `~/.config/kodemeio/config.yaml` under the `rustdesk` service key.
49
+
50
+ ```bash
51
+ # Initialize default profile
52
+ kctl-rustdesk config init
53
+
54
+ # Add a named profile
55
+ kctl-rustdesk config add prod \
56
+ --host https://rustdesk.example.com \
57
+ --api-key $RUSTDESK_API_KEY
58
+
59
+ # Switch active profile
60
+ kctl-rustdesk config use prod
61
+
62
+ # Show current profile (key masked)
63
+ kctl-rustdesk config show
64
+ ```
65
+
66
+ ## Development
67
+
68
+ ```bash
69
+ uv run pytest tests/ -v
70
+ uv run ruff check src/
71
+ uv run mypy src/
72
+ ```
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kctl-rustdesk"
7
+ version = "0.6.3"
8
+ description = "Kodemeio RustDesk CLI — manage RustDesk server infrastructure"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "kctl-lib>=0.8.0",
12
+ "typer>=0.15.0",
13
+ "rich>=13.9.0",
14
+ "pydantic>=2.10.0",
15
+ "pyyaml>=6.0.2",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.3.0",
21
+ "pytest-httpx>=0.35.0",
22
+ "ruff>=0.9.0",
23
+ "mypy>=1.14.0",
24
+ "types-PyYAML>=6.0.0",
25
+ ]
26
+
27
+ [project.scripts]
28
+ kctl-rustdesk = "kctl_rustdesk.cli:_run"
29
+
30
+ [tool.uv.sources]
31
+ kctl-lib = { workspace = true }
32
+
33
+ [project.entry-points."kctl_rustdesk.plugins"]
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/kctl_rustdesk"]
37
+
38
+ [tool.ruff]
39
+ target-version = "py312"
40
+ line-length = 120
41
+
42
+ [tool.mypy]
43
+ python_version = "3.12"
44
+ strict = true
@@ -0,0 +1,160 @@
1
+ ---
2
+ name: rustdesk-admin
3
+ description: >
4
+ RustDesk remote desktop administration via kctl-rustdesk CLI (10 groups, ~36 commands).
5
+ MUST use for ANY kctl-rustdesk operation.
6
+ Triggers on: "active", "audit", "backup", "check", "clean", "cleanup", "client-config", "config", "connections", "count", "dashboard", "db-optimize", "db-stats", "export", "firewall", "generate", "get-key", "groups", "health", "init", "kctl-rustdesk", "logins", "logs", "maintenance", "peers", "profile", "profiles", "restore", "search", "setup", "skill", "stats", "test", "users", "version".
7
+ Auto-generated: 2026-04-05
8
+ registry_hash: 74352b3f8a29
9
+ ---
10
+
11
+ # rustdesk-admin — kctl-rustdesk CLI Reference
12
+
13
+ > Auto-generated from `kctl-rustdesk` command registry. Do not edit manually.
14
+ > To regenerate: `kctl-rustdesk skill generate`
15
+ > To add custom content: edit `SKILL.extra.md` in the same directory.
16
+
17
+ ## Overview
18
+
19
+ **CLI:** `kctl-rustdesk`
20
+ **Command groups:** 10
21
+ **Total commands:** ~36
22
+ **Install:** `cd cli && uv tool install --editable .`
23
+
24
+ ## Global Options
25
+
26
+ | Flag | Description |
27
+ |------|-------------|
28
+ | `--json` | JSON output |
29
+ | `--quiet`, `-q` | Suppress info messages |
30
+ | `--format`, `-f` | Output format: pretty/json/csv/yaml |
31
+ | `--no-header` | Omit CSV header row |
32
+ | `--profile`, `-p` | Config profile name |
33
+ | `--version`, `-V` | Show version |
34
+
35
+ ## Command Reference
36
+
37
+ ### `kctl-rustdesk audit`
38
+
39
+ Audit logs and connection history.
40
+
41
+ | Command | Description |
42
+ |---------|-------------|
43
+ | `audit active` | Show currently active sessions. |
44
+ | `audit connections [--today] [--limit]` | Show connection history. |
45
+ | `audit logins [--failed] [--limit]` | Show login history. |
46
+ | `audit stats` | Show connection statistics. |
47
+
48
+ ### `kctl-rustdesk backup`
49
+
50
+ Backup and restore RustDesk server data.
51
+
52
+ | Command | Description |
53
+ |---------|-------------|
54
+ | `backup clean [--days]` | Remove old backups. |
55
+ | `backup create` | Create a backup of keys and database. |
56
+ | `backup list` | List available backups. |
57
+ | `backup restore <backup_file>` | Restore from a backup file. |
58
+
59
+ ### `kctl-rustdesk config`
60
+
61
+ Manage CLI configuration and profiles.
62
+
63
+ | Command | Description |
64
+ |---------|-------------|
65
+ | `config init [--host] [--ssh_user] [--compose_file] [--env_file] [--project_name] [--domain] [--name]` | Initialize CLI configuration with a new profile. |
66
+ | `config profiles` | List all profiles. |
67
+ | `config show` | Show current configuration. |
68
+ | `config test` | Test connection to the RustDesk server. |
69
+ | `config use <name>` | Set the default profile. |
70
+
71
+ ### `kctl-rustdesk dashboard`
72
+
73
+ System overview dashboard.
74
+
75
+ | Command | Description |
76
+ |---------|-------------|
77
+ | `dashboard show [--compact]` | Show system overview dashboard. |
78
+
79
+ ### `kctl-rustdesk health`
80
+
81
+ Health checks for RustDesk server.
82
+
83
+ | Command | Description |
84
+ |---------|-------------|
85
+ | `health check [--as_json]` | Run health checks on RustDesk server. |
86
+
87
+ ### `kctl-rustdesk maintenance`
88
+
89
+ Maintenance and operational tasks.
90
+
91
+ | Command | Description |
92
+ |---------|-------------|
93
+ | `maintenance cleanup` | Clean up unused Docker resources. |
94
+ | `maintenance db-optimize` | Optimize the SQLite database (VACUUM + ANALYZE). |
95
+ | `maintenance db-stats` | Show database statistics. |
96
+ | `maintenance logs [--service] [--lines]` | View container logs. |
97
+ | `maintenance status` | Show container status and resource usage. |
98
+ | `maintenance version` | Show version information. |
99
+
100
+ ### `kctl-rustdesk peers`
101
+
102
+ Manage RustDesk peers (devices).
103
+
104
+ | Command | Description |
105
+ |---------|-------------|
106
+ | `peers count` | Count total peers. |
107
+ | `peers export` | Export all peers as JSON. |
108
+ | `peers get <peer_id>` | Get details for a specific peer. |
109
+ | `peers list [--online]` | List all registered peers. |
110
+ | `peers search <term>` | Search peers by ID, UUID, or note. |
111
+
112
+ ### `kctl-rustdesk setup`
113
+
114
+ Server setup and configuration.
115
+
116
+ | Command | Description |
117
+ |---------|-------------|
118
+ | `setup client-config` | Generate RustDesk client configuration string. |
119
+ | `setup firewall` | Show required firewall rules. |
120
+ | `setup get-key` | Display the server's public key. |
121
+ | `setup status` | Show setup status checklist. |
122
+
123
+ ### `kctl-rustdesk skill`
124
+
125
+ Claude Code skill management.
126
+
127
+ | Command | Description |
128
+ |---------|-------------|
129
+ | `skill generate [--output] [--install] [--check]` | Auto-generate SKILL.md from CLI command registry. |
130
+
131
+ **Examples:**
132
+ ```bash
133
+ kctl-rustdesk skill generate
134
+ kctl-rustdesk skill generate --install
135
+ kctl-rustdesk skill generate --check
136
+ ```
137
+
138
+ ### `kctl-rustdesk users`
139
+
140
+ Manage RustDesk users.
141
+
142
+ | Command | Description |
143
+ |---------|-------------|
144
+ | `users count` | Count users. |
145
+ | `users export` | Export all users as JSON. |
146
+ | `users get <username>` | Get details for a specific user. |
147
+ | `users groups` | List user groups. |
148
+ | `users list [--active]` | List all users. |
149
+
150
+ ## Configuration
151
+
152
+ Shared config: `~/.config/kodemeio/config.yaml`
153
+
154
+ ```bash
155
+ kctl-rustdesk config init # Interactive setup
156
+ kctl-rustdesk config show # Show current config
157
+ kctl-rustdesk config profiles # List profiles
158
+ kctl-rustdesk config current # Show active profile
159
+ kctl-rustdesk config validate # Verify config
160
+ ```
@@ -0,0 +1,3 @@
1
+ """kctl-rustdesk: Kodemeio RustDesk server management CLI."""
2
+
3
+ __version__ = "0.6.3"
@@ -0,0 +1,3 @@
1
+ from kctl_rustdesk.cli import _run
2
+
3
+ _run()
@@ -0,0 +1,133 @@
1
+ """kctl-rustdesk CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from kctl_lib import handle_cli_error
11
+ from kctl_lib.exceptions import KctlError
12
+
13
+ from kctl_rustdesk import __version__
14
+ from kctl_rustdesk.commands.audit import app as audit_app
15
+ from kctl_rustdesk.commands.backup import app as backup_app
16
+ from kctl_rustdesk.commands.config_cmd import app as config_app
17
+ from kctl_rustdesk.commands.dashboard import app as dashboard_app
18
+ from kctl_rustdesk.commands.health import app as health_app
19
+ from kctl_rustdesk.commands.maintenance import app as maintenance_app
20
+ from kctl_rustdesk.commands.peers import app as peers_app
21
+ from kctl_rustdesk.commands.setup import app as setup_app
22
+ from kctl_rustdesk.commands.users import app as users_app
23
+ from kctl_rustdesk.core.plugins import discover_and_load_plugins
24
+ from kctl_rustdesk.commands.skill_cmd import app as skill_app
25
+ from kctl_rustdesk.commands.doctor_cmd import app as doctor_app
26
+ from kctl_lib.self_update import notify_if_outdated
27
+ from kctl_lib.tui import add_tui_command
28
+
29
+
30
+ def version_callback(value: bool) -> None:
31
+ if value:
32
+ typer.echo(f"kctl-rustdesk {__version__}")
33
+ raise typer.Exit()
34
+
35
+
36
+ app = typer.Typer(
37
+ name="kctl-rustdesk",
38
+ help="Kodemeio RustDesk CLI — manage RustDesk server infrastructure.",
39
+ no_args_is_help=True,
40
+ rich_markup_mode="rich",
41
+ pretty_exceptions_enable=False,
42
+ )
43
+
44
+
45
+ @app.callback()
46
+ def main(
47
+ ctx: typer.Context,
48
+ json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
49
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
50
+ profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile")] = None,
51
+ format: Annotated[str, typer.Option("--format", "-f", help="Output format")] = "pretty",
52
+ no_header: Annotated[bool, typer.Option("--no-header", help="Omit column headers")] = False,
53
+ host: Annotated[str | None, typer.Option("--host", help="Server host override")] = None,
54
+ version: Annotated[bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True)] = False,
55
+ ) -> None:
56
+ """Manage RustDesk server infrastructure."""
57
+ from kctl_rustdesk.core.callbacks import AppContext
58
+
59
+ ctx.ensure_object(dict)
60
+ ctx.obj = AppContext(
61
+ json_mode=json_output,
62
+ quiet=quiet,
63
+ profile=profile,
64
+ format=format,
65
+ no_header=no_header,
66
+ host_override=host,
67
+ )
68
+ notify_if_outdated(ctx.obj.output, "kctl-rustdesk", __version__)
69
+
70
+
71
+ app.add_typer(config_app, name="config")
72
+ app.add_typer(health_app, name="health")
73
+ app.add_typer(dashboard_app, name="dashboard")
74
+ app.add_typer(peers_app, name="peers")
75
+ app.add_typer(users_app, name="users")
76
+ app.add_typer(audit_app, name="audit")
77
+ app.add_typer(backup_app, name="backup")
78
+ app.add_typer(setup_app, name="setup")
79
+ app.add_typer(maintenance_app, name="maintenance")
80
+ app.add_typer(skill_app, name="skill", hidden=True)
81
+ app.add_typer(doctor_app, name="doctor")
82
+
83
+ discover_and_load_plugins(app)
84
+ add_tui_command(app, service_key="rustdesk", version=__version__)
85
+
86
+
87
+ @app.command("self-update")
88
+ def self_update_cmd(ctx: typer.Context) -> None:
89
+ """Check for updates and upgrade kctl-rustdesk."""
90
+ actx = ctx.obj
91
+ out = actx.output
92
+
93
+ from kctl_lib.self_update import check_update
94
+ from kctl_lib.self_update import update as do_update
95
+
96
+ latest = check_update("kctl-rustdesk", __version__)
97
+ if latest:
98
+ out.info(f"Updating to {latest}...")
99
+ do_update("kctl-rustdesk")
100
+ out.success(f"Updated to {latest}")
101
+ else:
102
+ out.success("Already up to date")
103
+
104
+
105
+ @app.command()
106
+ def completions(
107
+ shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
108
+ install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
109
+ ) -> None:
110
+ """Generate or install shell completions."""
111
+ from kctl_lib.completions import get_completion_script, install_completions
112
+
113
+ if install:
114
+ path = install_completions("kctl-rustdesk", shell)
115
+ if path:
116
+ typer.echo(f"Completions installed to {path}")
117
+ else:
118
+ typer.echo(f"Could not install completions for {shell}", err=True)
119
+ raise typer.Exit(code=1)
120
+ else:
121
+ script = get_completion_script("kctl-rustdesk", shell)
122
+ typer.echo(script)
123
+
124
+
125
+ def _run() -> None:
126
+ try:
127
+ app()
128
+ except KctlError as e:
129
+ handle_cli_error(e)
130
+
131
+
132
+ if __name__ == "__main__":
133
+ _run()
@@ -0,0 +1,143 @@
1
+ """Audit log and connection history commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from kctl_rustdesk.core.callbacks import AppContext
10
+
11
+ app = typer.Typer(help="Audit logs and connection history.")
12
+
13
+
14
+ @app.command()
15
+ def connections(
16
+ ctx: typer.Context,
17
+ today: Annotated[bool, typer.Option("--today", help="Only today")] = False,
18
+ limit: Annotated[int, typer.Option("--limit", "-n", help="Max rows")] = 50,
19
+ ) -> None:
20
+ """Show connection history."""
21
+ c: AppContext = ctx.obj
22
+ ex = c.executor
23
+
24
+ where = "WHERE date(created_at) = date('now')" if today else ""
25
+ sql = f"SELECT peer_id, ip, created_at FROM conn_log {where} ORDER BY created_at DESC LIMIT {limit};"
26
+
27
+ rows_data = ex.query_db(sql)
28
+ rows = [[r.get("peer_id", ""), r.get("ip", ""), r.get("created_at", "")] for r in rows_data]
29
+
30
+ title = "Connections (today)" if today else f"Connections (last {limit})"
31
+ c.output.table(
32
+ title,
33
+ [("Peer ID", "cyan"), ("IP", ""), ("Time", "dim")],
34
+ rows,
35
+ data_for_json=rows_data,
36
+ )
37
+
38
+
39
+ @app.command()
40
+ def logins(
41
+ ctx: typer.Context,
42
+ failed: Annotated[bool, typer.Option("--failed", help="Only failed logins")] = False,
43
+ limit: Annotated[int, typer.Option("--limit", "-n", help="Max rows")] = 50,
44
+ ) -> None:
45
+ """Show login history."""
46
+ c: AppContext = ctx.obj
47
+ ex = c.executor
48
+
49
+ where = "WHERE type != 0" if failed else ""
50
+ sql = f"SELECT user_id, ip, type, created_at FROM login_log {where} ORDER BY created_at DESC LIMIT {limit};"
51
+
52
+ rows_data = ex.query_db(sql)
53
+ rows = [
54
+ [
55
+ r.get("user_id", ""),
56
+ r.get("ip", ""),
57
+ "failed" if r.get("type", "0") != "0" else "ok",
58
+ r.get("created_at", ""),
59
+ ]
60
+ for r in rows_data
61
+ ]
62
+
63
+ title = "Failed Logins" if failed else f"Logins (last {limit})"
64
+ c.output.table(
65
+ title,
66
+ [("User", "cyan"), ("IP", ""), ("Status", ""), ("Time", "dim")],
67
+ rows,
68
+ data_for_json=rows_data,
69
+ )
70
+
71
+
72
+ @app.command()
73
+ def stats(ctx: typer.Context) -> None:
74
+ """Show connection statistics."""
75
+ c: AppContext = ctx.obj
76
+ ex = c.executor
77
+
78
+ total_conns = ex.query_db_scalar("SELECT count(*) FROM conn_log;")
79
+ today_conns = ex.query_db_scalar("SELECT count(*) FROM conn_log WHERE date(created_at) = date('now');")
80
+ unique_peers = ex.query_db_scalar("SELECT count(DISTINCT peer_id) FROM conn_log;")
81
+ unique_ips = ex.query_db_scalar("SELECT count(DISTINCT ip) FROM conn_log;")
82
+ total_logins = ex.query_db_scalar("SELECT count(*) FROM login_log;")
83
+ failed_logins = ex.query_db_scalar("SELECT count(*) FROM login_log WHERE type != 0;")
84
+
85
+ top_peers = ex.query_db("SELECT peer_id, count(*) as cnt FROM conn_log GROUP BY peer_id ORDER BY cnt DESC LIMIT 5;")
86
+
87
+ sections = [
88
+ (
89
+ "Connection Stats",
90
+ [
91
+ ("Total connections", total_conns),
92
+ ("Today", today_conns),
93
+ ("Unique peers", unique_peers),
94
+ ("Unique IPs", unique_ips),
95
+ ],
96
+ ),
97
+ (
98
+ "Login Stats",
99
+ [
100
+ ("Total logins", total_logins),
101
+ ("Failed logins", failed_logins),
102
+ ],
103
+ ),
104
+ ("Top Peers", [(p.get("peer_id", ""), f"{p.get('cnt', 0)} connections") for p in top_peers]),
105
+ ]
106
+
107
+ c.output.detail(
108
+ "Audit Statistics",
109
+ sections,
110
+ data_for_json={
111
+ "connections": {
112
+ "total": int(total_conns),
113
+ "today": int(today_conns),
114
+ "unique_peers": int(unique_peers),
115
+ "unique_ips": int(unique_ips),
116
+ },
117
+ "logins": {"total": int(total_logins), "failed": int(failed_logins)},
118
+ "top_peers": top_peers,
119
+ },
120
+ )
121
+
122
+
123
+ @app.command()
124
+ def active(ctx: typer.Context) -> None:
125
+ """Show currently active sessions."""
126
+ c: AppContext = ctx.obj
127
+ ex = c.executor
128
+
129
+ sql = (
130
+ "SELECT peer_id, ip, created_at FROM conn_log "
131
+ "WHERE created_at > datetime('now', '-5 minutes') "
132
+ "ORDER BY created_at DESC;"
133
+ )
134
+
135
+ rows_data = ex.query_db(sql)
136
+ rows = [[r.get("peer_id", ""), r.get("ip", ""), r.get("created_at", "")] for r in rows_data]
137
+
138
+ c.output.table(
139
+ "Active Sessions (last 5m)",
140
+ [("Peer ID", "cyan"), ("IP", ""), ("Connected", "dim")],
141
+ rows,
142
+ data_for_json=rows_data,
143
+ )