chore: rename project Roundtable → Steward

Renames the Python package directory, CLI command, env var prefix,
docker-compose service/container/image, Postgres role/db, and all
visible branding. Marketing form is "Fabled Steward".

Clean break from the previous rebrand: drops the fabledscryer→roundtable
import shim in __init__.py and the FABLEDSCRYER_* env var fallback in
config.py and migrations/env.py. Env vars are now STEWARD_* only.

Heads-up for existing deployments:
- Postgres user/db renamed fabledscryer → steward in docker-compose.yml.
  Existing volumes need the role/db renamed inside Postgres, or override
  POSTGRES_USER/POSTGRES_DB to keep the old names.
- Host-agent systemd unit is now steward-agent.service. Existing agents
  keep running under the old name; reinstall to switch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 16:20:14 -04:00
parent e118543b2e
commit 88ab5b917e
148 changed files with 570 additions and 619 deletions
+13 -13
View File
@@ -1,12 +1,12 @@
# Architecture
Roundtable is a single Quart (async Python) process. There are no separate worker processes, no message brokers, and no build pipeline for the frontend. All monitoring, scheduling, and request handling runs on a single asyncio event loop.
Steward is a single Quart (async Python) process. There are no separate worker processes, no message brokers, and no build pipeline for the frontend. All monitoring, scheduling, and request handling runs on a single asyncio event loop.
---
## Startup Sequence
`create_app()` in `roundtable/app.py` runs these steps synchronously before the event loop starts:
`create_app()` in `steward/app.py` runs these steps synchronously before the event loop starts:
| Step | What happens |
|---|---|
@@ -32,14 +32,14 @@ All routes are Quart Blueprints registered in `app.py`:
| Prefix | Blueprint | Module |
|---|---|---|
| `/auth/` | `auth_bp` | `roundtable/auth/routes.py` |
| `/` | `dashboard_bp` | `roundtable/dashboard/routes.py` |
| `/hosts/` | `hosts_bp` | `roundtable/hosts/routes.py` |
| `/ping/` | `ping_bp` | `roundtable/ping/routes.py` |
| `/dns/` | `dns_bp` | `roundtable/dns/routes.py` |
| `/alerts/` | `alerts_bp` | `roundtable/alerts/routes.py` |
| `/ansible/` | `ansible_bp` | `roundtable/ansible/routes.py` |
| `/settings/` | `settings_bp` | `roundtable/settings/routes.py` |
| `/auth/` | `auth_bp` | `steward/auth/routes.py` |
| `/` | `dashboard_bp` | `steward/dashboard/routes.py` |
| `/hosts/` | `hosts_bp` | `steward/hosts/routes.py` |
| `/ping/` | `ping_bp` | `steward/ping/routes.py` |
| `/dns/` | `dns_bp` | `steward/dns/routes.py` |
| `/alerts/` | `alerts_bp` | `steward/alerts/routes.py` |
| `/ansible/` | `ansible_bp` | `steward/ansible/routes.py` |
| `/settings/` | `settings_bp` | `steward/settings/routes.py` |
| `/plugins/<name>/` | plugin blueprint | `plugins/<name>/routes.py` |
Plugin blueprints are mounted automatically by `load_plugins()` using the plugin directory name as the URL prefix.
@@ -48,7 +48,7 @@ Plugin blueprints are mounted automatically by `load_plugins()` using the plugin
## Scheduler
`roundtable/core/scheduler.py` exports two things:
`steward/core/scheduler.py` exports two things:
- `ScheduledTask` dataclass — holds a `name`, `coro_factory` (zero-argument callable returning a coroutine), `interval_seconds`, and optional `run_on_startup` flag
- `start_scheduler(tasks)` — async function that loops every second, calling `asyncio.create_task()` for each task whose interval has elapsed
@@ -93,9 +93,9 @@ This means the only file you must touch to get the app running is the database U
No JavaScript framework, no build step. The frontend is:
- **Jinja2 templates** rendered server-side (`roundtable/templates/`)
- **Jinja2 templates** rendered server-side (`steward/templates/`)
- **HTMX** for live-updating fragments (dashboard widgets, ping pills, DNS status)
- A single CSS design system in `roundtable/templates/base.html` using CSS custom properties
- A single CSS design system in `steward/templates/base.html` using CSS custom properties
Live-updating widgets use HTMX polling:
```html
+2 -2
View File
@@ -14,7 +14,7 @@ When any monitor or plugin calls `record_metric()`:
4. If a state transition occurs, an `AlertEvent` row is written
5. Notification I/O is deferred outside the transaction via `asyncio.create_task()`
**Key function:** `roundtable/core/alerts.py``record_metric(session, source_module, resource_name, metric_name, value)`
**Key function:** `steward/core/alerts.py``record_metric(session, source_module, resource_name, metric_name, value)`
`record_metric()` must always be called inside an active transaction:
@@ -130,7 +130,7 @@ Webhook is skipped if `webhook.url` is empty.
## Data Models
Defined in `roundtable/models/alerts.py`:
Defined in `steward/models/alerts.py`:
- **`alert_rules`** — one row per configured rule
- **`alert_states`** — one row per rule, tracks current state and consecutive failure count
+6 -6
View File
@@ -1,6 +1,6 @@
# Ansible Integration
Roundtable can browse, trigger, and stream output from Ansible playbooks directly from the web UI. Runs execute as asyncio tasks inside the same process — no Celery, no external workers.
Steward can browse, trigger, and stream output from Ansible playbooks directly from the web UI. Runs execute as asyncio tasks inside the same process — no Celery, no external workers.
---
@@ -115,7 +115,7 @@ Output stored in the DB is capped at 1 MB. If truncated, `[output truncated]` is
## Data Model
`ansible_runs` table (defined in `roundtable/models/ansible.py`):
`ansible_runs` table (defined in `steward/models/ansible.py`):
| Column | Type | Description |
|---|---|---|
@@ -137,7 +137,7 @@ Runs older than `data.retention_days` (default 90) are pruned by the `data_clean
| File | Purpose |
|---|---|
| `roundtable/ansible/sources.py` | Source discovery, git pull logic |
| `roundtable/ansible/executor.py` | Subprocess execution and output streaming |
| `roundtable/ansible/routes.py` | HTTP routes (browse, trigger, stream, history) |
| `roundtable/models/ansible.py` | `AnsibleRun` model |
| `steward/ansible/sources.py` | Source discovery, git pull logic |
| `steward/ansible/executor.py` | Subprocess execution and output streaming |
| `steward/ansible/routes.py` | HTTP routes (browse, trigger, stream, history) |
| `steward/models/ansible.py` | `AnsibleRun` model |
+10 -12
View File
@@ -1,6 +1,6 @@
# Configuration
Roundtable uses a two-layer configuration system. Only the bare minimum needed to boot lives in files or environment variables. Everything else is stored in the database and managed through the Settings UI.
Steward uses a two-layer configuration system. Only the bare minimum needed to boot lives in files or environment variables. Everything else is stored in the database and managed through the Settings UI.
---
@@ -10,31 +10,29 @@ These three values are read at startup from `config.yaml` and/or environment var
| Key | Env var | Default | Description |
|---|---|---|---|
| `database.url` | `ROUNDTABLE_DATABASE_URL` | — | PostgreSQL async URL. **Required.** |
| `secret_key` | `ROUNDTABLE_SECRET_KEY` | auto-generated | Flask/Quart session signing key. Auto-generated and saved to `/data/secret.key` if not set. |
| `plugin_dir` | `ROUNDTABLE_PLUGIN_DIR` | `plugins` | Path to the plugins directory. |
| `database.url` | `STEWARD_DATABASE_URL` | — | PostgreSQL async URL. **Required.** |
| `secret_key` | `STEWARD_SECRET_KEY` | auto-generated | Flask/Quart session signing key. Auto-generated and saved to `/data/secret.key` if not set. |
| `plugin_dir` | `STEWARD_PLUGIN_DIR` | `plugins` | Path to the plugins directory. |
**Resolution order for `database_url`:** env var `ROUNDTABLE_DATABASE_URL` → env var `ROUNDTABLE_DATABASE__URL` (legacy double-underscore) → `database.url` in `config.yaml`.
**Resolution order for `database_url`:** env var `STEWARD_DATABASE_URL` → env var `STEWARD_DATABASE__URL` (legacy double-underscore) → `database.url` in `config.yaml`.
**Resolution order for `secret_key`:** env var `ROUNDTABLE_SECRET_KEY``secret_key` in `config.yaml``/data/secret.key` file → auto-generate and write to `/data/secret.key`.
**Resolution order for `secret_key`:** env var `STEWARD_SECRET_KEY``secret_key` in `config.yaml``/data/secret.key` file → auto-generate and write to `/data/secret.key`.
### Minimal config.yaml
```yaml
database:
url: "postgresql+asyncpg://user:password@localhost/roundtable"
url: "postgresql+asyncpg://user:password@localhost/steward"
```
### Minimal env-only setup (Docker)
```bash
ROUNDTABLE_DATABASE_URL=postgresql+asyncpg://user:password@db/roundtable
STEWARD_DATABASE_URL=postgresql+asyncpg://user:password@db/steward
```
A `.env` file is loaded automatically if present.
> **Legacy env vars:** `FABLEDSCRYER_*` env vars are still accepted as a fallback for existing deployments. They will be removed in a future release — migrate to `ROUNDTABLE_*` when convenient.
---
## App Settings (Database-backed)
@@ -68,7 +66,7 @@ Default webhook template:
### Reading and Writing Settings in Code
```python
from roundtable.core.settings import get_setting, set_setting
from steward.core.settings import get_setting, set_setting
# Read
async with current_app.db_sessionmaker() as session:
@@ -84,7 +82,7 @@ async with current_app.db_sessionmaker() as session:
### The DEFAULTS Dict
`roundtable/core/settings.py` contains the `DEFAULTS` dict — the canonical list of all recognised settings and their default values. Add new settings here to make them recognised by `get_all_settings()` and the settings UI.
`steward/core/settings.py` contains the `DEFAULTS` dict — the canonical list of all recognised settings and their default values. Add new settings here to make them recognised by `get_all_settings()` and the settings UI.
---
+7 -7
View File
@@ -1,13 +1,13 @@
# Core Monitors
Roundtable ships two built-in monitors: Ping and DNS. Both run as asyncio scheduled tasks on the same event loop as the web server, with no separate processes.
Steward ships two built-in monitors: Ping and DNS. Both run as asyncio scheduled tasks on the same event loop as the web server, with no separate processes.
---
## Ping Monitor
**Source:** `roundtable/monitors/ping.py`
**Scheduler task:** `ping_monitor` in `roundtable/app.py`
**Source:** `steward/monitors/ping.py`
**Scheduler task:** `ping_monitor` in `steward/app.py`
**Interval:** `monitors.poll_interval_seconds` (default 60s), runs on startup
### How It Works
@@ -29,7 +29,7 @@ Each probe writes a `PingResult` row and calls `record_metric()`:
### Data Model
`ping_results` table (defined in `roundtable/models/monitors.py`):
`ping_results` table (defined in `steward/models/monitors.py`):
| Column | Type | Description |
|---|---|---|
@@ -51,8 +51,8 @@ Old rows are pruned by the `data_cleanup` task (default: 90 days).
## DNS Monitor
**Source:** `roundtable/monitors/dns.py`
**Scheduler task:** `dns_monitor` in `roundtable/app.py`
**Source:** `steward/monitors/dns.py`
**Scheduler task:** `dns_monitor` in `steward/app.py`
**Interval:** `monitors.poll_interval_seconds` (default 60s), runs on startup
### How It Works
@@ -70,7 +70,7 @@ Each check writes a `DnsResult` row and calls `record_metric()`:
### Data Model
`dns_results` table (defined in `roundtable/models/monitors.py`):
`dns_results` table (defined in `steward/models/monitors.py`):
| Column | Type | Description |
|---|---|---|
+36 -36
View File
@@ -2,13 +2,13 @@
**Date:** 2026-04-14
**Status:** Approved, ready for implementation planning
**Scope:** Roundtable plugin for remote host resource monitoring (CPU, memory, storage, load, uptime) via a lightweight Python push agent.
**Scope:** Steward plugin for remote host resource monitoring (CPU, memory, storage, load, uptime) via a lightweight Python push agent.
---
## Goal
Give Roundtable a fleet-glance view of remote Linux hosts — current resource usage, history, and alert integration — without requiring agentless SSH/Ansible round-trips, a new toolchain, or SNMP gymnastics on every target.
Give Steward a fleet-glance view of remote Linux hosts — current resource usage, history, and alert integration — without requiring agentless SSH/Ansible round-trips, a new toolchain, or SNMP gymnastics on every target.
## Non-goals
@@ -24,7 +24,7 @@ Give Roundtable a fleet-glance view of remote Linux hosts — current resource u
```
┌──────────────┐ POST /plugins/host_agent/ingest ┌────────────────────────┐
│ Agent │ ──────────────────────────────────────────→ │ Roundtable
│ Agent │ ──────────────────────────────────────────→ │ Steward
│ (Python) │ Authorization: Bearer <per-host-token> │ host_agent plugin │
│ on target │ JSON body: metrics snapshot │ │
│ host │ │ routes.py (ingest + │
@@ -48,8 +48,8 @@ Give Roundtable a fleet-glance view of remote Linux hosts — current resource u
- **Agent** is a single Python file. No external dependencies beyond Python 3.8+ stdlib.
- **Plugin** is self-contained under `plugins/host_agent/`. Writes to the core `PluginMetric` time-series bus (the designed cross-plugin integration point) and its own private `host_agent_registrations` table. Writes to the core `Host` model for identity, but adds no new columns to it.
- **Auth** is per-host bearer tokens, minted on "Add host" in the plugin's settings page.
- **Install** is a one-line `curl | sh` command rendered per-host by Roundtable with the token already baked in.
- **Failure** is handled at the agent (in-memory ring buffer + exponential backoff) so Roundtable outages don't lose brief-window data.
- **Install** is a one-line `curl | sh` command rendered per-host by Steward with the token already baked in.
- **Failure** is handled at the agent (in-memory ring buffer + exponential backoff) so Steward outages don't lose brief-window data.
---
@@ -58,10 +58,10 @@ Give Roundtable a fleet-glance view of remote Linux hosts — current resource u
### File layout on the target host
```
/usr/local/lib/roundtable-agent/agent.py # the script, target ~300 lines
/etc/roundtable-agent.conf # key=value config, 0640 root:roundtable-agent
/etc/systemd/system/roundtable-agent.service # unit file
# dedicated system user: roundtable-agent
/usr/local/lib/steward-agent/agent.py # the script, target ~300 lines
/etc/steward-agent.conf # key=value config, 0640 root:steward-agent
/etc/systemd/system/steward-agent.service # unit file
# dedicated system user: steward-agent
```
### Config file format
@@ -69,7 +69,7 @@ Give Roundtable a fleet-glance view of remote Linux hosts — current resource u
Flat `key = value`, parsed by a ~20-line homegrown parser (no TOML/YAML dependency):
```
url = https://roundtable.home.lan
url = https://steward.home.lan
token = a1b2c3d4...
interval_seconds = 30
hostname = myhost # optional; defaults to uname -n
@@ -111,7 +111,7 @@ To stderr only (systemd captures to journal). No file logging, no log rotation.
### Identity
The agent's self-reported hostname in the payload is advisory. The real identity is the bearer token — Roundtable looks up the `Host` row via `HostAgentRegistration.token_hash`, not via hostname. Changing a host's hostname does not break its identity, and two hosts accidentally sharing a hostname can't collide.
The agent's self-reported hostname in the payload is advisory. The real identity is the bearer token — Steward looks up the `Host` row via `HostAgentRegistration.token_hash`, not via hostname. Changing a host's hostname does not break its identity, and two hosts accidentally sharing a hostname can't collide.
### Failure behavior
@@ -194,7 +194,7 @@ One `ScheduledTask` running every 60 seconds that flags `HostAgentRegistration`
### `METRIC_CATALOG` registration
Add to `roundtable/alerts/routes.py`:
Add to `steward/alerts/routes.py`:
```python
"host_agent": ["cpu_pct", "mem_used_pct", "mem_available_bytes",
@@ -202,7 +202,7 @@ Add to `roundtable/alerts/routes.py`:
"load_5m", "load_15m", "uptime_secs"],
```
This is the one edit outside the plugin directory, matching the pattern every other plugin already follows. Not ideal from a pure-plugin-independence standpoint but unavoidable until Roundtable core grows a plugin-registered catalog API — deferred as future core work.
This is the one edit outside the plugin directory, matching the pattern every other plugin already follows. Not ideal from a pure-plugin-independence standpoint but unavoidable until Steward core grows a plugin-registered catalog API — deferred as future core work.
---
@@ -212,7 +212,7 @@ This is the one edit outside the plugin directory, matching the pattern every ot
```http
POST /plugins/host_agent/ingest HTTP/1.1
Host: roundtable.home.lan
Host: steward.home.lan
Authorization: Bearer a1b2c3d4e5f6...
Content-Type: application/json
```
@@ -311,7 +311,7 @@ Agent treats anything non-2xx as failure → ring buffer. Agent treats 401 speci
### The one-liner (what the UI shows)
```
curl -sSL 'https://roundtable.home.lan/plugins/host_agent/install.sh?token=a1b2c3...' | sudo sh
curl -sSL 'https://steward.home.lan/plugins/host_agent/install.sh?token=a1b2c3...' | sudo sh
```
The UI also offers a "review script before running" link that opens a modal showing the two-step form:
@@ -326,19 +326,19 @@ sudo sh install.sh
```sh
#!/bin/sh
# Roundtable host agent installer
# Steward host agent installer
# Generated for: {{ host_name }} ({{ host_address }})
# Roundtable URL: {{ url }}
# Steward URL: {{ url }}
set -e
ROUNDTABLE_URL="{{ url }}"
STEWARD_URL="{{ url }}"
AGENT_TOKEN="{{ token }}"
AGENT_VERSION="{{ agent_version }}"
AGENT_USER="roundtable-agent"
AGENT_DIR="/usr/local/lib/roundtable-agent"
CONF_FILE="/etc/roundtable-agent.conf"
UNIT_FILE="/etc/systemd/system/roundtable-agent.service"
AGENT_USER="steward-agent"
AGENT_DIR="/usr/local/lib/steward-agent"
CONF_FILE="/etc/steward-agent.conf"
UNIT_FILE="/etc/systemd/system/steward-agent.service"
# ── preflight ────────────────────────────────────────────────────────────────
[ "$(id -u)" = "0" ] || { echo "must run as root (use sudo)"; exit 1; }
@@ -347,7 +347,7 @@ command -v python3 >/dev/null 2>&1 || { echo "python3 not found — install py
# Handle --uninstall
if [ "${1:-}" = "--uninstall" ]; then
systemctl disable --now roundtable-agent.service 2>/dev/null || true
systemctl disable --now steward-agent.service 2>/dev/null || true
rm -f "$UNIT_FILE" "$CONF_FILE"
rm -rf "$AGENT_DIR"
systemctl daemon-reload
@@ -363,13 +363,13 @@ fi
# ── drop the agent file ──────────────────────────────────────────────────────
mkdir -p "$AGENT_DIR"
curl -sSL "${ROUNDTABLE_URL}/plugins/host_agent/agent.py" -o "$AGENT_DIR/agent.py"
curl -sSL "${STEWARD_URL}/plugins/host_agent/agent.py" -o "$AGENT_DIR/agent.py"
chmod 0755 "$AGENT_DIR/agent.py"
chown root:root "$AGENT_DIR/agent.py"
# ── write config ─────────────────────────────────────────────────────────────
cat > "$CONF_FILE" <<EOF
url = $ROUNDTABLE_URL
url = $STEWARD_URL
token = $AGENT_TOKEN
interval_seconds = 30
EOF
@@ -379,7 +379,7 @@ chmod 0640 "$CONF_FILE"
# ── write systemd unit ───────────────────────────────────────────────────────
cat > "$UNIT_FILE" <<EOF
[Unit]
Description=Roundtable host agent
Description=Steward host agent
After=network-online.target
Wants=network-online.target
@@ -400,17 +400,17 @@ WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now roundtable-agent.service
systemctl enable --now steward-agent.service
echo
echo "Roundtable host agent $AGENT_VERSION installed and running."
echo "Check status: systemctl status roundtable-agent"
echo "Logs: journalctl -u roundtable-agent -f"
echo "Steward host agent $AGENT_VERSION installed and running."
echo "Check status: systemctl status steward-agent"
echo "Logs: journalctl -u steward-agent -f"
```
### Design points
- **Agent binary served by Roundtable itself.** `GET /plugins/host_agent/agent.py` serves the script bundled with the plugin, so version drift between install-script and agent is impossible — re-running the installer picks up whatever agent the currently-running Roundtable ships.
- **Agent binary served by Steward itself.** `GET /plugins/host_agent/agent.py` serves the script bundled with the plugin, so version drift between install-script and agent is impossible — re-running the installer picks up whatever agent the currently-running Steward ships.
- **Systemd hardening is cheap and correct.** `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, `PrivateTmp`, `ReadOnlyPaths=/proc /sys`. The agent only needs read access to `/proc`, `/sys`, and mount points; denying everything else narrows blast radius.
- **Uninstall is a first-class flag**, not a separate script. Same one-liner with `--uninstall` appended.
- **Fail-fast on preflight.** Missing systemd or python3 → clear error, exit. No half-installed agent.
@@ -421,7 +421,7 @@ echo "Logs: journalctl -u roundtable-agent -f"
## UI surfaces
### Dashboard widgets (`roundtable/core/widgets.py`)
### Dashboard widgets (`steward/core/widgets.py`)
- **`host_resources`** — table widget. One row per monitored host: name, CPU %, mem %, disk % (worst mount), load 1m, "last seen Xs ago" with red/yellow/green coloring. Fleet glance.
- **`host_resource_history`** — chart widget for one host. CPU / mem / disk over a selectable time range (1h, 6h, 24h, 7d). Same pattern as every other history widget — Chart.js.
@@ -447,9 +447,9 @@ List of registered hosts with their enable flags, an "Add host" button that open
| Failure | Who handles it | Response |
|---|---|---|
| Agent can't reach Roundtable | Agent | Push to ring buffer, exponential backoff (30→60→120→300s cap), log WARN. Resets on success. |
| Roundtable rejects with 401 | Agent | Log ERROR with remediation hint, continue retry loop. Admin fixes via UI + conf edit; no restart needed. |
| Roundtable rejects with 400 | Agent | Log ERROR, drop the sample (don't re-buffer), continue. Indicates agent/server version skew. |
| Agent can't reach Steward | Agent | Push to ring buffer, exponential backoff (30→60→120→300s cap), log WARN. Resets on success. |
| Steward rejects with 401 | Agent | Log ERROR with remediation hint, continue retry loop. Admin fixes via UI + conf edit; no restart needed. |
| Steward rejects with 400 | Agent | Log ERROR, drop the sample (don't re-buffer), continue. Indicates agent/server version skew. |
| Ring buffer full | Agent | Drop oldest, keep newest. DEBUG log (expected during outages). |
| Config file missing or malformed | Agent | Log ERROR, exit non-zero. Systemd restarts after 10s. Repeated restarts are visible in journal. |
| `/proc/stat` read fails between samples | Agent | Skip CPU for this cycle, still POST the rest. Partial samples allowed. |
@@ -528,7 +528,7 @@ These are not blockers; they are feature boundaries inherent to the design.
6. Settings page: list, add-host flow, rotate-token, delete.
7. Dashboard widgets: table + history chart. Register in `core/widgets.py`.
8. Per-host detail page.
9. `METRIC_CATALOG` entry in `roundtable/alerts/routes.py`.
9. `METRIC_CATALOG` entry in `steward/alerts/routes.py`.
10. Scheduler: stale-agent marker.
11. Tests at every stage; final integration test to tie it together.
+52 -52
View File
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Ship a `host_agent` Roundtable plugin plus a stdlib-only Python push agent that reports CPU/memory/storage/load/uptime from remote Linux hosts to Roundtable.
**Goal:** Ship a `host_agent` Steward plugin plus a stdlib-only Python push agent that reports CPU/memory/storage/load/uptime from remote Linux hosts to Steward.
**Architecture:** Plugin lives under `plugins/host_agent/` (mirrors `plugins/http/`). It owns a private `host_agent_registrations` table, writes time-series data into the core `PluginMetric` bus, serves its own agent source at `GET /plugins/host_agent/agent.py`, and renders a per-host install script at `GET /plugins/host_agent/install.sh`. The agent is a single ~300-line Python script with a 30s collect→POST loop, in-memory ring buffer, exponential backoff, and systemd unit installed by a curl one-liner.
@@ -14,7 +14,7 @@
## Execution note — path 1 (no DB-backed tests)
Mid-execution discovery: the Roundtable test harness runs `create_app(testing=True)`, which mocks `db_sessionmaker` and skips all migrations. There is no existing DB-backed test fixture in this codebase, and no existing plugin ships with tests. The first execution attempt tried to invent a SQLite-backed override fixture, which cascaded into modifying a production migration (`0007_dashboard_ownership.py`) to remove an inline FK — unacceptable. That commit was reverted.
Mid-execution discovery: the Steward test harness runs `create_app(testing=True)`, which mocks `db_sessionmaker` and skips all migrations. There is no existing DB-backed test fixture in this codebase, and no existing plugin ships with tests. The first execution attempt tried to invent a SQLite-backed override fixture, which cascaded into modifying a production migration (`0007_dashboard_ownership.py`) to remove an inline FK — unacceptable. That commit was reverted.
**Revised test strategy:** test the agent exhaustively (everything that lives in `plugins/host_agent/agent.py` — pure Python, stdlib only, trivially unit-testable). Test the server-side metric-expansion function as a pure function that takes a sample dict + host name and returns a list of `(metric_name, resource_name, value)` tuples. Everything else — ingest route, install route, settings routes, widgets, detail page, scheduler — is verified manually against the dev server.
@@ -43,7 +43,7 @@ Where code blocks in tasks below reference DB-backed tests, treat them as guidan
- `plugins/host_agent/scheduler.py` — stale-agent marker task.
- `plugins/host_agent/agent.py` — the Python agent script, served to targets.
- `plugins/host_agent/migrations/__init__.py`
- `plugins/host_agent/migrations/env.py` — alembic env (copy of http plugin's, but imports `roundtable.models.base`).
- `plugins/host_agent/migrations/env.py` — alembic env (copy of http plugin's, but imports `steward.models.base`).
- `plugins/host_agent/migrations/versions/__init__.py`
- `plugins/host_agent/migrations/versions/host_agent_001_initial.py` — creates `host_agent_registrations`.
- `plugins/host_agent/templates/install.sh.j2` — Jinja install script template.
@@ -68,8 +68,8 @@ Where code blocks in tasks below reference DB-backed tests, treat them as guidan
**Modified files (core, small edits):**
- `roundtable/alerts/routes.py` — add `"host_agent"` entry to `METRIC_CATALOG`.
- `roundtable/core/widgets.py` — add `host_resources` and `host_resource_history` entries.
- `steward/alerts/routes.py` — add `"host_agent"` entry to `METRIC_CATALOG`.
- `steward/core/widgets.py` — add `host_resources` and `host_resource_history` entries.
- `docs/plugins/index.yaml.example` — add catalog entry.
---
@@ -102,7 +102,7 @@ pytestmark = pytest.mark.asyncio
async def test_host_agent_migration_creates_table(app):
# The app fixture runs all plugin migrations on startup.
from roundtable.core.db import get_engine
from steward.core.db import get_engine
engine = get_engine()
async with engine.connect() as conn:
def _check(sync_conn):
@@ -127,11 +127,11 @@ Expected: FAIL (plugin not loaded, table missing).
name: host_agent
version: "1.0.0"
description: "Remote Linux host resource monitoring via a lightweight Python push agent (CPU, memory, storage, load, uptime)"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/host_agent"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/host_agent"
tags:
- host
- monitoring
@@ -180,7 +180,7 @@ from __future__ import annotations
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, String, DateTime, ForeignKey
from roundtable.models.base import Base
from steward.models.base import Base
def _uuid() -> str:
@@ -269,8 +269,8 @@ from alembic import context
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from roundtable.models.base import Base
import roundtable.models # noqa: F401
from steward.models.base import Base
import steward.models # noqa: F401
from plugins.host_agent.models import HostAgentRegistration # noqa: F401
config = context.config
@@ -282,14 +282,14 @@ target_metadata = Base.metadata
def _get_url() -> str:
import yaml
cfg_path = os.environ.get("ROUNDTABLE_CONFIG", "config.yaml")
cfg_path = os.environ.get("STEWARD_CONFIG", "config.yaml")
try:
with open(cfg_path) as f:
cfg = yaml.safe_load(f) or {}
url = cfg.get("database", {}).get("url", "")
except FileNotFoundError:
url = ""
return os.environ.get("ROUNDTABLE_DATABASE__URL", url)
return os.environ.get("STEWARD_DATABASE__URL", url)
def run_migrations_offline() -> None:
@@ -413,12 +413,12 @@ from plugins.host_agent.agent import read_config, ConfigError
def test_parses_flat_key_value(tmp_path):
p = tmp_path / "agent.conf"
p.write_text(
"url = https://roundtable.example\n"
"url = https://steward.example\n"
"token = abc123\n"
"interval_seconds = 45\n"
)
cfg = read_config(str(p))
assert cfg["url"] == "https://roundtable.example"
assert cfg["url"] == "https://steward.example"
assert cfg["token"] == "abc123"
assert cfg["interval_seconds"] == 45
@@ -466,7 +466,7 @@ Expected: FAIL (`read_config` does not exist).
```python
# plugins/host_agent/agent.py
"""Roundtable host agent — pushes resource metrics to a Roundtable instance.
"""Steward host agent — pushes resource metrics to a Steward instance.
Python 3.8+ stdlib only. Target ~300 lines. Served to targets at
GET /plugins/host_agent/agent.py.
@@ -1060,7 +1060,7 @@ def post_payload(url: str, token: str, payload: dict) -> tuple[bool, int | None]
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
"User-Agent": f"roundtable-host-agent/{AGENT_VERSION}",
"User-Agent": f"steward-host-agent/{AGENT_VERSION}",
},
method="POST",
)
@@ -1112,7 +1112,7 @@ def main_loop(conf_path: str) -> int:
buffer = RingBuffer(maxlen=20)
backoff = 0
_log("INFO", f"roundtable-host-agent {AGENT_VERSION} starting "
_log("INFO", f"steward-host-agent {AGENT_VERSION} starting "
f"(url={cfg['url']}, interval={cfg['interval_seconds']}s)")
while not _shutdown_requested:
@@ -1171,7 +1171,7 @@ def main_loop(conf_path: str) -> int:
if __name__ == "__main__":
conf = os.environ.get("ROUNDTABLE_AGENT_CONFIG", "/etc/roundtable-agent.conf")
conf = os.environ.get("STEWARD_AGENT_CONFIG", "/etc/steward-agent.conf")
sys.exit(main_loop(conf))
```
@@ -1209,8 +1209,8 @@ git commit -m "feat(host_agent): agent POST, backoff, and main loop"
import hashlib
import pytest_asyncio
from roundtable.models.hosts import Host
from roundtable.core.db import get_session
from steward.models.hosts import Host
from steward.core.db import get_session
from plugins.host_agent.models import HostAgentRegistration
@@ -1233,7 +1233,7 @@ async def registered_host(app):
return {"host": host, "registration": reg, "token": raw_token}
```
> **Note:** Verify `Host.__init__` accepts `name=` and `address=` kwargs before relying on this. If the real Host model requires more fields (e.g., `probe_type`), pass them here. Read `roundtable/models/hosts.py` first to confirm.
> **Note:** Verify `Host.__init__` accepts `name=` and `address=` kwargs before relying on this. If the real Host model requires more fields (e.g., `probe_type`), pass them here. Read `steward/models/hosts.py` first to confirm.
- [ ] **Step 2: Write failing ingest test**
@@ -1244,8 +1244,8 @@ from datetime import datetime, timezone
import pytest
from sqlalchemy import select
from roundtable.models.metrics import PluginMetric
from roundtable.core.db import get_session
from steward.models.metrics import PluginMetric
from steward.core.db import get_session
from plugins.host_agent.models import HostAgentRegistration
pytestmark = pytest.mark.asyncio
@@ -1352,9 +1352,9 @@ from typing import Any
from quart import Blueprint, jsonify, request
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.hosts import Host
from roundtable.models.metrics import PluginMetric
from steward.core.db import get_session
from steward.models.hosts import Host
from steward.models.metrics import PluginMetric
from .models import HostAgentRegistration
host_agent_bp = Blueprint("host_agent", __name__, template_folder="templates")
@@ -1628,7 +1628,7 @@ async def test_install_sh_renders_with_token(client, registered_host):
assert resp.status_code == 200
assert resp.content_type.startswith("text/plain")
text = (await resp.get_data()).decode()
assert "roundtable-agent" in text
assert "steward-agent" in text
assert registered_host["token"] in text
assert "systemctl enable --now" in text
assert "NoNewPrivileges=yes" in text
@@ -1758,11 +1758,11 @@ git commit -m "feat(host_agent): install.sh and agent.py serving routes"
- Create: `plugins/host_agent/templates/settings_list.html`
- Create: `tests/plugins/host_agent/test_settings_routes.py`
Auth note: the existing Roundtable admin decorator pattern needs to be used here. Read `roundtable/settings/routes.py` (or similar) to find the decorator name — likely something like `@require_admin` or a session check. Use whatever the rest of the codebase uses. **Do not invent a new auth mechanism.**
Auth note: the existing Steward admin decorator pattern needs to be used here. Read `steward/settings/routes.py` (or similar) to find the decorator name — likely something like `@require_admin` or a session check. Use whatever the rest of the codebase uses. **Do not invent a new auth mechanism.**
- [ ] **Step 1: Find the admin auth decorator**
Run: `grep -rn "require_admin\|@admin_required\|def admin" roundtable/settings/ roundtable/core/auth.py 2>/dev/null | head -20`
Run: `grep -rn "require_admin\|@admin_required\|def admin" steward/settings/ steward/core/auth.py 2>/dev/null | head -20`
Note the decorator name and import path for use in Step 3.
- [ ] **Step 2: Write failing test**
@@ -1772,8 +1772,8 @@ Note the decorator name and import path for use in Step 3.
import pytest
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.hosts import Host
from steward.core.db import get_session
from steward.models.hosts import Host
from plugins.host_agent.models import HostAgentRegistration
pytestmark = pytest.mark.asyncio
@@ -1839,7 +1839,7 @@ from quart import redirect, url_for
# TODO: replace _require_admin with the project-wide decorator found in Step 1.
# Placeholder below mirrors the shape; swap for real admin auth.
from roundtable.core.auth import require_admin # adjust import to actual path
from steward.core.auth import require_admin # adjust import to actual path
def _new_token_pair() -> tuple[str, str]:
@@ -1938,7 +1938,7 @@ async def settings_list():
```html
{% extends "base.html" %}
{% block title %}Host Agent — Roundtable{% endblock %}
{% block title %}Host Agent — Steward{% endblock %}
{% block content %}
<h1 class="page-title">Host Agent — Registered Hosts</h1>
@@ -2003,7 +2003,7 @@ async def settings_list():
- [ ] **Step 6: Run tests — iterate on auth decorator if needed**
Run: `pytest tests/plugins/host_agent/test_settings_routes.py -v`
Expected: PASS. If admin auth fails, the actual import path from Step 1 is wrong — fix the `from roundtable.core.auth import require_admin` line.
Expected: PASS. If admin auth fails, the actual import path from Step 1 is wrong — fix the `from steward.core.auth import require_admin` line.
- [ ] **Step 7: Commit**
@@ -2020,8 +2020,8 @@ git commit -m "feat(host_agent): plugin settings page — add, rotate, delete"
- Modify: `plugins/host_agent/routes.py` — add `/widget` and `/widget/history` partials.
- Create: `plugins/host_agent/templates/widget_table.html`
- Create: `plugins/host_agent/templates/widget_history.html`
- Modify: `roundtable/core/widgets.py`
- Modify: `roundtable/alerts/routes.py`
- Modify: `steward/core/widgets.py`
- Modify: `steward/alerts/routes.py`
- [ ] **Step 1: Add widget partial routes to `plugins/host_agent/routes.py`**
@@ -2165,7 +2165,7 @@ async def widget_history():
</div>
```
- [ ] **Step 4: Register widgets in `roundtable/core/widgets.py`**
- [ ] **Step 4: Register widgets in `steward/core/widgets.py`**
Add to `WIDGET_REGISTRY` (alphabetically by existing convention, or at the end — check the file first):
@@ -2206,7 +2206,7 @@ Add to `WIDGET_REGISTRY` (alphabetically by existing convention, or at the end
- [ ] **Step 5: Register `host_agent` in `METRIC_CATALOG`**
Read `roundtable/alerts/routes.py`. Locate the `METRIC_CATALOG` dict. Add:
Read `steward/alerts/routes.py`. Locate the `METRIC_CATALOG` dict. Add:
```python
"host_agent": [
@@ -2244,7 +2244,7 @@ Expected: PASS.
- [ ] **Step 7: Commit**
```bash
git add plugins/host_agent/routes.py plugins/host_agent/templates/widget_table.html plugins/host_agent/templates/widget_history.html roundtable/core/widgets.py roundtable/alerts/routes.py tests/plugins/host_agent/test_ingest_route.py
git add plugins/host_agent/routes.py plugins/host_agent/templates/widget_table.html plugins/host_agent/templates/widget_history.html steward/core/widgets.py steward/alerts/routes.py tests/plugins/host_agent/test_ingest_route.py
git commit -m "feat(host_agent): dashboard widgets and alert metric catalog entry"
```
@@ -2409,8 +2409,8 @@ from datetime import datetime, timedelta, timezone
import pytest
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.hosts import Host
from steward.core.db import get_session
from steward.models.hosts import Host
from plugins.host_agent.models import HostAgentRegistration
from plugins.host_agent.scheduler import find_stale_registrations
@@ -2459,8 +2459,8 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.hosts import Host
from steward.core.db import get_session
from steward.models.hosts import Host
from .models import HostAgentRegistration
@@ -2531,8 +2531,8 @@ from unittest.mock import patch
import pytest
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.metrics import PluginMetric
from steward.core.db import get_session
from steward.models.metrics import PluginMetric
from plugins.host_agent import agent as a
pytestmark = pytest.mark.asyncio
@@ -2609,12 +2609,12 @@ Append to the `plugins:` list in `docs/plugins/index.yaml.example`:
- name: host_agent
version: "1.0.0"
description: "Remote Linux host resource monitoring via a lightweight Python push agent"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/host_agent"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/host_agent"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
checksum_sha256: ""
tags:
- host
@@ -2643,7 +2643,7 @@ Use `fable_update_task` to set status=done on task 252 ("Implement host_agent pl
- [ ] **Step 2: Add a Fable note summarizing what shipped**
One-paragraph `fable_create_note` attached to Roundtable project (id 6) with:
One-paragraph `fable_create_note` attached to Steward project (id 6) with:
- Link to spec: `docs/plugins/host-agent-design.md`
- Link to plan: `docs/plugins/host-agent-plan.md`
- Note any deviations from the spec discovered during implementation (e.g., auth decorator name, fixture adjustments).
+25 -25
View File
@@ -1,10 +1,10 @@
# roundtable-plugins / index.yaml
# steward-plugins / index.yaml
#
# This file is the catalog index for the roundtable plugin repository.
# This file is the catalog index for the steward plugin repository.
# It is fetched by the app's Settings → Plugins → Plugin Catalog UI.
#
# Roundtable reads this file from:
# https://git.fabledsword.com/bvandeusen/Roundtable-plugins/raw/branch/main/index.yaml
# Steward reads this file from:
# https://git.fabledsword.com/bvandeusen/Steward-plugins/raw/branch/main/index.yaml
#
# After adding or updating a plugin entry, commit and push — the change is
# live immediately for anyone whose app fetches the catalog (cache TTL: 5 min).
@@ -25,7 +25,7 @@
#
# Download URL conventions:
# Gitea release assets (recommended):
# https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/traefik-v1.0.0/traefik.zip
# https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/traefik-v1.0.0/traefik.zip
# Upload the zip as a release attachment in Gitea; paste the URL here.
# Source archive tarballs work too but release assets are preferred (smaller, plugin-only).
@@ -37,12 +37,12 @@ plugins:
- name: http
version: "1.0.0"
description: "Synthetic HTTP endpoint monitoring — status code, response time, content match, TLS expiry"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/http"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/http-v1.0.0/http.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/http"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/http-v1.0.0/http.zip"
checksum_sha256: ""
tags:
- monitoring
@@ -53,12 +53,12 @@ plugins:
- name: docker
version: "1.0.0"
description: "Docker container status, resource usage, and restart tracking via Docker socket"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/docker"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/docker-v1.0.0/docker.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/docker"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/docker-v1.0.0/docker.zip"
checksum_sha256: ""
tags:
- containers
@@ -68,12 +68,12 @@ plugins:
- name: traefik
version: "1.0.0"
description: "Traefik reverse proxy metrics and access log integration"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/traefik"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/traefik-v1.0.0/traefik.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/traefik"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/traefik-v1.0.0/traefik.zip"
checksum_sha256: "" # fill in after running: sha256sum traefik.zip
tags:
- proxy
@@ -83,12 +83,12 @@ plugins:
- name: unifi
version: "1.0.0"
description: "UniFi Network controller integration — WAN health, devices, clients, DPI"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/unifi"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/unifi-v1.0.0/unifi.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/unifi"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/unifi-v1.0.0/unifi.zip"
checksum_sha256: ""
tags:
- network
@@ -98,12 +98,12 @@ plugins:
- name: host_agent
version: "1.0.0"
description: "Remote Linux host resource monitoring via a lightweight Python push agent"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/host_agent"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/host_agent"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
checksum_sha256: ""
tags:
- host
+5 -5
View File
@@ -1,12 +1,12 @@
# Plugin System Overview
Plugins extend Roundtable with new data sources, UI pages, scheduled tasks, and dashboard widgets. Only enabled plugins are imported — disabled or unlisted plugins have zero runtime overhead.
Plugins extend Steward with new data sources, UI pages, scheduled tasks, and dashboard widgets. Only enabled plugins are imported — disabled or unlisted plugins have zero runtime overhead.
---
## How Plugins Are Loaded
Plugin loading happens in step 9 of `create_app()`, after all core blueprints and tasks are registered. The entrypoint is `load_plugins(app)` in `roundtable/core/plugin_manager.py`.
Plugin loading happens in step 9 of `create_app()`, after all core blueprints and tasks are registered. The entrypoint is `load_plugins(app)` in `steward/core/plugin_manager.py`.
For each plugin listed as `enabled: true` in the `PLUGINS` config:
@@ -57,7 +57,7 @@ description: "Does a thing"
# Optional
author: "Your Name"
min_app_version: "0.1.0" # Minimum Roundtable version required
min_app_version: "0.1.0" # Minimum Steward version required
# Default config — merged with user overrides at runtime
# Access at runtime via: app.config["PLUGINS"]["myplugin"]["my_setting"]
@@ -90,7 +90,7 @@ def setup(app):
Returns a list of `ScheduledTask` objects. Return `[]` if the plugin has no background tasks. Called after `setup()`, so any app references set in `setup()` are available.
```python
from roundtable.core.scheduler import ScheduledTask
from steward.core.scheduler import ScheduledTask
def get_scheduled_tasks():
app = _app
@@ -154,7 +154,7 @@ A plugin can contribute a dashboard widget by:
1. Adding a `GET /widget` route to its blueprint that returns an HTMX HTML fragment
2. The dashboard template polling that endpoint with HTMX
The dashboard (`roundtable/templates/dashboard/index.html`) currently includes the Traefik widget conditionally based on `traefik_enabled`. To add a new plugin widget, the dashboard route and template both need to be updated to detect and render the new widget. See the Traefik widget implementation as a reference.
The dashboard (`steward/templates/dashboard/index.html`) currently includes the Traefik widget conditionally based on `traefik_enabled`. To add a new plugin widget, the dashboard route and template both need to be updated to detect and render the new widget. See the Traefik widget implementation as a reference.
---
+16 -16
View File
@@ -46,7 +46,7 @@ config:
## Step 3: Define Models (if needed)
If your plugin stores data, define SQLAlchemy models using the shared `Base` from `roundtable.models.base`.
If your plugin stores data, define SQLAlchemy models using the shared `Base` from `steward.models.base`.
```python
# plugins/myplugin/models.py
@@ -55,7 +55,7 @@ import uuid
from datetime import datetime
from sqlalchemy import String, Float, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from roundtable.models.base import Base
from steward.models.base import Base
class MyPluginMetric(Base):
@@ -79,7 +79,7 @@ Generate the initial migration:
# From the project root
alembic --config alembic.ini revision \
--autogenerate \
--head=roundtable@head \
--head=steward@head \
--branch-label=myplugin \
-m "myplugin initial"
```
@@ -106,8 +106,8 @@ Keep task logic in a separate file so `__init__.py` stays clean.
# plugins/myplugin/scheduler.py
from __future__ import annotations
import logging
from roundtable.core.scheduler import ScheduledTask
from roundtable.core.alerts import record_metric
from steward.core.scheduler import ScheduledTask
from steward.core.alerts import record_metric
logger = logging.getLogger(__name__)
@@ -174,8 +174,8 @@ async def _fetch_value(url: str) -> float:
```python
# plugins/myplugin/routes.py
from quart import Blueprint, current_app, render_template
from roundtable.auth.middleware import require_role
from roundtable.models.users import UserRole
from steward.auth.middleware import require_role
from steward.models.users import UserRole
from .models import MyPluginMetric
myplugin_bp = Blueprint("myplugin", __name__, template_folder="templates")
@@ -215,7 +215,7 @@ Templates live in `plugins/myplugin/templates/myplugin/` (the extra nesting avoi
```html
{# plugins/myplugin/templates/myplugin/index.html #}
{% extends "base.html" %}
{% block title %}My Plugin — Roundtable{% endblock %}
{% block title %}My Plugin — Steward{% endblock %}
{% block content %}
<div class="page-title">My Plugin</div>
{% for row in rows %}
@@ -283,7 +283,7 @@ On next startup, the plugin will be loaded, its migrations applied, and its blue
`record_metric()` is how plugins feed data into the alert pipeline. Any metric you write here can be the target of an alert rule created in the UI.
```python
from roundtable.core.alerts import record_metric
from steward.core.alerts import record_metric
# Must be inside an active transaction
async with session.begin():
@@ -302,11 +302,11 @@ async with session.begin():
## Auth in Routes
Use the `@require_role` decorator from `roundtable.auth.middleware`:
Use the `@require_role` decorator from `steward.auth.middleware`:
```python
from roundtable.auth.middleware import require_role
from roundtable.models.users import UserRole
from steward.auth.middleware import require_role
from steward.models.users import UserRole
@myplugin_bp.get("/admin-only")
@require_role(UserRole.admin)
@@ -325,13 +325,13 @@ Role hierarchy: `admin > operator > viewer`. Requiring `viewer` grants access to
## Publishing to the Catalog
The official plugin catalog is hosted at `https://git.fabledsword.com/bvandeusen/Roundtable-plugins`. Anyone can submit a plugin by opening a pull request — first-party and third-party plugins are treated identically by the catalog system.
The official plugin catalog is hosted at `https://git.fabledsword.com/bvandeusen/Steward-plugins`. Anyone can submit a plugin by opening a pull request — first-party and third-party plugins are treated identically by the catalog system.
### Repo layout
```
roundtable-plugins/
├── index.yaml ← catalog index — the only file Roundtable fetches
steward-plugins/
├── index.yaml ← catalog index — the only file Steward fetches
├── myplugin/
│ ├── plugin.yaml
│ ├── __init__.py
@@ -381,7 +381,7 @@ myplugin.zip
Generate the zip and its checksum:
```bash
cd roundtable-plugins
cd steward-plugins
zip -r myplugin.zip myplugin/
sha256sum myplugin.zip # paste this into index.yaml checksum_sha256
```
+59 -59
View File
@@ -8,15 +8,15 @@ Quick reference for where key functions, models, and entry points live in the co
| What | File | Function / Class |
|---|---|---|
| App factory | `roundtable/app.py` | `create_app()` |
| CLI entry point | `roundtable/cli.py` | `main()` |
| Bootstrap config loading | `roundtable/config.py` | `load_bootstrap()` |
| Secret key resolution | `roundtable/config.py` | `_resolve_secret_key()` |
| DB engine init | `roundtable/database.py` | `init_db()` |
| Core migrations | `roundtable/core/migration_runner.py` | `run_core_migrations()` |
| Plugin migrations | `roundtable/core/migration_runner.py` | `run_plugin_migrations()` |
| Core task registration | `roundtable/app.py` | `_register_core_tasks()` |
| Scheduler loop | `roundtable/core/scheduler.py` | `start_scheduler()` |
| App factory | `steward/app.py` | `create_app()` |
| CLI entry point | `steward/cli.py` | `main()` |
| Bootstrap config loading | `steward/config.py` | `load_bootstrap()` |
| Secret key resolution | `steward/config.py` | `_resolve_secret_key()` |
| DB engine init | `steward/database.py` | `init_db()` |
| Core migrations | `steward/core/migration_runner.py` | `run_core_migrations()` |
| Plugin migrations | `steward/core/migration_runner.py` | `run_plugin_migrations()` |
| Core task registration | `steward/app.py` | `_register_core_tasks()` |
| Scheduler loop | `steward/core/scheduler.py` | `start_scheduler()` |
---
@@ -24,15 +24,15 @@ Quick reference for where key functions, models, and entry points live in the co
| What | File | Function |
|---|---|---|
| All defaults | `roundtable/core/settings.py` | `DEFAULTS` dict |
| Read a setting | `roundtable/core/settings.py` | `get_setting(session, key)` |
| Write a setting | `roundtable/core/settings.py` | `set_setting(session, key, value)` |
| Read all settings | `roundtable/core/settings.py` | `get_all_settings(session)` |
| Sync load at startup | `roundtable/core/settings.py` | `load_settings_sync(db_url)` |
| Extract SMTP dict | `roundtable/core/settings.py` | `to_smtp_cfg(settings)` |
| Extract webhook dict | `roundtable/core/settings.py` | `to_webhook_cfg(settings)` |
| Extract Ansible dict | `roundtable/core/settings.py` | `to_ansible_cfg(settings)` |
| Extract plugins dict | `roundtable/core/settings.py` | `to_plugins_cfg(settings)` |
| All defaults | `steward/core/settings.py` | `DEFAULTS` dict |
| Read a setting | `steward/core/settings.py` | `get_setting(session, key)` |
| Write a setting | `steward/core/settings.py` | `set_setting(session, key, value)` |
| Read all settings | `steward/core/settings.py` | `get_all_settings(session)` |
| Sync load at startup | `steward/core/settings.py` | `load_settings_sync(db_url)` |
| Extract SMTP dict | `steward/core/settings.py` | `to_smtp_cfg(settings)` |
| Extract webhook dict | `steward/core/settings.py` | `to_webhook_cfg(settings)` |
| Extract Ansible dict | `steward/core/settings.py` | `to_ansible_cfg(settings)` |
| Extract plugins dict | `steward/core/settings.py` | `to_plugins_cfg(settings)` |
---
@@ -40,9 +40,9 @@ Quick reference for where key functions, models, and entry points live in the co
| What | File | Function |
|---|---|---|
| Plugin loading | `roundtable/core/plugin_manager.py` | `load_plugins(app)` |
| ScheduledTask dataclass | `roundtable/core/scheduler.py` | `ScheduledTask` |
| Task runner | `roundtable/core/scheduler.py` | `start_scheduler(tasks)` |
| Plugin loading | `steward/core/plugin_manager.py` | `load_plugins(app)` |
| ScheduledTask dataclass | `steward/core/scheduler.py` | `ScheduledTask` |
| Task runner | `steward/core/scheduler.py` | `start_scheduler(tasks)` |
---
@@ -50,11 +50,11 @@ Quick reference for where key functions, models, and entry points live in the co
| What | File | Function |
|---|---|---|
| Write metric + evaluate alerts | `roundtable/core/alerts.py` | `record_metric(session, source_module, resource_name, metric_name, value)` |
| Init alert pipeline | `roundtable/core/alerts.py` | `init_alerts(app)` |
| Rule evaluation | `roundtable/core/alerts.py` | `_evaluate_rule()` (internal) |
| Notification dispatch | `roundtable/core/alerts.py` | `_dispatch_notification()` (internal) |
| Email + webhook send | `roundtable/core/notifications.py` | `dispatch_notifications()` |
| Write metric + evaluate alerts | `steward/core/alerts.py` | `record_metric(session, source_module, resource_name, metric_name, value)` |
| Init alert pipeline | `steward/core/alerts.py` | `init_alerts(app)` |
| Rule evaluation | `steward/core/alerts.py` | `_evaluate_rule()` (internal) |
| Notification dispatch | `steward/core/alerts.py` | `_dispatch_notification()` (internal) |
| Email + webhook send | `steward/core/notifications.py` | `dispatch_notifications()` |
---
@@ -62,9 +62,9 @@ Quick reference for where key functions, models, and entry points live in the co
| What | File | Function |
|---|---|---|
| Ping a host | `roundtable/monitors/ping.py` | `ping_check(host, session)` |
| DNS check a host | `roundtable/monitors/dns.py` | `dns_check(host, session)` |
| Data cleanup | `roundtable/core/cleanup.py` | `run_cleanup(app)` |
| Ping a host | `steward/monitors/ping.py` | `ping_check(host, session)` |
| DNS check a host | `steward/monitors/dns.py` | `dns_check(host, session)` |
| Data cleanup | `steward/core/cleanup.py` | `run_cleanup(app)` |
---
@@ -72,9 +72,9 @@ Quick reference for where key functions, models, and entry points live in the co
| What | File | Function / Class |
|---|---|---|
| Role-based access decorator | `roundtable/auth/middleware.py` | `@require_role(UserRole.X)` |
| Login / session handling | `roundtable/auth/middleware.py` | `login_user()`, `logout_user()` |
| User count (for first-run) | `roundtable/auth/middleware.py` | `get_user_count(app)` |
| Role-based access decorator | `steward/auth/middleware.py` | `@require_role(UserRole.X)` |
| Login / session handling | `steward/auth/middleware.py` | `login_user()`, `logout_user()` |
| User count (for first-run) | `steward/auth/middleware.py` | `get_user_count(app)` |
---
@@ -82,18 +82,18 @@ Quick reference for where key functions, models, and entry points live in the co
| Model | File | Table |
|---|---|---|
| `Host` | `roundtable/models/hosts.py` | `hosts` |
| `PingResult` | `roundtable/models/monitors.py` | `ping_results` |
| `DnsResult` | `roundtable/models/monitors.py` | `dns_results` |
| `AlertRule` | `roundtable/models/alerts.py` | `alert_rules` |
| `AlertState` | `roundtable/models/alerts.py` | `alert_states` |
| `AlertEvent` | `roundtable/models/alerts.py` | `alert_events` |
| `PluginMetric` | `roundtable/models/metrics.py` | `plugin_metrics` |
| `AnsibleRun` | `roundtable/models/ansible.py` | `ansible_runs` |
| `User` | `roundtable/models/users.py` | `users` |
| `AppSetting` | `roundtable/models/settings.py` | `app_settings` |
| `Host` | `steward/models/hosts.py` | `hosts` |
| `PingResult` | `steward/models/monitors.py` | `ping_results` |
| `DnsResult` | `steward/models/monitors.py` | `dns_results` |
| `AlertRule` | `steward/models/alerts.py` | `alert_rules` |
| `AlertState` | `steward/models/alerts.py` | `alert_states` |
| `AlertEvent` | `steward/models/alerts.py` | `alert_events` |
| `PluginMetric` | `steward/models/metrics.py` | `plugin_metrics` |
| `AnsibleRun` | `steward/models/ansible.py` | `ansible_runs` |
| `User` | `steward/models/users.py` | `users` |
| `AppSetting` | `steward/models/settings.py` | `app_settings` |
| `TraefikMetric` | `plugins/traefik/models.py` | `traefik_metrics` |
| SQLAlchemy `Base` | `roundtable/models/base.py` | (shared declarative base) |
| SQLAlchemy `Base` | `steward/models/base.py` | (shared declarative base) |
---
@@ -101,16 +101,16 @@ Quick reference for where key functions, models, and entry points live in the co
| URL pattern | Blueprint | File |
|---|---|---|
| `/` (dashboard) | `dashboard_bp` | `roundtable/dashboard/routes.py` |
| `/auth/login`, `/auth/logout` | `auth_bp` | `roundtable/auth/routes.py` |
| `/hosts/` | `hosts_bp` | `roundtable/hosts/routes.py` |
| `/ping/`, `/ping/rows`, `/ping/settings` | `ping_bp` | `roundtable/ping/routes.py` |
| `/dns/`, `/dns/rows` | `dns_bp` | `roundtable/dns/routes.py` |
| `/alerts/` | `alerts_bp` | `roundtable/alerts/routes.py` |
| `/ansible/` | `ansible_bp` | `roundtable/ansible/routes.py` |
| `/settings/` | `settings_bp` | `roundtable/settings/routes.py` |
| `/` (dashboard) | `dashboard_bp` | `steward/dashboard/routes.py` |
| `/auth/login`, `/auth/logout` | `auth_bp` | `steward/auth/routes.py` |
| `/hosts/` | `hosts_bp` | `steward/hosts/routes.py` |
| `/ping/`, `/ping/rows`, `/ping/settings` | `ping_bp` | `steward/ping/routes.py` |
| `/dns/`, `/dns/rows` | `dns_bp` | `steward/dns/routes.py` |
| `/alerts/` | `alerts_bp` | `steward/alerts/routes.py` |
| `/ansible/` | `ansible_bp` | `steward/ansible/routes.py` |
| `/settings/` | `settings_bp` | `steward/settings/routes.py` |
| `/plugins/traefik/`, `/plugins/traefik/widget` | `traefik_bp` | `plugins/traefik/routes.py` |
| `/health` | (inline) | `roundtable/app.py` |
| `/health` | (inline) | `steward/app.py` |
---
@@ -118,12 +118,12 @@ Quick reference for where key functions, models, and entry points live in the co
| Template | Purpose |
|---|---|
| `roundtable/templates/base.html` | Layout, navigation, full CSS design system |
| `roundtable/templates/dashboard/index.html` | Dashboard with stat strip and widget grid |
| `roundtable/templates/ping/rows.html` | HTMX fragment: ping pill rows (shared by dashboard and /ping/) |
| `roundtable/templates/ping/index.html` | Full /ping/ page |
| `roundtable/templates/dns/rows.html` | HTMX fragment: DNS status rows |
| `roundtable/templates/dns/index.html` | Full /dns/ page |
| `steward/templates/base.html` | Layout, navigation, full CSS design system |
| `steward/templates/dashboard/index.html` | Dashboard with stat strip and widget grid |
| `steward/templates/ping/rows.html` | HTMX fragment: ping pill rows (shared by dashboard and /ping/) |
| `steward/templates/ping/index.html` | Full /ping/ page |
| `steward/templates/dns/rows.html` | HTMX fragment: DNS status rows |
| `steward/templates/dns/index.html` | Full /dns/ page |
| `plugins/traefik/templates/traefik/widget.html` | HTMX fragment: Traefik dashboard widget |
| `plugins/traefik/templates/traefik/index.html` | Full Traefik detail page |
@@ -133,6 +133,6 @@ Quick reference for where key functions, models, and entry points live in the co
| Location | Covers |
|---|---|
| `roundtable/migrations/versions/` | Core schema (hosts, users, monitors, alerts, app_settings) |
| `steward/migrations/versions/` | Core schema (hosts, users, monitors, alerts, app_settings) |
| `plugins/traefik/migrations/versions/` | `traefik_metrics` table |
| `alembic.ini` | Alembic config; `version_locations` lists all migration directories |
+95 -95
View File
@@ -1,10 +1,10 @@
# Roundtable Rebrand Implementation Plan
# Steward Rebrand Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
>
> **Testing:** Per project feedback, skip all test authoring and test runs during this work. Verify manually by running the app.
**Goal:** Rebrand FabledScryer to Roundtable: full visual reskin plus full rename of package, config, containers, and docs.
**Goal:** Rebrand FabledScryer to Steward: full visual reskin plus full rename of package, config, containers, and docs.
**Architecture:** Staged as six sequential PRs. PR 1 is a pure visual/copy reskin that still ships under the FabledScryer name. PRs 24 perform the mechanical rename in order (package → config → container + user-visible strings) with a fallback shim in PR 3 so self-hosters don't break mid-upgrade. PR 5 updates docs. PR 6 removes the shim after one release cycle.
@@ -25,29 +25,29 @@
- Any template with an empty-state message — themed line
**Renamed (PR 2 — package):**
- `fabledscryer/``roundtable/` (directory + every `from fabledscryer` / `import fabledscryer` reference)
- `fabledscryer/``steward/` (directory + every `from fabledscryer` / `import fabledscryer` reference)
- `pyproject.toml` — name, entry point, hatch packages
- `alembic.ini``script_location`
- `tests/**` — imports (tests not executed, but must not break collection; update imports mechanically)
**Modified (PR 3 — config & env):**
- `fabledscryer/config.py``roundtable/config.py` (already moved in PR 2) — read both `ROUNDTABLE_*` and `FABLEDSCRYER_*` with deprecation warning
- `fabledscryer/config.py``steward/config.py` (already moved in PR 2) — read both `STEWARD_*` and `FABLEDSCRYER_*` with deprecation warning
- `.env.example`, `config.example.yaml` — new names
- First-boot migration of `~/.config/fabledscryer``~/.config/roundtable` (if the app uses it — verify during task)
- First-boot migration of `~/.config/fabledscryer``~/.config/steward` (if the app uses it — verify during task)
**Modified (PR 4 — container + user strings):**
- `Dockerfile` — COPY paths, CMD, image labels
- `docker-compose.yml` — service name, image, container_name, volumes, env
- `entrypoint.sh` — package path for `nut_setup.py` invocation
- `roundtable/templates/base.html` — wordmark text "Fabled Scryer" → "Roundtable", `<title>`
- `roundtable/templates/auth/login.html` — any residual "Fabled Scryer" text
- `steward/templates/base.html` — wordmark text "Fabled Scryer" → "Steward", `<title>`
- `steward/templates/auth/login.html` — any residual "Fabled Scryer" text
- `README.md` — first-page references (full doc sweep is PR 5)
**Modified (PR 5 — docs):**
- `README.md`, `docs/**/*.md`
**Modified (PR 6 — cleanup):**
- `roundtable/config.py` — remove fallback shim
- `steward/config.py` — remove fallback shim
---
@@ -197,7 +197,7 @@ git commit -m "style: candlelit glow background replaces star field"
- [ ] **Step 1: Change the title block**
```html
<title>{% block title %}Roundtable{% endblock %}</title>
<title>{% block title %}Steward{% endblock %}</title>
```
Note: the visible wordmark text still reads "Fabled Scryer" — that flips in PR 4. This only changes the browser tab title, which is acceptable to flip early since it's not visible inside the UI.
@@ -263,7 +263,7 @@ Content for `fabledscryer/templates/errors/404.html`:
```html
{% extends "base.html" %}
{% block title %}Not Found — Roundtable{% endblock %}
{% block title %}Not Found — Steward{% endblock %}
{% block content %}
<div style="text-align:center; padding: 4rem 2rem; font-family: var(--font-serif);">
<h1 style="color: var(--gold); font-size: 2.5rem; margin-bottom: 0.5rem;">404</h1>
@@ -278,7 +278,7 @@ Content for `fabledscryer/templates/errors/500.html`:
```html
{% extends "base.html" %}
{% block title %}Error — Roundtable{% endblock %}
{% block title %}Error — Steward{% endblock %}
{% block content %}
<div style="text-align:center; padding: 4rem 2rem; font-family: var(--font-serif);">
<h1 style="color: var(--red); font-size: 2.5rem; margin-bottom: 0.5rem;">500</h1>
@@ -361,7 +361,7 @@ git add -A
git commit -m "style: visual polish after manual review"
```
**PR 1 done.** Open PR with title `feat: roundtable visual reskin (pewter & gold)`.
**PR 1 done.** Open PR with title `feat: steward visual reskin (pewter & gold)`.
---
@@ -370,39 +370,39 @@ git commit -m "style: visual polish after manual review"
### Task 10: Rename the package directory
**Files:**
- Rename: `fabledscryer/``roundtable/`
- Rename: `fabledscryer/``steward/`
- [ ] **Step 1: Rename the directory**
```bash
git mv fabledscryer roundtable
git mv fabledscryer steward
```
- [ ] **Step 2: Confirm the move**
```bash
ls roundtable/ | head
ls steward/ | head
git status | head
```
Expected: see `__init__.py`, `app.py`, `cli.py`, etc. inside `roundtable/`.
Expected: see `__init__.py`, `app.py`, `cli.py`, etc. inside `steward/`.
- [ ] **Step 3: Commit**
```bash
git commit -m "refactor: rename package directory fabledscryer → roundtable"
git commit -m "refactor: rename package directory fabledscryer → steward"
```
### Task 11: Rewrite all `fabledscryer` imports to `roundtable`
### Task 11: Rewrite all `fabledscryer` imports to `steward`
**Files:**
- Modify: every file under `roundtable/`, `tests/`, `alembic.ini`, `entrypoint.sh`, `Dockerfile` that imports from the old package
- Modify: every file under `steward/`, `tests/`, `alembic.ini`, `entrypoint.sh`, `Dockerfile` that imports from the old package
- [ ] **Step 1: Find every remaining `fabledscryer` reference in Python code**
Run:
```bash
grep -rn "fabledscryer" roundtable/ tests/ alembic.ini entrypoint.sh Dockerfile pyproject.toml
grep -rn "fabledscryer" steward/ tests/ alembic.ini entrypoint.sh Dockerfile pyproject.toml
```
Keep the output visible as a checklist.
@@ -410,24 +410,24 @@ Keep the output visible as a checklist.
- [ ] **Step 2: Run a safe codemod across Python files**
```bash
grep -rl "fabledscryer" roundtable/ tests/ | xargs sed -i 's/fabledscryer/roundtable/g'
grep -rl "fabledscryer" steward/ tests/ | xargs sed -i 's/fabledscryer/steward/g'
```
This touches only files under `roundtable/` and `tests/`. Do NOT run it across the whole repo yet — `docs/`, `.env.example`, `docker-compose.yml`, `config.example.yaml` and `README.md` are handled in later PRs.
This touches only files under `steward/` and `tests/`. Do NOT run it across the whole repo yet — `docs/`, `.env.example`, `docker-compose.yml`, `config.example.yaml` and `README.md` are handled in later PRs.
- [ ] **Step 3: Spot-check a few critical files**
```bash
grep -n "roundtable\|fabledscryer" roundtable/app.py roundtable/cli.py roundtable/config.py roundtable/migrations/env.py
grep -n "steward\|fabledscryer" steward/app.py steward/cli.py steward/config.py steward/migrations/env.py
```
Expected: only `roundtable` references remain; no stray `fabledscryer`.
Expected: only `steward` references remain; no stray `fabledscryer`.
- [ ] **Step 4: Commit**
```bash
git add -A
git commit -m "refactor: update imports fabledscryer → roundtable"
git commit -m "refactor: update imports fabledscryer → steward"
```
### Task 12: Update `pyproject.toml`
@@ -439,15 +439,15 @@ git commit -m "refactor: update imports fabledscryer → roundtable"
```toml
[project]
name = "roundtable"
name = "steward"
version = "0.1.0"
# ... dependencies unchanged ...
[project.scripts]
roundtable = "roundtable.cli:main"
steward = "steward.cli:main"
[tool.hatch.build.targets.wheel]
packages = ["roundtable"]
packages = ["steward"]
```
Leave `[project.optional-dependencies]`, `[tool.pytest.ini_options]`, and all dependency pins untouched.
@@ -456,7 +456,7 @@ Leave `[project.optional-dependencies]`, `[tool.pytest.ini_options]`, and all de
```bash
git add pyproject.toml
git commit -m "build: rename package to roundtable in pyproject"
git commit -m "build: rename package to steward in pyproject"
```
### Task 13: Update `alembic.ini`
@@ -468,7 +468,7 @@ git commit -m "build: rename package to roundtable in pyproject"
```ini
[alembic]
script_location = roundtable/migrations
script_location = steward/migrations
prepend_sys_path = .
version_path_separator = os
```
@@ -477,7 +477,7 @@ version_path_separator = os
```bash
git add alembic.ini
git commit -m "build: point alembic at roundtable/migrations"
git commit -m "build: point alembic at steward/migrations"
```
### Task 14: Verify the package installs and imports cleanly
@@ -487,8 +487,8 @@ git commit -m "build: point alembic at roundtable/migrations"
```bash
python -m venv /tmp/rt-verify
/tmp/rt-verify/bin/pip install -e .
/tmp/rt-verify/bin/python -c "import roundtable; import roundtable.app; import roundtable.cli; print('ok')"
/tmp/rt-verify/bin/roundtable --help
/tmp/rt-verify/bin/python -c "import steward; import steward.app; import steward.cli; print('ok')"
/tmp/rt-verify/bin/steward --help
```
Expected: `ok` and CLI help output. No ImportError.
@@ -501,20 +501,20 @@ rm -rf /tmp/rt-verify
- [ ] **Step 3: No commit needed (verification only).**
**PR 2 done.** Open PR with title `refactor: rename python package fabledscryer → roundtable`.
**PR 2 done.** Open PR with title `refactor: rename python package fabledscryer → steward`.
---
## PR 3 — Config & Environment Variables
### Task 15: Add env-var fallback shim in `roundtable/config.py`
### Task 15: Add env-var fallback shim in `steward/config.py`
**Files:**
- Modify: `roundtable/config.py`
- Modify: `steward/config.py`
- [ ] **Step 1: Replace the env var lookups with a fallback helper**
Open `roundtable/config.py` and replace the existing `load_bootstrap` body so it reads `ROUNDTABLE_*` first and falls back to `FABLEDSCRYER_*` with a deprecation warning.
Open `steward/config.py` and replace the existing `load_bootstrap` body so it reads `STEWARD_*` first and falls back to `FABLEDSCRYER_*` with a deprecation warning.
```python
def _env_with_fallback(new_name: str, old_name: str) -> str | None:
@@ -536,19 +536,19 @@ Then inside `load_bootstrap`, replace the existing env lookups with:
```python
database_url = (
_env_with_fallback("ROUNDTABLE_DATABASE_URL", "FABLEDSCRYER_DATABASE_URL")
or _env_with_fallback("ROUNDTABLE_DATABASE__URL", "FABLEDSCRYER_DATABASE__URL")
_env_with_fallback("STEWARD_DATABASE_URL", "FABLEDSCRYER_DATABASE_URL")
or _env_with_fallback("STEWARD_DATABASE__URL", "FABLEDSCRYER_DATABASE__URL")
or raw.get("database", {}).get("url")
)
if not database_url:
raise ValueError(
"Database URL is required. Set ROUNDTABLE_DATABASE_URL env var "
"Database URL is required. Set STEWARD_DATABASE_URL env var "
"or add 'database.url' to config.yaml."
)
# ...
plugin_dir = (
_env_with_fallback("ROUNDTABLE_PLUGIN_DIR", "FABLEDSCRYER_PLUGIN_DIR")
_env_with_fallback("STEWARD_PLUGIN_DIR", "FABLEDSCRYER_PLUGIN_DIR")
or raw.get("plugin_dir", "plugins")
)
```
@@ -557,15 +557,15 @@ And in `_resolve_secret_key`:
```python
from_env = (
_env_with_fallback("ROUNDTABLE_SECRET_KEY", "FABLEDSCRYER_SECRET_KEY")
_env_with_fallback("STEWARD_SECRET_KEY", "FABLEDSCRYER_SECRET_KEY")
or raw.get("secret_key")
)
```
- [ ] **Step 2: Grep for any other `FABLEDSCRYER_` or `FABLEDNETMON_` env var reads across `roundtable/`**
- [ ] **Step 2: Grep for any other `FABLEDSCRYER_` or `FABLEDNETMON_` env var reads across `steward/`**
```bash
grep -rn "FABLEDSCRYER_\|FABLEDNETMON_" roundtable/
grep -rn "FABLEDSCRYER_\|FABLEDNETMON_" steward/
```
For each hit, apply the same pattern (new name first, old as fallback with warning). If `FABLEDNETMON_` is pure residue with no current consumer, delete the reference.
@@ -573,8 +573,8 @@ For each hit, apply the same pattern (new name first, old as fallback with warni
- [ ] **Step 3: Commit**
```bash
git add roundtable/config.py
git commit -m "feat: ROUNDTABLE_* env vars with FABLEDSCRYER_* fallback"
git add steward/config.py
git commit -m "feat: STEWARD_* env vars with FABLEDSCRYER_* fallback"
```
### Task 16: Update `.env.example` and `config.example.yaml`
@@ -588,11 +588,11 @@ git commit -m "feat: ROUNDTABLE_* env vars with FABLEDSCRYER_* fallback"
grep -n "FABLEDSCRYER\|FABLEDNETMON" .env.example
```
Replace every occurrence with `ROUNDTABLE_*`. Add a short comment at top:
Replace every occurrence with `STEWARD_*`. Add a short comment at top:
```
# Roundtable environment configuration.
# Only ROUNDTABLE_DATABASE_URL is required. Other settings live in the DB.
# Steward environment configuration.
# Only STEWARD_DATABASE_URL is required. Other settings live in the DB.
```
- [ ] **Step 2: Rewrite `config.example.yaml`**
@@ -600,25 +600,25 @@ Replace every occurrence with `ROUNDTABLE_*`. Add a short comment at top:
Replace the header and example strings:
```yaml
# Roundtable — Bootstrap Configuration Example
# Steward — Bootstrap Configuration Example
#
# The only REQUIRED setting is the database URL.
# Set it via env var (recommended for Docker):
#
# ROUNDTABLE_DATABASE_URL=postgresql+asyncpg://user:pass@host/db
# STEWARD_DATABASE_URL=postgresql+asyncpg://user:pass@host/db
#
# All other settings are stored in the database and managed via
# the Settings UI at /settings/.
database:
url: "postgresql+asyncpg://roundtable:password@localhost/roundtable"
url: "postgresql+asyncpg://steward:password@localhost/steward"
```
- [ ] **Step 3: Commit**
```bash
git add .env.example config.example.yaml
git commit -m "docs: ROUNDTABLE_* env var examples"
git commit -m "docs: STEWARD_* env var examples"
```
### Task 17: Manual config verification
@@ -626,8 +626,8 @@ git commit -m "docs: ROUNDTABLE_* env var examples"
- [ ] **Step 1: Run the app with only the new env var**
```bash
ROUNDTABLE_DATABASE_URL=postgresql+asyncpg://fabledscryer:fabledscryer@localhost/fabledscryer \
python -m roundtable.cli --host 127.0.0.1 --port 5001
STEWARD_DATABASE_URL=postgresql+asyncpg://fabledscryer:fabledscryer@localhost/fabledscryer \
python -m steward.cli --host 127.0.0.1 --port 5001
```
(Use whatever local DB you have; the old db name is fine — data isn't being renamed.)
@@ -638,14 +638,14 @@ Expected: app starts, no deprecation warning.
```bash
FABLEDSCRYER_DATABASE_URL=postgresql+asyncpg://fabledscryer:fabledscryer@localhost/fabledscryer \
python -m roundtable.cli --host 127.0.0.1 --port 5001
python -m steward.cli --host 127.0.0.1 --port 5001
```
Expected: app starts, deprecation warning logged once.
- [ ] **Step 3: Kill processes.** No commit.
**PR 3 done.** Open PR with title `feat: ROUNDTABLE_* env vars (FABLEDSCRYER_* fallback)`.
**PR 3 done.** Open PR with title `feat: STEWARD_* env vars (FABLEDSCRYER_* fallback)`.
---
@@ -664,12 +664,12 @@ COPY fabledscryer/ fabledscryer/
```
to
```dockerfile
COPY roundtable/ roundtable/
COPY steward/ steward/
```
Change the CMD:
```dockerfile
CMD ["roundtable", "--host", "0.0.0.0", "--port", "5000"]
CMD ["steward", "--host", "0.0.0.0", "--port", "5000"]
```
Leave everything else (apt packages, gosu, app user, EXPOSE 5000) untouched.
@@ -678,7 +678,7 @@ Leave everything else (apt packages, gosu, app user, EXPOSE 5000) untouched.
```bash
git add Dockerfile
git commit -m "build: update Dockerfile for roundtable package"
git commit -m "build: update Dockerfile for steward package"
```
### Task 19: Update `entrypoint.sh`
@@ -688,15 +688,15 @@ git commit -m "build: update Dockerfile for roundtable package"
- [ ] **Step 1: Replace the `nut_setup.py` invocation path**
Change every `/app/fabledscryer/nut_setup.py` to `/app/roundtable/nut_setup.py`.
Change every `/app/fabledscryer/nut_setup.py` to `/app/steward/nut_setup.py`.
Update any comment that says "FabledScryer container entrypoint." to "Roundtable container entrypoint."
Update any comment that says "FabledScryer container entrypoint." to "Steward container entrypoint."
- [ ] **Step 2: Commit**
```bash
git add entrypoint.sh
git commit -m "build: entrypoint.sh uses roundtable package path"
git commit -m "build: entrypoint.sh uses steward package path"
```
### Task 20: Update `docker-compose.yml`
@@ -708,10 +708,10 @@ git commit -m "build: entrypoint.sh uses roundtable package path"
```yaml
services:
roundtable:
steward:
build: .
container_name: roundtable
image: roundtable:latest
container_name: steward
image: steward:latest
ports:
- "5000:5000"
volumes:
@@ -720,7 +720,7 @@ services:
- /mnt/Data/traefik/log:/var/log/traefik:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- ROUNDTABLE_DATABASE_URL=postgresql+asyncpg://roundtable:roundtable@db/roundtable
- STEWARD_DATABASE_URL=postgresql+asyncpg://steward:steward@db/steward
depends_on:
db:
condition: service_healthy
@@ -729,13 +729,13 @@ services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: roundtable
POSTGRES_PASSWORD: roundtable
POSTGRES_DB: roundtable
POSTGRES_USER: steward
POSTGRES_PASSWORD: steward
POSTGRES_DB: steward
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U roundtable"]
test: ["CMD-SHELL", "pg_isready -U steward"]
interval: 5s
timeout: 5s
retries: 5
@@ -752,37 +752,37 @@ volumes:
```bash
git add docker-compose.yml
git commit -m "build: docker-compose service → roundtable"
git commit -m "build: docker-compose service → steward"
```
### Task 21: Flip user-visible wordmark and title
**Files:**
- Modify: `roundtable/templates/base.html`
- Modify: `steward/templates/base.html`
- [ ] **Step 1: Update the `<title>` block (around line 6)**
```html
<title>{% block title %}Roundtable{% endblock %}</title>
<title>{% block title %}Steward{% endblock %}</title>
```
- [ ] **Step 2: Update the nav wordmark (around line 214)**
Change `Fabled Scryer` inside `nav .brand` to `Roundtable`.
Change `Fabled Scryer` inside `nav .brand` to `Steward`.
- [ ] **Step 3: Grep for any other user-visible "Fabled Scryer" strings in templates**
```bash
grep -rn "Fabled Scryer\|FabledScryer" roundtable/templates/
grep -rn "Fabled Scryer\|FabledScryer" steward/templates/
```
Replace each with "Roundtable".
Replace each with "Steward".
- [ ] **Step 4: Commit**
```bash
git add roundtable/templates/
git commit -m "copy: flip visible wordmark → Roundtable"
git add steward/templates/
git commit -m "copy: flip visible wordmark → Steward"
```
### Task 22: Manual container verification
@@ -795,8 +795,8 @@ docker compose up --build
```
- [ ] **Step 2: Verify**
- Container named `roundtable` runs
- Browser shows "Roundtable" in tab title and nav
- Container named `steward` runs
- Browser shows "Steward" in tab title and nav
- Login, dashboard, and error pages all render with the new palette and wordmark
- Logs show no import errors and no unhandled deprecation warnings
@@ -806,7 +806,7 @@ docker compose up --build
docker compose down
```
**PR 4 done.** Open PR with title `feat: roundtable container & wordmark flip`.
**PR 4 done.** Open PR with title `feat: steward container & wordmark flip`.
---
@@ -828,9 +828,9 @@ grep -rn "fabledscryer\|FabledScryer\|Fabled Scryer" README.md docs/
```bash
grep -rl "fabledscryer\|FabledScryer\|Fabled Scryer" README.md docs/ \
| xargs sed -i \
-e 's/FabledScryer/Roundtable/g' \
-e 's/fabledscryer/roundtable/g' \
-e 's/Fabled Scryer/Roundtable/g'
-e 's/FabledScryer/Steward/g' \
-e 's/fabledscryer/steward/g' \
-e 's/Fabled Scryer/Steward/g'
```
Then read each changed file top-to-bottom and fix any mangled sentences (e.g. capitalization at the start of sentences, code blocks that now reference a directory that still needs a `./` prefix, etc.). Pay extra attention to:
@@ -843,12 +843,12 @@ Then read each changed file top-to-bottom and fix any mangled sentences (e.g. ca
```bash
git add README.md docs/
git commit -m "docs: roundtable rename sweep"
git commit -m "docs: steward rename sweep"
```
### Task 24: Rename the Forgejo repo
- [ ] **Step 1: Via Forgejo UI or API, rename the repo from `FabledScryer` to `Roundtable`.** The old URL will redirect for one grace period.
- [ ] **Step 1: Via Forgejo UI or API, rename the repo from `FabledScryer` to `Steward`.** The old URL will redirect for one grace period.
- [ ] **Step 2: Update your local remote URL**
@@ -862,13 +862,13 @@ git remote -v
```bash
# from the parent dir
cd ..
mv FabledScryer Roundtable
cd Roundtable
mv FabledScryer Steward
cd Steward
```
- [ ] **Step 4: No commit.**
**PR 5 done.** Open PR with title `docs: roundtable rename sweep`.
**PR 5 done.** Open PR with title `docs: steward rename sweep`.
---
@@ -877,11 +877,11 @@ cd Roundtable
### Task 25: Remove env var fallback shim
**Files:**
- Modify: `roundtable/config.py`
- Modify: `steward/config.py`
- [ ] **Step 1: Delete `_env_with_fallback` and inline direct `os.environ.get("ROUNDTABLE_*")` lookups**
- [ ] **Step 1: Delete `_env_with_fallback` and inline direct `os.environ.get("STEWARD_*")` lookups**
Replace each `_env_with_fallback("ROUNDTABLE_X", "FABLEDSCRYER_X")` call with `os.environ.get("ROUNDTABLE_X")`. Delete the helper.
Replace each `_env_with_fallback("STEWARD_X", "FABLEDSCRYER_X")` call with `os.environ.get("STEWARD_X")`. Delete the helper.
- [ ] **Step 2: Grep to confirm no `FABLEDSCRYER_` references remain**
@@ -894,7 +894,7 @@ Expected: zero hits (or only historical commit messages, which don't show up in
- [ ] **Step 3: Commit**
```bash
git add roundtable/config.py
git add steward/config.py
git commit -m "refactor: remove FABLEDSCRYER_* env var fallback shim"
```
@@ -923,5 +923,5 @@ git commit -m "refactor: final rename sweep"
- Spec coverage: all 5 spec sections mapped to PRs 15; PR 6 handles the cleanup implied by the shim in PR 3.
- No placeholders — every template replacement includes concrete code; every grep includes the actual pattern.
- Type consistency: config helper is `_env_with_fallback` in both Task 15 and Task 25; env var names are `ROUNDTABLE_DATABASE_URL`, `ROUNDTABLE_PLUGIN_DIR`, `ROUNDTABLE_SECRET_KEY` consistently.
- Type consistency: config helper is `_env_with_fallback` in both Task 15 and Task 25; env var names are `STEWARD_DATABASE_URL`, `STEWARD_PLUGIN_DIR`, `STEWARD_SECRET_KEY` consistently.
- Tests intentionally skipped per project feedback; verification is manual (browser + container).
@@ -1,21 +1,21 @@
# Roundtable Rebrand — Design
# Steward Rebrand — Design
**Date:** 2026-04-13
**Status:** Approved for planning
## 1. Identity
**Name:** Roundtable (dropping the "Fabled" prefix).
**Name:** Steward (dropping the "Fabled" prefix).
**Why the change:** The old name `FabledScryer` framed the app as a single seer peering into one crystal ball. The actual product is a gathering point — hosts, metrics, alerts, plugins, and dashboards from across the homelab converge here. "Roundtable" captures that: a place where the realm's tools and information meet.
**Why the change:** The old name `FabledScryer` framed the app as a single seer peering into one crystal ball. The actual product is a gathering point — hosts, metrics, alerts, plugins, and dashboards from across the homelab converge here. "Steward" captures that: a place where the realm's tools and information meet.
**Name conflict check:**
- *Roundtable Software* — a Progress ABL tooling vendor. Different demographic, no overlap.
- *Steward Software* — a Progress ABL tooling vendor. Different demographic, no overlap.
- *Fabled.gg* — a virtual tabletop product. Closest concern, and the reason the "Fabled" prefix is dropped entirely to avoid confusion inside the tabletop/fantasy space.
**Umbrella:** `fabledsword.com` remains the family umbrella domain; `git.fabledsword.com` remains the plugin catalog host. Roundtable becomes one product under that umbrella.
**Umbrella:** `fabledsword.com` remains the family umbrella domain; `git.fabledsword.com` remains the plugin catalog host. Steward becomes one product under that umbrella.
**Tone:** Heraldic / Arthurian register. The roundtable is the gathering place of those who keep watch over the realm.
**Tone:** Heraldic / Arthurian register. The steward is the gathering place of those who keep watch over the realm.
## 2. Visual System
@@ -78,7 +78,7 @@ Candlelit glow — a soft, warm radial wash behind the main content area. Replac
### Wordmark
"Roundtable" in EB Garamond, gold (`#c8a840`), paired with the seal mark to the left in the nav header.
"Steward" in EB Garamond, gold (`#c8a840`), paired with the seal mark to the left in the nav header.
## 3. Voice & Copy
@@ -100,21 +100,21 @@ Candlelit glow — a soft, warm radial wash behind the main content area. Replac
## 4. Rename Mechanics
**Code & packaging**
- `fabledscryer/` package directory → `roundtable/`
- All `from fabledscryer...` / `import fabledscryer...``roundtable`
- `pyproject.toml`: project name, CLI entry point (`fabledscryer = "fabledscryer.cli:main"``roundtable = "roundtable.cli:main"`), tool sections
- `fabledscryer/` package directory → `steward/`
- All `from fabledscryer...` / `import fabledscryer...``steward`
- `pyproject.toml`: project name, CLI entry point (`fabledscryer = "fabledscryer.cli:main"``steward = "steward.cli:main"`), tool sections
- `__init__.py` package metadata
**Runtime config**
- Env vars: `FABLEDSCRYER_*``ROUNDTABLE_*`
- Env vars: `FABLEDSCRYER_*``STEWARD_*`
- Residual `FABLEDNETMON_*` env vars removed
- Config dir: `~/.config/fabledscryer``~/.config/roundtable` (one-shot copy on first boot)
- Config dir: `~/.config/fabledscryer``~/.config/steward` (one-shot copy on first boot)
- Log file names, systemd unit names if any
**Container & deploy**
- `Dockerfile` labels, workdir
- `docker-compose.yml` service name, image tag, volume names, `container_name`
- Published image: `fabledscryer:latest``roundtable:latest`
- Published image: `fabledscryer:latest``steward:latest`
**Database**
- Alembic `script_location` and any customized version table
@@ -144,10 +144,10 @@ Staged so `main` stays runnable between PRs.
New palette tokens, locked logo SVG, EB Garamond font swap, candlelit glow background, themed edge copy (login, empty states, 404/500, dashboard hero "Under Watch, Under Care"). Still named FabledScryer internally. Ships independently, low risk.
**PR 2 — Python package rename**
`fabledscryer/``roundtable/`, all imports, `pyproject.toml` name + entry point, `__init__.py` metadata. Docker/config untouched. Package installable as `roundtable` locally.
`fabledscryer/``steward/`, all imports, `pyproject.toml` name + entry point, `__init__.py` metadata. Docker/config untouched. Package installable as `steward` locally.
**PR 3 — Config & env vars**
`FABLEDSCRYER_*``ROUNDTABLE_*` with a short-lived fallback reader (accept both, warn on old) so self-hosters don't break mid-upgrade. Config dir migration on first boot. Drop `FABLEDNETMON_*` residue.
`FABLEDSCRYER_*``STEWARD_*` with a short-lived fallback reader (accept both, warn on old) so self-hosters don't break mid-upgrade. Config dir migration on first boot. Drop `FABLEDNETMON_*` residue.
**PR 4 — Container & deploy**
`Dockerfile`, `docker-compose.yml` service/image/volume/container names. Published image tag switch. Template wordmark + `<title>` + meta (the last user-visible string flip).