diff --git a/README.md b/README.md index 704cffa..a12cfe5 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,15 @@ rows here; the website core never learns there is more than one. | Public | `GET …/servers/:id/events` — the feed, served from a default-deny allowlist (`server/catalogue.js`) | | Public | `GET …/servers/:id/leaderboard` — per wipe, or all-time as those rows summed | | Public | `GET …/servers/:id/wipes` and `…/online` | +| Public | `GET …/servers/:id/clans` — the server's clans, best score first (public: names nobody) | +| Public | `GET /api/v1/public/rust/clans/:externalId` — one clan, and its roster inside the roster audience | | Player | `GET /api/v1/player/rust/servers` — the server list, on the authenticated tier | | Admin | `GET/PUT/DELETE /api/v1/admin/rust/servers` and `POST …/:id/test` | | Admin | `GET/PUT /api/v1/admin/rust/visibility` — who may see who is online, fleet-wide and per server | | Pages | `/rust` — the server list, and the module's landing page | -| Pages | `/rust/servers/:id` — one server: feed, leaderboard, who is on, wipes | +| Pages | `/rust/servers/:id` — one server: feed, leaderboard, who is on, wipes, clans | +| Pages | `/rust/clans/:externalId` — one clan, with core's Team notify, activity and forum in three module slots | +| Teams | The deployment's Team provider: a first-party Rust clan is a Team | | Slot | `site.footer.status` — a live server/player count in core's footer | **Nothing names who is online by default.** The Online list, every feed item that says a named @@ -62,7 +66,14 @@ Seven tables: `rust_servers` (configuration), `rust_server_state` and `rust_pres state), `rust_wipes`, `rust_players`, `rust_player_wipe_stats` and `rust_gather_totals` (the record a wipe does not erase), plus the bounded `rust_events` window and the `rust_ingest_cursor`. -The rest of the module — identity, site-owned permissions, Teams from Rust's clans, notifications, +**Teams come from Rust's own clans**, not from the uMod Clans plugin, which is optional and whose +clans never become Teams. A clan's roster reaches its own members and staff unless an operator +widens it in Admin → Rust visibility; its name, colour, score and count are public. The game lists +at most 100 clans per server, and a server at that ceiling answers core partially, so core never +removes a Team on its word. Core holds one Team provider per site, which is one reason **a site runs +one module**: core's installer refuses a second. + +The rest of the module — notifications, events, the live map, Discord commands — arrives phase by phase. **Nothing is registered before it has something behind it:** a declared trigger nothing emits and a declared slot nothing fills are both surfaces an operator can configure and then wait on, which is worse than an absent one. diff --git a/client/src/api.js b/client/src/api.js index 28df9ac..023325a 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -47,6 +47,21 @@ export const servers = { wipes: (id) => req(`/public/rust/servers/${encodeURIComponent(id)}/wipes`), online: (id) => req(`/public/rust/servers/${encodeURIComponent(id)}/online`), + + // Phase 9. The clan list is public (D58): name, colour, score and member count + // name nobody. `board` says whether the list can be trusted right now. + clans: (id) => req(`/public/rust/servers/${encodeURIComponent(id)}/clans`), +} + +// One clan. Its roster comes back only for a viewer inside the operator's roster +// audience (D48) — clan members and staff by default — and `roster.visible` +// says which answer this was, so a page can explain an empty roster rather than +// imply an empty clan. +// +// The id carries colons (`::`). They are legal in a path +// segment, and encoded anyway so that a server slug is never read as structure. +export const clans = { + get: (externalId) => req(`/public/rust/clans/${encodeURIComponent(externalId)}`), } /** @@ -231,6 +246,7 @@ export { BASE, query } export default { servers, + clans, playerServers, playerLinks, playerPermissions, diff --git a/client/src/components/Clans.jsx b/client/src/components/Clans.jsx new file mode 100644 index 0000000..7786a2b --- /dev/null +++ b/client/src/components/Clans.jsx @@ -0,0 +1,118 @@ +// ── The clans on one server ─────────────────────────────────────────────── +// +// Rust's OWN clans (R5), best score first. Public at every setting (D58): a +// clan's name, colour, score and member count name nobody. Who is IN a clan is +// the roster, and that lives on the clan's own page behind the operator's +// roster audience (D48). +// +// **The list is only as good as the board it came from**, and the answer says +// how good that is. Three cases would all look like an empty list if rendered +// bare, and they are three different sentences: +// +// • the bridge cannot read this server's clans at all (an older plugin, or a +// Nexus server whose clans live elsewhere) — "unavailable"; +// • the game's clan system is switched off — "this server has no clans"; +// • it can, and there are none — "nobody has founded one yet". +// +// And a board at the game's 100-clan ceiling (D55) says there may be more. + +import { Link } from 'react-router-dom' +import { ErrorState, Loading, useAsync } from '../core.js' +import Empty from './Empty.jsx' +import { count } from '../lib/format.js' +import api from '../api.js' + +export default function Clans({ serverId }) { + const { data, loading, error } = useAsync(() => api.servers.clans(serverId), [serverId]) + + if (loading) return + if (error) return + + const clans = (data && data.clans) || [] + const board = (data && data.board) || {} + + if (clans.length === 0) { + if (!board.supported) { + return ( + + ) + } + if (board.enabled === false) { + return + } + return + } + + return ( + <> + {board.truncated && ( +

+ The game lists at most 100 clans, by score, so there may be more on this server than are shown here. +

+ )} +
    + {clans.map((clan, index) => ( +
  • + + {index + 1} + + + + {clan.name} + + + {count(clan.memberCount)} {clan.memberCount === 1 ? 'member' : 'members'} + + + {count(clan.score)} pts + +
  • + ))} +
+ + ) +} + +/** Where a clan's page is: the same template the Team provider hands core. */ +export function clanPath(externalId) { + return `/rust/clans/${encodeURIComponent(externalId)}` +} + +/** + * A clan's colour, as a small square. The server has already checked it is a + * `#rrggbb` — it ends up in a style — and a clan with no colour gets an outline + * rather than a guess. + */ +export function Swatch({ color, size = 12 }) { + return ( +