@@ -14,6 +14,7 @@ import { Button } from "~/components/ui/button";
1414import AnimatedFilters from " ~/components/common/AnimatedFilters.vue" ;
1515import { Spinner } from " ~/components/ui/spinner" ;
1616import { Separator } from " ~/components/ui/separator" ;
17+ import { Fold , HeightSwap } from " ~/components/ui/transitions" ;
1718import ClipBoard from " ~/components/ClipBoard.vue" ;
1819import PlayerSearch from " ~/components/PlayerSearch.vue" ;
1920import RconCommander from " ~/components/servers/RconCommander.vue" ;
@@ -31,8 +32,6 @@ import {
3132 stopUtilityPracticeMutation ,
3233} from " ~/graphql/utilityGraphql" ;
3334import { readUtilityPracticeSession } from " ~/types/utility" ;
34- import { useAuthStore } from " ~/stores/AuthStore" ;
35- import { e_player_roles_enum } from " ~/generated/zeus" ;
3635import type {
3736 UtilityPracticeSession ,
3837 UtilityPracticeSessionOutput ,
@@ -161,15 +160,8 @@ const canDrive = computed(() => practice.value.isLive && canManage.value);
161160const matchId = computed (() => practice .value .matchId );
162161const serverId = computed (() => practice .value .serverId );
163162
164- // RCON is refused by the API for anyone below these roles, so showing it lower
165- // would only ever render a console that answers with an error. It takes an
166- // administrator, or the moderator running the match, which the practice host is.
167- const isAdmin = computed (() =>
168- useAuthStore ().isRoleAbove (e_player_roles_enum .administrator ),
169- );
170-
171- const canOperate = computed (
172- () => isAdmin .value || useAuthStore ().isRoleAbove (e_player_roles_enum .moderator ),
163+ const staffCanStop = computed (
164+ () => practice .value .canStop && ! canManage .value ,
173165);
174166
175167const operatorOpen = ref (false );
@@ -214,6 +206,10 @@ onBeforeUnmount(() => {
214206const invitees = ref <Invitee []>([]);
215207const inviting = ref (false );
216208const ending = ref (false );
209+ const confirmingStop = ref (false );
210+ const stopping = ref (false );
211+
212+ const hostName = computed (() => props .session ?.host ?.name ?? null );
217213
218214const inviteeSteamIds = computed (() =>
219215 invitees .value .map ((entry ) => entry .steamId ),
@@ -306,6 +302,35 @@ async function end() {
306302 }
307303}
308304
305+ async function staffStop() {
306+ const id = sessionId .value ;
307+
308+ if (! id || ! staffCanStop .value ) {
309+ return ;
310+ }
311+
312+ stopping .value = true ;
313+
314+ try {
315+ await getGraphqlClient ().mutate ({
316+ mutation: stopUtilityPracticeMutation ,
317+ variables: { session_id: id },
318+ });
319+
320+ confirmingStop .value = false ;
321+ invitees .value = [];
322+ emit (" ended" );
323+ } catch (error : any ) {
324+ toast ({
325+ title: t (" pages.utility.practice.stop_failed" ),
326+ description: error ?.message ,
327+ variant: " destructive" ,
328+ });
329+ } finally {
330+ stopping .value = false ;
331+ }
332+ }
333+
309334const sectionLabel =
310335 " flex items-center gap-1.5 font-mono text-[0.62rem] uppercase tracking-[0.16em] text-muted-foreground" ;
311336
@@ -463,12 +488,12 @@ const panelCta = "w-full font-bold uppercase tracking-[0.22em]";
463488 </template >
464489
465490 <!-- Operator tools. A practice pod is a game server like any other, and
466- when one refuses connections its console is what answers why. It
467- is staff-gated here because RCON is staff-gated on the server . -->
468- <template v-if = " canOperate " >
491+ when one refuses connections its console is what answers why. The
492+ api only hands the server id to viewers RCON lets in . -->
493+ <Fold : open = " !! serverId " >
469494 <Separator />
470495
471- <Collapsible v-model :open =" operatorOpen " >
496+ <Collapsible v-model :open =" operatorOpen " class="mt-3" >
472497 <CollapsibleTrigger
473498 class="flex w-full items-center justify-between gap-2 text-left"
474499 >
@@ -494,26 +519,75 @@ const panelCta = "w-full font-bold uppercase tracking-[0.22em]";
494519 />
495520 </CollapsibleContent >
496521 </Collapsible >
497- </template >
522+ </Fold >
498523 </template >
499524
500525 <!-- Whatever the surface wants between the session and the way out of it. -->
501526 <slot name =" extra" />
502527
503528 <Separator />
504529
505- <Button
506- :variant =" canManage ? ' destructive' : ' outline' "
507- size="lg"
508- :class =" panelCta "
509- :loading =" ending "
510- @click =" end ()"
511- >
512- {{
513- canManage
514- ? $t("pages.utility.practice.stop")
515- : $t("pages.utility.practice.leave")
516- }}
517- </Button >
530+ <!-- Staff who joined are still offered Leave: Stop sits beside it, and
531+ asks first, because it ends the server for everyone on it. -->
532+ <HeightSwap >
533+ <div
534+ v-if =" confirmingStop && staffCanStop"
535+ key =" confirm"
536+ class =" flex flex-col gap-2 rounded-md border border-destructive/50 bg-destructive/[0.06] p-2.5"
537+ >
538+ <p class =" text-xs text-foreground/90" >
539+ {{
540+ hostName
541+ ? $t("pages.utility.practice.staff_stop_ask", { host: hostName })
542+ : $t("pages.utility.practice.staff_stop_ask_unnamed")
543+ }}
544+ </p >
545+ <div class =" flex items-center justify-end gap-2" >
546+ <Button
547+ variant="outline"
548+ size="sm"
549+ class="h-7"
550+ :disabled =" stopping "
551+ @click =" confirmingStop = false "
552+ >
553+ {{ $t("common.cancel") }}
554+ </Button >
555+ <Button
556+ variant="destructive"
557+ size="sm"
558+ class="h-7"
559+ :loading =" stopping "
560+ @click =" staffStop ()"
561+ >
562+ {{ $t("pages.utility.practice.stop") }}
563+ </Button >
564+ </div >
565+ </div >
566+ <div v-else key =" way-out" class =" flex items-center gap-1.5" >
567+ <Button
568+ :variant =" canManage ? ' destructive' : ' outline' "
569+ size="lg"
570+ :class =" [panelCta , ' min-w-0 flex-1' ] "
571+ :loading =" ending "
572+ @click =" end ()"
573+ >
574+ {{
575+ canManage
576+ ? $t("pages.utility.practice.stop")
577+ : $t("pages.utility.practice.leave")
578+ }}
579+ </Button >
580+ <Button
581+ v-if =" staffCanStop "
582+ variant="outline"
583+ size="lg"
584+ class="shrink-0 font-bold uppercase tracking-[0.22em ] text-destructive hover:text-destructive "
585+ :disabled =" ending "
586+ @click =" confirmingStop = true "
587+ >
588+ {{ $t("pages.utility.practice.staff_stop") }}
589+ </Button >
590+ </div >
591+ </HeightSwap >
518592 </div >
519593</template >
0 commit comments