A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
615 lines
17 KiB
JavaScript
615 lines
17 KiB
JavaScript
/**
|
||
* BreakPilot Studio - File Manager Module
|
||
*
|
||
* Datei-Verwaltung für den Arbeitsblatt-Editor:
|
||
* - Laden und Rendern der Dateiliste
|
||
* - Upload von Dateien
|
||
* - Löschen von Dateien
|
||
* - Vorschau-Funktionen
|
||
* - Navigation zwischen Dateien
|
||
*
|
||
* Refactored: 2026-01-19
|
||
*/
|
||
|
||
import { t } from './i18n.js';
|
||
import { setStatus, setStatusWorking, setStatusError, setStatusSuccess, fetchJSON } from './api-helpers.js';
|
||
import { openLightbox } from './lightbox.js';
|
||
|
||
// State
|
||
let allEingangFiles = [];
|
||
let eingangFiles = [];
|
||
let currentIndex = 0;
|
||
let currentSelectedFile = null;
|
||
let worksheetPairs = {};
|
||
let allWorksheetPairs = {};
|
||
let showOnlyUnitFiles = false;
|
||
let currentUnitId = null;
|
||
|
||
// DOM References (werden bei init gesetzt)
|
||
let eingangListEl = null;
|
||
let eingangCountEl = null;
|
||
let previewContainer = null;
|
||
let fileInput = null;
|
||
let btnUploadInline = null;
|
||
|
||
/**
|
||
* Initialisiert den File Manager
|
||
* @param {Object} options - Konfiguration
|
||
*/
|
||
export function initFileManager(options = {}) {
|
||
eingangListEl = document.getElementById('eingang-list') || options.listEl;
|
||
eingangCountEl = document.getElementById('eingang-count') || options.countEl;
|
||
previewContainer = document.getElementById('preview-container') || options.previewEl;
|
||
fileInput = document.getElementById('file-input') || options.fileInput;
|
||
btnUploadInline = document.getElementById('btn-upload-inline') || options.uploadBtn;
|
||
|
||
// Upload-Button Event
|
||
if (btnUploadInline) {
|
||
btnUploadInline.addEventListener('click', handleUpload);
|
||
}
|
||
|
||
// Initial load
|
||
loadEingangFiles();
|
||
loadWorksheetPairs();
|
||
}
|
||
|
||
/**
|
||
* Setzt die aktuelle Lerneinheit
|
||
* @param {string} unitId - Die Unit-ID
|
||
*/
|
||
export function setCurrentUnit(unitId) {
|
||
currentUnitId = unitId;
|
||
}
|
||
|
||
/**
|
||
* Setzt den Filter für Lerneinheit-Dateien
|
||
* @param {boolean} show - Nur Unit-Dateien anzeigen
|
||
*/
|
||
export function setShowOnlyUnitFiles(show) {
|
||
showOnlyUnitFiles = show;
|
||
}
|
||
|
||
/**
|
||
* Gibt die aktuelle Dateiliste zurück
|
||
* @returns {string[]} - Liste der Dateinamen
|
||
*/
|
||
export function getFiles() {
|
||
return eingangFiles.slice();
|
||
}
|
||
|
||
/**
|
||
* Gibt den aktuellen Index zurück
|
||
* @returns {number}
|
||
*/
|
||
export function getCurrentIndex() {
|
||
return currentIndex;
|
||
}
|
||
|
||
/**
|
||
* Setzt den aktuellen Index
|
||
* @param {number} idx
|
||
*/
|
||
export function setCurrentIndex(idx) {
|
||
currentIndex = idx;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
}
|
||
|
||
/**
|
||
* Gibt den aktuell ausgewählten Dateinamen zurück
|
||
* @returns {string|null}
|
||
*/
|
||
export function getCurrentFile() {
|
||
return eingangFiles[currentIndex] || null;
|
||
}
|
||
|
||
/**
|
||
* Lädt die Dateien aus dem Eingang
|
||
*/
|
||
export async function loadEingangFiles() {
|
||
try {
|
||
const data = await fetchJSON('/api/eingang-dateien');
|
||
allEingangFiles = data.eingang || [];
|
||
eingangFiles = allEingangFiles.slice();
|
||
currentIndex = 0;
|
||
renderEingangList();
|
||
} catch (e) {
|
||
console.error('Fehler beim Laden der Dateien:', e);
|
||
setStatusError(t('error') || 'Fehler', String(e));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Lädt die Worksheet-Pairs (Original → Bereinigt)
|
||
*/
|
||
export async function loadWorksheetPairs() {
|
||
try {
|
||
const data = await fetchJSON('/api/worksheet-pairs');
|
||
allWorksheetPairs = {};
|
||
(data.pairs || []).forEach((p) => {
|
||
allWorksheetPairs[p.original] = { clean_html: p.clean_html, clean_image: p.clean_image };
|
||
});
|
||
worksheetPairs = { ...allWorksheetPairs };
|
||
renderPreviewForCurrent();
|
||
} catch (e) {
|
||
console.error('Fehler beim Laden der Neuaufbau-Daten:', e);
|
||
setStatusError(t('error') || 'Fehler', String(e));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Rendert die Dateiliste
|
||
*/
|
||
export function renderEingangList() {
|
||
if (!eingangListEl) return;
|
||
|
||
eingangListEl.innerHTML = '';
|
||
|
||
if (!eingangFiles.length) {
|
||
const li = document.createElement('li');
|
||
li.className = 'file-empty';
|
||
li.textContent = t('no_files') || 'Noch keine Dateien vorhanden.';
|
||
eingangListEl.appendChild(li);
|
||
if (eingangCountEl) {
|
||
eingangCountEl.textContent = '0 ' + (t('files') || 'Dateien');
|
||
}
|
||
return;
|
||
}
|
||
|
||
eingangFiles.forEach((filename, idx) => {
|
||
const li = document.createElement('li');
|
||
li.className = 'file-item';
|
||
if (idx === currentIndex) {
|
||
li.classList.add('active');
|
||
}
|
||
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.className = 'file-item-name';
|
||
nameSpan.textContent = filename;
|
||
|
||
const actionsSpan = document.createElement('span');
|
||
actionsSpan.style.display = 'flex';
|
||
actionsSpan.style.gap = '6px';
|
||
|
||
// Button: Aus Lerneinheit entfernen
|
||
const removeFromUnitBtn = document.createElement('span');
|
||
removeFromUnitBtn.className = 'file-item-delete';
|
||
removeFromUnitBtn.textContent = '✕';
|
||
removeFromUnitBtn.title = t('remove_from_unit') || 'Aus Lerneinheit entfernen';
|
||
removeFromUnitBtn.addEventListener('click', (ev) => {
|
||
ev.stopPropagation();
|
||
if (!currentUnitId) {
|
||
alert(t('select_unit_first') || 'Zum Entfernen bitte zuerst eine Lerneinheit auswählen.');
|
||
return;
|
||
}
|
||
const ok = confirm(t('confirm_remove_from_unit') || 'Dieses Arbeitsblatt aus der aktuellen Lerneinheit entfernen? Die Datei selbst bleibt erhalten.');
|
||
if (!ok) return;
|
||
removeWorksheetFromCurrentUnit(eingangFiles[idx]);
|
||
});
|
||
|
||
// Button: Datei komplett löschen
|
||
const deleteFileBtn = document.createElement('span');
|
||
deleteFileBtn.className = 'file-item-delete';
|
||
deleteFileBtn.textContent = '🗑️';
|
||
deleteFileBtn.title = t('delete_file') || 'Datei komplett löschen';
|
||
deleteFileBtn.style.color = '#ef4444';
|
||
deleteFileBtn.addEventListener('click', async (ev) => {
|
||
ev.stopPropagation();
|
||
const ok = confirm(t('confirm_delete_file') || `Datei "${eingangFiles[idx]}" wirklich komplett löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
|
||
if (!ok) return;
|
||
await deleteFileCompletely(eingangFiles[idx]);
|
||
});
|
||
|
||
actionsSpan.appendChild(removeFromUnitBtn);
|
||
actionsSpan.appendChild(deleteFileBtn);
|
||
|
||
li.appendChild(nameSpan);
|
||
li.appendChild(actionsSpan);
|
||
|
||
li.addEventListener('click', () => {
|
||
currentIndex = idx;
|
||
currentSelectedFile = filename;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
// Event für andere Module
|
||
window.dispatchEvent(new CustomEvent('fileSelected', {
|
||
detail: { filename, index: idx }
|
||
}));
|
||
});
|
||
|
||
eingangListEl.appendChild(li);
|
||
});
|
||
|
||
if (eingangCountEl) {
|
||
eingangCountEl.textContent = eingangFiles.length + ' ' + (eingangFiles.length === 1 ? (t('file') || 'Datei') : (t('files') || 'Dateien'));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Rendert die Vorschau für die aktuelle Datei
|
||
*/
|
||
export function renderPreviewForCurrent() {
|
||
if (!previewContainer) return;
|
||
|
||
if (!eingangFiles.length) {
|
||
const message = showOnlyUnitFiles && currentUnitId
|
||
? (t('no_files_in_unit') || 'Dieser Lerneinheit sind noch keine Arbeitsblätter zugeordnet.')
|
||
: (t('no_files') || 'Keine Dateien vorhanden.');
|
||
previewContainer.innerHTML = `<div class="preview-placeholder">${message}</div>`;
|
||
return;
|
||
}
|
||
|
||
if (currentIndex < 0) currentIndex = 0;
|
||
if (currentIndex >= eingangFiles.length) currentIndex = eingangFiles.length - 1;
|
||
|
||
const filename = eingangFiles[currentIndex];
|
||
const entry = worksheetPairs[filename] || { clean_html: null, clean_image: null };
|
||
|
||
renderPreview(entry, currentIndex);
|
||
}
|
||
|
||
/**
|
||
* Rendert die Vorschau (Original vs. Bereinigt)
|
||
* @param {Object} entry - Die Worksheet-Pair-Daten
|
||
* @param {number} index - Der Index
|
||
*/
|
||
function renderPreview(entry, index) {
|
||
if (!previewContainer) return;
|
||
|
||
previewContainer.innerHTML = '';
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'compare-wrapper';
|
||
|
||
// Original-Sektion
|
||
const originalSection = createPreviewSection(
|
||
t('original_scan') || 'Original-Scan',
|
||
t('old_left') || 'Alt (links)',
|
||
() => {
|
||
const img = document.createElement('img');
|
||
img.className = 'preview-img';
|
||
const imgSrc = '/preview-file/' + encodeURIComponent(eingangFiles[index]);
|
||
img.src = imgSrc;
|
||
img.alt = 'Original ' + eingangFiles[index];
|
||
img.addEventListener('dblclick', () => openLightbox(imgSrc, eingangFiles[index]));
|
||
return img;
|
||
}
|
||
);
|
||
|
||
// Bereinigt-Sektion
|
||
const cleanSection = createPreviewSection(
|
||
t('rebuilt_worksheet') || 'Neu aufgebautes Arbeitsblatt',
|
||
createPrintButton(),
|
||
() => {
|
||
if (entry.clean_image) {
|
||
const imgClean = document.createElement('img');
|
||
imgClean.className = 'preview-img';
|
||
const cleanSrc = '/preview-clean-file/' + encodeURIComponent(entry.clean_image);
|
||
imgClean.src = cleanSrc;
|
||
imgClean.alt = 'Neu aufgebaut ' + eingangFiles[index];
|
||
imgClean.addEventListener('dblclick', () => openLightbox(cleanSrc, eingangFiles[index] + ' (neu)'));
|
||
return imgClean;
|
||
} else if (entry.clean_html) {
|
||
const frame = document.createElement('iframe');
|
||
frame.className = 'clean-frame';
|
||
frame.src = '/api/clean-html/' + encodeURIComponent(entry.clean_html);
|
||
frame.title = t('rebuilt_worksheet') || 'Neu aufgebautes Arbeitsblatt';
|
||
frame.addEventListener('dblclick', () => {
|
||
window.open('/api/clean-html/' + encodeURIComponent(entry.clean_html), '_blank');
|
||
});
|
||
return frame;
|
||
} else {
|
||
const placeholder = document.createElement('div');
|
||
placeholder.className = 'preview-placeholder';
|
||
placeholder.textContent = t('no_rebuild_data') || 'Noch keine Neuaufbau-Daten vorhanden.';
|
||
return placeholder;
|
||
}
|
||
}
|
||
);
|
||
|
||
// Thumbnails in der Mitte
|
||
const thumbsColumn = document.createElement('div');
|
||
thumbsColumn.className = 'preview-thumbnails';
|
||
thumbsColumn.id = 'preview-thumbnails-middle';
|
||
renderThumbnailsInColumn(thumbsColumn);
|
||
|
||
wrapper.appendChild(originalSection);
|
||
wrapper.appendChild(thumbsColumn);
|
||
wrapper.appendChild(cleanSection);
|
||
|
||
// Navigation
|
||
const navDiv = createNavigationButtons();
|
||
wrapper.appendChild(navDiv);
|
||
|
||
previewContainer.appendChild(wrapper);
|
||
}
|
||
|
||
/**
|
||
* Erstellt eine Vorschau-Sektion
|
||
*/
|
||
function createPreviewSection(title, rightContent, contentFactory) {
|
||
const section = document.createElement('div');
|
||
section.className = 'compare-section';
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'compare-header';
|
||
|
||
const titleSpan = document.createElement('span');
|
||
titleSpan.textContent = title;
|
||
|
||
const rightSpan = document.createElement('span');
|
||
rightSpan.style.display = 'flex';
|
||
rightSpan.style.alignItems = 'center';
|
||
rightSpan.style.gap = '8px';
|
||
|
||
if (typeof rightContent === 'string') {
|
||
rightSpan.textContent = rightContent;
|
||
} else if (rightContent instanceof Node) {
|
||
rightSpan.appendChild(rightContent);
|
||
}
|
||
|
||
header.appendChild(titleSpan);
|
||
header.appendChild(rightSpan);
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'compare-body';
|
||
|
||
const inner = document.createElement('div');
|
||
inner.className = 'compare-body-inner';
|
||
|
||
const content = contentFactory();
|
||
inner.appendChild(content);
|
||
body.appendChild(inner);
|
||
|
||
section.appendChild(header);
|
||
section.appendChild(body);
|
||
|
||
return section;
|
||
}
|
||
|
||
/**
|
||
* Erstellt den Druck-Button
|
||
*/
|
||
function createPrintButton() {
|
||
const container = document.createElement('span');
|
||
container.style.display = 'flex';
|
||
container.style.alignItems = 'center';
|
||
container.style.gap = '8px';
|
||
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'btn btn-sm btn-ghost no-print';
|
||
btn.style.padding = '4px 10px';
|
||
btn.style.fontSize = '11px';
|
||
btn.textContent = '🖨️ ' + (t('print') || 'Drucken');
|
||
btn.addEventListener('click', () => {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) {
|
||
alert(t('worksheet_no_data') || 'Keine Arbeitsblatt-Daten vorhanden.');
|
||
return;
|
||
}
|
||
window.open('/api/print-worksheet/' + encodeURIComponent(currentFile), '_blank');
|
||
});
|
||
|
||
const label = document.createElement('span');
|
||
label.textContent = t('new_right') || 'Neu (rechts)';
|
||
|
||
container.appendChild(btn);
|
||
container.appendChild(label);
|
||
|
||
return container;
|
||
}
|
||
|
||
/**
|
||
* Rendert Thumbnails in einer Spalte
|
||
*/
|
||
function renderThumbnailsInColumn(container) {
|
||
container.innerHTML = '';
|
||
|
||
if (eingangFiles.length <= 1) return;
|
||
|
||
const maxThumbs = 5;
|
||
let thumbCount = 0;
|
||
|
||
for (let i = 0; i < eingangFiles.length && thumbCount < maxThumbs; i++) {
|
||
if (i === currentIndex) continue;
|
||
|
||
const filename = eingangFiles[i];
|
||
const thumb = document.createElement('div');
|
||
thumb.className = 'preview-thumb';
|
||
|
||
const img = document.createElement('img');
|
||
img.src = '/preview-file/' + encodeURIComponent(filename);
|
||
img.alt = filename;
|
||
|
||
const label = document.createElement('div');
|
||
label.className = 'preview-thumb-label';
|
||
label.textContent = `${i + 1}`;
|
||
|
||
thumb.appendChild(img);
|
||
thumb.appendChild(label);
|
||
|
||
thumb.addEventListener('click', () => {
|
||
currentIndex = i;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
});
|
||
|
||
container.appendChild(thumb);
|
||
thumbCount++;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Erstellt die Navigations-Buttons
|
||
*/
|
||
function createNavigationButtons() {
|
||
const navDiv = document.createElement('div');
|
||
navDiv.className = 'preview-nav';
|
||
|
||
const prevBtn = document.createElement('button');
|
||
prevBtn.type = 'button';
|
||
prevBtn.textContent = '‹';
|
||
prevBtn.disabled = currentIndex === 0;
|
||
prevBtn.addEventListener('click', () => {
|
||
if (currentIndex > 0) {
|
||
currentIndex--;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
}
|
||
});
|
||
|
||
const nextBtn = document.createElement('button');
|
||
nextBtn.type = 'button';
|
||
nextBtn.textContent = '›';
|
||
nextBtn.disabled = currentIndex >= eingangFiles.length - 1;
|
||
nextBtn.addEventListener('click', () => {
|
||
if (currentIndex < eingangFiles.length - 1) {
|
||
currentIndex++;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
}
|
||
});
|
||
|
||
const positionSpan = document.createElement('span');
|
||
positionSpan.textContent = `${currentIndex + 1} ${t('of') || 'von'} ${eingangFiles.length}`;
|
||
|
||
navDiv.appendChild(prevBtn);
|
||
navDiv.appendChild(positionSpan);
|
||
navDiv.appendChild(nextBtn);
|
||
|
||
return navDiv;
|
||
}
|
||
|
||
/**
|
||
* Handle File Upload
|
||
*/
|
||
async function handleUpload(ev) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
if (!fileInput) return;
|
||
|
||
const files = fileInput.files;
|
||
if (!files || !files.length) {
|
||
alert(t('select_files_first') || 'Bitte erst Dateien auswählen.');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
for (const file of files) {
|
||
formData.append('files', file);
|
||
}
|
||
|
||
try {
|
||
setStatusWorking(t('uploading') || 'Upload läuft …');
|
||
|
||
const resp = await fetch('/api/upload-multi', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
console.error('Upload-Fehler: HTTP', resp.status);
|
||
setStatusError(t('upload_error') || 'Fehler beim Upload', 'HTTP ' + resp.status);
|
||
return;
|
||
}
|
||
|
||
setStatusSuccess(t('upload_complete') || 'Upload abgeschlossen');
|
||
fileInput.value = '';
|
||
|
||
// Liste neu laden
|
||
await loadEingangFiles();
|
||
await loadWorksheetPairs();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Upload', e);
|
||
setStatusError(t('network_error') || 'Netzwerkfehler', String(e));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Entfernt ein Arbeitsblatt aus der aktuellen Lerneinheit
|
||
* @param {string} filename - Der Dateiname
|
||
*/
|
||
async function removeWorksheetFromCurrentUnit(filename) {
|
||
if (!currentUnitId) return;
|
||
|
||
try {
|
||
setStatusWorking(t('removing_from_unit') || 'Entferne aus Lerneinheit...');
|
||
|
||
const resp = await fetch(`/api/units/${currentUnitId}/worksheets/${encodeURIComponent(filename)}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
|
||
setStatusSuccess(t('removed_from_unit') || 'Aus Lerneinheit entfernt');
|
||
await loadEingangFiles();
|
||
} catch (e) {
|
||
console.error('Fehler beim Entfernen:', e);
|
||
setStatusError(t('error') || 'Fehler', String(e));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Löscht eine Datei komplett
|
||
* @param {string} filename - Der Dateiname
|
||
*/
|
||
async function deleteFileCompletely(filename) {
|
||
try {
|
||
setStatusWorking(t('deleting_file') || 'Lösche Datei...');
|
||
|
||
const resp = await fetch('/api/delete-file/' + encodeURIComponent(filename), {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
|
||
setStatusSuccess(t('file_deleted') || 'Datei gelöscht');
|
||
await loadEingangFiles();
|
||
await loadWorksheetPairs();
|
||
} catch (e) {
|
||
console.error('Fehler beim Löschen:', e);
|
||
setStatusError(t('error') || 'Fehler', String(e));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Navigiert zur nächsten Datei
|
||
*/
|
||
export function nextFile() {
|
||
if (currentIndex < eingangFiles.length - 1) {
|
||
currentIndex++;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Navigiert zur vorherigen Datei
|
||
*/
|
||
export function prevFile() {
|
||
if (currentIndex > 0) {
|
||
currentIndex--;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Aktualisiert die Worksheet-Pairs für einen bestimmten Dateinamen
|
||
* @param {string} filename
|
||
* @param {Object} pair
|
||
*/
|
||
export function updateWorksheetPair(filename, pair) {
|
||
worksheetPairs[filename] = pair;
|
||
allWorksheetPairs[filename] = pair;
|
||
if (eingangFiles[currentIndex] === filename) {
|
||
renderPreviewForCurrent();
|
||
}
|
||
}
|