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
+3 -3
View File
@@ -1,6 +1,6 @@
# Copy to .env for Docker / local development secrets # Copy to .env for Docker / local development secrets
# These override values in config.yaml # These override values in config.yaml
ROUNDTABLE_SECRET_KEY=change-me STEWARD_SECRET_KEY=change-me
ROUNDTABLE_DATABASE__URL=postgresql+asyncpg://roundtable:password@localhost/roundtable STEWARD_DATABASE__URL=postgresql+asyncpg://steward:password@localhost/steward
ROUNDTABLE_SMTP__PASSWORD= STEWARD_SMTP__PASSWORD=
+1 -1
View File
@@ -1,7 +1,7 @@
# Planning files # Planning files
docs/superpowers/ docs/superpowers/
# Plugin directory — managed as a separate repo (bvandeusen/roundtable-plugins) # Plugin directory — managed as a separate repo (bvandeusen/steward-plugins)
# PLUGIN_DIR in config.yaml points here at runtime # PLUGIN_DIR in config.yaml points here at runtime
/plugins/ /plugins/
+3 -3
View File
@@ -13,7 +13,7 @@ RUN pip install --upgrade pip
WORKDIR /app WORKDIR /app
COPY pyproject.toml . COPY pyproject.toml .
COPY roundtable/ roundtable/ COPY steward/ steward/
RUN pip install --no-cache-dir . RUN pip install --no-cache-dir .
COPY alembic.ini . COPY alembic.ini .
@@ -22,7 +22,7 @@ RUN chmod +x /entrypoint.sh
# Runtime directories — owned by app user. The container starts as root # Runtime directories — owned by app user. The container starts as root
# so the entrypoint can fix up /data permissions if needed, then drops to # so the entrypoint can fix up /data permissions if needed, then drops to
# 'app' (uid 1000) via gosu before launching roundtable. # 'app' (uid 1000) via gosu before launching steward.
RUN useradd -m -u 1000 app \ RUN useradd -m -u 1000 app \
&& mkdir -p /data/playbook_cache /data/plugins \ && mkdir -p /data/playbook_cache /data/plugins \
&& chown -R app:app /data /app && chown -R app:app /data /app
@@ -30,4 +30,4 @@ RUN useradd -m -u 1000 app \
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
CMD ["roundtable", "--host", "0.0.0.0", "--port", "5000"] CMD ["steward", "--host", "0.0.0.0", "--port", "5000"]
+5 -5
View File
@@ -1,6 +1,6 @@
# Roundtable # Steward
A self-hosted network monitoring and infrastructure management hub for home servers. Roundtable gives you a single pane of glass over your hosts, services, and automation — with live-updating dashboards, alerting, and Ansible integration. A self-hosted network monitoring and infrastructure management hub for home servers. Steward gives you a single pane of glass over your hosts, services, and automation — with live-updating dashboards, alerting, and Ansible integration.
--- ---
@@ -21,7 +21,7 @@ No JavaScript framework. No build step. No external workers or message brokers.
```bash ```bash
cp .env.example .env cp .env.example .env
# Set ROUNDTABLE_DATABASE_URL in .env # Set STEWARD_DATABASE_URL in .env
docker compose up -d docker compose up -d
``` ```
@@ -41,7 +41,7 @@ pip install -e .
cp config.example.yaml config.yaml cp config.example.yaml config.yaml
# Edit config.yaml — set database.url at minimum # Edit config.yaml — set database.url at minimum
roundtable --host 0.0.0.0 --port 5000 steward --host 0.0.0.0 --port 5000
``` ```
ICMP ping requires `CAP_NET_RAW` or the `setuid` ping binary. TCP mode (the default) needs no elevated privileges. ICMP ping requires `CAP_NET_RAW` or the `setuid` ping binary. TCP mode (the default) needs no elevated privileges.
@@ -54,7 +54,7 @@ Only two things must be set to run the app:
| What | How | | What | How |
|---|---| |---|---|
| Database URL | `ROUNDTABLE_DATABASE_URL` env var or `database.url` in `config.yaml` | | Database URL | `STEWARD_DATABASE_URL` env var or `database.url` in `config.yaml` |
| Secret key | Auto-generated on first run and saved to `/data/secret.key` | | Secret key | Auto-generated on first run and saved to `/data/secret.key` |
Everything else — SMTP, webhooks, monitor intervals, Ansible sources, plugin settings — is configured through the web UI at `/settings/`. Everything else — SMTP, webhooks, monitor intervals, Ansible sources, plugin settings — is configured through the web UI at `/settings/`.
+1 -1
View File
@@ -1,5 +1,5 @@
[alembic] [alembic]
script_location = roundtable/migrations script_location = steward/migrations
prepend_sys_path = . prepend_sys_path = .
version_path_separator = os version_path_separator = os
+3 -3
View File
@@ -1,9 +1,9 @@
# Roundtable — Bootstrap Configuration Example # Steward — Bootstrap Configuration Example
# #
# The only REQUIRED setting is the database URL. # The only REQUIRED setting is the database URL.
# Set it via env var (recommended for Docker): # 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 (SMTP, webhook, ansible, plugins, retention) # All other settings (SMTP, webhook, ansible, plugins, retention)
# are stored in the database and managed via the Settings UI at /settings/. # are stored in the database and managed via the Settings UI at /settings/.
@@ -11,7 +11,7 @@
# config.yaml is optional. Use it only if you prefer file-based bootstrap. # config.yaml is optional. Use it only if you prefer file-based bootstrap.
database: database:
url: "postgresql+asyncpg://roundtable:password@localhost/roundtable" url: "postgresql+asyncpg://steward:password@localhost/steward"
# Optional: override the auto-generated secret key. # Optional: override the auto-generated secret key.
# If not set, a key is auto-generated on first run and saved to /data/secret.key. # If not set, a key is auto-generated on first run and saved to /data/secret.key.
+8 -8
View File
@@ -1,8 +1,8 @@
services: services:
roundtable: steward:
build: . build: .
container_name: roundtable container_name: steward
image: roundtable:latest image: steward:latest
ports: ports:
- "5000:5000" - "5000:5000"
volumes: volumes:
@@ -11,7 +11,7 @@ services:
- /mnt/Data/traefik/log:/var/log/traefik:ro - /mnt/Data/traefik/log:/var/log/traefik:ro
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
environment: environment:
- ROUNDTABLE_DATABASE_URL=postgresql+asyncpg://fabledscryer:fabledscryer@db/fabledscryer - STEWARD_DATABASE_URL=postgresql+asyncpg://steward:steward@db/steward
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@@ -20,13 +20,13 @@ services:
db: db:
image: postgres:16-alpine image: postgres:16-alpine
environment: environment:
POSTGRES_USER: fabledscryer POSTGRES_USER: steward
POSTGRES_PASSWORD: fabledscryer POSTGRES_PASSWORD: steward
POSTGRES_DB: fabledscryer POSTGRES_DB: steward
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U fabledscryer"] test: ["CMD-SHELL", "pg_isready -U steward"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
+13 -13
View File
@@ -1,12 +1,12 @@
# Architecture # 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 ## 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 | | Step | What happens |
|---|---| |---|---|
@@ -32,14 +32,14 @@ All routes are Quart Blueprints registered in `app.py`:
| Prefix | Blueprint | Module | | Prefix | Blueprint | Module |
|---|---|---| |---|---|---|
| `/auth/` | `auth_bp` | `roundtable/auth/routes.py` | | `/auth/` | `auth_bp` | `steward/auth/routes.py` |
| `/` | `dashboard_bp` | `roundtable/dashboard/routes.py` | | `/` | `dashboard_bp` | `steward/dashboard/routes.py` |
| `/hosts/` | `hosts_bp` | `roundtable/hosts/routes.py` | | `/hosts/` | `hosts_bp` | `steward/hosts/routes.py` |
| `/ping/` | `ping_bp` | `roundtable/ping/routes.py` | | `/ping/` | `ping_bp` | `steward/ping/routes.py` |
| `/dns/` | `dns_bp` | `roundtable/dns/routes.py` | | `/dns/` | `dns_bp` | `steward/dns/routes.py` |
| `/alerts/` | `alerts_bp` | `roundtable/alerts/routes.py` | | `/alerts/` | `alerts_bp` | `steward/alerts/routes.py` |
| `/ansible/` | `ansible_bp` | `roundtable/ansible/routes.py` | | `/ansible/` | `ansible_bp` | `steward/ansible/routes.py` |
| `/settings/` | `settings_bp` | `roundtable/settings/routes.py` | | `/settings/` | `settings_bp` | `steward/settings/routes.py` |
| `/plugins/<name>/` | plugin blueprint | `plugins/<name>/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. 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 ## 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 - `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 - `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: 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) - **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: Live-updating widgets use HTMX polling:
```html ```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 4. If a state transition occurs, an `AlertEvent` row is written
5. Notification I/O is deferred outside the transaction via `asyncio.create_task()` 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: `record_metric()` must always be called inside an active transaction:
@@ -130,7 +130,7 @@ Webhook is skipped if `webhook.url` is empty.
## Data Models ## Data Models
Defined in `roundtable/models/alerts.py`: Defined in `steward/models/alerts.py`:
- **`alert_rules`** — one row per configured rule - **`alert_rules`** — one row per configured rule
- **`alert_states`** — one row per rule, tracks current state and consecutive failure count - **`alert_states`** — one row per rule, tracks current state and consecutive failure count
+6 -6
View File
@@ -1,6 +1,6 @@
# Ansible Integration # 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 ## Data Model
`ansible_runs` table (defined in `roundtable/models/ansible.py`): `ansible_runs` table (defined in `steward/models/ansible.py`):
| Column | Type | Description | | Column | Type | Description |
|---|---|---| |---|---|---|
@@ -137,7 +137,7 @@ Runs older than `data.retention_days` (default 90) are pruned by the `data_clean
| File | Purpose | | File | Purpose |
|---|---| |---|---|
| `roundtable/ansible/sources.py` | Source discovery, git pull logic | | `steward/ansible/sources.py` | Source discovery, git pull logic |
| `roundtable/ansible/executor.py` | Subprocess execution and output streaming | | `steward/ansible/executor.py` | Subprocess execution and output streaming |
| `roundtable/ansible/routes.py` | HTTP routes (browse, trigger, stream, history) | | `steward/ansible/routes.py` | HTTP routes (browse, trigger, stream, history) |
| `roundtable/models/ansible.py` | `AnsibleRun` model | | `steward/models/ansible.py` | `AnsibleRun` model |
+10 -12
View File
@@ -1,6 +1,6 @@
# Configuration # 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 | | Key | Env var | Default | Description |
|---|---|---|---| |---|---|---|---|
| `database.url` | `ROUNDTABLE_DATABASE_URL` | — | PostgreSQL async URL. **Required.** | | `database.url` | `STEWARD_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. | | `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` | `ROUNDTABLE_PLUGIN_DIR` | `plugins` | Path to the plugins directory. | | `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 ### Minimal config.yaml
```yaml ```yaml
database: database:
url: "postgresql+asyncpg://user:password@localhost/roundtable" url: "postgresql+asyncpg://user:password@localhost/steward"
``` ```
### Minimal env-only setup (Docker) ### Minimal env-only setup (Docker)
```bash ```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. 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) ## App Settings (Database-backed)
@@ -68,7 +66,7 @@ Default webhook template:
### Reading and Writing Settings in Code ### Reading and Writing Settings in Code
```python ```python
from roundtable.core.settings import get_setting, set_setting from steward.core.settings import get_setting, set_setting
# Read # Read
async with current_app.db_sessionmaker() as session: async with current_app.db_sessionmaker() as session:
@@ -84,7 +82,7 @@ async with current_app.db_sessionmaker() as session:
### The DEFAULTS Dict ### 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 # 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 ## Ping Monitor
**Source:** `roundtable/monitors/ping.py` **Source:** `steward/monitors/ping.py`
**Scheduler task:** `ping_monitor` in `roundtable/app.py` **Scheduler task:** `ping_monitor` in `steward/app.py`
**Interval:** `monitors.poll_interval_seconds` (default 60s), runs on startup **Interval:** `monitors.poll_interval_seconds` (default 60s), runs on startup
### How It Works ### How It Works
@@ -29,7 +29,7 @@ Each probe writes a `PingResult` row and calls `record_metric()`:
### Data Model ### Data Model
`ping_results` table (defined in `roundtable/models/monitors.py`): `ping_results` table (defined in `steward/models/monitors.py`):
| Column | Type | Description | | Column | Type | Description |
|---|---|---| |---|---|---|
@@ -51,8 +51,8 @@ Old rows are pruned by the `data_cleanup` task (default: 90 days).
## DNS Monitor ## DNS Monitor
**Source:** `roundtable/monitors/dns.py` **Source:** `steward/monitors/dns.py`
**Scheduler task:** `dns_monitor` in `roundtable/app.py` **Scheduler task:** `dns_monitor` in `steward/app.py`
**Interval:** `monitors.poll_interval_seconds` (default 60s), runs on startup **Interval:** `monitors.poll_interval_seconds` (default 60s), runs on startup
### How It Works ### How It Works
@@ -70,7 +70,7 @@ Each check writes a `DnsResult` row and calls `record_metric()`:
### Data Model ### Data Model
`dns_results` table (defined in `roundtable/models/monitors.py`): `dns_results` table (defined in `steward/models/monitors.py`):
| Column | Type | Description | | Column | Type | Description |
|---|---|---| |---|---|---|
+36 -36
View File
@@ -2,13 +2,13 @@
**Date:** 2026-04-14 **Date:** 2026-04-14
**Status:** Approved, ready for implementation planning **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 ## 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 ## Non-goals
@@ -24,7 +24,7 @@ Give Roundtable a fleet-glance view of remote Linux hosts — current resource u
``` ```
┌──────────────┐ POST /plugins/host_agent/ingest ┌────────────────────────┐ ┌──────────────┐ POST /plugins/host_agent/ingest ┌────────────────────────┐
│ Agent │ ──────────────────────────────────────────→ │ Roundtable │ Agent │ ──────────────────────────────────────────→ │ Steward
│ (Python) │ Authorization: Bearer <per-host-token> │ host_agent plugin │ │ (Python) │ Authorization: Bearer <per-host-token> │ host_agent plugin │
│ on target │ JSON body: metrics snapshot │ │ │ on target │ JSON body: metrics snapshot │ │
│ host │ │ routes.py (ingest + │ │ 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. - **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. - **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. - **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. - **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 Roundtable outages don't lose brief-window data. - **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 ### File layout on the target host
``` ```
/usr/local/lib/roundtable-agent/agent.py # the script, target ~300 lines /usr/local/lib/steward-agent/agent.py # the script, target ~300 lines
/etc/roundtable-agent.conf # key=value config, 0640 root:roundtable-agent /etc/steward-agent.conf # key=value config, 0640 root:steward-agent
/etc/systemd/system/roundtable-agent.service # unit file /etc/systemd/system/steward-agent.service # unit file
# dedicated system user: roundtable-agent # dedicated system user: steward-agent
``` ```
### Config file format ### 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): 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... token = a1b2c3d4...
interval_seconds = 30 interval_seconds = 30
hostname = myhost # optional; defaults to uname -n 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 ### 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 ### Failure behavior
@@ -194,7 +194,7 @@ One `ScheduledTask` running every 60 seconds that flags `HostAgentRegistration`
### `METRIC_CATALOG` registration ### `METRIC_CATALOG` registration
Add to `roundtable/alerts/routes.py`: Add to `steward/alerts/routes.py`:
```python ```python
"host_agent": ["cpu_pct", "mem_used_pct", "mem_available_bytes", "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"], "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 ```http
POST /plugins/host_agent/ingest HTTP/1.1 POST /plugins/host_agent/ingest HTTP/1.1
Host: roundtable.home.lan Host: steward.home.lan
Authorization: Bearer a1b2c3d4e5f6... Authorization: Bearer a1b2c3d4e5f6...
Content-Type: application/json 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) ### 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: 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 ```sh
#!/bin/sh #!/bin/sh
# Roundtable host agent installer # Steward host agent installer
# Generated for: {{ host_name }} ({{ host_address }}) # Generated for: {{ host_name }} ({{ host_address }})
# Roundtable URL: {{ url }} # Steward URL: {{ url }}
set -e set -e
ROUNDTABLE_URL="{{ url }}" STEWARD_URL="{{ url }}"
AGENT_TOKEN="{{ token }}" AGENT_TOKEN="{{ token }}"
AGENT_VERSION="{{ agent_version }}" AGENT_VERSION="{{ agent_version }}"
AGENT_USER="roundtable-agent" AGENT_USER="steward-agent"
AGENT_DIR="/usr/local/lib/roundtable-agent" AGENT_DIR="/usr/local/lib/steward-agent"
CONF_FILE="/etc/roundtable-agent.conf" CONF_FILE="/etc/steward-agent.conf"
UNIT_FILE="/etc/systemd/system/roundtable-agent.service" UNIT_FILE="/etc/systemd/system/steward-agent.service"
# ── preflight ──────────────────────────────────────────────────────────────── # ── preflight ────────────────────────────────────────────────────────────────
[ "$(id -u)" = "0" ] || { echo "must run as root (use sudo)"; exit 1; } [ "$(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 # Handle --uninstall
if [ "${1:-}" = "--uninstall" ]; then 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 -f "$UNIT_FILE" "$CONF_FILE"
rm -rf "$AGENT_DIR" rm -rf "$AGENT_DIR"
systemctl daemon-reload systemctl daemon-reload
@@ -363,13 +363,13 @@ fi
# ── drop the agent file ────────────────────────────────────────────────────── # ── drop the agent file ──────────────────────────────────────────────────────
mkdir -p "$AGENT_DIR" 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" chmod 0755 "$AGENT_DIR/agent.py"
chown root:root "$AGENT_DIR/agent.py" chown root:root "$AGENT_DIR/agent.py"
# ── write config ───────────────────────────────────────────────────────────── # ── write config ─────────────────────────────────────────────────────────────
cat > "$CONF_FILE" <<EOF cat > "$CONF_FILE" <<EOF
url = $ROUNDTABLE_URL url = $STEWARD_URL
token = $AGENT_TOKEN token = $AGENT_TOKEN
interval_seconds = 30 interval_seconds = 30
EOF EOF
@@ -379,7 +379,7 @@ chmod 0640 "$CONF_FILE"
# ── write systemd unit ─────────────────────────────────────────────────────── # ── write systemd unit ───────────────────────────────────────────────────────
cat > "$UNIT_FILE" <<EOF cat > "$UNIT_FILE" <<EOF
[Unit] [Unit]
Description=Roundtable host agent Description=Steward host agent
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
@@ -400,17 +400,17 @@ WantedBy=multi-user.target
EOF EOF
systemctl daemon-reload systemctl daemon-reload
systemctl enable --now roundtable-agent.service systemctl enable --now steward-agent.service
echo echo
echo "Roundtable host agent $AGENT_VERSION installed and running." echo "Steward host agent $AGENT_VERSION installed and running."
echo "Check status: systemctl status roundtable-agent" echo "Check status: systemctl status steward-agent"
echo "Logs: journalctl -u roundtable-agent -f" echo "Logs: journalctl -u steward-agent -f"
``` ```
### Design points ### 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. - **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. - **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. - **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 ## 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_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. - **`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 | | 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. | | Agent can't reach Steward | 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. | | Steward 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. | | 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). | | 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. | | 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. | | `/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. 6. Settings page: list, add-host flow, rotate-token, delete.
7. Dashboard widgets: table + history chart. Register in `core/widgets.py`. 7. Dashboard widgets: table + history chart. Register in `core/widgets.py`.
8. Per-host detail page. 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. 10. Scheduler: stale-agent marker.
11. Tests at every stage; final integration test to tie it together. 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. > **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. **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) ## 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. **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/scheduler.py` — stale-agent marker task.
- `plugins/host_agent/agent.py` — the Python agent script, served to targets. - `plugins/host_agent/agent.py` — the Python agent script, served to targets.
- `plugins/host_agent/migrations/__init__.py` - `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/__init__.py`
- `plugins/host_agent/migrations/versions/host_agent_001_initial.py` — creates `host_agent_registrations`. - `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. - `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):** **Modified files (core, small edits):**
- `roundtable/alerts/routes.py` — add `"host_agent"` entry to `METRIC_CATALOG`. - `steward/alerts/routes.py` — add `"host_agent"` entry to `METRIC_CATALOG`.
- `roundtable/core/widgets.py` — add `host_resources` and `host_resource_history` entries. - `steward/core/widgets.py` — add `host_resources` and `host_resource_history` entries.
- `docs/plugins/index.yaml.example` — add catalog entry. - `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): async def test_host_agent_migration_creates_table(app):
# The app fixture runs all plugin migrations on startup. # 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() engine = get_engine()
async with engine.connect() as conn: async with engine.connect() as conn:
def _check(sync_conn): def _check(sync_conn):
@@ -127,11 +127,11 @@ Expected: FAIL (plugin not loaded, table missing).
name: host_agent name: host_agent
version: "1.0.0" version: "1.0.0"
description: "Remote Linux host resource monitoring via a lightweight Python push agent (CPU, memory, storage, load, uptime)" description: "Remote Linux host resource monitoring via a lightweight Python push agent (CPU, memory, storage, load, uptime)"
author: "Roundtable" author: "Steward"
license: "MIT" license: "MIT"
min_app_version: "0.1.0" min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins" repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/host_agent" homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/host_agent"
tags: tags:
- host - host
- monitoring - monitoring
@@ -180,7 +180,7 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlalchemy import Column, String, DateTime, ForeignKey from sqlalchemy import Column, String, DateTime, ForeignKey
from roundtable.models.base import Base from steward.models.base import Base
def _uuid() -> str: def _uuid() -> str:
@@ -269,8 +269,8 @@ from alembic import context
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from roundtable.models.base import Base from steward.models.base import Base
import roundtable.models # noqa: F401 import steward.models # noqa: F401
from plugins.host_agent.models import HostAgentRegistration # noqa: F401 from plugins.host_agent.models import HostAgentRegistration # noqa: F401
config = context.config config = context.config
@@ -282,14 +282,14 @@ target_metadata = Base.metadata
def _get_url() -> str: def _get_url() -> str:
import yaml import yaml
cfg_path = os.environ.get("ROUNDTABLE_CONFIG", "config.yaml") cfg_path = os.environ.get("STEWARD_CONFIG", "config.yaml")
try: try:
with open(cfg_path) as f: with open(cfg_path) as f:
cfg = yaml.safe_load(f) or {} cfg = yaml.safe_load(f) or {}
url = cfg.get("database", {}).get("url", "") url = cfg.get("database", {}).get("url", "")
except FileNotFoundError: except FileNotFoundError:
url = "" url = ""
return os.environ.get("ROUNDTABLE_DATABASE__URL", url) return os.environ.get("STEWARD_DATABASE__URL", url)
def run_migrations_offline() -> None: 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): def test_parses_flat_key_value(tmp_path):
p = tmp_path / "agent.conf" p = tmp_path / "agent.conf"
p.write_text( p.write_text(
"url = https://roundtable.example\n" "url = https://steward.example\n"
"token = abc123\n" "token = abc123\n"
"interval_seconds = 45\n" "interval_seconds = 45\n"
) )
cfg = read_config(str(p)) cfg = read_config(str(p))
assert cfg["url"] == "https://roundtable.example" assert cfg["url"] == "https://steward.example"
assert cfg["token"] == "abc123" assert cfg["token"] == "abc123"
assert cfg["interval_seconds"] == 45 assert cfg["interval_seconds"] == 45
@@ -466,7 +466,7 @@ Expected: FAIL (`read_config` does not exist).
```python ```python
# plugins/host_agent/agent.py # 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 Python 3.8+ stdlib only. Target ~300 lines. Served to targets at
GET /plugins/host_agent/agent.py. GET /plugins/host_agent/agent.py.
@@ -1060,7 +1060,7 @@ def post_payload(url: str, token: str, payload: dict) -> tuple[bool, int | None]
headers={ headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": f"roundtable-host-agent/{AGENT_VERSION}", "User-Agent": f"steward-host-agent/{AGENT_VERSION}",
}, },
method="POST", method="POST",
) )
@@ -1112,7 +1112,7 @@ def main_loop(conf_path: str) -> int:
buffer = RingBuffer(maxlen=20) buffer = RingBuffer(maxlen=20)
backoff = 0 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)") f"(url={cfg['url']}, interval={cfg['interval_seconds']}s)")
while not _shutdown_requested: while not _shutdown_requested:
@@ -1171,7 +1171,7 @@ def main_loop(conf_path: str) -> int:
if __name__ == "__main__": 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)) sys.exit(main_loop(conf))
``` ```
@@ -1209,8 +1209,8 @@ git commit -m "feat(host_agent): agent POST, backoff, and main loop"
import hashlib import hashlib
import pytest_asyncio import pytest_asyncio
from roundtable.models.hosts import Host from steward.models.hosts import Host
from roundtable.core.db import get_session from steward.core.db import get_session
from plugins.host_agent.models import HostAgentRegistration from plugins.host_agent.models import HostAgentRegistration
@@ -1233,7 +1233,7 @@ async def registered_host(app):
return {"host": host, "registration": reg, "token": raw_token} 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** - [ ] **Step 2: Write failing ingest test**
@@ -1244,8 +1244,8 @@ from datetime import datetime, timezone
import pytest import pytest
from sqlalchemy import select from sqlalchemy import select
from roundtable.models.metrics import PluginMetric from steward.models.metrics import PluginMetric
from roundtable.core.db import get_session from steward.core.db import get_session
from plugins.host_agent.models import HostAgentRegistration from plugins.host_agent.models import HostAgentRegistration
pytestmark = pytest.mark.asyncio pytestmark = pytest.mark.asyncio
@@ -1352,9 +1352,9 @@ from typing import Any
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from sqlalchemy import select from sqlalchemy import select
from roundtable.core.db import get_session from steward.core.db import get_session
from roundtable.models.hosts import Host from steward.models.hosts import Host
from roundtable.models.metrics import PluginMetric from steward.models.metrics import PluginMetric
from .models import HostAgentRegistration from .models import HostAgentRegistration
host_agent_bp = Blueprint("host_agent", __name__, template_folder="templates") 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.status_code == 200
assert resp.content_type.startswith("text/plain") assert resp.content_type.startswith("text/plain")
text = (await resp.get_data()).decode() text = (await resp.get_data()).decode()
assert "roundtable-agent" in text assert "steward-agent" in text
assert registered_host["token"] in text assert registered_host["token"] in text
assert "systemctl enable --now" in text assert "systemctl enable --now" in text
assert "NoNewPrivileges=yes" 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: `plugins/host_agent/templates/settings_list.html`
- Create: `tests/plugins/host_agent/test_settings_routes.py` - 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** - [ ] **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. Note the decorator name and import path for use in Step 3.
- [ ] **Step 2: Write failing test** - [ ] **Step 2: Write failing test**
@@ -1772,8 +1772,8 @@ Note the decorator name and import path for use in Step 3.
import pytest import pytest
from sqlalchemy import select from sqlalchemy import select
from roundtable.core.db import get_session from steward.core.db import get_session
from roundtable.models.hosts import Host from steward.models.hosts import Host
from plugins.host_agent.models import HostAgentRegistration from plugins.host_agent.models import HostAgentRegistration
pytestmark = pytest.mark.asyncio 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. # TODO: replace _require_admin with the project-wide decorator found in Step 1.
# Placeholder below mirrors the shape; swap for real admin auth. # 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]: def _new_token_pair() -> tuple[str, str]:
@@ -1938,7 +1938,7 @@ async def settings_list():
```html ```html
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Host Agent — Roundtable{% endblock %} {% block title %}Host Agent — Steward{% endblock %}
{% block content %} {% block content %}
<h1 class="page-title">Host Agent — Registered Hosts</h1> <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** - [ ] **Step 6: Run tests — iterate on auth decorator if needed**
Run: `pytest tests/plugins/host_agent/test_settings_routes.py -v` 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** - [ ] **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. - 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_table.html`
- Create: `plugins/host_agent/templates/widget_history.html` - Create: `plugins/host_agent/templates/widget_history.html`
- Modify: `roundtable/core/widgets.py` - Modify: `steward/core/widgets.py`
- Modify: `roundtable/alerts/routes.py` - Modify: `steward/alerts/routes.py`
- [ ] **Step 1: Add widget partial routes to `plugins/host_agent/routes.py`** - [ ] **Step 1: Add widget partial routes to `plugins/host_agent/routes.py`**
@@ -2165,7 +2165,7 @@ async def widget_history():
</div> </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): 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`** - [ ] **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 ```python
"host_agent": [ "host_agent": [
@@ -2244,7 +2244,7 @@ Expected: PASS.
- [ ] **Step 7: Commit** - [ ] **Step 7: Commit**
```bash ```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" 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 import pytest
from sqlalchemy import select from sqlalchemy import select
from roundtable.core.db import get_session from steward.core.db import get_session
from roundtable.models.hosts import Host from steward.models.hosts import Host
from plugins.host_agent.models import HostAgentRegistration from plugins.host_agent.models import HostAgentRegistration
from plugins.host_agent.scheduler import find_stale_registrations from plugins.host_agent.scheduler import find_stale_registrations
@@ -2459,8 +2459,8 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import select from sqlalchemy import select
from roundtable.core.db import get_session from steward.core.db import get_session
from roundtable.models.hosts import Host from steward.models.hosts import Host
from .models import HostAgentRegistration from .models import HostAgentRegistration
@@ -2531,8 +2531,8 @@ from unittest.mock import patch
import pytest import pytest
from sqlalchemy import select from sqlalchemy import select
from roundtable.core.db import get_session from steward.core.db import get_session
from roundtable.models.metrics import PluginMetric from steward.models.metrics import PluginMetric
from plugins.host_agent import agent as a from plugins.host_agent import agent as a
pytestmark = pytest.mark.asyncio pytestmark = pytest.mark.asyncio
@@ -2609,12 +2609,12 @@ Append to the `plugins:` list in `docs/plugins/index.yaml.example`:
- name: host_agent - name: host_agent
version: "1.0.0" version: "1.0.0"
description: "Remote Linux host resource monitoring via a lightweight Python push agent" description: "Remote Linux host resource monitoring via a lightweight Python push agent"
author: "Roundtable" author: "Steward"
license: "MIT" license: "MIT"
min_app_version: "0.1.0" min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins" repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/host_agent" homepage: "https://git.fabledsword.com/bvandeusen/Steward-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" download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
checksum_sha256: "" checksum_sha256: ""
tags: tags:
- host - 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** - [ ] **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 spec: `docs/plugins/host-agent-design.md`
- Link to plan: `docs/plugins/host-agent-plan.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). - 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. # It is fetched by the app's Settings → Plugins → Plugin Catalog UI.
# #
# Roundtable reads this file from: # Steward reads this file from:
# https://git.fabledsword.com/bvandeusen/Roundtable-plugins/raw/branch/main/index.yaml # 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 # 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). # live immediately for anyone whose app fetches the catalog (cache TTL: 5 min).
@@ -25,7 +25,7 @@
# #
# Download URL conventions: # Download URL conventions:
# Gitea release assets (recommended): # 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. # 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). # Source archive tarballs work too but release assets are preferred (smaller, plugin-only).
@@ -37,12 +37,12 @@ plugins:
- name: http - name: http
version: "1.0.0" version: "1.0.0"
description: "Synthetic HTTP endpoint monitoring — status code, response time, content match, TLS expiry" description: "Synthetic HTTP endpoint monitoring — status code, response time, content match, TLS expiry"
author: "Roundtable" author: "Steward"
license: "MIT" license: "MIT"
min_app_version: "0.1.0" min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins" repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/http" homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/http"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/http-v1.0.0/http.zip" download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/http-v1.0.0/http.zip"
checksum_sha256: "" checksum_sha256: ""
tags: tags:
- monitoring - monitoring
@@ -53,12 +53,12 @@ plugins:
- name: docker - name: docker
version: "1.0.0" version: "1.0.0"
description: "Docker container status, resource usage, and restart tracking via Docker socket" description: "Docker container status, resource usage, and restart tracking via Docker socket"
author: "Roundtable" author: "Steward"
license: "MIT" license: "MIT"
min_app_version: "0.1.0" min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins" repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/docker" homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/docker"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/docker-v1.0.0/docker.zip" download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/docker-v1.0.0/docker.zip"
checksum_sha256: "" checksum_sha256: ""
tags: tags:
- containers - containers
@@ -68,12 +68,12 @@ plugins:
- name: traefik - name: traefik
version: "1.0.0" version: "1.0.0"
description: "Traefik reverse proxy metrics and access log integration" description: "Traefik reverse proxy metrics and access log integration"
author: "Roundtable" author: "Steward"
license: "MIT" license: "MIT"
min_app_version: "0.1.0" min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins" repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/traefik" homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/traefik"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/traefik-v1.0.0/traefik.zip" 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 checksum_sha256: "" # fill in after running: sha256sum traefik.zip
tags: tags:
- proxy - proxy
@@ -83,12 +83,12 @@ plugins:
- name: unifi - name: unifi
version: "1.0.0" version: "1.0.0"
description: "UniFi Network controller integration — WAN health, devices, clients, DPI" description: "UniFi Network controller integration — WAN health, devices, clients, DPI"
author: "Roundtable" author: "Steward"
license: "MIT" license: "MIT"
min_app_version: "0.1.0" min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins" repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/unifi" homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/unifi"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/unifi-v1.0.0/unifi.zip" download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/unifi-v1.0.0/unifi.zip"
checksum_sha256: "" checksum_sha256: ""
tags: tags:
- network - network
@@ -98,12 +98,12 @@ plugins:
- name: host_agent - name: host_agent
version: "1.0.0" version: "1.0.0"
description: "Remote Linux host resource monitoring via a lightweight Python push agent" description: "Remote Linux host resource monitoring via a lightweight Python push agent"
author: "Roundtable" author: "Steward"
license: "MIT" license: "MIT"
min_app_version: "0.1.0" min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins" repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/host_agent" homepage: "https://git.fabledsword.com/bvandeusen/Steward-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" download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
checksum_sha256: "" checksum_sha256: ""
tags: tags:
- host - host
+5 -5
View File
@@ -1,12 +1,12 @@
# Plugin System Overview # 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 ## 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: For each plugin listed as `enabled: true` in the `PLUGINS` config:
@@ -57,7 +57,7 @@ description: "Does a thing"
# Optional # Optional
author: "Your Name" 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 # Default config — merged with user overrides at runtime
# Access at runtime via: app.config["PLUGINS"]["myplugin"]["my_setting"] # 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. 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 ```python
from roundtable.core.scheduler import ScheduledTask from steward.core.scheduler import ScheduledTask
def get_scheduled_tasks(): def get_scheduled_tasks():
app = _app 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 1. Adding a `GET /widget` route to its blueprint that returns an HTMX HTML fragment
2. The dashboard template polling that endpoint with HTMX 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) ## 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 ```python
# plugins/myplugin/models.py # plugins/myplugin/models.py
@@ -55,7 +55,7 @@ import uuid
from datetime import datetime from datetime import datetime
from sqlalchemy import String, Float, DateTime from sqlalchemy import String, Float, DateTime
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from roundtable.models.base import Base from steward.models.base import Base
class MyPluginMetric(Base): class MyPluginMetric(Base):
@@ -79,7 +79,7 @@ Generate the initial migration:
# From the project root # From the project root
alembic --config alembic.ini revision \ alembic --config alembic.ini revision \
--autogenerate \ --autogenerate \
--head=roundtable@head \ --head=steward@head \
--branch-label=myplugin \ --branch-label=myplugin \
-m "myplugin initial" -m "myplugin initial"
``` ```
@@ -106,8 +106,8 @@ Keep task logic in a separate file so `__init__.py` stays clean.
# plugins/myplugin/scheduler.py # plugins/myplugin/scheduler.py
from __future__ import annotations from __future__ import annotations
import logging import logging
from roundtable.core.scheduler import ScheduledTask from steward.core.scheduler import ScheduledTask
from roundtable.core.alerts import record_metric from steward.core.alerts import record_metric
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -174,8 +174,8 @@ async def _fetch_value(url: str) -> float:
```python ```python
# plugins/myplugin/routes.py # plugins/myplugin/routes.py
from quart import Blueprint, current_app, render_template from quart import Blueprint, current_app, render_template
from roundtable.auth.middleware import require_role from steward.auth.middleware import require_role
from roundtable.models.users import UserRole from steward.models.users import UserRole
from .models import MyPluginMetric from .models import MyPluginMetric
myplugin_bp = Blueprint("myplugin", __name__, template_folder="templates") myplugin_bp = Blueprint("myplugin", __name__, template_folder="templates")
@@ -215,7 +215,7 @@ Templates live in `plugins/myplugin/templates/myplugin/` (the extra nesting avoi
```html ```html
{# plugins/myplugin/templates/myplugin/index.html #} {# plugins/myplugin/templates/myplugin/index.html #}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}My Plugin — Roundtable{% endblock %} {% block title %}My Plugin — Steward{% endblock %}
{% block content %} {% block content %}
<div class="page-title">My Plugin</div> <div class="page-title">My Plugin</div>
{% for row in rows %} {% 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. `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 ```python
from roundtable.core.alerts import record_metric from steward.core.alerts import record_metric
# Must be inside an active transaction # Must be inside an active transaction
async with session.begin(): async with session.begin():
@@ -302,11 +302,11 @@ async with session.begin():
## Auth in Routes ## Auth in Routes
Use the `@require_role` decorator from `roundtable.auth.middleware`: Use the `@require_role` decorator from `steward.auth.middleware`:
```python ```python
from roundtable.auth.middleware import require_role from steward.auth.middleware import require_role
from roundtable.models.users import UserRole from steward.models.users import UserRole
@myplugin_bp.get("/admin-only") @myplugin_bp.get("/admin-only")
@require_role(UserRole.admin) @require_role(UserRole.admin)
@@ -325,13 +325,13 @@ Role hierarchy: `admin > operator > viewer`. Requiring `viewer` grants access to
## Publishing to the Catalog ## 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 ### Repo layout
``` ```
roundtable-plugins/ steward-plugins/
├── index.yaml ← catalog index — the only file Roundtable fetches ├── index.yaml ← catalog index — the only file Steward fetches
├── myplugin/ ├── myplugin/
│ ├── plugin.yaml │ ├── plugin.yaml
│ ├── __init__.py │ ├── __init__.py
@@ -381,7 +381,7 @@ myplugin.zip
Generate the zip and its checksum: Generate the zip and its checksum:
```bash ```bash
cd roundtable-plugins cd steward-plugins
zip -r myplugin.zip myplugin/ zip -r myplugin.zip myplugin/
sha256sum myplugin.zip # paste this into index.yaml checksum_sha256 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 | | What | File | Function / Class |
|---|---|---| |---|---|---|
| App factory | `roundtable/app.py` | `create_app()` | | App factory | `steward/app.py` | `create_app()` |
| CLI entry point | `roundtable/cli.py` | `main()` | | CLI entry point | `steward/cli.py` | `main()` |
| Bootstrap config loading | `roundtable/config.py` | `load_bootstrap()` | | Bootstrap config loading | `steward/config.py` | `load_bootstrap()` |
| Secret key resolution | `roundtable/config.py` | `_resolve_secret_key()` | | Secret key resolution | `steward/config.py` | `_resolve_secret_key()` |
| DB engine init | `roundtable/database.py` | `init_db()` | | DB engine init | `steward/database.py` | `init_db()` |
| Core migrations | `roundtable/core/migration_runner.py` | `run_core_migrations()` | | Core migrations | `steward/core/migration_runner.py` | `run_core_migrations()` |
| Plugin migrations | `roundtable/core/migration_runner.py` | `run_plugin_migrations()` | | Plugin migrations | `steward/core/migration_runner.py` | `run_plugin_migrations()` |
| Core task registration | `roundtable/app.py` | `_register_core_tasks()` | | Core task registration | `steward/app.py` | `_register_core_tasks()` |
| Scheduler loop | `roundtable/core/scheduler.py` | `start_scheduler()` | | 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 | | What | File | Function |
|---|---|---| |---|---|---|
| All defaults | `roundtable/core/settings.py` | `DEFAULTS` dict | | All defaults | `steward/core/settings.py` | `DEFAULTS` dict |
| Read a setting | `roundtable/core/settings.py` | `get_setting(session, key)` | | Read a setting | `steward/core/settings.py` | `get_setting(session, key)` |
| Write a setting | `roundtable/core/settings.py` | `set_setting(session, key, value)` | | Write a setting | `steward/core/settings.py` | `set_setting(session, key, value)` |
| Read all settings | `roundtable/core/settings.py` | `get_all_settings(session)` | | Read all settings | `steward/core/settings.py` | `get_all_settings(session)` |
| Sync load at startup | `roundtable/core/settings.py` | `load_settings_sync(db_url)` | | Sync load at startup | `steward/core/settings.py` | `load_settings_sync(db_url)` |
| Extract SMTP dict | `roundtable/core/settings.py` | `to_smtp_cfg(settings)` | | Extract SMTP dict | `steward/core/settings.py` | `to_smtp_cfg(settings)` |
| Extract webhook dict | `roundtable/core/settings.py` | `to_webhook_cfg(settings)` | | Extract webhook dict | `steward/core/settings.py` | `to_webhook_cfg(settings)` |
| Extract Ansible dict | `roundtable/core/settings.py` | `to_ansible_cfg(settings)` | | Extract Ansible dict | `steward/core/settings.py` | `to_ansible_cfg(settings)` |
| Extract plugins dict | `roundtable/core/settings.py` | `to_plugins_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 | | What | File | Function |
|---|---|---| |---|---|---|
| Plugin loading | `roundtable/core/plugin_manager.py` | `load_plugins(app)` | | Plugin loading | `steward/core/plugin_manager.py` | `load_plugins(app)` |
| ScheduledTask dataclass | `roundtable/core/scheduler.py` | `ScheduledTask` | | ScheduledTask dataclass | `steward/core/scheduler.py` | `ScheduledTask` |
| Task runner | `roundtable/core/scheduler.py` | `start_scheduler(tasks)` | | 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 | | What | File | Function |
|---|---|---| |---|---|---|
| Write metric + evaluate alerts | `roundtable/core/alerts.py` | `record_metric(session, source_module, resource_name, metric_name, value)` | | Write metric + evaluate alerts | `steward/core/alerts.py` | `record_metric(session, source_module, resource_name, metric_name, value)` |
| Init alert pipeline | `roundtable/core/alerts.py` | `init_alerts(app)` | | Init alert pipeline | `steward/core/alerts.py` | `init_alerts(app)` |
| Rule evaluation | `roundtable/core/alerts.py` | `_evaluate_rule()` (internal) | | Rule evaluation | `steward/core/alerts.py` | `_evaluate_rule()` (internal) |
| Notification dispatch | `roundtable/core/alerts.py` | `_dispatch_notification()` (internal) | | Notification dispatch | `steward/core/alerts.py` | `_dispatch_notification()` (internal) |
| Email + webhook send | `roundtable/core/notifications.py` | `dispatch_notifications()` | | 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 | | What | File | Function |
|---|---|---| |---|---|---|
| Ping a host | `roundtable/monitors/ping.py` | `ping_check(host, session)` | | Ping a host | `steward/monitors/ping.py` | `ping_check(host, session)` |
| DNS check a host | `roundtable/monitors/dns.py` | `dns_check(host, session)` | | DNS check a host | `steward/monitors/dns.py` | `dns_check(host, session)` |
| Data cleanup | `roundtable/core/cleanup.py` | `run_cleanup(app)` | | 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 | | What | File | Function / Class |
|---|---|---| |---|---|---|
| Role-based access decorator | `roundtable/auth/middleware.py` | `@require_role(UserRole.X)` | | Role-based access decorator | `steward/auth/middleware.py` | `@require_role(UserRole.X)` |
| Login / session handling | `roundtable/auth/middleware.py` | `login_user()`, `logout_user()` | | Login / session handling | `steward/auth/middleware.py` | `login_user()`, `logout_user()` |
| User count (for first-run) | `roundtable/auth/middleware.py` | `get_user_count(app)` | | 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 | | Model | File | Table |
|---|---|---| |---|---|---|
| `Host` | `roundtable/models/hosts.py` | `hosts` | | `Host` | `steward/models/hosts.py` | `hosts` |
| `PingResult` | `roundtable/models/monitors.py` | `ping_results` | | `PingResult` | `steward/models/monitors.py` | `ping_results` |
| `DnsResult` | `roundtable/models/monitors.py` | `dns_results` | | `DnsResult` | `steward/models/monitors.py` | `dns_results` |
| `AlertRule` | `roundtable/models/alerts.py` | `alert_rules` | | `AlertRule` | `steward/models/alerts.py` | `alert_rules` |
| `AlertState` | `roundtable/models/alerts.py` | `alert_states` | | `AlertState` | `steward/models/alerts.py` | `alert_states` |
| `AlertEvent` | `roundtable/models/alerts.py` | `alert_events` | | `AlertEvent` | `steward/models/alerts.py` | `alert_events` |
| `PluginMetric` | `roundtable/models/metrics.py` | `plugin_metrics` | | `PluginMetric` | `steward/models/metrics.py` | `plugin_metrics` |
| `AnsibleRun` | `roundtable/models/ansible.py` | `ansible_runs` | | `AnsibleRun` | `steward/models/ansible.py` | `ansible_runs` |
| `User` | `roundtable/models/users.py` | `users` | | `User` | `steward/models/users.py` | `users` |
| `AppSetting` | `roundtable/models/settings.py` | `app_settings` | | `AppSetting` | `steward/models/settings.py` | `app_settings` |
| `TraefikMetric` | `plugins/traefik/models.py` | `traefik_metrics` | | `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 | | URL pattern | Blueprint | File |
|---|---|---| |---|---|---|
| `/` (dashboard) | `dashboard_bp` | `roundtable/dashboard/routes.py` | | `/` (dashboard) | `dashboard_bp` | `steward/dashboard/routes.py` |
| `/auth/login`, `/auth/logout` | `auth_bp` | `roundtable/auth/routes.py` | | `/auth/login`, `/auth/logout` | `auth_bp` | `steward/auth/routes.py` |
| `/hosts/` | `hosts_bp` | `roundtable/hosts/routes.py` | | `/hosts/` | `hosts_bp` | `steward/hosts/routes.py` |
| `/ping/`, `/ping/rows`, `/ping/settings` | `ping_bp` | `roundtable/ping/routes.py` | | `/ping/`, `/ping/rows`, `/ping/settings` | `ping_bp` | `steward/ping/routes.py` |
| `/dns/`, `/dns/rows` | `dns_bp` | `roundtable/dns/routes.py` | | `/dns/`, `/dns/rows` | `dns_bp` | `steward/dns/routes.py` |
| `/alerts/` | `alerts_bp` | `roundtable/alerts/routes.py` | | `/alerts/` | `alerts_bp` | `steward/alerts/routes.py` |
| `/ansible/` | `ansible_bp` | `roundtable/ansible/routes.py` | | `/ansible/` | `ansible_bp` | `steward/ansible/routes.py` |
| `/settings/` | `settings_bp` | `roundtable/settings/routes.py` | | `/settings/` | `settings_bp` | `steward/settings/routes.py` |
| `/plugins/traefik/`, `/plugins/traefik/widget` | `traefik_bp` | `plugins/traefik/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 | | Template | Purpose |
|---|---| |---|---|
| `roundtable/templates/base.html` | Layout, navigation, full CSS design system | | `steward/templates/base.html` | Layout, navigation, full CSS design system |
| `roundtable/templates/dashboard/index.html` | Dashboard with stat strip and widget grid | | `steward/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/) | | `steward/templates/ping/rows.html` | HTMX fragment: ping pill rows (shared by dashboard and /ping/) |
| `roundtable/templates/ping/index.html` | Full /ping/ page | | `steward/templates/ping/index.html` | Full /ping/ page |
| `roundtable/templates/dns/rows.html` | HTMX fragment: DNS status rows | | `steward/templates/dns/rows.html` | HTMX fragment: DNS status rows |
| `roundtable/templates/dns/index.html` | Full /dns/ page | | `steward/templates/dns/index.html` | Full /dns/ page |
| `plugins/traefik/templates/traefik/widget.html` | HTMX fragment: Traefik dashboard widget | | `plugins/traefik/templates/traefik/widget.html` | HTMX fragment: Traefik dashboard widget |
| `plugins/traefik/templates/traefik/index.html` | Full Traefik detail page | | `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 | | 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 | | `plugins/traefik/migrations/versions/` | `traefik_metrics` table |
| `alembic.ini` | Alembic config; `version_locations` lists all migration directories | | `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. > **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. > **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. **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 - Any template with an empty-state message — themed line
**Renamed (PR 2 — package):** **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 - `pyproject.toml` — name, entry point, hatch packages
- `alembic.ini``script_location` - `alembic.ini``script_location`
- `tests/**` — imports (tests not executed, but must not break collection; update imports mechanically) - `tests/**` — imports (tests not executed, but must not break collection; update imports mechanically)
**Modified (PR 3 — config & env):** **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 - `.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):** **Modified (PR 4 — container + user strings):**
- `Dockerfile` — COPY paths, CMD, image labels - `Dockerfile` — COPY paths, CMD, image labels
- `docker-compose.yml` — service name, image, container_name, volumes, env - `docker-compose.yml` — service name, image, container_name, volumes, env
- `entrypoint.sh` — package path for `nut_setup.py` invocation - `entrypoint.sh` — package path for `nut_setup.py` invocation
- `roundtable/templates/base.html` — wordmark text "Fabled Scryer" → "Roundtable", `<title>` - `steward/templates/base.html` — wordmark text "Fabled Scryer" → "Steward", `<title>`
- `roundtable/templates/auth/login.html` — any residual "Fabled Scryer" text - `steward/templates/auth/login.html` — any residual "Fabled Scryer" text
- `README.md` — first-page references (full doc sweep is PR 5) - `README.md` — first-page references (full doc sweep is PR 5)
**Modified (PR 5 — docs):** **Modified (PR 5 — docs):**
- `README.md`, `docs/**/*.md` - `README.md`, `docs/**/*.md`
**Modified (PR 6 — cleanup):** **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** - [ ] **Step 1: Change the title block**
```html ```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. 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 ```html
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Not Found — Roundtable{% endblock %} {% block title %}Not Found — Steward{% endblock %}
{% block content %} {% block content %}
<div style="text-align:center; padding: 4rem 2rem; font-family: var(--font-serif);"> <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> <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 ```html
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Error — Roundtable{% endblock %} {% block title %}Error — Steward{% endblock %}
{% block content %} {% block content %}
<div style="text-align:center; padding: 4rem 2rem; font-family: var(--font-serif);"> <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> <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" 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 ### Task 10: Rename the package directory
**Files:** **Files:**
- Rename: `fabledscryer/``roundtable/` - Rename: `fabledscryer/``steward/`
- [ ] **Step 1: Rename the directory** - [ ] **Step 1: Rename the directory**
```bash ```bash
git mv fabledscryer roundtable git mv fabledscryer steward
``` ```
- [ ] **Step 2: Confirm the move** - [ ] **Step 2: Confirm the move**
```bash ```bash
ls roundtable/ | head ls steward/ | head
git status | 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** - [ ] **Step 3: Commit**
```bash ```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:** **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** - [ ] **Step 1: Find every remaining `fabledscryer` reference in Python code**
Run: Run:
```bash ```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. 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** - [ ] **Step 2: Run a safe codemod across Python files**
```bash ```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** - [ ] **Step 3: Spot-check a few critical files**
```bash ```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** - [ ] **Step 4: Commit**
```bash ```bash
git add -A git add -A
git commit -m "refactor: update imports fabledscryer → roundtable" git commit -m "refactor: update imports fabledscryer → steward"
``` ```
### Task 12: Update `pyproject.toml` ### Task 12: Update `pyproject.toml`
@@ -439,15 +439,15 @@ git commit -m "refactor: update imports fabledscryer → roundtable"
```toml ```toml
[project] [project]
name = "roundtable" name = "steward"
version = "0.1.0" version = "0.1.0"
# ... dependencies unchanged ... # ... dependencies unchanged ...
[project.scripts] [project.scripts]
roundtable = "roundtable.cli:main" steward = "steward.cli:main"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["roundtable"] packages = ["steward"]
``` ```
Leave `[project.optional-dependencies]`, `[tool.pytest.ini_options]`, and all dependency pins untouched. 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 ```bash
git add pyproject.toml 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` ### Task 13: Update `alembic.ini`
@@ -468,7 +468,7 @@ git commit -m "build: rename package to roundtable in pyproject"
```ini ```ini
[alembic] [alembic]
script_location = roundtable/migrations script_location = steward/migrations
prepend_sys_path = . prepend_sys_path = .
version_path_separator = os version_path_separator = os
``` ```
@@ -477,7 +477,7 @@ version_path_separator = os
```bash ```bash
git add alembic.ini 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 ### Task 14: Verify the package installs and imports cleanly
@@ -487,8 +487,8 @@ git commit -m "build: point alembic at roundtable/migrations"
```bash ```bash
python -m venv /tmp/rt-verify python -m venv /tmp/rt-verify
/tmp/rt-verify/bin/pip install -e . /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/python -c "import steward; import steward.app; import steward.cli; print('ok')"
/tmp/rt-verify/bin/roundtable --help /tmp/rt-verify/bin/steward --help
``` ```
Expected: `ok` and CLI help output. No ImportError. Expected: `ok` and CLI help output. No ImportError.
@@ -501,20 +501,20 @@ rm -rf /tmp/rt-verify
- [ ] **Step 3: No commit needed (verification only).** - [ ] **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 ## 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:** **Files:**
- Modify: `roundtable/config.py` - Modify: `steward/config.py`
- [ ] **Step 1: Replace the env var lookups with a fallback helper** - [ ] **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 ```python
def _env_with_fallback(new_name: str, old_name: str) -> str | None: 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 ```python
database_url = ( database_url = (
_env_with_fallback("ROUNDTABLE_DATABASE_URL", "FABLEDSCRYER_DATABASE_URL") _env_with_fallback("STEWARD_DATABASE_URL", "FABLEDSCRYER_DATABASE_URL")
or _env_with_fallback("ROUNDTABLE_DATABASE__URL", "FABLEDSCRYER_DATABASE__URL") or _env_with_fallback("STEWARD_DATABASE__URL", "FABLEDSCRYER_DATABASE__URL")
or raw.get("database", {}).get("url") or raw.get("database", {}).get("url")
) )
if not database_url: if not database_url:
raise ValueError( 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." "or add 'database.url' to config.yaml."
) )
# ... # ...
plugin_dir = ( 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") or raw.get("plugin_dir", "plugins")
) )
``` ```
@@ -557,15 +557,15 @@ And in `_resolve_secret_key`:
```python ```python
from_env = ( 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") 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 ```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. 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** - [ ] **Step 3: Commit**
```bash ```bash
git add roundtable/config.py git add steward/config.py
git commit -m "feat: ROUNDTABLE_* env vars with FABLEDSCRYER_* fallback" git commit -m "feat: STEWARD_* env vars with FABLEDSCRYER_* fallback"
``` ```
### Task 16: Update `.env.example` and `config.example.yaml` ### 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 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. # Steward environment configuration.
# Only ROUNDTABLE_DATABASE_URL is required. Other settings live in the DB. # Only STEWARD_DATABASE_URL is required. Other settings live in the DB.
``` ```
- [ ] **Step 2: Rewrite `config.example.yaml`** - [ ] **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: Replace the header and example strings:
```yaml ```yaml
# Roundtable — Bootstrap Configuration Example # Steward — Bootstrap Configuration Example
# #
# The only REQUIRED setting is the database URL. # The only REQUIRED setting is the database URL.
# Set it via env var (recommended for Docker): # 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 # All other settings are stored in the database and managed via
# the Settings UI at /settings/. # the Settings UI at /settings/.
database: database:
url: "postgresql+asyncpg://roundtable:password@localhost/roundtable" url: "postgresql+asyncpg://steward:password@localhost/steward"
``` ```
- [ ] **Step 3: Commit** - [ ] **Step 3: Commit**
```bash ```bash
git add .env.example config.example.yaml 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 ### 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** - [ ] **Step 1: Run the app with only the new env var**
```bash ```bash
ROUNDTABLE_DATABASE_URL=postgresql+asyncpg://fabledscryer:fabledscryer@localhost/fabledscryer \ STEWARD_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
``` ```
(Use whatever local DB you have; the old db name is fine — data isn't being renamed.) (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 ```bash
FABLEDSCRYER_DATABASE_URL=postgresql+asyncpg://fabledscryer:fabledscryer@localhost/fabledscryer \ 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. Expected: app starts, deprecation warning logged once.
- [ ] **Step 3: Kill processes.** No commit. - [ ] **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 to
```dockerfile ```dockerfile
COPY roundtable/ roundtable/ COPY steward/ steward/
``` ```
Change the CMD: Change the CMD:
```dockerfile ```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. 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 ```bash
git add Dockerfile 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` ### 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** - [ ] **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** - [ ] **Step 2: Commit**
```bash ```bash
git add entrypoint.sh 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` ### Task 20: Update `docker-compose.yml`
@@ -708,10 +708,10 @@ git commit -m "build: entrypoint.sh uses roundtable package path"
```yaml ```yaml
services: services:
roundtable: steward:
build: . build: .
container_name: roundtable container_name: steward
image: roundtable:latest image: steward:latest
ports: ports:
- "5000:5000" - "5000:5000"
volumes: volumes:
@@ -720,7 +720,7 @@ services:
- /mnt/Data/traefik/log:/var/log/traefik:ro - /mnt/Data/traefik/log:/var/log/traefik:ro
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
environment: environment:
- ROUNDTABLE_DATABASE_URL=postgresql+asyncpg://roundtable:roundtable@db/roundtable - STEWARD_DATABASE_URL=postgresql+asyncpg://steward:steward@db/steward
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@@ -729,13 +729,13 @@ services:
db: db:
image: postgres:16-alpine image: postgres:16-alpine
environment: environment:
POSTGRES_USER: roundtable POSTGRES_USER: steward
POSTGRES_PASSWORD: roundtable POSTGRES_PASSWORD: steward
POSTGRES_DB: roundtable POSTGRES_DB: steward
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U roundtable"] test: ["CMD-SHELL", "pg_isready -U steward"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -752,37 +752,37 @@ volumes:
```bash ```bash
git add docker-compose.yml 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 ### Task 21: Flip user-visible wordmark and title
**Files:** **Files:**
- Modify: `roundtable/templates/base.html` - Modify: `steward/templates/base.html`
- [ ] **Step 1: Update the `<title>` block (around line 6)** - [ ] **Step 1: Update the `<title>` block (around line 6)**
```html ```html
<title>{% block title %}Roundtable{% endblock %}</title> <title>{% block title %}Steward{% endblock %}</title>
``` ```
- [ ] **Step 2: Update the nav wordmark (around line 214)** - [ ] **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** - [ ] **Step 3: Grep for any other user-visible "Fabled Scryer" strings in templates**
```bash ```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** - [ ] **Step 4: Commit**
```bash ```bash
git add roundtable/templates/ git add steward/templates/
git commit -m "copy: flip visible wordmark → Roundtable" git commit -m "copy: flip visible wordmark → Steward"
``` ```
### Task 22: Manual container verification ### Task 22: Manual container verification
@@ -795,8 +795,8 @@ docker compose up --build
``` ```
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
- Container named `roundtable` runs - Container named `steward` runs
- Browser shows "Roundtable" in tab title and nav - Browser shows "Steward" in tab title and nav
- Login, dashboard, and error pages all render with the new palette and wordmark - Login, dashboard, and error pages all render with the new palette and wordmark
- Logs show no import errors and no unhandled deprecation warnings - Logs show no import errors and no unhandled deprecation warnings
@@ -806,7 +806,7 @@ docker compose up --build
docker compose down 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 ```bash
grep -rl "fabledscryer\|FabledScryer\|Fabled Scryer" README.md docs/ \ grep -rl "fabledscryer\|FabledScryer\|Fabled Scryer" README.md docs/ \
| xargs sed -i \ | xargs sed -i \
-e 's/FabledScryer/Roundtable/g' \ -e 's/FabledScryer/Steward/g' \
-e 's/fabledscryer/roundtable/g' \ -e 's/fabledscryer/steward/g' \
-e 's/Fabled Scryer/Roundtable/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: 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 ```bash
git add README.md docs/ 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 ### 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** - [ ] **Step 2: Update your local remote URL**
@@ -862,13 +862,13 @@ git remote -v
```bash ```bash
# from the parent dir # from the parent dir
cd .. cd ..
mv FabledScryer Roundtable mv FabledScryer Steward
cd Roundtable cd Steward
``` ```
- [ ] **Step 4: No commit.** - [ ] **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 ### Task 25: Remove env var fallback shim
**Files:** **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** - [ ] **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** - [ ] **Step 3: Commit**
```bash ```bash
git add roundtable/config.py git add steward/config.py
git commit -m "refactor: remove FABLEDSCRYER_* env var fallback shim" 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. - 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. - 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). - 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 **Date:** 2026-04-13
**Status:** Approved for planning **Status:** Approved for planning
## 1. Identity ## 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:** **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. - *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 ## 2. Visual System
@@ -78,7 +78,7 @@ Candlelit glow — a soft, warm radial wash behind the main content area. Replac
### Wordmark ### 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 ## 3. Voice & Copy
@@ -100,21 +100,21 @@ Candlelit glow — a soft, warm radial wash behind the main content area. Replac
## 4. Rename Mechanics ## 4. Rename Mechanics
**Code & packaging** **Code & packaging**
- `fabledscryer/` package directory → `roundtable/` - `fabledscryer/` package directory → `steward/`
- All `from fabledscryer...` / `import fabledscryer...``roundtable` - All `from fabledscryer...` / `import fabledscryer...``steward`
- `pyproject.toml`: project name, CLI entry point (`fabledscryer = "fabledscryer.cli:main"``roundtable = "roundtable.cli:main"`), tool sections - `pyproject.toml`: project name, CLI entry point (`fabledscryer = "fabledscryer.cli:main"``steward = "steward.cli:main"`), tool sections
- `__init__.py` package metadata - `__init__.py` package metadata
**Runtime config** **Runtime config**
- Env vars: `FABLEDSCRYER_*``ROUNDTABLE_*` - Env vars: `FABLEDSCRYER_*``STEWARD_*`
- Residual `FABLEDNETMON_*` env vars removed - 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 - Log file names, systemd unit names if any
**Container & deploy** **Container & deploy**
- `Dockerfile` labels, workdir - `Dockerfile` labels, workdir
- `docker-compose.yml` service name, image tag, volume names, `container_name` - `docker-compose.yml` service name, image tag, volume names, `container_name`
- Published image: `fabledscryer:latest``roundtable:latest` - Published image: `fabledscryer:latest``steward:latest`
**Database** **Database**
- Alembic `script_location` and any customized version table - 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. 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** **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** **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** **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). `Dockerfile`, `docker-compose.yml` service/image/volume/container names. Published image tag switch. Template wordmark + `<title>` + meta (the last user-visible string flip).
+1 -1
View File
@@ -1,5 +1,5 @@
#!/bin/sh #!/bin/sh
# Roundtable container entrypoint. # Steward container entrypoint.
# #
# Drops from root to the 'app' user via gosu, then runs the command. # Drops from root to the 'app' user via gosu, then runs the command.
# The container starts as root only so /data permissions can be fixed up # The container starts as root only so /data permissions can be fixed up
+3 -3
View File
@@ -3,7 +3,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "roundtable" name = "steward"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
@@ -32,11 +32,11 @@ dev = [
] ]
[project.scripts] [project.scripts]
roundtable = "roundtable.cli:main" steward = "steward.cli:main"
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto" asyncio_mode = "auto"
testpaths = ["tests"] testpaths = ["tests"]
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["roundtable"] packages = ["steward"]
-39
View File
@@ -1,39 +0,0 @@
__version__ = "0.1.0"
# Compatibility shim: allow `import fabledscryer[.x.y]` to resolve to
# `roundtable[.x.y]`. Lets unmodified plugin repos keep working during the
# rebrand transition. Remove once all plugins migrate to `roundtable`.
import sys as _sys
import importlib as _importlib
import importlib.abc as _abc
import importlib.util as _util
_ALIAS = "fabledscryer"
class _RoundtableAliasLoader(_abc.Loader):
def __init__(self, real_name):
self._real_name = real_name
def create_module(self, spec):
return _importlib.import_module(self._real_name)
def exec_module(self, module):
pass
class _RoundtableAliasFinder(_abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname != _ALIAS and not fullname.startswith(_ALIAS + "."):
return None
real_name = "roundtable" + fullname[len(_ALIAS):]
if _util.find_spec(real_name) is None:
return None
return _util.spec_from_loader(
fullname, _RoundtableAliasLoader(real_name)
)
if not any(isinstance(f, _RoundtableAliasFinder) for f in _sys.meta_path):
_sys.meta_path.insert(0, _RoundtableAliasFinder())
_sys.modules.setdefault(_ALIAS, _sys.modules[__name__])
-1
View File
@@ -1 +0,0 @@
# roundtable/settings/__init__.py
+1
View File
@@ -0,0 +1 @@
__version__ = "0.1.0"
@@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timezone from datetime import datetime, timezone
from quart import Blueprint, render_template, request, redirect, url_for, current_app, session from quart import Blueprint, render_template, request, redirect, url_for, current_app, session
from roundtable.core.audit import log_audit from steward.core.audit import log_audit
from sqlalchemy import select from sqlalchemy import select
from roundtable.auth.middleware import require_role from steward.auth.middleware import require_role
from roundtable.models.alerts import AlertRule, AlertState, AlertStateEnum, AlertOperator, MaintenanceWindow from steward.models.alerts import AlertRule, AlertState, AlertStateEnum, AlertOperator, MaintenanceWindow
from roundtable.models.hosts import Host from steward.models.hosts import Host
from roundtable.models.metrics import PluginMetric from steward.models.metrics import PluginMetric
from roundtable.models.users import UserRole from steward.models.users import UserRole
alerts_bp = Blueprint("alerts", __name__, url_prefix="/alerts") alerts_bp = Blueprint("alerts", __name__, url_prefix="/alerts")
@@ -1,4 +1,4 @@
# roundtable/ansible/executor.py # steward/ansible/executor.py
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging import logging
@@ -77,7 +77,7 @@ async def start_run(
async def _flush(final_status: str | None = None) -> None: async def _flush(final_status: str | None = None) -> None:
nonlocal lines_since_flush, last_flush_at nonlocal lines_since_flush, last_flush_at
from sqlalchemy import update from sqlalchemy import update
from roundtable.models.ansible import AnsibleRun, AnsibleRunStatus from steward.models.ansible import AnsibleRun, AnsibleRunStatus
values: dict = {"output": db_output} values: dict = {"output": db_output}
if final_status is not None: if final_status is not None:
@@ -1,4 +1,4 @@
# roundtable/ansible/routes.py # steward/ansible/routes.py
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import uuid import uuid
@@ -11,10 +11,10 @@ from quart import (
) )
from sqlalchemy import select, update from sqlalchemy import select, update
from roundtable.ansible import executor, sources as src_module from steward.ansible import executor, sources as src_module
from roundtable.auth.middleware import require_role from steward.auth.middleware import require_role
from roundtable.models.ansible import AnsibleRun, AnsibleRunStatus from steward.models.ansible import AnsibleRun, AnsibleRunStatus
from roundtable.models.users import UserRole from steward.models.users import UserRole
ansible_bp = Blueprint("ansible", __name__, url_prefix="/ansible") ansible_bp = Blueprint("ansible", __name__, url_prefix="/ansible")
@@ -17,7 +17,7 @@ def get_sources(ansible_cfg: dict) -> list[dict]:
Config structure in config.yaml:: Config structure in config.yaml::
ansible: ansible:
cache_dir: /var/cache/roundtable/ansible cache_dir: /var/cache/steward/ansible
sources: sources:
- name: my-playbooks - name: my-playbooks
type: local type: local
@@ -29,7 +29,7 @@ def get_sources(ansible_cfg: dict) -> list[dict]:
pull_interval_seconds: 3600 pull_interval_seconds: 3600
""" """
sources = ansible_cfg.get("sources", []) sources = ansible_cfg.get("sources", [])
cache_dir = ansible_cfg.get("cache_dir", "/var/cache/roundtable/ansible") cache_dir = ansible_cfg.get("cache_dir", "/var/cache/steward/ansible")
result = [] result = []
for src in sources: for src in sources:
src_type = src.get("type", "local") src_type = src.get("type", "local")
+1 -1
View File
@@ -1,4 +1,4 @@
# roundtable/app.py # steward/app.py
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from pathlib import Path from pathlib import Path
@@ -2,9 +2,9 @@ from __future__ import annotations
import json import json
from quart import Blueprint, render_template, request, current_app from quart import Blueprint, render_template, request, current_app
from sqlalchemy import select from sqlalchemy import select
from roundtable.auth.middleware import require_role from steward.auth.middleware import require_role
from roundtable.models.audit import AuditEvent from steward.models.audit import AuditEvent
from roundtable.models.users import UserRole from steward.models.users import UserRole
audit_bp = Blueprint("audit", __name__, url_prefix="/audit") audit_bp = Blueprint("audit", __name__, url_prefix="/audit")
@@ -1,4 +1,4 @@
"""roundtable/auth/ldap_auth.py """steward/auth/ldap_auth.py
LDAP authentication helper. Requires the optional `ldap3` package. LDAP authentication helper. Requires the optional `ldap3` package.
Falls back gracefully if ldap3 is not installed. Falls back gracefully if ldap3 is not installed.
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import functools import functools
from quart import session, redirect, url_for, abort, g from quart import session, redirect, url_for, abort, g
from roundtable.models.users import UserRole from steward.models.users import UserRole
_ROLE_ORDER = [UserRole.viewer, UserRole.operator, UserRole.admin] _ROLE_ORDER = [UserRole.viewer, UserRole.operator, UserRole.admin]
@@ -1,4 +1,4 @@
"""roundtable/auth/oidc.py """steward/auth/oidc.py
OIDC Authorization Code flow helpers using httpx. OIDC Authorization Code flow helpers using httpx.
No extra dependencies works with any OIDC provider (Authentik, Keycloak, etc.). No extra dependencies works with any OIDC provider (Authentik, Keycloak, etc.).
@@ -2,8 +2,8 @@ from __future__ import annotations
import bcrypt import bcrypt
from quart import Blueprint, render_template, request, redirect, url_for, session from quart import Blueprint, render_template, request, redirect, url_for, session
from sqlalchemy import select, func from sqlalchemy import select, func
from roundtable.auth.middleware import login_user, logout_user from steward.auth.middleware import login_user, logout_user
from roundtable.models.users import User, UserRole from steward.models.users import User, UserRole
auth_bp = Blueprint("auth", __name__) auth_bp = Blueprint("auth", __name__)
@@ -15,7 +15,7 @@ async def _provision_external_user(app, username: str, email: str, role_str: str
Their role is updated on every login to reflect current IdP group membership. Their role is updated on every login to reflect current IdP group membership.
""" """
import secrets import secrets
from roundtable.models.users import UserRole from steward.models.users import UserRole
async with app.db_sessionmaker() as db: async with app.db_sessionmaker() as db:
result = await db.execute(select(User).where(User.username == username)) result = await db.execute(select(User).where(User.username == username))
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
@@ -60,7 +60,7 @@ async def login():
@auth_bp.post("/login") @auth_bp.post("/login")
async def login_post(): async def login_post():
from quart import current_app from quart import current_app
from roundtable.core.audit import log_audit from steward.core.audit import log_audit
form = await request.form form = await request.form
username = form.get("username", "").strip() username = form.get("username", "").strip()
password_str = form.get("password", "") password_str = form.get("password", "")
@@ -77,7 +77,7 @@ async def login_post():
# Try LDAP fallback if enabled # Try LDAP fallback if enabled
ldap_cfg = current_app.config.get("LDAP", {}) ldap_cfg = current_app.config.get("LDAP", {})
if ldap_cfg.get("enabled"): if ldap_cfg.get("enabled"):
from roundtable.auth.ldap_auth import ldap_authenticate from steward.auth.ldap_auth import ldap_authenticate
ldap_info = await ldap_authenticate(ldap_cfg, username, password_str) ldap_info = await ldap_authenticate(ldap_cfg, username, password_str)
if ldap_info: if ldap_info:
user = await _provision_external_user( user = await _provision_external_user(
@@ -99,7 +99,7 @@ async def login_post():
@auth_bp.get("/login/oidc") @auth_bp.get("/login/oidc")
async def login_oidc(): async def login_oidc():
from quart import current_app from quart import current_app
from roundtable.auth.oidc import get_discovery, build_authorize_url, generate_state from steward.auth.oidc import get_discovery, build_authorize_url, generate_state
oidc_cfg = current_app.config.get("OIDC", {}) oidc_cfg = current_app.config.get("OIDC", {})
if not oidc_cfg.get("enabled") or not oidc_cfg.get("discovery_url"): if not oidc_cfg.get("enabled") or not oidc_cfg.get("discovery_url"):
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
@@ -123,8 +123,8 @@ async def login_oidc():
@auth_bp.get("/login/oidc/callback") @auth_bp.get("/login/oidc/callback")
async def login_oidc_callback(): async def login_oidc_callback():
from quart import current_app from quart import current_app
from roundtable.auth.oidc import get_discovery, exchange_code, get_userinfo, map_role from steward.auth.oidc import get_discovery, exchange_code, get_userinfo, map_role
from roundtable.core.audit import log_audit from steward.core.audit import log_audit
oidc_cfg = current_app.config.get("OIDC", {}) oidc_cfg = current_app.config.get("OIDC", {})
error = request.args.get("error") error = request.args.get("error")
@@ -190,7 +190,7 @@ async def setup():
@auth_bp.post("/setup") @auth_bp.post("/setup")
async def setup_post(): async def setup_post():
from quart import current_app from quart import current_app
from roundtable.core.audit import log_audit from steward.core.audit import log_audit
if await get_user_count(current_app) > 0: if await get_user_count(current_app) > 0:
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
form = await request.form form = await request.form
+1 -1
View File
@@ -9,6 +9,6 @@ from .app import create_app
@click.option("--port", default=5000, type=int) @click.option("--port", default=5000, type=int)
@click.option("--debug", is_flag=True, default=False) @click.option("--debug", is_flag=True, default=False)
def main(config: str, host: str, port: int, debug: bool) -> None: def main(config: str, host: str, port: int, debug: bool) -> None:
"""Start the Roundtable server.""" """Start the Steward server."""
app = create_app(config_path=Path(config)) app = create_app(config_path=Path(config))
app.run(host=host, port=port, debug=debug) app.run(host=host, port=port, debug=debug)
+3 -10
View File
@@ -1,4 +1,4 @@
# roundtable/config.py # steward/config.py
from __future__ import annotations from __future__ import annotations
import logging import logging
import os import os
@@ -12,14 +12,7 @@ _SECRET_KEY_FILE = Path("/data/secret.key")
def _env(suffix: str) -> str | None: def _env(suffix: str) -> str | None:
"""Read ROUNDTABLE_<suffix>, falling back to FABLEDSCRYER_<suffix>. return os.environ.get(f"STEWARD_{suffix}")
Allows existing deployments to keep their old env vars working through
the rebrand transition. Remove the fallback once users have migrated.
"""
return os.environ.get(f"ROUNDTABLE_{suffix}") or os.environ.get(
f"FABLEDSCRYER_{suffix}"
)
def load_bootstrap(config_path: Path | str | None = None) -> dict[str, str]: def load_bootstrap(config_path: Path | str | None = None) -> dict[str, str]:
@@ -50,7 +43,7 @@ def load_bootstrap(config_path: Path | str | None = None) -> dict[str, str]:
) )
if not database_url: if not database_url:
raise ValueError( 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." "or add 'database.url' to config.yaml."
) )
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
from sqlalchemy import and_, or_, select from sqlalchemy import and_, or_, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from roundtable.models.alerts import ( from steward.models.alerts import (
AlertEvent, AlertEvent,
AlertOperator, AlertOperator,
AlertRule, AlertRule,
@@ -16,7 +16,7 @@ from roundtable.models.alerts import (
AlertStateEnum, AlertStateEnum,
MaintenanceWindow, MaintenanceWindow,
) )
from roundtable.models.metrics import PluginMetric from steward.models.metrics import PluginMetric
if TYPE_CHECKING: if TYPE_CHECKING:
from quart import Quart from quart import Quart
@@ -226,7 +226,7 @@ async def _dispatch_notification(
event_id: str, event_id: str,
) -> None: ) -> None:
"""Run after the transaction commits. Sends notifications and updates the event row.""" """Run after the transaction commits. Sends notifications and updates the event row."""
from roundtable.core.notifications import dispatch_notifications from steward.core.notifications import dispatch_notifications
alert_data = { alert_data = {
"rule_name": rule.name, "rule_name": rule.name,
@@ -1,4 +1,4 @@
"""roundtable/core/audit.py """steward/core/audit.py
Helpers for writing audit log entries. Each call opens its own DB Helpers for writing audit log entries. Each call opens its own DB
session so audit events are committed independently of the calling session so audit events are committed independently of the calling
@@ -23,7 +23,7 @@ async def log_audit(
detail: dict | None = None, detail: dict | None = None,
) -> None: ) -> None:
"""Write one audit event. Never raises — failures are logged and swallowed.""" """Write one audit event. Never raises — failures are logged and swallowed."""
from roundtable.models.audit import AuditEvent from steward.models.audit import AuditEvent
try: try:
async with app.db_sessionmaker() as db: async with app.db_sessionmaker() as db:
async with db.begin(): async with db.begin():
@@ -5,9 +5,9 @@ from typing import TYPE_CHECKING
from sqlalchemy import delete from sqlalchemy import delete
from roundtable.models.monitors import DnsResult, PingResult from steward.models.monitors import DnsResult, PingResult
from roundtable.models.metrics import PluginMetric from steward.models.metrics import PluginMetric
from roundtable.models.ansible import AnsibleRun from steward.models.ansible import AnsibleRun
if TYPE_CHECKING: if TYPE_CHECKING:
from quart import Quart from quart import Quart
@@ -1,4 +1,4 @@
# roundtable/core/migration_runner.py # steward/core/migration_runner.py
from __future__ import annotations from __future__ import annotations
import logging import logging
from pathlib import Path from pathlib import Path
@@ -33,8 +33,8 @@ async def dispatch_notifications(
async def _send_email(cfg: dict, alert: dict) -> dict: async def _send_email(cfg: dict, alert: dict) -> dict:
try: try:
msg = EmailMessage() msg = EmailMessage()
msg["Subject"] = f"[Roundtable] {alert['state']}{alert['rule_name']}" msg["Subject"] = f"[Steward] {alert['state']}{alert['rule_name']}"
msg["From"] = cfg.get("username", "roundtable@localhost") msg["From"] = cfg.get("username", "steward@localhost")
msg["To"] = ", ".join(cfg["recipients"]) msg["To"] = ", ".join(cfg["recipients"])
msg.set_content( msg.set_content(
f"Alert: {alert['state']}\n" f"Alert: {alert['state']}\n"
@@ -1,4 +1,4 @@
# roundtable/core/plugin_index.py # steward/core/plugin_index.py
"""Remote plugin catalog: fetch, parse, and cache the plugin index.yaml.""" """Remote plugin catalog: fetch, parse, and cache the plugin index.yaml."""
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -1,4 +1,4 @@
# roundtable/core/plugin_manager.py # steward/core/plugin_manager.py
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import importlib import importlib
@@ -41,7 +41,7 @@ def _import_plugin(name: str, plugin_path: Path):
""" """
import importlib.util import importlib.util
module_key = f"_roundtable_plugin_{name}" module_key = f"_steward_plugin_{name}"
if module_key in sys.modules: if module_key in sys.modules:
return sys.modules[module_key] return sys.modules[module_key]
@@ -78,7 +78,7 @@ def load_plugins(app: "Quart") -> None:
7. Register blueprint at /plugins/<name>/ if get_blueprint() exists 7. Register blueprint at /plugins/<name>/ if get_blueprint() exists
8. Append get_scheduled_tasks() results to app._task_registry 8. Append get_scheduled_tasks() results to app._task_registry
""" """
import roundtable import steward
plugin_dir = Path(app.config["PLUGIN_DIR"]) plugin_dir = Path(app.config["PLUGIN_DIR"])
plugins_cfg: dict = app.config["PLUGINS"] plugins_cfg: dict = app.config["PLUGINS"]
@@ -126,13 +126,13 @@ def load_plugins(app: "Quart") -> None:
min_ver = meta.get("min_app_version") min_ver = meta.get("min_app_version")
if min_ver: if min_ver:
try: try:
if Version(roundtable.__version__) < Version(min_ver): if Version(steward.__version__) < Version(min_ver):
_FAILED_PLUGINS[name] = ( _FAILED_PLUGINS[name] = (
f"Requires roundtable>={min_ver}, running {roundtable.__version__}" f"Requires steward>={min_ver}, running {steward.__version__}"
) )
logger.error( logger.error(
"Plugin %r: requires roundtable>=%s but running %s, skipping", "Plugin %r: requires steward>=%s but running %s, skipping",
name, min_ver, roundtable.__version__, name, min_ver, steward.__version__,
) )
continue continue
except Exception as exc: except Exception as exc:
@@ -267,7 +267,7 @@ async def download_and_install_plugin(
# Detect a single top-level directory to strip. Handles: # Detect a single top-level directory to strip. Handles:
# name/ (clean plugin zip) # name/ (clean plugin zip)
# name-v1.0.0/ (GitHub release tag archive) # name-v1.0.0/ (GitHub release tag archive)
# roundtable-plugins-main/name/ (whole-repo archive) # steward-plugins-main/name/ (whole-repo archive)
# Strategy: if there is exactly one top-level item and it is a # Strategy: if there is exactly one top-level item and it is a
# directory whose name starts with the plugin name (case-insensitive), # directory whose name starts with the plugin name (case-insensitive),
# strip it. Otherwise extract flat into dest. # strip it. Otherwise extract flat into dest.
@@ -302,7 +302,7 @@ async def download_and_install_plugin(
try: try:
mdir = plugin_dir / name / "migrations" mdir = plugin_dir / name / "migrations"
if mdir.exists(): if mdir.exists():
from roundtable.core.migration_runner import ( from steward.core.migration_runner import (
run_plugin_migrations, run_plugin_migrations,
discover_all_plugin_migration_dirs, discover_all_plugin_migration_dirs,
) )
@@ -325,7 +325,7 @@ def hot_reload_plugin(app: "Quart", name: str) -> tuple[bool, str]:
Returns (success, message). Returns (success, message).
""" """
import roundtable import steward
if name in _LOADED_PLUGINS: if name in _LOADED_PLUGINS:
return False, "Plugin already loaded — restart required to apply updates" return False, "Plugin already loaded — restart required to apply updates"
@@ -352,10 +352,10 @@ def hot_reload_plugin(app: "Quart", name: str) -> tuple[bool, str]:
min_ver = meta.get("min_app_version") min_ver = meta.get("min_app_version")
if min_ver: if min_ver:
try: try:
if Version(roundtable.__version__) < Version(min_ver): if Version(steward.__version__) < Version(min_ver):
return False, ( return False, (
f"Plugin requires roundtable>={min_ver} " f"Plugin requires steward>={min_ver} "
f"but running {roundtable.__version__}" f"but running {steward.__version__}"
) )
except Exception: except Exception:
return False, f"Invalid min_app_version: {min_ver!r}" return False, f"Invalid min_app_version: {min_ver!r}"
@@ -381,7 +381,7 @@ def hot_reload_plugin(app: "Quart", name: str) -> tuple[bool, str]:
mdir = plugin_path / "migrations" mdir = plugin_path / "migrations"
if mdir.exists(): if mdir.exists():
try: try:
from roundtable.core.migration_runner import ( from steward.core.migration_runner import (
run_plugin_migrations, run_plugin_migrations,
discover_all_plugin_migration_dirs, discover_all_plugin_migration_dirs,
) )
@@ -1,4 +1,4 @@
"""roundtable/core/reports.py """steward/core/reports.py
Weekly digest report: uptime, active alerts, alert events, top metrics. Weekly digest report: uptime, active alerts, alert events, top metrics.
Called by the scheduler (hourly check) or triggered manually from Settings. Called by the scheduler (hourly check) or triggered manually from Settings.
@@ -13,10 +13,10 @@ from email.message import EmailMessage
from sqlalchemy import and_, case, func, select from sqlalchemy import and_, case, func, select
from roundtable.models.alerts import AlertRule, AlertState, AlertStateEnum, AlertEvent from steward.models.alerts import AlertRule, AlertState, AlertStateEnum, AlertEvent
from roundtable.models.hosts import Host from steward.models.hosts import Host
from roundtable.models.metrics import PluginMetric from steward.models.metrics import PluginMetric
from roundtable.models.monitors import PingResult, PingStatus from steward.models.monitors import PingResult, PingStatus
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -152,7 +152,7 @@ def _render_report_text(data: dict) -> str:
now = data["generated_at"] now = data["generated_at"]
lines: list[str] = [] lines: list[str] = []
lines.append("Roundtable — Weekly Report") lines.append("Steward — Weekly Report")
lines.append(f"Generated: {now.strftime('%Y-%m-%d %H:%M UTC')}") lines.append(f"Generated: {now.strftime('%Y-%m-%d %H:%M UTC')}")
lines.append("") lines.append("")
@@ -204,7 +204,7 @@ def _render_report_text(data: dict) -> str:
lines.append("") lines.append("")
lines.append("" * 40) lines.append("" * 40)
lines.append("Roundtable — https://git.fabledsword.com/bvandeusen/Roundtable") lines.append("Steward — https://git.fabledsword.com/bvandeusen/Steward")
return "\n".join(lines) return "\n".join(lines)
@@ -222,17 +222,17 @@ async def send_report(app) -> tuple[bool, str]:
body = _render_report_text(data) body = _render_report_text(data)
date_str = data["generated_at"].strftime("%Y-%m-%d") date_str = data["generated_at"].strftime("%Y-%m-%d")
subject = f"[Roundtable] Weekly Report — {date_str}" subject = f"[Steward] Weekly Report — {date_str}"
msg = EmailMessage() msg = EmailMessage()
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = smtp_cfg.get("username", "roundtable@localhost") msg["From"] = smtp_cfg.get("username", "steward@localhost")
msg["To"] = ", ".join(smtp_cfg["recipients"]) msg["To"] = ", ".join(smtp_cfg["recipients"])
msg.set_content(body) msg.set_content(body)
try: try:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
from roundtable.core.notifications import _smtp_send_sync from steward.core.notifications import _smtp_send_sync
await loop.run_in_executor(None, _smtp_send_sync, smtp_cfg, msg) await loop.run_in_executor(None, _smtp_send_sync, smtp_cfg, msg)
logger.info("Weekly report sent to %s", smtp_cfg["recipients"]) logger.info("Weekly report sent to %s", smtp_cfg["recipients"])
return True, f"Report sent to {', '.join(smtp_cfg['recipients'])}." return True, f"Report sent to {', '.join(smtp_cfg['recipients'])}."
@@ -243,7 +243,7 @@ async def send_report(app) -> tuple[bool, str]:
async def check_and_send(app) -> None: async def check_and_send(app) -> None:
"""Hourly scheduler hook: send if day/hour match and not sent recently.""" """Hourly scheduler hook: send if day/hour match and not sent recently."""
from roundtable.core.settings import get_setting, set_setting from steward.core.settings import get_setting, set_setting
async with app.db_sessionmaker() as db: async with app.db_sessionmaker() as db:
enabled = await get_setting(db, "reports.enabled") enabled = await get_setting(db, "reports.enabled")
@@ -1,15 +1,15 @@
# roundtable/core/settings.py # steward/core/settings.py
"""DB-backed application settings. """DB-backed application settings.
Keys use dotted notation (e.g. "smtp.host"). Keys use dotted notation (e.g. "smtp.host").
Values are JSON-encoded in the DB. Values are JSON-encoded in the DB.
Usage in create_app() (before event loop): Usage in create_app() (before event loop):
from roundtable.core.settings import load_settings_sync from steward.core.settings import load_settings_sync
settings = load_settings_sync(db_url) settings = load_settings_sync(db_url)
Usage at runtime (inside async handlers): Usage at runtime (inside async handlers):
from roundtable.core.settings import get_setting, set_setting from steward.core.settings import get_setting, set_setting
async with app.db_sessionmaker() as session: async with app.db_sessionmaker() as session:
value = await get_setting(session, "smtp.host") value = await get_setting(session, "smtp.host")
await set_setting(session, "smtp.host", "mail.example.com") await set_setting(session, "smtp.host", "mail.example.com")
@@ -24,7 +24,7 @@ from typing import Any
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from roundtable.models.settings import AppSetting from steward.models.settings import AppSetting
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -53,7 +53,7 @@ DEFAULTS: dict[str, Any] = {
"ansible.sources": [], "ansible.sources": [],
"ping.threshold.good_ms": 50, "ping.threshold.good_ms": 50,
"ping.threshold.warn_ms": 200, "ping.threshold.warn_ms": 200,
"plugins.index_url": "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/raw/branch/main/index.yaml", "plugins.index_url": "https://git.fabledsword.com/bvandeusen/Steward-plugins/raw/branch/main/index.yaml",
# OIDC single-sign-on # OIDC single-sign-on
"oidc.enabled": False, "oidc.enabled": False,
"oidc.discovery_url": "", "oidc.discovery_url": "",
@@ -209,7 +209,7 @@ def load_settings_sync(db_url: str) -> dict[str, Any]:
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
def public_base_url(request) -> str: def public_base_url(request) -> str:
"""Return the externally-reachable base URL for this Roundtable instance. """Return the externally-reachable base URL for this Steward instance.
Prefers the admin-configured 'general.public_base_url' setting (cached in Prefers the admin-configured 'general.public_base_url' setting (cached in
current_app.config['PUBLIC_BASE_URL']) when set. Falls back to current_app.config['PUBLIC_BASE_URL']) when set. Falls back to
@@ -1,4 +1,4 @@
# roundtable/core/time_range.py # steward/core/time_range.py
"""Shared time-range utilities for scoping queries and sparkline data.""" """Shared time-range utilities for scoping queries and sparkline data."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@@ -1,4 +1,4 @@
# roundtable/core/widgets.py # steward/core/widgets.py
# #
# Central widget registry. Each entry describes one widget type that can be # Central widget registry. Each entry describes one widget type that can be
# placed on a dashboard. Plugin widgets declare which plugin must be enabled. # placed on a dashboard. Plugin widgets declare which plugin must be enabled.
@@ -6,13 +6,13 @@ from datetime import datetime, timezone, timedelta
from urllib.parse import urlencode from urllib.parse import urlencode
from quart import Blueprint, render_template, current_app, abort, redirect, request, session from quart import Blueprint, render_template, current_app, abort, redirect, request, session
from sqlalchemy import select, func, update from sqlalchemy import select, func, update
from roundtable.auth.middleware import require_role, current_user_id from steward.auth.middleware import require_role, current_user_id
from roundtable.models.users import UserRole from steward.models.users import UserRole
from roundtable.models.hosts import Host from steward.models.hosts import Host
from roundtable.models.monitors import PingResult, DnsResult from steward.models.monitors import PingResult, DnsResult
from roundtable.models.dashboard import Dashboard, DashboardWidget, DashboardShareToken from steward.models.dashboard import Dashboard, DashboardWidget, DashboardShareToken
from roundtable.core.settings import public_base_url from steward.core.settings import public_base_url
from roundtable.core.widgets import get_available_widgets, WIDGET_REGISTRY from steward.core.widgets import get_available_widgets, WIDGET_REGISTRY
dashboard_bp = Blueprint("dashboard", __name__) dashboard_bp = Blueprint("dashboard", __name__)
@@ -1,10 +1,10 @@
from __future__ import annotations from __future__ import annotations
from quart import Blueprint, current_app, render_template from quart import Blueprint, current_app, render_template
from sqlalchemy import select, func from sqlalchemy import select, func
from roundtable.auth.middleware import require_role from steward.auth.middleware import require_role
from roundtable.models.users import UserRole from steward.models.users import UserRole
from roundtable.models.hosts import Host from steward.models.hosts import Host
from roundtable.models.monitors import DnsResult from steward.models.monitors import DnsResult
dns_bp = Blueprint("dns", __name__, url_prefix="/dns") dns_bp = Blueprint("dns", __name__, url_prefix="/dns")
@@ -2,11 +2,11 @@ from __future__ import annotations
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from quart import Blueprint, render_template, request, redirect, url_for, current_app, session from quart import Blueprint, render_template, request, redirect, url_for, current_app, session
from sqlalchemy import and_, case, select, func from sqlalchemy import and_, case, select, func
from roundtable.auth.middleware import require_role from steward.auth.middleware import require_role
from roundtable.core.audit import log_audit from steward.core.audit import log_audit
from roundtable.models.hosts import Host, ProbeType from steward.models.hosts import Host, ProbeType
from roundtable.models.monitors import PingResult, DnsResult, PingStatus from steward.models.monitors import PingResult, DnsResult, PingStatus
from roundtable.models.users import UserRole from steward.models.users import UserRole
hosts_bp = Blueprint("hosts", __name__, url_prefix="/hosts") hosts_bp = Blueprint("hosts", __name__, url_prefix="/hosts")
@@ -4,8 +4,8 @@ from sqlalchemy import pool
from sqlalchemy.engine import Connection from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context from alembic import context
from roundtable.models.base import Base from steward.models.base import Base
import roundtable.models # noqa: F401 — registers all models in metadata import steward.models # noqa: F401 — registers all models in metadata
config = context.config config = context.config
if config.config_file_name is not None: if config.config_file_name is not None:
@@ -18,9 +18,7 @@ def get_url() -> str:
import os, yaml import os, yaml
def _env(suffix: str) -> str | None: def _env(suffix: str) -> str | None:
return os.environ.get(f"ROUNDTABLE_{suffix}") or os.environ.get( return os.environ.get(f"STEWARD_{suffix}")
f"FABLEDSCRYER_{suffix}"
)
cfg_path = _env("CONFIG") or "config.yaml" cfg_path = _env("CONFIG") or "config.yaml"
try: try:
@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlalchemy import DateTime, String, Text from sqlalchemy import DateTime, String, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from roundtable.models.base import Base from steward.models.base import Base
class AppSetting(Base): class AppSetting(Base):
@@ -6,9 +6,9 @@ import socket
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from roundtable.core.alerts import record_metric from steward.core.alerts import record_metric
from roundtable.models.hosts import Host from steward.models.hosts import Host
from roundtable.models.monitors import DnsResult, DnsStatus from steward.models.monitors import DnsResult, DnsStatus
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -5,9 +5,9 @@ import time
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from roundtable.core.alerts import record_metric from steward.core.alerts import record_metric
from roundtable.models.hosts import Host, ProbeType from steward.models.hosts import Host, ProbeType
from roundtable.models.monitors import PingResult, PingStatus from steward.models.monitors import PingResult, PingStatus
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -2,12 +2,12 @@ from __future__ import annotations
import logging import logging
from quart import Blueprint, current_app, render_template, request, redirect, url_for from quart import Blueprint, current_app, render_template, request, redirect, url_for
from sqlalchemy import select, func, case from sqlalchemy import select, func, case
from roundtable.auth.middleware import require_role from steward.auth.middleware import require_role
from roundtable.models.users import UserRole from steward.models.users import UserRole
from roundtable.models.hosts import Host from steward.models.hosts import Host
from roundtable.models.monitors import PingResult from steward.models.monitors import PingResult
from roundtable.core.settings import get_all_settings, set_setting, DEFAULTS from steward.core.settings import get_all_settings, set_setting, DEFAULTS
from roundtable.core.time_range import parse_range, DEFAULT_RANGE from steward.core.time_range import parse_range, DEFAULT_RANGE
ping_bp = Blueprint("ping", __name__, url_prefix="/ping") ping_bp = Blueprint("ping", __name__, url_prefix="/ping")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+1
View File
@@ -0,0 +1 @@
# steward/settings/__init__.py
@@ -1,14 +1,14 @@
# roundtable/settings/routes.py # steward/settings/routes.py
from __future__ import annotations from __future__ import annotations
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
from quart import Blueprint, current_app, render_template, request, redirect, url_for, session from quart import Blueprint, current_app, render_template, request, redirect, url_for, session
from roundtable.auth.middleware import require_role from steward.auth.middleware import require_role
from roundtable.core.audit import log_audit from steward.core.audit import log_audit
from roundtable.models.users import UserRole from steward.models.users import UserRole
from roundtable.core.settings import ( from steward.core.settings import (
DEFAULTS, get_all_settings, set_setting, DEFAULTS, get_all_settings, set_setting,
to_smtp_cfg, to_webhook_cfg, to_ansible_cfg, to_plugins_cfg, to_smtp_cfg, to_webhook_cfg, to_ansible_cfg, to_plugins_cfg,
to_oidc_cfg, to_ldap_cfg, to_oidc_cfg, to_ldap_cfg,
@@ -235,12 +235,12 @@ async def ansible_sync_source(idx: int):
source = sources[idx] source = sources[idx]
if source.get("type") != "git": if source.get("type") != "git":
return '<span style="color:var(--text-muted);font-size:0.8rem;">Not a git source</span>' return '<span style="color:var(--text-muted);font-size:0.8rem;">Not a git source</span>'
from roundtable.ansible.sources import git_pull from steward.ansible.sources import git_pull
from roundtable.core.settings import to_ansible_cfg from steward.core.settings import to_ansible_cfg
async with current_app.db_sessionmaker() as db: async with current_app.db_sessionmaker() as db:
settings = await get_all_settings(db) settings = await get_all_settings(db)
ansible_cfg = to_ansible_cfg(settings) ansible_cfg = to_ansible_cfg(settings)
from roundtable.ansible.sources import get_sources from steward.ansible.sources import get_sources
resolved = next((s for s in get_sources(ansible_cfg) if s["name"] == source["name"]), None) resolved = next((s for s in get_sources(ansible_cfg) if s["name"] == source["name"]), None)
if resolved is None: if resolved is None:
return '<span style="color:var(--red);font-size:0.8rem;">Could not resolve source path</span>' return '<span style="color:var(--red);font-size:0.8rem;">Could not resolve source path</span>'
@@ -298,7 +298,7 @@ async def save_auth_settings():
await log_audit(current_app, session.get("user_id"), session.get("username", ""), await log_audit(current_app, session.get("user_id"), session.get("username", ""),
"settings.saved", detail={"section": "auth"}) "settings.saved", detail={"section": "auth"})
# Clear OIDC discovery cache so new settings take effect # Clear OIDC discovery cache so new settings take effect
from roundtable.auth.oidc import _discovery_cache from steward.auth.oidc import _discovery_cache
_discovery_cache.clear() _discovery_cache.clear()
return redirect(url_for("settings.auth_settings")) return redirect(url_for("settings.auth_settings"))
@@ -330,7 +330,7 @@ async def save_reports():
@settings_bp.post("/reports/send-now/") @settings_bp.post("/reports/send-now/")
@require_role(UserRole.admin) @require_role(UserRole.admin)
async def send_report_now(): async def send_report_now():
from roundtable.core.reports import send_report from steward.core.reports import send_report
async with current_app.db_sessionmaker() as db: async with current_app.db_sessionmaker() as db:
settings = await get_all_settings(db) settings = await get_all_settings(db)
ok, message = await send_report(current_app._get_current_object()) ok, message = await send_report(current_app._get_current_object())
@@ -440,7 +440,7 @@ async def plugin_detail(name: str):
if plugin is None: if plugin is None:
return redirect(url_for("settings.plugins")) return redirect(url_for("settings.plugins"))
_merge_plugin_config([plugin], to_plugins_cfg(settings)) _merge_plugin_config([plugin], to_plugins_cfg(settings))
from roundtable.core.plugin_manager import _LOADED_PLUGINS, get_plugin_failures from steward.core.plugin_manager import _LOADED_PLUGINS, get_plugin_failures
return await render_template( return await render_template(
"settings/plugin_detail.html", "settings/plugin_detail.html",
plugin=plugin, plugin=plugin,
@@ -469,7 +469,7 @@ async def save_plugin_detail(name: str):
await set_setting(db, f"plugin.{name}", plugin_cfg) await set_setting(db, f"plugin.{name}", plugin_cfg)
await _reload_app_config() await _reload_app_config()
from roundtable.core.plugin_index import clear_catalog_cache from steward.core.plugin_index import clear_catalog_cache
clear_catalog_cache() clear_catalog_cache()
new_enabled = plugin_cfg.get("enabled", False) new_enabled = plugin_cfg.get("enabled", False)
@@ -478,7 +478,7 @@ async def save_plugin_detail(name: str):
await log_audit(current_app, session.get("user_id"), session.get("username", ""), await log_audit(current_app, session.get("user_id"), session.get("username", ""),
action, entity_type="plugin", entity_id=name) action, entity_type="plugin", entity_id=name)
if new_enabled and not old_enabled: if new_enabled and not old_enabled:
from roundtable.core.plugin_manager import hot_reload_plugin from steward.core.plugin_manager import hot_reload_plugin
hot_reload_plugin(current_app._get_current_object(), name) hot_reload_plugin(current_app._get_current_object(), name)
return redirect(url_for("settings.plugin_detail", name=name)) return redirect(url_for("settings.plugin_detail", name=name))
@@ -500,7 +500,7 @@ async def plugin_repos_add():
async with current_app.db_sessionmaker() as db: async with current_app.db_sessionmaker() as db:
async with db.begin(): async with db.begin():
await _save_plugin_repos(db, repos) await _save_plugin_repos(db, repos)
from roundtable.core.plugin_index import clear_catalog_cache from steward.core.plugin_index import clear_catalog_cache
clear_catalog_cache() clear_catalog_cache()
return await _plugin_repos_partial({"plugins.repositories": repos}) return await _plugin_repos_partial({"plugins.repositories": repos})
@@ -516,7 +516,7 @@ async def plugin_repos_remove(idx: int):
async with current_app.db_sessionmaker() as db: async with current_app.db_sessionmaker() as db:
async with db.begin(): async with db.begin():
await _save_plugin_repos(db, repos) await _save_plugin_repos(db, repos)
from roundtable.core.plugin_index import clear_catalog_cache from steward.core.plugin_index import clear_catalog_cache
clear_catalog_cache() clear_catalog_cache()
return await _plugin_repos_partial({"plugins.repositories": repos}) return await _plugin_repos_partial({"plugins.repositories": repos})
@@ -536,7 +536,7 @@ async def plugin_repos_save(idx: int):
async with current_app.db_sessionmaker() as db: async with current_app.db_sessionmaker() as db:
async with db.begin(): async with db.begin():
await _save_plugin_repos(db, repos) await _save_plugin_repos(db, repos)
from roundtable.core.plugin_index import clear_catalog_cache from steward.core.plugin_index import clear_catalog_cache
clear_catalog_cache() clear_catalog_cache()
return await _plugin_repos_partial({"plugins.repositories": repos}) return await _plugin_repos_partial({"plugins.repositories": repos})
@@ -561,8 +561,8 @@ async def plugins_catalog():
error="No plugin repositories configured.", error="No plugin repositories configured.",
) )
from roundtable.core.plugin_index import fetch_catalog from steward.core.plugin_index import fetch_catalog
from roundtable.core.plugin_manager import _LOADED_PLUGINS from steward.core.plugin_manager import _LOADED_PLUGINS
force = request.args.get("refresh") == "1" force = request.args.get("refresh") == "1"
try: try:
@@ -603,7 +603,7 @@ async def install_plugin(name: str):
message="No download URL provided.", message="No download URL provided.",
) )
from roundtable.core.plugin_manager import download_and_install_plugin, hot_reload_plugin from steward.core.plugin_manager import download_and_install_plugin, hot_reload_plugin
ok, msg = await download_and_install_plugin( ok, msg = await download_and_install_plugin(
current_app._get_current_object(), name, download_url, checksum current_app._get_current_object(), name, download_url, checksum
@@ -637,7 +637,7 @@ async def install_plugin(name: str):
@require_role(UserRole.admin) @require_role(UserRole.admin)
async def reload_plugin(name: str): async def reload_plugin(name: str):
"""Hot-reload a plugin that was installed but not yet active.""" """Hot-reload a plugin that was installed but not yet active."""
from roundtable.core.plugin_manager import hot_reload_plugin from steward.core.plugin_manager import hot_reload_plugin
ok, msg = hot_reload_plugin(current_app._get_current_object(), name) ok, msg = hot_reload_plugin(current_app._get_current_object(), name)
return await render_template( return await render_template(
"settings/_install_result.html", "settings/_install_result.html",
@@ -653,7 +653,7 @@ async def reload_plugin(name: str):
async def restart_app_route(): async def restart_app_route():
"""Trigger an in-app restart (replaces the process via os.execv).""" """Trigger an in-app restart (replaces the process via os.execv)."""
import asyncio import asyncio
from roundtable.core.plugin_manager import restart_app from steward.core.plugin_manager import restart_app
# Schedule the restart slightly after we return the response # Schedule the restart slightly after we return the response
async def _delayed_restart(): async def _delayed_restart():
@@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Alerts — Roundtable{% endblock %} {% block title %}Alerts — Steward{% endblock %}
{% block content %} {% block content %}
<h1 class="page-title">Alerts</h1> <h1 class="page-title">Alerts</h1>
@@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Maintenance Windows — Roundtable{% endblock %} {% block title %}Maintenance Windows — Steward{% endblock %}
{% block content %} {% block content %}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;">
<h1 class="page-title" style="margin-bottom:0;">Maintenance Windows</h1> <h1 class="page-title" style="margin-bottom:0;">Maintenance Windows</h1>
@@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}New Maintenance Window — Roundtable{% endblock %} {% block title %}New Maintenance Window — Steward{% endblock %}
{% block content %} {% block content %}
<div style="max-width:560px;margin:2rem auto;"> <div style="max-width:560px;margin:2rem auto;">
<h1 class="page-title">New Maintenance Window</h1> <h1 class="page-title">New Maintenance Window</h1>

Some files were not shown because too many files have changed in this diff Show More