Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ services:
container_name: augur
restart: unless-stopped
init: true
stop_grace_period: 45s
security_opt:
- no-new-privileges:true
tmpfs:
Expand Down
5 changes: 3 additions & 2 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Capabilities

Augur lets Discord members search Seerr, preview a movie or show, choose TV
seasons, submit a request, and review their recent requests without leaving
seasons across multiple pages, submit a request once, and review their recent requests without leaving
Discord. Members can opt in or out of direct messages for approvals, declines,
and availability.

Server administrators can send pending Seerr requests to a Discord channel as
approval cards. Those cards also cover requests created outside Discord. Augur
polls Seerr for request decisions and availability, remembers delivery state
across restarts, and can expose private health, readiness, and metrics endpoints.
and pending cleanup across restarts. Decision notifications and card repair run
independently of new-request polling. Augur can expose private health, readiness, and metrics endpoints.

For exact commands and permissions, see [Commands and approvals](features.md).
For deployment settings, see [Configuration](configuration.md).
25 changes: 23 additions & 2 deletions docs/development/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

`cmd/augur` loads configuration and creates an `app.Runner`. The runner owns
the Discord connection, Seerr client, SQLite store, optional health server, and
background monitor. Discord handlers ask the runner to perform application
background monitor and delivery worker. Discord handlers ask the runner to perform application
operations; protocol details stay in `internal/discordbot` and `internal/seer`,
and durable state stays in `internal/storage`.

Expand Down Expand Up @@ -30,7 +30,28 @@ so the Manage Server or Administrator permission check is a security boundary.
Account-name matching must never replace explicit Seerr linking.

The runner serializes lifecycle changes and approval reconciliation separately.
Background polling, Discord callbacks, and shutdown can overlap. Keep operations
Background polling, decision delivery, Discord callbacks, and shutdown can overlap. Keep operations
idempotent, make state changes durable before sending notifications where
practical, bound retries and waits, and honor context cancellation. SQLite uses
one connection and WAL mode; do not add parallel database owners.

## Approval recovery

The application serializes Seerr decisions and persists the intended action
before the external write. SQLite owns canonical decision records, recipient
receipts, send leases, backoff and orphan-message cleanup. Discord owns how cards
and notifications are rendered. Approval commands and delivery use the same
claim/finalization path.

The delivery worker repairs cards and reconciles uncertain decisions even when
the pending-request poll fails. A complete, recent pending-ID snapshot avoids
reloading covered requests; other status lookups are shared across guild cards.
The same worker sends decision DMs and performs retention cleanup. There is no
per-card background goroutine. Shutdown and failed startup close interaction
admission, cancel external calls and join accepted work before storage closes.

Request confirmation is claimed atomically per search. An interaction lock also
orders updates to that search's Discord response. Other searches can proceed
independently. Explicit Seerr rejection, an already-requested no-op, and an
uncertain submission have different user responses; uncertain POSTs are never
automatically retried.
6 changes: 3 additions & 3 deletions docs/development/codebase-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
| Path | What lives here |
| --- | --- |
| `cmd/augur` | Process entry point, signal handling, configuration loading, and logging setup. |
| `internal/app` | Application lifecycle, request/approval coordination, polling, health endpoints, and metrics. |
| `internal/app` | Application lifecycle, request/approval coordination, polling, durable-decision delivery, health endpoints, and metrics. |
| `internal/config` | JSON and environment configuration, defaults, normalization, and validation. |
| `internal/discordbot` | Discord session lifecycle, slash commands, buttons, previews, approval cards, formatting, and delivery cache. |
| `internal/discordbot` | Discord session lifecycle, slash commands, buttons, previews, approval controls, card presentation/delivery, formatting, and selection cache. |
| `internal/seer` | Seerr HTTP client, account links, requests, status, and user lookup. |
| `internal/storage` | SQLite schema, migrations, subscriptions, approvals, notification preferences, and scans. |
| `internal/storage` | SQLite schema, migrations, subscriptions, delivery leases, decision intents/jobs, card cleanup, notification preferences, and scans. |
| `internal/safelog` | Log redaction and safe error output. |
| `Dockerfile`, `docker-entrypoint.sh`, `docker-compose.yml` | Container build, startup, ownership, and example deployment. |
| `unraid` | Unraid application template. |
Expand Down
24 changes: 23 additions & 1 deletion docs/development/data.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

- request subscriptions and availability completion;
- approval settings per Discord server;
- approval-message IDs and decisions;
- approval-message IDs, delivery leases and retry deadlines;
- saved decision intents and confirmed decision notification jobs;
- cleanup of sent cards that could not be tracked;
- notification preferences per Discord user; and
- decision-notification deduplication.

Expand All @@ -25,3 +27,23 @@ supported version, preservation of existing rows, repeat startup, and failure
behavior. Keep SQL and migration compatibility in `internal/storage`. Restore
the full data directory for rollback; an older binary may not understand a newer
schema.

## Delivery state in revision 6

A claim token and two-minute lease fence each approval send. Retries honor the
current enabled channel, and acknowledgement/deletion of a card requires its
physical channel and message ID. Backoff grows from 30 seconds to 30 minutes.
Known untracked messages have separate durable cleanup records.

A decision intent preserves the moderator, reason and card presentation before
Seerr is called. An uncertain response is observed without replaying the write.
Confirmed decisions retain their first attribution and queue notification work
before Discord rendering. A single worker resolves current recipient links,
checks preferences, sends DMs and records successful or deliberately suppressed
recipients. Card removal does not remove notification work.

Discord delivery is at least once: a process crash after Discord accepts a message
but before SQLite records the receipt can cause a duplicate. Existing revision 5
receipts are retained; migration does not replay historical decisions. Legacy
blank card claims become recoverable. See [Operations](operations.md) before an
upgrade or rollback.
22 changes: 21 additions & 1 deletion docs/development/operations.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Operations

Run Augur with the smallest practical permissions: one writable `/data`
Run one Augur process per SQLite data directory, with the smallest practical permissions: one writable `/data`
mount, outbound access to Discord and Seerr, and no public inbound port unless
the optional health server is deliberately exposed to a private monitoring
network. Keep the Discord token, Seerr API key, `.env`, `config.json`, data
Expand Down Expand Up @@ -32,3 +32,23 @@ The release sequence is: update [release notes](../releases.md), run the final C
gate, create an annotated `vX.Y.Z` tag, publish the matching GitHub release,
verify the versioned image, and announce any operator action. Repairs that change
Discord messages, Seerr requests, or SQLite state must be explicit and opt-in.

## Revision 6 delivery recovery

Back up the complete data directory before upgrading. Revision 6 preserves
subscriptions, settings and existing notification receipts, recovers interrupted
blank approval claims, and adds durable decision and card-cleanup work. It does
not send historical decision notifications as part of migration. An older binary
requires the matching pre-upgrade data for rollback.

The Compose and Unraid examples allow 45 seconds for a graceful stop. Keep that
allowance when customizing deployment so accepted interactions can finish
persisting state. Retry logs distinguish decision checks, recipient delivery,
card updates and cleanup. Resolve expired credentials or Discord permissions;
queued work resumes with bounded backoff. Do not delete the database to clear a
failed delivery.

A lost Discord acknowledgement can result in a duplicate message because Discord
and SQLite cannot commit atomically. Known extra cards are queued for cleanup.
Keep the data directory private: saved moderator names, decline reasons and
request relationships are part of recovery state.
21 changes: 21 additions & 0 deletions docs/development/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,24 @@ The final gate is the complete `CI` workflow in `.github/workflows/ci.yml` on
the exact revision. It checks formatting, `go mod tidy -diff`, whitespace,
tests, vet, the race detector, pinned Staticcheck and govulncheck versions, the
release build, Docker build, entrypoint behavior, and runtime ownership.

## Recovery and interaction regressions

The deterministic suites cover upgrades from revisions 1–5, repeat startup and
migration rollback; expired claims, changed/disabled destinations, physical-card
fences and orphan cleanup; accepted decisions with lost responses or failed
local persistence; failed DMs, preference suppression and independent card repair;
modal components decoded from Discord JSON; and startup/shutdown draining.

Request-flow tests cover finite but non-exhausted quotas, clearing and merging
season pages, duplicate confirmations, stale response ordering, missing account
link data, ambiguous links across pages, and terminal versus uncertain POST
outcomes. Compare bounded account lookup to serial lookup with:

```sh
go test ./internal/seer -run '^$' -bench BenchmarkLinkedUserNotificationScan -benchtime=5x
```

The benchmark models 20 users with one millisecond of notification-endpoint
latency. The concurrency regression requires four workers and joins all of them;
benchmark results describe that fixture, not production Seerr latency.
26 changes: 21 additions & 5 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
| `/approvals disable` | Disables approval cards for the server. |

Requests use your linked Seerr account and its permissions and limits by default.
The all-seasons option requires an unlimited TV quota. Augur checks availability
The all-seasons option requires an unlimited TV quota. Shows with more than 25
seasons have Previous/Next controls; selections stay selected across pages and
share the same remaining quota. Clear a page's selection to free space.

A confirmation submits once. If Seerr's response cannot be confirmed, check
`/requests` or Seerr before starting another request. Augur checks availability
in the background and sends completion DMs for requests it tracks.

## Approval cards
Expand All @@ -27,7 +32,18 @@ Cards cover pending requests from all Seerr sources, including requests made
outside Discord. Choose a channel whose members may see those requests. The bot
needs View Channel, Send Messages, Embed Links, and Manage Messages there.

Declines can include an optional reason, stored by Augur. Linked requesters can
receive decision DMs according to their notification preferences. Cards are
removed two minutes after a decision. Discord privacy settings must allow DMs
from the bot for notifications to arrive.
Declines can include an optional reason, saved before the decision is sent to
Seerr. A failed update keeps the card available for retry. If Seerr accepted the
decision but its response was lost, Augur checks its status and preserves the
saved reason. A stale button shows the recorded decision and its original author.

Linked requesters can receive decision DMs according to their notification
preferences. Failed deliveries retry after restarts, independently of card
updates. Disabled notifications are skipped; enabling them later does not replay
those past decisions. Discord privacy settings must allow DMs from the bot.

Cards are removed about two minutes after their decided state is displayed.
Failed sends, updates and cleanup retry with backoff. Changing the approval
channel applies to new deliveries; existing cards remain in their original
channel until decided and removed. Disabling approvals prevents further card
sends for that server.
7 changes: 6 additions & 1 deletion internal/app/approval_monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,21 @@ func (r *Runner) reconcileApprovals(ctx context.Context) {
return
}
approvals := make([]seer.ApprovalRequest, 0, len(requests))
pendingIDs := make(map[int]bool, len(requests))
for _, request := range requests {
if ctx.Err() != nil {
return
}
if !seer.IsPendingRequest(request.Status) || request.ID <= 0 {
continue
}
pendingIDs[request.ID] = true
approval, ok := r.pendingApproval(ctx, request)
if ok {
approvals = append(approvals, approval)
}
}
if err := r.bot.ReconcileApprovals(ctx, approvals); err != nil && ctx.Err() == nil {
if err := r.bot.ReconcileApprovals(ctx, approvals, pendingIDs); err != nil && ctx.Err() == nil {
r.logger.Error("reconcile Discord approval messages", "error", err)
}
}
Expand Down
Loading