Some checks failed
Build & Push Docker Image / build-and-push (push) Failing after 5s
- FastAPI-based Matrix chat export tool with E2E support - Dockerfile with Python 3.12-slim base image - Docker Compose with dedicated network configuration - Gitea Actions workflow for automated image builds - README with setup instructions and feature overview - MIT license Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
655 lines
22 KiB
HTML
655 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>Matrix Chat Export</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0a0e14;
|
|
--surface: #131920;
|
|
--surface2: #1a2230;
|
|
--surface3: #222d3a;
|
|
--text: #e6edf3;
|
|
--text2: #8b949e;
|
|
--accent: #00a884;
|
|
--accent2: #06cf9c;
|
|
--danger: #f85149;
|
|
--warning: #d29922;
|
|
--border: #21262d;
|
|
--radius: 12px;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--bg); color: var(--text); line-height: 1.5;
|
|
min-height: 100vh;
|
|
}
|
|
.container { max-width: 800px; margin: 0 auto; padding: 24px 16px; }
|
|
|
|
h1 { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
|
|
h1 span { color: var(--accent); }
|
|
.subtitle { color: var(--text2); font-size: 14px; margin-bottom: 32px; }
|
|
|
|
.card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: 24px; margin-bottom: 16px;
|
|
transition: opacity .3s;
|
|
}
|
|
.card.disabled { opacity: .4; pointer-events: none; }
|
|
.card h2 {
|
|
font-size: 16px; font-weight: 600; margin-bottom: 16px;
|
|
display: flex; align-items: center; gap: 10px;
|
|
}
|
|
.card h2 .num {
|
|
background: var(--accent); color: #fff; width: 28px; height: 28px;
|
|
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
font-size: 13px; flex-shrink: 0;
|
|
}
|
|
.card h2 .check {
|
|
background: var(--accent2); color: #000; width: 28px; height: 28px;
|
|
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
font-size: 15px; flex-shrink: 0;
|
|
}
|
|
|
|
.form-row { margin-bottom: 12px; }
|
|
.form-row label { display: block; font-size: 13px; color: var(--text2); margin-bottom: 4px; }
|
|
.form-row input, .form-row select {
|
|
width: 100%; padding: 10px 14px; background: var(--surface2);
|
|
border: 1px solid var(--border); border-radius: 8px; color: var(--text);
|
|
font-size: 14px; outline: none; transition: border-color .2s;
|
|
}
|
|
.form-row input:focus { border-color: var(--accent); }
|
|
.form-row input[type="file"] { padding: 8px; }
|
|
|
|
.tabs { display: flex; gap: 0; margin-bottom: 16px; }
|
|
.tab {
|
|
flex: 1; padding: 8px; text-align: center; font-size: 13px; font-weight: 500;
|
|
cursor: pointer; background: var(--surface2); border: 1px solid var(--border);
|
|
color: var(--text2); transition: all .2s;
|
|
}
|
|
.tab:first-child { border-radius: 8px 0 0 8px; }
|
|
.tab:last-child { border-radius: 0 8px 8px 0; }
|
|
.tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
|
|
.btn {
|
|
display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px;
|
|
background: var(--accent); color: #fff; border: none; border-radius: 8px;
|
|
font-size: 14px; font-weight: 600; cursor: pointer; transition: all .2s;
|
|
text-decoration: none;
|
|
}
|
|
.btn:hover { background: var(--accent2); color: #000; }
|
|
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
.btn.secondary { background: var(--surface3); color: var(--text); }
|
|
.btn.secondary:hover { background: var(--surface2); }
|
|
.btn.danger { background: var(--danger); }
|
|
.btn-row { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
|
|
|
|
.status {
|
|
margin-top: 12px; padding: 10px 14px; border-radius: 8px;
|
|
font-size: 13px; display: none;
|
|
}
|
|
.status.success { display: block; background: rgba(0,168,132,.15); color: var(--accent2); border: 1px solid rgba(0,168,132,.3); }
|
|
.status.error { display: block; background: rgba(248,81,73,.15); color: var(--danger); border: 1px solid rgba(248,81,73,.3); }
|
|
.status.info { display: block; background: rgba(56,139,253,.15); color: #58a6ff; border: 1px solid rgba(56,139,253,.3); }
|
|
|
|
.room-search {
|
|
margin-bottom: 12px;
|
|
}
|
|
.room-search input {
|
|
width: 100%; padding: 10px 14px; background: var(--surface2);
|
|
border: 1px solid var(--border); border-radius: 8px; color: var(--text);
|
|
font-size: 14px; outline: none;
|
|
}
|
|
.room-search input:focus { border-color: var(--accent); }
|
|
|
|
.room-list { display: flex; flex-direction: column; gap: 4px; max-height: 400px; overflow-y: auto; }
|
|
.room-card {
|
|
display: flex; align-items: center; gap: 12px;
|
|
padding: 10px 14px; background: var(--surface2); border: 1px solid var(--border);
|
|
border-radius: 8px; cursor: pointer; transition: all .15s; flex-shrink: 0;
|
|
}
|
|
.room-card:hover { border-color: var(--accent); background: var(--surface3); }
|
|
.room-card .name {
|
|
font-weight: 500; font-size: 14px; flex: 1;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.room-card .meta { font-size: 12px; color: var(--text2); white-space: nowrap; }
|
|
.room-card .badge {
|
|
font-size: 11px; padding: 2px 8px; border-radius: 20px;
|
|
background: var(--surface3); color: var(--text2); white-space: nowrap;
|
|
}
|
|
.room-card .badge.enc { background: rgba(210,153,34,.2); color: var(--warning); }
|
|
|
|
.log-box {
|
|
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
|
|
padding: 16px; max-height: 400px; overflow-y: auto; font-family: 'JetBrains Mono',
|
|
'Cascadia Code', 'Fira Code', monospace; font-size: 12.5px; line-height: 1.7;
|
|
}
|
|
.log-line { padding: 1px 0; }
|
|
.log-line.info { color: #58a6ff; }
|
|
.log-line.progress { color: var(--accent2); }
|
|
.log-line.warning { color: var(--warning); }
|
|
.log-line.error { color: var(--danger); }
|
|
.log-line.done { color: var(--accent2); font-weight: 600; }
|
|
|
|
.progress-bar {
|
|
width: 100%; height: 6px; background: var(--surface3); border-radius: 3px;
|
|
margin: 12px 0; overflow: hidden;
|
|
}
|
|
.progress-bar .fill {
|
|
height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2));
|
|
border-radius: 3px; transition: width .3s ease; width: 0%;
|
|
}
|
|
|
|
.export-item {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 12px 16px; background: var(--surface2); border-radius: 8px;
|
|
margin-bottom: 8px; border: 1px solid var(--border); gap: 8px;
|
|
}
|
|
.export-item .info { flex: 1; min-width: 0; }
|
|
.export-item .info .name { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.export-item .info .meta { font-size: 12px; color: var(--text2); }
|
|
.export-item .actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
|
|
.note {
|
|
background: var(--surface2); border-left: 3px solid var(--warning);
|
|
padding: 12px 16px; border-radius: 0 8px 8px 0; margin-bottom: 16px;
|
|
font-size: 13px; color: var(--text2);
|
|
}
|
|
.note strong { color: var(--text); }
|
|
|
|
.hidden { display: none !important; }
|
|
|
|
@media (max-width: 600px) {
|
|
.container { padding: 16px 12px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1><span>▩</span> Matrix Chat Export</h1>
|
|
<p class="subtitle">Exportiere verschlüsselte Matrix-Chats als schöne HTML-Dateien mit Medien</p>
|
|
|
|
<!-- STEP 1: Connect -->
|
|
<div class="card" id="card-connect">
|
|
<h2><span class="num" id="step1-icon">1</span> Verbinden</h2>
|
|
|
|
<div class="tabs">
|
|
<div class="tab active" onclick="switchTab('password')">Passwort</div>
|
|
<div class="tab" onclick="switchTab('token')">Access Token</div>
|
|
</div>
|
|
|
|
<div id="tab-password">
|
|
<div class="form-row">
|
|
<label>Homeserver URL</label>
|
|
<input type="url" id="hs-url" placeholder="https://matrix.example.com" value="">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Benutzername (ohne @...)</label>
|
|
<input type="text" id="username" placeholder="benutzername" autocomplete="username">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Passwort</label>
|
|
<input type="password" id="password" placeholder="Passwort" autocomplete="current-password">
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-token" class="hidden">
|
|
<div class="form-row">
|
|
<label>Homeserver URL</label>
|
|
<input type="url" id="hs-url2" placeholder="https://matrix.example.com">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Access Token</label>
|
|
<input type="password" id="access-token" placeholder="syt_...">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<button class="btn" id="btn-connect" onclick="doConnect()">Verbinden</button>
|
|
<button class="btn danger hidden" id="btn-disconnect" onclick="doDisconnect()">Trennen</button>
|
|
</div>
|
|
<div class="status" id="connect-status"></div>
|
|
</div>
|
|
|
|
<!-- STEP 2: Import Keys -->
|
|
<div class="card disabled" id="card-keys">
|
|
<h2><span class="num" id="step2-icon">2</span> E2E-Schlüssel importieren</h2>
|
|
|
|
<div class="note">
|
|
<strong>Wichtig:</strong> Exportiere deine E2E-Schlüssel aus Element:
|
|
Einstellungen → Sicherheit → "E2E-Raumschlüssel exportieren".
|
|
Die exportierte Datei und das Passwort werden hier benötigt.
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label>Schlüsseldatei (.txt)</label>
|
|
<input type="file" id="key-file" accept=".txt,.key">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Export-Passphrase</label>
|
|
<input type="password" id="key-passphrase" placeholder="Passphrase vom Key-Export">
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<button class="btn" id="btn-import" onclick="doImportKeys()">Schlüssel importieren</button>
|
|
<button class="btn secondary" onclick="skipKeys()">Überspringen (ohne E2E)</button>
|
|
</div>
|
|
<div class="status" id="keys-status"></div>
|
|
</div>
|
|
|
|
<!-- STEP 3: Rooms -->
|
|
<div class="card disabled" id="card-rooms">
|
|
<h2><span class="num" id="step3-icon">3</span> Raum auswählen</h2>
|
|
|
|
<div class="room-search">
|
|
<input type="search" id="room-search" placeholder="Raum suchen..." oninput="filterRooms()">
|
|
</div>
|
|
|
|
<div id="rooms-loading" class="hidden" style="color:var(--text2);font-size:14px">Lade Räume …</div>
|
|
<div class="room-list" id="room-list"></div>
|
|
<div class="status" id="rooms-status"></div>
|
|
|
|
<div class="btn-row" style="margin-top:12px">
|
|
<button class="btn secondary" onclick="loadRooms()">Räume neu laden</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- STEP 4: Export Progress -->
|
|
<div class="card hidden" id="card-export">
|
|
<h2><span class="num" id="step4-icon">4</span> Export</h2>
|
|
|
|
<div class="progress-bar"><div class="fill" id="progress-fill"></div></div>
|
|
<div class="log-box" id="log-box"></div>
|
|
|
|
<div class="btn-row hidden" id="export-actions">
|
|
<a class="btn" id="btn-open-export" href="#" target="_blank">Export öffnen</a>
|
|
<a class="btn secondary hidden" id="btn-download-zip" href="#" download>📦 ZIP herunterladen</a>
|
|
<button class="btn secondary" onclick="location.reload()">Neuen Export starten</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Previous Exports -->
|
|
<div class="card" id="card-exports">
|
|
<h2>Vorherige Exports</h2>
|
|
<div id="exports-list"><span style="color:var(--text2);font-size:14px">Lade …</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var currentTab = "password";
|
|
var connected = false;
|
|
var keysImported = false;
|
|
var allRooms = [];
|
|
var roomsTotal = 0;
|
|
var roomsES = null;
|
|
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll(".tabs .tab").forEach(function(t) { t.classList.remove("active"); });
|
|
event.target.classList.add("active");
|
|
document.getElementById("tab-password").classList.toggle("hidden", tab !== "password");
|
|
document.getElementById("tab-token").classList.toggle("hidden", tab !== "token");
|
|
}
|
|
|
|
function setStatus(id, type, msg) {
|
|
var el = document.getElementById(id);
|
|
el.className = "status " + type;
|
|
el.textContent = msg;
|
|
}
|
|
|
|
function stepDone(num) {
|
|
var icon = document.getElementById("step" + num + "-icon");
|
|
icon.className = "check";
|
|
icon.textContent = "✓";
|
|
}
|
|
|
|
async function doConnect() {
|
|
var btn = document.getElementById("btn-connect");
|
|
btn.disabled = true;
|
|
btn.textContent = "Verbinde …";
|
|
|
|
try {
|
|
var body;
|
|
if (currentTab === "password") {
|
|
body = {
|
|
method: "password",
|
|
homeserver: document.getElementById("hs-url").value,
|
|
username: document.getElementById("username").value,
|
|
password: document.getElementById("password").value
|
|
};
|
|
} else {
|
|
body = {
|
|
method: "token",
|
|
homeserver: document.getElementById("hs-url2").value,
|
|
token: document.getElementById("access-token").value
|
|
};
|
|
}
|
|
|
|
var resp = await fetch("/api/connect", {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
var err = await resp.json();
|
|
throw new Error(err.detail || "Verbindung fehlgeschlagen");
|
|
}
|
|
|
|
var data = await resp.json();
|
|
connected = true;
|
|
setStatus("connect-status", "success",
|
|
"Verbunden als " + data.user_id + (data.device_id ? " (Device: " + data.device_id + ")" : ""));
|
|
|
|
stepDone(1);
|
|
document.getElementById("btn-disconnect").classList.remove("hidden");
|
|
document.getElementById("card-keys").classList.remove("disabled");
|
|
|
|
if (data.new_session) {
|
|
setStatus("keys-status", "info",
|
|
"Neues Device erstellt. Bitte importiere deine E2E-Schlüssel für entschlüsselte Exports.");
|
|
}
|
|
} catch (e) {
|
|
setStatus("connect-status", "error", e.message);
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = "Verbinden";
|
|
}
|
|
|
|
async function doDisconnect() {
|
|
await fetch("/api/disconnect", {method: "POST"});
|
|
location.reload();
|
|
}
|
|
|
|
async function doImportKeys() {
|
|
var btn = document.getElementById("btn-import");
|
|
var fileInput = document.getElementById("key-file");
|
|
var passphrase = document.getElementById("key-passphrase").value;
|
|
|
|
if (!fileInput.files.length) {
|
|
setStatus("keys-status", "error", "Bitte Schlüsseldatei auswählen");
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = "Importiere …";
|
|
|
|
try {
|
|
var form = new FormData();
|
|
form.append("file", fileInput.files[0]);
|
|
form.append("passphrase", passphrase);
|
|
|
|
var resp = await fetch("/api/import-keys", {method: "POST", body: form});
|
|
|
|
if (!resp.ok) {
|
|
var err = await resp.json();
|
|
throw new Error(err.detail || "Import fehlgeschlagen");
|
|
}
|
|
|
|
var data = await resp.json();
|
|
keysImported = true;
|
|
setStatus("keys-status", "success", data.message);
|
|
stepDone(2);
|
|
enableRooms();
|
|
} catch (e) {
|
|
setStatus("keys-status", "error", e.message);
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = "Schlüssel importieren";
|
|
}
|
|
|
|
function skipKeys() {
|
|
keysImported = false;
|
|
setStatus("keys-status", "info", "Übersprungen. Verschlüsselte Nachrichten werden nicht entschlüsselt.");
|
|
stepDone(2);
|
|
enableRooms();
|
|
}
|
|
|
|
function enableRooms() {
|
|
document.getElementById("card-rooms").classList.remove("disabled");
|
|
loadRooms();
|
|
}
|
|
|
|
function filterRooms() {
|
|
var q = document.getElementById("room-search").value.toLowerCase();
|
|
var cards = document.querySelectorAll("#room-list .room-card");
|
|
cards.forEach(function(card) {
|
|
var name = card.dataset.name.toLowerCase();
|
|
card.style.display = name.includes(q) ? "" : "none";
|
|
});
|
|
}
|
|
|
|
function addRoomCard(room) {
|
|
allRooms.push(room);
|
|
var list = document.getElementById("room-list");
|
|
var card = document.createElement("div");
|
|
card.className = "room-card";
|
|
card.dataset.name = room.name;
|
|
card.onclick = function() { startExport(room.room_id, room.name); };
|
|
card.innerHTML =
|
|
'<div class="name">' + escHtml(room.name) + "</div>" +
|
|
'<div class="meta">' + room.member_count + " Mitgl.</div>" +
|
|
(room.encrypted ? '<div class="badge enc">🔒 E2E</div>' : '<div class="badge">Offen</div>');
|
|
|
|
var q = document.getElementById("room-search").value.toLowerCase();
|
|
if (q && !room.name.toLowerCase().includes(q)) {
|
|
card.style.display = "none";
|
|
}
|
|
|
|
list.appendChild(card);
|
|
}
|
|
|
|
function loadRooms() {
|
|
if (roomsES) { roomsES.close(); roomsES = null; }
|
|
|
|
var list = document.getElementById("room-list");
|
|
var loading = document.getElementById("rooms-loading");
|
|
list.innerHTML = "";
|
|
allRooms = [];
|
|
roomsTotal = 0;
|
|
loading.classList.remove("hidden");
|
|
loading.textContent = "Lade Räume …";
|
|
|
|
roomsES = new EventSource("/api/rooms/stream");
|
|
|
|
roomsES.onmessage = function(e) {
|
|
var data = JSON.parse(e.data);
|
|
|
|
if (data.type === "total") {
|
|
roomsTotal = data.count;
|
|
loading.textContent = "Lade 0/" + roomsTotal + " Räume …";
|
|
} else if (data.type === "room") {
|
|
setTimeout(function() {
|
|
addRoomCard(data.data);
|
|
loading.textContent = "Lade " + allRooms.length + "/" + roomsTotal + " Räume …";
|
|
}, 0);
|
|
} else if (data.type === "done") {
|
|
roomsES.close();
|
|
roomsES = null;
|
|
setTimeout(function() {
|
|
loading.classList.add("hidden");
|
|
if (allRooms.length === 0) {
|
|
list.innerHTML = '<p style="color:var(--text2)">Keine Räume gefunden</p>';
|
|
}
|
|
}, 50);
|
|
} else if (data.type === "error") {
|
|
roomsES.close();
|
|
roomsES = null;
|
|
loading.classList.add("hidden");
|
|
setStatus("rooms-status", "error", data.message);
|
|
}
|
|
};
|
|
|
|
roomsES.onerror = function() {
|
|
roomsES.close();
|
|
roomsES = null;
|
|
loading.classList.add("hidden");
|
|
};
|
|
}
|
|
|
|
async function startExport(roomId, roomName) {
|
|
if (!confirm('Chat "' + roomName + '" wirklich exportieren?')) return;
|
|
|
|
document.getElementById("card-export").classList.remove("hidden");
|
|
document.getElementById("card-export").scrollIntoView({behavior: "smooth"});
|
|
|
|
var logBox = document.getElementById("log-box");
|
|
logBox.innerHTML = "";
|
|
document.getElementById("progress-fill").style.width = "0%";
|
|
document.getElementById("export-actions").classList.add("hidden");
|
|
document.getElementById("btn-download-zip").classList.add("hidden");
|
|
|
|
addLog("info", "Starte Export für: " + roomName);
|
|
|
|
try {
|
|
var resp = await fetch("/api/export", {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({room_id: roomId})
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
var err = await resp.json();
|
|
addLog("error", err.detail || "Fehler");
|
|
return;
|
|
}
|
|
|
|
var es = new EventSource("/api/export/progress/" + encodeURIComponent(roomId));
|
|
|
|
es.onmessage = function(e) {
|
|
var data = JSON.parse(e.data);
|
|
|
|
if (data.type === "stream_end") {
|
|
es.close();
|
|
return;
|
|
}
|
|
|
|
addLog(data.type, data.message);
|
|
|
|
if (data.progress) {
|
|
document.getElementById("progress-fill").style.width = data.progress + "%";
|
|
}
|
|
|
|
if (data.type === "done") {
|
|
es.close();
|
|
stepDone(4);
|
|
document.getElementById("export-actions").classList.remove("hidden");
|
|
if (data.html_path) {
|
|
document.getElementById("btn-open-export").href = data.html_path;
|
|
}
|
|
if (data.zip_path) {
|
|
var zipBtn = document.getElementById("btn-download-zip");
|
|
zipBtn.href = data.zip_path;
|
|
zipBtn.classList.remove("hidden");
|
|
}
|
|
loadExports();
|
|
}
|
|
};
|
|
|
|
es.onerror = function() {
|
|
es.close();
|
|
addLog("error", "Verbindung zum Server verloren");
|
|
};
|
|
} catch (e) {
|
|
addLog("error", e.message);
|
|
}
|
|
}
|
|
|
|
function addLog(type, message) {
|
|
var box = document.getElementById("log-box");
|
|
var line = document.createElement("div");
|
|
line.className = "log-line " + type;
|
|
var prefix = {info: "[i]", progress: "[>]", warning: "[!]", error: "[x]", done: "[OK]"};
|
|
line.textContent = (prefix[type] || "[?]") + " " + message;
|
|
box.appendChild(line);
|
|
box.scrollTop = box.scrollHeight;
|
|
}
|
|
|
|
async function loadExports() {
|
|
var list = document.getElementById("exports-list");
|
|
try {
|
|
var resp = await fetch("/api/exports");
|
|
var exports = await resp.json();
|
|
|
|
if (exports.length === 0) {
|
|
list.innerHTML = '<span style="color:var(--text2);font-size:14px">Noch keine Exports vorhanden</span>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = "";
|
|
exports.forEach(function(ex) {
|
|
var item = document.createElement("div");
|
|
item.className = "export-item";
|
|
item.innerHTML =
|
|
'<div class="info">' +
|
|
'<div class="name">' + escHtml(ex.room_name) + "</div>" +
|
|
'<div class="meta">' + ex.total_events + " Nachrichten, " + ex.total_media + " Medien · " + ex.last_export + "</div>" +
|
|
"</div>" +
|
|
'<div class="actions">' +
|
|
'<a class="btn secondary" href="' + ex.html_path + '" target="_blank" style="font-size:13px;padding:6px 14px">Öffnen</a>' +
|
|
(ex.zip_path ? '<a class="btn secondary" href="' + ex.zip_path + '" download style="font-size:13px;padding:6px 14px">📦 ZIP</a>' : "") +
|
|
'<button class="btn danger" style="font-size:13px;padding:6px 14px" onclick="deleteExport(\'' + ex.dir_name.replace(/'/g, "\\'") + '\', \'' + escHtml(ex.room_name).replace(/'/g, "\\'") + '\')">🗑 Löschen</button>' +
|
|
"</div>";
|
|
list.appendChild(item);
|
|
});
|
|
} catch (e) {
|
|
list.innerHTML = '<span style="color:var(--text2)">Fehler beim Laden</span>';
|
|
}
|
|
}
|
|
|
|
async function deleteExport(dirName, roomName) {
|
|
if (!confirm('Export "' + roomName + '" wirklich löschen?')) return;
|
|
try {
|
|
var resp = await fetch("/api/exports/" + encodeURIComponent(dirName), {method: "DELETE"});
|
|
if (!resp.ok) throw new Error("Löschen fehlgeschlagen");
|
|
loadExports();
|
|
} catch (e) {
|
|
alert("Fehler: " + e.message);
|
|
}
|
|
}
|
|
|
|
function escHtml(s) {
|
|
var d = document.createElement("div");
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function onEnter(ids, fn) {
|
|
ids.forEach(function(id) {
|
|
document.getElementById(id).addEventListener("keydown", function(e) {
|
|
if (e.key === "Enter") { e.preventDefault(); fn(); }
|
|
});
|
|
});
|
|
}
|
|
onEnter(["hs-url", "username", "password", "hs-url2", "access-token"], doConnect);
|
|
onEnter(["key-passphrase"], doImportKeys);
|
|
|
|
// Init
|
|
(async function() {
|
|
try {
|
|
var resp = await fetch("/api/session");
|
|
var data = await resp.json();
|
|
if (data.user_id) {
|
|
connected = true;
|
|
setStatus("connect-status", "success", "Session wiederhergestellt: " + data.user_id);
|
|
stepDone(1);
|
|
document.getElementById("btn-disconnect").classList.remove("hidden");
|
|
document.getElementById("card-keys").classList.remove("disabled");
|
|
|
|
if (data.homeserver) {
|
|
document.getElementById("hs-url").value = data.homeserver;
|
|
document.getElementById("hs-url2").value = data.homeserver;
|
|
}
|
|
}
|
|
} catch(e) {}
|
|
|
|
loadExports();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|