feat: initial release — traefik, unifi, ups plugins

First-party plugins for Fabled Scryer (https://github.com/bvandeusen/fabledscryer).

traefik: Prometheus metrics scraping, stats history, access log parsing (GeoIP optional)
unifi:   UniFi controller integration — WAN health, devices, clients, DPI, events
ups:     UPS monitoring via NUT (Network UPS Tools), Ansible shutdown automation

Each plugin follows the Fabled Scryer plugin contract:
  setup(app), get_scheduled_tasks(), get_blueprint()

index.yaml lists all three plugins in the catalog format. Populate
checksum_sha256 entries once release zips are published via GitHub Releases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 18:28:43 -04:00
commit ad4fcd1cfd
50 changed files with 4808 additions and 0 deletions
@@ -0,0 +1,37 @@
# plugins/traefik/migrations/versions/traefik_001_initial.py
"""Traefik metrics table
Revision ID: traefik_001_initial
Revises: (none — this is a branch off the core head via depends_on)
Create Date: 2026-03-17
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "traefik_001_initial"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = "traefik"
depends_on: Union[str, Sequence[str], None] = ("0004_app_settings",)
def upgrade() -> None:
op.create_table(
"traefik_metrics",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("router_name", sa.String(255), nullable=False),
sa.Column("scraped_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("request_rate", sa.Float, nullable=False),
sa.Column("error_rate_4xx_pct", sa.Float, nullable=False),
sa.Column("error_rate_5xx_pct", sa.Float, nullable=False),
sa.Column("latency_p50_ms", sa.Float, nullable=False),
sa.Column("latency_p95_ms", sa.Float, nullable=False),
sa.Column("latency_p99_ms", sa.Float, nullable=False),
)
op.create_index("ix_traefik_metrics_router_scraped", "traefik_metrics",
["router_name", "scraped_at"])
def downgrade() -> None:
op.drop_index("ix_traefik_metrics_router_scraped", "traefik_metrics")
op.drop_table("traefik_metrics")
@@ -0,0 +1,48 @@
"""Traefik extended stats: bandwidth, certs, global stats
Revision ID: traefik_002_stats
Revises: traefik_001_initial
Create Date: 2026-03-19
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "traefik_002_stats"
down_revision: Union[str, None] = "traefik_001_initial"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"traefik_metrics",
sa.Column("response_bytes_rate", sa.Float, nullable=False, server_default="0"),
)
op.create_table(
"traefik_certs",
sa.Column("serial", sa.String(255), primary_key=True),
sa.Column("cn", sa.String(512), nullable=True),
sa.Column("sans", sa.Text, nullable=True),
sa.Column("not_after", sa.DateTime(timezone=True), nullable=False),
sa.Column("scraped_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_table(
"traefik_global_stats",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("scraped_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("open_conns_total", sa.Float, nullable=True),
sa.Column("config_reloads_total", sa.Float, nullable=True),
sa.Column("config_last_reload_success", sa.Float, nullable=True),
sa.Column("process_memory_bytes", sa.Float, nullable=True),
)
op.create_index("ix_traefik_global_stats_scraped", "traefik_global_stats", ["scraped_at"])
def downgrade() -> None:
op.drop_index("ix_traefik_global_stats_scraped", "traefik_global_stats")
op.drop_table("traefik_global_stats")
op.drop_table("traefik_certs")
op.drop_column("traefik_metrics", "response_bytes_rate")
@@ -0,0 +1,40 @@
"""Traefik access log traffic summaries
Revision ID: traefik_003_access_log
Revises: traefik_002_stats
Create Date: 2026-03-20
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "traefik_003_access_log"
down_revision: Union[str, None] = "traefik_002_stats"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"traefik_access_summaries",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("period_start", sa.DateTime(timezone=True), nullable=False),
sa.Column("period_end", sa.DateTime(timezone=True), nullable=False),
sa.Column("total_requests", sa.Integer, nullable=False, server_default="0"),
sa.Column("internal_requests", sa.Integer, nullable=False, server_default="0"),
sa.Column("external_requests", sa.Integer, nullable=False, server_default="0"),
sa.Column("total_bytes", sa.Float, nullable=False, server_default="0"),
sa.Column("top_ips_json", sa.Text, nullable=False, server_default="[]"),
sa.Column("top_countries_json", sa.Text, nullable=False, server_default="[]"),
sa.Column("top_routers_json", sa.Text, nullable=False, server_default="[]"),
)
op.create_index(
"ix_traefik_access_summaries_period_end",
"traefik_access_summaries",
["period_end"],
)
def downgrade() -> None:
op.drop_index("ix_traefik_access_summaries_period_end", "traefik_access_summaries")
op.drop_table("traefik_access_summaries")