Player Communications¶
maid ops announce is the only functional operator-notification command
today, and it delivers to one place: the outbound webhook endpoints
configured in MAID_BRIDGES_WEBHOOK_URLS (Discord / Slack / generic JSON).
It does not post in-game, send email, or update the status page — the
--channels flag is recorded as audit/payload metadata only; it does not
route anywhere. Each audience needs a different, separately-invoked
mechanism:
| Audience | Functional mechanism today |
|---|---|
| Connected players (in-game) | POST /api/v1/admin/broadcast (admin API key) — broadcasts to all sessions. maid ops broadcast is an M9 stub. |
| Discord / Slack / webhook | maid ops announce (this doc). |
| Static status page | Edit the HTML file by hand (see status page); nothing writes it automatically — maid ops maintenance, which was to write it, is an M9 stub. |
| Not implemented — there is no email/SMTP path in the engine. |
maid ops announce accepts --severity of info, warn, or critical
(note: warn, not warning). Successful invocations are audit-logged
to /var/log/maid/ops-audit.jsonl.
Pre-maintenance cadence¶
Three webhook announcements at T-24h, T-1h, and T-15m, escalating the
severity step from info to warn. Reaching connected players in-game is
a separate step — see Reaching in-game players.
maid ops announce reads MAID_BRIDGES_WEBHOOK_URLS from its own process
environment. A plain source <file> without export, or a bare sudo
(which resets the environment and does not load the instance env file), will
NOT pass the value through. Export it first and forward it explicitly across
sudo:
export MAID_BRIDGES_WEBHOOK_URLS="discord:https://discord.com/api/webhooks/<id>/<token>"
# Forward it across sudo with --preserve-env (shown below), or equivalently:
# sudo -u maid-engine env MAID_BRIDGES_WEBHOOK_URLS="$MAID_BRIDGES_WEBHOOK_URLS" \
# "${MAID_SRC}/.venv/bin/maid" ops announce ...
Interim
maidCLI (MAID_SRC): there is no system-widemaidbinary until M1.4 (install.shleaves/opt/maid/venvempty), so the CLI runs from a source checkout.export MAID_SRC=/path/to/maid-checkoutand build its venv once:sudo -u maid-engine -H uv sync --frozen --project "${MAID_SRC}"(uv:/opt/maid/current/.uv/bin/uv). The commands here run"${MAID_SRC}/.venv/bin/maid"as themaid-engineservice user; insidebash -cblocksMAID_SRCis forwarded via--preserve-env=MAID_SRC.
T-24h (announce only; non-blocking)¶
sudo -u maid-engine --preserve-env=MAID_BRIDGES_WEBHOOK_URLS "${MAID_SRC}/.venv/bin/maid" ops announce \
--instance <inst> \
--channels webhook \
--severity info \
--eta "$(date -u -d '2025-11-20T03:00:00Z' --iso-8601=seconds)" \
--message "Scheduled maintenance window: 2025-11-20 03:00–03:30 UTC. \
Expected planned outage ~10 minutes for the engine restart. \
Details: https://status.example.com"
T-1h (announce + reminder)¶
sudo -u maid-engine --preserve-env=MAID_BRIDGES_WEBHOOK_URLS "${MAID_SRC}/.venv/bin/maid" ops announce \
--instance <inst> \
--channels webhook \
--severity info \
--eta "$(date -u -d '+1 hour' --iso-8601=seconds)" \
--message "Reminder: maintenance in 1 hour. Save your progress and \
end PvP/quest activity in the next 45 minutes. https://status.example.com"
T-15m (urgent)¶
sudo -u maid-engine --preserve-env=MAID_BRIDGES_WEBHOOK_URLS "${MAID_SRC}/.venv/bin/maid" ops announce \
--instance <inst> \
--channels webhook \
--severity warn \
--eta "$(date -u -d '+15 minutes' --iso-8601=seconds)" \
--message "Maintenance in 15 minutes. The engine will restart at \
$(date -u -d '+15 minutes' '+%H:%M UTC'). Auto-reconnect will resume \
when the server returns. https://status.example.com"
At T-15m, also notify connected players in-game — the webhook does not reach them (see Reaching in-game players).
Reaching in-game players¶
maid ops announce never reaches connected players — it only POSTs to
webhooks. To broadcast a line to every connected session (Telnet and
WebSocket), use the admin REST API POST /api/v1/admin/broadcast, which is
functional and takes effect immediately. It requires an ADMIN-scoped API
key in the X-API-Key header:
curl -fsS -X POST http://127.0.0.1:8080/api/v1/admin/broadcast \
-H "X-API-Key: ${MAID_ADMIN_API_KEY}" \
-H 'Content-Type: application/json' \
-d '{"prefix":"[MAINTENANCE]","message":"Restart in 15 minutes; auto-reconnect when the server returns."}'
The JSON response reports the recipient count. There is no working shell
equivalent — maid ops broadcast prints NOT YET IMPLEMENTED (M9).
During maintenance¶
Status (M9):
maid ops maintenanceis not yet implemented — it currently printsNOT YET IMPLEMENTED (M9)and exits without setting the login gate, sending banners, or rewriting the status page. Everything in this section describes the intended behavior once the verb lands. In the meantime, usemaid ops announce(functional) for player notifications and publish the static status page manually (see the nginx snippet).
maid ops maintenance on is the single source of truth (per plan.md §3 M1
line 80, line 203). It:
- Sets the in-game login gate (new logins refused for non-admins).
- Sends the per-protocol banner to all currently-connected sessions:
- Telnet: ANSI red banner above the prompt.
- WebSocket / player-frontend: structured
maintenance.bannerevent consumed by the React client. - Admin UI: yellow modal with the message body.
- Atomically rewrites
deploy/status-page/templates/index.html.tmpl→/var/lib/maid-engine/status/index.html(per plan.md R3.13 — plain HTML, < 4 KB, no JS, no external assets) so the public status page reflects the new state without operator intervention. See the nginx snippet below for how to serve this file.
# Enter maintenance.
sudo -u maid-engine "${MAID_SRC}/.venv/bin/maid" ops maintenance on \
--instance <inst> \
--message "Upgrading to v0.4.2; back at $(date -u -d '+10 minutes' '+%H:%M UTC')"
# ... perform the upgrade ...
# Exit maintenance.
sudo -u maid-engine "${MAID_SRC}/.venv/bin/maid" ops maintenance off --instance <inst>
While maintenance is ON, admin sessions still log in normally (admin bypass). The login gate applies to player sessions only.
Post-incident summary¶
After any unplanned outage or after any maintenance window that took longer than its announced ETA, send a summary to the same audience that received the heads-up. Template:
sudo -u maid-engine --preserve-env=MAID_BRIDGES_WEBHOOK_URLS "${MAID_SRC}/.venv/bin/maid" ops announce \
--instance <inst> \
--channels webhook \
--severity info \
--message "Resolved: <one-line symptom, e.g. 'persistence save queue
backed up to 5 min, players saw quest-completion errors'>.
Players affected: <scope — all / specific zone / specific feature>.
Root cause: <brief — e.g. 'AI provider 5xx caused off-tick LLM queue
backpressure into the save scheduler'>.
Fix: <brief — e.g. 'circuit-breaker on provider; queue cap reduced'>.
Follow-up: <link to public postmortem markdown URL>."
Post-incident summaries go to the webhook only. There is no email path; if you also want to notify players who are currently connected, send an in-game admin broadcast.
Discord webhook setup¶
maid ops announce POSTs to the endpoints configured in
MAID_BRIDGES_WEBHOOK_URLS (there is no --webhook-urls flag on the
maid ops announce verb — the value must be in the command's environment).
For Discord the payload is {"content": "<text>"}; see
integrations/discord-webhooks.md for
the full setup and troubleshooting guide. In brief:
-
Create the webhook in Discord.
- Navigate: target channel → Edit Channel → Integrations → Webhooks → New Webhook.
- Set name (e.g.
maid-ops) and avatar. - Click Copy Webhook URL. The URL has the form
https://discord.com/api/webhooks/<id>/<token>.
-
Put the URL in the environment that runs
announce.announcereads its own process environment, not the engine'sEnvironmentFile. Export it in the operator shell (or a sourced ops env file that usesexport), with thediscord:format prefix:No engine reload or restart is needed —
announcepicks the value up on its next invocation. -
Verify with a test announcement.
-
Expected rendered output. A plain-text message appears in the configured channel (the payload is
{"content": "<text>"}— there is no embed card, colored sidebar, title, or footer). The text is rendered as:The severity is upper-cased in the
[...]prefix (INFO,WARN, orCRITICAL). If--eta/--in-minutes/--instancewere supplied, they are appended askey=valuepairs.
If nothing appears within 10 seconds, re-run with the CLI output visible —
announce prints a per-endpoint OK/ERR line with the HTTP status and
exits non-zero if any delivery failed. Each invocation also emits one
OPS_ANNOUNCE_BROADCAST audit row. The poster does one retry on a 5xx
response only (4xx and connection errors are not retried).
Static status page (nginx snippet)¶
Status (M9): nothing writes the status page automatically today. The
maid ops maintenancetoggle that was intended to atomically rewrite it is an unimplemented stub (NOT YET IMPLEMENTED (M9)), andmaid ops announceonly POSTs to webhooks — it never touches this file. Until the maintenance toggle lands, an operator must create/update/var/lib/maid-engine/status/index.htmlby hand (or from a script).
A small plain-HTML status page (per plan.md R3.13: plain HTML, < 4 KB, no JS, no external assets) is meant to be served by a separate static-file frontend (nginx, caddy, apache) so that it remains reachable even when the MAID engine itself is down — which is when players most want to read it.
Minimal nginx server block (drop into /etc/nginx/sites-enabled/maid-status):
# /etc/nginx/sites-enabled/maid-status
# Serves the MAID maintenance/status page independently of the engine.
# Per docs/deployment/player_comms.md and plan.md R3.13.
server {
listen 80;
listen [::]:80;
server_name status.example.com;
# Directory an operator/script writes the status page into (see the
# M9 note above — nothing writes it automatically yet).
root /var/lib/maid-engine/status;
index index.html;
# The status page MUST NOT be cached: it reflects current state and
# should change whenever an operator updates it.
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header X-Content-Type-Options "nosniff" always;
default_type text/html;
location / {
try_files $uri $uri/ /index.html =404;
}
# Fallback if no status page has been written yet (fresh install).
error_page 404 = @notready;
location @notready {
default_type text/html;
return 200 '<!doctype html><meta charset="utf-8"><title>MAID</title>
<h1>MAID</h1><p>Status page not yet initialised.</p>';
}
}
Behind a TLS terminator (recommended for any public deployment) the same
block goes inside the existing server { listen 443 ssl; ... }, with the
HTTP listen 80 block doing a redirect-to-HTTPS as you normally would.
Filesystem permissions:
/var/lib/maid-engine/status/must be readable by the nginx user. The install script creates the directory0755 maid-engine:maid-enginewhich is world-readable, so no further ACL is required.- Keep the directory on the same filesystem as whatever writes the page so
a script can update it via a
tmp→index.htmlrename(2)atomically.
Verify the page reaches players from outside the host:
The body is whatever an operator (or a status-page script) last wrote —
there is no maid ops maintenance writer today (see the M9 note above).
Channel reference¶
maid ops announce --channels is metadata only — the value is recorded
in the audit row but does not select a delivery mechanism. Every
announce invocation, regardless of --channels, POSTs to the webhooks in
MAID_BRIDGES_WEBHOOK_URLS and does nothing else. The table below shows
how to actually reach each audience:
| Audience | Mechanism | Status |
|---|---|---|
| Currently-connected players | POST /api/v1/admin/broadcast (see Reaching in-game players) |
Functional |
| Ops + community moderators | maid ops announce → MAID_BRIDGES_WEBHOOK_URLS webhooks |
Functional |
| Anyone with the URL | Static status page, updated manually | Functional (manual) |
| Email subscribers | — | Not implemented (no SMTP path exists) |
Every maid ops announce invocation produces one OPS_ANNOUNCE_BROADCAST
audit row recording the severity, the --channels metadata string, the
message body, and per-webhook-endpoint delivery status.