Files
workflow-editor/tools/editor_test_server.html
T
Sven f979c797ba feat: Editor-Aufruf mit Task- und Typenrückgabe statt MongoDB
Tasks, eigene Typen und Metadaten pro Editor-Aufruf laden und an den Aufrufer zurückgeben. MongoDB-Persistenz entfernen und Header auf Rückgängig, Abbrechen und Fertig umstellen.

Python-Testserver und Regressionstests ergänzen; Verbindungslinien nach Import und Layoutänderungen neu berechnen.
2026-10-07 21:06:28 +02:00

204 lines
9.9 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TaskList Editor · Testaufrufer</title>
<style>
:root { color-scheme: light; font-family: system-ui, sans-serif; color: #172033; background: #f3f5f9; }
* { box-sizing: border-box; }
body { margin: 0; }
main { max-width: 1100px; padding: 32px 24px; margin: auto; }
h1 { font-size: 27px; margin: 0 0 10px; }
p { line-height: 1.5; }
.muted { color: #566278; }
section { padding: 24px; margin-top: 24px; border: 1px solid #dfe5ef; border-radius: 12px; background: white; }
label { display: block; font-weight: 600; margin-bottom: 10px; }
textarea { display: block; width: 100%; min-height: 300px; padding: 16px; border: 1px solid #bac5d6; border-radius: 8px; font: 14px/1.5 ui-monospace, monospace; resize: vertical; }
button, .file-label { font: inherit; cursor: pointer; border: 1px solid #bac5d6; border-radius: 6px; padding: 10px 16px; color: #172033; background: #fff; margin: 0; }
button.primary { background: #2563eb; color: white; border-color: #2563eb; }
button:disabled { cursor: wait; opacity: .55; }
input[type=file] { max-width: 100%; font: inherit; }
.actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-top: 16px; }
#status { min-height: 24px; margin-top: 20px; white-space: pre-wrap; overflow-wrap: anywhere; }
#status.error { color: #b42318; }
#result { background: #f4f6fa; padding: 16px; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 420px; overflow-y: auto; }
#editor-panel { position: fixed; inset: 0; background: #fff; z-index: 5; display: flex; flex-direction: column; }
#editor-panel[hidden], [hidden] { display: none !important; }
#editor-header { padding: 10px 20px; display: flex; flex-wrap: wrap; gap: 12px; align-items: center; border-bottom: 1px solid #dfe5ef; background: #f3f5f9; }
#editor-status { flex: 1; margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
iframe { width: 100%; border: 0; flex: 1; min-height: 0; }
code { font: .93em ui-monospace, monospace; }
</style>
</head>
<body>
<main>
<h1>TaskList Editor testen</h1>
<p class="muted">Tasks, eigene Typen und Metadaten übergeben, im Editor bearbeiten und das Ergebnis in der Python-Konsole lesen.</p>
<p class="muted" id="editor-url">Verbindung wird vorbereitet …</p>
<section>
<label for="payload">JSON für den Editor</label>
<textarea id="payload" spellcheck="false" aria-describedby="payload-help"></textarea>
<p class="muted" id="payload-help">Ein <code>tasks</code>-Array oder ein Objekt mit <code>name</code>, <code>tasks</code>, dem vollständigen Katalog eigener Typen in <code>customTypes</code> und beliebigen <code>metadata</code>. Für eine neue Liste: <code>"tasks": []</code>. Nach „Fertig“ wird das Eingabefeld mit den zurückgegebenen Tasks und Typen aktualisiert.</p>
<div class="actions">
<button class="primary" id="open" disabled>Im Editor öffnen</button>
<button id="new" disabled>Neue Liste</button>
<input id="file" type="file" accept=".json,application/json" aria-label="JSON-Datei laden" disabled>
</div>
<p id="status" role="status" aria-live="polite"></p>
</section>
<section id="result-section" hidden>
<h2>Empfangene Rückgabe</h2>
<pre id="result"></pre>
</section>
</main>
<div id="editor-panel" hidden>
<div id="editor-header">
<p id="editor-status" role="status" aria-live="polite">Mit „Fertig“ übernehmen oder mit „Abbrechen“ ohne Änderungen zurückkehren.</p>
<button id="retry" hidden>Rückgabe erneut senden</button>
</div>
<iframe id="editor" title="TaskList Editor"></iframe>
</div>
<script>
'use strict';
const payloadInput = document.querySelector('#payload');
const status = document.querySelector('#status');
const openButton = document.querySelector('#open');
const newButton = document.querySelector('#new');
const fileInput = document.querySelector('#file');
const panel = document.querySelector('#editor-panel');
const frame = document.querySelector('#editor');
const editorStatus = document.querySelector('#editor-status');
const retryButton = document.querySelector('#retry');
let config;
let activeToken = null;
let pendingResult = null;
let submitting = false;
function setStatus(message, error = false) {
status.textContent = message;
status.classList.toggle('error', error);
}
async function request(path, value) {
let response;
try {
response = await fetch(path, value === undefined ? {} : {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(value)
});
} catch (error) {
throw new Error(`Verbindung zum Python-Testserver unter ${window.location.origin} fehlgeschlagen. ` +
'Starte ihn mit „python3 tools/editor_test_server.py“ und lasse das Terminal geöffnet. ' +
'Versuche es anschließend erneut.');
}
const result = await response.json();
if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
return result;
}
openButton.addEventListener('click', async () => {
openButton.disabled = true;
setStatus('JSON wird an den Editor übergeben …');
try {
const payload = JSON.parse(payloadInput.value);
const result = await request('/api/open', payload);
if (new URL(result.deepLink).origin !== config.editorOrigin) {
throw new Error('Der Editor hat einen unerwarteten Origin zurückgegeben.');
}
activeToken = result.token;
pendingResult = null;
retryButton.hidden = true;
editorStatus.textContent = 'Mit „Fertig“ übernehmen oder mit „Abbrechen“ ohne Änderungen zurückkehren.';
frame.src = result.deepLink;
panel.hidden = false;
setStatus('Editor geöffnet.');
} catch (error) {
setStatus(error.message, true);
} finally {
openButton.disabled = false;
}
});
newButton.addEventListener('click', () => {
try {
const previous = JSON.parse(payloadInput.value);
const customTypes = previous?.customTypes ?? [];
payloadInput.value = JSON.stringify({name: 'Neue Testliste', tasks: [], customTypes,
metadata: {source: 'python-testserver'}}, null, 2);
setStatus('Neue leere Liste mit den vorhandenen eigenen Typen vorbereitet. Mit „Im Editor öffnen“ beginnen.');
} catch (error) {
setStatus('Die Eingabe enthält ungültiges JSON. Bitte korrigieren, damit eigene Typen übernommen werden können.', true);
}
});
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
if (!file) return;
try {
if (file.size > 2 * 1024 * 1024) throw new Error('Die JSON-Datei darf höchstens 2 MiB groß sein.');
const value = JSON.parse((await file.text()).replace(/^\uFEFF/, ''));
payloadInput.value = JSON.stringify(value, null, 2);
setStatus(`Datei „${file.name}“ geladen.`);
} catch (error) {
setStatus(error.message, true);
} finally {
fileInput.value = '';
}
});
async function submitResult() {
if (submitting || !pendingResult) return;
submitting = true;
retryButton.disabled = true;
editorStatus.textContent = 'Rückgabe wird an die Python-Konsole gesendet …';
try {
await request('/api/result', pendingResult);
document.querySelector('#result').textContent = JSON.stringify(pendingResult, null, 2);
document.querySelector('#result-section').hidden = false;
if (pendingResult.status === 'completed') {
const payload = {tasks: pendingResult.tasks, customTypes: pendingResult.customTypes};
for (const key of ['name', 'metadata']) {
if (Object.hasOwn(pendingResult, key)) payload[key] = pendingResult[key];
}
payloadInput.value = JSON.stringify(payload, null, 2);
}
setStatus(pendingResult.status === 'completed'
? 'Bearbeitung abgeschlossen. Tasks und eigene Typen wurden für den nächsten Aufruf übernommen und in der Python-Konsole ausgegeben.'
: 'Bearbeitung abgebrochen. Die Abbruchmeldung wurde in der Python-Konsole ausgegeben.');
activeToken = null;
pendingResult = null;
panel.hidden = true;
frame.src = 'about:blank';
openButton.focus();
} catch (error) {
editorStatus.textContent = `Rückgabe noch nicht angekommen: ${error.message}`;
retryButton.hidden = false;
} finally {
submitting = false;
retryButton.disabled = false;
}
}
window.addEventListener('message', event => {
if (!config || !activeToken || event.origin !== config.editorOrigin || event.source !== frame.contentWindow) return;
const result = event.data;
if (!result || typeof result !== 'object' || result.type !== 'tasklist-editor:result' || result.token !== activeToken) return;
if (!['completed', 'cancelled'].includes(result.status)) return;
if (result.status === 'completed' && (!Array.isArray(result.tasks) || !Array.isArray(result.customTypes))) return;
if (result.status === 'cancelled' && (Object.hasOwn(result, 'tasks') || Object.hasOwn(result, 'customTypes'))) return;
if (submitting) return;
pendingResult = result;
submitResult();
});
retryButton.addEventListener('click', submitResult);
request('/api/config').then(value => {
config = value;
payloadInput.value = JSON.stringify(config.initialPayload, null, 2);
document.querySelector('#editor-url').textContent = 'Editor: ' + config.editorUrl;
openButton.disabled = newButton.disabled = fileInput.disabled = false;
}).catch(error => setStatus(error.message, true));
</script>
</body>
</html>