This document is the implementation-oriented reference for maintainers, API clients, and AI agents. VotingPlugin Control is an optional local-first management plane. It discovers enrolled nodes, coordinates bounded configuration changes, and requests typed read-only inspections. It never receives or processes votes, and VotingPlugin remains fully operational when Control is stopped.
There are four independent lanes:
| Lane | Capability examples | Direction | Can mutate a node? | Persistence |
|---|---|---|---|---|
| Discovery | discovery.read, presence.snapshot |
Node pushes registration/heartbeat/presence | No | Current topology is in memory |
| Configuration | config.files.v1, config.quick-setup.v1 |
Browser queues; node polls and reports | Only after preview and approval | Redacted history and audit are durable; live task input is in memory |
| Inspection | data.inspect.v1 |
Browser queues; Bukkit node polls and reports | Never | Short-lived result is in memory; kind-only audit is durable |
| Deployment | plugin.deploy.v1 |
Browser queues; node polls and downloads under its exact session and attempt lease | Stages a verified JAR only; never reloads or restarts | Artifacts and deployment history are durable |
Connectors always initiate outbound HTTP(S) to Control. No Control feature adds an inbound port to a Minecraft process. One node credential is bound to one stable node ID. Browser sessions and the API automation credential are separate from node credentials.
For a Control-only change, run the focused tests first and then mvn -B clean verify in this repository. Any change to
the negotiated capability map, a node task/result, proxy transport activation, or inspection schema also requires the
paired VotingPlugin/VotingPlugin checkout to pass its full mvn -B clean package verification. Run git diff --check
in every modified repository; run node --check src/main/resources/web/app.js when the static WebUI changes. Control
must remain optional throughout these checks: an unavailable or older peer must reject the new capability cleanly, never
block normal vote processing or server/proxy startup.
Capability negotiation is the compatibility boundary. A node advertises capabilities during registration/heartbeat; Control accepts only the intersection with its own allow-list.
| Capability | Role |
|---|---|
discovery.read |
Current node identity and status |
presence.snapshot |
Full replacement backend presence snapshots from a proxy |
config.proxy-routing.v1 |
SendVotesToAllServers and BlockedServers on a proxy |
config.proxy-files.v1 |
Revisioned, redacted management of that proxy's single bungeeconfig.yml file |
config.files.v1 |
Bounded reads/previews/applies for managed Bukkit YAML files |
config.reward-files.v1 |
Bounded inventory and revisioned editing of existing direct Rewards/<name>.yml files on a Bukkit node |
config.file-comments.v1 |
Preserves Control-managed comment metadata where supported |
config.quick-setup.v1 |
Typed guided settings and reward/site presets |
config.quick-setup.v2 |
Vote Party guided settings including revision-safe Enabled round trips |
config.vote-sites-sync.v1 |
Reward-safe VoteSites merge from one backend to selected targets |
config.transport-test.v1 |
Typed, bounded proxy-to-backend communication check |
config.proxy-method.v1 |
Coordinated preview/apply and acknowledged runtime replacement for a supported network proxy method |
config.proxy-method.v2 |
HTTP proxy-method selection and HTTP backend quick setup; only paired connectors implementing the HTTP contract advertise it |
plugin.deploy.v1 |
Verify and stage a VotingPlugin JAR for the node's next process restart; never hot reload or restart it |
data.inspect.v1 |
Typed read-only data, health, simulation, and diagnostics requests |
Do not infer support from plugin version strings. Check acceptedCapabilities for the exact capability.
Deploy the paired VotingPlugin connector change before enabling Control features that require a new capability. During a
mixed-version rollout, older nodes remain connected but are excluded from v2 HTTP previews and applies.
The dependency-free WebUI is an API client, not a privileged implementation path. Every write below still uses the same authenticated, CSRF-protected endpoint and node capability checks as an external client.
| Area | What the WebUI does | Safety/accuracy boundary |
|---|---|---|
| Navigation and context | Keeps the selected VotingPlugin node visible in the persistent header, groups existing tools in a desktop sidebar/mobile drawer, and provides bounded search shortcuts for pages, settings, and registered nodes | Search does not enumerate players; shortcut pages reuse the existing capability-gated panels; server changes keep the normal stale-result and unsaved-draft protections |
| Overview dashboard | Builds six summary cards, Attention Required, quick actions, topology, logged-service activity, and recent operation activity from the node registry plus existing overview, vote-site-health, and vote-log-summary inspections |
Read-only aggregation; failed, incomplete, or malformed sub-inspections produce a warning rather than a Healthy claim; disconnected registered nodes produce a warning; unknown Minecraft presence is not treated as offline; VoteLog counts are labeled as logged events; actions only open existing safe workflows rather than inventing auto-fixes |
| Network Doctor | Runs diagnostics (which includes the overview fields), combines node health with Control's current topology, and displays checks for connector, configuration, Votifier, vote sites, rewards, logging, and proxy topology |
Read-only; “healthy” is bounded reported state, not a synthetic vote |
| Diagnostics download | Downloads the last Network Doctor result as local JSON | Redacted status bundle only; no raw configuration/logs/player records/infrastructure secrets |
| Activity | Loads the newest 50 live/recovered configuration operation views, labels phases, lineage, reload/rollback, resumes eligible guided preview approvals, and offers retry only when retryable |
Recovered configuration history cannot be retried; approval is single-use and apply is CSRF-protected; proxy-method apply needs a new preview |
| Plugin update | Uploads one bounded JAR, shows the deployment-capable subset, and stages it on those nodes | SHA-256 and JAR identity are verified; session/attempt leases authorize downloads; Control never automatically reloads or restarts nodes; private storage is capped at 32 artifacts / 512 MiB and evicts only artifacts not referenced by retained deployment history |
| Fast file reads | Caches a successful file read for 30 seconds by node ID, node session, and file | Browser memory only; cleared on logout and successful relevant writes; session binding prevents reuse after reconnect |
| Full-YAML drafts | Keeps unsaved editor contents during a registry refresh | A dirty draft is bound to its source node, session, and file; it cannot preview or apply after that session changes. The operator must explicitly confirm a current-file read/reload, which discards the retained draft and rebinds the editor. |
| Proxy configuration | Opens bungeeconfig.yml only for the selected online proxy that negotiated config.proxy-files.v1 |
Fixed one-file capability, never proxy file browsing; redacted READ, PREVIEW, and one-time approved APPLY still apply |
| Configuration drift | Reads the same redacted managed file from two or more selected capable nodes, groups exact revisions, and compares each target with the first successful baseline | Read-only; renders at most 50 differing line pairs per target and truncates each redacted line to 200 characters |
| Snapshots | Creates a named durable snapshot from the last completed file read and loads one document into the editor | Stores the full redacted read result; restore is proposed content and must be freshly previewed/approved |
| Settings catalog | Filters a static schema of commonly managed setting key, file, type, default, and effect | Reference for guided forms, not a generic setting API or claim to cover every VotingPlugin option |
| Setup checklist | Uses live node state plus overview to mark enrollment, topology, vote-site, reward, logging/storage, and communication readiness |
Vote logging is explicitly optional; a check is not an end-to-end vote test |
| Auto-create setup | Reads/previews/applies the dedicated single-setting preset to selected Bukkit targets | Does not overwrite other common settings |
| Vote-logging setup | Reads/previews/applies enabled state, retention, and main-connection choice | Never accepts credentials; dedicated connection details stay in the redacted editor |
| Setup profiles | Stores up to 20 named guided-form profiles in browser localStorage |
Browser-local, versioned, non-secret values only; no raw YAML or credentials; loading never applies |
| Reward builder/simulator | Builds site/every-site/vote-party proposals with commands, player/broadcast messages, items, money, permissions, chance, and online-only behavior; simulates or previews/applies the exact proposal; can copy one command/message into simple Setup | Simulation has no side effects; persistence replaces only the selected Rewards subtree through normal preview/approval; editing invalidates approval |
| Votes & Data | Shows overview, exact player lookup, vote-site health plus persisted unconfigured-service observations, a 30-day VoteLog summary, exact/bounded logged-event search, and correlation trace | Inspection-capable Bukkit node only; trace starts its bounded node set concurrently under one 90-second budget, so a stalled node cannot delay healthy sources; VoteLog reads require logging; results and form inputs are cleared on logout; no player enumeration |
| Safe service-site test | Dry-runs resolution, including optional disabled-site matching and whether auto-create would be considered | Sends no fake vote, creates no site, changes no total, and runs no reward |
The Setup tab replaces the former “Quick Setup” framing but retains existing typed presets, VoteSites sync, detected-plugin command suggestions, preview, approval, node backup, reload, and rollback. Setup profiles are convenience input only; live values should be loaded before modifying an existing configuration. The normal Settings flow performs that read automatically on entry and whenever the selected node or file changes. It clears the prior node's editable state before the request, deduplicates navigation-triggered reads, caches successful reads briefly by node session and file, and invalidates/re-reads after a successful apply. Failure leaves no stale editable value and exposes an inline Retry action. None of these reads can trigger APPLY.
ManagedConfiguration is a tagged union:
proxy-routingmanages onlysendVotesToAllServersandblockedServers;filemanages one allow-listed file:Config.yml,VoteSites.yml,SpecialRewards.yml,GUI.yml,Shop.yml,BungeeSettings.yml, or one validated split file underVoteSites/on a Bukkit node. A proxy node can separately manage only its fixedbungeeconfig.ymlwhen it negotiatedconfig.proxy-files.v1;quick-setupmanages one typed preset with at most 20 bounded string options. Ordinary options are at most 500 UTF-8 bytes; the internal VoteSites sync source may be 512 KiB and areward-builderproposal may be 64 KiB. Both large inputs are stripped from every public operation view.
Control accepts only the fixed preset/option-name schema in ManagedConfiguration, including the small safe fields that a
node may return for a READ result. Proposal creation also enforces each exact required option set. VotingPlugin independently
rejects unknown presets/options and applies phase-specific rules (for example, most READ presets take no options and
reward-builder has no READ form). The WebUI settings catalog is static/versioned guidance over these typed operations;
it does not turn a displayed YAML key into a generic write request.
reward-builder is PREVIEW/APPLY-only and requires exactly options.proposal, a JSON-serialized copy of the typed reward
proposal documented below. It deliberately has no READ form: Control rejects that phase before an operation is queued,
and the node independently revalidates the phase. The selected scope determines the managed file and path, and the plugin
replaces only that path so a second preview is deterministic and cannot leave stale actions behind. Control never returns
the proposal in a public operation view, and the durable operation journal records only the domain/preset.
Public/history-only quick-setup selectors carry an internal non-serialized redacted marker that proposal validation rejects,
so they cannot become executable requests. The node's acknowledged result exposes only the derived target file, not
proposal actions/messages.
config.reward-files.v1 is an additive Bukkit capability for an existing direct
Rewards/<name>.yml file, where <name> is an ASCII letter or digit followed by at
most 99 ASCII letters, digits, _, or -. It is independent from ordinary managed
files and from inspections: an older connector remains connected and simply leaves
named files unavailable. The connector advertises it only when its Rewards directory
supports secure pinned-directory staging. Paths, .yaml, traversal, symlinks,
case-only aliases, files over 512 KiB, and inventories over 100 eligible files are
rejected; a missing Rewards directory has an empty inventory.
The read-only reward-file-inventory inspection takes no filters and returns only
the bounded names list. It requires both accepted data.inspect.v1 and
config.reward-files.v1. A named-file READ, PREVIEW, or APPLY uses the normal
domain:"file" envelope with fileName:"Rewards/<name>.yml", requires the exact
accepted capability, and keeps the usual revision-bound retained READ and one-time
approval. APPLY stages and atomically publishes through the pinned directory,
reloads VotingPlugin, verifies the file is active, and rolls back locally if reload
or verification fails. This capability never creates or deletes files and never
grants access outside Rewards/.
File content is limited to 512 KiB. Node results mask secret-like YAML paths. A replacement secret may pass through an
authenticated proposal, but Control omits file proposal contents from operation views and never records them in its audit.
bungeeconfig.yml is strict UTF-8 safe YAML with duplicate keys, aliases, merge keys, symlinks, and oversized content
rejected. Its comment-aware editor preserves existing comments/formatting where possible and keeps redacted secret values
and their markers safe. It stages a permission-preserving backup and an atomic replacement before reporting a save. A
normal proxy-file save intentionally does not reload or restart the proxy: the result must tell the administrator that
general setting activation still requires a proxy restart.
POST /api/v1/configuration/readqueues a read for 1–100 online nodes that accept the selected capability.POST /api/v1/configuration/previewqueues independent parsing/validation and deterministic change reporting. A fully successful preview returns a random one-timeapprovalToken.POST /api/v1/configuration/applyaccepts only that preview ID and exact unused token. It carries each node revision from preview so a concurrent edit becomes a stale-revision failure.- Nodes stage and atomically replace managed YAML, reload VotingPlugin, and restore the local
.control-backupif reload fails. The result distinguishes reload and rollback from a successful save.
The proxy method preset validates and persists the requested MYSQL, PLUGINMESSAGING, REDIS, MQTT, SOCKETS, or
HTTP method on the proxy and its reported backends. HTTP requires every target to negotiate
config.proxy-method.v2; the other methods use config.proxy-method.v1. The proxy acknowledges its durable result before replacing its runtime, so
the operation result cannot be lost during teardown. Backends reload only their proxy communication handler. If a durable
write or backend handler reload fails, its local backup is restored and the operation reports the failed/rolled-back state
rather than a false network-wide success.
Each target state is QUEUED, IN_PROGRESS, or COMPLETE; the aggregate state is RUNNING, SUCCEEDED, or
COMPLETED_WITH_ERRORS. A claim has a two-minute lease and new attemptId. The result must echo that attempt and the
current node session, preventing a stale execution from completing reissued work.
The claimed ConfigurationTask also contains an exact capability field. Connectors must dispatch and validate the task
against that field rather than deriving a contract from configuration. In particular, an HTTP proxy-backend READ
uses config.proxy-method.v2 while its task configuration deliberately omits the requested method so the current value is
not supplied as an answer. Legacy proxy-method reads carry config.proxy-method.v1. A connector must implement this
field and the matching v2 task/result contract before advertising config.proxy-method.v2; older connectors remain on
v1 and reject unsupported capability work through normal negotiation.
POST /api/v1/operations/{operationId}/retry creates a new operation; it never mutates the historical view.
- The original operation must be complete and have at least one failed node.
- A failed
READorAPPLYretries only failed nodes. Already successful applies are never repeated. - Retrying a
PREVIEWincludes every original target and returns a new approval token after all targets pass. - A coordinated proxy-method apply returns
PREVIEW_REQUIREDinstead of reusing old topology assumptions. - Retry still revalidates current online state and capability support.
Operations are bounded to 1,000 retained entries, with at most 16 file/VoteSites-sync-source operations retained. An
unleased active operation expires 15 minutes after creation; a completed operation is pruned 24 hours after creation.
GET /api/v1/operations returns at most the newest 100, newest first; each view includes sourceOperationId, recovered,
and retryable. Its separate voteLoggingRestartSessions map is derived from every retained operation, so restart-required
warnings are not lost when the WebUI renders only its newest 50 history rows. List entries are summaries whose retained file
bodies are omitted. Fetching
GET /api/v1/operations/{operationId} returns that operation's full bounded redacted result bodies. Both list and detail
views may include an unused completed-preview approval token so an authenticated UI can resume after refresh; applying
still requires the admin role/browser CSRF protection and consumes the token exactly once.
Vote-logging applies (including retries) that share any target are serialized with OPERATION_CONFLICT, making the newest
successful apply's retained session unambiguous even if several previews were prepared concurrently.
Production also atomically maintains an owner-readable, 2 MiB-bounded data/configuration-operations.json. It stores only
operation identity/type/time, redacted domain selector (fileName or preset), retry lineage, and bounded per-node
completion/success/code/revision/reload/rollback metadata. A completed result's backend session ID is also retained so the
WebUI does not lose restart-required setup state when Control restarts. It deliberately excludes options/proposal values,
file content, approval tokens, result messages/changes, credentials, and task attempt IDs.
Journal reads reject unknown or duplicate JSON fields, trailing tokens, duplicate operation IDs, completed nodes without a
session ID, and incomplete nodes that claim one rather than accepting ambiguous recovered state. An interrupted recovered
node stays incomplete in the journal on later shutdowns, while each public recovered view still reports
CONTROL_RESTARTED.
After a restart, journal entries are exposed as recovered:true history. Any node that had not completed is shown failed
with CONTROL_RESTARTED; a recovered entry is never resumed or retried because its sensitive input, live session binding,
task attempt, and approval are not persisted. A completed result's historical session ID is display state, not a live task
binding. Capability-loss, target-role, topology, and proxy-dependency cancellations are persisted immediately when Control
completes them automatically. Start a fresh read or preview. This preserves operator visibility without replaying an
ambiguous write.
Snapshots are durable Control-side copies of the redacted managed-file content returned by successful READ results. A
successful empty file is retained as an empty document; null alone means content was omitted. Known secret paths and
sensitive comment values contain __VOTINGPLUGIN_CONTROL_REDACTED__ rather than credentials:
POST /api/v1/snapshotsaccepts a name and completed read operation ID;GET /api/v1/snapshotslists summaries without file content;GET /api/v1/snapshots/{snapshotId}returns the selected documents with their full stored redacted content to an authenticated administrator/browser session.
A snapshot can contain at most 100 documents and 8 MiB of UTF-8 content. The store retains at most 100 snapshots and
64 MiB of encoded files in aggregate; before creation it removes the oldest files by modification time until both the
count and aggregate-byte bounds can fit the new snapshot. Files are validated, published atomically under
data/configuration-snapshots/<uuid>.json, and rejected if they are symlinks, malformed, oversized, contain unknown or
duplicate JSON fields/trailing tokens, or contain an invalid managed document identity/revision.
On POSIX-capable filesystems Control creates the snapshot directory owner-only and snapshot files owner read/write. POSIX modes are not available on every platform, so operators must protect the entire Control data directory and every backup with equivalent ACLs.
A snapshot is not a server backup and has no privileged restore endpoint. To restore, load one snapshot document as the proposed content, preview it against the node's current revision, review the diff, and apply with the new one-time approval. Snapshot retrieval returns the stored redacted document. During preview/apply, an unchanged redaction marker is resolved against each target's current secret; a snapshot never recovers or overwrites an old credential. The WebUI tracks whether document content is loaded independently from its length, so an intentionally empty snapshot is still a valid restore proposal and can proceed through preview and approval.
An administrator starts exactly one node query. nodeId is required and must match
[A-Za-z0-9][A-Za-z0-9._-]{0,63} before Control performs a registry lookup:
POST /api/v1/inspections
Content-Type: application/json
{
"nodeId": "backend-lobby",
"query": {"kind": "overview", "filters": {}}
}The node polls its independent inspection queue:
POST /api/v1/nodes/backend-lobby/inspections
Content-Type: application/json
{"sessionId":"<node-session-uuid>"}204 means no work. A task uses the shape:
{
"inspectionId": "<uuid>",
"query": {"kind": "overview", "filters": {}},
"attemptId": "<uuid>"
}The node posts the result to
/api/v1/nodes/{nodeId}/inspections/{inspectionId}/result with its sessionId, the same attemptId, success, a bounded
message, and either data or an error code. The session must still be both the registry's current session and the exact
session that claimed the lease; reconnecting cannot complete an earlier session's attempt. A successful result may omit/set
code to null or send "OK" for
connector compatibility. A failed result must omit data and use a code matching [A-Z][A-Z0-9_]{0,63}.
The Bukkit connector runs these handlers on a dedicated single-thread daemon executor, separate from its presence/configuration executor and never on the Bukkit primary thread. One slow read delays later inspections only; shutdown cancels the inspection lane and waits at most five seconds for that worker.
Successful data is a JSON object with a common envelope:
{
"schemaVersion": 1,
"kind": "overview",
"generatedAt": "2026-08-30T12:00:00Z",
"result": {}
}schemaVersion must be the JSON integer 1 (not a string), kind must exactly match the assigned query,
generatedAt must parse as an ISO-8601 instant, and result must be a JSON object. Control limits serialized data to
512 KiB and the message to 4 KiB. There are at most 100 retained inspections. The task
lease is two minutes; an unleased active inspection expires five minutes after creation and an unobserved complete
inspection is pruned 15 minutes after creation. After an API client retrieves a terminal result, that acknowledged entry
may be evicted early only when the 100-entry store needs capacity. Retrying after a lost acknowledgement is safe because
handlers are read-only.
Control audit records only the inspection kind. Filter values may contain a player identity or vote correlation ID and must never be copied into audit or ordinary application logs. Claim, completion, and capability-loss cancellation update in-memory inspection state transactionally with the audit append; an audit failure restores the prior lease/result state so a retry can emit the missing record.
The outer Control DTO permits at most 12 filters. Every filter value is a JSON string on the wire. Each key must match
[a-z][A-Za-z0-9]{0,39} and ordinary values are bounded to 500 UTF-8 bytes before the node parses the stricter per-kind
schema below. The sole larger value is reward-simulation's proposal, capped at 64 KiB. Examples:
"days":"30", "limit":"25", and "includeDisabled":"false".
API clients should always send that canonical string shape. The current Jackson mapper can coerce some scalar values in an
administrator request before constructing Map<String,String>, but that is not a compatibility guarantee; the queued node
task is string-valued and VotingPlugin's handler validates text after selecting the kind.
| Kind | Allowed filters | Result and important semantics |
|---|---|---|
overview |
none | Plugin/platform versions; configuration health; bounded data-storage mode; proxy mode; vote-site counts; auto-create state; configured/available/readable VoteLog state |
vote-site-health |
string days 1–365, default 30 |
Configured site state, bounded logged aggregates, unmatched logged services, and bounded persisted service observations with no configured match |
player |
exactly one of name (1–16 characters) or uuid (canonical 36-character UUID) |
Exact existing-player lookup; totals, points, streaks, up to 100 per-site last-vote rows, backend pending-offline count, and a bounded read-only allow-list of stored VotingPlugin fields when storage is available; never player enumeration or editing |
vote-log-summary |
string days 1–365, default 30 |
Vote count, immediate/cached split, unique voters, and top 20 services/servers |
vote-log-search |
at most one of exact player (1–16 characters), service (1–64), or server (1–64); optional event and string days/limit |
Most recent bounded logged-event rows; default 25 and maximum 100 |
vote-trace |
required canonical 36-character UUID voteId; optional string days/limit |
Chronological logged events sharing one correlation ID; default 50 and maximum 100 |
vote-site-resolution |
required valid serviceSite (1–64 characters); optional string boolean includeDisabled |
Non-creating resolution and whether automatic creation would be attempted; always reports no side effects |
reward-simulation |
required proposal, a JSON object encoded as one filter string |
Validation, normalization, action count, and warnings only; never executes or saves rewards |
diagnostics |
none | Bounded redacted runtime/configuration health, VoteLog readability, and detected plugin names |
Unknown kinds, filters, proposal fields, or invalid types/ranges fail with VALIDATION_ERROR. VoteLog summary/search/trace
fail with UNAVAILABLE when logging is disabled, enabled without an initialized adapter, or unreadable. An oversized result fails with
RESULT_TOO_LARGE; an unexpected handler failure becomes INSPECTION_FAILED without exposing a stack trace.
Valid event filters are VOTE_RECEIVED, VOTEMILESTONE, VOTE_STREAK_REWARD, TOP_VOTER_REWARD, and
VOTESHOP_PURCHASE.
Every kind returns its fields under the common envelope's result object. Time fields below are VotingPlugin epoch-millis
values; generatedAt is the ISO-8601 string generated by the connector.
| Kind | Result fields |
|---|---|
overview |
pluginVersion, platform, serverSoftware, serverVersion, configured/enabled vote-site counts, autoCreateVoteSites, processRewards, dataStorage, voteLoggingEnabled, voteLogAvailable, voteLogReadable, proxy mode/method, votifierDetected, configurationHealthy |
vote-site-health |
days, voteLoggingEnabled, voteLoggingAvailable, voteLogReadable, autoCreateVoteSites, sites, unmatchedLoggedServices, detectedUnconfiguredServices, and truncation flags. Site rows always include identity/settings/reward presence and status; logged/immediate/cached counts and last-vote time are present only when VoteLog is readable. Status is ACTIVE, DISABLED, SERVICE_SITE_MISSING, VOTE_LOG_UNAVAILABLE, VOTE_LOG_UNREADABLE, or NO_RECENT_VOTES |
player |
Either {found:false, entity:"player"} or identity/online state, daily/weekly/monthly/all-time totals, points, streaks, lastVoteTime, lastVotes, lastVotesTruncated, and pendingOfflineVotes saturated at 100,000. When the storage row is safely available, storage, storageRowAvailable, bounded allow-listed columns, and columnsTruncated describe stored VotingPlugin values. Last-vote rows contain siteKey, displayName, serviceSite, and time, and include only stored keys that currently resolve as enabled sites |
vote-log-summary |
days, total, immediate, cached, uniqueVoters, top-20 topServices (service, canonical count) and topServers (server, canonical count) rows. During schema-v1 staggered upgrades, new nodes also emit and new dashboards accept the equal legacy votes alias at the normalization boundary |
vote-log-search |
days, limit, entries, truncated; each entry has voteId, voteTime, player UUID/name, service, server, event, context, status, and cachedTotal |
vote-trace |
normalized voteId, found, chronological events using the same entry schema, and truncated |
vote-site-resolution |
requested service/options, matched, optional matched-site identity/state, wouldAutoCreate, and sideEffects:false |
reward-simulation |
valid, actionCount, wouldExecute:false, sideEffects:false, normalizedProposal, and bounded warnings |
diagnostics |
All overview fields plus build/profile/Java/background-task/storage status, at most 128 detected plugin names, and omittedSensitiveData |
The decoded proposal JSON is:
{
"scope": "site",
"site": "PMC",
"commands": ["eco give %player% 100"],
"playerMessages": ["Thanks for voting"],
"broadcastMessages": [],
"items": [{"material": "DIAMOND", "amount": 2}],
"money": 0,
"permissions": [],
"chancePercent": 100,
"onlineOnly": false
}The inspection request carries that object as a string, for example
{"kind":"reward-simulation","filters":{"proposal":"{\"scope\":\"site\",...}"}}. Use JSON.stringify (or the
equivalent standard JSON serializer); do not build this escaped text by concatenating user input.
scope is site, every-site, or vote-party; for site scope, site must match [A-Za-z0-9_-]{1,64} and name an
existing site. For a global scope it must be omitted, null, or empty. Each action collection has at most 20 entries. Command and message
entries are nonblank/single-line and at most 500 characters; permissions have the same rules with a 200-character limit.
Item material names are normalized to uppercase, must match [A-Z0-9_]{1,80}, resolve through Bukkit
Material.matchMaterial, and be item materials; amounts are 1–64. Money is a finite number from 0–1,000,000,000 and chance is finite from 0–100;
onlineOnly is a native boolean. At least one command, message, item, permission, or positive money value is required.
The result always contains
wouldExecute:false and sideEffects:false. The encoded proposal filter is capped at 64 KiB.
The same object can be persisted only through the PREVIEW/APPLY-only reward-builder quick setup. Persistence clears and
rebuilds exactly one selected path:
| Scope | File and replaced path |
|---|---|
site |
VoteSites.yml → VoteSites.<site>.Rewards; the named site must already exist |
every-site |
VoteSites.yml → EverySiteReward |
vote-party |
SpecialRewards.yml → VoteParty.Rewards |
Commands map to Commands; player and broadcast messages to Messages.Player / Messages.Broadcast; items to numbered
Items.ControlItemN material/amount entries; positive money to Money; and permissions to numbered
AdvancedRewards.ControlPermissionN.TempPermission.{Permission,Expiration} entries with Expiration: 2147483647.
Chance maps to Chance, while
online-only maps to RewardType: ONLINE instead of BOTH. The preset never executes the proposal and never changes a
different site, every-site rewards, or unrelated VoteParty settings. The regular revision, approval, backup, reload, and
rollback guarantees still apply.
The dedicated auto-create-vote-sites quick setup reads/writes only Config.yml -> AutoCreateVoteSites. Use it for the
prominent toggle rather than submitting the six unrelated fields in common-settings; its READ form takes no options.
Disabling automatic creation does not make vote-site resolution writable and does not delete previously observed service
names. It gates only inbound
unknown-service generation; explicit administrator command/GUI creation remains available.
vote-site-health keeps two unconfigured-service signals distinct. unmatchedLoggedServices is derived from retained
VoteLog rows and remains empty/non-authoritative unless voteLogReadable is true. detectedUnconfiguredServices is the
case-insensitive, deduplicated, sorted, at-most-100 view of VotingPlugin's persisted GottenServiceSites observations that do not match a
configured site's ServiceSite; its separate truncation flag reports overflow. That observation list remains useful when
VoteLogging is disabled and when automatic site creation is turned off. It is an inbox for review, not an automatic create
or approval action.
The dedicated vote-logging quick setup owns only VoteLogging.Enabled, VoteLogging.PurgeDays (-1 disables automatic
purging, otherwise 1–3650; 0 and other negatives are invalid), and VoteLogging.UseMainMySQL. It rejects unknown
options and never accepts or returns database connection fields or credentials. Its READ form takes no options and
round-trips -1. Configuring a dedicated connection remains a full redacted-editor change.
VoteLog is optional and SQL-backed. voteLoggingEnabled reports the current configuration. voteLogAvailable is true
only when logging is enabled and its table adapter exists; voteLogReadable additionally requires a bounded live probe
whose JDBC statement timeout is 10 seconds. The quick setup writes and reloads Config.yml, but it does not create or
close the runtime VoteLog manager. Restart VotingPlugin after changing VoteLogging.Enabled: immediately after disabling,
all inspection reads gate the possibly stale adapter and report available/readable false; immediately after enabling a
previously disabled instance, overview can report enabled true but available false until restart.
Summary, search, and trace require enabled, available, and readable state; otherwise they fail with UNAVAILABLE instead
of presenting legacy empty/zero SQL fallbacks as real data. Vote-site health remains useful without SQL: it reports
voteLogReadable:false, skips aggregates, and uses VOTE_LOG_UNAVAILABLE or VOTE_LOG_UNREADABLE rather than
NO_RECENT_VOTES for enabled rows with a configured service; DISABLED and SERVICE_SITE_MISSING retain precedence.
Readability is a point-in-time probe, not a transaction around the later query. A database failure after
a successful probe can still hit the legacy table method's empty/zero fallback; fully eliminating that narrow race needs a
future table API that propagates query errors.
The data views expose logged events, not a complete vote-delivery trace. Logged event rows can contain correlation ID,
event time, player UUID/name, service, server, event, context, IMMEDIATE or
CACHED status, and cached total. The log does not promise a row for every validation rejection, transport hop, duplicate
decision, executed reward command, command result, or expiry. A vote trace therefore means “all retained logged events
with this voteId”, not packet tracing.
Do not add an inspection escape hatch for SQL, table names, connection settings, raw logs, arbitrary player fields, fuzzy player search, or all-player enumeration.
All write requests from a browser session require its X-CSRF-Token. API automation uses the admin bearer credential;
node resources require the bearer credential bound to the path node ID.
| Method | Resource | Role | Purpose |
|---|---|---|---|
GET |
/api/v1/health |
public | Application, instance, protocol, and optional hosted-launch identity |
GET/POST |
/api/v1/auth/setup |
public/setup code | First-run password state and one-time setup |
POST |
/api/v1/auth/login |
password | Create bounded browser session |
GET |
/api/v1/auth/session |
browser | Restore CSRF token after refresh |
POST |
/api/v1/auth/logout |
browser + CSRF | Revoke current session |
GET/POST |
/api/v1/enrollments |
admin/browser | List node IDs or rotate one node credential |
DELETE |
/api/v1/enrollments/{nodeId} |
admin/browser | Revoke one node credential |
GET |
/api/v1/nodes |
admin/browser | Stable paginated current topology |
POST |
/api/v1/nodes/register |
matching node | Register/replace a process session |
PUT |
/api/v1/nodes/{nodeId}/heartbeat |
matching node | Refresh liveness and capabilities |
PUT |
/api/v1/nodes/{nodeId}/presence |
matching node | Replace proxy backend presence |
POST |
/api/v1/configuration/{read,preview,apply} |
admin/browser + CSRF | Queue typed configuration work |
GET |
/api/v1/operations |
admin/browser | List at most 100 newest-first summaries without retained file bodies; unused preview approval may be present |
GET |
/api/v1/operations/{operationId} |
admin/browser | Read full bounded redacted aggregate/per-node detail and any unused preview approval |
POST |
/api/v1/operations/{operationId}/retry |
admin/browser + CSRF | Reissue safe failed work as a new operation |
POST |
/api/v1/nodes/{nodeId}/operations |
matching node | Claim one configuration task or 204 |
POST |
/api/v1/nodes/{nodeId}/operations/{operationId}/result |
matching node | Complete one claimed configuration task |
POST |
/api/v1/artifacts/votingplugin |
admin/browser + CSRF | Stream, hash, inspect, and atomically retain one VotingPlugin JAR (64 MiB maximum) |
GET, POST |
/api/v1/deployments |
admin/browser; CSRF for POST | List durable staging operations or target an exact verified artifact |
GET |
/api/v1/deployments/{deploymentId} |
admin/browser | Read per-node staging state |
POST |
/api/v1/deployments/{deploymentId}/retry |
admin/browser + CSRF | Create a new operation for currently eligible failed targets only |
POST |
/api/v1/nodes/{nodeId}/deployments |
matching node | Claim one session-pinned deployment task |
GET |
/api/v1/nodes/{nodeId}/deployments/{deploymentId}/artifact |
matching node + session/attempt headers | Stream the verified artifact during the exact live lease |
POST |
/api/v1/nodes/{nodeId}/deployments/{deploymentId}/result |
matching node | Complete the exact attempt with staged/restart-required or a bounded failure |
POST |
/api/v1/inspections |
admin/browser + CSRF | Queue one typed read-only query |
GET |
/api/v1/inspections/{inspectionId} |
admin/browser | Read short-lived inspection status/result |
POST |
/api/v1/nodes/{nodeId}/inspections |
matching node | Claim one inspection or 204 |
POST |
/api/v1/nodes/{nodeId}/inspections/{inspectionId}/result |
matching node | Complete one claimed inspection |
GET/POST |
/api/v1/snapshots |
admin/browser | List snapshot summaries or create from a completed read |
GET |
/api/v1/snapshots/{snapshotId} |
admin/browser | Load one snapshot's full stored redacted documents |
Routes and methods are exact. Known resources return structured 405; unknown paths return structured 404. Errors use:
{"error":{"code":"VALIDATION_ERROR","message":"Request validation failed","details":[]}}Control assumes an authenticated administrator may make intended changes, but it does not trust HTTP clients, nodes, their clocks, returned strings, or durable files. Authentication does not encrypt traffic; use HTTPS or a trusted private tunnel/network outside loopback.
| Boundary | Limit/behavior |
|---|---|
| Generic JSON request | 4 MiB; bounded Jackson depth/string/number constraints; duplicate and trailing JSON rejected |
| VotingPlugin artifact upload | Separate bounded streaming route; 64 MiB maximum |
| HTTP execution | 8 active request workers plus queue of 32; bounded request/response time |
| Browser sessions | 100; 30-minute idle and 8-hour absolute expiry; HttpOnly, SameSite=Strict cookie |
| Node operation targets | 1–100 distinct online capable nodes |
| Managed YAML / VoteSites sync source | 512 KiB per document/source |
| Ordinary option/filter | 500 UTF-8 bytes per value; a reward simulation/builder proposal is the sole 64 KiB exception |
| Retained operation detail | 8 MiB file content, 256 KiB changes, 256 KiB messages across live retained operations |
| Durable operation history | 1,000 entries, 100 nodes each, 24 hours, and 2 MiB; metadata only |
| Inspection | 100 retained; 512 KiB data; 4 KiB message; 2-minute lease |
| Snapshot | 100 snapshots and 64 MiB encoded aggregate; 100 documents and 8 MiB content per snapshot |
| Topology | 4,096 backends per snapshot; 65,536 retained across registry; 128 plugins per node and 16,384 total |
| Audit | bounded hash-chained JSONL; rotates at 5 MiB; values, credentials, tokens, and filters excluded |
Sensitive output must omit credentials, password/token/API-key/authorization values, database/Redis/MQTT connection details, webhook URLs, raw logs, raw configuration in diagnostics, and unrestricted player records. Configuration reads and snapshots contain bounded managed YAML with known secrets replaced by redaction markers. The diagnostics result includes an explicit list of categories it omitted so a support recipient does not assume completeness.
- Confirm it is read-only, bounded, non-secret, and belongs to an existing kind.
- Add it in VotingPlugin's typed handler and test the exact result, disabled/unavailable state, and size bound.
- Render it as text in the WebUI and clear any cached value on logout.
- Update this document and the paired
docs/control-agent-contract.md.
- Prefer extending an existing kind unless the authorization/data semantics genuinely differ.
- Add the exact kind to Control's
InspectionQueryand VotingPlugin'sControlInspectionServiceallow-lists. - Define exact filters, types, ranges, default/max rows, error states, and sensitive omissions before implementation.
- Add coordinator, HTTP, connector, handler, and browser tests. If an old counterpart must not see the request, introduce a new versioned capability.
- Give the preset the narrowest ownership possible; do not rewrite unrelated settings.
- Implement read/preview/apply through the existing quick-setup capability.
- Preserve deterministic revisions, secret behavior, atomic write, reload, rollback, and audit boundaries.
- Expose it through the same preview/approval UI; never write directly from a toggle.
- Define a hard count and byte limit plus deterministic eviction.
- Reject symlinks and malformed/oversized files, stage in the destination directory, force file data, publish atomically, and force the directory.
- Never persist browser CSRF tokens, approval tokens, node raw credentials, new configuration secrets, or inspection filters/results containing player data. Snapshot content must remain the redacted node read, admin-only, owner-permissioned, and bounded.