Skip to content

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.
Email 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 maid CLI (MAID_SRC): there is no system-wide maid binary until M1.4 (install.sh leaves /opt/maid/venv empty), so the CLI runs from a source checkout. export MAID_SRC=/path/to/maid-checkout and 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 the maid-engine service user; inside bash -c blocks MAID_SRC is 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 maintenance is not yet implemented — it currently prints NOT 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, use maid 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:

  1. Sets the in-game login gate (new logins refused for non-admins).
  2. Sends the per-protocol banner to all currently-connected sessions:
  3. Telnet: ANSI red banner above the prompt.
  4. WebSocket / player-frontend: structured maintenance.banner event consumed by the React client.
  5. Admin UI: yellow modal with the message body.
  6. 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:

  1. Create the webhook in Discord.

    • Navigate: target channel → Edit ChannelIntegrationsWebhooksNew 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>.
  2. Put the URL in the environment that runs announce. announce reads its own process environment, not the engine's EnvironmentFile. Export it in the operator shell (or a sourced ops env file that uses export), with the discord: format prefix:

    export MAID_BRIDGES_WEBHOOK_URLS="discord:https://discord.com/api/webhooks/<id>/<token>"
    

    No engine reload or restart is needed — announce picks the value up on its next invocation.

  3. Verify with a test announcement.

    sudo -u maid-engine --preserve-env=MAID_BRIDGES_WEBHOOK_URLS "${MAID_SRC}/.venv/bin/maid" ops announce \
      --instance <inst> \
      --channels webhook \
      --severity info \
      --message "Webhook smoke test — please ignore"
    
  4. 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:

    [INFO] Webhook smoke test — please ignore (actor: <you>)
    

    The severity is upper-cased in the [...] prefix (INFO, WARN, or CRITICAL). If --eta/--in-minutes/--instance were supplied, they are appended as key=value pairs.

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 maintenance toggle that was intended to atomically rewrite it is an unimplemented stub (NOT YET IMPLEMENTED (M9)), and maid ops announce only POSTs to webhooks — it never touches this file. Until the maintenance toggle lands, an operator must create/update /var/lib/maid-engine/status/index.html by 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 directory 0755 maid-engine:maid-engine which 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 tmpindex.html rename(2) atomically.

Verify the page reaches players from outside the host:

curl -fsS -o - -H 'Cache-Control: no-cache' http://status.example.com/ \
  | head -5

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 announceMAID_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.