Update release notes and deploy workspace
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 59s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 53s

This commit is contained in:
AzuTear
2026-06-29 17:49:54 +02:00
parent c466348d18
commit 441ef2b850
196 changed files with 9279 additions and 39292 deletions
+14 -1
View File
@@ -24,9 +24,22 @@ const adminWorkspacePermissions = new Set([
'risk',
'audit',
'analytics',
'voting',
'winners',
])
function hasEffectivePermission(permissionKeys: string[] = [], permissionKey: string) {
if (permissionKeys.includes(permissionKey)) {
return true
}
if (permissionKey === 'voting') {
return permissionKeys.includes('analytics') || permissionKeys.includes('winners')
}
return false
}
function readStoredToken() {
if (typeof window === 'undefined') return null
return window.localStorage.getItem(AUTH_TOKEN_KEY)
@@ -56,7 +69,7 @@ export const useAuthStore = defineStore('auth', {
canManageAdminWorkspace: (state) => adminWorkspaceRoles.has(state.session?.role ?? '') || (state.session?.permissionKeys ?? []).some((key) => adminWorkspacePermissions.has(key)),
canManageOperationalSettings: (state) => state.session?.role === 'owner' || state.session?.role === 'creator',
canManageTeam: (state) => (state.session?.permissionKeys ?? []).includes('team'),
hasPermission: (state) => (permissionKey: string) => (state.session?.permissionKeys ?? []).includes(permissionKey),
hasPermission: (state) => (permissionKey: string) => hasEffectivePermission(state.session?.permissionKeys ?? [], permissionKey),
isOwner: (state) => state.session?.role === 'owner',
isCreator: (state) => state.session?.role === 'creator',
isOwnerOrCreator: (state) => state.session?.role === 'owner' || state.session?.role === 'creator',
+43 -32
View File
@@ -5,6 +5,7 @@ import {
classifyPublicLoadError,
createAwardsState,
createEmptyAdminDashboard,
createEmptyAdminOperationalSettings,
createEmptyAdminRiskFlagsResponse,
createEmptyAdminSeasonDetail,
createEmptyAdminOptionalFeatureSettings,
@@ -65,12 +66,10 @@ export const useAwardsStore = defineStore('awards', {
this.categories = await api.getSeasonCategories(this.overview.year)
await this.loadPublicSponsors(this.overview.year)
const overviewArchiveYears = this.overview.archiveYears ?? []
const initialArchiveYear = canExposeCurrentSeasonWinners(this.overview.currentPhase)
? this.overview.year
: overviewArchiveYears[0]?.year
?? this.overview.winnersPreview[0]?.year
?? this.overview.year - 1
this.archive = await api.getWinnerArchive(initialArchiveYear)
const initialArchiveYear = overviewArchiveYears[0]?.year ?? this.overview.year - 1
this.archive = overviewArchiveYears.length > 0
? await api.getWinnerArchive(initialArchiveYear)
: createEmptyArchive(initialArchiveYear)
this.apiMode = 'api'
} catch (error) {
this.apiMode = 'fallback'
@@ -102,18 +101,18 @@ export const useAwardsStore = defineStore('awards', {
},
async loadAdmin() {
try {
const [admin, adminSeasons, adminSiteSettings, adminOptionalFeatureSettings, databaseHealth] = await Promise.all([
api.getAdminDashboard(),
const [adminSeasons, adminSiteSettings, adminOptionalFeatureSettings, adminOperationalSettings, databaseHealth] = await Promise.all([
api.getAdminSeasons(),
api.getAdminSiteSettings(),
api.getAdminOptionalFeatureSettings(),
api.getDatabaseHealth(),
api.getAdminSiteSettings().catch(() => createEmptyAdminSiteSettings()),
api.getAdminOptionalFeatureSettings().catch(() => createEmptyAdminOptionalFeatureSettings()),
api.getAdminOperationalSettings().catch(() => createEmptyAdminOperationalSettings()),
api.getDatabaseHealth().catch(() => createEmptyDatabaseHealth()),
])
this.admin = admin
this.adminSeasons = adminSeasons
this.adminSiteSettings = adminSiteSettings
this.adminOptionalFeatureSettings = adminOptionalFeatureSettings
this.adminOperationalSettings = adminOperationalSettings
this.databaseHealth = databaseHealth
if (!this.adminSelectedSeasonId || !this.adminSeasons.some((season) => season.id === this.adminSelectedSeasonId)) {
@@ -121,17 +120,20 @@ export const useAwardsStore = defineStore('awards', {
}
if (this.adminSelectedSeasonId) {
const [seasonDetail, sponsors, showacts, adminWorkflowRules] = await Promise.all([
const [admin, seasonDetail, sponsors, showacts, adminWorkflowRules] = await Promise.all([
api.getAdminDashboard(this.adminSelectedSeasonId),
api.getAdminSeasonDetail(this.adminSelectedSeasonId),
api.getAdminSponsors(this.adminSelectedSeasonId),
api.getAdminShowactApplications(this.adminSelectedSeasonId),
api.getAdminWorkflowRules(this.adminSelectedSeasonId),
api.getAdminSponsors(this.adminSelectedSeasonId).catch(() => []),
api.getAdminShowactApplications(this.adminSelectedSeasonId).catch(() => []),
api.getAdminWorkflowRules(this.adminSelectedSeasonId).catch(() => createEmptyAdminWorkflowRulesResponse()),
])
this.admin = admin
this.adminSeasonDetail = normalizeSeasonDetail(seasonDetail)
this.adminSponsors = sponsors
this.adminShowactApplications = showacts
this.adminWorkflowRules = adminWorkflowRules
} else {
this.admin = createEmptyAdminDashboard()
this.adminSeasonDetail = createEmptyAdminSeasonDetail()
this.adminSponsors = []
this.adminShowactApplications = []
@@ -144,6 +146,7 @@ export const useAwardsStore = defineStore('awards', {
this.adminSeasons = []
this.adminSeasonDetail = createEmptyAdminSeasonDetail()
this.adminSiteSettings = createEmptyAdminSiteSettings()
this.adminOperationalSettings = createEmptyAdminOperationalSettings()
this.adminOptionalFeatureSettings = createEmptyAdminOptionalFeatureSettings()
this.adminWorkflowRules = createEmptyAdminWorkflowRulesResponse()
this.adminTrackingRules = createEmptyAdminTrackingRulesResponse()
@@ -158,19 +161,22 @@ export const useAwardsStore = defineStore('awards', {
},
async loadAdminContentWorkspace() {
try {
const [adminSiteSettings, adminOptionalFeatureSettings, databaseHealth] = await Promise.all([
api.getAdminSiteSettings(),
api.getAdminOptionalFeatureSettings(),
api.getDatabaseHealth(),
const [adminSiteSettings, adminOptionalFeatureSettings, adminOperationalSettings, databaseHealth] = await Promise.all([
api.getAdminSiteSettings().catch(() => createEmptyAdminSiteSettings()),
api.getAdminOptionalFeatureSettings().catch(() => createEmptyAdminOptionalFeatureSettings()),
api.getAdminOperationalSettings().catch(() => createEmptyAdminOperationalSettings()),
api.getDatabaseHealth().catch(() => createEmptyDatabaseHealth()),
])
this.adminSiteSettings = adminSiteSettings
this.adminOptionalFeatureSettings = adminOptionalFeatureSettings
this.adminOperationalSettings = adminOperationalSettings
this.databaseHealth = databaseHealth
this.apiMode = 'api'
} catch {
this.apiMode = 'fallback'
this.adminSiteSettings = createEmptyAdminSiteSettings()
this.adminOperationalSettings = createEmptyAdminOperationalSettings()
this.adminOptionalFeatureSettings = createEmptyAdminOptionalFeatureSettings()
this.databaseHealth = createEmptyDatabaseHealth()
}
@@ -189,12 +195,14 @@ export const useAwardsStore = defineStore('awards', {
async loadAdminSeasonDetail(seasonId: number) {
try {
this.adminSelectedSeasonId = seasonId
const [seasonDetail, sponsors, showacts, adminWorkflowRules] = await Promise.all([
const [admin, seasonDetail, sponsors, showacts, adminWorkflowRules] = await Promise.all([
api.getAdminDashboard(seasonId),
api.getAdminSeasonDetail(seasonId),
api.getAdminSponsors(seasonId),
api.getAdminShowactApplications(seasonId),
api.getAdminWorkflowRules(seasonId),
api.getAdminSponsors(seasonId).catch(() => []),
api.getAdminShowactApplications(seasonId).catch(() => []),
api.getAdminWorkflowRules(seasonId).catch(() => createEmptyAdminWorkflowRulesResponse()),
])
this.admin = admin
this.adminSeasonDetail = normalizeSeasonDetail(seasonDetail)
this.adminSponsors = sponsors
this.adminShowactApplications = showacts
@@ -202,6 +210,7 @@ export const useAwardsStore = defineStore('awards', {
this.apiMode = 'api'
} catch {
this.apiMode = 'fallback'
this.admin = createEmptyAdminDashboard()
this.adminSeasonDetail = createEmptyAdminSeasonDetail()
this.adminSponsors = []
this.adminShowactApplications = []
@@ -416,6 +425,16 @@ export const useAwardsStore = defineStore('awards', {
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true, reloadHome: true })
return result
},
async publishAdminWinners(seasonId: number) {
const result = await api.publishAdminWinners(seasonId)
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true, reloadHome: true })
return result
},
async unpublishAdminWinners(seasonId: number) {
const result = await api.unpublishAdminWinners(seasonId)
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true, reloadHome: true })
return result
},
async approveAdminNomination(nominationId: number, seasonId: number, payload: ApproveNominationPayload) {
const result = await api.approveAdminNomination(nominationId, payload)
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true, reloadHome: true })
@@ -545,12 +564,4 @@ export const useAwardsStore = defineStore('awards', {
},
})
function canExposeCurrentSeasonWinners(currentPhase: string) {
const normalized = currentPhase.trim().toLowerCase()
return normalized.includes('abgeschlossen')
|| normalized.includes('archiv')
|| normalized.includes('complete')
|| normalized.includes('ended')
}
export type AwardsStore = ReturnType<typeof useAwardsStore>
+91 -2
View File
@@ -2,6 +2,7 @@ import { ApiRequestError } from '../../lib/http'
import type {
AdminDashboardResponse,
AdminOptionalFeatureSettingsResponse,
AdminOperationalSettingsResponse,
AdminRiskFlag,
AdminRiskFlagsResponse,
AdminSeasonDetailResponse,
@@ -28,7 +29,6 @@ export function createEmptyOverview(): OverviewResponse {
title: '',
showDate: '',
showStartsAt: '20:00:00',
showStreamUrl: '',
currentPhase: '',
isCommunityOnly: true,
loginProvider: 'Twitch',
@@ -44,6 +44,24 @@ export function createEmptyOverview(): OverviewResponse {
shareDiscordUrl: '',
privacyEmail: '',
privacyPolicyContent: '',
awardsSectionTitle: '',
awardsSectionDescription: '',
subcategoriesSectionTitle: '',
subcategoriesSectionDescription: '',
streamBanner: {
eyebrow: 'Das grosse Finale',
title: 'Award-Show Finale',
text: 'Finale, Countdown und Stream-Link an einem Ort. Sei live dabei, wenn die Community ihre Stars feiert.',
liveButtonLabel: 'Jetzt live · Zum Stream',
liveButtonUrl: '',
lockedButtonLabel: 'Stream noch gesperrt',
useCompletedContent: false,
completedEyebrow: 'Danke fürs Mitfiebern',
completedTitle: 'Award-Show abgeschlossen',
completedText: 'Die grosse Award-Show ist vorbei. Danke an alle, die nominiert, gevotet und live mitgefiebert haben.',
completedButtonLabel: 'Highlights ansehen',
completedButtonUrl: '',
},
socialLinks: [],
footerLinks: [],
},
@@ -87,6 +105,10 @@ export function createEmptyArchive(year = new Date().getFullYear() - 1): WinnerA
export function createEmptyAdminDashboard(): AdminDashboardResponse {
return {
seasonId: 0,
year: 0,
seasonName: '',
isCurrent: false,
metrics: [],
activities: [],
topCategories: [],
@@ -95,6 +117,27 @@ export function createEmptyAdminDashboard(): AdminDashboardResponse {
}
}
export function createEmptyAdminOperationalSettings(): AdminOperationalSettingsResponse {
return {
demoLoginManagedByDatabase: false,
demoLoginEnabled: false,
demoLoginEmail: '',
demoLoginPasswordSet: false,
demoLoginTwitchUserId: '',
demoLoginDisplayName: '',
twitchAuthManagedByDatabase: false,
twitchAuthConfigured: false,
twitchClientId: '',
twitchClientSecretSet: false,
twitchRedirectUri: '',
twitchScope: '',
sessionIdleTimeoutHours: 12,
maintenanceModeEnabled: false,
maintenanceTitle: 'Sternenpause',
maintenanceMessage: 'Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.',
}
}
export function createEmptyAdminRiskFlagsResponse(): AdminRiskFlagsResponse {
return {
items: [],
@@ -135,7 +178,7 @@ export function createEmptyAdminSeasonDetail(): AdminSeasonDetailResponse {
id: 0,
year: 0,
name: '',
showStreamUrl: '',
isDemo: false,
currentPhase: '',
isCurrent: false,
isCommunityOnly: true,
@@ -147,6 +190,8 @@ export function createEmptyAdminSeasonDetail(): AdminSeasonDetailResponse {
reviewEndsAt: '',
showDate: '',
showStartsAt: '20:00:00',
winnersPublishedAt: null,
winnersPublishedByTwitchId: null,
subcategoryTemplates: [],
categories: [],
candidates: [],
@@ -156,6 +201,17 @@ export function createEmptyAdminSeasonDetail(): AdminSeasonDetailResponse {
trackingReviewNotes: '',
showTrackingReviewNotes: true,
results: [],
votingWorkspace: {
summary: {
totalVotes: 0,
totalSubcategories: 0,
votedSubcategories: 0,
readySubcategories: 0,
problemSubcategories: 0,
winnerSetSubcategories: 0,
},
categories: [],
},
clipSubmissions: [],
}
}
@@ -179,6 +235,22 @@ export function createEmptyAdminSiteSettings(): AdminSiteSettingsResponse {
sponsorsContent: '',
showactsUrl: '',
showactsContent: '',
streamBannerEyebrow: 'Das grosse Finale',
streamBannerTitle: 'Award-Show Finale',
streamBannerText: 'Finale, Countdown und Stream-Link an einem Ort. Sei live dabei, wenn die Community ihre Stars feiert.',
streamBannerLiveButtonLabel: 'Jetzt live · Zum Stream',
streamBannerLiveButtonUrl: '',
streamBannerLockedButtonLabel: 'Stream noch gesperrt',
streamBannerUseCompletedContent: false,
streamBannerCompletedEyebrow: 'Danke fürs Mitfiebern',
streamBannerCompletedTitle: 'Award-Show abgeschlossen',
streamBannerCompletedText: 'Die grosse Award-Show ist vorbei. Danke an alle, die nominiert, gevotet und live mitgefiebert haben.',
streamBannerCompletedButtonLabel: 'Highlights ansehen',
streamBannerCompletedButtonUrl: '',
awardsSectionTitle: '',
awardsSectionDescription: '',
subcategoriesSectionTitle: '',
subcategoriesSectionDescription: '',
socialLinks: [],
faq: [],
showactFormSchemaJson: '[]',
@@ -221,6 +293,7 @@ export function createAwardsState() {
adminSeasons: [] as AdminSeasonListItem[],
adminSeasonDetail: createEmptyAdminSeasonDetail(),
adminSiteSettings: createEmptyAdminSiteSettings(),
adminOperationalSettings: createEmptyAdminOperationalSettings(),
adminOptionalFeatureSettings: createEmptyAdminOptionalFeatureSettings(),
adminWorkflowRules: createEmptyAdminWorkflowRulesResponse(),
adminTrackingRules: createEmptyAdminTrackingRulesResponse(),
@@ -245,6 +318,8 @@ export function createAwardsState() {
export function normalizeSeasonDetail(detail: AdminSeasonDetailResponse): AdminSeasonDetailResponse {
return {
...detail,
winnersPublishedAt: detail.winnersPublishedAt ?? null,
winnersPublishedByTwitchId: detail.winnersPublishedByTwitchId ?? null,
subcategoryTemplates: detail.subcategoryTemplates ?? [],
categories: detail.categories ?? [],
candidates: (detail.candidates ?? []).map((candidate) => ({
@@ -263,6 +338,20 @@ export function normalizeSeasonDetail(detail: AdminSeasonDetailResponse): AdminS
reviewedNominations: detail.reviewedNominations ?? [],
trackingReviewNotes: detail.trackingReviewNotes ?? '',
results: detail.results ?? [],
votingWorkspace: {
summary: {
totalVotes: detail.votingWorkspace?.summary?.totalVotes ?? 0,
totalSubcategories: detail.votingWorkspace?.summary?.totalSubcategories ?? 0,
votedSubcategories: detail.votingWorkspace?.summary?.votedSubcategories ?? 0,
readySubcategories: detail.votingWorkspace?.summary?.readySubcategories ?? 0,
problemSubcategories: detail.votingWorkspace?.summary?.problemSubcategories ?? 0,
winnerSetSubcategories: detail.votingWorkspace?.summary?.winnerSetSubcategories ?? 0,
},
categories: (detail.votingWorkspace?.categories ?? []).map((category) => ({
...category,
leaderboard: category.leaderboard ?? [],
})),
},
clipSubmissions: detail.clipSubmissions ?? [],
}
}