feat: File Locking System (Ein-/Auschecken) + Konflikt-Email

Backend - FileLock Model + API:
- POST /files/<id>/lock - Datei auschecken (sperren)
- POST /files/<id>/unlock - Datei einchecken (entsperren)
- POST /files/<id>/heartbeat - "Datei noch offen" (alle 60s)
- GET /files/<id>/lock-status - Sperrstatus abfragen
- GET /files/locks - Alle aktiven Sperren auflisten
- Auto-Unlock: Kein Heartbeat seit 5 Min -> Sperre wird freigegeben
- 423 Locked wenn bereits von anderem User gesperrt
- Admin kann fremde Sperren aufheben

Dateiliste + Sync-API:
- Lock-Info (locked, locked_by, locked_at) pro Datei mitgeliefert
- Sync-Tree enthaelt Lock-Status fuer Desktop/Mobile-Clients

Web-UI:
- Schloss-Icon mit Benutzername bei gesperrten Dateien
- Tooltip: "Ausgecheckt von Adam seit 14:30"
- Gesperrte Dateien: "Oeffnen nicht moeglich" Toast-Meldung
  (eigene Sperren sind erlaubt)

Konflikt-Email an Admin:
- Wer hat die Konflikt-Kopie erstellt (Name + Email)
- Welche Datei (Name + Ordnerpfad)
- Name der Konflikt-Kopie
- Von wem gesperrt (Name + Email + seit wann)
- Erklaerungstext was passiert ist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-11 23:20:55 +02:00
parent 33156f9431
commit 748537b9f5
5 changed files with 216 additions and 0 deletions
+20
View File
@@ -58,6 +58,9 @@
<i :class="fileIcon(data)" class="file-icon"></i>
<span>{{ data.name }}</span>
<Tag v-if="data.shared" value="Geteilt" severity="info" class="shared-tag" />
<span v-if="data.locked" class="lock-badge" :title="'Ausgecheckt von ' + data.locked_by + ' seit ' + formatDate(data.locked_at)">
<i class="pi pi-lock"></i> {{ data.locked_by }}
</span>
</div>
</template>
</Column>
@@ -223,6 +226,7 @@
<script setup>
import { ref, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useFilesStore } from '../stores/files'
import { useToast } from 'primevue/usetoast'
import apiClient from '../api/client'
@@ -238,6 +242,7 @@ import ProgressBar from 'primevue/progressbar'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const filesStore = useFilesStore()
const toast = useToast()
@@ -298,6 +303,15 @@ function handleDoubleClick(event) {
}
function openPreview(data) {
if (data.locked && data.locked_by !== auth.user?.username) {
toast.add({
severity: 'warn',
summary: 'Datei gesperrt',
detail: `${data.name} wird von ${data.locked_by} bearbeitet. Oeffnen nicht moeglich.`,
life: 5000,
})
return
}
const previewable = /\.(pdf|docx?|xlsx?|pptx?|txt|md|json|xml|csv|py|js|html|css|yml|yaml|png|jpe?g|gif|svg|webp|bmp|odt|ods|odp|rtf)$/i
if (previewable.test(data.name)) {
router.push(`/preview/${data.id}`)
@@ -706,6 +720,12 @@ onMounted(() => {
}
.file-icon { font-size: 1.125rem; width: 1.25rem; text-align: center; }
.shared-tag { font-size: 0.7rem; }
.lock-badge {
display: inline-flex; align-items: center; gap: 0.25rem;
font-size: 0.7rem; color: var(--p-orange-600); background: var(--p-orange-50);
padding: 0.125rem 0.375rem; border-radius: 4px; margin-left: 0.25rem;
}
.lock-badge i { font-size: 0.65rem; }
.row-actions { display: flex; gap: 0; }
.empty-state {
display: flex; flex-direction: column; align-items: center;