diff --git a/backend/version.py b/backend/version.py index 06d1d39..c9d0572 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.88" +APP_VERSION = "0.8.89" BUILD_DATE = "2026-05-11" DB_SCHEMA_VERSION = "20260511052" @@ -30,10 +30,19 @@ MODULE_VERSIONS = { "membership": "1.0.0", "catalogs": "1.5.0", # Updated: Trainer Contexts API (Migration 012) "maturity_models": "1.4.0", # matrix_stack_bundle: vollständiger Katalog+Modelle+Bindings Export/Import - "content_reports": "1.1.0", # P-13: Melde-Button in Medienbibliothek + ExerciseAttachmentMediaStrip + "content_reports": "1.2.0", # P-13: Viewer konsolidiert — MediaPreviewModal (geteilt); Melden in ExerciseFormPage-Viewer } CHANGELOG = [ + { + "version": "0.8.89", + "date": "2026-05-11", + "changes": [ + "Feat P-13: MediaPreviewModal — geteilter Medienvorschau-Dialog für alle Kontexte (Bibliothek, Übungsbearbeitung, angehängte Medien); optionale Melden- und Bearbeiten-Buttons im Header.", + "Feat P-13: Melde-Button im ExerciseFormPage-Viewer (verlinktes Medium → Vorschau → Melden).", + "Refactor: Inline-Vorschau-Blöcke in MediaLibraryPage, ExerciseFormPage und ExerciseAttachmentMediaStrip durch MediaPreviewModal ersetzt.", + ], + }, { "version": "0.8.88", "date": "2026-05-11", diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 1be7643..0a7e370 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -1,7 +1,7 @@ # Shinkan Jinkendo – Entwicklungsstand & Handover -**Stand:** 2026-05-07 -**App-Version / DB-Schema:** App **0.8.59**, DB-Schema siehe `backend/version.py` (`DB_SCHEMA_VERSION`) +**Stand:** 2026-05-11 +**App-Version / DB-Schema:** App **0.8.89**, DB-Schema siehe `backend/version.py` (`DB_SCHEMA_VERSION`) Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**. @@ -119,13 +119,38 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl --- +## 5b. P-13: Content-Meldeverfahren (vollständig implementiert, 2026-05-11) + +**DSA-konformes Meldeverfahren (KRIT-03) — App 0.8.87–0.8.89.** + +**Backend (`backend/routers/content_reports.py`, Migration 052):** +- `POST /api/content-reports` — optionale Auth; `official`-Medien ohne Login meldbar. +- `GET /api/me/inbox/content-reports` — Plattform-Admin-Postfach. +- `PATCH /api/content-reports/{id}` — Status-Übergang (submitted → in_review → resolved/dismissed). +- `POST /api/content-reports/{id}/legal-hold` — Superadmin; integriert P-11 `set_legal_hold()`. +- Automatische Priorisierung `high` für `minors`/`illegal_content`/`youth_protection`. +- 15 Backend-Tests in `backend/tests/test_p13_content_reports.py`. + +**Frontend:** +- `ReportContentModal.jsx` — Melde-Formular (Grund, Beschreibung, Name, E-Mail, Gutglaubenserklärung). +- `MediaPreviewModal.jsx` — geteilter Vorschau-Dialog; optionale Melden- und Bearbeiten-Buttons. +- `InboxPage.jsx` — zweiter Abschnitt „Inhaltsmeldungen” für Plattform-Admins mit `ReportDetailModal`. +- `OrgInboxContext.jsx` — liefert `contentReports`, `contentReportCount`, `canAccessContentReports`. +- Melde-Button in **MediaLibraryPage** (Grid + Liste + Viewer), **ExerciseFormPage** (Viewer), **ExerciseAttachmentMediaStrip** (Viewer). + +**Offen (explizit zurückgestellt):** +- Melde-Einstieg im Coaching-Modus (Feedback-Schritt, nicht kritisch). + +--- + ## 6. Nächste Session — sinnvolle Arbeitspakete -1. **Inline §11:** Syntax festlegen (z. B. `{{exerciseMedia:id}}` → kanonisches HTML), Server normalisieren bei Speichern, einen **`renderExerciseRichText()`**-Pfad im Frontend. -2. **Tests:** pytest für `media_assets`-Router (Leserechte, Lifecycle, `from-asset`); ggf. Snapshot der Pfad-Umzug-Logik. -3. **Retention:** Job-Dokumentation + Betrieb (ENV, Intervall); Dry-Run beschreiben. -4. **S3/Adapter:** Speicher-Abstraktion §7 — wenn Produkt es verlangt. -5. **Rahmen/UI:** Kalender „aus Rahmen übernehmen“ weiter anbinden (parallel, unabhängig von Medien). +1. **Frontend testen:** P-13 Melde-Flow in Medienbibliothek und Übungsbearbeitung manuell durchspielen. +2. **Inline §11:** Syntax festlegen (`{{exerciseMedia:id}}` → kanonisches HTML), Server normalisieren bei Speichern, einen `renderExerciseRichText()`-Pfad im Frontend. +3. **Tests:** pytest für `media_assets`-Router (Leserechte, Lifecycle, `from-asset`); ggf. Snapshot der Pfad-Umzug-Logik. +4. **Retention:** Job-Dokumentation + Betrieb (ENV, Intervall); Dry-Run beschreiben. +5. **S3/Adapter:** Speicher-Abstraktion §7 — wenn Produkt es verlangt. +6. **Rahmen/UI:** Kalender „aus Rahmen übernehmen“ weiter anbinden (parallel, unabhängig von Medien). --- diff --git a/frontend/src/components/ExerciseAttachmentMediaStrip.jsx b/frontend/src/components/ExerciseAttachmentMediaStrip.jsx index aa6d697..74ef84f 100644 --- a/frontend/src/components/ExerciseAttachmentMediaStrip.jsx +++ b/frontend/src/components/ExerciseAttachmentMediaStrip.jsx @@ -4,11 +4,10 @@ import React, { useMemo, useState } from 'react' import ExerciseMediaEmbed from './ExerciseMediaEmbed' import ExerciseMediaThumbTile from './ExerciseMediaThumbTile' +import MediaPreviewModal from './MediaPreviewModal' import ReportContentModal from './ReportContentModal' import { resolveExerciseMediaFileUrl } from '../utils/exerciseMediaUrl' -import { - collectInlineExerciseMediaIdsFromExercise, -} from '../utils/exerciseInlineMediaRefs' +import { collectInlineExerciseMediaIdsFromExercise } from '../utils/exerciseInlineMediaRefs' function isTrashHidden(m) { return String(m?.asset_lifecycle_state || 'active').toLowerCase() === 'trash_hidden' @@ -58,28 +57,28 @@ export default function ExerciseAttachmentMediaStrip({ exerciseId, exercise }) { - {(m.asset_lifecycle_state || 'active') === 'active' && !m.asset_legal_hold_active && ( - - )} ) })} + + {preview && ( + setPreview(null)} + onReport={ + (preview.asset_lifecycle_state || 'active') === 'active' && !preview.asset_legal_hold_active + ? () => { + setReportTarget(preview) + setPreview(null) + } + : null + } + /> + )} + {reportTarget && ( setReportTarget(null)} /> )} - - {preview && ( -
setPreview(null)} - onKeyDown={(e) => e.key === 'Escape' && setPreview(null)} - > -
e.stopPropagation()} - > -

Vorschau

- {preview.embed_url ? ( -

- - {preview.embed_url} - -

- ) : preview.mime_type?.startsWith('video/') || preview.media_type === 'video' ? ( -
-
- )} ) } diff --git a/frontend/src/components/MediaPreviewModal.jsx b/frontend/src/components/MediaPreviewModal.jsx new file mode 100644 index 0000000..2164bf8 --- /dev/null +++ b/frontend/src/components/MediaPreviewModal.jsx @@ -0,0 +1,140 @@ +/** + * Gemeinsamer Medienvorschau-Dialog. + * + * Props: + * title – Anzeigename des Mediums (Dateiname o.ä.) + * media – Medienobjekt (mime_type, embed_url, asset_legal_hold_active, media_type) + * fileUrl – Bereits aufgelöste Datei-URL (null für Embed-only-Medien) + * onClose – Pflicht + * onReport – optional; wenn gesetzt, erscheint "Melden"-Button + * onEdit – optional; wenn gesetzt, erscheint "Bearbeiten"-Button + */ +import React from 'react' + +export default function MediaPreviewModal({ title, media, fileUrl, onClose, onReport, onEdit }) { + const mlow = (media?.mime_type || '').toLowerCase() + const isHeic = mlow.includes('heic') || mlow.includes('heif') + const isImage = !isHeic && (media?.mime_type?.startsWith('image/') || media?.media_type === 'image') + const isVideo = media?.mime_type?.startsWith('video/') || media?.media_type === 'video' + const isPdf = mlow.includes('pdf') + const isLegalHold = !!media?.asset_legal_hold_active + const canReport = onReport && !isLegalHold + + function handleBackdrop(e) { + if (e.target === e.currentTarget) onClose() + } + + function handleKeyDown(e) { + if (e.key === 'Escape') onClose() + } + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

+ {title || 'Vorschau'} +

+
+ {canReport && ( + + )} + {onEdit && ( + + )} + +
+
+ + {/* Body */} + {isLegalHold ? ( +

+ Dieses Medium ist gesperrt und steht nicht zur Verfügung. +

+ ) : media?.embed_url ? ( +

+ {media.embed_url} +

+ ) : isHeic ? ( +
+

+ HEIC/HEIF — in diesem Browser oft keine eingebettete Vorschau. Zum Ansehen herunterladen oder im neuen Tab öffnen. +

+ {fileUrl && ( + + Datei öffnen + + )} +
+ ) : isImage ? ( + + ) : isVideo ? ( + + ) : isPdf ? ( +
+

PDF — zur Ansicht im neuen Tab öffnen.

+ {fileUrl && ( + + PDF öffnen + + )} +
+ ) : fileUrl ? ( +

+ Datei öffnen +

+ ) : ( +

Keine Vorschau verfügbar.

+ )} +
+
+ ) +} diff --git a/frontend/src/pages/ExerciseFormPage.jsx b/frontend/src/pages/ExerciseFormPage.jsx index 76026a6..c54de8a 100644 --- a/frontend/src/pages/ExerciseFormPage.jsx +++ b/frontend/src/pages/ExerciseFormPage.jsx @@ -5,6 +5,8 @@ import { resolveExerciseMediaFileUrl, resolveMediaAssetFileUrl } from '../utils/ import RichTextEditor from '../components/RichTextEditor' import ExerciseProgressionGraphPanel from '../components/ExerciseProgressionGraphPanel' import ExerciseMediaThumbTile from '../components/ExerciseMediaThumbTile' +import MediaPreviewModal from '../components/MediaPreviewModal' +import ReportContentModal from '../components/ReportContentModal' import { SHINKAN_EXERCISE_MEDIA_DRAG_MIME, buildExerciseMediaDragPayload, @@ -391,6 +393,7 @@ function ExerciseFormPage() { const [archiveItems, setArchiveItems] = useState([]) const [archiveError, setArchiveError] = useState(null) const [mediaPreview, setMediaPreview] = useState(null) + const [reportTarget, setReportTarget] = useState(null) useEffect(() => { const next = {} @@ -1652,66 +1655,28 @@ function ExerciseFormPage() { )} {mediaPreview && ( -
setMediaPreview(null)} - onKeyDown={(e) => e.key === 'Escape' && setMediaPreview(null)} - > -
e.stopPropagation()} - > -

Vorschau

- {mediaPreview.asset_legal_hold_active ? ( -

- Dieses Medium ist gesperrt und steht nicht zur Verfügung. -

- ) : mediaPreview.embed_url ? ( -

- - {mediaPreview.embed_url} - -

- ) : mediaPreview.mime_type?.startsWith('video/') || mediaPreview.media_type === 'video' ? ( -
-
+ setMediaPreview(null)} + onReport={ + !mediaPreview.asset_legal_hold_active + ? () => { + setReportTarget(mediaPreview) + setMediaPreview(null) + } + : null + } + /> + )} + {reportTarget && ( + setReportTarget(null)} + /> )} )} diff --git a/frontend/src/pages/MediaLibraryPage.jsx b/frontend/src/pages/MediaLibraryPage.jsx index 072e388..7be3454 100644 --- a/frontend/src/pages/MediaLibraryPage.jsx +++ b/frontend/src/pages/MediaLibraryPage.jsx @@ -26,6 +26,7 @@ import { activeClubMemberships } from '../utils/activeClub' import { resolveMediaAssetFileUrl } from '../utils/exerciseMediaUrl' import RightsDeclarationDialog from '../components/RightsDeclarationDialog' import ReportContentModal from '../components/ReportContentModal' +import MediaPreviewModal from '../components/MediaPreviewModal' const LC_OPTIONS = [ { value: 'active', label: 'Aktiv' }, @@ -1097,121 +1098,18 @@ export default function MediaLibraryPage() { {preview ? ( -
setPreview(null)} - > -
e.stopPropagation()} - > -
-

- {preview.original_filename || `Medium #${preview.id}`} -

-
- {(preview.lifecycle_state || 'active') === 'active' && !preview.legal_hold_active && ( - - )} - - -
-
-
- {(() => { - const url = resolveMediaAssetFileUrl(preview.id) - const kind = previewDisplayKind(preview.mime_type) - const mlow = (preview.mime_type || '').toLowerCase() - if (!url) { - return

Keine Datei-URL.

- } - if (mlow.includes('heic') || mlow.includes('heif')) { - return ( -
-

- HEIC/HEIF — in diesem Browser oft keine eingebettete Vorschau. Zum Ansehen herunterladen oder im neuen Tab öffnen. -

- - Datei öffnen - -
- ) - } - if (kind === 'image') { - return ( - - ) - } - if (kind === 'video') { - return ( - - ) - } - if (kind === 'pdf') { - return ( -
-

PDF — zur Ansicht im neuen Tab öffnen.

- - PDF öffnen - -
- ) - } - return ( -
-

- Vorschau für diesen Typ nicht verfügbar ({preview.mime_type || 'unbekannt'}). -

- - Datei öffnen - -
- ) - })()} -
-
-
+ setPreview(null)} + onReport={ + (preview.lifecycle_state || 'active') === 'active' && !preview.legal_hold_active + ? () => { setReportTarget(preview); setPreview(null) } + : null + } + onEdit={() => { openEdit(preview); setPreview(null) }} + /> ) : null} {bulkOpen ? ( diff --git a/frontend/src/version.js b/frontend/src/version.js index 8efb9c6..4964ae7 100644 --- a/frontend/src/version.js +++ b/frontend/src/version.js @@ -1,6 +1,6 @@ // Shinkan Jinkendo Frontend Version -export const APP_VERSION = "0.8.88" +export const APP_VERSION = "0.8.89" export const BUILD_DATE = "2026-05-11" export const PAGE_VERSIONS = { @@ -20,12 +20,14 @@ export const PAGE_VERSIONS = { TrainingCoachPage: "1.0.0", AdminCatalogsPage: "2.2.0", TrainerContextsPage: "1.0.0", - MediaLibraryPage: "1.6.0", // P-11: Legal-Hold-Badge, Superadmin-Aktionen, Bestaetigungs-Dialog + MediaLibraryPage: "1.8.0", // P-13: MediaPreviewModal (geteilt) + Melde-Button in Viewer + ExerciseFormPage: "1.1.0", // P-13: MediaPreviewModal (geteilt) + Melden im Viewer ExerciseInlineFileMediaModal: "1.1.0", // P-06: RightsDeclarationDialog vor Upload ExerciseInlineEmbedModal: "1.1.0", // P-06: RightsDeclarationDialog vor Embed-Upload ExerciseMediaEmbed: "1.1.0", // P-11: Legal-Hold-Placeholder statt Datei ExerciseMediaThumbTile: "1.1.0", // P-11: Legal-Hold-Kachel statt Datei-Vorschau + ExerciseAttachmentMediaStrip: "1.2.0", // P-13: MediaPreviewModal (geteilt) + Melden im Viewer InboxPage: "2.0.0", // P-13: Inhaltsmeldungen-Abschnitt integriert - MediaLibraryPage: "1.7.0", // P-13: Melde-Button an Medienkacheln - ExerciseAttachmentMediaStrip: "1.1.0", // P-13: Melde-Link an angehängten Medien + MediaPreviewModal: "1.0.0", // P-13: geteilter Medienvorschau-Dialog (Melden + Bearbeiten optional) + ReportContentModal: "1.0.0", // P-13: Melde-Formular (Grund, Beschreibung, Name, E-Mail) }