10 Commits

Author SHA1 Message Date
Torsten Schulz (local)
3314aa06b4 Aktualisiere Footer-Stile und verbessere Zugänglichkeit; erhöhe die Version auf 1.8.8
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 6m7s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 3m13s
2026-08-27 13:03:44 +02:00
Torsten Schulz (local)
12c909d6a4 Füge Sparse-Checkout für den Web-Workflow hinzu und ignoriere JVM Heap-Dumps
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 5m37s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m53s
2026-08-26 14:09:52 +02:00
Torsten Schulz (local)
c2594fd330 Entferne nicht mehr benötigte HPROF-Datei zur Optimierung der App-Größe
Some checks failed
Code Analysis and Production Deploy / analyze (push) Has started running
Code Analysis and Production Deploy / deploy-production (push) Has been cancelled
Code Analysis and Production Deploy / deploy-test (push) Has been cancelled
2026-08-26 14:09:40 +02:00
Torsten Schulz (local)
00b08e2f92 Füge Scrollsteuerung für Mannschaften-Untermenü hinzu und optimiere Importpfade
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 2m47s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
2026-08-26 13:51:07 +02:00
Torsten Schulz (local)
93da9fbcbe Füge Scrollfunktion für Mannschaften-Untermenü hinzu und optimiere Button-Interaktion
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 9m39s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m56s
2026-08-26 12:49:07 +02:00
Torsten Schulz (local)
82dcb1da40 Update Navigation component styles and bump version to 1.8.7
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 10m21s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 3m4s
2026-08-26 11:54:48 +02:00
Torsten Schulz (local)
e82c1d9b0a Optimierung der PM2-Instanzverwaltung: Entfernen der zweiten Instanz und Anpassung der Fehlermeldungen in den Deployment-Skripten
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 10m23s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m52s
2026-08-26 11:23:48 +02:00
Torsten Schulz (local)
c1c648de74 Erweiterung der Teamformatierungsfunktionen um Altersgruppen für bessere Anzeige in Spielplänen
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 10m50s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m56s
2026-08-26 10:53:24 +02:00
Torsten Schulz (local)
7523023be8 Optimierung des Checkout-Prozesses im Analyse-Workflow durch flachen Checkout
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 12m48s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 3m25s
2026-08-26 10:31:02 +02:00
Torsten Schulz (local)
2103e9c1e8 Refactor team name formatting and update version to 1.8.6
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 2m27s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
2026-08-26 10:22:08 +02:00
21 changed files with 214 additions and 130 deletions

View File

@@ -13,7 +13,16 @@ jobs:
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with: with:
clean: true clean: true
fetch-depth: 0 # Der Analyse-Workflow benötigt keine Historie. Ein flacher Checkout
# verhindert, dass der Runner das große Repository-Pack vollständig lädt.
fetch-depth: 1
# Der Web-Workflow braucht keine Android-Artefakte. Insbesondere liegt
# dort ein großer Heap-Dump im aktuellen Commit, der auch bei einem
# flachen Checkout übertragen würde und den Runner-Checkout abbricht.
sparse-checkout: |
/*
!/android-app/
sparse-checkout-cone-mode: false
- name: Ensure clean workspace - name: Ensure clean workspace
run: | run: |

2
.gitignore vendored
View File

@@ -94,6 +94,8 @@ dist
/android-app/**/build/ /android-app/**/build/
/android-app/local.properties /android-app/local.properties
/android-app/gradle-local.properties /android-app/gradle-local.properties
# JVM Heap-Dumps sind lokale Diagnoseartefakte und dürfen nie ins Repository.
*.hprof
# Build output (but keep production data!) # Build output (but keep production data!)
.output .output

Binary file not shown.

View File

@@ -1,39 +1,39 @@
<template> <template>
<footer class="fixed bottom-0 left-0 right-0 z-40 bg-gray-900 border-t border-gray-800 shadow-2xl"> <footer class="fixed bottom-0 left-0 right-0 z-40 border-t border-gray-200/90 bg-white/95 shadow-[0_-5px_18px_rgba(24,24,27,0.07)] backdrop-blur-sm">
<div class="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 py-2 sm:py-3"> <div class="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 py-2.5 sm:py-3">
<div class="flex flex-col sm:flex-row justify-between items-center gap-y-1 sm:gap-y-0"> <div class="flex flex-col sm:flex-row justify-between items-center gap-y-1 sm:gap-y-0">
<p class="text-xs sm:text-sm text-gray-400 whitespace-nowrap"> <p class="text-xs sm:text-sm text-gray-600 whitespace-nowrap">
© {{ currentYear }} Harheimer TC 1954 e.V. © {{ currentYear }} Harheimer TC 1954 e.V.
</p> </p>
<div class="flex flex-wrap justify-center items-center gap-x-4 gap-y-1 text-xs sm:text-sm relative"> <div class="flex flex-wrap justify-center items-center gap-x-4 gap-y-1 text-xs sm:text-sm relative">
<span <span
v-if="isLoggedIn && appVersion" v-if="isLoggedIn && appVersion"
class="text-xs text-gray-600" class="text-xs text-gray-500"
title="Version" title="Version"
> >
v{{ appVersion }} v{{ appVersion }}
</span> </span>
<NuxtLink <NuxtLink
to="/impressum" to="/impressum"
class="text-gray-400 hover:text-primary-400 transition-colors" class="text-gray-700 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 rounded-sm transition-colors"
> >
Impressum Impressum
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/datenschutz" to="/datenschutz"
class="text-gray-400 hover:text-primary-400 transition-colors" class="text-gray-700 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 rounded-sm transition-colors"
> >
Datenschutz Datenschutz
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/konto-loeschen" to="/konto-loeschen"
class="text-gray-400 hover:text-primary-400 transition-colors" class="text-gray-700 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 rounded-sm transition-colors"
> >
Konto loeschen Konto loeschen
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/kontakt" to="/kontakt"
class="text-gray-400 hover:text-primary-400 transition-colors" class="text-gray-700 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 rounded-sm transition-colors"
> >
Kontakt Kontakt
</NuxtLink> </NuxtLink>
@@ -41,7 +41,7 @@
<!-- Login/Logout --> <!-- Login/Logout -->
<template v-if="isLoggedIn"> <template v-if="isLoggedIn">
<button <button
class="flex items-center space-x-1 text-gray-400 hover:text-primary-400 transition-colors" class="flex items-center space-x-1 rounded-sm text-gray-700 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 transition-colors"
@click="handleLogout" @click="handleLogout"
> >
<User :size="16" /> <User :size="16" />
@@ -53,7 +53,7 @@
class="relative" class="relative"
> >
<button <button
class="flex items-center space-x-1 text-gray-400 hover:text-primary-400 transition-colors" class="flex items-center space-x-1 rounded-sm text-gray-700 hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 transition-colors"
@click="toggleMemberMenu" @click="toggleMemberMenu"
> >
<User :size="16" /> <User :size="16" />
@@ -75,25 +75,25 @@
> >
<div <div
v-if="isMemberMenuOpen" v-if="isMemberMenuOpen"
class="absolute bottom-full right-0 mb-2 w-48 bg-gray-800 border border-gray-700 rounded-lg shadow-xl overflow-hidden" class="absolute bottom-full right-0 mb-2 w-48 overflow-hidden rounded-md border border-gray-200 bg-white shadow-[0_12px_28px_rgba(24,24,27,0.14)] ring-1 ring-black/5"
> >
<NuxtLink <NuxtLink
to="/login" to="/login"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors" class="block px-4 py-2 text-sm text-gray-700 hover:bg-primary-50 hover:text-primary-800 focus-visible:outline-none focus-visible:bg-primary-50 focus-visible:text-primary-800 transition-colors"
@click="isMemberMenuOpen = false" @click="isMemberMenuOpen = false"
> >
Anmelden Anmelden
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/registrieren" to="/registrieren"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors" class="block px-4 py-2 text-sm text-gray-700 hover:bg-primary-50 hover:text-primary-800 focus-visible:outline-none focus-visible:bg-primary-50 focus-visible:text-primary-800 transition-colors"
@click="isMemberMenuOpen = false" @click="isMemberMenuOpen = false"
> >
Registrieren Registrieren
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/passwort-vergessen" to="/passwort-vergessen"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors" class="block px-4 py-2 text-sm text-gray-700 hover:bg-primary-50 hover:text-primary-800 focus-visible:outline-none focus-visible:bg-primary-50 focus-visible:text-primary-800 transition-colors"
@click="isMemberMenuOpen = false" @click="isMemberMenuOpen = false"
> >
Passwort vergessen Passwort vergessen

View File

@@ -58,7 +58,9 @@
</p> </p>
</div> </div>
<p class="text-sm text-gray-800"> <p class="text-sm text-gray-800">
{{ game.HeimMannschaft }} vs {{ game.GastMannschaft }} {{ formatMatchTeamName(game.HeimMannschaft, game.HeimMannschaftAltersklasse, game.Altersklasse) }}
vs
{{ formatMatchTeamName(game.GastMannschaft, game.GastMannschaftAltersklasse, game.Altersklasse) }}
</p> </p>
</div> </div>
</div> </div>
@@ -69,6 +71,7 @@
<script setup> <script setup>
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { formatTeamDisplayName } from '~/utils/team-display'
const props = defineProps({ const props = defineProps({
season: { season: {
@@ -91,10 +94,13 @@ const games = ref([])
const widgetTitle = computed(() => { const widgetTitle = computed(() => {
if (!props.teamName) return 'Mannschaft' if (!props.teamName) return 'Mannschaft'
const youth = String(props.teamAgeGroup || '').toLowerCase().includes('jugend') return formatTeamDisplayName(props.teamName, props.teamAgeGroup)
return youth ? `(J) ${props.teamName}` : props.teamName
}) })
function formatMatchTeamName(teamName, teamAgeGroup, competitionAgeGroup) {
return formatTeamDisplayName(teamName, teamAgeGroup, competitionAgeGroup) || '-'
}
const seasonLabel = computed(() => { const seasonLabel = computed(() => {
const match = String(props.season || '').match(/^(\d{2})--(\d{2})$/) const match = String(props.season || '').match(/^(\d{2})--(\d{2})$/)
if (!match) return props.season || '-' if (!match) return props.season || '-'
@@ -184,4 +190,4 @@ watch(
}, },
{ immediate: true } { immediate: true }
) )
</script> </script>

View File

@@ -5,11 +5,11 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full">
<div class="flex flex-col justify-between h-full py-2"> <div class="flex flex-col justify-between h-full py-2">
<!-- Hauptmenü --> <!-- Hauptmenü -->
<div class="flex justify-between items-center"> <div class="flex min-w-0 items-center gap-3">
<!-- Logo --> <!-- Logo -->
<NuxtLink <NuxtLink
to="/" to="/"
class="flex items-center space-x-3 hover:scale-105 transition-transform" class="flex shrink-0 items-center space-x-3 origin-left hover:scale-[1.02] transition-transform"
> >
<img <img
src="~/assets/images/logos/Harheimer TC.svg" src="~/assets/images/logos/Harheimer TC.svg"
@@ -23,9 +23,12 @@
</div> </div>
</NuxtLink> </NuxtLink>
<div style="display:flex;flex-direction:column;"> <div class="flex min-w-0 flex-1 flex-col">
<!-- Desktop Navigation --> <!-- Desktop Navigation -->
<div class="hidden lg:flex items-center space-x-1"> <div
class="hidden min-w-0 max-w-full lg:flex lg:items-center lg:gap-1 lg:overflow-x-auto lg:whitespace-nowrap [scrollbar-color:theme(colors.primary.700)_transparent] [scrollbar-width:thin]"
aria-label="Hauptmenü – horizontal scrollbar"
>
<NuxtLink <NuxtLink
to="/" to="/"
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50" class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
@@ -111,10 +114,10 @@
</NuxtLink> </NuxtLink>
</div> </div>
<div class="hidden lg:flex items-center h-6 border-t border-primary-700/20"> <div class="hidden min-w-0 flex-1 lg:flex items-center h-6 border-t border-primary-700/20">
<div <div
v-if="currentSubmenu" v-if="currentSubmenu"
class="flex items-center space-x-1" :class="currentSubmenu === 'mannschaften' ? 'relative min-w-0 flex-1' : 'flex min-w-0 items-center gap-1 overflow-x-auto whitespace-nowrap'"
> >
<!-- Newsletter Submenu --> <!-- Newsletter Submenu -->
<template v-if="currentSubmenu === 'newsletter'"> <template v-if="currentSubmenu === 'newsletter'">
@@ -188,41 +191,69 @@
<!-- Mannschaften Submenu --> <!-- Mannschaften Submenu -->
<template v-if="currentSubmenu === 'mannschaften'"> <template v-if="currentSubmenu === 'mannschaften'">
<NuxtLink <div
to="/mannschaften" ref="mannschaftenSubmenuRail"
class="px-2.5 py-1 text-xs font-semibold text-white hover:bg-primary-700/50 rounded transition-all" class="flex min-w-0 items-center gap-1 overflow-x-auto whitespace-nowrap scroll-smooth px-7 [scrollbar-color:theme(colors.primary.700)_transparent] [scrollbar-width:thin]"
active-class="bg-primary-600" tabindex="0"
> aria-label="Mannschaften-Untermenü – horizontal scrollbar"
Übersicht @scroll.passive="updateMannschaftenSubmenuControls"
</NuxtLink>
<div class="h-3 w-px bg-primary-700" />
<template
v-for="mannschaft in mannschaften"
:key="mannschaft.slug"
> >
<NuxtLink <NuxtLink
:to="`/mannschaften/${mannschaft.slug}`" to="/mannschaften"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all" class="shrink-0 px-2.5 py-1 text-xs font-semibold text-white hover:bg-primary-700/50 rounded transition-all"
active-class="bg-primary-600"
>
Übersicht
</NuxtLink>
<div class="h-3 w-px shrink-0 bg-primary-700" />
<template
v-for="mannschaft in mannschaften"
:key="mannschaft.slug"
>
<NuxtLink
:to="`/mannschaften/${mannschaft.slug}`"
class="shrink-0 whitespace-nowrap px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
{{ mannschaft.mannschaft }}
</NuxtLink>
</template>
<div class="h-3 w-px shrink-0 bg-primary-700" />
<NuxtLink
to="/mannschaften/spielplaene"
class="shrink-0 whitespace-nowrap px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600" active-class="text-white bg-primary-600"
> >
{{ mannschaft.mannschaft }} Spielpläne
</NuxtLink> </NuxtLink>
</template> <NuxtLink
<div class="h-3 w-px bg-primary-700" /> to="/spielsysteme"
<NuxtLink class="shrink-0 whitespace-nowrap px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
to="/mannschaften/spielplaene" active-class="text-white bg-primary-600"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all" >
active-class="text-white bg-primary-600" Spielsysteme
</NuxtLink>
</div>
<button
v-if="showMannschaftenSubmenuLeftControl"
type="button"
class="absolute inset-y-0 left-0 z-10 flex w-9 items-center justify-start bg-gradient-to-r from-primary-900 via-primary-900/90 to-transparent pl-1 text-primary-300 transition-colors hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-300"
aria-label="Vorherige Mannschaften anzeigen"
@pointerdown.stop
@click.stop.prevent="scrollMannschaftenSubmenuLeft"
> >
Spielpläne ‹
</NuxtLink> </button>
<NuxtLink <button
to="/spielsysteme" v-if="showMannschaftenSubmenuRightControl"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all" type="button"
active-class="text-white bg-primary-600" class="absolute inset-y-0 right-0 z-10 flex w-9 items-center justify-end bg-gradient-to-l from-primary-900 via-primary-900/90 to-transparent pr-1 text-primary-300 transition-colors hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-300"
aria-label="Weitere Mannschaften anzeigen"
@pointerdown.stop
@click.stop.prevent="scrollMannschaftenSubmenuRight"
> >
Spielsysteme ›
</NuxtLink> </button>
</template> </template>
<!-- Training Submenu --> <!-- Training Submenu -->
@@ -853,7 +884,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue' import { ref, onMounted, onUnmounted, computed, nextTick, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { Menu, X, ChevronDown } from 'lucide-vue-next' import { Menu, X, ChevronDown } from 'lucide-vue-next'
@@ -861,6 +892,9 @@ const route = useRoute()
const isMobileMenuOpen = ref(false) const isMobileMenuOpen = ref(false)
const mobileSubmenu = ref(null) const mobileSubmenu = ref(null)
const mannschaften = ref([]) const mannschaften = ref([])
const mannschaftenSubmenuRail = ref(null)
const showMannschaftenSubmenuLeftControl = ref(false)
const showMannschaftenSubmenuRightControl = ref(false)
const hasGalleryImages = ref(false) const hasGalleryImages = ref(false)
const showCmsDropdown = ref(false) const showCmsDropdown = ref(false)
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -898,6 +932,38 @@ const toggleMobileSubmenu = (menu) => {
mobileSubmenu.value = mobileSubmenu.value === menu ? null : menu mobileSubmenu.value = mobileSubmenu.value === menu ? null : menu
} }
const updateMannschaftenSubmenuControls = () => {
const rail = mannschaftenSubmenuRail.value
if (!rail) return
const canScroll = rail.scrollWidth > rail.clientWidth + 1
const atStart = rail.scrollLeft <= 1
const atEnd = rail.scrollLeft + rail.clientWidth >= rail.scrollWidth - 1
showMannschaftenSubmenuLeftControl.value = canScroll && !atStart
showMannschaftenSubmenuRightControl.value = canScroll && !atEnd
}
const scrollMannschaftenSubmenuLeft = () => {
const rail = mannschaftenSubmenuRail.value
if (!rail) return
rail.scrollBy({
left: -Math.max(rail.clientWidth * 0.8, 160),
behavior: 'smooth'
})
}
const scrollMannschaftenSubmenuRight = () => {
const rail = mannschaftenSubmenuRail.value
if (!rail) return
rail.scrollBy({
left: Math.max(rail.clientWidth * 0.8, 160),
behavior: 'smooth'
})
}
const loadMannschaften = async () => { const loadMannschaften = async () => {
try { try {
const attempt = async () => { const attempt = async () => {
@@ -982,16 +1048,23 @@ onMounted(() => {
// Listen for global updates to mannschaften (e.g., CMS saved) // Listen for global updates to mannschaften (e.g., CMS saved)
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.addEventListener('mannschaften:changed', loadMannschaften) window.addEventListener('mannschaften:changed', loadMannschaften)
window.addEventListener('resize', updateMannschaftenSubmenuControls)
} }
nextTick(updateMannschaftenSubmenuControls)
}) })
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleDocumentClick) document.removeEventListener('click', handleDocumentClick)
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.removeEventListener('mannschaften:changed', loadMannschaften) window.removeEventListener('mannschaften:changed', loadMannschaften)
window.removeEventListener('resize', updateMannschaftenSubmenuControls)
} }
}) })
watch([currentSubmenu, mannschaften], () => {
nextTick(updateMannschaftenSubmenuControls)
}, { flush: 'post' })
const toggleSubmenu = (menu) => { const toggleSubmenu = (menu) => {
// Wenn wir schon im richtigen Bereich sind, nichts tun (Submenu bleibt offen) // Wenn wir schon im richtigen Bereich sind, nichts tun (Submenu bleibt offen)
// Wenn nicht, zur Hauptseite navigieren // Wenn nicht, zur Hauptseite navigieren

View File

@@ -146,7 +146,7 @@
Heim Heim
</p> </p>
<p class="font-semibold text-gray-900"> <p class="font-semibold text-gray-900">
{{ formatTeamName(game.HeimMannschaft, game.HeimMannschaftAltersklasse) }} {{ formatTeamName(game.HeimMannschaft, game.HeimMannschaftAltersklasse, game.Altersklasse) }}
</p> </p>
</div> </div>
<div class="text-center mx-4"> <div class="text-center mx-4">
@@ -159,7 +159,7 @@
Gast Gast
</p> </p>
<p class="font-semibold text-gray-900"> <p class="font-semibold text-gray-900">
{{ formatTeamName(game.GastMannschaft, game.GastMannschaftAltersklasse) }} {{ formatTeamName(game.GastMannschaft, game.GastMannschaftAltersklasse, game.Altersklasse) }}
</p> </p>
</div> </div>
</div> </div>
@@ -220,6 +220,7 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { formatTeamDisplayName } from '~/utils/team-display'
const spielplanData = ref([]) const spielplanData = ref([])
const isLoading = ref(false) const isLoading = ref(false)
@@ -355,17 +356,10 @@ const formatTime = (terminString) => {
} }
} }
const formatTeamName = (teamName, ageGroup) => { const formatTeamName = (teamName, ageGroup, competitionAgeGroup) => {
if (!teamName) return 'Nicht angegeben' if (!teamName) return 'Nicht angegeben'
// Prüfe ob es Nachwuchs ist return formatTeamDisplayName(teamName, ageGroup, competitionAgeGroup)
const isNachwuchs = ageGroup && (
ageGroup.toLowerCase().includes('jugend') ||
teamName.toLowerCase().includes('jugend')
)
// Füge (J) für Nachwuchs hinzu
return isNachwuchs ? `(J) ${teamName}` : teamName
} }
const formatRunde = (runde) => { const formatRunde = (runde) => {

View File

@@ -282,17 +282,11 @@ install_dependencies_if_needed
# 4. Stop running apps before replacing build artifacts # 4. Stop running apps before replacing build artifacts
echo "" echo ""
echo "4. Stopping PM2 before replacing build artifacts..." echo "4. Stopping PM2 before replacing build artifacts..."
if command -v pm2 >/dev/null 2>&1; then if command -v pm2 >/dev/null 2>&1 && pm2 describe harheimertc >/dev/null 2>&1; then
for instance_name in harheimertc harheimertc-3102; do pm2 stop harheimertc || true
if pm2 describe "$instance_name" >/dev/null 2>&1; then echo " ✓ harheimertc gestoppt"
pm2 stop "$instance_name" || true
echo " ✓ $instance_name gestoppt"
else
echo " PM2-Prozess $instance_name läuft nicht"
fi
done
else else
echo " PM2 ist nicht verfügbar" echo " PM2-Prozess harheimertc läuft nicht oder PM2 ist nicht verfügbar"
fi fi
# 5. Remove old build (but keep data!) # 5. Remove old build (but keep data!)
@@ -559,18 +553,14 @@ restart_pm2_instance() {
fi fi
} }
# Starte/Neustarte beide Instanzen # Starte/Neustarte die Produktionsinstanz
INSTANCE_ERRORS=0 INSTANCE_ERRORS=0
if ! restart_pm2_instance "harheimertc"; then if ! restart_pm2_instance "harheimertc"; then
INSTANCE_ERRORS=$((INSTANCE_ERRORS + 1)) INSTANCE_ERRORS=$((INSTANCE_ERRORS + 1))
fi fi
if ! restart_pm2_instance "harheimertc-3102"; then # Prüfe, ob der Prozess läuft
INSTANCE_ERRORS=$((INSTANCE_ERRORS + 1))
fi
# Prüfe, ob beide Prozesse laufen
sleep 2 sleep 2
echo "" echo ""
echo " Checking PM2 instances status..." echo " Checking PM2 instances status..."
@@ -582,19 +572,11 @@ else
INSTANCE_ERRORS=$((INSTANCE_ERRORS + 1)) INSTANCE_ERRORS=$((INSTANCE_ERRORS + 1))
fi fi
if pm2 describe harheimertc-3102 | grep -q "online"; then
echo " ✓ PM2-Prozess 'harheimertc-3102' läuft (online)"
else
echo " WARNING: PM2-Prozess 'harheimertc-3102' ist nicht online. Prüfe Logs: pm2 logs harheimertc-3102"
INSTANCE_ERRORS=$((INSTANCE_ERRORS + 1))
fi
if [ "$INSTANCE_ERRORS" -gt 0 ]; then if [ "$INSTANCE_ERRORS" -gt 0 ]; then
echo "" echo ""
echo "WARNING: Einige PM2-Instanzen haben Probleme. Bitte manuell prüfen:" echo "WARNING: Einige PM2-Instanzen haben Probleme. Bitte manuell prüfen:"
echo " pm2 status" echo " pm2 status"
echo " pm2 logs harheimertc" echo " pm2 logs harheimertc"
echo " pm2 logs harheimertc-3102"
fi fi
echo "" echo ""
@@ -603,8 +585,5 @@ echo "The application is now running with the latest code and your production da
echo "" echo ""
echo "Useful commands:" echo "Useful commands:"
echo " pm2 logs harheimertc # View logs (Port 3100)" echo " pm2 logs harheimertc # View logs (Port 3100)"
echo " pm2 logs harheimertc-3102 # View logs (Port 3102)"
echo " pm2 status # View status" echo " pm2 status # View status"
echo " pm2 restart harheimertc # Restart instance on port 3100" echo " pm2 restart harheimertc # Restart instance on port 3100"
echo " pm2 restart harheimertc-3102 # Restart instance on port 3102"
echo " pm2 restart all # Restart all instances"

View File

@@ -567,9 +567,10 @@ restart_pm2_instance() {
# Starte/Neustarte Test-Instanz # Starte/Neustarte Test-Instanz
if ! restart_pm2_instance "harheimertc.test"; then if ! restart_pm2_instance "harheimertc.test"; then
echo "" echo ""
echo "WARNING: PM2-Instanz konnte nicht gestartet werden. Bitte manuell prüfen:" echo "ERROR: PM2-Instanz konnte nicht gestartet werden."
echo " pm2 status" echo " pm2 status"
echo " pm2 logs harheimertc.test" echo " pm2 logs harheimertc.test"
exit 1
fi fi
# Prüfe, ob der Prozess läuft # Prüfe, ob der Prozess läuft
@@ -580,9 +581,16 @@ echo " Checking PM2 instance status..."
if pm2 describe harheimertc.test | grep -q "online"; then if pm2 describe harheimertc.test | grep -q "online"; then
echo " ✓ PM2-Prozess 'harheimertc.test' läuft (online)" echo " ✓ PM2-Prozess 'harheimertc.test' läuft (online)"
else else
echo " WARNING: PM2-Prozess 'harheimertc.test' ist nicht online. Prüfe Logs: pm2 logs harheimertc.test" echo " ERROR: PM2-Prozess 'harheimertc.test' ist nicht online. Prüfe Logs: pm2 logs harheimertc.test"
exit 1
fi fi
if ! curl --fail --silent --show-error --max-time 15 http://127.0.0.1:3102/ >/dev/null; then
echo "ERROR: Test-Instanz antwortet nicht auf Port 3102."
exit 1
fi
echo " ✓ HTTP-Healthcheck auf Port 3102 erfolgreich"
echo "" echo ""
echo "=== Test-Instanz Deployment completed successfully! ===" echo "=== Test-Instanz Deployment completed successfully! ==="
echo "The test application is now running with the latest code and your test data preserved." echo "The test application is now running with the latest code and your test data preserved."

View File

@@ -61,22 +61,6 @@ module.exports = {
out_file: '/var/log/pm2/harheimertc-out.log', out_file: '/var/log/pm2/harheimertc-out.log',
log_file: '/var/log/pm2/harheimertc-combined.log', log_file: '/var/log/pm2/harheimertc-combined.log',
time: true time: true
},
{
name: 'harheimertc-3102',
// Zweite Instanz auf Port 3102
script: 'node',
args: '.output/server/index.mjs',
cwd: '/var/www/harheimertc',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: createEnv(3102),
error_file: '/var/log/pm2/harheimertc-3102-error.log',
out_file: '/var/log/pm2/harheimertc-3102-out.log',
log_file: '/var/log/pm2/harheimertc-3102-combined.log',
time: true
} }
] ]
} }

View File

@@ -58,7 +58,7 @@ module.exports = {
autorestart: true, autorestart: true,
watch: false, watch: false,
max_memory_restart: '1G', max_memory_restart: '1G',
env: createEnv(process.env.PORT || 3102), env: createEnv(3102),
error_file: '/var/log/pm2/harheimertc.test-error.log', error_file: '/var/log/pm2/harheimertc.test-error.log',
out_file: '/var/log/pm2/harheimertc.test-out.log', out_file: '/var/log/pm2/harheimertc.test-out.log',
log_file: '/var/log/pm2/harheimertc.test-combined.log', log_file: '/var/log/pm2/harheimertc.test-combined.log',

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "harheimertc-website", "name": "harheimertc-website",
"version": "1.8.4", "version": "1.8.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "harheimertc-website", "name": "harheimertc-website",
"version": "1.8.4", "version": "1.8.7",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.11.2", "@pinia/nuxt": "^0.11.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "harheimertc-website", "name": "harheimertc-website",
"version": "1.8.5", "version": "1.8.8",
"description": "Moderne Webseite für den Harheimer Tischtennis Club", "description": "Moderne Webseite für den Harheimer Tischtennis Club",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -18,7 +18,7 @@
"test": "vitest run", "test": "vitest run",
"test:data-rotation": "vitest run tests/data-file-rotation.spec.ts", "test:data-rotation": "vitest run tests/data-file-rotation.spec.ts",
"check-security": "node scripts/verify-no-public-writes.js", "check-security": "node scripts/verify-no-public-writes.js",
"smoke-local": "BASE_URL=http://127.0.0.1:3100 node scripts/smoke-tests.js", "smoke-local": "BASE_URL=http://127.0.0.1:3100 node scripts/smoke-test.js",
"sync-public-data": "node scripts/sync-public-data.js", "sync-public-data": "node scripts/sync-public-data.js",
"data-backups:list": "node scripts/data-backup-restore.js list", "data-backups:list": "node scripts/data-backup-restore.js list",
"data-backups:restore": "node scripts/data-backup-restore.js restore", "data-backups:restore": "node scripts/data-backup-restore.js restore",

View File

@@ -362,7 +362,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { rowMatchesTeamFilter } from '../../utils/spielplan-filter.js' import { rowMatchesTeamFilter } from '~/utils/spielplan-filter.js'
const route = useRoute() const route = useRoute()

View File

@@ -360,7 +360,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { filterSpielplanRows, toApiTeamParam } from '../../utils/spielplan-filter.js' import { filterSpielplanRows, toApiTeamParam } from '~/utils/spielplan-filter.js'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -1,18 +1,15 @@
import { listSpielplanSeasons, readSpielplanData, validateSeasonSlug } from '../../utils/spielplan-data.js' import { listSpielplanSeasons, readSpielplanData, validateSeasonSlug } from '../../utils/spielplan-data.js'
import { formatTeamDisplayName } from '~/utils/team-display.js'
function teamLabel(teamName, teamAgeGroup) { function teamLabel(teamName, teamAgeGroup, competitionAgeGroup) {
const name = String(teamName || '').trim() return formatTeamDisplayName(teamName, teamAgeGroup, competitionAgeGroup)
const age = String(teamAgeGroup || '').trim()
if (!name) return ''
const isYouth = age.toLowerCase().includes('jugend') || name.toLowerCase().includes('jugend')
return isYouth ? `(J) ${name}` : name
} }
function extractHarheimerTeams(rows) { function extractHarheimerTeams(rows) {
const seen = new Set() const seen = new Set()
const teams = [] const teams = []
const addTeam = (teamName, teamAgeGroup) => { const addTeam = (teamName, teamAgeGroup, competitionAgeGroup) => {
const name = String(teamName || '').trim() const name = String(teamName || '').trim()
if (!name) return if (!name) return
const age = String(teamAgeGroup || '').trim() const age = String(teamAgeGroup || '').trim()
@@ -21,7 +18,7 @@ function extractHarheimerTeams(rows) {
seen.add(key) seen.add(key)
teams.push({ teams.push({
key, key,
label: teamLabel(name, age), label: teamLabel(name, age, competitionAgeGroup),
teamName: name, teamName: name,
teamAgeGroup: age teamAgeGroup: age
}) })
@@ -29,10 +26,10 @@ function extractHarheimerTeams(rows) {
for (const row of rows || []) { for (const row of rows || []) {
if (String(row.HeimVereinName || '').trim() === 'Harheimer TC') { if (String(row.HeimVereinName || '').trim() === 'Harheimer TC') {
addTeam(row.HeimMannschaft, row.HeimMannschaftAltersklasse) addTeam(row.HeimMannschaft, row.HeimMannschaftAltersklasse, row.Altersklasse)
} }
if (String(row.GastVereinName || '').trim() === 'Harheimer TC') { if (String(row.GastVereinName || '').trim() === 'Harheimer TC') {
addTeam(row.GastMannschaft, row.GastMannschaftAltersklasse) addTeam(row.GastMannschaft, row.GastMannschaftAltersklasse, row.Altersklasse)
} }
} }
@@ -58,4 +55,4 @@ export default defineEventHandler(async (event) => {
seasons, seasons,
teams: extractHarheimerTeams(dataResult.data) teams: extractHarheimerTeams(dataResult.data)
} }
}) })

View File

@@ -2,7 +2,7 @@ import fs from 'fs/promises'
import path from 'path' import path from 'path'
import { readSpielplanData, validateSeasonSlug } from '../../utils/spielplan-data.js' import { readSpielplanData, validateSeasonSlug } from '../../utils/spielplan-data.js'
import { info as loggerInfo, error as loggerError } from '../../utils/logger.js' import { info as loggerInfo, error as loggerError } from '../../utils/logger.js'
import { filterSpielplanRows } from '../../../utils/spielplan-filter.js' import { filterSpielplanRows } from '~/utils/spielplan-filter.js'
function seasonSlugToLabel(slug) { function seasonSlugToLabel(slug) {
const match = String(slug || '').match(/^(\d{2})--(\d{2})$/) const match = String(slug || '').match(/^(\d{2})--(\d{2})$/)

View File

@@ -83,6 +83,7 @@ describe('Auth API Endpoints', () => {
afterEach(() => { afterEach(() => {
delete process.env.NODE_ENV delete process.env.NODE_ENV
delete process.env.APP_ENV delete process.env.APP_ENV
delete process.env.DEBUG
delete process.env.NUXT_PUBLIC_BASE_URL delete process.env.NUXT_PUBLIC_BASE_URL
delete process.env.PASSWORD_RESET_TTL_MIN delete process.env.PASSWORD_RESET_TTL_MIN
}) })
@@ -282,6 +283,7 @@ describe('Auth API Endpoints', () => {
it('benachrichtigt in Testumgebung nicht die Vorstand-Empfänger', async () => { it('benachrichtigt in Testumgebung nicht die Vorstand-Empfänger', async () => {
process.env.NODE_ENV = 'production' process.env.NODE_ENV = 'production'
process.env.APP_ENV = 'test' process.env.APP_ENV = 'test'
delete process.env.DEBUG
const event = createEvent() const event = createEvent()
mockSuccessReadBody({ mockSuccessReadBody({

30
utils/team-display.js Normal file
View File

@@ -0,0 +1,30 @@
/**
* Makes youth teams identifiable while retaining the team name supplied by
* click-TT. Age groups occur in a few forms, for example "Jugend 13",
* "Jugend 13 1. Kreisklasse", and "J13". Some imports provide the age
* only for the whole fixture, therefore `competitionAgeGroup` is considered
* as a fallback.
*/
export function youthTeamCode(teamAgeGroup, teamName = '', competitionAgeGroup = '') {
const ageGroup = [teamAgeGroup, competitionAgeGroup]
.map(value => String(value || '').trim())
.filter(Boolean)
.join(' ')
const name = String(teamName || '').trim()
const codeMatch = `${ageGroup} ${name}`.match(/\b(?:jugend\s*|j\s*\(?)(\d{1,2})\b/i)
if (codeMatch) return `J${codeMatch[1]}`
if (/\bjugend\b/i.test(ageGroup) || /\bjugend\b/i.test(name)) return 'J'
return ''
}
export function formatTeamDisplayName(teamName, teamAgeGroup, competitionAgeGroup = '') {
const rawName = String(teamName || '').trim()
if (!rawName) return ''
const youthCode = youthTeamCode(teamAgeGroup, rawName, competitionAgeGroup)
const name = youthCode
? rawName.replace(/\s*\(J\d{1,2}\)\s*$/i, '').trim()
: rawName
return youthCode ? `(${youthCode}) ${name}` : name
}