fix(frontend): Vite-Build reparieren und Unterpfad-Deployment korrigieren

Der Build läuft über Vite ("build": "vite build"), der Code las
Umgebungsvariablen aber über process.env – das gibt es im Browser-Bundle
nicht. Damit war der Build-Arg REACT_APP_API_URL wirkungslos und der
Zugriff selbst lief in "process is not defined".

- constants.js nutzt import.meta.env und leitet den Deployment-Basispfad
  aus import.meta.env.BASE_URL ab (Fallback: Laufzeit-Erkennung).
  REACT_APP_* heißt überall VITE_* (Dockerfile, Compose, .env.example,
  Doku).
- /verwaltung und /passwort-zuruecksetzen wurden gegen
  window.location.pathname ohne Basispfad verglichen und trafen unter
  /nachsuche/ nie zu – die Passwort-Reset-Seite war produktiv nicht
  erreichbar. Neue Helfer withBase()/normalizePath().
- Der Service Worker wurde als '/sw.js' registriert und war damit der des
  Portals mit Scope '/'. Registrierung läuft jetzt über BASE_URL, inkl.
  passendem Scope.
- index.html verlinkte '/manifest.json' absolut, also das Portal-Manifest:
  die App hätte sich als Portal installiert. Jetzt %BASE_URL%.
- public/index.html (CRA-Rest) kollidierte im Vite-Build mit der
  index.html im Projektwurzelverzeichnis und wurde entfernt.
- manifest.json verwies auf ein favicon.ico, das es nicht gibt.
- package.json: Scripts auf Vite umgestellt. react-scripts,
  @testing-library/* und web-vitals entfernt – nichts davon wird
  importiert, und react-scripts stand im Widerspruch zum Build.
- Die ungenutzten Konstanten USER_TYPES/USER_TYPE_LABELS/RULES sind
  entfallen (die echten Werte kommen aus der Datenbank).
- Header bekam eine Prop onXLogin, die er gar nicht entgegennimmt und die
  den Logout-Handler durchreichte.

Verifiziert: alle drei Frontends bauen, im Bundle kein process.env mehr,
alle Pfade korrekt auf den jeweiligen Basispfad.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
thomas 2026-08-25 08:10:55 +02:00
parent 718d84a545
commit 63f6d97318
42 changed files with 451 additions and 57086 deletions

View File

@ -139,7 +139,7 @@ Im Frontend wird die API-URL zur **Build-Zeit** gesetzt. Für Production:
frontend:
build:
args:
- REACT_APP_API_URL=https://api.yourdomain.com
- VITE_API_URL=https://api.yourdomain.com
```
2. Rebuild erforderlich:

View File

@ -61,7 +61,7 @@ services:
context: ./frontend
args:
- PUBLIC_URL=/drohnenfuehrer/
- REACT_APP_API_URL=${REACT_APP_API_URL:-}
- VITE_API_URL=${VITE_API_URL:-}
container_name: drohnenfuehrer-frontend
restart: unless-stopped
ports:

View File

@ -183,4 +183,4 @@ frontend/src/
- `CORS_ORIGIN`: Erlaubter CORS-Origin
### Frontend (.env)
- `REACT_APP_API_URL`: Backend-API-URL (Standard: http://localhost:5000)
- `VITE_API_URL`: Backend-API-URL (Standard: http://localhost:5000)

View File

@ -1,9 +1,9 @@
# API Configuration
# For local development
REACT_APP_API_URL=http://localhost:5000
VITE_API_URL=http://localhost:5000
# For production, use your actual backend URL
# REACT_APP_API_URL=https://api.yourdomain.com
# VITE_API_URL=https://api.yourdomain.com
# Admin path (optional, defaults to /verwaltung)
# REACT_APP_ADMIN_PATH=/verwaltung
# VITE_ADMIN_PATH=/verwaltung

View File

@ -10,8 +10,8 @@ COPY . .
ARG PUBLIC_URL=/drohnenfuehrer/
ENV PUBLIC_URL=$PUBLIC_URL
ARG REACT_APP_API_URL=
ENV REACT_APP_API_URL=$REACT_APP_API_URL
ARG VITE_API_URL=
ENV VITE_API_URL=$VITE_API_URL
# Limit Node.js heap during build to prevent OOM kills
ENV NODE_OPTIONS="--max_old_space_size=512"

View File

@ -1,8 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%BASE_URL%favicon.ico" />
<link rel="icon" href="%BASE_URL%icons/icon-192.png" />
<link rel="apple-touch-icon" href="%BASE_URL%icons/icon-192.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2d6a2d" />
@ -10,7 +10,9 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Drohnenführer" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="/manifest.json" />
<!-- Pfad relativ zur Deployment-Basis: ein absolutes "/manifest.json" wuerde
das Portal-Manifest laden und die App als Portal installieren. -->
<link rel="manifest" href="%BASE_URL%manifest.json" />
<meta
name="description"
content="Drohnenführer Heidekreis Übersicht der Drohnenführer"

File diff suppressed because it is too large Load Diff

View File

@ -3,28 +3,16 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.13.5",
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
"react-leaflet": "^4.2.1"
},
"scripts": {
"start": "react-scripts start",
"start": "vite",
"build": "vite build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"preview": "vite preview"
},
"browserslist": {
"production": [

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/icon-192.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2d6a2d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Drohnenführer Heidekreis" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<meta
name="description"
content="Drohnenführer Heidekreis Übersicht der Drohnenführer"
/>
<title>Drohnenführer Heidekreis</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -2,11 +2,6 @@
"short_name": "Drohnenführer",
"name": "Drohnenführer Heidekreis",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "icons/icon-192.png",
"sizes": "192x192",
@ -27,4 +22,4 @@
"theme_color": "#2d6a2d",
"background_color": "#ffffff",
"description": "Drohnenführer Heidekreis Übersicht der Drohnenführer"
}
}

View File

@ -11,13 +11,15 @@ import Admin from './pages/Admin';
import DrohnenfuehrerLogin from './components/drohnenfuehrer/DrohnenfuehrerLogin';
import DrohnenfuehrerDashboard from './components/drohnenfuehrer/DrohnenfuehrerDashboard';
import InstallBanner from './components/common/InstallBanner';
import { ADMIN_PATH, RESET_PASSWORD_PATH, withBase, normalizePath } from './utils/constants';
import './App.css';
function App() {
const adminPath = process.env.REACT_APP_ADMIN_PATH || '/verwaltung';
const resetPasswordPath = '/passwort-zuruecksetzen';
const isAdminRoute = window.location.pathname === adminPath;
const isResetPasswordRoute = window.location.pathname === resetPasswordPath;
// Die Pfade muessen den Deployment-Basispfad enthalten: produktiv laeuft die
// App unter /<app>/, ein Vergleich gegen '/verwaltung' traefe dort nie zu.
const currentPath = normalizePath(window.location.pathname);
const isAdminRoute = currentPath === normalizePath(withBase(ADMIN_PATH));
const isResetPasswordRoute = currentPath === normalizePath(withBase(RESET_PASSWORD_PATH));
const [view, setView] = useState('public');
const [drohnenfuehrerUser, setDrohnenfuehrerUser] = useState(null);
const { isAuthenticated, loading: authLoading, login, logout } = useAuth();
@ -97,7 +99,6 @@ function App() {
isAdmin={false}
currentView={view}
onViewChange={handleViewChange}
onDrohnenfuehrerLogin={handleDrohnenfuehrerLogout}
/>
<main className="app-main">
{isAdminRoute || view === 'login' ? (

View File

@ -10,10 +10,14 @@ root.render(
</React.StrictMode>
);
// Register service worker
// Register service worker.
// BASE_URL endet immer auf '/' und ist bei Unterpfad-Deployments '/nachsuche/'.
// Ein absolutes '/sw.js' würde stattdessen den Service Worker des Portals
// registrieren und dessen Scope übernehmen.
if ('serviceWorker' in navigator) {
const base = import.meta.env.BASE_URL || '/';
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js')
navigator.serviceWorker.register(`${base}sw.js`, { scope: base })
.then(registration => {
console.log('SW registered: ', registration);
})
@ -21,4 +25,4 @@ if ('serviceWorker' in navigator) {
console.log('SW registration failed: ', registrationError);
});
});
}
}

View File

@ -1,34 +1,34 @@
// Use configured API URL when set, otherwise auto-detect common subpath deployment (/drohnenfuehrer)
// Der Build läuft über Vite: Umgebungsvariablen kommen aus import.meta.env und
// müssen mit VITE_ beginnen. `process.env` existiert im Browser-Bundle NICHT
// ein Zugriff darauf wirft "process is not defined" und die App bleibt weiß.
const env = import.meta.env;
// Fallback, falls ein Build ohne PUBLIC_URL/VITE_BASE_PATH unter einem Unterpfad
// ausgeliefert wird: dann steht in BASE_URL nur '/'.
const detectRuntimeBasePath = () => {
if (typeof window === 'undefined') return '';
const path = window.location.pathname || '';
if (path === '/drohnenfuehrer' || path.startsWith('/drohnenfuehrer/')) {
return '/drohnenfuehrer';
}
return '';
const knownBasePaths = ['/nachsuche', '/drohnenfuehrer', '/stoeberhunde'];
const match = knownBasePaths.find(basePath => path === basePath || path.startsWith(`${basePath}/`));
return match || '';
};
const configuredApiBaseUrl = process.env.REACT_APP_API_URL;
export const API_BASE_URL = (typeof configuredApiBaseUrl === 'string' && configuredApiBaseUrl.trim().length > 0)
? configuredApiBaseUrl
: detectRuntimeBasePath();
const stripTrailingSlash = (value) => String(value || '').replace(/\/+$/, '');
export const USER_TYPES = {
DF: 'DF',
WK: 'WK',
RGB: 'RGB'
};
// Basis-Pfad des Deployments ohne abschließenden Slash: '/nachsuche' bzw. '' im Root.
export const BASE_PATH = stripTrailingSlash(env.BASE_URL) || detectRuntimeBasePath();
export const USER_TYPE_LABELS = {
[USER_TYPES.DF]: 'Drohnenführer',
[USER_TYPES.WK]: 'Wärmebildkamera',
[USER_TYPES.RGB]: 'RGB-Kamera'
};
// Vollständige API-Basis. VITE_API_URL überschreibt (z. B. eigene API-Domain).
export const API_BASE_URL = (typeof env.VITE_API_URL === 'string' && env.VITE_API_URL.trim())
? stripTrailingSlash(env.VITE_API_URL.trim())
: BASE_PATH;
export const RULES = [
"Drohnenflug nur mit gültigem Drohnenführerschein (A1/A3 oder A2).",
"Informieren Sie den zuständigen Revierinhaber vor jedem Einsatz.",
"Halten Sie die Datenschutzbestimmungen beim Einsatz von Wärmebildkameras ein.",
"Geben Sie keine Aufnahmen ohne Zustimmung des Revierinhabers weiter.",
"Melden Sie Ihren Einsatz unverzüglich an die koordinierende Stelle."
];
export const ADMIN_PATH = env.VITE_ADMIN_PATH || '/verwaltung';
export const RESET_PASSWORD_PATH = '/passwort-zuruecksetzen';
// Hängt den Deployment-Basispfad vor einen App-Pfad: '/nachsuche/verwaltung'.
export const withBase = (appPath) =>
`${BASE_PATH}${appPath.startsWith('/') ? appPath : `/${appPath}`}`;
// Vergleichbare Normalform eines Pfads (ohne abschließenden Slash).
export const normalizePath = (pathname) => stripTrailingSlash(pathname) || '/';

View File

@ -28,7 +28,7 @@ services:
build:
context: ./frontend
args:
- REACT_APP_API_URL=http://localhost:5000
- VITE_API_URL=http://localhost:5000
container_name: drohnenfuehrer-frontend
ports:
- "8080:80"

View File

@ -139,7 +139,7 @@ Im Frontend wird die API-URL zur **Build-Zeit** gesetzt. Für Production:
frontend:
build:
args:
- REACT_APP_API_URL=https://api.yourdomain.com
- VITE_API_URL=https://api.yourdomain.com
```
2. Rebuild erforderlich:

View File

@ -63,7 +63,7 @@ services:
context: ./frontend
args:
- PUBLIC_URL=/nachsuche/
- REACT_APP_API_URL=${REACT_APP_API_URL:-}
- VITE_API_URL=${VITE_API_URL:-}
container_name: nachsuche-frontend
restart: unless-stopped
ports:

View File

@ -183,4 +183,4 @@ frontend/src/
- `CORS_ORIGIN`: Erlaubter CORS-Origin
### Frontend (.env)
- `REACT_APP_API_URL`: Backend-API-URL (Standard: http://localhost:5000)
- `VITE_API_URL`: Backend-API-URL (Standard: http://localhost:5000)

View File

@ -1,9 +1,9 @@
# API Configuration
# For local development
REACT_APP_API_URL=http://localhost:5000
VITE_API_URL=http://localhost:5000
# For production, use your actual backend URL
# REACT_APP_API_URL=https://api.yourdomain.com
# VITE_API_URL=https://api.yourdomain.com
# Admin path (optional, defaults to /verwaltung)
# REACT_APP_ADMIN_PATH=/verwaltung
# VITE_ADMIN_PATH=/verwaltung

View File

@ -10,8 +10,8 @@ COPY . .
ARG PUBLIC_URL=/nachsuche/
ENV PUBLIC_URL=$PUBLIC_URL
ARG REACT_APP_API_URL=
ENV REACT_APP_API_URL=$REACT_APP_API_URL
ARG VITE_API_URL=
ENV VITE_API_URL=$VITE_API_URL
# Limit Node.js heap during build to prevent OOM kills
ENV NODE_OPTIONS="--max_old_space_size=512"

View File

@ -1,8 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%BASE_URL%favicon.ico" />
<link rel="icon" href="%BASE_URL%icons/icon-192.png" />
<link rel="apple-touch-icon" href="%BASE_URL%icons/icon-192.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2d6a2d" />
@ -10,7 +10,9 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="NSS Heidekreis" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="/manifest.json" />
<!-- Pfad relativ zur Deployment-Basis: ein absolutes "/manifest.json" wuerde
das Portal-Manifest laden und die App als Portal installieren. -->
<link rel="manifest" href="%BASE_URL%manifest.json" />
<meta
name="description"
content="Nachsuchenstation Heidekreis Übersicht der Nachsuchenführer"

File diff suppressed because it is too large Load Diff

View File

@ -3,28 +3,16 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.13.5",
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
"react-leaflet": "^4.2.1"
},
"scripts": {
"start": "react-scripts start",
"start": "vite",
"build": "vite build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"preview": "vite preview"
},
"browserslist": {
"production": [

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/icon-192.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2d6a2d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="NSS Heidekreis" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<meta
name="description"
content="Nachsuchenstation Heidekreis Übersicht der Nachsuchenführer"
/>
<title>NSS Heidekreis</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -2,11 +2,6 @@
"short_name": "NSS Heidekreis",
"name": "Nachsuchenstation Heidekreis",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "icons/icon-192.png",
"sizes": "192x192",
@ -27,4 +22,4 @@
"theme_color": "#2d6a2d",
"background_color": "#ffffff",
"description": "Nachsuchenstation Heidekreis Übersicht der Nachsuchenführer"
}
}

View File

@ -11,13 +11,15 @@ import Admin from './pages/Admin';
import HandlerLogin from './components/handler/HandlerLogin';
import HandlerDashboard from './components/handler/HandlerDashboard';
import InstallBanner from './components/common/InstallBanner';
import { ADMIN_PATH, RESET_PASSWORD_PATH, withBase, normalizePath } from './utils/constants';
import './App.css';
function App() {
const adminPath = process.env.REACT_APP_ADMIN_PATH || '/verwaltung';
const resetPasswordPath = '/passwort-zuruecksetzen';
const isAdminRoute = window.location.pathname === adminPath;
const isResetPasswordRoute = window.location.pathname === resetPasswordPath;
// Die Pfade muessen den Deployment-Basispfad enthalten: produktiv laeuft die
// App unter /<app>/, ein Vergleich gegen '/verwaltung' traefe dort nie zu.
const currentPath = normalizePath(window.location.pathname);
const isAdminRoute = currentPath === normalizePath(withBase(ADMIN_PATH));
const isResetPasswordRoute = currentPath === normalizePath(withBase(RESET_PASSWORD_PATH));
const [view, setView] = useState('public');
const [handlerUser, setHandlerUser] = useState(null);
const { isAuthenticated, loading: authLoading, login, logout } = useAuth();
@ -97,7 +99,6 @@ function App() {
isAdmin={false}
currentView={view}
onViewChange={handleViewChange}
onHandlerLogin={handleHandlerLogout}
/>
<main className="app-main">
{isAdminRoute || view === 'login' ? (

View File

@ -10,10 +10,14 @@ root.render(
</React.StrictMode>
);
// Register service worker
// Register service worker.
// BASE_URL endet immer auf '/' und ist bei Unterpfad-Deployments '/nachsuche/'.
// Ein absolutes '/sw.js' würde stattdessen den Service Worker des Portals
// registrieren und dessen Scope übernehmen.
if ('serviceWorker' in navigator) {
const base = import.meta.env.BASE_URL || '/';
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
navigator.serviceWorker.register(`${base}sw.js`, { scope: base })
.then(registration => {
console.log('SW registered: ', registration);
})
@ -21,4 +25,4 @@ if ('serviceWorker' in navigator) {
console.log('SW registration failed: ', registrationError);
});
});
}
}

View File

@ -1,4 +1,10 @@
// Use configured API URL when set, otherwise auto-detect common subpath deployment
// Der Build läuft über Vite: Umgebungsvariablen kommen aus import.meta.env und
// müssen mit VITE_ beginnen. `process.env` existiert im Browser-Bundle NICHT
// ein Zugriff darauf wirft "process is not defined" und die App bleibt weiß.
const env = import.meta.env;
// Fallback, falls ein Build ohne PUBLIC_URL/VITE_BASE_PATH unter einem Unterpfad
// ausgeliefert wird: dann steht in BASE_URL nur '/'.
const detectRuntimeBasePath = () => {
if (typeof window === 'undefined') return '';
const path = window.location.pathname || '';
@ -7,32 +13,22 @@ const detectRuntimeBasePath = () => {
return match || '';
};
const configuredApiBaseUrl = process.env.REACT_APP_API_URL;
export const API_BASE_URL = (typeof configuredApiBaseUrl === 'string' && configuredApiBaseUrl.trim().length > 0)
? configuredApiBaseUrl
: detectRuntimeBasePath();
const stripTrailingSlash = (value) => String(value || '').replace(/\/+$/, '');
export const USER_TYPES = {
HS: 'HS',
BGS: 'BGS',
SB: 'SB',
LAB: 'LAB'
};
// Basis-Pfad des Deployments ohne abschließenden Slash: '/nachsuche' bzw. '' im Root.
export const BASE_PATH = stripTrailingSlash(env.BASE_URL) || detectRuntimeBasePath();
export const USER_TYPE_LABELS = {
[USER_TYPES.HS]: 'Hannoverscher Schweißhund',
[USER_TYPES.BGS]: 'Bayerischer Gebirgsschweißhund',
[USER_TYPES.SB]: 'Schweißhund (allgemein)',
[USER_TYPES.LAB]: 'Labrador'
};
// Vollständige API-Basis. VITE_API_URL überschreibt (z. B. eigene API-Domain).
export const API_BASE_URL = (typeof env.VITE_API_URL === 'string' && env.VITE_API_URL.trim())
? stripTrailingSlash(env.VITE_API_URL.trim())
: BASE_PATH;
export const RULES = [
"1. Verbrechen Sie den Standort und den Anschuss.",
"2. Vertreten Sie keine Pirschzeichen.",
"3. Versuchen Sie die Nachsuche möglichst nicht erst mit ungeübten Hunden.",
"4. Benachrichtigen Sie unverzüglich den Nachsuchenführer und die evtl. betroffenen Revierinhaber der Nachbarjagdbezirke.",
"5. Der Hundeführer gibt den Fangschuss auf das gestellte Stück ab.",
"6. Bei der Nachsuche hat der Hundeführer die Stellung eines Jagdleiters.",
"7. Der anfordernde Revierinhaber muss die betroffenen Nachbarreviere verständigen.",
"Auf Empfehlung der Jägerschaften Soltau und Fallingbostel ist bei Inanspruchnahme eine pauschale Aufwandsentschädigung an den Nachsuchenführer von 50,00 € zu entrichten."
];
export const ADMIN_PATH = env.VITE_ADMIN_PATH || '/verwaltung';
export const RESET_PASSWORD_PATH = '/passwort-zuruecksetzen';
// Hängt den Deployment-Basispfad vor einen App-Pfad: '/nachsuche/verwaltung'.
export const withBase = (appPath) =>
`${BASE_PATH}${appPath.startsWith('/') ? appPath : `/${appPath}`}`;
// Vergleichbare Normalform eines Pfads (ohne abschließenden Slash).
export const normalizePath = (pathname) => stripTrailingSlash(pathname) || '/';

View File

@ -28,7 +28,7 @@ services:
build:
context: ./frontend
args:
- REACT_APP_API_URL=http://localhost:5000
- VITE_API_URL=http://localhost:5000
container_name: tracking-leaders-frontend
ports:
- "8080:80"

View File

@ -139,7 +139,7 @@ Im Frontend wird die API-URL zur **Build-Zeit** gesetzt. Für Production:
frontend:
build:
args:
- REACT_APP_API_URL=https://api.yourdomain.com
- VITE_API_URL=https://api.yourdomain.com
```
2. Rebuild erforderlich:

View File

@ -61,7 +61,7 @@ services:
context: ./frontend
args:
- PUBLIC_URL=/stoeberhunde/
- REACT_APP_API_URL=${REACT_APP_API_URL:-}
- VITE_API_URL=${VITE_API_URL:-}
container_name: stoeberhunde-frontend
restart: unless-stopped
ports:

View File

@ -183,4 +183,4 @@ frontend/src/
- `CORS_ORIGIN`: Erlaubter CORS-Origin
### Frontend (.env)
- `REACT_APP_API_URL`: Backend-API-URL (Standard: http://localhost:5000)
- `VITE_API_URL`: Backend-API-URL (Standard: http://localhost:5000)

View File

@ -1,9 +1,9 @@
# API Configuration
# For local development
REACT_APP_API_URL=http://localhost:5000
VITE_API_URL=http://localhost:5000
# For production, use your actual backend URL
# REACT_APP_API_URL=https://api.yourdomain.com
# VITE_API_URL=https://api.yourdomain.com
# Admin path (optional, defaults to /verwaltung)
# REACT_APP_ADMIN_PATH=/verwaltung
# VITE_ADMIN_PATH=/verwaltung

View File

@ -10,8 +10,8 @@ COPY . .
ARG PUBLIC_URL=/stoeberhunde/
ENV PUBLIC_URL=$PUBLIC_URL
ARG REACT_APP_API_URL=
ENV REACT_APP_API_URL=$REACT_APP_API_URL
ARG VITE_API_URL=
ENV VITE_API_URL=$VITE_API_URL
# Limit Node.js heap during build to prevent OOM kills
ENV NODE_OPTIONS="--max_old_space_size=512"

View File

@ -1,8 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%BASE_URL%favicon.ico" />
<link rel="icon" href="%BASE_URL%icons/icon-192.png" />
<link rel="apple-touch-icon" href="%BASE_URL%icons/icon-192.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2d6a2d" />
@ -10,7 +10,9 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Stöberhunde" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="/manifest.json" />
<!-- Pfad relativ zur Deployment-Basis: ein absolutes "/manifest.json" wuerde
das Portal-Manifest laden und die App als Portal installieren. -->
<link rel="manifest" href="%BASE_URL%manifest.json" />
<meta
name="description"
content="Stöberhunde Heidekreis Übersicht der Stöberhundleiter"

File diff suppressed because it is too large Load Diff

View File

@ -3,28 +3,16 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.13.5",
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
"react-leaflet": "^4.2.1"
},
"scripts": {
"start": "react-scripts start",
"start": "vite",
"build": "vite build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"preview": "vite preview"
},
"browserslist": {
"production": [

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/icon-192.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2d6a2d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Stöberhunde Heidekreis" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<meta
name="description"
content="Stöberhunde Heidekreis Übersicht der Stöberhundeführer"
/>
<title>Stöberhunde Heidekreis</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -2,11 +2,6 @@
"short_name": "Stöberhunde",
"name": "Stöberhunde Heidekreis",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "icons/icon-192.png",
"sizes": "192x192",
@ -27,4 +22,4 @@
"theme_color": "#2d6a2d",
"background_color": "#ffffff",
"description": "Stöberhunde Heidekreis Übersicht der Stöberhundleiter"
}
}

View File

@ -11,13 +11,15 @@ import Admin from './pages/Admin';
import StoeberhundefuehrerLogin from './components/stoeberhundefuehrer/StoeberhundefuehrerLogin';
import StoeberhundefuehrerDashboard from './components/stoeberhundefuehrer/StoeberhundefuehrerDashboard';
import InstallBanner from './components/common/InstallBanner';
import { ADMIN_PATH, RESET_PASSWORD_PATH, withBase, normalizePath } from './utils/constants';
import './App.css';
function App() {
const adminPath = process.env.REACT_APP_ADMIN_PATH || '/verwaltung';
const resetPasswordPath = '/passwort-zuruecksetzen';
const isAdminRoute = window.location.pathname === adminPath;
const isResetPasswordRoute = window.location.pathname === resetPasswordPath;
// Die Pfade muessen den Deployment-Basispfad enthalten: produktiv laeuft die
// App unter /<app>/, ein Vergleich gegen '/verwaltung' traefe dort nie zu.
const currentPath = normalizePath(window.location.pathname);
const isAdminRoute = currentPath === normalizePath(withBase(ADMIN_PATH));
const isResetPasswordRoute = currentPath === normalizePath(withBase(RESET_PASSWORD_PATH));
const [view, setView] = useState('public');
const [stoeberhundefuehrerUser, setStoeberhundefuehrerUser] = useState(null);
const { isAuthenticated, loading: authLoading, login, logout } = useAuth();
@ -97,7 +99,6 @@ function App() {
isAdmin={false}
currentView={view}
onViewChange={handleViewChange}
onStoeberhundefuehrerLogin={handleStoeberhundefuehrerLogout}
/>
<main className="app-main">
{isAdminRoute || view === 'login' ? (

View File

@ -10,10 +10,14 @@ root.render(
</React.StrictMode>
);
// Register service worker
// Register service worker.
// BASE_URL endet immer auf '/' und ist bei Unterpfad-Deployments '/nachsuche/'.
// Ein absolutes '/sw.js' würde stattdessen den Service Worker des Portals
// registrieren und dessen Scope übernehmen.
if ('serviceWorker' in navigator) {
const base = import.meta.env.BASE_URL || '/';
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js')
navigator.serviceWorker.register(`${base}sw.js`, { scope: base })
.then(registration => {
console.log('SW registered: ', registration);
})
@ -21,4 +25,4 @@ if ('serviceWorker' in navigator) {
console.log('SW registration failed: ', registrationError);
});
});
}
}

View File

@ -1,34 +1,34 @@
// Use configured API URL when set, otherwise auto-detect common subpath deployment (/stoeberhunde)
// Der Build läuft über Vite: Umgebungsvariablen kommen aus import.meta.env und
// müssen mit VITE_ beginnen. `process.env` existiert im Browser-Bundle NICHT
// ein Zugriff darauf wirft "process is not defined" und die App bleibt weiß.
const env = import.meta.env;
// Fallback, falls ein Build ohne PUBLIC_URL/VITE_BASE_PATH unter einem Unterpfad
// ausgeliefert wird: dann steht in BASE_URL nur '/'.
const detectRuntimeBasePath = () => {
if (typeof window === 'undefined') return '';
const path = window.location.pathname || '';
if (path === '/stoeberhunde' || path.startsWith('/stoeberhunde/')) {
return '/stoeberhunde';
}
return '';
const knownBasePaths = ['/nachsuche', '/drohnenfuehrer', '/stoeberhunde'];
const match = knownBasePaths.find(basePath => path === basePath || path.startsWith(`${basePath}/`));
return match || '';
};
const configuredApiBaseUrl = process.env.REACT_APP_API_URL;
export const API_BASE_URL = (typeof configuredApiBaseUrl === 'string' && configuredApiBaseUrl.trim().length > 0)
? configuredApiBaseUrl
: detectRuntimeBasePath();
const stripTrailingSlash = (value) => String(value || '').replace(/\/+$/, '');
export const USER_TYPES = {
SH: 'SH',
BSH: 'BSH',
SHK: 'SHK'
};
// Basis-Pfad des Deployments ohne abschließenden Slash: '/nachsuche' bzw. '' im Root.
export const BASE_PATH = stripTrailingSlash(env.BASE_URL) || detectRuntimeBasePath();
export const USER_TYPE_LABELS = {
[USER_TYPES.SH]: 'Stöberhundleiter',
[USER_TYPES.BSH]: 'Begleithund',
[USER_TYPES.SHK]: 'Stöberhund kombiniert'
};
// Vollständige API-Basis. VITE_API_URL überschreibt (z. B. eigene API-Domain).
export const API_BASE_URL = (typeof env.VITE_API_URL === 'string' && env.VITE_API_URL.trim())
? stripTrailingSlash(env.VITE_API_URL.trim())
: BASE_PATH;
export const RULES = [
"Informieren Sie den Revierinhaber vor jedem Einsatz.",
"Der Stöberhundleiter ist verantwortlich für den sicheren Einsatz seines Hundes.",
"Halten Sie die vereinbarten Gebietsabgrenzungen ein.",
"Melden Sie das Ergebnis unverzüglich an den Revierinhaber.",
"Bei Inanspruchnahme ist eine pauschale Aufwandsentschädigung zu entrichten."
];
export const ADMIN_PATH = env.VITE_ADMIN_PATH || '/verwaltung';
export const RESET_PASSWORD_PATH = '/passwort-zuruecksetzen';
// Hängt den Deployment-Basispfad vor einen App-Pfad: '/nachsuche/verwaltung'.
export const withBase = (appPath) =>
`${BASE_PATH}${appPath.startsWith('/') ? appPath : `/${appPath}`}`;
// Vergleichbare Normalform eines Pfads (ohne abschließenden Slash).
export const normalizePath = (pathname) => stripTrailingSlash(pathname) || '/';

View File

@ -28,7 +28,7 @@ services:
build:
context: ./frontend
args:
- REACT_APP_API_URL=http://localhost:5000
- VITE_API_URL=http://localhost:5000
container_name: stoeberhunde-frontend
ports:
- "8080:80"