ParamManagerScripts/backend/script_groups/EmailCrono/static/app.js

280 lines
10 KiB
JavaScript

const state = {
data: null,
rules: [],
};
const el = (sel) => document.querySelector(sel);
function setStatus(msg, type = "info") {
const s = el('#status');
s.textContent = msg || '';
s.className = `status ${type}`;
}
async function fetchJSON(url, options = {}) {
const res = await fetch(url, options);
const data = await res.json();
if (!res.ok || data.status === 'error') {
const message = data && data.message ? data.message : `HTTP ${res.status}`;
throw new Error(message);
}
return data;
}
function ruleRow(rule, index) {
const div = document.createElement('div');
div.className = 'rule-row';
div.innerHTML = `
<div><textarea class="comment autoresize" rows="2" placeholder="Comentario">${rule.__comment || ''}</textarea></div>
<div><textarea class="pattern autoresize" rows="2" placeholder="Pattern">${rule.pattern || ''}</textarea></div>
<div><input class="replacement" value="${rule.replacement || ''}" placeholder="Replacement"/></div>
<div>
<select class="action">
${['replace', 'remove_line', 'remove_block', 'add_before', 'add_after'].map(a => `<option value="${a}" ${rule.action === a ? 'selected' : ''}>${a}</option>`).join('')}
</select>
</div>
<div>
<select class="type">
${['string', 'regex', 'left', 'right', 'substring'].map(t => `<option value="${t}" ${rule.type === t ? 'selected' : ''}>${t}</option>`).join('')}
</select>
</div>
<div><input class="priority" type="number" value="${Number.isInteger(rule.priority) ? rule.priority : 5}"/></div>
<div class="actions">
<button class="up">↑</button>
<button class="down">↓</button>
<button class="dup">⧉</button>
<button class="del">✕</button>
<button class="test">Edicion y Test</button>
</div>
`;
// bindings
const cEl = div.querySelector('.comment');
const pEl = div.querySelector('.pattern');
cEl.addEventListener('input', (e) => { state.rules[index].__comment = e.target.value; autoResize(e.target); });
pEl.addEventListener('input', (e) => { state.rules[index].pattern = e.target.value; autoResize(e.target); });
// Nota: no auto-redimensionar en el render inicial para mantener 2 líneas visibles
div.querySelector('.replacement').addEventListener('input', (e) => state.rules[index].replacement = e.target.value);
div.querySelector('.action').addEventListener('change', (e) => state.rules[index].action = e.target.value);
div.querySelector('.type').addEventListener('change', (e) => state.rules[index].type = e.target.value);
div.querySelector('.priority').addEventListener('input', (e) => state.rules[index].priority = parseInt(e.target.value, 10));
div.querySelector('.up').addEventListener('click', () => moveRule(index, -1));
div.querySelector('.down').addEventListener('click', () => moveRule(index, 1));
div.querySelector('.del').addEventListener('click', () => removeRule(index));
div.querySelector('.dup').addEventListener('click', () => duplicateRule(index));
div.querySelector('.test').addEventListener('click', () => openModal(index));
return div;
}
function render() {
const list = el('#rulesList');
list.innerHTML = '';
state.rules
.map((r, i) => [r, i])
.sort((a, b) => (a[0].priority ?? 9999) - (b[0].priority ?? 9999) || a[1] - b[1])
.forEach(([r, i]) => list.appendChild(ruleRow(r, i)));
const raw = {
...state.data,
rules: state.rules,
};
el('#rawJson').value = JSON.stringify(raw, null, 2);
}
function moveRule(index, delta) {
const newIndex = index + delta;
if (newIndex < 0 || newIndex >= state.rules.length) return;
const tmp = state.rules[index];
state.rules[index] = state.rules[newIndex];
state.rules[newIndex] = tmp;
render();
}
function removeRule(index) {
state.rules.splice(index, 1);
render();
}
function duplicateRule(index) {
const original = state.rules[index];
const copy = JSON.parse(JSON.stringify(original));
state.rules.splice(index + 1, 0, copy);
render();
}
function addRule() {
state.rules.push({
__comment: '',
pattern: '',
replacement: '',
action: 'replace',
type: 'string',
priority: 5,
});
render();
}
async function loadAll() {
try {
setStatus('Cargando...', 'info');
const [meta, rulesRes] = await Promise.all([
fetchJSON('/api/meta'),
fetchJSON('/api/rules'),
]);
state.data = rulesRes.data;
state.rules = Array.isArray(rulesRes.data.rules) ? JSON.parse(JSON.stringify(rulesRes.data.rules)) : [];
el('#metaInfo').textContent = meta.exists ? `${meta.path} (${meta.size} bytes) — modificado: ${meta.modified}` : 'Archivo no encontrado';
render();
setStatus('Listo', 'success');
} catch (e) {
setStatus(`Error: ${e.message}`, 'error');
}
}
async function saveAll() {
try {
setStatus('Guardando...', 'info');
// Fusionar: tomar el JSON avanzado si es válido, pero SIEMPRE usar las reglas del estado actual
const payloadFromState = { ...state.data, rules: state.rules };
let payload = payloadFromState;
try {
const parsed = JSON.parse(el('#rawJson').value);
payload = { ...parsed, rules: state.rules };
} catch (_) { /* si el JSON está inválido, persistimos el estado visible */ }
const res = await fetchJSON('/api/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
setStatus(`${res.message} (backup: ${res.backup})`, 'success');
await loadAll();
} catch (e) {
setStatus(`Error: ${e.message}`, 'error');
}
}
function wireUI() {
const bind = (selector, event, handler) => {
const node = el(selector);
if (node) node.addEventListener(event, handler);
return node;
};
bind('#btnReload', 'click', loadAll);
bind('#btnSave', 'click', saveAll);
bind('#btnAdd', 'click', addRule);
bind('#btnShutdown', 'click', async () => {
try {
await fetchJSON('/_shutdown', { method: 'POST' });
} catch (_) { }
try { window.close(); } catch (_) { }
});
// modal
bind('#modalClose', 'click', closeModal);
bind('#btnModalCancel', 'click', closeModal);
bind('#btnModalApply', 'click', applyModalToRule);
bind('#btnModalTest', 'click', testModalRule);
}
document.addEventListener('DOMContentLoaded', async () => {
wireUI();
await loadAll();
// Iniciar heartbeat periódico para indicar presencia del cliente
try {
const beat = async () => {
try {
await fetch('/api/heartbeat', { cache: 'no-store' });
} catch (_) { /* noop */ }
};
// primer latido inmediato y luego cada 10s
beat();
setInterval(beat, 10000);
} catch (_) { /* noop */ }
});
// --- Auto-resize helpers ---
function autoResize(t) {
t.style.height = 'auto';
t.style.height = (t.scrollHeight) + 'px';
}
// --- Modal logic ---
let modalIndex = -1;
function openModal(index) {
modalIndex = index;
const r = state.rules[index];
// fill selects
el('#mAction').innerHTML = ['replace', 'remove_line', 'remove_block', 'add_before', 'add_after']
.map(a => `<option value="${a}" ${r.action === a ? 'selected' : ''}>${a}</option>`).join('');
el('#mType').innerHTML = ['string', 'regex', 'left', 'right', 'substring']
.map(t => `<option value="${t}" ${r.type === t ? 'selected' : ''}>${t}</option>`).join('');
el('#mPattern').value = r.pattern || '';
el('#mReplacement').value = r.replacement || '';
el('#mOriginal').textContent = '';
el('#mProcessed').textContent = '';
updateActionHelp();
el('#mAction').addEventListener('change', updateActionHelp, { once: false });
document.getElementById('modal').classList.remove('hidden');
}
function closeModal() {
document.getElementById('modal').classList.add('hidden');
modalIndex = -1;
}
function applyModalToRule() {
if (modalIndex < 0) return;
const r = state.rules[modalIndex];
r.pattern = el('#mPattern').value;
r.replacement = el('#mReplacement').value;
r.action = el('#mAction').value;
r.type = el('#mType').value;
closeModal();
render();
}
async function testModalRule() {
try {
const payload = {
pattern: el('#mPattern').value,
replacement: el('#mReplacement').value,
action: el('#mAction').value,
type: el('#mType').value,
max_chars: 4000,
};
const res = await fetchJSON('/api/test_pattern', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
el('#mOriginal').textContent = res.original_excerpt || '';
el('#mProcessed').textContent = res.processed_excerpt || '';
el('#modalMeta').textContent = `Longitud original: ${res.total_len}`;
} catch (e) {
el('#modalMeta').textContent = `Error: ${e.message}`;
}
}
function actionHelpText(action) {
const map = {
replace: 'Reemplaza coincidencias de "pattern" por "replacement". Respeta el "type": string/substring (reemplazo literal), regex (expresión regular), left/right (aplica por línea al inicio/fin).',
remove_line: 'Elimina las líneas que coinciden con "pattern" según el "type". Si la línea siguiente queda en blanco, también se elimina.',
remove_block: 'Elimina bloques completos que coincidan con el patrón multi-línea. Usa "....." como comodín para cualquier texto entre medio. Ej: "Inicio.....Fin" borrará todo desde la línea con "Inicio" hasta la línea con "Fin".',
add_before: 'Inserta el contenido de "replacement" en una nueva línea antes de cada línea que coincida con "pattern" (según el "type").',
add_after: 'Inserta el contenido de "replacement" en una nueva línea después de cada línea que coincida con "pattern" (según el "type").',
};
return map[action] || '';
}
function updateActionHelp() {
const sel = el('#mAction');
if (!sel) return;
const txt = actionHelpText(sel.value);
const node = el('#mActionHelp');
if (node) node.textContent = txt;
}