refactor: update media handling configuration and improve upload acceptance
Some checks failed
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Failing after 1m45s

- Modified .env.example to include SHINKAN_MEDIA_HOST and MEDIA_ROOT for better media path management.
- Updated docker-compose files to utilize SHINKAN_MEDIA_HOST for volume mounts, ensuring consistent media directory handling across environments.
- Enhanced ExerciseFormPage and MediaLibraryPage to specify accepted media types explicitly, improving compatibility and user experience during uploads.
This commit is contained in:
Lars 2026-05-08 08:35:15 +02:00
parent ceef6f09e2
commit c921f0dd8f
5 changed files with 30 additions and 20 deletions

View File

@ -8,18 +8,16 @@
# ─── Typische Werte PROD (docker-compose.yml) ───────────────────────────────── # ─── Typische Werte PROD (docker-compose.yml) ─────────────────────────────────
# DB_NAME=shinkan # DB_NAME=shinkan
# DB_USER=shinkan_user # DB_USER=shinkan_user
# DB_PASSWORD=… # SHINKAN_MEDIA_HOST=/shinkan-media
# APP_URL=https://shinkan.jinkendo.de # MEDIA_ROOT=/app/media
# ALLOWED_ORIGINS=https://shinkan.jinkendo.de # …
# ENVIRONMENT=production
# ─── Typische Werte DEV (docker-compose.dev-env.yml) ───────────────────────── # ─── Typische Werte DEV (docker-compose.dev-env.yml) ─────────────────────────
# DB_NAME=shinkan_dev # DB_NAME=shinkan_dev
# DB_USER=shinkan_dev # DB_USER=shinkan_dev
# DB_PASSWORD=dev_password # SHINKAN_MEDIA_HOST=/shinkan-media/dev
# APP_URL=https://dev.shinkan.jinkendo.de # MEDIA_ROOT=/app/media
# ALLOWED_ORIGINS=https://dev.shinkan.jinkendo.de,http://192.168.2.49:3098 # …
# ENVIRONMENT=development
# ─── Ab hier: eine ausfüllbare Vorlage (bei uns meist Prod-Defaults) ─────────── # ─── Ab hier: eine ausfüllbare Vorlage (bei uns meist Prod-Defaults) ───────────
DB_HOST=postgres DB_HOST=postgres
@ -46,7 +44,10 @@ APP_URL=https://shinkan.jinkendo.de
ALLOWED_ORIGINS=https://shinkan.jinkendo.de ALLOWED_ORIGINS=https://shinkan.jinkendo.de
ENVIRONMENT=production ENVIRONMENT=production
MEDIA_DIR=/app/media # Host-Pfad auf dem Rechner (Bind-Mount); im Container siehe MEDIA_ROOT. Pflicht für docker compose up.
SHINKAN_MEDIA_HOST=/shinkan-media
# Optional: Pfad im Container (FastAPI MEDIA_ROOT); Standard reicht fast immer.
MEDIA_ROOT=/app/media
MEDIAWIKI_API_URL=https://karatetrainer.net/api.php MEDIAWIKI_API_URL=https://karatetrainer.net/api.php
MEDIAWIKI_USER=Jinkendo MEDIAWIKI_USER=Jinkendo

View File

@ -1,5 +1,4 @@
# Keine festen container_name — Compose-Namen haben Projektprefix (<projekt>-postgres-1). # Medien-Ablage: SHINKAN_MEDIA_HOST in der .env neben dieser Datei setzen (Host-Pfad, Bind-Mount).
# Gleiche Variablennamen wie docker-compose.yml; andere Werte in einer eigenen .env neben dieser Datei.
services: services:
postgres: postgres:
@ -43,8 +42,9 @@ services:
MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}" MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}"
MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}" MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}"
MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}" MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}"
MEDIA_ROOT: "${MEDIA_ROOT:-/app/media}"
volumes: volumes:
- dev-shinkan-media:/app/media - ${SHINKAN_MEDIA_HOST:?SHINKAN_MEDIA_HOST_missing_in_dotenv}:${MEDIA_ROOT:-/app/media}
ports: ports:
- "8098:8000" - "8098:8000"
depends_on: depends_on:
@ -70,7 +70,6 @@ services:
volumes: volumes:
dev-shinkan-db-data: dev-shinkan-db-data:
dev-shinkan-media:
networks: networks:
dev-shinkan-network: dev-shinkan-network:

View File

@ -51,9 +51,10 @@ services:
MEDIAWIKI_CATEGORY_EXERCISES: "${MEDIAWIKI_CATEGORY_EXERCISES:-Übungen}" MEDIAWIKI_CATEGORY_EXERCISES: "${MEDIAWIKI_CATEGORY_EXERCISES:-Übungen}"
MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}" MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}"
MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}" MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}"
MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}" # Medien: Pfad im Container (meist /app/media). Host-Pfad für Bind-Mount: SHINKAN_MEDIA_HOST in .env (obligatorisch).
MEDIA_ROOT: "${MEDIA_ROOT:-/app/media}"
volumes: volumes:
- shinkan-media:/app/media - ${SHINKAN_MEDIA_HOST:?SHINKAN_MEDIA_HOST_missing_in_dotenv}:${MEDIA_ROOT:-/app/media}
ports: ports:
- "8003:8000" - "8003:8000"
depends_on: depends_on:
@ -79,7 +80,6 @@ services:
volumes: volumes:
shinkan-db-data: shinkan-db-data:
shinkan-media:
networks: networks:
shinkan-network: shinkan-network:

View File

@ -6,6 +6,10 @@ import RichTextEditor from '../components/RichTextEditor'
import ExerciseProgressionGraphPanel from '../components/ExerciseProgressionGraphPanel' import ExerciseProgressionGraphPanel from '../components/ExerciseProgressionGraphPanel'
import { SKILL_LEVEL_OPTIONS, normalizeSkillLevelSlug } from '../constants/skillLevels' import { SKILL_LEVEL_OPTIONS, normalizeSkillLevelSlug } from '../constants/skillLevels'
/** Kein image/*/video/* — WebKit (iOS) ist damit oft kaputt. */
const EXERCISE_MEDIA_UPLOAD_ACCEPT =
'image/jpeg,image/png,image/gif,image/heic,image/heif,video/mp4,video/quicktime,application/pdf,.jpg,.jpeg,.png,.gif,.heic,.heif,.mp4,.mov,.pdf'
/** Kachelvorschau: Video nutzt ersten Frame (metadata), Bild = img. */ /** Kachelvorschau: Video nutzt ersten Frame (metadata), Bild = img. */
function ExerciseMediaThumbTile({ exerciseId, media, onOpenPreview }) { function ExerciseMediaThumbTile({ exerciseId, media, onOpenPreview }) {
const src = !media.embed_url ? resolveExerciseMediaFileUrl(exerciseId, media) : null const src = !media.embed_url ? resolveExerciseMediaFileUrl(exerciseId, media) : null
@ -1460,7 +1464,7 @@ function ExerciseFormPage() {
<label className="form-label">Datei</label> <label className="form-label">Datei</label>
<input <input
type="file" type="file"
accept="image/*,video/*,application/pdf" accept={EXERCISE_MEDIA_UPLOAD_ACCEPT}
onChange={(e) => setMediaFile(e.target.files?.[0] || null)} onChange={(e) => setMediaFile(e.target.files?.[0] || null)}
/> />
<div className="form-row" style={{ marginTop: '8px' }}> <div className="form-row" style={{ marginTop: '8px' }}>

View File

@ -45,6 +45,10 @@ const MEDIA_KIND_OPTIONS = [
{ value: 'other', label: 'Sonstiges' }, { value: 'other', label: 'Sonstiges' },
] ]
/** Kein image/*/video/* — WebKit (iOS Safari) ist damit oft kaputt; alles explizit. */
const MEDIA_UPLOAD_ACCEPT =
'image/jpeg,image/png,image/gif,image/heic,image/heif,video/mp4,video/quicktime,application/pdf,.jpg,.jpeg,.png,.gif,.heic,.heif,.mp4,.mov,.pdf'
const LC_STATUS_LABELS = { const LC_STATUS_LABELS = {
active: 'Aktiv', active: 'Aktiv',
trash_soft: 'Papierkorb (1)', trash_soft: 'Papierkorb (1)',
@ -474,12 +478,13 @@ export default function MediaLibraryPage() {
const selCount = selected.size const selCount = selected.size
const onBulkArchiveFiles = async (e) => { const onBulkArchiveFiles = async (e) => {
const fl = e.target.files const input = e.target
const fl = input.files
if (!fl?.length) return if (!fl?.length) return
const list = Array.from(fl) const list = Array.from(fl)
e.target.value = ''
if (uploadVis === 'club' && !Number(uploadClubId)) { if (uploadVis === 'club' && !Number(uploadClubId)) {
window.alert('Bitte einen Verein für die Sichtbarkeit „Verein“ wählen.') window.alert('Bitte einen Verein für die Sichtbarkeit „Verein“ wählen.')
input.value = ''
return return
} }
setUploadBusy(true) setUploadBusy(true)
@ -502,6 +507,7 @@ export default function MediaLibraryPage() {
} catch (err) { } catch (err) {
window.alert(err.message || String(err)) window.alert(err.message || String(err))
} finally { } finally {
input.value = ''
setUploadBusy(false) setUploadBusy(false)
} }
} }
@ -624,7 +630,7 @@ export default function MediaLibraryPage() {
ref={bulkFileInputRef} ref={bulkFileInputRef}
type="file" type="file"
className="media-library__sr-file" className="media-library__sr-file"
accept="image/*,video/*,application/pdf" accept={MEDIA_UPLOAD_ACCEPT}
multiple multiple
onChange={onBulkArchiveFiles} onChange={onBulkArchiveFiles}
/> />