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:
+3
-3
@@ -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
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 |
|
||||||
|
|||||||
@@ -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 2–4 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 2–4 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 1–5; PR 6 handles the cleanup implied by the shim in PR 3.
|
- Spec coverage: all 5 spec sections mapped to PRs 1–5; 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
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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 +0,0 @@
|
|||||||
# roundtable/settings/__init__.py
|
|
||||||
@@ -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,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
|
||||||
@@ -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)
|
||||||
@@ -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__)
|
||||||
@@ -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
-1
@@ -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
-1
@@ -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
Reference in New Issue
Block a user