diff --git a/README.md b/README.md index e4ba50a3..a3e3a536 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ It connects the Minecraft world with a browser experience: players can explore t - **Character creation and profiles** — create characters and manage their profiles, wardrobes, and kits through the website. - **Custom skin submissions** — submit supported cosmetic skins through an in-game-code-linked workflow. - **Drink design** — create BreweryX recipes and submit custom drinks for staff review. +- **Accounts and staff tools** — sign in with Discord, manage linked accounts and supporter status, and use role-controlled player, activity, rail, and permission panels. - **Player knowledge hub** — browse the wiki's guides to crafting, professions, equipment, economy, and other server features. ## Connected to the server @@ -25,22 +26,22 @@ It connects the Minecraft world with a browser experience: players can explore t Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs). -Each branch gets a preview at `https://.tfminecraft.net`, merging to `main` -deploys `https://dev.tfminecraft.net`, and **Actions → Deploy** puts `main` live on -`https://www.tfminecraft.net`. See [Previews and deploys](https://github.com/TF-Minecraft/Docs/blob/main/projects/ProvinceSystem/DEPLOY.md). +[Previews and deployment](https://github.com/TF-Minecraft/Docs/blob/main/projects/ProvinceSystem/DEPLOY.md) ## Tests -CI runs the frontend Vitest suite and the backend pytest suite, then builds the -Next.js frontend. To run the same suites from the repository root: +CI uses Node 22 and Python 3.12. From the repository root, install dependencies +and run the frontend Vitest and backend pytest suites with the same JUnit output: ```sh -(cd frontend && npm ci && npm test) -(cd backend && python -m pip install -r requirements.txt -r requirements-dev.txt && python -m pytest -q) +(cd frontend && npm ci && npm test -- --reporter=default --reporter=junit --outputFile.junit=../test-results/frontend.xml) && +(cd backend && python -m pip install -r requirements.txt -r requirements-dev.txt && python -m pytest -q --junitxml=../test-results/backend.xml) ``` -The suites cover the website and API in isolation; they do not run a Minecraft -server or the companion plugins. +CI uploads `test-results/frontend.xml` and `test-results/backend.xml`, then builds +the Next.js frontend. No coverage threshold is enforced. The suites exercise +website components, application logic, and APIs in isolation; they do not run a +live Minecraft server, companion plugins, or real external-service flows. ## License diff --git a/backend/src/coreprotect/README.md b/backend/src/coreprotect/README.md index c66245c8..943f1258 100644 --- a/backend/src/coreprotect/README.md +++ b/backend/src/coreprotect/README.md @@ -1,154 +1,7 @@ # CoreProtect reader -The staff panel's player pages read the Minecraft server's live CoreProtect -SQLite database: who has played, their sessions, and their recent actions. -`src/auth/players.py` joins that with `discord_links`, `users` and -`character_roster`; the routes are `GET /admin/players`, -`/admin/players/{uuid}`, `/sessions` and `/activity` (`view_players`, mod and -above). Movement, `GET /admin/players/{uuid}/movement` and `/admin/movement` -for everyone, is for admins and the owner (`view_player_movement`). +Read-only CoreProtect activity, session, and movement data for the staff panel. -## Settings +[Configuration and technical reference](https://github.com/TF-Minecraft/Docs/blob/main/projects/ProvinceSystem/docs/integrations/coreprotect.md) -| Variable | Meaning | -| --- | --- | -| `COREPROTECT_DB` | Path to `database.db` inside the backend container. Unset: the directory still lists linked players and characters, and CoreProtect sections say they are unavailable. | -| `COREPROTECT_SERVER` | Short id that scopes caches and page cursors (default `main`). | -| `COREPROTECT_SERVER_LABEL` | World name shown with the data, for example `Vardera`. | -| `COREPROTECT_PING_SECONDS` | The server's `player-pings` interval (default 60; `0` if pings are off). Used to guess whether someone is online, to end crashed sessions and to tell a gap in a movement path. | -| `COREPROTECT_MAP_WORLD` | The CoreProtect world the site's map shows: the server's `level-name` (default `TFMC_Map`). Movement in other worlds is summarised, not drawn. | - -Mount the CoreProtect **directory** read-only, so SQLite can see a hot -`-journal`. Set `disable-wal: true` in CoreProtect's `config.yml` (then -`/co reload`): our fork uses WAL by default, and through a read-only mount -SQLite can open a WAL database only while its `-wal` and `-shm` files already -exist. CoreProtect closes its connections between batches, which removes -them, so in WAL mode the site mostly reports `cannot_open`. The option is -appended to `config.yml` once and applied on every start or reload; CoreProtect -only ever appends missing options to that file, so updates keep it. The paths are host-specific, so they belong in the host's -`docker-compose.override.yml`, not the repository's compose files: - -```yaml -services: - backend: - volumes: - - /home/amp/.ampdata/instances/TFMCMain01/Minecraft/plugins/CoreProtect:/coreprotect:ro - environment: - - COREPROTECT_DB=/coreprotect/database.db - - COREPROTECT_SERVER=main - - COREPROTECT_SERVER_LABEL=Vardera -``` - -Docker resolves the bind as root, so the AMP tree needs no permission -changes. The mount also exposes CoreProtect's `config.yml`, which holds -connection settings for database engines the server does not use. - -## Keeping CoreProtect's writer unblocked - -With `disable-wal: true` the database uses a rollback journal, so a -reader's lock holds off CoreProtect's commits. `reader.py` explains the guards: autocommit, one -reader per process, a 1.5 s budget per request (waiting for the lock -included), and every statement fully fetched before the next. The backend -runs as one uvicorn process; running more would need the reader limit -shared between them. Never open the file with `immutable=1`: on Main that -read torn pages while CoreProtect wrote. - -Candidate scans are indexed seeks by player; final-page metadata reads use -row IDs. The only whole-table reads are the small tables holding -CoreProtect names, including `co_user` and `co_username_log` (a few hundred -to a few thousand rows). Activity pages examine at most `SCAN_LIMIT` rows per -table, so a sparse filter stops early and returns a cursor to continue -(`searched_to`). - -## What is shown - -For moderators, chat is never read and commands are cut to their first word -inside SQL. Admins and the owner (`view_player_messages`) also get chat and -whole commands, each cut to 512 characters; every page that shows any is -recorded in `admin_audit` as `player.messages.view` (which rows, never the -text) before it is returned, and the page is refused if that record cannot -be written. Sign text is never selected. After selecting the final activity page, -point reads -fetch at most 64 KiB per item, entity, or identity blob for displayed rows. After closing the -reader, a bounded, read-only Java-stream/NBT decoder extracts only custom names -and MMOItems type/ID; raw metadata, lore, and other tags never enter API responses. -Decompression is capped at 256 KiB, with depth and node limits. Malformed, -oversized, or unsupported metadata retains the vanilla label. - -Item and container labels include the recorded MMOItems identity beside the -saved name (including alloys). Named vanilla items are explicitly marked renamed. -Historical mob kills show their saved name as a name, not a proven mob type. New -kills from the custom-identity CoreProtect build additionally show the MythicMobs -ID from the `coreprotect:mythic` block-metadata marker. No history rewrite is -needed. Custom-ID search and ItemsAdder block identity are outside this change. - -Each activity entry also carries `target_info`: the name the panel shows -(`Short Grass`, not `short_grass`), where it came from (vanilla, MMOItems, -MythicMobs, a player) and the ids behind it. Vanilla names come from -Minecraft's own `en_us.json`, which is Mojang's to publish, not ours: the -backend image runs `scripts/build_minecraft_names.py` to fetch the pinned -client jar, check its SHA-1 and keep the block, item and entity names -(about 94 KB) in `data/minecraft_names.json`, which git ignores. Bump the -pinned version there when the server updates. Without the file, ids are -tidied instead. Block changes use block names (`wheat` is "Wheat Crops"), -items use item names ("Wheat"); only `minecraft:` ids are looked up. -Activity responses also return `server_label` and `map_world`, so the panel -calls the map's world by its name (Vardera). The panel merges consecutive -changes to the same block within a minute into one row; the API does not. - -RPCharacters chat sent by command (`/looc hi`, `/fooc`, `/me`, `/shout`…; -the aliases are `CHANNELS` in `activity.py`, from Main's `chat.yml`) is chat: -for admins it comes back as kind `chat` with its `channel` and the text after -the command, under the Chat filter and audited like chat; moderators still get -only the command word, with the `channel` named. Plain chat goes to the -player's current channel, which CoreProtect does not record, so it is worked -out (`channel_inferred`): RPCharacters keeps the channel picked with -`/channel ` in memory until the player quits, so a line's channel is -the last valid switch since their latest login, else RP. That looks back at -most 5,000 commands; further than that, or with no login recorded, the -channel is left unknown. Character-creation answers typed in chat are not -channel chat but are labelled as if they were. - -Sessions are rebuilt from login, logout and ping rows; see `sessions.py` for how crashed sessions end. - -Movement is a player's session rows in a window (up to 7 days for one -player, 24 hours for everyone): logins, logouts and a position ping once a -minute, so the path between pings is a guess. Every request is recorded as -`player.movement.view` (who, which player or everyone, the window and how -many rows, never positions) before it is returned, and refused if that -record cannot be written. Rows are read newest first in batches of 5,000, -one statement each; a window holding more than the limit keeps the newest -rows and reports `complete_from`. - -## Deployment gate - -`backend/benchmarks/coreprotect_reader.py` measures both sides: - -```sh -# Lock hold time per request against a live database (opens it immutable, benchmark only) -python3 benchmarks/coreprotect_reader.py hold --no-lock /path/to/database.db -# A disposable database at Main's size, then writer impact on it (contend writes) -python3 benchmarks/coreprotect_reader.py synth /tmp/main-synth.db -python3 benchmarks/coreprotect_reader.py contend /tmp/main-synth.db --readers 1 -``` - -Acceptance: `hold` p99 ≤ 100 ms and max ≤ 500 ms; `contend` with no writer -failures and writer transaction p99/max up by no more than 100/500 ms. - -Results on 2026-10-06: - -- Movement, inside Main's backend container (60 requests): everyone over - 24 h (26,900 rows, 10 statements) 33 ms per request, longest statement - 58 ms, usually under 8 ms; one player over 7 days (3,155 rows) 4 ms. Do - not time live reads through `~/work/amp-readonly`: it is a FUSE mount that - does not pass file locks through, so SQLite sees CoreProtect's journal as - hot and fails with "attempt to write a readonly database". - -- `hold` on live Main (7.4 GB, 242 players, 1,455 requests): p99 8.6 ms, - max 18.5 ms. -- `contend` on a 6.5 GB synthetic Main, one reader (as in production): - writer p99 +10 ms, max +18 ms, no failures, no reader errors. -- Three readers making back-to-back requests: writer p99 +18 ms, max - +14 ms, throughput −5%, no writer failures; 2% of reader requests gave - up as busy or out of budget. The synthetic writer spills its cache on - every batch, so this is harsher than CoreProtect. +Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs). diff --git a/backend/src/luckperms/README.md b/backend/src/luckperms/README.md index 874d5652..c029f84d 100644 --- a/backend/src/luckperms/README.md +++ b/backend/src/luckperms/README.md @@ -1,133 +1,7 @@ # LuckPerms in the staff panel -The website shows and edits LuckPerms without touching its database. One -server's TFMCWeb (Main's) is the bridge: it publishes a snapshot of LuckPerms -to the site and applies changes that staff queue on the site, through the -LuckPerms API. Dev and Main share one LuckPerms MariaDB, so exactly one server -may apply changes. Another server may publish to its own site (Dev publishes to -dev.tfminecraft.net) without applying, which makes that site read-only. +Permission snapshots and queued staff changes through the TFMCWeb bridge. -## Rights +[Configuration and technical reference](https://github.com/TF-Minecraft/Docs/blob/main/projects/ProvinceSystem/docs/integrations/luckperms.md) -| Who | May | -| --- | --- | -| mod | view everything: groups, tracks, every player's groups and permissions, change history | -| admin | add or remove the groups in `policy.yaml` `admin_groups` on players, promote/demote players along tracks between those groups, set or unset player permissions matching `admin_permissions` but not `root_only_permissions` (such as `armourshop.admin`) | -| root | everything, including staff groups, other permissions, meta (prefix/suffix/weight) on players, group definitions and tracks | - -A listed group counts as an admin group only while everything it inherits, in -any context, is listed too, so a root edit to a group definition cannot hand -admins a staff group by inheritance. Unknown permissions, wildcards and regex -nodes are root-only. - -Admins may not change anything on their own Minecraft account, on a player -whose linked website account is admin or root, or on a player who holds or -inherits a root-only group (in-game staff, linked or not). Rights are checked -when a change is queued and again when the bridge collects it, so a demotion -in between cancels it (`no_longer_allowed`). Every change carries a -reason and is written to `admin_audit`, and LuckPerms logs it (`/lp log`) with -the source `web:`. - -## Plugin protocol - -All routes need the primary `X-Plugin-Key`; secondary keys are refused. - -### Snapshot - -`PUT /luckperms/plugin/snapshot` (JSON, at most 16 MiB): - -```json -{ - "server": "main", - "generated_at": 1791321779, - "revision": 1791321779123, - "hash": "", - "groups": [{"name": "staff", "display_name": null, "weight": 200, "nodes": [NODE]}], - "tracks": [{"name": "staff", "groups": ["staff_player", "staff_inactive", "staff"]}], - "users": [{"uuid": "…", "name": "drefvelin", "nodes": [NODE]}] -} -``` - -`NODE` is `{"key": "group.staff", "value": true, "contexts": {"server": ["main"]}, "expiry": 0}`; -`expiry` is unix seconds, 0 for permanent; `contexts` maps each key to its values (empty object for global). -`users` holds every user with at least one stored node (`UserManager.searchAll` with an empty key prefix). -The site replaces its whole mirror in one transaction and answers `{"ok": true}`. - -`revision` orders everything the bridge reports: it strictly increases across snapshots and -results, also across restarts (the bridge uses `max(last + 1, current time in ms)`). The site -ignores a snapshot or result state whose revision is not above the newest it has stored -(answering `{"ok": true, "stale": true}`), so a request delayed past a newer one cannot undo it. - -`POST /luckperms/plugin/snapshot/unchanged` `{"hash": "…", "revision": …}` answers `{"ok": true}` and -marks the mirror current when the hash matches the stored one, else `{"ok": false, "need_full": true}`. - -The bridge builds a snapshot every `snapshot-seconds` (default 30) and right after applying changes, -on the same single worker thread that applies changes, so a snapshot never predates an applied change. - -### Changes - -`GET /luckperms/plugin/changes` returns up to 20 changes in id order and records the poll time -(the site shows the bridge as connected while polls are recent): - -```json -{"changes": [{ - "id": 12, - "target_type": "user", // user | group | track - "target": "5b0c…-uuid", // user UUID, group name or track name - "target_name": "drefvelin", - "actor_name": "web:w.o.n", // LuckPerms action source name - "actor_uuid": null, // the actor's linked Minecraft UUID, if any - "description": "parent add staff", // LuckPerms action log text - "ops": [OP], - "guard": {"admin_groups": ["default", "commoner"]} // admin changes only -}]} -``` - -A group or track change is handed out on its own, and nothing else is handed out until its -result arrives, so later changes are checked against the definitions it leaves. - -`guard` is on changes an admin made. The mirror can lag in-game edits, so the bridge checks live -LuckPerms before mutating: the player must hold no group (any context) outside `admin_groups` -(else `target_is_staff`), and every group an `add_node` grants must inherit only groups in -`admin_groups` (else `root_only`). - -Ops, applied in order to one holder and saved once: - -| target | op | fields | precondition | -| --- | --- | --- | --- | -| user, group | `add_node` | `node` | the exact node (key, value, contexts, expiry) is absent | -| user, group | `remove_node` | `node` | the exact node is present | -| group | `create_group` | | the group does not exist | -| group | `delete_group` | | the group exists | -| track | `create_track` | | the track does not exist | -| track | `delete_track` | | the track exists | -| track | `set_groups` | `groups` (ordered) | the track exists and every group exists | - -The bridge checks every precondition in op order against a staged copy of the live LuckPerms data -(so create-then-add and remove-then-add work) before mutating anything; if one fails, nothing is -mutated or saved and the result is `{"ok": false, "error": ""}` with codes -`node_exists`, `node_missing`, `group_exists`, `group_missing`, `track_exists`, `track_missing`, -`bad_op`, `bad_target`, `save_failed`. A `group.` node whose group does not exist is `group_missing`. -If saving fails after mutating a loaded holder, the bridge reloads that holder from storage so the -server does not keep an unsaved change in memory. After saving a user it pushes a user update; after -a group or track change it pushes a full update. It submits one LuckPerms action per change -(source UUID: the actor's linked Minecraft UUID, else the nil UUID). - -`POST /luckperms/plugin/changes/results`: - -```json -{"results": [{"id": 12, "ok": true, "error": null, "revision": 1791321780456, "state": STATE}]} -``` - -`STATE` (optional) is the target after the change: a user `{"uuid", "name", "nodes"}`, a group -`{"name", "display_name", "weight", "nodes"}` or `null` (deleted, or tracks). The site updates its -mirror for that target at once. The bridge keeps results it could not post and posts them again -on later ticks until the site answers `ok`. - -Site side, a change is `pending` until fetched, then `sent`, and the site never offers it again: a -change is applied at most once. A sent change with no result after 300 s becomes `unknown` (staff -are told to check the player); a late result still settles it. A change not fetched within -10 minutes `expired`. Both timeouts are settled on staff reads too, so they happen even when the -bridge is gone. New changes are refused with `bridge_offline` unless the bridge polled in the -last 60 s, and always on a site with `LUCKPERMS_READ_ONLY=1` (dev.tfminecraft.net, whose server -shares LuckPerms with Main). +Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs). diff --git a/backend/src/patreon/README.md b/backend/src/patreon/README.md index 134f0cb8..9bbe9a99 100644 --- a/backend/src/patreon/README.md +++ b/backend/src/patreon/README.md @@ -1,143 +1,7 @@ -# Patreon backend handoff +# Patreon backend -The tier mapping is `tiers.yaml`. New tables are in `src/skins/schema.sql`, -created by the existing `src.skins.db.migrate()` in the app lifespan. They -share `province.db` with `discord_links`. Patron linking lives in `linking.py`: -hashed single-use OAuth state and confirmation tokens, `link/start`, -`oauth/callback`, `link/pending`, `link/confirm`, `link/cancel`, and `webhook`. +Supporter linking, entitlement decisions, and delivery to Discord and Minecraft. -## Explicit OAuth link confirmation +[Configuration and technical reference](https://github.com/TF-Minecraft/Docs/blob/main/projects/ProvinceSystem/docs/integrations/patreon-backend.md) -`POST /patreon/link/start` keeps the existing staff (Discord), plugin -(Minecraft), and profile-session entry points. It stores the target kind and -the supplied `discord_username` or `minecraft_name` with the subject and -hashed state. Profile sessions always use the session UUID and ignore supplied -names: resolve the Minecraft name from `discord_links`, then the most recent -`discord_link_codes` record. If no name is available, display the UUID (or the -Discord id for a nameless Discord request) so the target remains explicit. - -Consent alone does not create a link or grant perks. The callback consumes -the state, exchanges the code, reads the Patreon id and full name, and discards -the patron tokens. It stores the identity and target in `patreon_pending_links` -under a fresh random token, stored only as a SHA-256 hash, valid for 10 minutes. -It redirects to `{PATREON_PUBLIC_SITE_URL}/patreon/linked#confirm=`. -The fragment is never sent in an HTTP request or Referer header. Failure -redirects retain `?status=&tier=` and never contain identities. - -All three public confirmation routes accept `{"token": ".."}` in a POST -body; the token is the credential, with no browser session required: - -- `/patreon/link/pending` returns only `target_kind` (`discord` or `minecraft`), - `target_name`, and `patreon_name`, without consuming the token. -- `/patreon/link/confirm` consumes it before attempting the link, then creates - the OAuth link and recomputes. An unknown member triggers the existing - creator sync before returning an entitlement result. The response is always - `{"status": "ok|not_a_member|already_linked|relink_cooldown|expired|error", - "tier": ""}`. Consumed tokens stay consumed on failures. -- `/patreon/link/cancel` consumes it and creates nothing, returning - `{"status": "ok"}`. - -Unknown, used or expired tokens return `expired` (`{"status": "expired"}` -for pending/cancel, with an empty `tier` for confirm). Responses and callback -redirects disable caching. Logs contain fixed messages/status codes only. -OAuth states without the target kind or display name are treated as expired: -callers must start again so the confirmation page can identify the link target. - -The website removes the fragment from the address bar, loads the pending -names as text, and asks the person to confirm that the target is theirs. -Cancel displays the existing “Link not completed” result. This addresses login -CSRF across all three entry points: someone completing an attacker's consent -URL sees the attacker's account before any link is created. A browser-session -binding cannot establish ownership for links started in Discord or Minecraft. -`POST /patreon/link/unlink` also accepts an absent body for profile sessions; -staff/plugin requests still require their subject through the existing auth -helper. - -## Interface for OAuth and webhook routes - -Import from `src.patreon.service`: - -- `create_or_update_link(patreon_user_id, discord_user_id=None, - player_uuid=None, method="oauth", *, force=False, config=None, conn=None, - now=None) -> str`. Returns `ok`, `already_linked`, `relink_cooldown`, or - `invalid_subject`. Invalid UUID or method raises `ServiceError` with a stable - code. Staff force bypasses cooldown but never subject uniqueness. This - function stores the link but does not recompute; call `recompute_link` next. -- `recompute_link(patreon_user_id, *, config=None, conn=None, - link_success=False) -> dict`. Returns the public status shape, computes - current entitlement, and enqueues changes (or logs them in shadow mode). - Pass `link_success=True` after OAuth. It deduplicates this notification by - the link event. Recomputing also clears previously owned subjects on relink. -- `refresh_member(member_id, *, config=None, client=None) -> dict`. Fetches - one complete API member and stores/recomputes atomically. Returns - `{"ok": true, "members": 1, "brake_held": bool}`, or `{"ok": false, - "detail": "patreon_sync_busy"|"patreon_sync_failed"|"patreon_disabled"}`. - It is synchronous: dispatch it with `asyncio.to_thread` or a background - worker. Webhook handling should retain/retry busy or failed refreshes. -- `sync_now(*, config=None, client=None)` has the same summary contract for - a complete paginated campaign snapshot. Never call the real API in tests; - inject `PatreonClient(..., http=httpx.Client(transport=MockTransport(...)))`. - -Import `PatreonClient` from `src.patreon.client`: - -- `exchange_authorization_code(code, redirect_uri=None) -> dict` returns the - patron token response. Nothing persists this response. Discard it after use. -- `identity(access_token) -> dict` returns the JSON:API `data` resource, with - `id` and `attributes`; it does not persist the patron token. -- `member(member_id)`, `members()`, `creator_get(path, **kwargs)`, - `refresh_tokens()` use authoritative persisted creator credentials. -- `close()` closes an internally owned HTTP client. Errors are `PatreonError` - with fixed, non-sensitive codes. Never log tokens or API response bodies. - -`conn=` on link and recompute functions participates in the caller's -transaction without committing. Confirmation uses -`with src.skins.db.connect() as conn`, `BEGIN IMMEDIATE`, create the link and, -if successful, recompute with that connection. `Config.from_env()` supplies -runtime configuration. Router helpers `require_enabled`, `caller_subject`, -`require_staff`, `require_plugin` are in `src.api.patreon_routes`. Register -future routes on `patreon_router` or on a separate router under `/patreon`. - -## State and delivery behavior - -Unlink retains an inactive link tombstone for cooldown enforcement. Auto-link -runs only when no link row exists for that Patreon user, so an explicit unlink -stays disconnected until the patron links again or staff links them. Missing -identity halves are resolved dynamically from `discord_links`; they cannot -claim a subject explicitly owned by another Patreon link. - -Desired state and applied state are separate. Only an acknowledgement (or an -explicit staff legacy import grant) records ownership of a tier. Outbox rows -not yet fetched may coalesce into a new id. Fetched rows remain immutable; -their acknowledgement records exactly their contents, then queues a fresh -change toward the latest desired tier. Cancelled/other-target ids cannot grant -ownership. Appliers should deduplicate DMs by change id before acknowledging: -HTTP outbox delivery is at least once, so an ack lost after sending a DM can -otherwise replay it. Backend recomputes do not create a second DM for the -same transition. `PATREON_SUPPRESS_DMS=1` still stores that DM on the change -row (so an acknowledgement marks it delivered) but the Discord outbox sends -`dm: null`, including after the flag is turned off, via `dm_suppressed`. - -The persistent removal brake affects both outboxes and rosters. While held, -new removals are not delivered and rosters retain the highest currently owned -tier. Additions continue. Staff release replans against current desired state, -not a stale saved snapshot. Already dispatched changes cannot be recalled. -Release approvals are persisted by desired-state generation, so another sync -cannot immediately re-hold the same reductions while acknowledgements are -still pending. Distinct subsequent transitions remain subject to the brake. - -Shadow mode computes member history, links and desired state, but never -queues or delivers outbox changes. Rosters expose the recorded applied tier -in shadow mode, avoiding reconciliation applying shadow decisions. Switching -apply on causes the next sync to enqueue outstanding differences. - -The lifespan loop keeps a database leader lease across polling intervals; -individual snapshots and member refreshes share a second lease. HTTP calls -renew these leases, and ownership is checked inside the snapshot transaction. -Other workers wait to take leadership after expiry. Shutdown waits for the -current atomic sync and releases leadership. Creator token refresh has its -own lease to protect rotating tokens. - -Emails are returned only by lookup, unlinked and import. Alerts and logging -use fixed messages and tier keys, with no emails, names, tokens or API bodies. -Only the primary `PLUGIN_KEY` may use the rank outbox/roster; secondary server -keys are refused so one server owns LuckPerms application. +Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs). diff --git a/backend/src/rail/README.md b/backend/src/rail/README.md index 5583fa5a..1ab86055 100644 --- a/backend/src/rail/README.md +++ b/backend/src/rail/README.md @@ -1,40 +1,7 @@ # Rail network -The staff panel's Rail tab draws the Minecraft server's rail network on the -live map: every VehicleFramework track, its junctions and ends, broken or -damaged stretches, and the stops along it. `GET /admin/rail?map=` serves -it to admins and the owner (`view_rail`). Track positions are not personal -data, so views are not audited. +Read-only VehicleFramework track data for the staff map. -VehicleFramework saves one JSON file per track in -`plugins/VehicleFramework/data/tracks//`, with switches in -`junctions/` beside them. A track holds a sample about every block and one -segment per pair of samples; a segment can be broken or damaged. The backend -reads the files on request and keeps the result until any file, the map's -`map_markers.json` or its `province_id_runs.bin.gz` changes. A file caught -mid-save is skipped and counted, so the page can say so. +[Configuration and technical reference](https://github.com/TF-Minecraft/Docs/blob/main/projects/ProvinceSystem/docs/integrations/rail.md) -- **Stops** are the settlements whose provinces a track crosses. Each sits - where the track comes closest to the settlement inside its provinces. -- **Lines** are tracks joined by junctions. A line is named after its first and - last stop along its longest track; one with no stops is an "Unnamed line". -- Tracks are simplified (Douglas-Peucker, 0.35 blocks) before they are sent. - -## Settings - -| Variable | Meaning | -| --- | --- | -| `RAIL_TRACKS_DIR` | VehicleFramework's `data/tracks` folder inside the backend container. Unset: the tab says the site is not set up to read the network. | -| `RAIL_WORLD` | The world folder to read: the server's `level-name`. Defaults to `COREPROTECT_MAP_WORLD`, then `TFMC_Map`. | - -Mount the folder read-only in the host's `docker-compose.override.yml`, like -CoreProtect's: - -```yaml -services: - backend: - volumes: - - /home/amp/.ampdata/instances/TFMCMain01/Minecraft/plugins/VehicleFramework/data/tracks:/vehicleframework-tracks:ro - environment: - - RAIL_TRACKS_DIR=/vehicleframework-tracks -``` +Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs).