# Operations

## Guarded Hranice Armory Import

`scripts/import_production_armory_sheet1.py` is a one-time, source-checksummed migration from read-only `Sheet1` in the production Hranice spreadsheet into production armory ID 3. It validates the exact guild, Dragonbane ruleset, mirror destination (`Armory Mirror`, never `Sheet1`), borrower membership, catalogue mappings, duplicate consistency, and source checksum. The default invocation is a dry run; `--apply` performs one SQLite transaction, preserves unrelated target items, reconciles matching imported quantities and loans, and marks only the Discord board and configured mirror dirty.

Back up `therook.db` before using `--apply`. The source sheet is accessed only with the Sheets values GET API.

## Local Run

```bash
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
```

Set `DISCORD_TOKEN` in `.env`, then run:

```bash
.venv/bin/python bot.py
```

Optional Google Sheets armory mirrors require a Google Cloud service account with the Sheets API enabled. Download its JSON key outside the repository, share only the target spreadsheet with the service-account email, and configure:

```env
GOOGLE_SERVICE_ACCOUNT_FILE=/etc/therook/google-service-account.json
```

The systemd service must have read access to that exact file. Keep the key out of Git and backups unless secret-bearing backups are explicitly required. Create a dedicated tab (for example `Armory Mirror`) before running `/armory sheet-set`; the bot intentionally refuses to create or guess a tab.

Optional operator DM allowlist:

```env
ROOK_OPERATOR_USER_IDS=123456789012345678,234567890123456789
```

Only these Discord user IDs receive lifecycle/health DMs and can use `/rook status` in bot DMs.

## Required Discord Intents

The bot expects:

- Server Members Intent
- Message Content Intent
- Reaction events

Missing intents will break welcome/member tracking, message analytics, forbidden-word responses, or reaction-role/report behavior.

## Deployment

The root README contains a systemd service example. The important operational details are:

- Run from the repository working directory so `therook.db` resolves correctly.
- Use the virtualenv Python.
- Keep `PYTHONUNBUFFERED=1` for useful journald logs.
- Let systemd restart the process on failure.

The production retry override is tracked in `deploy/systemd/therook.service.d/retry.conf`. Install it with:

```bash
sudo install -D -m 644 deploy/systemd/therook.service.d/retry.conf /etc/systemd/system/therook.service.d/retry.conf
sudo systemctl daemon-reload
sudo systemctl restart therook.service
```

The existing restart policy is retained; `RestartSec=10s` waits ten seconds after process exit. Login requests and the library's internal HTTP retries add to the interval between login attempts. Gateway reconnect waits inside a running process are capped at 30 seconds by `rookbot/reconnect.py`; Discord rate limits are still honored. Check `systemctl show therook.service -p RestartUSec -p NRestarts` and the journal for successful online/command-sync confirmation: an active process alone does not prove Discord connectivity. To revert the service override, remove `/etc/systemd/system/therook.service.d/retry.conf` and run daemon-reload and restart again.

`bot.py` also uses `/tmp/therook-bot.lock` to avoid running two bot processes at once.

## Temporary Profile-Link Workaround

Wishes and RSVP attendance share `USER_PROFILE_LINKS_ENABLED` in `rookbot/wishes.py`, currently `False` because Discord profile cards can bypass embed suppression. To restore the previous links, set it to `True` and restart `therook.service`. Enabled wish boards are queued for regeneration on startup; existing current-occurrence RSVP messages also need to be regenerated with their stored base text and attendance renderer. Changing the switch alone does not edit existing Discord messages.

The 8 October 2026 deployment restarted the bot and removed only the generated profile-link suffixes from tracked wish-board messages and current-occurrence RSVP messages (joined to `next_games` by guild, channel, and occurrence). It preserved content, buttons, and RSVP data, suppressed embeds, disabled mentions for edits, and fetched messages again to verify removal. The private refresh script, edit backups, and verification results are stored outside the public web root at `/home/ubuntu/.local/share/therook-profile-links-2026-10-08/`. It can be rerun with `.venv/bin/python /home/ubuntu/.local/share/therook-profile-links-2026-10-08/refresh.py` while the workaround is enabled. It removes links rather than restoring them; do not rerun it after re-enabling links. Historical RSVP messages and untracked past command replies are outside this refresh scope.

## Public Documentation Site

The bot documentation is served by nginx, separately from the Discord bot process:

- Public URL: `https://therook.idler.quest/`
- web editor URL: `https://therook.idler.quest/edit/`
- nginx site: `/etc/nginx/sites-available/therook-docs`
- enabled symlink: `/etc/nginx/sites-enabled/therook-docs`
- web root: `/home/ubuntu/therook/docs`
- HTML entry point: `/home/ubuntu/therook/docs/index.html`
- static assets: `/home/ubuntu/therook/docs/style.css`, `/home/ubuntu/therook/docs/icon.png`, and `/home/ubuntu/therook/docs/screenshots/`

The nginx site listens on ports 80 and 443 with `server_name therook.idler.quest;` and serves files directly from the docs directory. HTTP requests redirect to HTTPS. Reloading nginx for documentation hosting changes does not restart or otherwise affect `therook.service`.

The Symbane: Hranice campaign summary, checked through 15 October 2026 (latest reviewed posts: 15 October; includes Yeferon, both Faraah Moroun expeditions and completed Karohar peace), is published at `https://therook.idler.quest/symbane-hranice/story-so-far.html`, from `docs/symbane-hranice/story-so-far.html`. **Czech is the default**, independently written from the original Czech Discord messages, threads, and attachments. The English edition is `story-so-far.en.html` in the same directory and at the corresponding public URL. The older `story-so-far.cs.html` URL remains an identical static copy of the Czech main page, with a canonical link to the main URL. An English/Česky toggle links the main Czech and explicit English pages and retains the current section with JavaScript enabled. All editions share fragment IDs; source citations can differ because the Czech text is independently composed. Keep the Czech alias byte-identical and maintain all three files together.

These are static HTML snapshots with full campaign spoilers and source links, publicly readable without Discord authentication; linked Discord posts still require server access. Each edition’s text works offline; offline switching requires `story-so-far.html` and `story-so-far.en.html` in the same folder. The five selected illustrations are shared local assets under `docs/symbane-hranice/illustrations-v1/`; preserve that folder alongside downloaded HTML for offline images, or save the complete webpage. Images load lazily and can be enlarged in a keyboard-accessible dialog or opened directly without JavaScript. The character reference is at `https://therook.idler.quest/symbane-hranice/story-so-far.html#characters` and records aliases, basic information, and last-known status at the same cutoff. Readers can use **Save Page As** to download it. The pages are not automatically refreshed or expired. For updates, follow `notes/hranice-story-continuation.md` and its state JSON: compose Czech first directly from the original Czech Discord posts, then independently compose English from those same posts (never from the Czech summary); compare both completed drafts against the sources for consistency, preserve private backups outside the web root, publish English, the Czech alias, and the Czech main page using atomic file replacement, then verify all three HTTPS responses. To unpublish the chronicle, remove all three files. Static changes take effect immediately without reloading nginx or restarting the bot. The 15 October continuation reviewed 175 new messages and ten attachments, bringing coverage to 982 messages, 17 threads, and 30 attachments without retrieval gaps. Czech was composed first; the English continuation was independently composed from the same original Czech sources before factual comparison. Both editions now have 25 narrative chapters, 68 character-reference rows split into PCs and NPCs, and two contextual notes for House Argona and Haman Dakh. The PC group includes deceased and former player characters. Editorial regrouping does not advance source coverage or reviewed-message cursors. The current private baseline, review record, source map, independent drafts, validation results and preceding editions are under `/home/ubuntu/.local/share/therook-story-notes/hranice/2026-10-15/`; no raw sources or newly reviewed attachments are served publicly. The outcome now includes Burbero’s death, Brousek’s departure, Belissandra’s sacrifice, verified clearing of Faraah Moroun and accepted Karohar peace. Future meetings, return of bodies, the postponed funeral and announced delegation arrivals remain distinguished from completed events. The existing five illustrations are unchanged. Their September text-only predecessor and historical rollback remain under `/home/ubuntu/.local/share/therook-story-notes/hranice/illustrated-2026-09-24/`, but that guarded rollback now refuses the newer edition; remove illustrations with a focused edit if needed, preserving October narrative and checkpoint.

TLS is managed by Certbot and Let's Encrypt:

- certificate: `/etc/letsencrypt/live/therook.idler.quest/fullchain.pem`
- private key: `/etc/letsencrypt/live/therook.idler.quest/privkey.pem`
- renewal service: `certbot.timer`

The instance firewall must allow TCP 443 in addition to TCP 80. On this host, the allow rule is saved through `netfilter-persistent` in `/etc/iptables/rules.v4`.

The web editor is served by `therook-docs-editor.service`, which runs `docs/editor.py` on port 8081 and is proxied by nginx under `/edit/`. It is protected with HTTP Basic auth using `DOCS_EDITOR_PASSWORD` from `/home/ubuntu/therook/.env.editor`. POST saves additionally require a cryptographically random CSRF token embedded in the authenticated editor page and an exact `Origin` match. The production origin defaults to `https://therook.idler.quest`; set a comma-separated `DOCS_EDITOR_ALLOWED_ORIGINS` only when another trusted origin is required. Requests are limited to 1 MiB.

The preview iframe is sandboxed with scripts disabled. Visual editing remains constrained to top-level `main section[id]` blocks. Each save copies the previous page to `docs/.editor-versions/index.<UTC timestamp>.html`, writes and flushes a temporary file in `docs/`, and publishes it with `os.replace()`. Restore a version with an atomic same-filesystem copy/rename, or copy it to a temporary file in `docs/` and use `mv` to replace `index.html`.

Rate-limit the editor at nginx. Define the shared zone in the `http` context (for example in `/etc/nginx/conf.d/therook-editor-rate.conf`):

```nginx
limit_req_zone $binary_remote_addr zone=therook_editor:10m rate=5r/m;
```

Then apply it to `location /edit/` with `limit_req zone=therook_editor burst=5 nodelay;` and `limit_req_status 429;`. This limits both failed Basic-auth attempts and authenticated editor traffic; keep the burst small because saves and page loads are infrequent. nginx-native `auth_basic` with an htpasswd file is preferred when failed-login-only accounting is required.

Run `therook-docs-editor.service` as a dedicated locked account such as `therook-docs-editor`, not the bot or deployment user. Grant it access only to the editor program/virtualenv, `docs/index.html`, the `docs/.editor-versions/` directory, and the `docs/` directory operations required for atomic rename; keep `.env`, the database, and other application state inaccessible. Retain systemd hardening (`ProtectSystem=strict`, `ProtectHome=tmpfs`, `NoNewPrivileges=yes`) and narrow `ReadWritePaths` to the publication and version paths. Because atomic replacement necessarily requires write permission on the containing directory, isolate the public document root from source and secrets rather than granting this account write access to the repository root.

The preferred production workflow is Git-based: edit on a branch, review the diff, merge, and deploy the committed `docs/index.html`. Keep the browser editor as an emergency operator tool, and commit or deliberately revert any emergency edit promptly so production does not drift from Git.

Validate and reload nginx after changing the site config:

```bash
sudo nginx -t
sudo systemctl reload nginx
```

Check the local virtual host before relying on public DNS:

```bash
curl -I -H 'Host: therook.idler.quest' http://127.0.0.1/
```

Check HTTPS locally with SNI:

```bash
curl -I --resolve therook.idler.quest:443:127.0.0.1 https://therook.idler.quest/
```

Exercise the editor protections after deployment (supply valid Basic credentials): a save without `Origin` or without `X-CSRF-Token` must return 403, a body over 1 MiB must return 413, and repeated requests beyond the nginx burst must return 429.

## Diagnosing Slow Startup

After a restart, inspect `journalctl -u therook.service --since '<restart time>'` for the immediate import marker and `Startup phase=` records. Compare cumulative `elapsed` values between phases; `cpu` is cumulative process CPU time. Time before `connecting` is local imports/registration/database work; `connecting` to `login-complete` is Discord HTTP login; `cogs-loaded` to `gateway-ready` includes gateway connection and guild initialization. The remaining phases separate database/views, role synchronization, and slash-command synchronization. A running service is not sufficient: require the final online and command-sync confirmation. Network waits and host resource pressure can both produce wall time much larger than CPU time.

The 11 October 2026 investigation compared a 28-second restart, a 91-second restart, and a measured 11.806-second restart after phase instrumentation. In the measured run, local setup took 3.917 seconds, login 0.403 seconds, gateway/guild readiness 3.162 seconds, ready-time initialization 1.420 seconds, and command synchronization 2.903 seconds. The earlier slow run had no phase records, so its exact delayed operation cannot be reconstructed. Host sysstat measurements covering 00:20–00:30 reported 22.75% CPU steal, 11.42% I/O wait, and active swapping; temporary host contention/cold imports are plausible contributors, not a proven sole cause. No persistent startup bottleneck reproduced. Use the new timing markers if the delay returns.

## Backups

`therook-backup.timer` runs daily at 03:15 Prague time with up to 15 minutes of randomized delay. `Persistent=true` catches up after downtime. The service runs as `ubuntu`; inspect `systemctl show therook-backup.service -p Result -p ExecMainStatus` and its journal to verify completion.

`scripts/backup.sh` creates two recovery sets with matching SHA-256 sidecars:

- `therook-backup-<UTC timestamp>.tar.gz`: bot code and requirements, all helper scripts, README/agent instructions, hosted `docs/` including hidden editor versions, local `notes/`, tracked `deploy/` configuration, a SQLite backup-API snapshot checked with `PRAGMA quick_check`, and private `~/.local/share/therook-*` directories (campaign sources and manual recovery snapshots). It excludes repository `backups/`, the virtualenv, and application credential files.
- `therook-secrets-<UTC timestamp>.tar.gz.gpg`: `.env`, `.env.editor`, rclone configuration, `/etc/therook` (including the Google service-account key), The Rook systemd services/timers and their drop-ins, and nginx configuration. All deployment configuration is encrypted because it may contain inline secrets. `restore-manifest.json` maps the archive paths back to their original locations. Missing optional paths are skipped; unreadable existing files fail the run.

Credentials are streamed into GPG AES-256 symmetric encryption; no unencrypted credential tarball is created. Staging and output files are private (`umask 077`), and temporary staging is removed on exit. The recovery passphrase is read from `/home/ubuntu/.config/therook-backup/recovery.key` (mode 600, parent mode 700). It is excluded from both archives and must be saved in an off-server password manager or other secure location separate from Google Drive. Losing this key makes the encrypted recovery set unusable. Do not print or commit its contents. Do not rotate the key without preserving the previous key for historical archives.

Both sets and their checksums upload to `gdrive:backups/therook/`. Retention independently keeps every current UTC month's archive and the newest archive per older month for each set, with no fixed expiry for monthly copies. Remote pruning follows successful uploads; local pruning follows completion of the upload stage. Unrelated files and manual snapshots are not pruned. An unavailable configured Drive upload fails the service; an absent `gdrive` remote explicitly skips upload. `/rook status` currently checks only the regular set's freshness; use the service result and verify both sets for complete recovery coverage.

Environment variables: `APP_DIR`, `BACKUP_DIR`, `BACKUP_REMOTE_PATH`, `BACKUP_KEY_FILE`, and `BACKUP_EMAIL_TO`. The old `BACKUP_INCLUDE_ENV=1` option is rejected: credentials must go through the encrypted set. Optional email sends the regular archive only. Private campaign files and user state are included in that archive, so treat it as private too.

To verify/restore a downloaded encrypted archive, keep the recovery key file private and use a new private scratch directory:

```bash
sha256sum -c therook-secrets-<timestamp>.tar.gz.gpg.sha256
gpg --batch --pinentry-mode loopback --passphrase-file /secure/path/recovery.key \
  --output credentials.tar.gz --decrypt therook-secrets-<timestamp>.tar.gz.gpg
tar -tzf credentials.tar.gz
```

Inspect the manifest before restoring selected files with their original ownership and restrictive permissions; never extract over a running production installation blindly. Restore the regular archive and SQLite snapshot while the bot is stopped, install dependencies, restore the credential/configuration set, then validate nginx and systemd and start the services. Use the same recovery key to configure future backups. Periodically download both sets, verify SHA-256, decrypt the credentials, and run SQLite integrity checks in scratch storage. Clean up decrypted scratch files afterward.

TLS private keys/Certbot account state and host firewall configuration are not included. Reissue certificates and rebuild firewall rules from the deployment guide when restoring the host. GitHub remains a second copy of committed source/docs; private and uncommitted files rely on these archives. This is application recovery, not a whole-server image.

## Logging

Log output goes to stdout (captured by journald) and optionally a rotating file.

Environment variables:

- `LOG_LEVEL` — `DEBUG`, `INFO`, `WARNING`, etc. Default: `INFO`.
- `LOG_FILE` — path to a rotating log file (optional; omit to log to stdout only).

View live logs:

```bash
journalctl -u therook -f --output=cat
```

Last 50 lines:

```bash
journalctl -u therook -n 50 --no-pager --output=cat
```

Inside Discord, admins can use `/log` (last N lines) and `/status` (loop state, report config, DB size) without needing SSH access.

## Operator DMs

When `ROOK_OPERATOR_USER_IDS` is set, The Rook can DM configured operators for high-signal process health events:

- Startup completion after Discord `on_ready()` finishes.
- Database initialization failure.
- Background task loop stopped by an unhandled exception.
- Repeated unexpected slash-command failures.
- A significant Discord gateway reconnect.

Alerts are rate-limited and deduplicated in memory to avoid spam during repeated failures. `/rook status` is a global operator-only DM command showing uptime, latency, guild count, storage and backup state, loop state with last successful pass timestamps, and recent serious errors. It does not expose global operational information to ordinary guild admins.

Operator DM command replies are normal DM messages, not ephemeral responses. `/rook status` acknowledges the Discord interaction immediately, then sends a visible follow-up. `/rook talk` confirms receipt immediately, waits 5 seconds, then sends one of 100 day-brightening notes as a separate DM message rather than an interaction follow-up.

The `/rook status` storage/backup section is read-only. It checks:

- `therook.db` presence and size.
- SQLite WAL file size when `therook.db-wal` exists.
- local disk free space.
- newest local backup archive in `BACKUP_DIR` or the default `backups/` directory.
- matching local `.sha256` sidecar for the newest backup.
- best-effort remote backup state at `gdrive:backups/therook/` through `rclone lsf`, with a short timeout.

Backup freshness is marked healthy when the newest backup timestamp is within 26 hours. If `rclone` is not installed, the `gdrive` remote is unavailable, or the command times out, `/rook status` shows a warning but does not fail the command.

The production `therook.service` uses `ProtectHome=tmpfs`, so rclone's default config under `/home/ubuntu/.config/rclone/rclone.conf` is hidden from the bot unless it is explicitly exposed. The host uses a drop-in at `/etc/systemd/system/therook.service.d/rclone.conf`:

```ini
[Service]
Environment=RCLONE_CONFIG=/home/ubuntu/.config/rclone/rclone.conf
BindReadOnlyPaths=/home/ubuntu/.config/rclone/rclone.conf:/home/ubuntu/.config/rclone/rclone.conf
```

Run `sudo systemctl daemon-reload && sudo systemctl restart therook.service` after creating or changing that drop-in.

## Safety

- Do not commit `.env`, `therook.db`, or `backups/`.
- Back up `therook.db` before schema or migration changes.
- Rotate the Discord token if it is printed, pasted, committed, or shared.
- Be careful with Discord role hierarchy: nickname, role, and channel-management features can silently fail if the bot's role is too low.

## Gemini Conversation Summaries

`/summarize` uses the Gemini Developer API. Create a key in [Google AI Studio](https://aistudio.google.com/apikey) for a project on the **Free Tier**, and add these values to the bot's `.env` without committing the key:

```env
GEMINI_API_KEY=your-gemini-api-key
GEMINI_SUMMARIZE_ENABLED=true
GEMINI_SUMMARIZE_MODEL=gemini-3.1-flash-lite
```

The model setting is optional and defaults to `gemini-3.1-flash-lite`. After deploying the code/configuration, restart `therook.service`; the normal startup sync publishes the command. Set `GEMINI_SUMMARIZE_ENABLED=false` and restart to disable use. No new package, schema migration, or background service is required. If configuration is missing, the command explains that summarization is not enabled and fetches no history.

Google's [pricing table](https://ai.google.dev/gemini-api/docs/pricing#gemini-3.1-flash-lite) lists free-tier input/output for this model. Availability and quotas depend on the project; inspect its limits in AI Studio. Keep the project on Free Tier to avoid charges: the bot cannot select or verify the billing tier using a generation parameter. A key from a paid project uses that project's billing. There is no automatic paid-model fallback or retry on quota exhaustion. The bot's one-minute guild cooldown and two-job concurrency cap are burst protection, not a spending cap.

The bot sends selected ordinary message/reply text, display names, timestamps, short per-request source references, and the optional focus instruction to `generativelanguage.googleapis.com` over HTTPS using the key in the `x-goog-api-key` header. It does not send attachments, resolve URLs, enable tools, or write transcripts/summaries to its logs, database, or files. Summaries are ephemeral Discord replies by default; `public:true` publishes all parts in a new public thread under the invoking channel, or reuses the invoking thread, where they remain as ordinary Discord messages. Both caller and bot need Send Messages in Threads; new threads also require Send Messages and Create Public Threads in the parent. Announcement channels receive a short starter notice. Optional `words` selects an approximate 50–1000-word target within the existing output limits. `language` offers Czech (`cs`, default), Auto (based on the conversation), and English (`en`); explicit selections override language requests in `focus`. These options apply per invocation and require no additional configuration. Google's pricing page lists free-tier content as used for product improvement, subject to its [API terms](https://ai.google.dev/gemini-api/terms); local non-storage is not a promise of zero provider retention. Review the applicable account/region terms when choosing which conversations to summarize.

Gemini summary HTTP failures log only the numeric HTTP status and transcript character count, never response bodies, source text, focus, or API keys. Generic provider errors also show the HTTP status in the private reply; authentication and quota errors retain their dedicated messages. Inspect these statuses in `journalctl -u therook.service` before attributing a failure to provider availability.

Validate locally with `.venv/bin/python -m pytest tests/test_summarize.py tests/test_command_registration.py`. These tests mock Discord and Gemini and consume no API quota. A live check requires a configured key and an invocation such as `/summarize count:20` in a channel with suitable history permissions.


### Conversation export operations

`/export` works without Gemini configuration. Deploy with the bot and restart `therook.service` to publish it. Its history guard is shared with `/summarize`; no separate worker or service is required. Exports are bounded to 2,000 messages, 32 MiB of rendered text and four minutes, with one running export globally and a five-minute server cooldown. Discord history pagination/rate-limit waits remain asynchronous; files are built off the event loop and uploaded privately. No permanent local export archive or migration is created; attachments remain subject to Discord availability. Tests: `.venv/bin/python -m pytest tests/test_export.py tests/test_summarize.py tests/test_help.py tests/test_command_registration.py`.


Conversation-command security checks fetch current member roles and channel/parent overwrites through Discord REST before reads and delivery (each summary chunk/export upload batch). These checks add bounded API requests under the existing timeout; failed verification stops delivery. Export permission failures do not consume the five-minute server cooldown; a separate 15-second per-user/server attempt limit applies. Export Markdown now contains complete fenced JSON records with losslessly escaped text. This preserves structural separation, but does not guarantee that an external AI will resist all semantic prompt injection. Summaries are limited after citation rendering to 10,000 characters and six messages; links remain intact across chunk boundaries. No database or deployment topology changes are needed.

## Meme Tracking Deployment

Meme tracking ships in the existing `therook.service`; no new package, environment variable, or service is required. Before restarting, take a consistent SQLite backup using the SQLite backup API. Restart with `sudo systemctl restart therook.service`, then verify `systemctl is-active therook.service` and the journal's successful global slash-command sync. Startup creates the additive meme tables automatically. The documentation web root is served directly, so the updated `/help memes` page needs no nginx reload.

An admin enables tracking with `/memes settings enabled:true`. New settings use 🤡, two votes (Rook can supply one), one meme per Prague calendar month, and a jokey Czech warning for each newly confirmed current-month meme above the allowance (once per post). Default sources are `tenor.com`, `media.tenor.com`, `giphy.com`, and `media*.giphy.com`. Set `channel` to restrict tracking to a text/forum channel and its threads; otherwise it covers accessible server channels. Existing explicitly saved emoji settings are preserved; set `emoji:🤡` to override them. Turning the feature off uses `/memes settings enabled:false` and retains its records.

The bot needs channel visibility, Read Message History, Add Reactions, and permission to reply (including Send Messages in Threads where applicable). Verify enablement and sources with `/memes settings` and `/memes source list`, then use `/memes leaderboard` to inspect totals. Source matching contributes one vote, so a matching GIF is not automatically confirmed at the default two-vote threshold. See [the command reference](commands.md#meme-tracking) for period queries and corrections.

The per-post warning upgrade adds `meme_message_warnings` during startup and preserves legacy monthly reservations. Take a SQLite backup before restart. Known posts from already-warned user/months are conservatively marked handled; new posts can each trigger a joke. No enablement or other settings are reset.

## Opt-In Armory 2.0 Trial

The bot now requires `discord.py>=2.7.1,<3.0` for Components V2. Database startup adds V2 presentation state/triggers without converting existing armories or boards. Deploying the code alone posts no V2 board. After normal deployment, a GM/admin can create one with `/armory2 board-set channel:#testovani-bota source:#zbrojnice`; the source must be on the same server and already contain an armory. The two interfaces share live stock and history. Use an existing test-server armory when experimenting with disposable data.

`/armory2 board-disable channel:#testovani-bota` is the interface rollback: it leaves all stock, loans, audit history, original messages, and mirror configuration intact. `/armory2 board-status` and the `armory_v2_retry` loop in `/status` provide operational visibility. If rolling back the application version, disable V2 boards first; older code will not service their persistent controls. No schema rollback or inventory restoration is needed to stop using V2.

The V2 full public inventory upgrade adds `armory_v2_chunks` at startup. Take a consistent SQLite backup and restart `therook.service`; startup invalidates existing V2 layouts and republishes their full inventory. Large inventories use multiple public messages with controls at the end; size changes or missing chunks rebuild the sequence. V1 and shared stock are preserved. Verify published/revision equality and a clear `last_error` in `armory_v2_boards` after refresh.

Restoring the compact V2 overview requires a bot restart. Startup refresh removes surplus public item-button messages and publishes text inventory with weights and shared controls. No additional schema or inventory migration is needed.

## Automatic threads

After deploying the code and restarting the bot, normal database initialization creates `autothread_channels` and `autothread_messages`, and normal slash-command synchronization publishes `/autothread`. Nothing is enabled automatically. Grant the bot View Channel, Read Message History, Create Public Threads, and Manage Threads in each target text channel, then use `/autothread channel:#novinky enabled:true`. Participants need Send Messages in Threads. Check the saved setting with `/autothread channel:#novinky`.

Source deletion permanently deletes the managed thread and all replies. `enabled:false` stops new creation but retains deletion tracking for existing managed threads. Keep thread-management/history permissions available for cleanup. The `autothread_cleanup` loop in `/status` checks 25 records per minute and retries failed or offline deletions; large tracked populations take multiple passes. Logs report creation, lookup, permission, and deletion failures without storing message bodies. New posts missed while offline are not backfilled. Include both tables in normal SQLite backups. Rolling back to a version without this service stops automatic creation and deletion processing.

Startup initializes/migrates SQLite before connecting to Discord, then rechecks it in `on_ready`. This ensures messages arriving during initial guild loading can use newly added tables. A pre-connect database failure stops startup; inspect the service journal before retrying.

Automatic threads include new bot posts (Rook included), webhook announcements, and public command replies in already-enabled channels. No extra option or schema migration is needed. Restart the bot after this update; system/ephemeral messages and messages inside threads remain excluded, and existing settings are preserved.

## Thread monitoring in status

`/status` now lists every registered loop, including `autothread_cleanup`, `armory_v2_retry`, and `wish_board_retry`, and shows current-server thread operation results. `/rook status` provides global totals to configured operators in bot DMs. Thread creation, autojoin membership requests, and cleanup checks each report success/failure counts and last-success/last-error timestamps since restart. Errors expose only exception type and HTTP status. Source tracking and known pending-deletion counts are read from SQLite and survive restart; event counters do not.

The automatic-thread cleanup heartbeat advances only after an error-free batch, including empty batches; failed checks retain their records for retry. Wish-board retries also now report successful passes. Check both the last successful pass and known pending deletions: a running loop is not proof of completed cleanup, and deletions missed while offline are absent from the pending count until discovered. No attempts can simply mean no eligible activity. Both status replies split into Discord-sized messages with mentions suppressed. Deploy by restarting the existing service; no configuration or database migration is needed.

## Scoped autojoin deployment

Take a consistent SQLite backup before restarting `therook.service`. Startup creates `thread_autojoin_scopes` while retaining existing server-wide subscriptions in `thread_autojoin_users`; normal slash-command synchronization publishes the new `/autojoin` options (`channel`, `category`, `server`). Verify the journal's online and successful global command-sync messages, then test `/autojoin enabled:true` in the desired channel or select an explicit target. `/autojoin` lists all subscriptions privately. Existing server-wide subscribers must remove `server:true` before limiting themselves to selected channels/categories. No backfill occurs. Include both subscription tables in backups. An older application version ignores scoped subscriptions but continues to honor the original server-wide table.

## Optional archive-role deletion deployment

Restart `therook.service` and verify successful global command synchronization to publish `/archive delete_role`. No schema migration or automatic role deletion occurs. To test, select an explicitly disposable game role with `/archive channel:#game delete_role:@role`: successful channel archival produces a separate private five-minute confirmation. Cancelling leaves the role intact. The bot needs Manage Roles and its highest role must be above the target. Confirmation checks cached channel/category overwrites and stored role links for other active usage; only categories named `Archive N` are excluded. Successful deletion clears saved channel-role links for that role in the current guild. Pending confirmations expire and are not restored after restart. The `/gming` panel retains its existing independent role-deletion flow.

## Hranice Goals Board Configuration

On 10 October 2026 the Symbane: Hranice server-wide goals collection was updated to **Hlavní cíle**, **Výpravy**, **Menší cíle**, in that order. Hráčské cíle was renamed without changing its stable category key or goals; Výpravy was created empty with `everyone` permissions and author display; the empty Další cíle category was removed. The existing bot repair loop published each committed change. Consistent SQLite backups taken before the edits are stored privately under `/home/ubuntu/.local/share/therook-goals-backups/`. Other collections and global category defaults were unchanged.

## RSVP rescheduling deployment and duplicate repair

Take a consistent SQLite backup before restarting `therook.service`. Startup adds/backfills `game_rsvp.confirmed_game_at` and adds `game_rsvp_messages.refresh_pending`; current-session pending posts refresh at startup and in the five-minute reminder loop. Existing boards follow date changes and count as an already-sent reminder for the new term, preventing a duplicate automatic post. Verify online/global command-sync logs and pending-board delivery.

The 12 October 2026 repair kept the original RSVP board active, verified its updated date and attendance through Discord, and replaced the duplicate post with a link to the original and no buttons. Responses were preserved because both posts shared the same occurrence records. SQLite and message backups, plus the narrowly scoped repair script, are private under `/home/ubuntu/.local/share/therook-rsvp-repairs/`; the script expects exactly one current-session duplicate group and must not be rerun without checking its scope.

## Reminder rollout and recovery

Normal startup additively creates `reminders`; normal command sync publishes `/remindme`, `/reminders`, and `/remindcancel`. Include the table in consistent SQLite backups. Verify the `reminders` loop in `/status` and schedule a disposable `1m` reminder in a test channel/thread. Confirm private setup, requester-only mention, and original-channel delivery; repeat across a restart. The loop checks every 30 seconds, catching up overdue reminders after downtime. Ordinary archived threads reopen when delivering; locked threads are left locked and marked failed. No new environment variables or service units are needed.

Delivery failures retain exception names/fixed reasons only. Requesters can inspect `/reminders`, cancel failures, and recreate after fixing access. Transient failures retry every five minutes. Do not run multiple bot processes against the same database: delivery locking is process-local. A crash between sending and deleting the row may duplicate a reminder; restoring an older backup can also resend previously delivered reminders. Restore the database with the service stopped as described above.

The reminder `dm:true` upgrade additively adds `reminders.dm` (default `0`, preserving existing destinations). Back up SQLite before restart and verify command sync. DM delivery uses the same loop and retry policy; blocked DMs remain failed, and never fall back to a public channel. No additional loop or configuration is required.

New reminders now default to DM delivery; `dm:false` explicitly selects the original channel/thread. Restart and synchronize commands to publish the default. Existing reminder rows and destinations remain unchanged; no new migration is needed for this default change.

## Reminder feature rollout

Back up SQLite before restarting. Startup adds `source_message_id`, `repeat_spec`, `repeat_anchor`, `repeat_time`, and `parent_reminder_id`, plus `reminder_controls`, `reminder_actions`, and indexes. Existing deadlines, destinations, notes, quotas, and failure states remain unchanged; old rows remain one-shot. Normal command sync publishes `/remindshare`, optional `/remindme at:` / `repeat:`, and the message context menu **Připomenout zprávu**. The two persistent views register on ready; existing loops are reused. Keep all three reminder tables in backups.

Verify on disposable test reminders: a Czech specific date; a repeating schedule; message Apps → **Připomenout zprávu**; owner-only snooze after delivery; restart and snooze; an explicitly shared panel with a second user subscribing; blocked-DM failure without public fallback; cancel owner/subscriber independently. Repeat minimum is an hour, so automated tests simulate recurrence timestamps. No live test reminders are created during automated validation. Delivered note snapshots are retained for 30 days for snooze, and share-control IDs for 396 days; indexed cleanup runs in the existing 30-second loop. Publicly shared panels remain in Discord until manually deleted. A restored old backup can replay deliveries; a crash between sending and committing can also duplicate them.
