Add viewer-range categories, nomination tracking, dynamic showact form, session timeout, share URLs, and workflow-per-season

Features:
- Category viewer ranges + subcategory templates (admin group modal, tree workspace)
- Nomination enrichment via TwitchTracker API (NominationEnrichmentService,
  TwitchTrackerViewerStatsProvider) with admin tracking rules editor
- Nomination group tracker: CategoryGroupName as primary identifier,
  CategoryId stays as nullable legacy field; StreamerIdentity table
- Dynamic showact application form builder (AdminShowactFormBuilder,
  ShowactApplicationSchedule)
- Session idle timeout setting (AdminSessionTimeoutCard)
- Share URLs for X and Discord (SiteSettings, public extras)
- Workflow rules now stored per season (falls back to global SiteSettings)
- New admin routes: settings/access, settings/workflows, tracking-rules
- New admin review workspace with subcategory tabs
- AdminCategoriesView rebuilt with group/subcategory modals

Migrations (all additive):
- AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges,
  AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates,
  AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule,
  AddSeasonWorkflowRulesJson

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-06-28 23:32:21 +02:00
parent 4b2c5fa15d
commit b53c7fb736
178 changed files with 28933 additions and 1908 deletions
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import { BarChart3, CheckCircle2, ExternalLink, TrendingUp } from '@lucide/vue'
import { RouterLink } from 'vue-router'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
@@ -23,6 +23,8 @@ const {
totalVotes,
} = useAdminAnalyticsManager()
const route = useRoute()
const router = useRouter()
const healthFilter = ref<string>('Alle')
const groupFilter = ref<string>('Alle')
@@ -42,6 +44,43 @@ const healthStatusOptions = [
{ key: 'Bereit', label: 'Bereit', dot: 'bg-sky-400' },
{ key: 'Gewinner gesetzt', label: 'Gewinner', dot: 'bg-emerald-400' },
]
function applyRouteFilters() {
const rawHealth = Array.isArray(route.query.health) ? route.query.health[0] : route.query.health
const rawGroup = Array.isArray(route.query.group) ? route.query.group[0] : route.query.group
healthFilter.value = healthStatusOptions.some((option) => option.key === rawHealth) ? String(rawHealth) : 'Alle'
groupFilter.value = groupOptions.value.includes(String(rawGroup)) ? String(rawGroup) : 'Alle'
}
watch(
() => [route.query.health, route.query.group, groupOptions.value.join('|')] as const,
() => {
applyRouteFilters()
},
{ immediate: true },
)
watch([healthFilter, groupFilter], ([nextHealth, nextGroup]) => {
const currentHealth = Array.isArray(route.query.health) ? route.query.health[0] : route.query.health
const currentGroup = Array.isArray(route.query.group) ? route.query.group[0] : route.query.group
const normalizedHealth = nextHealth === 'Alle' ? undefined : nextHealth
const normalizedGroup = nextGroup === 'Alle' ? undefined : nextGroup
if (currentHealth === normalizedHealth && currentGroup === normalizedGroup) {
return
}
const nextQuery = { ...route.query } as Record<string, string>
if (normalizedHealth) nextQuery.health = normalizedHealth
else delete nextQuery.health
if (normalizedGroup) nextQuery.group = normalizedGroup
else delete nextQuery.group
void router.replace({ query: nextQuery })
})
</script>
<template>
@@ -163,6 +202,14 @@ const healthStatusOptions = [
<p class="mt-3 text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">{{ insight.label }}</p>
<strong class="mt-1 block text-2xl text-slate-950">{{ insight.value }}</strong>
<p class="mt-1 text-xs leading-5 text-slate-500">{{ insight.note }}</p>
<RouterLink
v-if="'to' in insight && insight.to"
:to="insight.to"
class="mt-3 inline-flex items-center gap-1 text-xs font-semibold text-violet-700 hover:text-violet-800 hover:underline"
>
Öffnen
<ExternalLink class="h-3.5 w-3.5" />
</RouterLink>
</div>
</div>
</Card>
@@ -261,7 +308,7 @@ const healthStatusOptions = [
<RouterLink
v-for="category in filteredHealth"
:key="category.id"
to="/admin/categories"
:to="`/admin/categories?group=${encodeURIComponent(category.groupName || 'Ohne Gruppe')}`"
class="grid items-center gap-3 px-5 py-3.5 transition hover:bg-violet-50/50 sm:grid-cols-[minmax(0,1fr)_80px_80px_100px_140px]"
>
<!-- Name + group -->