Skip to content
Open
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ rollback workflow still applies; redaction placeholders preserve each target's c

### WebUI management suite

- **Overview** uses a persistent grouped sidebar and server-aware header to keep the selected target visible. Its six
bounded summary cards, Attention Required feed, topology, logged-service activity, recent operations, and capability-
gated quick actions reuse existing inspections and management flows. Health reflects only currently observed Control,
topology, VoteSite, communication, operation, and VoteLog state: a disconnected registered node is a warning, unknown
Minecraft presence is not called offline, and vote counts remain explicitly labeled as logged `VOTE_RECEIVED` events.
- **Setup** provides a readiness checklist, browser-local non-secret profiles, dedicated auto-create and VoteLogging cards,
the existing typed setup assistant, a reward builder with simulation plus preview/apply, and a searchable common-setting
catalog.
Expand Down
4 changes: 3 additions & 1 deletion docs/control-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ authenticated, CSRF-protected endpoint and node capability checks as an external

| 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 operation views, labels phases, lineage, reload/rollback, resumes eligible guided preview approvals, and offers retry only when `retryable` | Recovered history cannot be retried; approval is single-use and apply is CSRF-protected; proxy-method apply needs a new preview |
Expand Down Expand Up @@ -310,7 +312,7 @@ values; `generatedAt` is the ISO-8601 string generated by the connector.
| `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` and `topServers` count rows |
| `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` |
Expand Down
95 changes: 82 additions & 13 deletions src/main/resources/web/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,26 @@ button:disabled { opacity: .46; cursor: not-allowed; }
button.secondary { border-color: var(--border-strong); background: transparent; color: #d6e5f7; }
button.secondary:hover:not(:disabled) { background: var(--surface-soft); border-color: #53769d; }
button.compact { padding: 9px 12px; }
button:focus-visible, a:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible {
outline: 2px solid #8cc4ff;
outline-offset: 2px;
}

.topbar { position: sticky; z-index: 10; top: 0; display: flex; align-items: center; justify-content: space-between; gap: 24px; min-height: 76px; padding: 12px max(24px, calc((100vw - 1240px) / 2)); border-bottom: 1px solid rgba(54, 84, 117, .72); background: rgba(7, 16, 29, .9); backdrop-filter: blur(18px); }
.topbar { position: sticky; z-index: 20; top: 0; display: flex; align-items: center; justify-content: space-between; gap: 20px; min-height: 76px; padding: 12px max(24px, calc((100vw - 1480px) / 2)); border-bottom: 1px solid rgba(54, 84, 117, .72); background: rgba(7, 16, 29, .94); backdrop-filter: blur(18px); }
.brand { display: flex; align-items: center; gap: 11px; color: inherit; text-decoration: none; }
.brand-mark { display: grid; width: 40px; height: 40px; place-items: center; border-radius: 11px; background: linear-gradient(145deg, #5babff, #246ac4); box-shadow: 0 8px 30px rgba(49, 135, 238, .24); font-size: 1.2rem; font-weight: 900; }
.brand strong, .brand small { display: block; line-height: 1.05; }
.brand small { margin-top: 4px; color: var(--muted); font-size: .72rem; letter-spacing: .13em; text-transform: uppercase; }
.topbar-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; }
.sidebar-toggle { display: none; flex: none; font-size: 1.05rem; }
.global-search { width: min(26vw, 360px); }
.global-search input { width: 100%; margin: 0; padding-block: 9px; background: #091421; }
.server-picker { display: grid; grid-template-columns: auto minmax(190px, 280px); align-items: center; gap: 9px; margin: 0; color: var(--muted); font-size: .78rem; }
.server-picker select { margin: 0; padding: 9px 35px 9px 11px; }
.status { padding: 8px 12px; border: 1px solid var(--border-strong); border-radius: 999px; color: var(--muted); font-size: .78rem; white-space: nowrap; }
.status.online { color: var(--green); border-color: #2b7750; background: var(--green-soft); }

main { width: min(1240px, calc(100% - 40px)); margin: 0 auto; padding: 48px 0 80px; }
main { width: min(1480px, calc(100% - 40px)); margin: 0 auto; padding: 42px 0 80px; }
.welcome { max-width: 780px; margin: 18px 0 34px; }
.welcome h1 { margin-bottom: 12px; font-size: clamp(2.2rem, 5vw, 4rem); line-height: 1.04; letter-spacing: -.045em; }
.welcome > p:last-child { max-width: 660px; color: var(--muted); font-size: 1.08rem; line-height: 1.7; }
Expand All @@ -59,19 +66,53 @@ p { line-height: 1.55; }
.section-title { align-items: flex-start; }
.section-title > div { min-width: 0; }

.tabs { display: flex; gap: 4px; margin: 0 -20px; padding: 0 20px; overflow-x: auto; border-bottom: 1px solid var(--border); scrollbar-width: thin; }
.tabs button { flex: 0 0 auto; padding: 14px 15px; border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: transparent; color: var(--muted); }
.tabs button:hover:not(:disabled) { background: transparent; color: #fff; transform: none; }
.tabs button[aria-selected="true"] { border-bottom-color: var(--blue); color: #fff; }
.app-shell { display: grid; grid-template-columns: 226px minmax(0, 1fr); gap: 34px; align-items: start; }
.content-shell { min-width: 0; }
.sidebar { position: sticky; top: 100px; display: flex; max-height: calc(100vh - 120px); flex-direction: column; gap: 2px; padding: 12px; overflow-y: auto; border: 1px solid var(--border); border-radius: 15px; background: rgba(9, 20, 33, .95); box-shadow: 0 20px 60px rgba(0, 0, 0, .18); }
.sidebar p { margin: 16px 10px 6px; color: #71869f; font-size: .64rem; font-weight: 850; letter-spacing: .14em; text-transform: uppercase; }
.sidebar p:first-of-type { margin-top: 12px; }
.sidebar button { display: grid; grid-template-columns: 22px minmax(0, 1fr); align-items: center; gap: 9px; width: 100%; padding: 10px 11px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); font-size: .84rem; font-weight: 700; text-align: left; }
.sidebar button span { color: #7194b9; text-align: center; }
.sidebar button:hover:not(:disabled) { background: var(--surface-soft); color: #fff; transform: none; }
.sidebar button[aria-current="page"] { background: linear-gradient(90deg, #163f6d, #112d4c); color: #fff; box-shadow: inset 3px 0 0 var(--blue); }
.sidebar button[aria-current="page"] span { color: #a9d4ff; }
.tab-panel { animation: enter .16s ease-out; }
@keyframes enter { from { opacity: 0; transform: translateY(3px); } }

.metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 13px; margin-bottom: 18px; }
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 13px; margin-bottom: 18px; }
.metrics article { display: flex; min-height: 112px; flex-direction: column; justify-content: space-between; padding: 19px; border: 1px solid var(--border); border-radius: 13px; background: var(--surface); }
.metrics span { color: var(--muted); font-size: .85rem; }
.metrics strong { font-size: 2rem; }
.metrics strong { margin: 10px 0 3px; font-size: 1.65rem; line-height: 1.05; }
.metrics small { color: #71869f; }
.metrics article.healthy { border-color: #2b7750; background: linear-gradient(145deg, var(--green-soft), var(--surface)); }
.metrics article.warning { border-color: #785927; background: linear-gradient(145deg, var(--amber-soft), var(--surface)); }
.metrics article.critical { border-color: #82445a; background: linear-gradient(145deg, #341521, var(--surface)); }
.selected-summary-card { margin-bottom: 18px; background: linear-gradient(135deg, rgba(17, 47, 81, .88), rgba(12, 24, 40, .96)); }
.selected-summary-card > p { color: var(--muted); }
.overview-grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(280px, .75fr); gap: 18px; margin-bottom: 18px; }
.overview-grid > .card { min-width: 0; }
.lower-overview-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 18px; }
.attention-feed, .compact-activity, .activity-bars { display: grid; gap: 10px; margin-top: 18px; }
.attention-item { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 11px; align-items: start; padding: 12px; border: 1px solid var(--border); border-radius: 10px; background: #091421; }
.attention-indicator { width: 9px; height: 9px; margin-top: 6px; border-radius: 50%; background: var(--amber); box-shadow: 0 0 0 4px rgba(255, 201, 107, .1); }
.attention-item.critical .attention-indicator { background: var(--danger); box-shadow: 0 0 0 4px rgba(255, 137, 164, .1); }
.attention-item.informational .attention-indicator { background: #79b7f2; }
.attention-item strong, .attention-item small { display: block; }
.attention-item small { margin-top: 3px; color: var(--muted); line-height: 1.4; }
.attention-empty { padding: 24px; border: 1px dashed #2b7750; border-radius: 10px; color: var(--green); text-align: center; }
.quick-action-grid { display: grid; gap: 9px; margin-top: 18px; }
.quick-action-grid button { display: flex; align-items: center; justify-content: space-between; width: 100%; border-color: var(--border); background: #091421; color: #dceaff; text-align: left; }
.quick-action-grid button::after { color: #7194b9; content: "→"; }
.activity-bar-row { display: grid; grid-template-columns: minmax(95px, .8fr) minmax(100px, 1.2fr) auto; gap: 10px; align-items: center; font-size: .82rem; }
.activity-bar-row strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.activity-bar-track { width: 100%; height: 8px; overflow: hidden; border: 0; border-radius: 999px; background: #07101d; appearance: none; }
.activity-bar-track::-webkit-progress-bar { border-radius: inherit; background: #07101d; }
.activity-bar-track::-webkit-progress-value { border-radius: inherit; background: linear-gradient(90deg, #3187ee, #79e6a3); }
.activity-bar-track::-moz-progress-bar { border-radius: inherit; background: linear-gradient(90deg, #3187ee, #79e6a3); }
.activity-item { padding: 11px 0; border-bottom: 1px solid var(--border); }
.activity-item:last-child { border-bottom: 0; }
.activity-item strong, .activity-item small { display: block; }
.activity-item small { margin-top: 4px; color: var(--muted); }
.pills { display: flex; flex-wrap: wrap; gap: 7px; }
.pill { display: inline-flex; align-items: center; width: fit-content; padding: 5px 9px; border: 1px solid #37628d; border-radius: 999px; background: var(--blue-soft); color: #a9d4ff; font-size: .74rem; font-weight: 750; }
.pill.online { border-color: #2b7750; background: var(--green-soft); color: var(--green); }
Expand Down Expand Up @@ -211,31 +252,59 @@ li span { color: var(--muted); font-size: .9rem; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

@media (max-width: 920px) {
.topbar { position: static; align-items: flex-start; }
.topbar { align-items: center; }
.sidebar-toggle { display: inline-flex; align-items: center; justify-content: center; }
.topbar-actions { flex-wrap: wrap; }
.global-search { order: 4; width: 100%; }
.server-picker { order: 3; width: 100%; grid-template-columns: auto 1fr; }
.app-shell { display: block; }
body.sidebar-open { overflow: hidden; }
body::before { position: fixed; z-index: 25; top: 76px; right: 0; bottom: 0; left: 0; visibility: hidden; background: rgba(2, 8, 15, .62); opacity: 0; content: ""; transition: opacity .18s ease, visibility 0s linear .18s; }
body.sidebar-open::before { visibility: visible; opacity: 1; transition-delay: 0s; }
.sidebar { position: fixed; z-index: 30; top: 76px; bottom: 0; left: 0; visibility: hidden; width: min(300px, calc(100vw - 42px)); max-height: none; border-radius: 0 15px 15px 0; transform: translateX(-105%); transition: transform .18s ease, visibility 0s linear .18s; }
body.sidebar-open .sidebar { visibility: visible; transform: translateX(0); box-shadow: 20px 0 80px rgba(0, 0, 0, .6); transition-delay: 0s; }
.metrics, .settings-grid, .feature-grid { grid-template-columns: repeat(2, 1fr); }
.overview-grid { grid-template-columns: 1fr; }
.sync-grid { grid-template-columns: 1fr; }
.roadmap-card { grid-template-columns: 1fr; }
.roadmap-card > div { border-right: 0; border-bottom: 1px solid var(--border); }
.topology-row { grid-template-columns: 1fr; }
}

@media (max-width: 640px) {
.topbar { flex-direction: column; padding: 14px 18px; }
.topbar-actions { width: 100%; justify-content: flex-start; }
.topbar { padding: 12px 14px; }
.brand { margin-right: auto; }
.brand > span:last-child { display: none; }
.brand-mark { width: 36px; height: 36px; }
.topbar-actions { display: contents; }
.status { display: none; }
#header-action { display: none; }
.server-picker { grid-template-columns: 1fr; }
main { width: min(100% - 28px, 1240px); padding-top: 30px; }
.server-picker span { display: none; }
.server-picker select { width: min(48vw, 220px); }
.global-search { position: absolute; top: 74px; right: 14px; left: 14px; width: auto; }
main { width: min(100% - 28px, 1480px); padding-top: 72px; }
.welcome h1 { font-size: 2.35rem; }
.page-heading, .section-title, .auth-row, .editor-footer { align-items: stretch; flex-direction: column; }
.page-heading > button, .auth-row button { width: 100%; }
.metrics, .settings-grid, .feature-grid, .checklist { grid-template-columns: 1fr; }
.metrics article { min-height: 92px; }
.tabs { margin: 0 -14px; padding: 0 14px; }
.subtabs button { padding-inline: 8px; }
.editor-toolbar { grid-template-columns: 1fr; }
.editor-toolbar > span { justify-self: start; }
.yaml-editor { min-height: 420px; padding: 16px; }
.operation-actions button { width: 100%; }
.result-item { align-items: stretch; flex-direction: column; }
.attention-item { grid-template-columns: auto minmax(0, 1fr); }
.attention-item button { grid-column: 2; width: 100%; }
}

@media (max-width: 420px) {
.topbar { gap: 8px; }
.brand { display: none; }
.server-picker select { width: min(50vw, 180px); }
}

@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; }
}
Loading