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
@@ -86,21 +86,27 @@ export function useAdminAnalyticsManager() {
value: `${seasonDetail.value.categories.length - emptyCategories.value.length}/${seasonDetail.value.categories.length}`,
note: emptyCategories.value.length === 0 ? 'Alle Kategorien haben Kandidaten.' : `${emptyCategories.value.length} Kategorien sind noch leer.`,
icon: Tags,
to: emptyCategories.value.length > 0 ? '/admin/candidates' : '/admin/categories',
to: emptyCategories.value.length > 0
? `/admin/categories?group=${encodeURIComponent(emptyCategories.value[0]!.groupName)}`
: '/admin/categories',
},
{
label: 'Gewinnerstatus',
value: `${winnerCoveragePct.value}%`,
note: `${seasonDetail.value.results.length} von ${seasonDetail.value.categories.length} Kategorien final gesetzt.`,
icon: Trophy,
to: '/admin/winners',
to: categoriesWithoutWinner.value.length > 0
? `/admin/winners?status=open&categoryId=${categoriesWithoutWinner.value[0]!.id}`
: '/admin/winners?status=set',
},
{
label: 'Freigabe-Risiko',
value: String(categoriesWithReviews.value.length + pendingClipCount.value),
note: `${categoriesWithReviews.value.length} Kategorien mit Reviews, ${pendingClipCount.value} Clips offen.`,
icon: AlertTriangle,
to: categoriesWithReviews.value.length > 0 ? '/admin/nominations?review=1' : '/admin/clips',
to: categoriesWithReviews.value.length > 0
? `/admin/nominations?review=1&categoryId=${categoriesWithReviews.value[0]!.id}&status=heavy`
: '/admin/clips?status=pending',
},
])
@@ -127,6 +133,9 @@ export function useAdminAnalyticsManager() {
value: String(busiestReviewCategory?.reviews ?? 0),
note: busiestReviewCategory ? busiestReviewCategory.name : 'Keine offenen Review-Hotspots.',
icon: Clock3,
to: busiestReviewCategory
? `/admin/nominations?review=1&categoryId=${busiestReviewCategory.id}&status=heavy`
: '/admin/nominations?review=1',
},
]
})
@@ -137,7 +146,9 @@ export function useAdminAnalyticsManager() {
label: 'Leere Kategorien',
value: emptyCategories.value.length,
note: emptyCategories.value.length === 0 ? 'Keine Luecke in der Kandidatenbasis.' : emptyCategories.value.slice(0, 3).map((category) => category.name).join(', '),
to: '/admin/candidates',
to: emptyCategories.value.length > 0
? `/admin/categories?group=${encodeURIComponent(emptyCategories.value[0]!.groupName)}`
: '/admin/categories',
tone: emptyCategories.value.length > 0 ? 'border-rose-100 bg-rose-50 text-rose-800' : 'border-emerald-100 bg-emerald-50 text-emerald-800',
},
{
@@ -145,7 +156,9 @@ export function useAdminAnalyticsManager() {
label: 'Offene Reviews',
value: seasonDetail.value.pendingNominations.length,
note: categoriesWithReviews.value.length === 0 ? 'Review-Queue ist leer.' : `${categoriesWithReviews.value.length} Kategorien betroffen.`,
to: '/admin/nominations?review=1',
to: categoriesWithReviews.value.length > 0
? `/admin/nominations?review=1&categoryId=${categoriesWithReviews.value[0]!.id}&status=heavy`
: '/admin/nominations?review=1',
tone: seasonDetail.value.pendingNominations.length > 0 ? 'border-amber-100 bg-amber-50 text-amber-800' : 'border-emerald-100 bg-emerald-50 text-emerald-800',
},
{
@@ -153,7 +166,9 @@ export function useAdminAnalyticsManager() {
label: 'Gewinner fehlen',
value: categoriesWithoutWinner.value.length,
note: categoriesWithoutWinner.value.length === 0 ? 'Alle Gewinner sind gesetzt.' : 'Finalisierung auf der Gewinner-Seite abschliessen.',
to: '/admin/winners',
to: categoriesWithoutWinner.value.length > 0
? `/admin/winners?status=open&categoryId=${categoriesWithoutWinner.value[0]!.id}`
: '/admin/winners?status=set',
tone: categoriesWithoutWinner.value.length > 0 ? 'border-sky-100 bg-sky-50 text-sky-800' : 'border-emerald-100 bg-emerald-50 text-emerald-800',
},
])