feat(sources): PDF-Upload auch in der Endkunden-App (Kundenquelle)

- POST /api/sources/upload-pdf: tenant-scoped Upload, gleiche Speicher-
  Konvention wie der Verwaltungs-Endpoint (<dirname(DB)>/pdfs/{sha}.pdf).
  Duplikat-Check beruecksichtigt globale Quellen.
- dashboard.html: +PDF-Button in der Quellenverwaltungs-Toolbar +
  eigenes Modal modal-pdf-upload (closeModal-Quotes via &#39;).
- app.js: App.openPdfUpload + _bindPdfUploadFormOnce (Submit nur einmal
  binden).
- api.js: API.upload(path, formData) Helper analog Verwaltung.
Dieser Commit ist enthalten in:
Claude Code
2026-05-16 23:57:32 +00:00
Ursprung e68386f6bb
Commit 168fbc3987
4 geänderte Dateien mit 255 neuen und 1 gelöschten Zeilen

Datei anzeigen

@@ -555,6 +555,7 @@
<input type="text" id="sources-search" class="timeline-filter-input sources-search-input" placeholder="Suche..." oninput="App.filterSources()" data-i18n-attr="placeholder:sources_modal.search_placeholder">
</div>
<div class="sources-toolbar-actions">
<button class="btn btn-secondary btn-small" onclick="App.openPdfUpload()" style="margin-right:8px;">+ PDF hochladen</button>
<button class="btn btn-primary btn-small" onclick="App.toggleSourceForm()" data-i18n="sources_modal.add_source">+ Quelle</button>
</div>
</div>
@@ -633,6 +634,57 @@
</div>
</div>
<!-- Modal: PDF als Quelle hochladen -->
<div class="modal-overlay" id="modal-pdf-upload" role="dialog" aria-modal="true" aria-labelledby="modal-pdf-upload-title">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="modal-pdf-upload-title">PDF als Quelle hochladen</div>
<button class="modal-close" onclick="closeModal(&#39;modal-pdf-upload&#39;)" aria-label="Schliessen">&times;</button>
</div>
<form id="pdf-upload-form" enctype="multipart/form-data">
<div class="modal-body">
<p class="text-secondary" style="margin-top:0;">
Die PDF wird gespeichert und im Hintergrund verarbeitet: Text wird extrahiert (OCR-Fallback fuer gescannte Dokumente) und nach Deutsch und Englisch uebersetzt. Sie erscheint danach in Ihrer Quellenliste.
</p>
<div class="form-group">
<label for="pdf-upload-file">PDF-Datei (max. 50 MB)</label>
<input type="file" id="pdf-upload-file" accept="application/pdf,.pdf" required>
</div>
<div class="form-group">
<label for="pdf-upload-name">Anzeige-Name (optional)</label>
<input type="text" id="pdf-upload-name" maxlength="200" placeholder="leer = Dateiname">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-group">
<label for="pdf-upload-category">Kategorie</label>
<select id="pdf-upload-category">
<option value="sonstige" selected>Sonstige</option>
<option value="behoerde">Behoerde</option>
<option value="think-tank">Think-Tank</option>
<option value="fachmedien">Fachmedien</option>
<option value="international">International</option>
</select>
</div>
<div class="form-group">
<label for="pdf-upload-language">Sprache (optional)</label>
<input type="text" id="pdf-upload-language" placeholder="z.B. Deutsch, Englisch">
</div>
</div>
<div class="form-group">
<label for="pdf-upload-notes">Notizen</label>
<input type="text" id="pdf-upload-notes" placeholder="Optional">
</div>
<div id="pdf-upload-error" class="error-msg" style="display:none"></div>
<div id="pdf-upload-progress" class="text-secondary" style="display:none;margin-top:8px;">Laedt hoch &hellip;</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal(&#39;modal-pdf-upload&#39;)">Abbrechen</button>
<button type="submit" class="btn btn-primary" id="pdf-upload-submit">Hochladen</button>
</div>
</form>
</div>
</div>
<!-- Modal: Content-Viewer (wiederverwendbar für Lagebild, Faktencheck, Quellenübersicht, Timeline) -->
<div class="modal-overlay" id="modal-content-viewer" role="dialog" aria-modal="true" aria-labelledby="content-viewer-title">
<div class="modal modal-content-viewer">

Datei anzeigen

@@ -22,6 +22,31 @@ const API = {
};
},
async upload(path, formData) {
const token = localStorage.getItem("osint_token");
const headers = {};
if (token) headers["Authorization"] = `Bearer ${token}`;
const response = await fetch(`${this.baseUrl}${path}`, {
method: "POST",
headers,
body: formData,
});
if (response.status === 401) {
localStorage.removeItem("osint_token");
localStorage.removeItem("osint_username");
window.location.href = "/";
return;
}
if (!response.ok) {
const data = await response.json().catch(() => ({}));
let d = data.detail;
if (Array.isArray(d)) d = d.map(e => e.msg || JSON.stringify(e)).join("; ");
else if (typeof d === "object" && d !== null) d = JSON.stringify(d);
throw new Error(d || `Fehler ${response.status}`);
}
return response.json();
},
async _request(method, path, body = null, externalSignal = null) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);

Datei anzeigen

@@ -3106,6 +3106,70 @@ async handleRefresh() {
_discoveredData: null,
openPdfUpload() {
const form = document.getElementById("pdf-upload-form");
if (form) form.reset();
const err = document.getElementById("pdf-upload-error");
if (err) { err.style.display = "none"; err.textContent = ""; }
const prog = document.getElementById("pdf-upload-progress");
if (prog) prog.style.display = "none";
openModal("modal-pdf-upload");
this._bindPdfUploadFormOnce();
},
_bindPdfUploadFormOnce() {
const form = document.getElementById("pdf-upload-form");
if (!form || form.dataset.bound === "1") return;
form.dataset.bound = "1";
form.addEventListener("submit", async (e) => {
e.preventDefault();
const errEl = document.getElementById("pdf-upload-error");
const progEl = document.getElementById("pdf-upload-progress");
const submitBtn = document.getElementById("pdf-upload-submit");
errEl.style.display = "none";
const fileInput = document.getElementById("pdf-upload-file");
const f = fileInput && fileInput.files && fileInput.files[0];
if (!f) {
errEl.textContent = "Bitte eine PDF-Datei auswaehlen.";
errEl.style.display = "block";
return;
}
if (f.size > 50 * 1024 * 1024) {
errEl.textContent = "Datei ueberschreitet 50 MB.";
errEl.style.display = "block";
return;
}
const fd = new FormData();
fd.append("file", f);
const nm = (document.getElementById("pdf-upload-name").value || "").trim();
if (nm) fd.append("name", nm);
fd.append("category", document.getElementById("pdf-upload-category").value || "sonstige");
const lng = (document.getElementById("pdf-upload-language").value || "").trim();
if (lng) fd.append("language", lng);
const nt = (document.getElementById("pdf-upload-notes").value || "").trim();
if (nt) fd.append("notes", nt);
submitBtn.disabled = true;
progEl.style.display = "block";
try {
await API.upload("/sources/upload-pdf", fd);
closeModal("modal-pdf-upload");
if (typeof UI !== "undefined" && UI.showToast) {
UI.showToast("PDF hochgeladen -- Verarbeitung laeuft im Hintergrund", "success");
}
await App.loadSources();
} catch (err) {
errEl.textContent = err && err.message ? err.message : "Upload fehlgeschlagen";
errEl.style.display = "block";
} finally {
submitBtn.disabled = false;
progEl.style.display = "none";
}
});
},
toggleSourceForm(show) {
const form = document.getElementById('sources-add-form');
if (!form) return;