Download MD no histórico, relatórios externos e service systemd

- Botão de download .md no modal de detalhe do histórico (agente e pipeline)
- Relatórios de execução gravados também em ~/agent_reports/ (configurável via AGENT_REPORTS_DIR)
- Service systemd (user) para iniciar o orchestrator no boot com auto-restart
This commit is contained in:
Frederico Castro
2026-02-27 04:19:10 -03:00
parent 9b66a415ff
commit a2a1aa2c7a
12 changed files with 499 additions and 12 deletions

View File

@@ -35,6 +35,71 @@ const Utils = {
if (pending.length === 0) return;
lucide.createIcons();
},
formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
},
initDropzone(dropzoneId, fileInputId, fileListId) {
const zone = document.getElementById(dropzoneId);
const input = document.getElementById(fileInputId);
const list = document.getElementById(fileListId);
if (!zone || !input || !list) return null;
const state = { files: [] };
function render() {
list.innerHTML = state.files.map((f, i) => `
<li class="dropzone-file">
<span class="dropzone-file-name">${Utils.escapeHtml(f.name)}</span>
<span class="dropzone-file-size">${Utils.formatFileSize(f.size)}</span>
<button type="button" class="dropzone-file-remove" data-index="${i}" title="Remover">&times;</button>
</li>
`).join('');
const content = zone.querySelector('.dropzone-content');
if (content) content.style.display = state.files.length > 0 ? 'none' : '';
}
function addFiles(fileList) {
for (const f of fileList) {
if (state.files.length >= 20) break;
if (f.size > 10 * 1024 * 1024) continue;
const dupe = state.files.some(x => x.name === f.name && x.size === f.size);
if (!dupe) state.files.push(f);
}
render();
}
zone.addEventListener('click', (e) => {
if (e.target.closest('.dropzone-file-remove')) {
const idx = parseInt(e.target.closest('.dropzone-file-remove').dataset.index);
state.files.splice(idx, 1);
render();
return;
}
if (!e.target.closest('.dropzone-file')) input.click();
});
input.addEventListener('change', () => {
if (input.files.length > 0) addFiles(input.files);
input.value = '';
});
zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); });
zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) addFiles(e.dataTransfer.files);
});
state.reset = () => { state.files = []; render(); };
state.getFiles = () => state.files;
return state;
},
};
window.Utils = Utils;