Viel neues

This commit is contained in:
Sven Steinert
2026-04-30 12:06:00 +02:00
parent 118809bfae
commit fce31ebcd7
1274 changed files with 181255 additions and 0 deletions

253
qa-tool/htdocs/js/main.js Normal file
View File

@@ -0,0 +1,253 @@
// ===================== QA App komplette app.js =====================
// === DocBee: Token HIER eintragen ===
const DOCBEE_TOKEN = window.DOCBEE_TOKEN || ""; // DocBee Token eintragen"
const ENABLE_FALLBACK_NOTE = false; // auf true setzen, falls bei Fehler automatisch Notiz angelegt werden soll
window.addEventListener('error', e => {
try {
console.error(e.error || e.message || e);
} catch (_) {}
alert('JS-Fehler: ' + (e.message || e));
});
let template = null; // {name,module,module_version,pbx_version,steps:[...]}
// === DOM-Refs ===
const els = {
yamlFile: document.getElementById('yamlFile'),
stepsTableBody: document.querySelector('#stepsTable tbody'),
tplName: document.getElementById('tplName'),
statusTag: document.getElementById('statusTag'),
module: document.getElementById('module'),
moduleVersion: document.getElementById('moduleVersion'),
pbxVersion: document.getElementById('pbxVersion'),
tester: document.getElementById('tester'),
olmNummer: document.getElementById('olmNummer'),
docbeeUrl: document.getElementById('docbeeUrl'),
btnAddStep: document.getElementById('btnAddStep'),
btnSaveJSON: document.getElementById('btnSaveJSON'),
loadJSON: document.getElementById('loadJSON'),
btnAddGroup: document.getElementById('btnAddGroup'),
btnExportTemplateYAML: document.getElementById('btnExportTemplateYAML'),
btnExportAll: document.getElementById('btnExportAll'),
gitlabTplSelect: document.getElementById('gitlabTplSelect'),
gitlabTplStatus: document.getElementById('gitlabTplStatus'),
docbeeTokenStatus: document.getElementById('docbeeTokenStatus'),
btnGitlabReload: document.getElementById('btnGitlabReload'),
};
// === Drag Guard + Observer: Nur Drag über expliziten Griff zulassen ===
function enforceDragHandleOnly() {
if (!els.stepsTableBody) return;
els.stepsTableBody.querySelectorAll('tr[draggable="true"]').forEach(tr => tr.setAttribute('draggable', 'false'));
els.stepsTableBody.querySelectorAll('.drag-handle').forEach(h => h.setAttribute('draggable', 'true'));
}
document.addEventListener('DOMContentLoaded', () => {
if (els.stepsTableBody) {
const mo = new MutationObserver(() => enforceDragHandleOnly());
mo.observe(els.stepsTableBody, {
childList: true,
subtree: true
});
enforceDragHandleOnly();
}
});
document.addEventListener('dragstart', (e) => {
if (!e.target.closest('.drag-handle')) {
e.preventDefault();
}
}, {
capture: true
});
// === GitLab Events ===
if (els.gitlabTplSelect) els.gitlabTplSelect.addEventListener('change', async (e) => {
const p = e.target.value;
if (!p) return;
try {
els.gitlabTplStatus && (els.gitlabTplStatus.textContent = "GitLab: lade Datei…");
const y = await fetchGitlabFileRaw(p);
loadYAMLText(y);
els.gitlabTplStatus && (els.gitlabTplStatus.textContent = `GitLab: geladen ${p.split('/').pop()}`);
} catch (err) {
els.gitlabTplStatus && (els.gitlabTplStatus.textContent = "GitLab: Fehler");
console.error("[GitLab] raw failed:", err);
alert("Vorlage aus GitLab konnte nicht geladen werden: " + err.message);
}
});
if (els.btnGitlabReload) els.btnGitlabReload.addEventListener('click', (e) => {
e.preventDefault();
populateGitlabDropdown();
});
// === Drag & Drop (Rows verschieben, inkl. Gruppen) ===
let dragIndex = -1;
// ===== Funktion: rowIndexOf =====
// Zweck: siehe Inline-Kommentare im Funktionskörper.
function rowIndexOf(target) {
const tr = target.closest('tr');
if (!tr) return -1;
return [...els.stepsTableBody.children].indexOf(tr);
}
els.stepsTableBody && els.stepsTableBody.addEventListener('dragstart', (e) => {
if (!e.target.closest('.drag-handle')) {
e.preventDefault();
return;
}
const i = rowIndexOf(e.target);
if (i < 0) return;
dragIndex = i;
e.dataTransfer?.setData('text/plain', String(i));
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
const row = e.target.closest('tr') || e.target;
e.dataTransfer?.setDragImage?.(row, 16, 16);
row.classList.add('dragging');
});
els.stepsTableBody && els.stepsTableBody.addEventListener('dragend', (e) => {
const tr = e.target.closest('tr');
if (tr) tr.classList.remove('dragging');
[...els.stepsTableBody.children].forEach(r => {
r.classList.remove('drag-over');
r.removeAttribute('data-pos');
});
dragIndex = -1;
});
els.stepsTableBody && els.stepsTableBody.addEventListener('dragover', (e) => {
e.preventDefault();
const tr = e.target.closest('tr');
if (!tr) return;
[...els.stepsTableBody.children].forEach(r => {
r.classList.remove('drag-over');
r.removeAttribute('data-pos');
});
tr.classList.add('drag-over');
const rect = tr.getBoundingClientRect();
const pos = (e.clientY - rect.top) < rect.height / 2 ? 'before' : 'after';
tr.setAttribute('data-pos', pos);
});
els.stepsTableBody && els.stepsTableBody.addEventListener('drop', (e) => {
e.preventDefault();
captureEditsIntoTemplate();
const from = dragIndex >= 0 ? dragIndex : parseInt(e.dataTransfer?.getData('text/plain') || '-1', 10);
const tr = e.target.closest('tr');
if (from < 0 || !tr) return;
const toBase = rowIndexOf(tr);
const pos = tr.getAttribute('data-pos') || 'after';
let to = toBase + (pos === 'after' ? 1 : 0);
if (to === from || to - 1 === from) {
renderSteps(template.steps);
recomputeGroupStyles();
return;
}
// Element verschieben
const item = template.steps.splice(from, 1)[0];
if (to > from) to--; // nach dem Entfernen verschiebt sich Index
template.steps.splice(to, 0, item);
ensureRenumberAndRender();
});
document.getElementById('stepsTable')?.addEventListener('change', (e) => {
if (e.target && e.target.matches('select.status')) {
updateStatusClass(e.target);
recomputeGroupStyles();
}
});
els.btnExportMD && els.btnExportMD.addEventListener('click', exportMD);
els.btnExportCSV && els.btnExportCSV.addEventListener('click', exportCSV);
els.btnPrintPDF && els.btnPrintPDF.addEventListener('click', printPDF);
els.btnExportTemplateYAML && els.btnExportTemplateYAML.addEventListener('click', exportTemplateYAML);
els.btnSaveJSON && els.btnSaveJSON.addEventListener('click', () => {
const run = collectRun();
if (!run) return;
if (!checkRequired(run)) return;
const base = `qa-run-${safeName(run.module)}-${safeName(run.module_version)}-${safeName(run.pbx_version)}`;
download(JSON.stringify(run, null, 2), `${base}.json`, 'application/json');
});
els.loadJSON && els.loadJSON.addEventListener('change', async (e) => {
const f = e.target.files?.[0];
if (!f) return;
const txt = await f.text();
try {
const run = JSON.parse(txt);
template = {
name: run.name || '',
steps: (run.steps || []).map(s => ({
id: s.id,
title: s.title,
expected: s.expected,
required: !!s.required,
status: s.status || '',
comment: s.comment || '',
evidence: s.evidence || ''
}))
};
if (els.tplName) els.tplName.textContent = run.name || '—';
els.module.value = run.module || '';
els.moduleVersion.value = run.module_version || '';
els.pbxVersion.value = run.pbx_version || '';
els.tester.value = run.tester || '';
els.docbeeUrl.value = run.docbee_url || '';
renderSteps(template.steps);
recomputeGroupStyles();
if (els.statusTag) els.statusTag.textContent = 'Lauf geladen';
updateAutosave();
} catch (err) {
alert('Ungültiges JSON: ' + err.message);
}
});
// === Bindings absichern + Busy-Fix + Mini-CSS ===
(function ensureBindings() {
// ===== Funktion: bind =====
// Zweck: siehe Inline-Kommentare im Funktionskörper.
function bind() {
const b = document.getElementById('btnPushDocBee');
if (b) {
b.removeAttribute('disabled');
b.classList.remove('is-busy');
b.style.pointerEvents = 'auto';
b.addEventListener('click', postToDocBee, {
once: false
});
}
const p = document.getElementById('btnPrintPDF');
const btn = document.getElementById('btnExportAll');
if (btn) {
btn.addEventListener('click', e => {
e.preventDefault();
exportAll().catch(err => alert('Export error: ' + err.message));
});
console.log('Export-Button gebunden');
} else {
alert('Export-Button nicht gefunden (id=btnExportAll).');
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bind);
else bind();
window.addEventListener('pageshow', () => {
const b = document.getElementById('btnPushDocBee');
if (b) {
b.removeAttribute('disabled');
b.classList.remove('is-busy');
b.style.pointerEvents = 'auto';
}
});
})();
(function injectAuxCss() {
const s = document.createElement('style');
s.textContent = `
.is-busy{opacity:.6;pointer-events:none}
.docbee-hint{margin-top:8px;font-size:14px}
.docbee-hint a{text-decoration:underline}
`;
document.head.appendChild(s);
})();
// ===================== Ende app.js =====================