2026-05-14 21:31:04 +02:00
/ * *
* HTTP - Client : Token , Mandanten - Header , Fehler - Mapping .
2026-05-14 21:49:56 +02:00
* Alle API - Aufrufe laufen über request ( ) — siehe utils / api . js ( Facade ) und Domänenmodule ( planning . js , exercises . js ) .
2026-05-14 21:31:04 +02:00
* /
2026-05-14 21:49:56 +02:00
export const API _URL = import . meta . env . VITE _API _URL || ''
2026-05-14 21:31:04 +02:00
/** LocalStorage + Request-Header für Mandanten-Kontext */
export const ACTIVE _CLUB _STORAGE _KEY = 'shinkan_active_club_id'
2026-05-14 21:49:56 +02:00
export function mergeActiveClubHeader ( headers = { } ) {
2026-05-14 21:31:04 +02:00
const cid = localStorage . getItem ( ACTIVE _CLUB _STORAGE _KEY )
if ( cid && /^\d+$/ . test ( String ( cid ) . trim ( ) ) ) {
return { ... headers , 'X-Active-Club-Id' : String ( cid ) . trim ( ) }
}
2026-05-22 11:12:50 +02:00
return headers
2026-05-14 21:31:04 +02:00
}
2026-05-22 11:12:50 +02:00
async function _fetchWithAuth ( endpoint , options = { } ) {
2026-05-14 21:31:04 +02:00
const token = localStorage . getItem ( 'authToken' )
const method = ( options . method || 'GET' ) . toUpperCase ( )
const headers = mergeActiveClubHeader ( {
... options . headers ,
} )
if ( method !== 'GET' && method !== 'HEAD' ) {
if ( ! headers [ 'Content-Type' ] && ! headers [ 'content-type' ] ) {
headers [ 'Content-Type' ] = 'application/json'
}
}
if ( token ) {
headers [ 'X-Auth-Token' ] = token
}
const url = ` ${ API _URL } ${ endpoint } `
try {
const response = await fetch ( url , {
... options ,
headers ,
} )
if ( ! response . ok ) {
const text = await response . text ( )
let parsed = null
try {
parsed = JSON . parse ( text )
} catch {
parsed = null
}
if ( parsed ? . detail != null ) {
const d = parsed . detail
throw new Error ( typeof d === 'string' ? d : JSON . stringify ( d ) )
}
if ( response . status === 502 ) {
throw new Error (
'HTTP 502 (Bad Gateway): Der Reverse-Proxy hat die API nicht korrekt erreicht. Ist `shinkan-api` aktiv (`docker compose ps`, `docker logs shinkan-api`)? Bei Host-Routing nur einen Weg verwenden — alles auf Port 3003 (Nginx nach `backend:8000`) oder sauber `/api` → Backend-Port.'
)
}
const snippet = ( text || '' ) . replace ( /\s+/g , ' ' ) . trim ( ) . slice ( 0 , 180 )
throw new Error ( snippet ? ` HTTP ${ response . status } : ${ snippet } ` : ` HTTP ${ response . status } ` )
}
2026-05-22 11:12:50 +02:00
return response
2026-05-14 21:31:04 +02:00
} catch ( e ) {
if ( e instanceof TypeError && ( e . message === 'Failed to fetch' || e . message . includes ( 'fetch' ) ) ) {
const hint =
API _URL && API _URL . length > 0
? ` Verbindung zum API unter ${ API _URL } fehlgeschlagen. Läuft das Backend (z. B. Port 8098) und ist CORS erlaubt? `
: 'Kein VITE_API_URL gesetzt: Anfragen gehen an die Frontend-URL und schlagen oft fehl. Setze in .env z. B. VITE_API_URL=http://localhost:8098 und starte Vite neu.'
throw new Error ( ` ${ hint } [Technisch: ${ e . message } ; URL war ${ endpoint } ] ` )
}
throw e
}
}
2026-05-22 11:12:50 +02:00
/ * *
* Generischer API - Aufruf inkl . X - Auth - Token und X - Active - Club - Id .
* /
export async function request ( endpoint , options = { } ) {
const response = await _fetchWithAuth ( endpoint , options )
return response . json ( )
}
/** Text-Download (z. B. CSV-Export) mit gleicher Auth wie request(). */
export async function requestText ( endpoint , options = { } ) {
const response = await _fetchWithAuth ( endpoint , options )
const disposition = response . headers . get ( 'Content-Disposition' ) || ''
const match = disposition . match ( /filename="([^"]+)"/ )
return {
text : await response . text ( ) ,
filename : match ? match [ 1 ] : null ,
}
}