Enhance backend configuration and error handling: Update CORS settings to allow dynamic origins, improve RabbitMQ connection handling in chat services, and adjust API server host configuration. Refactor environment variables for better flexibility and add fallback mechanisms for WebSocket and chat services. Update frontend environment files for consistent API and WebSocket URLs.

This commit is contained in:
Torsten Schulz (local)
2026-03-18 22:45:22 +01:00
parent 59869e077e
commit 4442937ebd
29 changed files with 1226 additions and 396 deletions

View File

@@ -1,10 +1,12 @@
<template>
<main class="contenthidden">
<div class="contentscroll">
<main class="app-content contenthidden">
<div class="app-content__scroll contentscroll">
<div class="app-content__inner">
<router-view></router-view>
</div>
</main>
</template>
</div>
</main>
</template>
<script>
export default {
@@ -12,15 +14,31 @@
};
</script>
<style scoped>
main {
padding: 0;
background-color: #ffffff;
flex: 1;
<style scoped>
.app-content {
flex: 1;
min-height: 0;
padding: 0;
overflow: hidden;
}
.app-content__scroll {
background: transparent;
min-height: 0;
}
.app-content__inner {
max-width: var(--shell-max-width);
height: 100%;
min-height: 0;
margin: 0 auto;
padding: 14px 18px;
}
@media (max-width: 960px) {
.app-content__inner {
padding: 12px;
}
}
</style>
.contentscroll {
padding: 20px;
}
</style>

View File

@@ -1,18 +1,23 @@
<template>
<footer>
<div class="logo" @click="showFalukantDaemonStatus"><img src="/images/icons/logo_color.png"></div>
<div class="window-bar">
<footer class="app-footer">
<div class="app-footer__inner">
<button class="footer-brand" type="button" @click="showFalukantDaemonStatus">
<img src="/images/icons/logo_color.png" alt="YourPart" />
<span>System</span>
</button>
<div class="window-bar">
<button v-for="dialog in openDialogs" :key="dialog.dialog.name" class="dialog-button"
@click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.localTitle">
<img v-if="dialog.dialog.icon" :src="'/images/icons/' + dialog.dialog.icon" />
<span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.localTitle) :
dialog.dialog.localTitle }}</span>
</button>
</div>
<div class="static-block">
<a href="#" @click.prevent="openImprintDialog">{{ $t('imprint.button') }}</a>
<a href="#" @click.prevent="openDataPrivacyDialog">{{ $t('dataPrivacy.button') }}</a>
<a href="#" @click.prevent="openContactDialog">{{ $t('contact.button') }}</a>
</div>
<div class="static-block">
<a href="#" @click.prevent="openImprintDialog">{{ $t('imprint.button') }}</a>
<a href="#" @click.prevent="openDataPrivacyDialog">{{ $t('dataPrivacy.button') }}</a>
<a href="#" @click.prevent="openContactDialog">{{ $t('contact.button') }}</a>
</div>
</div>
</footer>
</template>
@@ -63,18 +68,47 @@ export default {
</script>
<style scoped>
footer {
display: flex;
background-color: var(--color-primary-green);
height: 38px;
width: 100%;
color: #1F2937; /* Dunkles Grau für besseren Kontrast auf hellem Grün */
.app-footer {
flex: 0 0 auto;
padding: 0;
}
.logo,
.window-bar,
.static-block {
text-align: center;
.app-footer__inner {
max-width: var(--shell-max-width);
margin: 0 auto;
display: flex;
align-items: center;
gap: 16px;
min-height: 44px;
padding: 6px 12px;
border-radius: 0;
background:
linear-gradient(180deg, rgba(242, 248, 243, 0.96) 0%, rgba(224, 238, 227, 0.98) 100%);
border-top: 1px solid rgba(120, 195, 138, 0.28);
box-shadow: 0 -6px 18px rgba(93, 64, 55, 0.06);
}
.footer-brand {
min-height: 32px;
padding: 0 10px 0 8px;
background: rgba(120, 195, 138, 0.12);
border: 1px solid rgba(120, 195, 138, 0.22);
color: #24523a;
box-shadow: none;
}
.footer-brand:hover {
background: rgba(120, 195, 138, 0.18);
box-shadow: none;
}
.footer-brand img {
width: 22px;
height: 22px;
}
.footer-brand span {
font-weight: 700;
}
.window-bar {
@@ -83,24 +117,25 @@ footer {
align-items: center;
justify-content: flex-start;
gap: 10px;
padding-left: 10px;
overflow: auto;
}
.dialog-button {
max-width: 12em;
max-width: 15em;
min-height: 30px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
background: none;
height: 1.8em;
border: 1px solid #0a4337;
box-shadow: 1px 1px 2px #484949;
padding: 0 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
color: var(--color-text-primary);
border: 1px solid rgba(120, 195, 138, 0.18);
box-shadow: none;
}
.dialog-button:hover {
background: rgba(255, 255, 255, 0.92);
}
.dialog-button>img {
@@ -111,16 +146,35 @@ footer {
margin-left: 5px;
}
.logo>img {
width: 36px;
height: 36px;
}
.static-block {
line-height: 38px;
display: flex;
align-items: center;
gap: 18px;
white-space: nowrap;
}
.static-block>a {
padding-right: 1.5em;
color: #42634e;
font-weight: 600;
}
</style>
.static-block > a:hover {
color: #24523a;
}
@media (max-width: 960px) {
.app-footer__inner {
flex-wrap: wrap;
}
.window-bar,
.static-block {
width: 100%;
}
.static-block {
justify-content: space-between;
gap: 12px;
}
}
</style>

View File

@@ -1,15 +1,25 @@
<template>
<header>
<div class="logo"><img src="/images/logos/logo.png" /></div>
<div class="advertisement">Advertisement</div>
<div class="connection-status" v-if="isLoggedIn">
<div class="status-indicator" :class="backendStatusClass">
<span class="status-dot"></span>
<span class="status-text">B</span>
<header class="app-header">
<div class="app-header__inner">
<div class="brand">
<div class="logo"><img src="/images/logos/logo.png" alt="YourPart" /></div>
<div class="brand-copy">
<strong>YourPart</strong>
<span>Community, Spiele und Lernen auf einer Plattform</span>
</div>
</div>
<div class="status-indicator" :class="daemonStatusClass">
<span class="status-dot"></span>
<span class="status-text">D</span>
<div class="header-meta">
<div class="header-pill">Beta</div>
<div class="connection-status" v-if="isLoggedIn">
<div class="status-indicator" :class="backendStatusClass">
<span class="status-dot"></span>
<span class="status-text">Backend</span>
</div>
<div class="status-indicator" :class="daemonStatusClass">
<span class="status-dot"></span>
<span class="status-text">Daemon</span>
</div>
</div>
</div>
</div>
</header>
@@ -43,43 +53,107 @@ export default {
</script>
<style scoped>
header {
.app-header {
position: relative;
flex: 0 0 auto;
padding: 8px 14px;
background:
linear-gradient(180deg, rgba(255, 248, 236, 0.94) 0%, rgba(247, 235, 216, 0.98) 100%);
color: #2b1f14;
border-bottom: 1px solid rgba(93, 64, 55, 0.12);
box-shadow: 0 6px 18px rgba(93, 64, 55, 0.08);
}
.app-header__inner {
max-width: var(--shell-max-width);
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
background-color: #f8a22b;
gap: 16px;
}
.logo, .title, .advertisement {
text-align: center;
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.advertisement {
flex: 1;
.logo {
width: 42px;
height: 42px;
padding: 6px;
border-radius: 14px;
background:
linear-gradient(180deg, rgba(248, 162, 43, 0.2) 0%, rgba(255, 255, 255, 0.7) 100%);
border: 1px solid rgba(248, 162, 43, 0.22);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55);
}
.logo > img {
max-height: 50px;
width: 100%;
height: 100%;
object-fit: contain;
}
.brand-copy {
display: flex;
flex-direction: column;
gap: 1px;
}
.brand-copy strong {
font-size: 1.05rem;
line-height: 1.1;
color: #3a2a1b;
}
.brand-copy span {
font-size: 0.8rem;
color: rgba(95, 75, 57, 0.88);
}
.header-meta {
display: flex;
align-items: center;
gap: 12px;
}
.header-pill {
padding: 5px 10px;
border-radius: 999px;
background: rgba(248, 162, 43, 0.12);
border: 1px solid rgba(248, 162, 43, 0.24);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #8a5411;
}
.connection-status {
display: flex;
align-items: center;
margin-left: 10px;
gap: 5px;
gap: 8px;
}
.status-indicator {
display: flex;
align-items: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 6pt;
font-weight: 500;
gap: 8px;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
border: 1px solid rgba(93, 64, 55, 0.1);
background: rgba(255, 255, 255, 0.62);
}
.status-dot {
width: 6px;
height: 6px;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
animation: pulse 2s infinite;
}
@@ -100,23 +174,23 @@ header {
}
.status-connected {
background-color: rgba(76, 175, 80, 0.1);
color: #2e7d32;
background-color: rgba(76, 175, 80, 0.12);
color: #245b2c;
}
.status-connecting {
background-color: rgba(255, 152, 0, 0.1);
color: #f57c00;
background-color: rgba(255, 152, 0, 0.12);
color: #8b5e0d;
}
.status-disconnected {
background-color: rgba(244, 67, 54, 0.1);
color: #d32f2f;
background-color: rgba(244, 67, 54, 0.12);
color: #8f2c27;
}
.status-error {
background-color: rgba(244, 67, 54, 0.1);
color: #d32f2f;
background-color: rgba(244, 67, 54, 0.12);
color: #8f2c27;
}
@keyframes pulse {
@@ -124,4 +198,24 @@ header {
50% { opacity: 0.5; }
100% { opacity: 1; }
}
@media (max-width: 960px) {
.app-header {
padding: 6px 10px;
}
.app-header__inner {
gap: 8px;
flex-wrap: wrap;
}
.header-meta {
justify-content: space-between;
flex-wrap: wrap;
}
.brand-copy span {
font-size: 0.76rem;
}
}
</style>

View File

@@ -15,8 +15,8 @@
>&nbsp;</span>
<span>{{ $t(`navigation.${key}`) }}</span>
<!-- Untermenü Ebene 1 -->
<ul v-if="item.children" class="submenu1">
<!-- Untermenü Ebene 1 -->
<ul v-if="hasTopLevelSubmenu(item)" class="submenu1">
<li
v-for="(subitem, subkey) in item.children"
:key="subkey"
@@ -29,7 +29,7 @@
>&nbsp;</span>
<span>{{ subitem?.label || $t(`navigation.m-${key}.${subkey}`) }}</span>
<span
v-if="subkey === 'forum' || subkey === 'vocabtrainer' || subitem.children"
v-if="hasSecondLevelSubmenu(subitem, subkey)"
class="subsubmenu"
>&#x25B6;</span>
@@ -183,6 +183,34 @@ export default {
methods: {
...mapActions(['loadMenu', 'logout']),
hasChildren(item) {
if (!item?.children) {
return false;
}
if (Array.isArray(item.children)) {
return item.children.length > 0;
}
return Object.keys(item.children).length > 0;
},
hasTopLevelSubmenu(item) {
return this.hasChildren(item) || (item?.showLoggedinFriends === 1 && this.friendsList.length > 0);
},
hasSecondLevelSubmenu(subitem, subkey) {
if (subkey === 'forum') {
return this.forumList.length > 0;
}
if (subkey === 'vocabtrainer') {
return this.vocabLanguagesList.length > 0;
}
return this.hasChildren(subitem);
},
openMultiChat() {
// Räume können später dynamisch geladen werden, hier als Platzhalter ein Beispiel:
const exampleRooms = [
@@ -254,7 +282,7 @@ export default {
event.stopPropagation();
// 1) nur aufklappen, wenn es echte Untermenüs gibt (nicht bei leerem children wie bei Startseite)
if (item.children && Object.keys(item.children).length > 0) return;
if (this.hasChildren(item)) return;
// 2) view → Dialog/Window
if (item.view) {
@@ -295,8 +323,6 @@ nav,
nav > ul {
display: flex;
justify-content: space-between;
background-color: #f8a22b;
color: #000;
padding: 0;
margin: 0;
cursor: pointer;
@@ -304,6 +330,28 @@ nav > ul {
z-index: 999;
}
nav {
max-width: var(--shell-max-width);
margin: 0 auto;
align-items: stretch;
gap: 10px;
padding: 6px 12px;
border-radius: 0;
background: var(--color-primary-orange-light);
border-top: 1px solid rgba(93, 64, 55, 0.08);
border-bottom: 1px solid rgba(93, 64, 55, 0.12);
box-shadow: none;
color: var(--color-text-primary);
}
nav > ul {
flex: 1;
align-items: center;
gap: 6px;
background: transparent;
flex-wrap: wrap;
}
ul {
list-style-type: none;
padding: 0;
@@ -311,18 +359,23 @@ ul {
}
nav > ul > li {
padding: 0 1em;
line-height: 2.5em;
transition: background-color 0.25s;
display: flex;
align-items: center;
min-height: 36px;
padding: 0 12px;
line-height: 1;
border-radius: 999px;
transition: background-color 0.25s, color 0.25s, transform 0.2s;
}
nav > ul > li:hover {
background-color: #f8a22b;
background-color: rgba(248, 162, 43, 0.16);
white-space: nowrap;
transform: translateY(-1px);
}
nav > ul > li:hover > span {
color: #000;
color: var(--color-primary);
}
nav > ul > li:hover > ul {
@@ -335,17 +388,22 @@ a {
.right-block {
display: flex;
gap: 10px;
align-items: center;
gap: 12px;
padding-left: 8px;
}
.logoutblock {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.menuitem {
cursor: pointer;
color: #5D4037;
color: var(--color-primary);
font-weight: 700;
}
.mailbox {
@@ -353,20 +411,29 @@ a {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
padding-left: 24px;
width: 40px;
height: 40px;
text-align: left;
border-radius: 999px;
background-color: rgba(120, 195, 138, 0.12);
border: 1px solid rgba(93, 64, 55, 0.1);
}
.mainmenuitem {
position: relative;
font-weight: 700;
}
.submenu1 {
position: absolute;
border: 1px solid #5D4037;
background-color: #f8a22b;
border: 1px solid rgba(93, 64, 55, 0.12);
background: rgba(255, 252, 247, 0.98);
left: 0;
top: 2.5em;
top: calc(100% + 10px);
min-width: 220px;
padding: 8px;
border-radius: 18px;
box-shadow: 0 10px 18px rgba(93, 64, 55, 0.12);
max-height: 0;
overflow: visible;
opacity: 0;
@@ -386,15 +453,16 @@ a {
}
.submenu1 > li {
padding: 0.5em;
padding: 0.75em 0.9em;
line-height: 1em;
color: #5D4037;
color: var(--color-text-secondary);
position: relative;
border-radius: 14px;
}
.submenu1 > li:hover {
color: #000;
background-color: #f8a22b;
color: var(--color-text-primary);
background-color: rgba(248, 162, 43, 0.12);
}
.menu-icon,
@@ -407,7 +475,7 @@ a {
.menu-icon {
width: 24px;
height: 24px;
margin-right: 3px;
margin-right: 8px;
}
.submenu-icon {
@@ -419,10 +487,14 @@ a {
.submenu2 {
position: absolute;
background-color: #f8a22b;
left: 100%;
background: rgba(255, 252, 247, 0.98);
left: calc(100% + 8px);
top: 0;
border: 1px solid #5D4037;
min-width: 220px;
padding: 8px;
border-radius: 18px;
border: 1px solid rgba(71, 52, 35, 0.12);
box-shadow: 0 10px 18px rgba(93, 64, 55, 0.12);
max-height: 0;
overflow: hidden;
opacity: 0;
@@ -442,14 +514,15 @@ a {
}
.submenu2 > li {
padding: 0.5em;
padding: 0.75em 0.9em;
line-height: 1em;
color: #5D4037;
color: var(--color-text-secondary);
border-radius: 14px;
}
.submenu2 > li:hover {
color: #000;
background-color: #f8a22b;
color: var(--color-text-primary);
background-color: rgba(120, 195, 138, 0.14);
}
.subsubmenu {
@@ -457,4 +530,37 @@ a {
font-size: 8pt;
margin-right: -4px;
}
.username {
font-weight: 800;
}
@media (max-width: 960px) {
nav {
margin: 0;
flex-direction: column;
padding: 8px 10px;
}
nav > ul,
.right-block {
width: 100%;
}
.right-block {
justify-content: space-between;
padding-left: 0;
}
.logoutblock {
align-items: flex-start;
}
.submenu1,
.submenu2 {
position: static;
min-width: 100%;
margin-top: 8px;
}
}
</style>

View File

@@ -1,5 +1,13 @@
<template>
<div ref="container" class="character-3d-container"></div>
<div class="character-3d-shell">
<div v-show="!showFallback" ref="container" class="character-3d-container"></div>
<img
v-if="showFallback"
class="character-fallback"
:src="fallbackImageSrc"
:alt="`Character ${actualGender}`"
/>
</div>
</template>
<script>
@@ -41,7 +49,8 @@ export default {
animationId: null,
mixer: null,
clock: markRaw(new THREE.Clock()),
baseYPosition: 0 // Basis-Y-Position für Animation
baseYPosition: 0,
showFallback: false
};
},
computed: {
@@ -93,6 +102,11 @@ export default {
const base = getApiBaseURL();
const prefix = base ? `${base}${MODELS_API_PATH}` : MODELS_API_PATH;
return `${prefix}/${this.actualGender}_${age}y.glb`;
},
fallbackImageSrc() {
return this.actualGender === 'female'
? '/images/mascot/mascot_female.png'
: '/images/mascot/mascot_male.png';
}
},
watch: {
@@ -115,6 +129,7 @@ export default {
init3D() {
const container = this.$refs.container;
if (!container) return;
this.showFallback = false;
// Scene erstellen - markRaw verwenden, um Vue's Reactivity zu vermeiden
this.scene = markRaw(new THREE.Scene());
@@ -301,6 +316,7 @@ export default {
}
} catch (error) {
console.error('Error loading 3D model:', error);
this.showFallback = true;
}
},
@@ -375,10 +391,25 @@ export default {
</script>
<style scoped>
.character-3d-shell {
width: 100%;
height: 100%;
min-height: 0;
position: relative;
}
.character-3d-container {
width: 100%;
height: 100%;
min-height: 0;
position: relative;
overflow: hidden;
}
.character-fallback {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center bottom;
}
</style>

View File

@@ -23,11 +23,19 @@
<img :src="'/images/icons/falukant/relationship-' + item.image + '.png'" class="relationship-icon" :title="$t(`falukant.statusbar.${item.key}`)" />
</div>
</template>
<span v-if="statusItems.length > 0 && menu.falukant && menu.falukant.children">
<div
v-if="statusItems.length > 0 && menu.falukant && menu.falukant.children"
class="quick-access"
>
<template v-for="(menuItem, key) in menu.falukant.children" :key="menuItem.id" >
<img :src="'/images/icons/falukant/shortmap/' + key + '.png'" class="menu-icon" @click="openPage(menuItem)" :title="$t(`navigation.m-falukant.${key}`)" />
<img
:src="'/images/icons/falukant/shortmap/' + key + '.png'"
class="menu-icon"
@click="openPage(menuItem)"
:title="$t(`navigation.m-falukant.${key}`)"
/>
</template>
</span>
</div>
<MessagesDialog ref="msgs" />
</div>
</template>
@@ -220,13 +228,18 @@ export default {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
background-color: #f4f4f4;
border: 1px solid #ccc;
border-radius: 4px;
width: calc(100% + 40px);
box-sizing: border-box;
width: 100%;
max-width: 100%;
gap: 1.2em;
margin: -21px -20px 1.5em -20px;
position: fixed;
padding: 0.4rem 0.75rem;
margin: 0 0 1.5em 0;
position: sticky;
top: 0;
z-index: 100;
}
@@ -237,6 +250,14 @@ export default {
align-items: center;
}
.quick-access {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.2rem;
}
.status-icon-wrapper {
display: inline-flex;
align-items: center;
@@ -254,6 +275,8 @@ export default {
.menu-icon {
width: 30px;
height: 30px;
display: block;
flex: 0 0 auto;
cursor: pointer;
padding: 4px 2px 0 0;
}