From f46ba69bca8a7ae78838dc9e73c5646600aa290a Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Tue, 29 Sep 2026 21:45:31 -0400 Subject: [PATCH 1/2] feature: player management plugin status and install steps for community servers --- components/PlayerSanctions.vue | 11 +- components/SanctionPlayer.vue | 17 +- components/servers/ServerPlayerManagement.vue | 327 +++++++++++++++++- components/servers/ServerStatus.vue | 23 +- constants/gameServerReleases.ts | 62 ++++ i18n/locales/ar_SA.json | 14 +- i18n/locales/da_DK.json | 14 +- i18n/locales/de_DE.json | 14 +- i18n/locales/en.json | 14 +- i18n/locales/es_ES.json | 14 +- i18n/locales/fr_FR.json | 14 +- i18n/locales/it_IT.json | 14 +- i18n/locales/ja_JP.json | 14 +- i18n/locales/ko_KR.json | 14 +- i18n/locales/pl_PL.json | 14 +- i18n/locales/pt_BR.json | 14 +- i18n/locales/ru_RU.json | 14 +- i18n/locales/sv_SE.json | 14 +- i18n/locales/tr_TR.json | 14 +- i18n/locales/uk_UA.json | 14 +- i18n/locales/zh_Hans.json | 14 +- i18n/locales/zh_Hant.json | 14 +- pages/dedicated-servers/[id].vue | 3 + stores/ApplicationSettings.ts | 10 + tests/components/SanctionPlayer.spec.ts | 66 +++- .../servers/ServerPlayerManagement.spec.ts | 139 ++++++++ tests/constants/gameServerReleases.spec.ts | 70 ++++ 27 files changed, 892 insertions(+), 74 deletions(-) create mode 100644 constants/gameServerReleases.ts create mode 100644 tests/components/servers/ServerPlayerManagement.spec.ts create mode 100644 tests/constants/gameServerReleases.spec.ts diff --git a/components/PlayerSanctions.vue b/components/PlayerSanctions.vue index 8ed8e6370..7c154f1fb 100644 --- a/components/PlayerSanctions.vue +++ b/components/PlayerSanctions.vue @@ -1016,7 +1016,7 @@ export default { this.removingSanction = true; try { - await (this as any).$apollo.mutate({ + const { data } = await (this as any).$apollo.mutate({ mutation: gql` mutation UnsanctionServerPlayer( $serverId: String @@ -1043,8 +1043,17 @@ export default { }, }); + const result = data?.unsanctionServerPlayer; + toast({ title: this.$t("player.sanctions.removed"), + description: + this.serverId && + this.sanctionToDelete.type !== "warning" && + result && + !result.enforced + ? result.message + : undefined, }); this.sanctionToDelete = null; diff --git a/components/SanctionPlayer.vue b/components/SanctionPlayer.vue index 30f8c8613..7107e42f2 100644 --- a/components/SanctionPlayer.vue +++ b/components/SanctionPlayer.vue @@ -120,13 +120,6 @@ import SettingHeader from "~/components/match/SettingHeader.vue";

{{ $t("player.sanction.warning_note") }}

-
- - {{ $t("pages.dedicated_servers.detail.sanctions_wip") }} -
@@ -329,7 +322,7 @@ export default { this.submitting = true; try { - await this.$apollo.mutate({ + const { data } = await this.$apollo.mutate({ mutation: gql` mutation SanctionServerPlayer( $serverId: String @@ -363,10 +356,18 @@ export default { }, }); + const result = data?.sanctionServerPlayer; + + // Saved either way; the message says why it could not reach the + // server live, e.g. a community server without the plugin. toast({ title: this.$t(`player.sanction.applied.${this.sanctionType}`, { name: this.player.name, }), + description: + this.serverId && !this.isWarning && result && !result.enforced + ? result.message + : undefined, }); this.sanctioningPlayer = false; diff --git a/components/servers/ServerPlayerManagement.vue b/components/servers/ServerPlayerManagement.vue index 84d75aabd..0278b3d2a 100644 --- a/components/servers/ServerPlayerManagement.vue +++ b/components/servers/ServerPlayerManagement.vue @@ -2,9 +2,10 @@ import { ref, computed, onMounted, onBeforeUnmount } from "vue"; import { useI18n } from "vue-i18n"; import gql from "graphql-tag"; -import { RefreshCw, Users, TriangleAlert } from "lucide-vue-next"; +import { RefreshCw, Users, TriangleAlert, Download } from "lucide-vue-next"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; +import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { Tooltip, TooltipContent, @@ -16,13 +17,151 @@ import PlayerDisplay from "~/components/PlayerDisplay.vue"; import SanctionPlayer from "~/components/SanctionPlayer.vue"; import PlayerSanctions from "~/components/PlayerSanctions.vue"; import KickPlayer from "~/components/KickPlayer.vue"; +import ClipBoard from "~/components/ClipBoard.vue"; +import getGraphqlClient from "~/graphql/getGraphqlClient"; +import { useAuthStore } from "~/stores/AuthStore"; +import { useApplicationSettingsStore } from "~/stores/ApplicationSettings"; +import { effectivePluginRuntime } from "~/constants/rconCommands"; +import { + PLAYER_MANAGEMENT_CONFIG_PATHS, + playerManagementConfig, + playerManagementDownloadUrl, +} from "~/constants/gameServerReleases"; +// Only an administrator can read the node assignment and the api password, so +// for anyone else these arrive undefined and the install steps stay hidden. const props = defineProps<{ serverId: string; + gameServerNodeId?: string | null; + apiPassword?: string | null; + pluginRuntime?: string | null; }>(); const { t } = useI18n(); const nuxtApp = useNuxtApp(); +const authStore = useAuthStore(); +const applicationSettings = useApplicationSettingsStore(); + +// The plugin syncs every 30s and the api keeps at most one heartbeat a minute. +const PLUGIN_STALE_MS = 3 * 60 * 1000; + +const RUNTIME_LABELS: Record = { + swiftlys2: "SwiftlyS2", + counterstrikesharp: "CounterStrikeSharp", +}; + +const pluginSubscription = gql` + subscription ServerPlayerManagementPlugin($serverId: uuid!) { + servers_by_pk(id: $serverId) { + id + type + game + is_dedicated + player_management_version + player_management_runtime + player_management_seen_at + } + } +`; + +const pluginState = ref<{ + type: string; + game: string; + is_dedicated: boolean; + player_management_version: string | null; + player_management_runtime: string | null; + player_management_seen_at: string | null; +} | null>(null); +const now = ref(Date.now()); +const installOpen = ref(false); +const selectedRuntime = ref(null); +let pluginSub: { unsubscribe: () => void } | null = null; +let clockTimer: ReturnType | null = null; + +// A Ranked server's sanctions ride its match payload and a Practice server has +// no public players, so only community servers need the plugin. +const isCommunityServer = computed(() => { + const server = pluginState.value; + + return ( + !!server && + server.is_dedicated && + server.game !== "csgo" && + server.type !== "Ranked" && + server.type !== "Practice" + ); +}); + +const pluginActive = computed(() => { + const seenAt = pluginState.value?.player_management_seen_at; + + return !!seenAt && now.value - new Date(seenAt).getTime() < PLUGIN_STALE_MS; +}); + +const pluginVersionLabel = computed(() => { + const version = pluginState.value?.player_management_version; + + return version && /^\d/.test(version) ? `v${version}` : version; +}); + +const canInstall = computed( + () => props.gameServerNodeId === null && authStore.isAdmin, +); + +const notDetectedMessage = computed(() => { + if (props.gameServerNodeId === null) { + return t( + "pages.dedicated_servers.detail.player_management_plugin.not_detected_external", + ); + } + + if (props.gameServerNodeId) { + return t( + "pages.dedicated_servers.detail.player_management_plugin.not_detected_node", + ); + } + + return t( + "pages.dedicated_servers.detail.player_management_plugin.not_detected", + ); +}); + +const installRuntime = computed({ + get: () => + selectedRuntime.value ?? + effectivePluginRuntime( + pluginState.value?.player_management_runtime ?? props.pluginRuntime, + applicationSettings.gameServerPluginRuntime, + ), + set: (runtime: string) => { + selectedRuntime.value = runtime; + }, +}); + +const downloadUrl = computed(() => + playerManagementDownloadUrl( + installRuntime.value, + applicationSettings.latestPluginVersion(installRuntime.value), + ), +); + +const downloadName = computed(() => { + const file = downloadUrl.value.split("/").pop() ?? ""; + + return file.endsWith(".zip") ? file : null; +}); + +const configPath = computed( + () => PLAYER_MANAGEMENT_CONFIG_PATHS[installRuntime.value], +); + +const config = computed(() => + playerManagementConfig(installRuntime.value, { + apiDomain: `https://${useRuntimeConfig().public.apiDomain}`, + serverId: props.serverId, + apiPassword: props.apiPassword, + }), +); const loading = ref(false); const roster = ref>([]); @@ -150,12 +289,36 @@ onMounted(() => { pollTimer = setInterval(() => { void fetchRoster(); }, 30 * 1000); + + pluginSub = getGraphqlClient() + .subscribe({ + query: pluginSubscription, + variables: { serverId: props.serverId }, + }) + .subscribe({ + next: ({ data }: any) => { + pluginState.value = data?.servers_by_pk ?? null; + }, + error: () => { + pluginState.value = null; + }, + }); + + clockTimer = setInterval(() => { + now.value = Date.now(); + }, 15 * 1000); }); onBeforeUnmount(() => { if (pollTimer) { clearInterval(pollTimer); } + + if (clockTimer) { + clearInterval(clockTimer); + } + + pluginSub?.unsubscribe(); }); @@ -186,19 +349,157 @@ onBeforeUnmount(() => { -
- -
-

- {{ $t("pages.dedicated_servers.detail.sanctions_wip_title") }} -

-

- {{ $t("pages.dedicated_servers.detail.sanctions_wip") }} -

+
import { e_server_types_enum } from "~/generated/zeus"; import { effectivePluginRuntime } from "~/constants/rconCommands"; - -// both plugins release out of the one repo, so the tag carries the framework -const PLUGIN_RELEASE_TAG_PREFIXES: Record = { - counterstrikesharp: "css", - swiftlys2: "sw", -}; +import { pluginReleaseUrl } from "~/constants/gameServerReleases"; export default { props: { @@ -79,15 +74,13 @@ export default { return useApplicationSettingsStore().gameServerPluginRuntime; }, pluginReleaseLink() { - const prefix = - PLUGIN_RELEASE_TAG_PREFIXES[ - effectivePluginRuntime( - this.server.plugin_runtime, - this.gameServerPluginRuntime, - ) - ]; - - return `https://github.com/5stackgg/game-server/releases/tag/${prefix}-v${this.currentPluginVersion}`; + return pluginReleaseUrl( + effectivePluginRuntime( + this.server.plugin_runtime, + this.gameServerPluginRuntime, + ), + this.currentPluginVersion, + ); }, pluginVersionMismatch() { if (this.server.type !== e_server_types_enum.Ranked) { diff --git a/constants/gameServerReleases.ts b/constants/gameServerReleases.ts new file mode 100644 index 000000000..8d86b3a08 --- /dev/null +++ b/constants/gameServerReleases.ts @@ -0,0 +1,62 @@ +export const GAME_SERVER_RELEASES_URL = + "https://github.com/5stackgg/game-server/releases"; + +// Both plugins release out of the one repo, so the tag carries the framework. +const PLUGIN_RELEASE_TAG_PREFIXES: Record = { + counterstrikesharp: "css", + swiftlys2: "sw", +}; + +export const PLAYER_MANAGEMENT_CONFIG_PATHS: Record = { + counterstrikesharp: + "addons/counterstrikesharp/configs/plugins/PlayerManagement/PlayerManagement.json", + swiftlys2: "addons/swiftlys2/configs/plugins/PlayerManagement/config.jsonc", +}; + +function pluginReleaseTag(runtime: string, version: string) { + return `${PLUGIN_RELEASE_TAG_PREFIXES[runtime] ?? "sw"}-v${version}`; +} + +export function pluginReleaseUrl(runtime: string, version: string) { + return `${GAME_SERVER_RELEASES_URL}/tag/${pluginReleaseTag(runtime, version)}`; +} + +// The player management zip is published on the same release as the match +// plugin, named after its tag, so a version is all a link needs. +export function playerManagementDownloadUrl( + runtime: string, + version: string | null | undefined, +) { + if (!version) { + return GAME_SERVER_RELEASES_URL; + } + + const tag = pluginReleaseTag(runtime, version); + + return `${GAME_SERVER_RELEASES_URL}/download/${tag}/PlayerManagement-${tag}.zip`; +} + +// SwiftlyS2 binds the "PlayerManagement" section of config.jsonc, while +// CounterStrikeSharp loads PlayerManagement.json as a flat object. +export function playerManagementConfig( + runtime: string, + settings: { + apiDomain: string; + serverId: string; + apiPassword: string | null | undefined; + }, +) { + const config = { + API_DOMAIN: settings.apiDomain, + SERVER_ID: settings.serverId, + SERVER_API_PASSWORD: settings.apiPassword ?? "", + }; + + return JSON.stringify( + runtime === "counterstrikesharp" + ? { Version: 1, ...config } + : { PlayerManagement: config }, + null, + 2, + ); +} diff --git a/i18n/locales/ar_SA.json b/i18n/locales/ar_SA.json index c45f9ff2b..c2bd70cb6 100644 --- a/i18n/locales/ar_SA.json +++ b/i18n/locales/ar_SA.json @@ -3675,8 +3675,18 @@ "files": "ملفات", "player_management": "إدارة اللاعبين", "no_players": "لا يوجد لاعبون على هذا السيرفر حاليًا", - "sanctions_wip_title": "قيد التطوير", - "sanctions_wip": "الطرد يعمل اليوم. أما كتم الصوت والدردشة المباشر على السيرفرات العامة فما زال قيد التطوير — إضافة 5Stack جديدة للسيرفرات العامة قادمة قريبًا، وسيُطبَّق الكتم تلقائيًا بمجرد إطلاقها.", + "player_management_plugin": { + "active": "إضافة إدارة اللاعبين نشطة", + "not_detected": "لم تتصل إضافة إدارة اللاعبين من هذا الخادم بعد، لذلك لا يُطبَّق الكتم والمنع من الكتابة مباشرةً حتى الآن.", + "not_detected_node": "لم تتصل إضافة إدارة اللاعبين بعد. تقوم هذه العقدة بتحميلها تلقائيًا، لذا أعد تشغيل الخادم إذا استمر ذلك.", + "not_detected_external": "لا يُطبَّق الكتم والمنع من الكتابة والحظر مباشرةً إلا بعد تثبيت إضافة إدارة اللاعبين من 5Stack على هذا الخادم.", + "install": "تثبيت الإضافة", + "install_steps": "خطوات التثبيت", + "step_download": "نزّل الإضافة الخاصة بإطار العمل الذي يعمل به خادمك.", + "step_extract": "فك ضغط الملف في مجلد game/csgo على الخادم.", + "step_config": "احفظ هذا الإعداد على الخادم في:", + "step_restart": "أعد تشغيل الخادم. ستظهر الإضافة هنا كنشطة خلال دقيقة." + }, "status": { "disconnected": "السيرفر غير متصل", "no_rcon": "السيرفر متصل لكن RCON غير متاح", diff --git a/i18n/locales/da_DK.json b/i18n/locales/da_DK.json index 8ebcf4469..3578318e5 100644 --- a/i18n/locales/da_DK.json +++ b/i18n/locales/da_DK.json @@ -3675,8 +3675,18 @@ "files": "Filer", "player_management": "Spillerstyring", "no_players": "Ingen spillere på denne server lige nu", - "sanctions_wip_title": "Under udvikling", - "sanctions_wip": "Kick virker allerede. Live mute/gag på offentlige servere er stadig under udvikling — et nyt offentligt 5Stack-serverplugin er på vej, og mutes/gags håndhæves automatisk, når det udkommer.", + "player_management_plugin": { + "active": "Player Management-plugin aktivt", + "not_detected": "Player Management-pluginet har ikke meldt sig fra denne server, så mutes og gags håndhæves ikke live endnu.", + "not_detected_node": "Player Management-pluginet har ikke meldt sig. Denne node indlæser det automatisk, så genstart serveren, hvis det fortsætter.", + "not_detected_external": "Mutes, gags og bans håndhæves først live, når 5Stack Player Management-pluginet er installeret på denne server.", + "install": "Installer plugin", + "install_steps": "Installationstrin", + "step_download": "Download pluginet til det framework, din server kører.", + "step_extract": "Pak zip-filen ud i serverens game/csgo-mappe.", + "step_config": "Gem denne konfiguration på serveren her:", + "step_restart": "Genstart serveren. Pluginet vises som aktivt her inden for et minut." + }, "status": { "disconnected": "Serveren er afbrudt", "no_rcon": "Serveren er forbundet, men RCON er ikke tilgængelig", diff --git a/i18n/locales/de_DE.json b/i18n/locales/de_DE.json index d8863d808..32323b330 100644 --- a/i18n/locales/de_DE.json +++ b/i18n/locales/de_DE.json @@ -3675,8 +3675,18 @@ "files": "Dateien", "player_management": "Spielerverwaltung", "no_players": "Aktuell keine Spieler auf diesem Server", - "sanctions_wip_title": "In Arbeit", - "sanctions_wip": "Kicks funktionieren bereits. Live-Mutes und -Gags auf öffentlichen Servern sind noch in Arbeit — ein neues öffentliches 5Stack-Server-Plugin kommt bald, danach greifen Mutes und Gags automatisch.", + "player_management_plugin": { + "active": "Player-Management-Plugin aktiv", + "not_detected": "Das Player-Management-Plugin hat sich von diesem Server noch nicht gemeldet, daher werden Stummschaltungen und Chatsperren noch nicht live durchgesetzt.", + "not_detected_node": "Das Player-Management-Plugin hat sich noch nicht gemeldet. Dieser Node lädt es automatisch, starte den Server also neu, falls das anhält.", + "not_detected_external": "Stummschaltungen, Chatsperren und Banns werden erst live durchgesetzt, wenn das 5Stack-Player-Management-Plugin auf diesem Server installiert ist.", + "install": "Plugin installieren", + "install_steps": "Installationsschritte", + "step_download": "Lade das Plugin für das Framework herunter, das auf deinem Server läuft.", + "step_extract": "Entpacke die ZIP-Datei in den Ordner game/csgo des Servers.", + "step_config": "Speichere diese Konfiguration auf dem Server unter:", + "step_restart": "Starte den Server neu. Das Plugin wird hier innerhalb einer Minute als aktiv angezeigt." + }, "status": { "disconnected": "Server ist getrennt", "no_rcon": "Server ist verbunden, aber RCON ist nicht verfügbar", diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 5a06132e9..d9ac1a772 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -3675,8 +3675,18 @@ "files": "Files", "player_management": "Player Management", "no_players": "No players currently on this server", - "sanctions_wip_title": "Work in progress", - "sanctions_wip": "Kicks work today. Live mute/gag enforcement on public servers is a work in progress — a new 5Stack public server plugin is coming soon, and mutes/gags will apply automatically once it ships.", + "player_management_plugin": { + "active": "Player Management plugin active", + "not_detected": "The Player Management plugin has not checked in from this server, so mutes and gags are not enforced live yet.", + "not_detected_node": "The Player Management plugin has not checked in. This node loads it automatically, so restart the server if this persists.", + "not_detected_external": "Mutes, gags and bans are only enforced live once the 5Stack Player Management plugin is installed on this server.", + "install": "Install Plugin", + "install_steps": "Install Steps", + "step_download": "Download the plugin for the framework your server runs.", + "step_extract": "Extract the zip into the server's game/csgo folder.", + "step_config": "Save this config on the server at:", + "step_restart": "Restart the server. The plugin shows as active here within a minute." + }, "status": { "disconnected": "Server is disconnected", "no_rcon": "Server is connected but RCON is not available", diff --git a/i18n/locales/es_ES.json b/i18n/locales/es_ES.json index 8024b13f3..d7b6a9f31 100644 --- a/i18n/locales/es_ES.json +++ b/i18n/locales/es_ES.json @@ -3675,8 +3675,18 @@ "files": "Archivos", "player_management": "Gestión de jugadores", "no_players": "Ahora mismo no hay jugadores en este servidor", - "sanctions_wip_title": "En desarrollo", - "sanctions_wip": "Los kicks ya funcionan. El silenciado y el gag en directo en servidores públicos siguen en desarrollo: pronto llegará un nuevo plugin público de 5Stack y, en cuanto salga, los mutes y gags se aplicarán automáticamente.", + "player_management_plugin": { + "active": "Plugin de gestión de jugadores activo", + "not_detected": "El plugin de gestión de jugadores aún no se ha comunicado desde este servidor, así que los muteos y silencios todavía no se aplican en directo.", + "not_detected_node": "El plugin de gestión de jugadores aún no se ha comunicado. Este nodo lo carga automáticamente, así que reinicia el servidor si esto continúa.", + "not_detected_external": "Los muteos, silencios y baneos solo se aplican en directo cuando el plugin de gestión de jugadores de 5Stack está instalado en este servidor.", + "install": "Instalar plugin", + "install_steps": "Pasos de instalación", + "step_download": "Descarga el plugin para el framework que usa tu servidor.", + "step_extract": "Extrae el zip en la carpeta game/csgo del servidor.", + "step_config": "Guarda esta configuración en el servidor en:", + "step_restart": "Reinicia el servidor. El plugin aparecerá aquí como activo en menos de un minuto." + }, "status": { "disconnected": "El servidor está desconectado", "no_rcon": "El servidor está conectado, pero RCON no está disponible", diff --git a/i18n/locales/fr_FR.json b/i18n/locales/fr_FR.json index b373bb08b..b554516a7 100644 --- a/i18n/locales/fr_FR.json +++ b/i18n/locales/fr_FR.json @@ -3675,8 +3675,18 @@ "files": "Fichiers", "player_management": "Gestion des joueurs", "no_players": "Aucun joueur actuellement sur ce serveur", - "sanctions_wip_title": "En cours de développement", - "sanctions_wip": "Les kicks fonctionnent déjà. Le mute/gag en direct sur les serveurs publics est encore en cours de développement — un nouveau plugin de serveur public 5Stack arrive bientôt, et les mutes/gags s'appliqueront automatiquement dès sa sortie.", + "player_management_plugin": { + "active": "Plugin de gestion des joueurs actif", + "not_detected": "Le plugin de gestion des joueurs ne s'est pas encore signalé depuis ce serveur, les mutes et bâillons ne sont donc pas encore appliqués en direct.", + "not_detected_node": "Le plugin de gestion des joueurs ne s'est pas encore signalé. Ce nœud le charge automatiquement, redémarrez donc le serveur si cela persiste.", + "not_detected_external": "Les mutes, bâillons et bannissements ne sont appliqués en direct qu'une fois le plugin de gestion des joueurs 5Stack installé sur ce serveur.", + "install": "Installer le plugin", + "install_steps": "Étapes d'installation", + "step_download": "Téléchargez le plugin pour le framework utilisé par votre serveur.", + "step_extract": "Extrayez le zip dans le dossier game/csgo du serveur.", + "step_config": "Enregistrez cette configuration sur le serveur à l'emplacement :", + "step_restart": "Redémarrez le serveur. Le plugin apparaîtra ici comme actif en moins d'une minute." + }, "status": { "disconnected": "Le serveur est déconnecté", "no_rcon": "Le serveur est connecté mais RCON n'est pas disponible", diff --git a/i18n/locales/it_IT.json b/i18n/locales/it_IT.json index a3d9bc9b6..ac91b527b 100644 --- a/i18n/locales/it_IT.json +++ b/i18n/locales/it_IT.json @@ -3675,8 +3675,18 @@ "files": "File", "player_management": "Gestione giocatori", "no_players": "Nessun giocatore attualmente su questo server", - "sanctions_wip_title": "Lavori in corso", - "sanctions_wip": "I kick funzionano già. Il mute/gag live sui server pubblici è ancora in lavorazione: presto arriverà un nuovo plugin 5Stack per i server pubblici e mute e gag verranno applicati automaticamente.", + "player_management_plugin": { + "active": "Plugin di gestione giocatori attivo", + "not_detected": "Il plugin di gestione giocatori non ha ancora comunicato da questo server, quindi mute e gag non vengono ancora applicati in tempo reale.", + "not_detected_node": "Il plugin di gestione giocatori non ha ancora comunicato. Questo nodo lo carica automaticamente, quindi riavvia il server se il problema persiste.", + "not_detected_external": "Mute, gag e ban vengono applicati in tempo reale solo dopo aver installato il plugin di gestione giocatori di 5Stack su questo server.", + "install": "Installa plugin", + "install_steps": "Passaggi di installazione", + "step_download": "Scarica il plugin per il framework usato dal tuo server.", + "step_extract": "Estrai lo zip nella cartella game/csgo del server.", + "step_config": "Salva questa configurazione sul server in:", + "step_restart": "Riavvia il server. Il plugin risulterà attivo qui entro un minuto." + }, "status": { "disconnected": "Il server è disconnesso", "no_rcon": "Il server è connesso ma RCON non è disponibile", diff --git a/i18n/locales/ja_JP.json b/i18n/locales/ja_JP.json index 1fc450296..7dcb64420 100644 --- a/i18n/locales/ja_JP.json +++ b/i18n/locales/ja_JP.json @@ -3675,8 +3675,18 @@ "files": "ファイル", "player_management": "プレイヤー管理", "no_players": "現在このサーバーにプレイヤーはいません", - "sanctions_wip_title": "対応中の機能です", - "sanctions_wip": "キックは現時点で利用できます。パブリックサーバーでのミュート・ゲグの即時反映は現在対応中です。新しい5Stackパブリックサーバープラグインが近日公開予定で、リリース後はミュート・ゲグも自動的に適用されます。", + "player_management_plugin": { + "active": "プレイヤー管理プラグイン稼働中", + "not_detected": "このサーバーからプレイヤー管理プラグインの応答がまだないため、ミュートとチャット禁止はまだリアルタイムで適用されません。", + "not_detected_node": "プレイヤー管理プラグインの応答がまだありません。このノードは自動で読み込むため、続く場合はサーバーを再起動してください。", + "not_detected_external": "ミュート、チャット禁止、BANは、このサーバーに5Stackのプレイヤー管理プラグインをインストールするまでリアルタイムで適用されません。", + "install": "プラグインをインストール", + "install_steps": "インストール手順", + "step_download": "サーバーで使用しているフレームワーク用のプラグインをダウンロードします。", + "step_extract": "zipをサーバーのgame/csgoフォルダーに展開します。", + "step_config": "この設定をサーバーの次の場所に保存します:", + "step_restart": "サーバーを再起動します。1分以内にここで稼働中と表示されます。" + }, "status": { "disconnected": "サーバーは切断されています", "no_rcon": "サーバーは接続していますがRCONが利用できません", diff --git a/i18n/locales/ko_KR.json b/i18n/locales/ko_KR.json index 9b7deb1aa..40ad2b33e 100644 --- a/i18n/locales/ko_KR.json +++ b/i18n/locales/ko_KR.json @@ -3675,8 +3675,18 @@ "files": "파일", "player_management": "플레이어 관리", "no_players": "현재 이 서버에 접속 중인 플레이어가 없습니다", - "sanctions_wip_title": "개발 진행 중", - "sanctions_wip": "킥 기능은 현재 정상 작동합니다. 퍼블릭 서버의 실시간 뮤트/각 적용은 아직 개발 중입니다. 새로운 5Stack 퍼블릭 서버 플러그인이 곧 출시되며, 출시 후에는 뮤트/각이 자동으로 적용됩니다.", + "player_management_plugin": { + "active": "플레이어 관리 플러그인 활성", + "not_detected": "이 서버에서 플레이어 관리 플러그인이 아직 응답하지 않아 음성 및 채팅 차단이 아직 실시간으로 적용되지 않습니다.", + "not_detected_node": "플레이어 관리 플러그인이 아직 응답하지 않았습니다. 이 노드는 자동으로 불러오므로 계속되면 서버를 다시 시작하세요.", + "not_detected_external": "음성 차단, 채팅 차단, 밴은 이 서버에 5Stack 플레이어 관리 플러그인을 설치해야 실시간으로 적용됩니다.", + "install": "플러그인 설치", + "install_steps": "설치 단계", + "step_download": "서버에서 사용하는 프레임워크용 플러그인을 다운로드하세요.", + "step_extract": "zip 파일을 서버의 game/csgo 폴더에 압축 해제하세요.", + "step_config": "이 설정을 서버의 다음 위치에 저장하세요:", + "step_restart": "서버를 다시 시작하세요. 1분 안에 여기에 활성으로 표시됩니다." + }, "status": { "disconnected": "서버 연결이 끊겼습니다", "no_rcon": "서버는 연결되었지만 RCON을 사용할 수 없습니다", diff --git a/i18n/locales/pl_PL.json b/i18n/locales/pl_PL.json index 531fd8dbc..d01568585 100644 --- a/i18n/locales/pl_PL.json +++ b/i18n/locales/pl_PL.json @@ -3675,8 +3675,18 @@ "files": "Pliki", "player_management": "Zarządzanie graczami", "no_players": "Obecnie brak graczy na tym serwerze", - "sanctions_wip_title": "W trakcie prac", - "sanctions_wip": "Kicki działają już teraz. Egzekwowanie wyciszeń na żywo na serwerach publicznych jest w trakcie prac — nowy publiczny plugin 5Stack pojawi się wkrótce, a wyciszenia zaczną obowiązywać automatycznie po jego wydaniu.", + "player_management_plugin": { + "active": "Wtyczka zarządzania graczami aktywna", + "not_detected": "Wtyczka zarządzania graczami nie zgłosiła się jeszcze z tego serwera, więc wyciszenia i blokady czatu nie są jeszcze egzekwowane na żywo.", + "not_detected_node": "Wtyczka zarządzania graczami jeszcze się nie zgłosiła. Ten węzeł ładuje ją automatycznie, więc zrestartuj serwer, jeśli to się utrzymuje.", + "not_detected_external": "Wyciszenia, blokady czatu i bany są egzekwowane na żywo dopiero po zainstalowaniu wtyczki zarządzania graczami 5Stack na tym serwerze.", + "install": "Zainstaluj wtyczkę", + "install_steps": "Kroki instalacji", + "step_download": "Pobierz wtyczkę dla frameworka, na którym działa twój serwer.", + "step_extract": "Rozpakuj plik zip do folderu game/csgo serwera.", + "step_config": "Zapisz tę konfigurację na serwerze w:", + "step_restart": "Zrestartuj serwer. Wtyczka pojawi się tutaj jako aktywna w ciągu minuty." + }, "status": { "disconnected": "Serwer jest rozłączony", "no_rcon": "Serwer jest połączony, ale RCON jest niedostępny", diff --git a/i18n/locales/pt_BR.json b/i18n/locales/pt_BR.json index 68d8ef932..05eac6f46 100644 --- a/i18n/locales/pt_BR.json +++ b/i18n/locales/pt_BR.json @@ -3675,8 +3675,18 @@ "files": "Arquivos", "player_management": "Gerenciamento de Jogadores", "no_players": "Nenhum jogador atualmente neste servidor", - "sanctions_wip_title": "Em desenvolvimento", - "sanctions_wip": "Expulsões já funcionam hoje. A aplicação de mudo/gag ao vivo em servidores públicos está em desenvolvimento — um novo plugin público do 5Stack está chegando em breve, e os mutes/gags serão aplicados automaticamente assim que for lançado.", + "player_management_plugin": { + "active": "Plugin de gerenciamento de jogadores ativo", + "not_detected": "O plugin de gerenciamento de jogadores ainda não se comunicou a partir deste servidor, então mutes e silenciamentos ainda não são aplicados ao vivo.", + "not_detected_node": "O plugin de gerenciamento de jogadores ainda não se comunicou. Este nó o carrega automaticamente, então reinicie o servidor se isso continuar.", + "not_detected_external": "Mutes, silenciamentos e banimentos só são aplicados ao vivo depois que o plugin de gerenciamento de jogadores da 5Stack é instalado neste servidor.", + "install": "Instalar plugin", + "install_steps": "Passos de instalação", + "step_download": "Baixe o plugin para o framework que seu servidor usa.", + "step_extract": "Extraia o zip na pasta game/csgo do servidor.", + "step_config": "Salve esta configuração no servidor em:", + "step_restart": "Reinicie o servidor. O plugin aparecerá aqui como ativo em até um minuto." + }, "status": { "disconnected": "O servidor está desconectado", "no_rcon": "O servidor está conectado, mas o RCON não está disponível", diff --git a/i18n/locales/ru_RU.json b/i18n/locales/ru_RU.json index 7d354ba44..076bc356a 100644 --- a/i18n/locales/ru_RU.json +++ b/i18n/locales/ru_RU.json @@ -3675,8 +3675,18 @@ "files": "Файлы", "player_management": "Управление игроками", "no_players": "На этом сервере сейчас нет игроков", - "sanctions_wip_title": "В разработке", - "sanctions_wip": "Кики работают уже сейчас. Применение мута/гага в реальном времени на публичных серверах пока в разработке — скоро выйдет новый публичный плагин 5Stack, и муты/гаги начнут применяться автоматически после его выпуска.", + "player_management_plugin": { + "active": "Плагин управления игроками активен", + "not_detected": "Плагин управления игроками ещё не отозвался с этого сервера, поэтому заглушение и запрет чата пока не применяются в реальном времени.", + "not_detected_node": "Плагин управления игроками ещё не отозвался. Этот узел загружает его автоматически, поэтому перезапустите сервер, если это продолжится.", + "not_detected_external": "Заглушение, запрет чата и баны применяются в реальном времени только после установки плагина управления игроками 5Stack на этот сервер.", + "install": "Установить плагин", + "install_steps": "Шаги установки", + "step_download": "Скачайте плагин для фреймворка, на котором работает ваш сервер.", + "step_extract": "Распакуйте zip в папку game/csgo сервера.", + "step_config": "Сохраните эту конфигурацию на сервере по пути:", + "step_restart": "Перезапустите сервер. Плагин появится здесь как активный в течение минуты." + }, "status": { "disconnected": "Сервер отключён", "no_rcon": "Сервер подключён, но RCON недоступен", diff --git a/i18n/locales/sv_SE.json b/i18n/locales/sv_SE.json index 637e1fba5..fe2bc207c 100644 --- a/i18n/locales/sv_SE.json +++ b/i18n/locales/sv_SE.json @@ -3675,8 +3675,18 @@ "files": "Filer", "player_management": "Spelarhantering", "no_players": "Inga spelare på servern just nu", - "sanctions_wip_title": "Under utveckling", - "sanctions_wip": "Kickar fungerar redan idag. Live mute/gag-tillämpning på publika servrar är under utveckling — en ny 5Stack-serverplugin är på gång, och mute/gag kommer att tillämpas automatiskt när den släpps.", + "player_management_plugin": { + "active": "Plugin för spelarhantering aktivt", + "not_detected": "Pluginet för spelarhantering har inte hört av sig från den här servern ännu, så tystningar och chattförbud tillämpas inte live än.", + "not_detected_node": "Pluginet för spelarhantering har inte hört av sig ännu. Den här noden laddar det automatiskt, så starta om servern om det fortsätter.", + "not_detected_external": "Tystningar, chattförbud och avstängningar tillämpas live först när 5Stacks plugin för spelarhantering är installerat på den här servern.", + "install": "Installera plugin", + "install_steps": "Installationssteg", + "step_download": "Ladda ner pluginet för det ramverk din server kör.", + "step_extract": "Packa upp zip-filen i serverns game/csgo-mapp.", + "step_config": "Spara den här konfigurationen på servern i:", + "step_restart": "Starta om servern. Pluginet visas som aktivt här inom en minut." + }, "status": { "disconnected": "Servern är frånkopplad", "no_rcon": "Servern är ansluten men RCON är inte tillgängligt", diff --git a/i18n/locales/tr_TR.json b/i18n/locales/tr_TR.json index 24abe40bf..f8c42e9b5 100644 --- a/i18n/locales/tr_TR.json +++ b/i18n/locales/tr_TR.json @@ -3675,8 +3675,18 @@ "files": "Dosyalar", "player_management": "Oyuncu yönetimi", "no_players": "Bu sunucuda şu anda oyuncu yok", - "sanctions_wip_title": "Geliştirme aşamasında", - "sanctions_wip": "Kick işlemi bugün çalışıyor. Herkese açık sunucularda canlı sessize alma/susturma uygulaması henüz geliştirme aşamasında — yeni bir 5Stack herkese açık sunucu eklentisi yakında geliyor, yayınlandığında sessize alma/susturma otomatik olarak uygulanacak.", + "player_management_plugin": { + "active": "Oyuncu yönetimi eklentisi etkin", + "not_detected": "Oyuncu yönetimi eklentisi bu sunucudan henüz bildirim göndermedi, bu yüzden susturmalar ve sohbet yasakları henüz canlı uygulanmıyor.", + "not_detected_node": "Oyuncu yönetimi eklentisi henüz bildirim göndermedi. Bu düğüm onu otomatik yükler, bu durum sürerse sunucuyu yeniden başlatın.", + "not_detected_external": "Susturmalar, sohbet yasakları ve yasaklamalar ancak 5Stack oyuncu yönetimi eklentisi bu sunucuya kurulduğunda canlı uygulanır.", + "install": "Eklentiyi Kur", + "install_steps": "Kurulum Adımları", + "step_download": "Sunucunuzun çalıştırdığı çerçeve için eklentiyi indirin.", + "step_extract": "Zip dosyasını sunucunun game/csgo klasörüne çıkarın.", + "step_config": "Bu yapılandırmayı sunucuda şu konuma kaydedin:", + "step_restart": "Sunucuyu yeniden başlatın. Eklenti bir dakika içinde burada etkin olarak görünür." + }, "status": { "disconnected": "Sunucu bağlı değil", "no_rcon": "Sunucu bağlı ama RCON kullanılamıyor", diff --git a/i18n/locales/uk_UA.json b/i18n/locales/uk_UA.json index b3c807c72..b628f5a07 100644 --- a/i18n/locales/uk_UA.json +++ b/i18n/locales/uk_UA.json @@ -3675,8 +3675,18 @@ "files": "Файли", "player_management": "Керування гравцями", "no_players": "Наразі на цьому сервері немає гравців", - "sanctions_wip_title": "У розробці", - "sanctions_wip": "Кіки вже працюють. Застосування живих мутів/гегів на публічних серверах ще в розробці — новий публічний плагін 5Stack скоро вийде, і мути/геги застосовуватимуться автоматично, щойно він з'явиться.", + "player_management_plugin": { + "active": "Плагін керування гравцями активний", + "not_detected": "Плагін керування гравцями ще не відгукнувся з цього сервера, тому заглушення та заборона чату поки не застосовуються в реальному часі.", + "not_detected_node": "Плагін керування гравцями ще не відгукнувся. Цей вузол завантажує його автоматично, тож перезапустіть сервер, якщо це триватиме.", + "not_detected_external": "Заглушення, заборона чату та бани застосовуються в реальному часі лише після встановлення плагіна керування гравцями 5Stack на цей сервер.", + "install": "Встановити плагін", + "install_steps": "Кроки встановлення", + "step_download": "Завантажте плагін для фреймворку, на якому працює ваш сервер.", + "step_extract": "Розпакуйте zip у папку game/csgo сервера.", + "step_config": "Збережіть цю конфігурацію на сервері за шляхом:", + "step_restart": "Перезапустіть сервер. Плагін з'явиться тут як активний протягом хвилини." + }, "status": { "disconnected": "Сервер відключено", "no_rcon": "Сервер підключено, але RCON недоступний", diff --git a/i18n/locales/zh_Hans.json b/i18n/locales/zh_Hans.json index 7c9eed786..e2e6ca9e4 100644 --- a/i18n/locales/zh_Hans.json +++ b/i18n/locales/zh_Hans.json @@ -3675,8 +3675,18 @@ "files": "文件", "player_management": "玩家管理", "no_players": "此服务器上当前无玩家", - "sanctions_wip_title": "功能开发中", - "sanctions_wip": "踢出功能现已可用。公共服务器上的实时静音/禁言执行正在开发中——新的 5Stack 公共服务器插件即将推出,上线后静音/禁言将自动应用。", + "player_management_plugin": { + "active": "玩家管理插件运行中", + "not_detected": "玩家管理插件尚未从此服务器报到,因此禁麦和禁言暂时无法实时生效。", + "not_detected_node": "玩家管理插件尚未报到。此节点会自动加载它,如果持续如此,请重启服务器。", + "not_detected_external": "只有在此服务器上安装 5Stack 玩家管理插件后,禁麦、禁言和封禁才会实时生效。", + "install": "安装插件", + "install_steps": "安装步骤", + "step_download": "下载与服务器所用框架对应的插件。", + "step_extract": "将 zip 解压到服务器的 game/csgo 文件夹。", + "step_config": "将此配置保存到服务器上的以下位置:", + "step_restart": "重启服务器。插件会在一分钟内在此显示为运行中。" + }, "status": { "disconnected": "服务器已断开", "no_rcon": "服务器已连接但 RCON 不可用", diff --git a/i18n/locales/zh_Hant.json b/i18n/locales/zh_Hant.json index 61b076ce0..c220bfed4 100644 --- a/i18n/locales/zh_Hant.json +++ b/i18n/locales/zh_Hant.json @@ -3675,8 +3675,18 @@ "files": "檔案", "player_management": "玩家管理", "no_players": "此伺服器上目前沒有玩家", - "sanctions_wip_title": "功能開發中", - "sanctions_wip": "踢出功能現已可用。公開伺服器上的即時靜音/禁言執行仍在開發中——全新的 5Stack 公開伺服器外掛即將推出,上線後靜音/禁言將自動套用。", + "player_management_plugin": { + "active": "玩家管理外掛運作中", + "not_detected": "玩家管理外掛尚未從此伺服器回報,因此禁麥和禁言暫時無法即時生效。", + "not_detected_node": "玩家管理外掛尚未回報。此節點會自動載入它,若持續如此,請重新啟動伺服器。", + "not_detected_external": "只有在此伺服器上安裝 5Stack 玩家管理外掛後,禁麥、禁言和封鎖才會即時生效。", + "install": "安裝外掛", + "install_steps": "安裝步驟", + "step_download": "下載與伺服器所用框架對應的外掛。", + "step_extract": "將 zip 解壓縮到伺服器的 game/csgo 資料夾。", + "step_config": "將此設定儲存到伺服器上的以下位置:", + "step_restart": "重新啟動伺服器。外掛會在一分鐘內在此顯示為運作中。" + }, "status": { "disconnected": "伺服器已中斷連線", "no_rcon": "伺服器已連線,但 RCON 無法使用", diff --git a/pages/dedicated-servers/[id].vue b/pages/dedicated-servers/[id].vue index 24435634b..8e7513e78 100644 --- a/pages/dedicated-servers/[id].vue +++ b/pages/dedicated-servers/[id].vue @@ -259,6 +259,9 @@ const titleClasses = diff --git a/stores/ApplicationSettings.ts b/stores/ApplicationSettings.ts index 19c882720..2bf68f38b 100644 --- a/stores/ApplicationSettings.ts +++ b/stores/ApplicationSettings.ts @@ -93,6 +93,15 @@ export const useApplicationSettingsStore = defineStore( ); }); + // Newest first, so the first match is the latest release of a runtime. + const latestPluginVersion = (runtime: string): string | null => { + return ( + pluginVersions.value.find((pluginVersion) => { + return pluginVersion.runtime === runtime; + })?.version ?? null + ); + }; + const subscribeToPluginVersion = async () => { const { subscribe } = useSubscriptionManager(); const authStore = useAuthStore(); @@ -732,6 +741,7 @@ export const useApplicationSettingsStore = defineStore( defaultBroadcastHud, canCreateMatch, currentPluginVersion, + latestPluginVersion, gameServerPluginRuntime, settingsLoaded, brandName, diff --git a/tests/components/SanctionPlayer.spec.ts b/tests/components/SanctionPlayer.spec.ts index dce49efc9..a0397510c 100644 --- a/tests/components/SanctionPlayer.spec.ts +++ b/tests/components/SanctionPlayer.spec.ts @@ -6,6 +6,13 @@ import SanctionPlayer from "~/components/SanctionPlayer.vue"; const PLAYER = "76561198000000001"; +const { toast } = vi.hoisted(() => ({ toast: vi.fn() })); + +vi.mock("@/components/ui/toast", async (importOriginal) => ({ + ...(await importOriginal()), + toast, +})); + let mutate: ReturnType; let unmount: (() => void) | null = null; @@ -15,14 +22,20 @@ afterEach(() => { vi.restoreAllMocks(); }); -async function openDrawer(type: string, serverId?: string) { +async function openDrawer( + type: string, + serverId?: string, + result: { enforced: boolean; message: string } = { + enforced: false, + message: "warning saved", + }, +) { const client = (useNuxtApp() as any).$apollo.defaultClient; mutate = vi.fn().mockResolvedValue({ data: { sanctionServerPlayer: { id: "sanction-1", - enforced: false, - message: "warning saved", + ...result, }, }, }); @@ -160,3 +173,50 @@ describe("SanctionPlayer warnings", () => { }); }); }); + +describe("SanctionPlayer on a server", () => { + afterEach(() => { + toast.mockReset(); + }); + + it("tells the moderator why the server could not enforce it live", async () => { + const wrapper = await openDrawer("mute", "server-1", { + enforced: false, + message: + "sanction saved; the Player Management plugin is not installed on this server", + }); + + await submit(wrapper); + + expect(toast).toHaveBeenCalledWith( + expect.objectContaining({ + description: + "sanction saved; the Player Management plugin is not installed on this server", + }), + ); + }); + + it("keeps the toast plain once the server has applied it", async () => { + const wrapper = await openDrawer("gag", "server-1", { + enforced: true, + message: "sanction saved and synced to server", + }); + + await submit(wrapper); + + expect(toast).toHaveBeenCalledWith( + expect.objectContaining({ description: undefined }), + ); + }); + + it("never repeats a warning's save message, which no server enforces", async () => { + const wrapper = await openDrawer("warning", "server-1"); + + await typeReason("Griefing"); + await submit(wrapper); + + expect(toast).toHaveBeenCalledWith( + expect.objectContaining({ description: undefined }), + ); + }); +}); diff --git a/tests/components/servers/ServerPlayerManagement.spec.ts b/tests/components/servers/ServerPlayerManagement.spec.ts new file mode 100644 index 000000000..accf7597a --- /dev/null +++ b/tests/components/servers/ServerPlayerManagement.spec.ts @@ -0,0 +1,139 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { flushPromises } from "@vue/test-utils"; +import { mountSuspended } from "@nuxt/test-utils/runtime"; +import ServerPlayerManagement from "~/components/servers/ServerPlayerManagement.vue"; +import { useAuthStore } from "~/stores/AuthStore"; +import { useApplicationSettingsStore } from "~/stores/ApplicationSettings"; + +const state = vi.hoisted(() => ({ + server: null as Record | null, +})); + +vi.mock("~/graphql/getGraphqlClient", async () => { + const { print } = await import("graphql"); + + return { + default: () => ({ + query: async () => ({ data: {} }), + subscribe: ({ query }: any) => ({ + subscribe: ({ next }: any) => { + const text = typeof query === "string" ? query : print(query); + + next({ + data: text.includes("player_management_seen_at") + ? { servers_by_pk: state.server } + : {}, + }); + + return { unsubscribe() {} }; + }, + }), + }), + }; +}); + +const community = (seenAt: string | null) => ({ + id: "server-1", + type: "Casual", + game: "cs2", + is_dedicated: true, + player_management_version: seenAt ? "0.0.412" : null, + player_management_runtime: seenAt ? "swiftlys2" : null, + player_management_seen_at: seenAt, +}); + +async function mountCard(props: Record = {}) { + const wrapper = await mountSuspended(ServerPlayerManagement, { + props: { serverId: "server-1", ...props }, + }); + await flushPromises(); + return wrapper; +} + +beforeEach(() => { + state.server = null; + vi.spyOn( + useApplicationSettingsStore(), + "latestPluginVersion", + ).mockImplementation((runtime: string) => + runtime === "swiftlys2" ? "0.0.412" : "0.0.390", + ); + useAuthStore().me = { + steam_id: "76561198000000001", + role: "administrator", + } as any; +}); + +describe("ServerPlayerManagement plugin status", () => { + it("shows a community server whose plugin checked in as active", async () => { + state.server = community(new Date().toISOString()); + + const wrapper = await mountCard({ gameServerNodeId: "node-1" }); + + expect(wrapper.text()).toContain("Player Management plugin active"); + expect(wrapper.text()).toContain("v0.0.412"); + expect(wrapper.text()).toContain("SwiftlyS2"); + expect(wrapper.text()).not.toContain("has not checked in"); + }); + + it("treats a heartbeat older than a few minutes as the plugin being gone", async () => { + state.server = community( + new Date(Date.now() - 10 * 60 * 1000).toISOString(), + ); + + const wrapper = await mountCard({ gameServerNodeId: "node-1" }); + + expect(wrapper.text()).toContain("This node loads it automatically"); + expect(wrapper.text()).not.toContain("Install Plugin"); + }); + + it("offers an administrator the download and config for a server outside a node", async () => { + state.server = community(null); + + const wrapper = await mountCard({ + gameServerNodeId: null, + apiPassword: "secret-password", + }); + + expect(wrapper.text()).toContain( + "only enforced live once the 5Stack Player Management plugin is installed", + ); + + const install = wrapper + .findAll("button") + .find((button) => button.text() === "Install Plugin"); + await install!.trigger("click"); + + const download = wrapper.find('a[href*="PlayerManagement"]'); + expect(download.attributes("href")).toBe( + "https://github.com/5stackgg/game-server/releases/download/sw-v0.0.412/PlayerManagement-sw-v0.0.412.zip", + ); + expect(wrapper.text()).toContain( + "addons/swiftlys2/configs/plugins/PlayerManagement/config.jsonc", + ); + expect(wrapper.find("pre").text()).toContain('"PlayerManagement"'); + expect(wrapper.find("pre").text()).toContain("secret-password"); + }); + + it("never shows install steps to someone who is not an administrator", async () => { + useAuthStore().me = { + steam_id: "76561198000000002", + role: "moderator", + } as any; + state.server = community(null); + + const wrapper = await mountCard({ gameServerNodeId: null }); + + expect(wrapper.text()).toContain("installed on this server"); + expect(wrapper.text()).not.toContain("Install Plugin"); + }); + + it("says nothing about the plugin on a Ranked server", async () => { + state.server = { ...community(null), type: "Ranked" }; + + const wrapper = await mountCard({ gameServerNodeId: "node-1" }); + + expect(wrapper.text()).not.toContain("Player Management plugin"); + expect(wrapper.text()).not.toContain("has not checked in"); + }); +}); diff --git a/tests/constants/gameServerReleases.spec.ts b/tests/constants/gameServerReleases.spec.ts new file mode 100644 index 000000000..504a6853e --- /dev/null +++ b/tests/constants/gameServerReleases.spec.ts @@ -0,0 +1,70 @@ +// @vitest-environment node +import { describe, expect, it } from "vitest"; +import { + GAME_SERVER_RELEASES_URL, + PLAYER_MANAGEMENT_CONFIG_PATHS, + playerManagementConfig, + playerManagementDownloadUrl, + pluginReleaseUrl, +} from "~/constants/gameServerReleases"; + +describe("gameServerReleases", () => { + it("links a plugin release by its framework-prefixed tag", () => { + expect(pluginReleaseUrl("swiftlys2", "0.0.412")).toBe( + `${GAME_SERVER_RELEASES_URL}/tag/sw-v0.0.412`, + ); + expect(pluginReleaseUrl("counterstrikesharp", "0.0.390")).toBe( + `${GAME_SERVER_RELEASES_URL}/tag/css-v0.0.390`, + ); + }); + + // The asset name is set in game-server's plugin-build workflow; this is the + // other half of that contract. + it("downloads the player management zip published on that release", () => { + expect(playerManagementDownloadUrl("swiftlys2", "0.0.412")).toBe( + `${GAME_SERVER_RELEASES_URL}/download/sw-v0.0.412/PlayerManagement-sw-v0.0.412.zip`, + ); + expect(playerManagementDownloadUrl("counterstrikesharp", "0.0.390")).toBe( + `${GAME_SERVER_RELEASES_URL}/download/css-v0.0.390/PlayerManagement-css-v0.0.390.zip`, + ); + }); + + it("falls back to the releases page before any version is known", () => { + expect(playerManagementDownloadUrl("swiftlys2", null)).toBe( + GAME_SERVER_RELEASES_URL, + ); + }); + + it("nests the SwiftlyS2 config under its section and keeps CounterStrikeSharp flat", () => { + const settings = { + apiDomain: "https://api.example.com", + serverId: "server-1", + apiPassword: "secret", + }; + + expect(JSON.parse(playerManagementConfig("swiftlys2", settings))).toEqual({ + PlayerManagement: { + API_DOMAIN: "https://api.example.com", + SERVER_ID: "server-1", + SERVER_API_PASSWORD: "secret", + }, + }); + expect( + JSON.parse(playerManagementConfig("counterstrikesharp", settings)), + ).toEqual({ + Version: 1, + API_DOMAIN: "https://api.example.com", + SERVER_ID: "server-1", + SERVER_API_PASSWORD: "secret", + }); + }); + + it("points each framework at the file its plugin reads", () => { + expect(PLAYER_MANAGEMENT_CONFIG_PATHS.swiftlys2).toBe( + "addons/swiftlys2/configs/plugins/PlayerManagement/config.jsonc", + ); + expect(PLAYER_MANAGEMENT_CONFIG_PATHS.counterstrikesharp).toBe( + "addons/counterstrikesharp/configs/plugins/PlayerManagement/PlayerManagement.json", + ); + }); +}); From 469b8cbf21f1ab23fc8cb04c16b746ab93a760fe Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 30 Sep 2026 00:12:01 -0400 Subject: [PATCH 2/2] bug: player management card review fixes: redeploy hint, hidden password, optional subscription --- components/PlayerSanctions.vue | 13 ++-- components/servers/ServerPlayerManagement.vue | 39 ++++++++++-- i18n/locales/ar_SA.json | 2 +- i18n/locales/da_DK.json | 2 +- i18n/locales/de_DE.json | 2 +- i18n/locales/en.json | 2 +- i18n/locales/es_ES.json | 2 +- i18n/locales/fr_FR.json | 2 +- i18n/locales/it_IT.json | 2 +- i18n/locales/ja_JP.json | 2 +- i18n/locales/ko_KR.json | 2 +- i18n/locales/pl_PL.json | 2 +- i18n/locales/pt_BR.json | 2 +- i18n/locales/ru_RU.json | 2 +- i18n/locales/sv_SE.json | 2 +- i18n/locales/tr_TR.json | 2 +- i18n/locales/uk_UA.json | 2 +- i18n/locales/zh_Hans.json | 2 +- i18n/locales/zh_Hant.json | 2 +- tests/components/PlayerSanctions.spec.ts | 62 ++++++++++++++++++- tests/components/SanctionPlayer.spec.ts | 13 ++++ .../servers/ServerPlayerManagement.spec.ts | 12 +++- 22 files changed, 143 insertions(+), 30 deletions(-) diff --git a/components/PlayerSanctions.vue b/components/PlayerSanctions.vue index 7c154f1fb..69fd7a51f 100644 --- a/components/PlayerSanctions.vue +++ b/components/PlayerSanctions.vue @@ -1013,6 +1013,9 @@ export default { if (!this.sanctionToDelete) { return; } + // Held locally: the dialog closes on confirm, so another removal can + // replace sanctionToDelete while this one waits on the server. + const sanction = this.sanctionToDelete; this.removingSanction = true; try { @@ -1038,8 +1041,8 @@ export default { variables: { serverId: this.serverId ?? null, steam_id: this.playerId, - type: this.sanctionToDelete.type, - sanction_id: this.sanctionToDelete.id, + type: sanction.type, + sanction_id: sanction.id, }, }); @@ -1049,14 +1052,16 @@ export default { title: this.$t("player.sanctions.removed"), description: this.serverId && - this.sanctionToDelete.type !== "warning" && + sanction.type !== "warning" && result && !result.enforced ? result.message : undefined, }); - this.sanctionToDelete = null; + if (this.sanctionToDelete === sanction) { + this.sanctionToDelete = null; + } } catch (error) { console.error("Failed to remove sanction:", error); toast({ diff --git a/components/servers/ServerPlayerManagement.vue b/components/servers/ServerPlayerManagement.vue index 0278b3d2a..ed12b15fb 100644 --- a/components/servers/ServerPlayerManagement.vue +++ b/components/servers/ServerPlayerManagement.vue @@ -2,7 +2,14 @@ import { ref, computed, onMounted, onBeforeUnmount } from "vue"; import { useI18n } from "vue-i18n"; import gql from "graphql-tag"; -import { RefreshCw, Users, TriangleAlert, Download } from "lucide-vue-next"; +import { + RefreshCw, + Users, + TriangleAlert, + Download, + Eye, + EyeOff, +} from "lucide-vue-next"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs"; @@ -28,8 +35,8 @@ import { playerManagementDownloadUrl, } from "~/constants/gameServerReleases"; -// Only an administrator can read the node assignment and the api password, so -// for anyone else these arrive undefined and the install steps stay hidden. +// Only an administrator can read the api password, so only they get the +// install steps; everyone else is told the plugin is missing and nothing more. const props = defineProps<{ serverId: string; gameServerNodeId?: string | null; @@ -74,6 +81,7 @@ const pluginState = ref<{ } | null>(null); const now = ref(Date.now()); const installOpen = ref(false); +const showConfig = ref(false); const selectedRuntime = ref(null); let pluginSub: { unsubscribe: () => void } | null = null; let clockTimer: ReturnType | null = null; @@ -290,10 +298,13 @@ onMounted(() => { void fetchRoster(); }, 30 * 1000); + // Optional: the columns only exist once the api has migrated, and until then + // the card simply says nothing about the plugin. pluginSub = getGraphqlClient() .subscribe({ query: pluginSubscription, variables: { serverId: props.serverId }, + context: { optional: true }, }) .subscribe({ next: ({ data }: any) => { @@ -354,7 +365,7 @@ onBeforeUnmount(() => { v-if="pluginActive" class="mb-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground" > - + {{ $t("pages.dedicated_servers.detail.player_management_plugin.active") @@ -371,6 +382,7 @@ onBeforeUnmount(() => { variant="ghost" size="xs" class="ml-auto" + :aria-expanded="installOpen" @click="installOpen = !installOpen" > {{ @@ -392,6 +404,7 @@ onBeforeUnmount(() => { variant="outline" size="sm" class="shrink-0" + :aria-expanded="installOpen" @click="installOpen = !installOpen" > {{ @@ -417,7 +430,7 @@ onBeforeUnmount(() => { ) }}

- + SwiftlyS2 @@ -474,7 +487,21 @@ onBeforeUnmount(() => {
-
+ +
{{ config }}
diff --git a/i18n/locales/ar_SA.json b/i18n/locales/ar_SA.json index c2bd70cb6..0ebf14035 100644 --- a/i18n/locales/ar_SA.json +++ b/i18n/locales/ar_SA.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "إضافة إدارة اللاعبين نشطة", "not_detected": "لم تتصل إضافة إدارة اللاعبين من هذا الخادم بعد، لذلك لا يُطبَّق الكتم والمنع من الكتابة مباشرةً حتى الآن.", - "not_detected_node": "لم تتصل إضافة إدارة اللاعبين بعد. تقوم هذه العقدة بتحميلها تلقائيًا، لذا أعد تشغيل الخادم إذا استمر ذلك.", + "not_detected_node": "لم تتصل إضافة إدارة اللاعبين بعد. تحصل الخوادم التي أُنشئت قبل إصدارها عليها بعد إعادة النشر: أوقف تشغيل الخادم ثم شغّله مجددًا.", "not_detected_external": "لا يُطبَّق الكتم والمنع من الكتابة والحظر مباشرةً إلا بعد تثبيت إضافة إدارة اللاعبين من 5Stack على هذا الخادم.", "install": "تثبيت الإضافة", "install_steps": "خطوات التثبيت", diff --git a/i18n/locales/da_DK.json b/i18n/locales/da_DK.json index 3578318e5..153d3d11e 100644 --- a/i18n/locales/da_DK.json +++ b/i18n/locales/da_DK.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Player Management-plugin aktivt", "not_detected": "Player Management-pluginet har ikke meldt sig fra denne server, så mutes og gags håndhæves ikke live endnu.", - "not_detected_node": "Player Management-pluginet har ikke meldt sig. Denne node indlæser det automatisk, så genstart serveren, hvis det fortsætter.", + "not_detected_node": "Player Management-pluginet har ikke meldt sig. Servere oprettet før det kom, får det, når de udrulles igen: slå serveren fra og til igen.", "not_detected_external": "Mutes, gags og bans håndhæves først live, når 5Stack Player Management-pluginet er installeret på denne server.", "install": "Installer plugin", "install_steps": "Installationstrin", diff --git a/i18n/locales/de_DE.json b/i18n/locales/de_DE.json index 32323b330..a2970d885 100644 --- a/i18n/locales/de_DE.json +++ b/i18n/locales/de_DE.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Player-Management-Plugin aktiv", "not_detected": "Das Player-Management-Plugin hat sich von diesem Server noch nicht gemeldet, daher werden Stummschaltungen und Chatsperren noch nicht live durchgesetzt.", - "not_detected_node": "Das Player-Management-Plugin hat sich noch nicht gemeldet. Dieser Node lädt es automatisch, starte den Server also neu, falls das anhält.", + "not_detected_node": "Das Player-Management-Plugin hat sich noch nicht gemeldet. Server, die vor seiner Veröffentlichung erstellt wurden, erhalten es nach einer Neubereitstellung: Schalte den Server aus und wieder ein.", "not_detected_external": "Stummschaltungen, Chatsperren und Banns werden erst live durchgesetzt, wenn das 5Stack-Player-Management-Plugin auf diesem Server installiert ist.", "install": "Plugin installieren", "install_steps": "Installationsschritte", diff --git a/i18n/locales/en.json b/i18n/locales/en.json index d9ac1a772..b12a14247 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Player Management plugin active", "not_detected": "The Player Management plugin has not checked in from this server, so mutes and gags are not enforced live yet.", - "not_detected_node": "The Player Management plugin has not checked in. This node loads it automatically, so restart the server if this persists.", + "not_detected_node": "The Player Management plugin has not checked in. Servers created before it shipped get it once redeployed: switch the server off and back on.", "not_detected_external": "Mutes, gags and bans are only enforced live once the 5Stack Player Management plugin is installed on this server.", "install": "Install Plugin", "install_steps": "Install Steps", diff --git a/i18n/locales/es_ES.json b/i18n/locales/es_ES.json index d7b6a9f31..a1fcfbd7e 100644 --- a/i18n/locales/es_ES.json +++ b/i18n/locales/es_ES.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Plugin de gestión de jugadores activo", "not_detected": "El plugin de gestión de jugadores aún no se ha comunicado desde este servidor, así que los muteos y silencios todavía no se aplican en directo.", - "not_detected_node": "El plugin de gestión de jugadores aún no se ha comunicado. Este nodo lo carga automáticamente, así que reinicia el servidor si esto continúa.", + "not_detected_node": "El plugin de gestión de jugadores aún no se ha comunicado. Los servidores creados antes de su lanzamiento lo reciben al volver a desplegarse: desactiva el servidor y vuelve a activarlo.", "not_detected_external": "Los muteos, silencios y baneos solo se aplican en directo cuando el plugin de gestión de jugadores de 5Stack está instalado en este servidor.", "install": "Instalar plugin", "install_steps": "Pasos de instalación", diff --git a/i18n/locales/fr_FR.json b/i18n/locales/fr_FR.json index b554516a7..353e40547 100644 --- a/i18n/locales/fr_FR.json +++ b/i18n/locales/fr_FR.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Plugin de gestion des joueurs actif", "not_detected": "Le plugin de gestion des joueurs ne s'est pas encore signalé depuis ce serveur, les mutes et bâillons ne sont donc pas encore appliqués en direct.", - "not_detected_node": "Le plugin de gestion des joueurs ne s'est pas encore signalé. Ce nœud le charge automatiquement, redémarrez donc le serveur si cela persiste.", + "not_detected_node": "Le plugin de gestion des joueurs ne s'est pas encore signalé. Les serveurs créés avant sa sortie le reçoivent une fois redéployés : désactivez puis réactivez le serveur.", "not_detected_external": "Les mutes, bâillons et bannissements ne sont appliqués en direct qu'une fois le plugin de gestion des joueurs 5Stack installé sur ce serveur.", "install": "Installer le plugin", "install_steps": "Étapes d'installation", diff --git a/i18n/locales/it_IT.json b/i18n/locales/it_IT.json index ac91b527b..7777cb392 100644 --- a/i18n/locales/it_IT.json +++ b/i18n/locales/it_IT.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Plugin di gestione giocatori attivo", "not_detected": "Il plugin di gestione giocatori non ha ancora comunicato da questo server, quindi mute e gag non vengono ancora applicati in tempo reale.", - "not_detected_node": "Il plugin di gestione giocatori non ha ancora comunicato. Questo nodo lo carica automaticamente, quindi riavvia il server se il problema persiste.", + "not_detected_node": "Il plugin di gestione giocatori non ha ancora comunicato. I server creati prima del suo rilascio lo ricevono dopo un nuovo deploy: disattiva e riattiva il server.", "not_detected_external": "Mute, gag e ban vengono applicati in tempo reale solo dopo aver installato il plugin di gestione giocatori di 5Stack su questo server.", "install": "Installa plugin", "install_steps": "Passaggi di installazione", diff --git a/i18n/locales/ja_JP.json b/i18n/locales/ja_JP.json index 7dcb64420..c6917a2bc 100644 --- a/i18n/locales/ja_JP.json +++ b/i18n/locales/ja_JP.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "プレイヤー管理プラグイン稼働中", "not_detected": "このサーバーからプレイヤー管理プラグインの応答がまだないため、ミュートとチャット禁止はまだリアルタイムで適用されません。", - "not_detected_node": "プレイヤー管理プラグインの応答がまだありません。このノードは自動で読み込むため、続く場合はサーバーを再起動してください。", + "not_detected_node": "プレイヤー管理プラグインの応答がまだありません。公開前に作成したサーバーは再デプロイで導入されます:サーバーを一度無効にしてから再度有効にしてください。", "not_detected_external": "ミュート、チャット禁止、BANは、このサーバーに5Stackのプレイヤー管理プラグインをインストールするまでリアルタイムで適用されません。", "install": "プラグインをインストール", "install_steps": "インストール手順", diff --git a/i18n/locales/ko_KR.json b/i18n/locales/ko_KR.json index 40ad2b33e..1ee121920 100644 --- a/i18n/locales/ko_KR.json +++ b/i18n/locales/ko_KR.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "플레이어 관리 플러그인 활성", "not_detected": "이 서버에서 플레이어 관리 플러그인이 아직 응답하지 않아 음성 및 채팅 차단이 아직 실시간으로 적용되지 않습니다.", - "not_detected_node": "플레이어 관리 플러그인이 아직 응답하지 않았습니다. 이 노드는 자동으로 불러오므로 계속되면 서버를 다시 시작하세요.", + "not_detected_node": "플레이어 관리 플러그인이 아직 응답하지 않았습니다. 플러그인 출시 전에 만든 서버는 다시 배포해야 적용됩니다: 서버를 껐다가 다시 켜세요.", "not_detected_external": "음성 차단, 채팅 차단, 밴은 이 서버에 5Stack 플레이어 관리 플러그인을 설치해야 실시간으로 적용됩니다.", "install": "플러그인 설치", "install_steps": "설치 단계", diff --git a/i18n/locales/pl_PL.json b/i18n/locales/pl_PL.json index d01568585..b0f18b592 100644 --- a/i18n/locales/pl_PL.json +++ b/i18n/locales/pl_PL.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Wtyczka zarządzania graczami aktywna", "not_detected": "Wtyczka zarządzania graczami nie zgłosiła się jeszcze z tego serwera, więc wyciszenia i blokady czatu nie są jeszcze egzekwowane na żywo.", - "not_detected_node": "Wtyczka zarządzania graczami jeszcze się nie zgłosiła. Ten węzeł ładuje ją automatycznie, więc zrestartuj serwer, jeśli to się utrzymuje.", + "not_detected_node": "Wtyczka zarządzania graczami jeszcze się nie zgłosiła. Serwery utworzone przed jej wydaniem otrzymają ją po ponownym wdrożeniu: wyłącz serwer i włącz go ponownie.", "not_detected_external": "Wyciszenia, blokady czatu i bany są egzekwowane na żywo dopiero po zainstalowaniu wtyczki zarządzania graczami 5Stack na tym serwerze.", "install": "Zainstaluj wtyczkę", "install_steps": "Kroki instalacji", diff --git a/i18n/locales/pt_BR.json b/i18n/locales/pt_BR.json index 05eac6f46..a9ebc6ef0 100644 --- a/i18n/locales/pt_BR.json +++ b/i18n/locales/pt_BR.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Plugin de gerenciamento de jogadores ativo", "not_detected": "O plugin de gerenciamento de jogadores ainda não se comunicou a partir deste servidor, então mutes e silenciamentos ainda não são aplicados ao vivo.", - "not_detected_node": "O plugin de gerenciamento de jogadores ainda não se comunicou. Este nó o carrega automaticamente, então reinicie o servidor se isso continuar.", + "not_detected_node": "O plugin de gerenciamento de jogadores ainda não se comunicou. Servidores criados antes do lançamento o recebem ao serem reimplantados: desative o servidor e ative-o novamente.", "not_detected_external": "Mutes, silenciamentos e banimentos só são aplicados ao vivo depois que o plugin de gerenciamento de jogadores da 5Stack é instalado neste servidor.", "install": "Instalar plugin", "install_steps": "Passos de instalação", diff --git a/i18n/locales/ru_RU.json b/i18n/locales/ru_RU.json index 076bc356a..6ffec69b3 100644 --- a/i18n/locales/ru_RU.json +++ b/i18n/locales/ru_RU.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Плагин управления игроками активен", "not_detected": "Плагин управления игроками ещё не отозвался с этого сервера, поэтому заглушение и запрет чата пока не применяются в реальном времени.", - "not_detected_node": "Плагин управления игроками ещё не отозвался. Этот узел загружает его автоматически, поэтому перезапустите сервер, если это продолжится.", + "not_detected_node": "Плагин управления игроками ещё не отозвался. Серверы, созданные до его выхода, получат его после повторного развёртывания: выключите сервер и включите его снова.", "not_detected_external": "Заглушение, запрет чата и баны применяются в реальном времени только после установки плагина управления игроками 5Stack на этот сервер.", "install": "Установить плагин", "install_steps": "Шаги установки", diff --git a/i18n/locales/sv_SE.json b/i18n/locales/sv_SE.json index fe2bc207c..e5335cf83 100644 --- a/i18n/locales/sv_SE.json +++ b/i18n/locales/sv_SE.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Plugin för spelarhantering aktivt", "not_detected": "Pluginet för spelarhantering har inte hört av sig från den här servern ännu, så tystningar och chattförbud tillämpas inte live än.", - "not_detected_node": "Pluginet för spelarhantering har inte hört av sig ännu. Den här noden laddar det automatiskt, så starta om servern om det fortsätter.", + "not_detected_node": "Pluginet för spelarhantering har inte hört av sig ännu. Servrar som skapades innan det släpptes får det när de driftsätts om: stäng av servern och slå på den igen.", "not_detected_external": "Tystningar, chattförbud och avstängningar tillämpas live först när 5Stacks plugin för spelarhantering är installerat på den här servern.", "install": "Installera plugin", "install_steps": "Installationssteg", diff --git a/i18n/locales/tr_TR.json b/i18n/locales/tr_TR.json index f8c42e9b5..377858379 100644 --- a/i18n/locales/tr_TR.json +++ b/i18n/locales/tr_TR.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Oyuncu yönetimi eklentisi etkin", "not_detected": "Oyuncu yönetimi eklentisi bu sunucudan henüz bildirim göndermedi, bu yüzden susturmalar ve sohbet yasakları henüz canlı uygulanmıyor.", - "not_detected_node": "Oyuncu yönetimi eklentisi henüz bildirim göndermedi. Bu düğüm onu otomatik yükler, bu durum sürerse sunucuyu yeniden başlatın.", + "not_detected_node": "Oyuncu yönetimi eklentisi henüz bildirim göndermedi. Eklenti yayınlanmadan önce oluşturulan sunucular yeniden dağıtıldığında onu alır: sunucuyu kapatıp yeniden açın.", "not_detected_external": "Susturmalar, sohbet yasakları ve yasaklamalar ancak 5Stack oyuncu yönetimi eklentisi bu sunucuya kurulduğunda canlı uygulanır.", "install": "Eklentiyi Kur", "install_steps": "Kurulum Adımları", diff --git a/i18n/locales/uk_UA.json b/i18n/locales/uk_UA.json index b628f5a07..972d0dc6a 100644 --- a/i18n/locales/uk_UA.json +++ b/i18n/locales/uk_UA.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "Плагін керування гравцями активний", "not_detected": "Плагін керування гравцями ще не відгукнувся з цього сервера, тому заглушення та заборона чату поки не застосовуються в реальному часі.", - "not_detected_node": "Плагін керування гравцями ще не відгукнувся. Цей вузол завантажує його автоматично, тож перезапустіть сервер, якщо це триватиме.", + "not_detected_node": "Плагін керування гравцями ще не відгукнувся. Сервери, створені до його випуску, отримають його після повторного розгортання: вимкніть сервер і ввімкніть його знову.", "not_detected_external": "Заглушення, заборона чату та бани застосовуються в реальному часі лише після встановлення плагіна керування гравцями 5Stack на цей сервер.", "install": "Встановити плагін", "install_steps": "Кроки встановлення", diff --git a/i18n/locales/zh_Hans.json b/i18n/locales/zh_Hans.json index e2e6ca9e4..f11566378 100644 --- a/i18n/locales/zh_Hans.json +++ b/i18n/locales/zh_Hans.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "玩家管理插件运行中", "not_detected": "玩家管理插件尚未从此服务器报到,因此禁麦和禁言暂时无法实时生效。", - "not_detected_node": "玩家管理插件尚未报到。此节点会自动加载它,如果持续如此,请重启服务器。", + "not_detected_node": "玩家管理插件尚未报到。在它发布前创建的服务器需要重新部署才能获得它:请先停用服务器再重新启用。", "not_detected_external": "只有在此服务器上安装 5Stack 玩家管理插件后,禁麦、禁言和封禁才会实时生效。", "install": "安装插件", "install_steps": "安装步骤", diff --git a/i18n/locales/zh_Hant.json b/i18n/locales/zh_Hant.json index c220bfed4..cf2913b3a 100644 --- a/i18n/locales/zh_Hant.json +++ b/i18n/locales/zh_Hant.json @@ -3678,7 +3678,7 @@ "player_management_plugin": { "active": "玩家管理外掛運作中", "not_detected": "玩家管理外掛尚未從此伺服器回報,因此禁麥和禁言暫時無法即時生效。", - "not_detected_node": "玩家管理外掛尚未回報。此節點會自動載入它,若持續如此,請重新啟動伺服器。", + "not_detected_node": "玩家管理外掛尚未回報。在它發布前建立的伺服器需要重新部署才能取得它:請先停用伺服器再重新啟用。", "not_detected_external": "只有在此伺服器上安裝 5Stack 玩家管理外掛後,禁麥、禁言和封鎖才會即時生效。", "install": "安裝外掛", "install_steps": "安裝步驟", diff --git a/tests/components/PlayerSanctions.spec.ts b/tests/components/PlayerSanctions.spec.ts index c97f10925..4bb3c0f6a 100644 --- a/tests/components/PlayerSanctions.spec.ts +++ b/tests/components/PlayerSanctions.spec.ts @@ -6,6 +6,13 @@ import { useAuthStore } from "~/stores/AuthStore"; import { e_player_roles_enum } from "~/generated/zeus"; const PLAYER = "76561198000000001"; + +const { toast } = vi.hoisted(() => ({ toast: vi.fn() })); + +vi.mock("@/components/ui/toast", async (importOriginal) => ({ + ...(await importOriginal()), + toast, +})); const MODERATOR = "76561198000000099"; const AMBER_BAR = "bg-[hsl(var(--tac-amber))]"; @@ -29,6 +36,7 @@ const tomorrow = () => new Date(Date.now() + 86_400_000).toISOString(); const yesterday = () => new Date(Date.now() - 86_400_000).toISOString(); let rows: any[] = []; +let removal = { enforced: false, message: "ok" }; let mutate: ReturnType; let unmount: (() => void) | null = null; @@ -36,6 +44,8 @@ afterEach(() => { unmount?.(); unmount = null; useAuthStore().me = undefined; + removal = { enforced: false, message: "ok" }; + toast.mockReset(); vi.restoreAllMocks(); }); @@ -51,12 +61,12 @@ function serve() { }, })); mutate = vi.fn().mockResolvedValue({ - data: { unsanctionServerPlayer: { enforced: false, message: "ok" } }, + data: { unsanctionServerPlayer: removal }, }); vi.spyOn(client, "mutate").mockImplementation(mutate); } -async function mountSheet(sanctions: any[]) { +async function mountSheet(sanctions: any[], serverId?: string) { rows = sanctions; useAuthStore().me = { steam_id: MODERATOR, @@ -70,6 +80,7 @@ async function mountSheet(sanctions: any[]) { player: { steam_id: PLAYER, name: "Player" }, variant: "external", open: true, + serverId, }, attachTo: document.body, global: { @@ -261,3 +272,50 @@ describe("PlayerSanctions warnings", () => { }); }); }); + +describe("PlayerSanctions removal on a server", () => { + async function remove(label: string) { + card(label) + .querySelector('[aria-label="Remove"]')! + .click(); + await flushPromises(); + + Array.from( + document.body.querySelectorAll( + '[role="alertdialog"] button', + ), + ) + .find((button) => button.textContent?.trim() === "Confirm")! + .click(); + await flushPromises(); + } + + it("tells the moderator why a lifted sanction did not reach the server", async () => { + removal = { + enforced: false, + message: + "sanction removed; the Player Management plugin is not installed on this server", + }; + await mountSheet([sanction("mute-1", "mute", tomorrow())], "server-1"); + + await remove("Mute"); + + expect(toast).toHaveBeenCalledWith( + expect.objectContaining({ + description: + "sanction removed; the Player Management plugin is not installed on this server", + }), + ); + }); + + it("never repeats a warning's removal message, which no server enforces", async () => { + removal = { enforced: false, message: "warning removed" }; + await mountSheet([sanction("warning-1", "warning")], "server-1"); + + await remove("Warning"); + + expect(toast).toHaveBeenCalledWith( + expect.objectContaining({ description: undefined }), + ); + }); +}); diff --git a/tests/components/SanctionPlayer.spec.ts b/tests/components/SanctionPlayer.spec.ts index a0397510c..45ec05fe5 100644 --- a/tests/components/SanctionPlayer.spec.ts +++ b/tests/components/SanctionPlayer.spec.ts @@ -209,6 +209,19 @@ describe("SanctionPlayer on a server", () => { ); }); + it("adds nothing when the sanction was not aimed at a server", async () => { + const wrapper = await openDrawer("mute", undefined, { + enforced: false, + message: "sanction saved", + }); + + await submit(wrapper); + + expect(toast).toHaveBeenCalledWith( + expect.objectContaining({ description: undefined }), + ); + }); + it("never repeats a warning's save message, which no server enforces", async () => { const wrapper = await openDrawer("warning", "server-1"); diff --git a/tests/components/servers/ServerPlayerManagement.spec.ts b/tests/components/servers/ServerPlayerManagement.spec.ts index accf7597a..cbad5295e 100644 --- a/tests/components/servers/ServerPlayerManagement.spec.ts +++ b/tests/components/servers/ServerPlayerManagement.spec.ts @@ -83,7 +83,7 @@ describe("ServerPlayerManagement plugin status", () => { const wrapper = await mountCard({ gameServerNodeId: "node-1" }); - expect(wrapper.text()).toContain("This node loads it automatically"); + expect(wrapper.text()).toContain("switch the server off and back on"); expect(wrapper.text()).not.toContain("Install Plugin"); }); @@ -111,6 +111,16 @@ describe("ServerPlayerManagement plugin status", () => { expect(wrapper.text()).toContain( "addons/swiftlys2/configs/plugins/PlayerManagement/config.jsonc", ); + + // The config carries the api password, so it stays hidden like the + // page's own plugin config until asked for. + expect(wrapper.html()).not.toContain("secret-password"); + + const show = wrapper + .findAll("button") + .find((button) => button.text() === "Show Config"); + await show!.trigger("click"); + expect(wrapper.find("pre").text()).toContain('"PlayerManagement"'); expect(wrapper.find("pre").text()).toContain("secret-password"); });