Skip to content
Merged
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
120 changes: 104 additions & 16 deletions components/utility/StartPracticeDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,21 @@ async function loadPracticeServers() {
practiceServersError.value = error?.message ?? "unknown error";
console.error("[utility] practice server load error:", error);
practiceServers.value = [];
} finally {
forgetPickedServer();
}
}

// `region` outlives the dialog, so a server picked last time is still picked
// when it reopens -- and by then it can be gone from the list entirely. A
// selection the picker cannot show is one the trigger cannot name, and Start
// would send it anyway.
function forgetPickedServer() {
if (
selectedServerId.value &&
!practiceServers.value.some((entry) => entry.id === selectedServerId.value)
) {
region.value = ANY_REGION;
}
}

Expand Down Expand Up @@ -257,17 +272,80 @@ const freeServerCount = computed(
() => practiceServers.value.filter((entry) => !entry.in_use).length,
);

// LAN regions only exist for the people who can reach them, and the probe is
// the only thing that knows which people those are -- the same test the
// matchmaking region list runs.
// Offered only where a pod could actually be booted. A region with no node is
// not a slower answer, it is no answer -- the API refuses it outright, and
// listing it under ON DEMAND turns "start a server" into an error message.
//
// LAN regions narrow it further: they only exist for the people who can reach
// them, and the probe is the only thing that knows which people those are --
// the same test the matchmaking region list runs.
const onDemandRegions = computed(() =>
regions.value.filter(
(entry: any) =>
!entry.is_lan ||
useMatchmakingStore().getRegionlatencyResult(entry.value)?.isLan,
entry.has_node &&
(!entry.is_lan ||
useMatchmakingStore().getRegionlatencyResult(entry.value)?.isLan),
),
);

// Spelled once and read by both the trigger and the row: the two drifting
// apart is how the closed select came to name a region the list had renamed.
function regionLabel(value: string) {
const entry = onDemandRegions.value.find(
(candidate: any) => candidate.value === value,
);

return entry?.description || value;
}

function serverStateLabel(entry: { in_use: boolean; held_by: string | null }) {
if (!entry.in_use) {
return null;
}

return entry.held_by
? t("pages.utility.practice.server_held_by", { name: entry.held_by })
: t("pages.utility.practice.server_in_use");
}

// What the closed select says. Reka builds that from the selected item's
// flattened textContent, so every decoration in the row came with it and ran
// straight into the name -- "US-EAST1ms", and "US-East Boxin use by Someone"
// for a taken server. Structured instead, so the trigger keeps the one
// decoration that changes what Start does: a server somebody else has claimed
// since it was picked is still selected, and pressing Start on it fails.
const selection = computed<{
name: string;
region: string | null;
state: string | null;
}>(() => {
if (region.value === ANY_REGION) {
return {
name: t("pages.utility.practice.any_region"),
region: null,
state: null,
};
}

if (selectedServerId.value) {
// forgetPickedServer is what keeps this from being the trigger saying
// "select a region" over a server id Start would still send: every open
// reloads the list and drops a pick that is no longer on it. This is the
// window before that lands.
const server = practiceServers.value.find(
(entry) => entry.id === selectedServerId.value,
);

return {
name: server?.label ?? t("pages.utility.practice.select_region"),
region: server?.region ?? null,
state: server ? serverStateLabel(server) : null,
};
}

return { name: regionLabel(region.value), region: null, state: null };
});

// Only the automatic choice needs a line under the closed select: it is the one
// row whose name does not say what it does. A named server and a named region
// explain themselves, and captioning them just put a sentence under every
Expand Down Expand Up @@ -507,7 +585,23 @@ const footerCta = "w-full font-bold uppercase tracking-[0.22em]";
<SelectTrigger>
<SelectValue
:placeholder="$t('pages.utility.practice.select_region')"
/>
>
<span class="flex min-w-0 items-center gap-1.5">
<span class="truncate">{{ selection.name }}</span>
<span
v-if="selection.region"
class="shrink-0 text-muted-foreground"
>
({{ selection.region }})
</span>
<span
v-if="selection.state"
class="truncate text-xs text-muted-foreground"
>
{{ selection.state }}
</span>
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<!-- The default, and the only row that does not name a
Expand Down Expand Up @@ -567,16 +661,10 @@ const footerCta = "w-full font-bold uppercase tracking-[0.22em]";
</span>
</span>
<span
v-if="entry.in_use"
v-if="serverStateLabel(entry)"
class="ml-auto shrink-0 truncate pl-2 text-xs text-muted-foreground"
>
{{
entry.held_by
? $t("pages.utility.practice.server_held_by", {
name: entry.held_by,
})
: $t("pages.utility.practice.server_in_use")
}}
{{ serverStateLabel(entry) }}
</span>
</SelectItem>
</SelectGroup>
Expand All @@ -593,7 +681,7 @@ const footerCta = "w-full font-bold uppercase tracking-[0.22em]";
>
<span :class="optionGutter" aria-hidden="true" />
<span class="truncate">
{{ entry.description || entry.value }}
{{ regionLabel(entry.value) }}
</span>
<!-- Which of them is actually near you: the one thing a
region name never says. -->
Expand All @@ -616,7 +704,7 @@ const footerCta = "w-full font-bold uppercase tracking-[0.22em]";
}}
</p>
<p
v-else-if="!regions.length && !practiceServers.length"
v-else-if="!onDemandRegions.length && !practiceServers.length"
class="text-xs text-muted-foreground"
>
{{ $t("pages.utility.practice.no_regions") }}
Expand Down
17 changes: 13 additions & 4 deletions components/utility/UtilityRepairDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,14 @@ const { t } = useI18n();
// Reka Select rejects an empty-string value, so "unset" rides a sentinel.
const ANY_REGION = "any";

const regions = computed(() => useApplicationSettingsStore().availableRegions);
// Under an ON DEMAND heading, so only the regions that can actually boot one.
// A region with no node is not a slower answer, it is no answer -- the API
// refuses it outright.
const regions = computed(() =>
useApplicationSettingsStore().availableRegions.filter(
(entry: { has_node: boolean }) => entry.has_node,
),
);
const region = ref<string>(ANY_REGION);
const sessionId = ref<string | null>(null);
const session = ref<UtilityPracticeSession | null>(null);
Expand Down Expand Up @@ -128,9 +135,11 @@ watch(
},
);

const isBooting = computed(
() => !!sessionId.value && !practice.value.connectionString,
);
// Not "has a connect string": that is derived from the match's server row, so
// it resolves the moment a server is assigned -- for an on-demand session, the
// instant the pod is scheduled and minutes before cs2 is listening. Offering
// the connect then hands somebody an address that refuses them.
const isBooting = computed(() => !!sessionId.value && !practice.value.isLive);

const canRepair = computed(
() =>
Expand Down
3 changes: 3 additions & 0 deletions generated/zeus/const.full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41089,6 +41089,7 @@ export const AllTypesProps: Record<string,any> = {
map_name:"String_comparison_exp",
match:"matches_bool_exp",
match_id:"uuid_comparison_exp",
notify_when_ready:"Boolean_comparison_exp",
playbook:"utility_playbooks_bool_exp",
playbook_id:"uuid_comparison_exp",
region:"String_comparison_exp",
Expand Down Expand Up @@ -41199,6 +41200,7 @@ export const AllTypesProps: Record<string,any> = {
map_name:"order_by",
match:"matches_order_by",
match_id:"order_by",
notify_when_ready:"order_by",
playbook:"utility_playbooks_order_by",
playbook_id:"order_by",
region:"order_by",
Expand Down Expand Up @@ -66317,6 +66319,7 @@ export const ReturnTypes: Record<string,any> = {
map_name:"String",
match:"matches",
match_id:"uuid",
notify_when_ready:"Boolean",
playbook:"utility_playbooks",
playbook_id:"uuid",
region:"String",
Expand Down
2 changes: 1 addition & 1 deletion generated/zeus/const.ts

Large diffs are not rendered by default.

Loading