From bb4d92709017390f0dee5b03cb61cbf98fd9e718 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 11 May 2026 21:16:21 +0200 Subject: [PATCH] fix(P-13): leserliche Journals, readOnly-Felder, Meldungs-Badge auf Medienkacheln - ReportContentModal: Name/E-Mail readOnly fuer eingeloggte Nutzer - MediaLibraryPage: content_report_filed im Journal leserlich (Meldegrund, Prioritaet, Status DE) - MediaLibraryPage: Badge auf Medienkacheln zeigt offene Meldungsanzahl (nur Admins) - media_assets.py: open_report_count Subquery fuer Admin-Sicht in Listendaten - Inbox-500-Fix: ma.media_kind -> ma.mime_type (alle 3 Stellen) - PATCH content_reports: Statuswechsel wird in Audit-Log protokolliert - E-Mails: Dateiname statt Medium-ID, lesbarer Inhalt version: 0.8.91 Co-Authored-By: Claude Sonnet 4.6 --- backend/routers/content_reports.py | 82 +++++++++++++++---- backend/routers/media_assets.py | 16 ++++ backend/version.py | 14 +++- .../src/components/ReportContentModal.jsx | 8 +- frontend/src/pages/MediaLibraryPage.jsx | 48 +++++++++++ frontend/src/version.js | 6 +- 6 files changed, 151 insertions(+), 23 deletions(-) diff --git a/backend/routers/content_reports.py b/backend/routers/content_reports.py index d00b3d3..454e444 100644 --- a/backend/routers/content_reports.py +++ b/backend/routers/content_reports.py @@ -341,32 +341,33 @@ def _notify_report_submitted( priority: str, target_type: str, target_id: int, + target_label: str, admin_emails: list[str], ) -> None: """Sendet Bestaetigungs-Mail an Melder + Benachrichtigung an alle Plattform-Admins (best-effort).""" app_url = (os.getenv("APP_URL") or "https://shinkan.jinkendo.de").rstrip("/") reason_label = REASON_LABELS_DE.get(reason, reason) - target_label = ("Medium" if target_type == "media_asset" else "Übung") + f" #{target_id}" + prio_label = "DRINGEND" if priority == "high" else "normal" # Bestaetigungs-Mail an Melder confirmation_body = ( f"Hallo {reporter_name},\n\n" f"Ihre Meldung (#{report_id}) wurde entgegengenommen.\n\n" - f"Meldegrund: {reason_label}\n" + f"Meldegrund: {reason_label}\n" f"Gemeldeter Inhalt: {target_label}\n\n" - f"Ein Administrator wird Ihre Meldung zeitnah prüfen.\n\n" - f"Shinkan Jinkendo" + f"Ein Administrator wird Ihre Meldung zeitnah prüfen. Sie müssen nichts weiter tun.\n\n" + f"Shinkan Jinkendo\n" + f"{app_url}" ) _send_email(reporter_email, f"Meldung #{report_id} eingegangen – Shinkan Jinkendo", confirmation_body) # Admin-Benachrichtigung - prio_label = "DRINGEND" if priority == "high" else "normal" admin_body = ( f"Neue Inhaltsmeldung #{report_id} [{prio_label}]\n\n" - f"Meldegrund: {reason_label}\n" - f"Ziel: {target_label}\n" - f"Gemeldet von: {reporter_name} <{reporter_email}>\n\n" - f"Zur Inbox: {app_url}/inbox\n" + f"Meldegrund: {reason_label}\n" + f"Gemeldeter Inhalt: {target_label}\n" + f"Gemeldet von: {reporter_name} <{reporter_email}>\n\n" + f"Posteingang öffnen: {app_url}/inbox\n" ) subject = f"Inhaltsmeldung #{report_id} [{prio_label}] – Shinkan Jinkendo" for email in admin_emails: @@ -456,6 +457,24 @@ def submit_content_report( result = r2d(row) report_id = int(result["id"]) + # Dateinamen / Zielbezeichnung fuer E-Mail abfragen + target_label_for_email = f"{'Medium' if body.target_type == 'media_asset' else 'Übung'} #{body.target_id}" + if body.target_type == "media_asset": + cur.execute("SELECT original_filename FROM media_assets WHERE id = %s", (body.target_id,)) + fn_row = cur.fetchone() + if fn_row: + fn = r2d(fn_row).get("original_filename") or "" + if fn: + target_label_for_email = f"{fn} (Medium #{body.target_id})" + + elif body.target_type == "exercise": + cur.execute("SELECT name FROM exercises WHERE id = %s", (body.target_id,)) + ex_row = cur.fetchone() + if ex_row: + ex_name = r2d(ex_row).get("name") or "" + if ex_name: + target_label_for_email = f"{ex_name} (Übung #{body.target_id})" + # Audit-Log-Eintrag (nur fuer media_asset-Meldungen) if body.target_type == "media_asset": write_audit_log_entry( @@ -466,9 +485,8 @@ def submit_content_report( {}, { "content_report_id": report_id, - "report_reason": body.report_reason, - "priority": priority, - "reporter_email": body.reporter_email, + "report_reason": REASON_LABELS_DE.get(body.report_reason, body.report_reason), + "priority": "hoch" if priority == "high" else "normal", }, ) @@ -486,6 +504,7 @@ def submit_content_report( priority=priority, target_type=body.target_type, target_id=body.target_id, + target_label=target_label_for_email, admin_emails=admin_emails, ) @@ -539,7 +558,7 @@ def list_inbox_content_reports( cr.updated_at, ma.original_filename AS target_filename, ma.visibility AS target_visibility, - ma.media_kind AS target_media_kind, + ma.mime_type AS target_mime_type, ma.legal_hold_active AS target_legal_hold_active, ex.name AS target_exercise_name, rev.name AS reviewed_by_name, @@ -596,7 +615,7 @@ def list_inbox_content_reports( cr.updated_at, ma.original_filename AS target_filename, ma.visibility AS target_visibility, - ma.media_kind AS target_media_kind, + ma.mime_type AS target_mime_type, ma.legal_hold_active AS target_legal_hold_active, ex.name AS target_exercise_name, rev.name AS reviewed_by_name, @@ -643,7 +662,7 @@ def get_content_report( cr.*, ma.original_filename AS target_filename, ma.visibility AS target_visibility, - ma.media_kind AS target_media_kind, + ma.mime_type AS target_mime_type, ma.legal_hold_active AS target_legal_hold_active, ma.legal_hold_reason_code AS target_legal_hold_reason_code, ex.name AS target_exercise_name, @@ -686,12 +705,25 @@ def patch_content_report( detail="Bei Abschluss ohne Massnahme ist eine Begründung (resolution_note) Pflicht.", ) + STATUS_LABELS_DE = { + "submitted": "Eingegangen", + "under_review": "In Bearbeitung", + "resolved_no_action": "Abgeschlossen (kein Handlungsbedarf)", + "resolved_legal_hold": "Abgeschlossen (Legal Hold)", + "rejected_invalid": "Abgewiesen (ungültig)", + } + with get_db() as conn: cur = get_cursor(conn) - cur.execute("SELECT id, status FROM content_reports WHERE id = %s", (report_id,)) + cur.execute( + "SELECT id, status, target_type, target_id FROM content_reports WHERE id = %s", + (report_id,), + ) row = cur.fetchone() if not row: raise HTTPException(status_code=404, detail="Meldung nicht gefunden") + old_report = r2d(row) + old_status = old_report["status"] updates = ["updated_at = NOW()"] params = [] @@ -721,6 +753,24 @@ def patch_content_report( params, ) updated = r2d(cur.fetchone()) + new_status = updated["status"] + + # Audit-Log-Eintrag bei Status-Aenderung auf media_asset-Meldungen + if body.status is not None and old_status != new_status and old_report["target_type"] == "media_asset": + resolution = (body.resolution_note or "").strip() or None + write_audit_log_entry( + cur, + int(old_report["target_id"]), + pid, + "content_report_filed", + {"status": STATUS_LABELS_DE.get(old_status, old_status)}, + { + "content_report_id": report_id, + "status": STATUS_LABELS_DE.get(new_status, new_status), + **({"begründung": resolution} if resolution else {}), + }, + ) + conn.commit() return {"ok": True, "id": updated["id"], "status": updated["status"]} diff --git a/backend/routers/media_assets.py b/backend/routers/media_assets.py index 975eabe..4ed4a79 100644 --- a/backend/routers/media_assets.py +++ b/backend/routers/media_assets.py @@ -1168,10 +1168,26 @@ def list_media_assets( show_club = sup or is_adm or bool(admin_club_ids) asset_ids = [int(r["id"]) for r in rows] usage_map = _usage_for_media_assets(cur, asset_ids) + # open_report_count: nur für Admins relevant; immer befüllen (0 für normale Nutzer ok) + report_count_map: dict[int, int] = {} + if asset_ids and (is_adm or bool(admin_club_ids)): + cur.execute( + """SELECT target_id, COUNT(*) AS cnt + FROM content_reports + WHERE target_type = 'media_asset' + AND target_id = ANY(%s) + AND status IN ('submitted', 'under_review') + GROUP BY target_id""", + (asset_ids,), + ) + for row in cur.fetchall(): + rd = r2d(row) + report_count_map[int(rd["target_id"])] = int(rd["cnt"]) for r in rows: r["permissions"] = _item_permissions(r, tenant, admin_club_ids) tid = int(r["id"]) r["usage"] = usage_map.get(tid, {"exercises": [], "training_units": []}) + r["open_report_count"] = report_count_map.get(tid, 0) tags_val = r.get("tags") if tags_val is None: r["tags"] = [] diff --git a/backend/version.py b/backend/version.py index 0340bfc..b3b3770 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.90" +APP_VERSION = "0.8.91" BUILD_DATE = "2026-05-11" DB_SCHEMA_VERSION = "20260511053" @@ -15,7 +15,7 @@ MODULE_VERSIONS = { "admin_users": "1.0.0", # GET /api/admin/users "platform_media_storage": "1.0.0", # GET/PUT /api/admin/platform-media-storage (Superadmin-Pfad unter MEDIA_ROOT) "media_rights": "1.3.1", # acting_profile_id in write_audit_log_entry auf Optional[int] (P-13 anonyme Meldungen) - "media_assets": "1.18.0", # P-11: Legal-Hold nur fuer Superadmin sichtbar (nicht fuer alle Plattform-Admins) + "media_assets": "1.18.1", # P-13: open_report_count in Listendaten (fuer Admins) "media_legal_hold": "1.0.0", # P-11: Sofortsperre-Services (set_legal_hold, release_legal_hold) "media_lifecycle": "1.1.0", # P-11: Retention-Job ueberspringt Legal-Hold-Assets "groups": "0.1.0", @@ -34,6 +34,16 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.91", + "date": "2026-05-11", + "changes": [ + "Fix P-13: Name- und E-Mail-Felder im Melde-Dialog fuer eingeloggte Nutzer nicht mehr bearbeitbar (readOnly).", + "Fix P-13: Journaleintraege fuer content_report_filed leserlich (Meldegrund, Prioritaet, Status auf Deutsch).", + "Fix P-13: Badge auf Medienkacheln zeigt Anzahl offener Meldungen (nur fuer Admins).", + "Fix P-13: Statuswechsel an Meldungen werden im Journal des Mediums protokolliert.", + ], + }, { "version": "0.8.90", "date": "2026-05-11", diff --git a/frontend/src/components/ReportContentModal.jsx b/frontend/src/components/ReportContentModal.jsx index fb3ad57..0476560 100644 --- a/frontend/src/components/ReportContentModal.jsx +++ b/frontend/src/components/ReportContentModal.jsx @@ -143,7 +143,9 @@ export default function ReportContentModal({ targetType, targetId, targetLabel, type="text" className="form-input" value={name} - onChange={(e) => setName(e.target.value)} + onChange={user?.name ? undefined : (e) => setName(e.target.value)} + readOnly={!!user?.name} + style={user?.name ? { background: 'var(--surface2)', color: 'var(--text2)' } : undefined} required /> @@ -154,7 +156,9 @@ export default function ReportContentModal({ targetType, targetId, targetLabel, type="email" className="form-input" value={email} - onChange={(e) => setEmail(e.target.value)} + onChange={user?.email ? undefined : (e) => setEmail(e.target.value)} + readOnly={!!user?.email} + style={user?.email ? { background: 'var(--surface2)', color: 'var(--text2)' } : undefined} required /> diff --git a/frontend/src/pages/MediaLibraryPage.jsx b/frontend/src/pages/MediaLibraryPage.jsx index 7be3454..e45005f 100644 --- a/frontend/src/pages/MediaLibraryPage.jsx +++ b/frontend/src/pages/MediaLibraryPage.jsx @@ -254,6 +254,7 @@ function eventTypeLabel(et) { lifecycle_change: 'Lifecycle geändert', legal_hold_set: 'Sofortsperre gesetzt', legal_hold_released: 'Sofortsperre aufgehoben', + content_report_filed: 'Inhaltsmeldung', } return MAP[et] || et } @@ -978,6 +979,20 @@ export default function MediaLibraryPage() { title="Altbestand – Rechtserklärung nach neuem Standard (P-06) noch nicht erfasst" >Altbestand ⚠ )} + {it.open_report_count > 0 && ( + 1 ? 'en' : ''}`} + >{it.open_report_count} Meldung{it.open_report_count > 1 ? 'en' : ''} + )} {(it.tags || []).length ? (
@@ -1598,6 +1613,39 @@ export default function MediaLibraryPage() { {nw.rights_status || '—'}
+ ) : evt.event_type === 'content_report_filed' ? ( + <> + {nw.content_report_id ? ( +
+ Meldungs-ID: + #{nw.content_report_id} +
+ ) : null} + {nw.report_reason ? ( +
+ Meldegrund: + {nw.report_reason} +
+ ) : null} + {nw.priority ? ( +
+ Priorität: + {nw.priority} +
+ ) : null} + {nw.status ? ( +
+ Status: + {old.status ? `${old.status} → ` : ''}{nw.status} +
+ ) : null} + {nw.begründung ? ( +
+ Begründung: + {nw.begründung} +
+ ) : null} + ) : ( Object.keys(nw).map((k) => (
diff --git a/frontend/src/version.js b/frontend/src/version.js index 38e69ef..a278c2a 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.90" +export const APP_VERSION = "0.8.91" export const BUILD_DATE = "2026-05-11" export const PAGE_VERSIONS = { @@ -20,7 +20,7 @@ export const PAGE_VERSIONS = { TrainingCoachPage: "1.0.0", AdminCatalogsPage: "2.2.0", TrainerContextsPage: "1.0.0", - MediaLibraryPage: "1.8.0", // P-13: MediaPreviewModal (geteilt) + Melde-Button in Viewer + MediaLibraryPage: "1.9.0", // P-13: open_report_count Badge + Journal content_report_filed 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 @@ -30,5 +30,5 @@ export const PAGE_VERSIONS = { InboxPage: "2.1.0", // P-13: target_filename/target_exercise_name fix; Club-Admin-Sicht OrgInboxContext: "1.1.0", // P-13: canSeeContentReports schliesst Club-Admins ein 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) + ReportContentModal: "1.1.0", // P-13: Name/E-Mail readOnly fuer eingeloggte Nutzer }