Fix admin demo review and landing regressions
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 59s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m34s

This commit is contained in:
AzuTear
2026-06-30 08:18:14 +02:00
parent 0803a0124b
commit 76ac60d3fd
9 changed files with 1726 additions and 29 deletions
@@ -76,21 +76,7 @@ public static partial class AdminSeasonManagementEndpoints
ViewerRangeMin = category.ViewerRangeMin, ViewerRangeMin = category.ViewerRangeMin,
ViewerRangeMax = category.ViewerRangeMax, ViewerRangeMax = category.ViewerRangeMax,
})); }));
var visibleCategoryRows = categoryRows var categories = categoryRows
.Where(category => SeasonSubcategoryTemplateSettings.MatchesTemplate(
new Backend.Domain.Category
{
GroupName = category.GroupName,
Name = category.Name,
Slug = category.Slug,
SortOrder = category.SortOrder,
ViewerRangeMin = category.ViewerRangeMin,
ViewerRangeMax = category.ViewerRangeMax,
},
subcategoryTemplateSettings))
.ToArray();
var categories = visibleCategoryRows
.Select(category => new AdminCategoryItemDto( .Select(category => new AdminCategoryItemDto(
category.Id, category.Id,
category.GroupName, category.GroupName,
@@ -128,7 +128,7 @@ public partial class SeedLiveDemoAwardData : Migration
award_groups.group_name, award_groups.group_name,
tiers.tier_name, tiers.tier_name,
award_groups.group_slug || '-' || tiers.tier_slug, award_groups.group_slug || '-' || tiers.tier_slug,
award_groups.description || ' Demo-Tier: ' || tiers.tier_name || '.', award_groups.description,
(award_groups.ord * 100) + (tiers.ord * 10), (award_groups.ord * 100) + (tiers.ord * 10),
3, 3,
tiers.min_viewers, tiers.min_viewers,
@@ -45,7 +45,7 @@ public partial class SeedCurrentSeasonDemoVotingData : Migration
WITH award_groups(ord, group_name, group_slug, description, name_prefix) AS ( WITH award_groups(ord, group_name, group_slug, description, name_prefix) AS (
VALUES VALUES
(1, 'Main Awards', 'main-awards', 'Die groessten Allround-Auszeichnungen der aktuellen Demo-Season.', 'Astra'), (1, 'Main Awards', 'main-awards', 'Die groessten Allround-Auszeichnungen der aktuellen Season.', 'Astra'),
(2, 'Discovery', 'discovery', 'Neue Stimmen, Debuets und Creator, die in dieser Season besonders sichtbar geworden sind.', 'Nova'), (2, 'Discovery', 'discovery', 'Neue Stimmen, Debuets und Creator, die in dieser Season besonders sichtbar geworden sind.', 'Nova'),
(3, 'Creative', 'creative', 'Model, Design, Rigging, Art und visuelle Praesentation im Stream.', 'Velvet'), (3, 'Creative', 'creative', 'Model, Design, Rigging, Art und visuelle Praesentation im Stream.', 'Velvet'),
(4, 'Performance', 'performance', 'Gesang, Musik, Buehnenpraesenz und besondere Live-Momente.', 'Melo'), (4, 'Performance', 'performance', 'Gesang, Musik, Buehnenpraesenz und besondere Live-Momente.', 'Melo'),
@@ -76,7 +76,7 @@ public partial class SeedCurrentSeasonDemoVotingData : Migration
award_groups.group_name, award_groups.group_name,
tiers.tier_name, tiers.tier_name,
award_groups.group_slug || '-' || tiers.tier_slug, award_groups.group_slug || '-' || tiers.tier_slug,
award_groups.description || ' Demo-Tier: ' || tiers.tier_name || '.', award_groups.description,
(award_groups.ord * 100) + (tiers.ord * 10), (award_groups.ord * 100) + (tiers.ord * 10),
3, 3,
tiers.min_viewers, tiers.min_viewers,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class SanitizeDemoCategoryDescriptions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
UPDATE "Categories"
SET "Description" = trim(
regexp_replace(
replace("Description", 'aktuellen Demo-Season', 'aktuellen Season'),
'\s*Demo-Tier:\s*[^.]+\.',
'',
'g'
)
)
WHERE "Description" LIKE '%Demo-Tier:%'
OR "Description" LIKE '%aktuellen Demo-Season%';
"""
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
UPDATE "Categories"
SET "Description" = "Description" || ' Demo-Tier: ' || "Name" || '.'
WHERE "ViewerRangeMin" IS NOT NULL
AND "Description" NOT LIKE '%Demo-Tier:%';
UPDATE "Categories"
SET "Description" = replace("Description", 'aktuellen Season', 'aktuellen Demo-Season')
WHERE "Slug" LIKE 'main-awards-%'
AND "Description" LIKE '%aktuellen Season%';
"""
);
}
}
}
@@ -86,6 +86,15 @@ export function useAdminCandidateManager() {
const categoryOptions = computed(() => const categoryOptions = computed(() =>
seasonDetail.value.categories.map((category) => ({ label: `${category.groupName} · ${category.name}`, value: category.id })), seasonDetail.value.categories.map((category) => ({ label: `${category.groupName} · ${category.name}`, value: category.id })),
) )
const preferredCreateCategoryId = computed(() => {
if (categoryFilter.value) {
return categoryFilter.value
}
return seasonDetail.value.categories.find((category) =>
category.viewerRangeMin !== null || category.viewerRangeMax !== null,
)?.id ?? seasonDetail.value.categories[0]?.id ?? 0
})
const categoryFilterOptions = computed(() => [{ label: 'Alle Kategorien', value: null }, ...categoryOptions.value]) const categoryFilterOptions = computed(() => [{ label: 'Alle Kategorien', value: null }, ...categoryOptions.value])
const categoryLabelMap = computed<Record<number, string>>(() => const categoryLabelMap = computed<Record<number, string>>(() =>
Object.fromEntries(seasonDetail.value.categories.map((category) => [category.id, `${category.groupName} · ${category.name}`])), Object.fromEntries(seasonDetail.value.categories.map((category) => [category.id, `${category.groupName} · ${category.name}`])),
@@ -307,7 +316,7 @@ export function useAdminCandidateManager() {
adminMessage.value = '' adminMessage.value = ''
adminError.value = '' adminError.value = ''
editingId.value = 'new' editingId.value = 'new'
form.categoryId = categoryFilter.value ?? seasonDetail.value.categories[0]?.id ?? 0 form.categoryId = preferredCreateCategoryId.value
form.displayName = '' form.displayName = ''
form.channelSlug = '' form.channelSlug = ''
form.platform = 'Twitch' form.platform = 'Twitch'
@@ -6,6 +6,7 @@ import { useAwardsStore } from '../stores/awards'
const ADMIN_API_DISCONNECT_TOAST_KEY = 'admin-api-disconnected' const ADMIN_API_DISCONNECT_TOAST_KEY = 'admin-api-disconnected'
const ADMIN_API_DISCONNECT_MESSAGE = const ADMIN_API_DISCONNECT_MESSAGE =
'Backend/API nicht erreichbar. Der Admin-Bereich bleibt eingeschränkt, bis die Verbindung wieder da ist.' 'Backend/API nicht erreichbar. Der Admin-Bereich bleibt eingeschränkt, bis die Verbindung wieder da ist.'
const ADMIN_API_DISCONNECT_TOAST_DELAY_MS = 1200
const RECONNECT_PROBE_INTERVAL_MS = 5000 const RECONNECT_PROBE_INTERVAL_MS = 5000
export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) { export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
@@ -13,6 +14,7 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
const { dismissAdminToast, showAdminToast } = useAdminToast() const { dismissAdminToast, showAdminToast } = useAdminToast()
let reconnectTimer: number | undefined let reconnectTimer: number | undefined
let disconnectToastTimer: number | undefined
let reconnectProbeRunning = false let reconnectProbeRunning = false
function clearReconnectTimer() { function clearReconnectTimer() {
@@ -21,6 +23,12 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
reconnectTimer = undefined reconnectTimer = undefined
} }
function clearDisconnectToastTimer() {
if (disconnectToastTimer === undefined || typeof window === 'undefined') return
window.clearTimeout(disconnectToastTimer)
disconnectToastTimer = undefined
}
function scheduleReconnectProbe() { function scheduleReconnectProbe() {
if (reconnectTimer !== undefined || typeof window === 'undefined') return if (reconnectTimer !== undefined || typeof window === 'undefined') return
@@ -42,18 +50,30 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
}, RECONNECT_PROBE_INTERVAL_MS) }, RECONNECT_PROBE_INTERVAL_MS)
} }
watch( function scheduleDisconnectToast() {
() => isAdminRoute.value && awardsStore.apiMode === 'fallback', if (disconnectToastTimer !== undefined || typeof window === 'undefined') return
(shouldShowDisconnectToast) => {
if (shouldShowDisconnectToast) { disconnectToastTimer = window.setTimeout(() => {
disconnectToastTimer = undefined
if (!isAdminRoute.value || awardsStore.apiMode !== 'fallback') return
showAdminToast(ADMIN_API_DISCONNECT_MESSAGE, 'error', { showAdminToast(ADMIN_API_DISCONNECT_MESSAGE, 'error', {
key: ADMIN_API_DISCONNECT_TOAST_KEY, key: ADMIN_API_DISCONNECT_TOAST_KEY,
persistent: true, persistent: true,
}) })
}, ADMIN_API_DISCONNECT_TOAST_DELAY_MS)
}
watch(
() => isAdminRoute.value && awardsStore.apiMode === 'fallback',
(shouldShowDisconnectToast) => {
if (shouldShowDisconnectToast) {
scheduleDisconnectToast()
scheduleReconnectProbe() scheduleReconnectProbe()
return return
} }
clearDisconnectToastTimer()
clearReconnectTimer() clearReconnectTimer()
dismissAdminToast(ADMIN_API_DISCONNECT_TOAST_KEY) dismissAdminToast(ADMIN_API_DISCONNECT_TOAST_KEY)
}, },
@@ -61,6 +81,7 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
) )
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearDisconnectToastTimer()
clearReconnectTimer() clearReconnectTimer()
}) })
} }
+16 -3
View File
@@ -28,6 +28,9 @@ const loginFormElement = ref<HTMLFormElement | null>(null)
const passwordChangeFormElement = ref<HTMLFormElement | null>(null) const passwordChangeFormElement = ref<HTMLFormElement | null>(null)
const passwordChangeRequired = computed(() => Boolean(authStore.session?.mustChangePassword)) const passwordChangeRequired = computed(() => Boolean(authStore.session?.mustChangePassword))
const successMessageClass = computed(() =>
passwordChangeRequired.value ? 'login-form__warning' : 'login-form__success',
)
const redirectTarget = computed(() => { const redirectTarget = computed(() => {
const redirect = Array.isArray(route.query.redirect) const redirect = Array.isArray(route.query.redirect)
@@ -211,7 +214,7 @@ async function submitPasswordChange() {
{{ errorMessage }} {{ errorMessage }}
</p> </p>
<p v-if="successMessage" class="login-form__success" role="status"> <p v-if="successMessage" :class="successMessageClass" role="status">
{{ successMessage }} {{ successMessage }}
</p> </p>
@@ -231,7 +234,7 @@ async function submitPasswordChange() {
{{ errorMessage }} {{ errorMessage }}
</p> </p>
<p v-if="successMessage" class="login-form__success" role="status"> <p v-if="successMessage" :class="successMessageClass" role="status">
{{ successMessage }} {{ successMessage }}
</p> </p>
@@ -284,7 +287,7 @@ async function submitPasswordChange() {
{{ errorMessage }} {{ errorMessage }}
</p> </p>
<p v-if="successMessage" class="login-form__success" role="status"> <p v-if="successMessage" :class="successMessageClass" role="status">
{{ successMessage }} {{ successMessage }}
</p> </p>
@@ -535,6 +538,16 @@ async function submitPasswordChange() {
font-weight: 800; font-weight: 800;
} }
.login-form__warning {
padding: 13px 15px;
border: 1px solid rgba(217, 119, 6, 0.24);
border-radius: 18px;
background: rgba(255, 251, 235, 0.96);
color: #b45309 !important;
font-size: 14px !important;
font-weight: 800;
}
.login-form__submit { .login-form__submit {
min-height: 58px; min-height: 58px;
border: 0; border: 0;
+3 -3
View File
@@ -260,13 +260,13 @@ watch(
</Teleport> </Teleport>
<div class="admin-panel pb-10 pt-4 xl:pt-0"> <div class="admin-panel pb-10 pt-4 xl:pt-0">
<div class="grid gap-6 xl:grid-cols-[292px_minmax(0,1fr)]"> <div class="grid gap-6 xl:min-h-[calc(100vh-1rem)] xl:grid-cols-[292px_minmax(0,1fr)]">
<main class="order-1 min-w-0 xl:order-2"> <main class="order-1 min-w-0 xl:order-2 xl:min-h-0 xl:overflow-y-auto xl:pr-2">
<RouterView /> <RouterView />
</main> </main>
<!-- Desktop sidebar (hidden on mobile) --> <!-- Desktop sidebar (hidden on mobile) -->
<aside class="order-2 hidden space-y-3 xl:order-1 xl:block xl:sticky xl:top-4 xl:h-fit"> <aside class="order-2 hidden space-y-3 xl:order-1 xl:block xl:max-h-[calc(100vh-1rem)] xl:overflow-y-auto xl:pr-1">
<Card class="p-3"> <Card class="p-3">
<nav class="space-y-4"> <nav class="space-y-4">
<section v-for="group in navGroups" :key="group.label" class="space-y-1.5"> <section v-for="group in navGroups" :key="group.label" class="space-y-1.5">