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,
ViewerRangeMax = category.ViewerRangeMax,
}));
var visibleCategoryRows = 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
var categories = categoryRows
.Select(category => new AdminCategoryItemDto(
category.Id,
category.GroupName,
@@ -128,7 +128,7 @@ public partial class SeedLiveDemoAwardData : Migration
award_groups.group_name,
tiers.tier_name,
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),
3,
tiers.min_viewers,
@@ -45,7 +45,7 @@ public partial class SeedCurrentSeasonDemoVotingData : Migration
WITH award_groups(ord, group_name, group_slug, description, name_prefix) AS (
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'),
(3, 'Creative', 'creative', 'Model, Design, Rigging, Art und visuelle Praesentation im Stream.', 'Velvet'),
(4, 'Performance', 'performance', 'Gesang, Musik, Buehnenpraesenz und besondere Live-Momente.', 'Melo'),
@@ -76,7 +76,7 @@ public partial class SeedCurrentSeasonDemoVotingData : Migration
award_groups.group_name,
tiers.tier_name,
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),
3,
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(() =>
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 categoryLabelMap = computed<Record<number, string>>(() =>
Object.fromEntries(seasonDetail.value.categories.map((category) => [category.id, `${category.groupName} · ${category.name}`])),
@@ -307,7 +316,7 @@ export function useAdminCandidateManager() {
adminMessage.value = ''
adminError.value = ''
editingId.value = 'new'
form.categoryId = categoryFilter.value ?? seasonDetail.value.categories[0]?.id ?? 0
form.categoryId = preferredCreateCategoryId.value
form.displayName = ''
form.channelSlug = ''
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_MESSAGE =
'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
export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
@@ -13,6 +14,7 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
const { dismissAdminToast, showAdminToast } = useAdminToast()
let reconnectTimer: number | undefined
let disconnectToastTimer: number | undefined
let reconnectProbeRunning = false
function clearReconnectTimer() {
@@ -21,6 +23,12 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
reconnectTimer = undefined
}
function clearDisconnectToastTimer() {
if (disconnectToastTimer === undefined || typeof window === 'undefined') return
window.clearTimeout(disconnectToastTimer)
disconnectToastTimer = undefined
}
function scheduleReconnectProbe() {
if (reconnectTimer !== undefined || typeof window === 'undefined') return
@@ -42,18 +50,30 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
}, RECONNECT_PROBE_INTERVAL_MS)
}
watch(
() => isAdminRoute.value && awardsStore.apiMode === 'fallback',
(shouldShowDisconnectToast) => {
if (shouldShowDisconnectToast) {
function scheduleDisconnectToast() {
if (disconnectToastTimer !== undefined || typeof window === 'undefined') return
disconnectToastTimer = window.setTimeout(() => {
disconnectToastTimer = undefined
if (!isAdminRoute.value || awardsStore.apiMode !== 'fallback') return
showAdminToast(ADMIN_API_DISCONNECT_MESSAGE, 'error', {
key: ADMIN_API_DISCONNECT_TOAST_KEY,
persistent: true,
})
}, ADMIN_API_DISCONNECT_TOAST_DELAY_MS)
}
watch(
() => isAdminRoute.value && awardsStore.apiMode === 'fallback',
(shouldShowDisconnectToast) => {
if (shouldShowDisconnectToast) {
scheduleDisconnectToast()
scheduleReconnectProbe()
return
}
clearDisconnectToastTimer()
clearReconnectTimer()
dismissAdminToast(ADMIN_API_DISCONNECT_TOAST_KEY)
},
@@ -61,6 +81,7 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef<boolean>) {
)
onBeforeUnmount(() => {
clearDisconnectToastTimer()
clearReconnectTimer()
})
}
+16 -3
View File
@@ -28,6 +28,9 @@ const loginFormElement = ref<HTMLFormElement | null>(null)
const passwordChangeFormElement = ref<HTMLFormElement | null>(null)
const passwordChangeRequired = computed(() => Boolean(authStore.session?.mustChangePassword))
const successMessageClass = computed(() =>
passwordChangeRequired.value ? 'login-form__warning' : 'login-form__success',
)
const redirectTarget = computed(() => {
const redirect = Array.isArray(route.query.redirect)
@@ -211,7 +214,7 @@ async function submitPasswordChange() {
{{ errorMessage }}
</p>
<p v-if="successMessage" class="login-form__success" role="status">
<p v-if="successMessage" :class="successMessageClass" role="status">
{{ successMessage }}
</p>
@@ -231,7 +234,7 @@ async function submitPasswordChange() {
{{ errorMessage }}
</p>
<p v-if="successMessage" class="login-form__success" role="status">
<p v-if="successMessage" :class="successMessageClass" role="status">
{{ successMessage }}
</p>
@@ -284,7 +287,7 @@ async function submitPasswordChange() {
{{ errorMessage }}
</p>
<p v-if="successMessage" class="login-form__success" role="status">
<p v-if="successMessage" :class="successMessageClass" role="status">
{{ successMessage }}
</p>
@@ -535,6 +538,16 @@ async function submitPasswordChange() {
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 {
min-height: 58px;
border: 0;
+3 -3
View File
@@ -260,13 +260,13 @@ watch(
</Teleport>
<div class="admin-panel pb-10 pt-4 xl:pt-0">
<div class="grid gap-6 xl:grid-cols-[292px_minmax(0,1fr)]">
<main class="order-1 min-w-0 xl:order-2">
<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 xl:min-h-0 xl:overflow-y-auto xl:pr-2">
<RouterView />
</main>
<!-- 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">
<nav class="space-y-4">
<section v-for="group in navGroups" :key="group.label" class="space-y-1.5">