diff --git a/backend/routers/media_assets.py b/backend/routers/media_assets.py index e135f67..3b8db3a 100644 --- a/backend/routers/media_assets.py +++ b/backend/routers/media_assets.py @@ -1692,3 +1692,55 @@ def get_legacy_rights_assets( total_row = cur.fetchone() total = int(r2d(total_row)["cnt"]) if total_row else 0 return {"total": total, "limit": limit, "offset": offset, "assets": assets} + + +@admin_rights_router.get("/assets/{asset_id}/journal") +def get_media_asset_journal( + asset_id: int, + tenant: TenantContext = Depends(get_tenant_context), +): + """P-06 Superadmin: Vollständiges Deklarationsjournal für ein Medium.""" + if not is_superadmin(tenant.global_role): + raise HTTPException(status_code=403, detail="Keine Berechtigung (Superadmin erforderlich)") + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + """ + SELECT id, original_filename, visibility, rights_status, + rights_declared_for_visibility, rights_declared_at, + copyright_notice, mime_type, lifecycle_state, + uploaded_by_profile_id, created_at + FROM media_assets + WHERE id = %s + """, + (asset_id,), + ) + asset_row = cur.fetchone() + if not asset_row: + raise HTTPException(status_code=404, detail="Medium nicht gefunden") + asset = r2d(asset_row) + cur.execute( + """ + SELECT d.id, d.declared_at, d.action_type, d.target_visibility, + d.declaration_version, + d.rights_holder_confirmed, + d.contains_identifiable_persons, d.person_consent_confirmed, + d.person_consent_context, + d.contains_minors, d.parental_consent_confirmed, + d.parental_consent_context, + d.contains_music, d.music_rights_confirmed, + d.music_rights_context, + d.contains_third_party_content, d.third_party_rights_confirmed, + d.third_party_rights_context, + d.declared_by_profile_id, + p.username AS declared_by_username, + p.email AS declared_by_email + FROM media_asset_rights_declarations d + LEFT JOIN profiles p ON p.id = d.declared_by_profile_id + WHERE d.media_asset_id = %s + ORDER BY d.declared_at DESC + """, + (asset_id,), + ) + declarations = [r2d(r) for r in cur.fetchall()] + return {"asset": asset, "declarations": declarations} diff --git a/backend/version.py b/backend/version.py index 35012ba..84c5ce4 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.77" +APP_VERSION = "0.8.78" BUILD_DATE = "2026-05-11" DB_SCHEMA_VERSION = "20260511048" @@ -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.1.0", # P-06+: write_rights_declaration + 4 Kontext-Freitextfelder - "media_assets": "1.14.0", # P-06+: copyright_notice im Upload-Dialog; 4 Kontext-Felder in Bulk-Upload + PATCH + Re-Deklaration + "media_assets": "1.15.0", # P-06+: Superadmin-Journal GET /api/admin/media-rights/assets/{id}/journal "groups": "0.1.0", "skills": "0.1.0", "methods": "0.1.0", @@ -31,6 +31,13 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.78", + "date": "2026-05-11", + "changes": [ + "P-06 Superadmin Medienjournal: GET /api/admin/media-rights/assets/{id}/journal liefert vollstaendiges Deklarations-Log pro Medium; Frontend: Journal-Button im Bearbeitungs-Modal (nur Superadmin), scrollbare Timeline aller Einwilligungserklaerungen mit Kontext-Feldern.", + ], + }, { "version": "0.8.77", "date": "2026-05-11", diff --git a/frontend/src/app.css b/frontend/src/app.css index 7faf38b..d77bd4c 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -6530,3 +6530,92 @@ a.analysis-split__nav-item { gap: 0.5rem; flex-shrink: 0; } + +/* Superadmin Media Journal */ +.media-library__journal-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 8px; +} +.media-library__journal-entry { + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 8px; + padding: 12px 14px; +} +.media-library__journal-entry-head { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 4px; +} +.media-library__journal-action { + font-size: 0.78rem; + font-weight: 600; + background: var(--accent); + color: #fff; + border-radius: 4px; + padding: 1px 7px; + letter-spacing: 0.01em; +} +.media-library__journal-arrow { + color: var(--text3); + font-size: 0.85rem; +} +.media-library__journal-vis { + font-size: 0.82rem; + font-weight: 600; + color: var(--text1); +} +.media-library__journal-date { + margin-left: auto; + font-size: 0.78rem; + color: var(--text3); + white-space: nowrap; +} +.media-library__journal-entry-by { + font-size: 0.8rem; + color: var(--text2); + margin-bottom: 8px; +} +.media-library__journal-version { + font-size: 0.74rem; + color: var(--text3); +} +.media-library__journal-fields { + display: flex; + flex-direction: column; + gap: 4px; +} +.media-library__journal-field { + display: flex; + align-items: baseline; + gap: 8px; + flex-wrap: wrap; + font-size: 0.82rem; +} +.media-library__journal-field-label { + color: var(--text2); + min-width: 180px; +} +.media-library__journal-field-val { + font-weight: 600; + border-radius: 3px; + padding: 0 5px; + font-size: 0.78rem; +} +.media-library__journal-field-val--yes { + background: rgba(29, 158, 117, 0.12); + color: var(--accent-dark); +} +.media-library__journal-field-val--no { + background: rgba(0, 0, 0, 0.06); + color: var(--text2); +} +.media-library__journal-field-context { + font-size: 0.78rem; + color: var(--text2); + font-style: italic; +} diff --git a/frontend/src/pages/MediaLibraryPage.jsx b/frontend/src/pages/MediaLibraryPage.jsx index b90d119..315aee0 100644 --- a/frontend/src/pages/MediaLibraryPage.jsx +++ b/frontend/src/pages/MediaLibraryPage.jsx @@ -18,6 +18,7 @@ import { FileText, File, Upload, + ScrollText, } from 'lucide-react' import { useAuth } from '../context/AuthContext' import api from '../utils/api' @@ -231,6 +232,19 @@ function MediaTypeGlyph({ mimeType, compact }) { ) } +function JournalBoolField({ label, val, context }) { + if (val === null || val === undefined) return null + return ( +
Noch keine Einwilligungserklärungen für dieses Medium erfasst.
+ ) : ( +