feat: Add endpoint to delete recordings
This commit introduces a new API endpoint for deleting recordings. The endpoint handles the removal of the audio file and its associated sidecar files (transcript, duration). It also invalidates derived artifacts like oneliner.json, document.md, and analysis_input.json. Additionally, it includes checks for path traversal, correct file extensions, and case ownership to ensure security and data integrity. A new `RecordingDeleted` event is also added to the `CaseEventKind` enum.
This commit is contained in:
@@ -14,7 +14,7 @@ header form { margin: 0; }
|
||||
.recording { margin: 1em 0; padding: 0.7em; border: 1px solid #ddd; border-radius: 4px; }
|
||||
.recording.failed-row { background: #fff7f7; border-left: 3px solid #e24a4a; }
|
||||
.recording-head { display: flex; align-items: center; gap: 1em; flex-wrap: wrap; }
|
||||
.rec-time { font-family: monospace; font-size: 0.95em; min-width: 11em; color: #333; }
|
||||
.rec-time { font-size: 0.95em; color: #666; }
|
||||
.filename { font-family: monospace; font-size: 0.8em; color: #999; }
|
||||
.transcript { margin: 0.6em 0 0; padding: 0.6em; background: #f6f6f6; border-radius: 4px; white-space: pre-wrap; font-family: serif; }
|
||||
.pending { color: #888; font-style: italic; margin-top: 0.5em; }
|
||||
@@ -61,6 +61,15 @@ header form { margin: 0; }
|
||||
.header-right { display: flex; align-items: center; gap: 0.8em; }
|
||||
.admin-toggle { font-size: 0.85em; color: #666; display: inline-flex; align-items: center; gap: 0.3em; cursor: pointer; user-select: none; }
|
||||
html.admin-view-off .admin-only { display: none !important; }
|
||||
|
||||
.rec-delete-form { margin: 0; }
|
||||
.rec-delete-form .delete-btn {
|
||||
padding: 0.3em; background: transparent; border: none; color: #888;
|
||||
cursor: pointer; display: inline-flex; align-items: center;
|
||||
line-height: 0; border-radius: 4px;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
.rec-delete-form .delete-btn:hover { color: #e24a4a; background: #fff0f0; }
|
||||
</style>
|
||||
<script>
|
||||
try {
|
||||
@@ -98,8 +107,12 @@ try {
|
||||
<input type="range" class="seek" min="0" max="1000" value="0" step="1" aria-label="Position">
|
||||
<span class="time">0:00 / 0:00</span>
|
||||
</div>
|
||||
{% if is_admin %}<span class="filename admin-only">{{ rec.filename }}</span>{% endif %}
|
||||
<form method="post" action="/web/cases/{{ case_id }}/recordings/delete" class="rec-delete-form" data-ts="{{ rec.recorded_at_iso }}">
|
||||
<input type="hidden" name="filename" value="{{ rec.filename }}">
|
||||
<button type="submit" class="delete-btn" title="Aufnahme entfernen" aria-label="Aufnahme entfernen"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg></button>
|
||||
</form>
|
||||
</div>
|
||||
{% if is_admin %}<div class="filename admin-only">{{ rec.filename }}</div>{% endif %}
|
||||
{% if rec.failed %}
|
||||
<div class="failed">Transkription fehlgeschlagen — .failed-Suffix entfernen zum Erneut-Versuchen.</div>
|
||||
{% else %}
|
||||
@@ -121,6 +134,9 @@ try {
|
||||
</div>
|
||||
{% endfor %}
|
||||
<script>
|
||||
{% include "partials/time_format.js" %}
|
||||
</script>
|
||||
<script>
|
||||
(() => {
|
||||
const cb = document.getElementById('admin-view-toggle');
|
||||
if (cb) {
|
||||
@@ -132,18 +148,30 @@ try {
|
||||
});
|
||||
}
|
||||
|
||||
// Render the timestamp in the browser's local timezone. Server renders UTC
|
||||
// (RFC3339) into `datetime=""`; JS formats it for display.
|
||||
const timeFmt = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
});
|
||||
// Render the timestamp in the browser's local timezone using the same
|
||||
// "Heute/Gestern/DD.MM.YYYY - HH:MM" format as the case page. Server
|
||||
// renders the raw ISO into `datetime=""`; JS replaces the text.
|
||||
const formatRec = d => TimeFmt.dateLabel(d) + ' - ' + TimeFmt.timeLabel(d);
|
||||
document.querySelectorAll('.rec-time[datetime]').forEach(el => {
|
||||
const iso = el.getAttribute('datetime');
|
||||
if (!iso) return;
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return;
|
||||
el.textContent = timeFmt.format(d);
|
||||
el.textContent = formatRec(d);
|
||||
});
|
||||
|
||||
// Native confirm() before destructive delete. If JS is off, the form
|
||||
// submits unconfirmed — acceptable for an internal tool.
|
||||
document.querySelectorAll('.rec-delete-form').forEach(form => {
|
||||
form.addEventListener('submit', e => {
|
||||
const iso = form.dataset.ts;
|
||||
let label = 'diese Aufnahme';
|
||||
if (iso) {
|
||||
const d = new Date(iso);
|
||||
if (!isNaN(d.getTime())) label = 'die Aufnahme vom ' + formatRec(d);
|
||||
}
|
||||
if (!confirm(`Soll ${label} wirklich gelöscht werden?`)) e.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
const fmtDur = s => {
|
||||
|
||||
Reference in New Issue
Block a user