revert: Dev/Compose/Frontend auf Stand ceef6f0 (07.05.2026 22:02)
All checks were successful
Deploy Development / deploy (push) Successful in 36s
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) Successful in 23s
All checks were successful
Deploy Development / deploy (push) Successful in 36s
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) Successful in 23s
Alle Änderungen nach dem letzten verifizierten Deploy (ceef6f0) für
docker-compose.dev-env.yml, docker-compose.yml, .env.example sowie
main.jsx, MediaLibraryPage, ExerciseFormPage zurückgesetzt;
ErrorBoundary entfernt. Entspricht dem funktionierenden develop-Stand
von Gitea an diesem Zeitpunkt.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d758de3852
commit
f8a7247ccc
19
.env.example
19
.env.example
|
|
@ -8,16 +8,18 @@
|
||||||
# ─── 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
|
||||||
# SHINKAN_MEDIA_HOST=/shinkan-media
|
# DB_PASSWORD=…
|
||||||
# MEDIA_ROOT=/app/media
|
# APP_URL=https://shinkan.jinkendo.de
|
||||||
# …
|
# 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
|
||||||
# SHINKAN_MEDIA_HOST=/shinkan-media/dev
|
# DB_PASSWORD=dev_password
|
||||||
# MEDIA_ROOT=/app/media
|
# APP_URL=https://dev.shinkan.jinkendo.de
|
||||||
# …
|
# 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
|
||||||
|
|
@ -44,10 +46,7 @@ APP_URL=https://shinkan.jinkendo.de
|
||||||
ALLOWED_ORIGINS=https://shinkan.jinkendo.de
|
ALLOWED_ORIGINS=https://shinkan.jinkendo.de
|
||||||
ENVIRONMENT=production
|
ENVIRONMENT=production
|
||||||
|
|
||||||
# Host-Pfad (Bind-Mount). Compose-Defaults: Prod /shinkan-media, Dev /shinkan-media/dev — hier nur setzen zum Überschreiben.
|
MEDIA_DIR=/app/media
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# Medien: Host-Pfad SHINKAN_MEDIA_HOST in .env überschreiben; Default /shinkan-media/dev (getrennt von Prod).
|
# Keine festen container_name — Compose-Namen haben Projektprefix (<projekt>-postgres-1).
|
||||||
|
# Gleiche Variablennamen wie docker-compose.yml; andere Werte in einer eigenen .env neben dieser Datei.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|
@ -42,9 +43,8 @@ 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:
|
||||||
- ${SHINKAN_MEDIA_HOST:-/shinkan-media/dev}:${MEDIA_ROOT:-/app/media}
|
- dev-shinkan-media:/app/media
|
||||||
ports:
|
ports:
|
||||||
- "8098:8000"
|
- "8098:8000"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -70,6 +70,7 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dev-shinkan-db-data:
|
dev-shinkan-db-data:
|
||||||
|
dev-shinkan-media:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
dev-shinkan-network:
|
dev-shinkan-network:
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,8 @@ 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}"
|
||||||
# Medien: Container-Pfad MEDIA_ROOT; Host-Pfad SHINKAN_MEDIA_HOST (in .env überschreiben; Default /shinkan-media).
|
|
||||||
MEDIA_ROOT: "${MEDIA_ROOT:-/app/media}"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${SHINKAN_MEDIA_HOST:-/shinkan-media}:${MEDIA_ROOT:-/app/media}
|
- shinkan-media:/app/media
|
||||||
ports:
|
ports:
|
||||||
- "8003:8000"
|
- "8003:8000"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -81,6 +79,7 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
shinkan-db-data:
|
shinkan-db-data:
|
||||||
|
shinkan-media:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
shinkan-network:
|
shinkan-network:
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fängt Render-Fehler ab — verhindert „weißen Bildschirm“ ohne Hinweis (z. B. nach Deploy/Chaches).
|
|
||||||
*/
|
|
||||||
export default class ErrorBoundary extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = { error: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error) {
|
|
||||||
return { error }
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error, info) {
|
|
||||||
console.error('ErrorBoundary:', error, info?.componentStack)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.error) {
|
|
||||||
const msg = this.state.error?.message || String(this.state.error)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
minHeight: '100vh',
|
|
||||||
padding: '24px',
|
|
||||||
fontFamily: 'system-ui, sans-serif',
|
|
||||||
background: '#f8fafc',
|
|
||||||
color: '#0f172a',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1 style={{ fontSize: '1.25rem', marginBottom: '12px' }}>Ein Fehler ist aufgetreten</h1>
|
|
||||||
<p style={{ marginBottom: '16px', maxWidth: '40rem', lineHeight: 1.5 }}>
|
|
||||||
Die Oberfläche konnte nicht geladen werden. Details siehe unten oder in der
|
|
||||||
Browser-Konsole (F12). Nach einem Deploy einen <strong>harten Reload</strong> (Cache leeren)
|
|
||||||
versuchen.
|
|
||||||
</p>
|
|
||||||
<pre
|
|
||||||
style={{
|
|
||||||
padding: '12px',
|
|
||||||
background: '#fff',
|
|
||||||
border: '1px solid #e2e8f0',
|
|
||||||
borderRadius: 8,
|
|
||||||
overflow: 'auto',
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{msg}
|
|
||||||
</pre>
|
|
||||||
<p style={{ marginTop: '20px' }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
style={{
|
|
||||||
padding: '10px 16px',
|
|
||||||
borderRadius: 8,
|
|
||||||
border: '1px solid #cbd5e1',
|
|
||||||
background: '#fff',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
>
|
|
||||||
Seite neu laden
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return this.props.children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import ErrorBoundary from './ErrorBoundary.jsx'
|
|
||||||
import './app.css'
|
import './app.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ErrorBoundary>
|
<App />
|
||||||
<App />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,6 @@ 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
|
||||||
|
|
@ -1464,7 +1460,7 @@ function ExerciseFormPage() {
|
||||||
<label className="form-label">Datei</label>
|
<label className="form-label">Datei</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept={EXERCISE_MEDIA_UPLOAD_ACCEPT}
|
accept="image/*,video/*,application/pdf"
|
||||||
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' }}>
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import {
|
||||||
CircleDot,
|
CircleDot,
|
||||||
FilePenLine,
|
FilePenLine,
|
||||||
Copyright,
|
Copyright,
|
||||||
Image as LucideImage,
|
Image,
|
||||||
Video as LucideVideo,
|
Video,
|
||||||
FileText,
|
FileText,
|
||||||
File,
|
File,
|
||||||
Upload,
|
Upload,
|
||||||
|
|
@ -45,10 +45,6 @@ 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)',
|
||||||
|
|
@ -219,8 +215,8 @@ function MediaTypeGlyph({ mimeType, compact }) {
|
||||||
const kind = previewDisplayKind(mimeType)
|
const kind = previewDisplayKind(mimeType)
|
||||||
const label = MEDIA_KIND_LABELS[kind] || 'Medium'
|
const label = MEDIA_KIND_LABELS[kind] || 'Medium'
|
||||||
let Icon = File
|
let Icon = File
|
||||||
if (kind === 'image') Icon = LucideImage
|
if (kind === 'image') Icon = Image
|
||||||
else if (kind === 'video') Icon = LucideVideo
|
else if (kind === 'video') Icon = Video
|
||||||
else if (kind === 'pdf') Icon = FileText
|
else if (kind === 'pdf') Icon = FileText
|
||||||
const sz = compact ? 12 : 14
|
const sz = compact ? 12 : 14
|
||||||
return (
|
return (
|
||||||
|
|
@ -478,13 +474,12 @@ export default function MediaLibraryPage() {
|
||||||
const selCount = selected.size
|
const selCount = selected.size
|
||||||
|
|
||||||
const onBulkArchiveFiles = async (e) => {
|
const onBulkArchiveFiles = async (e) => {
|
||||||
const input = e.target
|
const fl = e.target.files
|
||||||
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)
|
||||||
|
|
@ -507,7 +502,6 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -630,7 +624,7 @@ export default function MediaLibraryPage() {
|
||||||
ref={bulkFileInputRef}
|
ref={bulkFileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
className="media-library__sr-file"
|
className="media-library__sr-file"
|
||||||
accept={MEDIA_UPLOAD_ACCEPT}
|
accept="image/*,video/*,application/pdf"
|
||||||
multiple
|
multiple
|
||||||
onChange={onBulkArchiveFiles}
|
onChange={onBulkArchiveFiles}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user