Automatic AI Posts, setup automatizado e seeder de permalinks/avatar
Plugin automatic-ai-posts (v2.0.0) - Pipeline completo: coleta de feeds RSS/Atom, filtro por IA, reescrita, categoria, imagem de capa e agendamento - Página de configuração com teste real do provedor de IA - Editor: botão para refazer imagem de capa e metadados, textos em PT-BR - Ajuda com botão Pix (copia-e-cola + QR) e sem nota de avaliação - Cache-busting por mtime nos assets do admin Tema gustavoo-portfolio - Marca do cabeçalho reduzida a "Gustavo" - Suporte a título próprio da marca Setup (docker/wp-init.sh, compose.yaml) - Plugins de terceiros (Post Views Counter, Rank Math, Site Kit) instalados do WordPress.org em vez de versionados - Falha explícita ao final se algum plugin obrigatório não instalar - WPCLI_UID/WPCLI_GID configuráveis Seeder do tema - Links permanentes "Dia e nome" e avatar "Iniciais" aplicados apenas em instalação nova, com regravação do .htaccess
This commit is contained in:
1 parent
5891467266
commit
0ea3651fb5
73 files changed
+8858
-39
No files matched your search
@@ -0,0 +1,802 @@
|
||||
jQuery(function ($) {
|
||||
|
||||
if (typeof aapData === 'undefined') return;
|
||||
|
||||
var ajaxurl = aapData.ajaxurl;
|
||||
var s = aapData.s; // translated strings
|
||||
|
||||
var $doc = $(document);
|
||||
var $form = $('#aap-settings-form');
|
||||
var $exec = $('#rap-auto-interval');
|
||||
|
||||
// O <form> é fechado logo após o grid de configurações (para que os botões de
|
||||
// ação dos outros painéis não o enviem). Por isso os campos do formulário são
|
||||
// mantidos em sincronia com os valores únicos exibidos na aba Execuções:
|
||||
// - intervalo do automático: preenchido a partir do valor salvo
|
||||
// - categorias GNews: os checkboxes só existem no formulário principal
|
||||
function mirrorFromSaved() {
|
||||
if (!$exec.length) return;
|
||||
$exec.val(aapData.interval_value);
|
||||
$('#rap-auto-unit').val(aapData.interval_unit);
|
||||
}
|
||||
|
||||
function mirrorToForm() {
|
||||
if (!$exec.length) return;
|
||||
$('input[name="aap_auto_interval_value"]').val($exec.val());
|
||||
$('select[name="aap_auto_interval_unit"]').val($('#rap-auto-unit').val());
|
||||
}
|
||||
|
||||
// O destaque dos botões de categoria é controlado pela rolagem (scroll spy)
|
||||
|
||||
// ── Doação Pix: copiar a chave ──────────────────────────────────────────
|
||||
$doc.on('click', '#aap-pix-btn', function () {
|
||||
var key = String($(this).data('pix') || '');
|
||||
if (!key) return;
|
||||
|
||||
var $feedback = $('#aap-pix-feedback');
|
||||
|
||||
function done() {
|
||||
$feedback.removeClass('is-error').addClass('is-success').text(s.pix_copied || 'OK');
|
||||
setTimeout(function () {
|
||||
$feedback.text('').removeClass('is-success');
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function fallback() {
|
||||
var $tmp = $('<textarea>').val(key).css({ position: 'fixed', opacity: 0 }).appendTo('body');
|
||||
$tmp[0].select();
|
||||
try { document.execCommand('copy'); done(); } catch (e) {}
|
||||
$tmp.remove();
|
||||
}
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(key).then(done).catch(fallback);
|
||||
} else {
|
||||
fallback();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Teste da conexão com a IA ───────────────────────────────────────────
|
||||
$doc.on('click', '#aap-test-ai-btn', function () {
|
||||
var $btn = $(this);
|
||||
var $feedback = $('#aap-test-ai-feedback');
|
||||
var i18n = aapData.ai_test || {};
|
||||
|
||||
if ($btn.data('busy')) return;
|
||||
$btn.data('busy', true).prop('disabled', true);
|
||||
|
||||
$feedback.removeClass('is-error is-success').addClass('is-pending')
|
||||
.text(i18n.testing || '...');
|
||||
|
||||
$.post(aapData.ajaxurl, {
|
||||
action: 'aap_test_ai',
|
||||
nonce: $btn.data('nonce')
|
||||
})
|
||||
.done(function (res) {
|
||||
var data = (res && res.data) || {};
|
||||
|
||||
if (res && res.success) {
|
||||
$feedback.removeClass('is-pending').addClass('is-success').text(
|
||||
(i18n.ok || 'OK') + ' — ' + data.provider
|
||||
+ (data.model ? ' · ' + data.model : '')
|
||||
+ (data.ms ? ' (' + data.ms + ' ms)' : '')
|
||||
);
|
||||
} else {
|
||||
$feedback.removeClass('is-pending').addClass('is-error')
|
||||
.text(data.error || i18n.error || 'Error.');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$feedback.removeClass('is-pending').addClass('is-error')
|
||||
.text(i18n.error || 'Error.');
|
||||
})
|
||||
.always(function () {
|
||||
$btn.data('busy', false).prop('disabled', false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Provedor de IA: mostra apenas os campos do provedor ativo ───────────
|
||||
(function providerFields() {
|
||||
var $select = $('#aap_ai_provider');
|
||||
if (!$select.length) return;
|
||||
|
||||
var defaults = aapData.provider_defaults || {};
|
||||
|
||||
function refresh() {
|
||||
var id = $select.val();
|
||||
|
||||
$('.aap-provider-block').each(function () {
|
||||
var $block = $(this);
|
||||
$block.prop('hidden', String($block.data('provider')) !== String(id));
|
||||
});
|
||||
|
||||
var $model = $('#aap_model');
|
||||
if ($model.length && defaults[id]) {
|
||||
$model.attr('placeholder', defaults[id]);
|
||||
|
||||
var known = $.map(defaults, function (value) { return value; });
|
||||
var value = $model.val();
|
||||
|
||||
if (value === '' || $.inArray(value, known) !== -1) {
|
||||
$model.val(defaults[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$select.on('change', refresh);
|
||||
refresh();
|
||||
})();
|
||||
|
||||
// ── Alterações não salvas: barra flutuante + descartar ──────────────────
|
||||
(function unsavedChanges() {
|
||||
if (!$form.length) return;
|
||||
|
||||
var $float = $('#aap-save-float');
|
||||
var $discard = $('#aap-discard-all');
|
||||
if (!$float.length) return;
|
||||
|
||||
var fields = function () {
|
||||
return $form.find('input, select, textarea').filter(':enabled');
|
||||
};
|
||||
|
||||
// Fotografia do estado inicial de cada campo
|
||||
function snapshot() {
|
||||
var snap = {};
|
||||
fields().each(function () {
|
||||
var el = this;
|
||||
var key = el.name || el.id;
|
||||
if (!key) return;
|
||||
if (el.type === 'checkbox' || el.type === 'radio') {
|
||||
snap[key + '::' + (el.value || '')] = el.checked;
|
||||
} else {
|
||||
snap[key] = el.value;
|
||||
}
|
||||
});
|
||||
return snap;
|
||||
}
|
||||
|
||||
var initial = snapshot();
|
||||
|
||||
function isDirty() {
|
||||
var now = snapshot();
|
||||
var k;
|
||||
for (k in now) {
|
||||
if (now[k] !== initial[k]) return true;
|
||||
}
|
||||
for (k in initial) {
|
||||
if (!(k in now)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
$float.prop('hidden', !isDirty());
|
||||
}
|
||||
|
||||
// Desfaz tudo o que foi digitado desde o carregamento
|
||||
function restore() {
|
||||
fields().each(function () {
|
||||
var el = this;
|
||||
var key = el.name || el.id;
|
||||
if (!key) return;
|
||||
if (el.type === 'checkbox' || el.type === 'radio') {
|
||||
var k = key + '::' + (el.value || '');
|
||||
if (k in initial) el.checked = initial[k];
|
||||
} else if (key in initial) {
|
||||
el.value = initial[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$form.on('input change', 'input, select, textarea', refresh);
|
||||
|
||||
$discard.on('click', function () {
|
||||
restore();
|
||||
refresh();
|
||||
// Recoloca o foco no início do conteúdo para o usuário ver o estado original
|
||||
var body = document.querySelector('#aap-app .aap-panel:not([hidden]) .aap-panel-body');
|
||||
if (body) body.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
refresh();
|
||||
})();
|
||||
|
||||
// ── Salva configurações antes de executar ────────────────────────────────
|
||||
function saveSettings() {
|
||||
mirrorToForm();
|
||||
if (!$form.length) return $.Deferred().resolve().promise();
|
||||
return $.ajax({
|
||||
url: aapData.options_url,
|
||||
type: 'POST',
|
||||
data: $form.serialize(),
|
||||
});
|
||||
}
|
||||
|
||||
function feedback($el, text, type) {
|
||||
$el.removeClass('is-error is-success is-warn is-pending').addClass(type ? 'is-' + type : '').html(text);
|
||||
}
|
||||
|
||||
// ── Links de categoria (âncoras) ────────────────────────────────────────
|
||||
$doc.on('click', '#aap-app .aap-cat-links a', function (e) {
|
||||
var $card = $($(this).attr('href'));
|
||||
if (!$card.length) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Rola até o card
|
||||
// No layout de aplicação o conteúdo rola dentro do painel; fora dele,
|
||||
// a página inteira é que rola.
|
||||
var body = document.querySelector('#aap-app .aap-panel:not([hidden]) .aap-panel-body');
|
||||
if (body && body.scrollHeight > body.clientHeight + 2) {
|
||||
var delta = $card[0].getBoundingClientRect().top - body.getBoundingClientRect().top;
|
||||
body.scrollTo({ top: Math.max(0, body.scrollTop + delta - 16), behavior: 'smooth' });
|
||||
} else {
|
||||
var top = $card[0].getBoundingClientRect().top + window.pageYOffset - aapHeadHeight() - 16;
|
||||
window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
|
||||
}
|
||||
|
||||
$('#aap-app .aap-cat-links a').removeClass('is-active');
|
||||
$(this).addClass('is-active');
|
||||
|
||||
$card.addClass('aap-flash');
|
||||
setTimeout(function () { $card.removeClass('aap-flash'); }, 1400);
|
||||
});
|
||||
|
||||
// ── Destaque automático do botão da seção visível (scroll spy) ──────────
|
||||
(function catScrollSpy() {
|
||||
var links = Array.prototype.slice.call(
|
||||
document.querySelectorAll('#aap-app .aap-cat-links a')
|
||||
);
|
||||
if (!links.length) return;
|
||||
|
||||
var body = document.querySelector('#aap-app .aap-panel:not([hidden]) .aap-panel-body');
|
||||
var head = document.querySelector('#aap-app .aap-sticky-head');
|
||||
|
||||
function update() {
|
||||
// Linha de referência: logo abaixo do cabeçalho visível
|
||||
var threshold = (head ? head.getBoundingClientRect().bottom : 0) + 24;
|
||||
var current = links[0];
|
||||
|
||||
links.forEach(function (link) {
|
||||
var card = document.getElementById(link.getAttribute('data-cat'));
|
||||
if (!card) return;
|
||||
if (card.getBoundingClientRect().top <= threshold) current = link;
|
||||
});
|
||||
|
||||
// A última seção costuma ser curta e nunca chega ao topo:
|
||||
// no fim da rolagem ela é a ativa.
|
||||
var atBottom;
|
||||
if (body && body.scrollHeight > body.clientHeight + 2) {
|
||||
atBottom = body.scrollTop + body.clientHeight >= body.scrollHeight - 2;
|
||||
} else {
|
||||
atBottom = window.innerHeight + window.pageYOffset >=
|
||||
document.documentElement.scrollHeight - 2;
|
||||
}
|
||||
if (atBottom) current = links[links.length - 1];
|
||||
|
||||
links.forEach(function (link) {
|
||||
link.classList.toggle('is-active', link === current);
|
||||
});
|
||||
}
|
||||
|
||||
// Evita recalcular a cada pixel: no máximo um cálculo por quadro
|
||||
var ticking = false;
|
||||
function onScroll() {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(function () {
|
||||
ticking = false;
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
// Rola dentro do painel (desktop) e na janela (mobile)
|
||||
if (body) body.addEventListener('scroll', onScroll, { passive: true });
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
window.addEventListener('resize', onScroll, { passive: true });
|
||||
update();
|
||||
})();
|
||||
|
||||
// ── Cabeçalho fixo de cada aba ─────────────────────────────────────────
|
||||
function adminBarHeight() {
|
||||
var bar = document.getElementById('wpadminbar');
|
||||
if (!bar) return 0;
|
||||
var r = bar.getBoundingClientRect();
|
||||
return r.bottom > 0 ? Math.round(r.height) : 0; // 32px (desktop) / 46px (mobile)
|
||||
}
|
||||
|
||||
// Altura total ocupada no topo = barra do WordPress (quando visível) + header
|
||||
function aapHeadHeight() {
|
||||
var head = document.querySelector('#aap-app .aap-sticky-head');
|
||||
return head ? Math.round(head.getBoundingClientRect().height) + adminBarHeight() : 0;
|
||||
}
|
||||
|
||||
(function stickyHead() {
|
||||
var head = document.querySelector('#aap-app .aap-sticky-head');
|
||||
var side = document.querySelector('#aap-app .aap-sidebar-inner');
|
||||
if (!head && !side) return;
|
||||
|
||||
function update() {
|
||||
var barH = adminBarHeight();
|
||||
var foot = document.getElementById('wpfooter');
|
||||
var footH = (foot && getComputedStyle(foot).display !== 'none')
|
||||
? Math.round(foot.getBoundingClientRect().height) : 0;
|
||||
|
||||
// Altura do rodapé usada no layout do painel (desktop)
|
||||
document.documentElement.style.setProperty('--aap-footer-h', footH + 'px');
|
||||
|
||||
// Cola exatamente abaixo da barra do WordPress; se ela sair da tela, cola no topo
|
||||
if (head) {
|
||||
head.style.top = barH + 'px';
|
||||
head.classList.toggle('is-stuck', window.pageYOffset > 0);
|
||||
document.documentElement.style.setProperty('--aap-head-h', aapHeadHeight() + 'px');
|
||||
}
|
||||
// A sidebar fica fixa: sai do fluxo da página e não é afetada pelo fim
|
||||
// da coluna (era isso que fazia os itens "subirem" no fim da rolagem).
|
||||
if (side) {
|
||||
if (window.matchMedia('(max-width: 782px)').matches) {
|
||||
// No mobile ela é empilhada no topo, sem posicionamento fixo
|
||||
clearSide();
|
||||
} else {
|
||||
var colRect = side.parentElement.getBoundingClientRect();
|
||||
|
||||
// No topo da página o app começa abaixo da barra (e de avisos);
|
||||
// ao rolar ele encosta na barra e ali permanece.
|
||||
var top = Math.max(barH, Math.round(colRect.top));
|
||||
var h = Math.max(200, Math.round(window.innerHeight - top - footH));
|
||||
|
||||
setSide('position', 'fixed');
|
||||
setSide('left', Math.round(colRect.left) + 'px');
|
||||
setSide('width', Math.round(colRect.width) + 'px');
|
||||
setSide('top', top + 'px');
|
||||
setSide('height', h + 'px');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Escreve no estilo só quando o valor muda, evitando recálculos por quadro
|
||||
var last = {};
|
||||
function setSide(prop, value) {
|
||||
if (last[prop] === value) return;
|
||||
last[prop] = value;
|
||||
side.style[prop] = value;
|
||||
}
|
||||
|
||||
function clearSide() {
|
||||
['position', 'left', 'width', 'top', 'height'].forEach(function (p) {
|
||||
if (last[p] !== '') { last[p] = ''; side.style[p] = ''; }
|
||||
});
|
||||
}
|
||||
|
||||
update();
|
||||
window.addEventListener('scroll', update, { passive: true });
|
||||
window.addEventListener('resize', update, { passive: true });
|
||||
})();
|
||||
|
||||
// ── Preferências de log (compartilhadas entre configurações e aba log) ──
|
||||
function pref(key, value) {
|
||||
try {
|
||||
if (value === undefined) return localStorage.getItem('aap_' + key) !== '0';
|
||||
localStorage.setItem('aap_' + key, value ? '1' : '0');
|
||||
} catch (err) {}
|
||||
return value;
|
||||
}
|
||||
|
||||
$doc.on('change', '.aap-pref-autoscroll', function () {
|
||||
var on = this.checked;
|
||||
$('.aap-pref-autoscroll').prop('checked', on);
|
||||
pref('autoscroll', on);
|
||||
});
|
||||
|
||||
$doc.on('change', '.aap-pref-live', function () {
|
||||
var on = this.checked;
|
||||
$('.aap-pref-live').prop('checked', on);
|
||||
pref('live', on);
|
||||
if (on) startLogPolling(); else stopLogPolling();
|
||||
});
|
||||
|
||||
// ── Mensagem de "configurações salvas" ──────────────────────────────────
|
||||
if (/[?&](aap-saved=1|settings-updated=true)/.test(window.location.search)) {
|
||||
var $toast = $('#aap-save-toast');
|
||||
var $saveFb = $('#aap-save-feedback');
|
||||
if ($toast.length && $saveFb.length) {
|
||||
$saveFb.text(s.settings_saved);
|
||||
$toast.prop('hidden', false);
|
||||
setTimeout(function () { $toast.prop('hidden', true); }, 5000);
|
||||
// Limpa a marca da URL para não repetir o aviso ao recarregar
|
||||
if (window.history && window.history.replaceState) {
|
||||
var clean = window.location.href
|
||||
.replace(/([?&])aap-saved=1&?/, '$1')
|
||||
.replace(/([?&])settings-updated=true&?/, '$1')
|
||||
.replace(/[?&]$/, '');
|
||||
window.history.replaceState({}, '', clean);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Estado de execução ──────────────────────────────────────────────────
|
||||
var $execSection = $('#rap-exec-section');
|
||||
var $execText = $('#rap-exec-text');
|
||||
var $execSub = $('#rap-exec-sub');
|
||||
var $progress = $('#rap-progress');
|
||||
var $runStatus = $('#rap-run-status');
|
||||
var $abortBtn = $('#rap-abort-btn');
|
||||
var $abortStatus = $('#rap-abort-status');
|
||||
var $lastRun = $('#rap-last-run');
|
||||
|
||||
function setBadge(processing, abortRequested) {
|
||||
if ($execSection.length) {
|
||||
$execSection.removeClass('is-idle is-aborting');
|
||||
if (processing) {
|
||||
$execSection.toggleClass('is-aborting', !!abortRequested);
|
||||
$progress.toggleClass('is-indeterminate', true);
|
||||
$execText.text(abortRequested ? s.aborting : s.running);
|
||||
if (aapData.started_label) $execSub.text(aapData.started_label);
|
||||
} else {
|
||||
$execSection.addClass('is-idle');
|
||||
$progress.removeClass('is-indeterminate');
|
||||
$execText.text(s.no_execution);
|
||||
$execSub.text(idleDesc);
|
||||
}
|
||||
if (processing) {
|
||||
$abortBtn.prop('disabled', false).show();
|
||||
} else {
|
||||
$abortBtn.hide();
|
||||
$abortStatus.text('');
|
||||
if (($runStatus.text() || '') === s.processing_feeds) $runStatus.text('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subtítulo do estado ocioso (vem pronto do servidor)
|
||||
var idleDesc = aapData.idle_desc || $execSub.text();
|
||||
|
||||
// ── Botão "Executar agora" ──────────────────────────────────────────────
|
||||
var $runBtn = $('#rap-run-now');
|
||||
|
||||
function setRunBtn(running) {
|
||||
$runBtn.prop('disabled', running);
|
||||
var label = running ? s.running : s.run_now;
|
||||
var $lbl = $runBtn.find('span');
|
||||
if ($lbl.length) $lbl.text(label); else $runBtn.text(label);
|
||||
$runBtn.toggleClass('is-busy', running);
|
||||
}
|
||||
|
||||
function showRunError(msg) {
|
||||
feedback($runStatus, s.error_prefix + ' ' + msg, 'error');
|
||||
}
|
||||
|
||||
// ── Polling permanente — o servidor é a fonte da verdade ────────────────
|
||||
var serverProcessing = aapData.processing;
|
||||
var consecutiveErrors = 0;
|
||||
var wasProcessing = serverProcessing;
|
||||
|
||||
setBadge(serverProcessing, false);
|
||||
setRunBtn(serverProcessing);
|
||||
|
||||
function handleAjaxError(xhr) {
|
||||
if (xhr.status === 403 || xhr.status === 400) {
|
||||
window.location.reload();
|
||||
}
|
||||
consecutiveErrors++;
|
||||
}
|
||||
|
||||
function pollStatus() {
|
||||
$.post(ajaxurl, { action: 'aap_check_status', nonce: aapData.nonce_status })
|
||||
.done(function (res) {
|
||||
consecutiveErrors = 0;
|
||||
if (!res.success) return;
|
||||
var was = serverProcessing;
|
||||
serverProcessing = res.data.processing;
|
||||
setBadge(serverProcessing, res.data.abort_requested);
|
||||
setRunBtn(serverProcessing);
|
||||
|
||||
if (res.data.last_run && $lastRun.length) {
|
||||
$lastRun.html(s.last_run_prefix + ' <strong>' + $('<span>').text(res.data.last_run).html() + '</strong>');
|
||||
}
|
||||
|
||||
// Execução terminou → recarrega a lista de últimos posts uma única vez
|
||||
if (was && !serverProcessing && aapData.tab === 'execucoes') {
|
||||
setTimeout(function () {
|
||||
if (window.location.hash !== '#no-reload') window.location.reload();
|
||||
}, 1500);
|
||||
}
|
||||
})
|
||||
.fail(handleAjaxError);
|
||||
}
|
||||
|
||||
setInterval(pollStatus, 3000);
|
||||
|
||||
// ── Iniciar pipeline (retorna imediatamente — roda em processo separado) ─
|
||||
$runBtn.on('click', function () {
|
||||
setRunBtn(true);
|
||||
setBadge(true, false);
|
||||
feedback($runStatus, s.preparing, 'pending');
|
||||
$abortStatus.text('');
|
||||
|
||||
saveSettings().always(function () {
|
||||
$.post(ajaxurl, { action: 'aap_start_pipeline', nonce: aapData.nonce_start })
|
||||
.done(function (res) {
|
||||
if (!res.success) {
|
||||
showRunError(res.data || s.error_start);
|
||||
setRunBtn(false);
|
||||
setBadge(false, false);
|
||||
} else {
|
||||
feedback($runStatus, s.processing_feeds, 'pending');
|
||||
}
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
var detail = xhr.responseText ? xhr.responseText.substring(0, 300) : s.comm_error;
|
||||
showRunError('HTTP ' + xhr.status + ' — ' + detail);
|
||||
setRunBtn(false);
|
||||
setBadge(false, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Abortar — encerra o processo imediatamente via PID ──────────────────
|
||||
function doAbort() {
|
||||
$abortBtn.prop('disabled', true);
|
||||
feedback($abortStatus, s.aborting, 'pending');
|
||||
setBadge(true, true);
|
||||
|
||||
$.post(ajaxurl, { action: 'aap_abort_execution', nonce: aapData.nonce_abort })
|
||||
.done(function (res) {
|
||||
if (res.success) {
|
||||
setBadge(false, false);
|
||||
setRunBtn(false);
|
||||
$abortStatus.text('');
|
||||
feedback($runStatus, s.aborted, 'warn');
|
||||
} else {
|
||||
feedback($abortStatus, res.data || s.error_abort, 'error');
|
||||
$abortBtn.prop('disabled', false);
|
||||
setBadge(serverProcessing, false);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
feedback($abortStatus, s.comm_error, 'error');
|
||||
$abortBtn.prop('disabled', false);
|
||||
});
|
||||
}
|
||||
|
||||
$abortBtn.on('click', doAbort);
|
||||
|
||||
// ── Geração automática ──────────────────────────────────────────────────
|
||||
var $toggleBtn = $('#rap-toggle-auto');
|
||||
var $autoBadge = $('#rap-auto-badge');
|
||||
var $autoBadgeDot = $('#rap-auto-badge-dot');
|
||||
var $autoBadgeText = $('#rap-auto-badge-text');
|
||||
var $nextRunEl = $('#rap-next-run');
|
||||
var $autoModeWrap = $('#rap-auto-mode-wrap');
|
||||
var $modeSchedule = $('#rap-mode-schedule');
|
||||
var $scheduleWrap = $('#rap-schedule-wrap');
|
||||
var $scheduledAt = $('#rap-scheduled-at');
|
||||
var $autoStatus = $('#rap-auto-status');
|
||||
var $interval = $('#rap-auto-interval');
|
||||
var $unit = $('#rap-auto-unit');
|
||||
var autoEnabled = aapData.auto_enabled;
|
||||
|
||||
// Mostra/esconde o campo de agendamento conforme o rádio selecionado
|
||||
$('input[name="aap_auto_mode"]').on('change', function () {
|
||||
if ($modeSchedule.is(':checked')) {
|
||||
$scheduleWrap.show();
|
||||
if (!$scheduledAt.val()) {
|
||||
var d = new Date();
|
||||
d.setHours(d.getHours() + 1, 0, 0, 0);
|
||||
var pad = function (n) { return String(n).padStart(2, '0'); };
|
||||
$scheduledAt.val(d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':00');
|
||||
}
|
||||
} else {
|
||||
$scheduleWrap.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Intervalo do automático também é uma opção salva — o valor editável fica na aba
|
||||
// Execuções, por isso o formulário de configurações é atualizado antes de qualquer save.
|
||||
$interval.add($unit).on('change', mirrorToForm);
|
||||
mirrorToForm();
|
||||
|
||||
function updateAutoUI(enabled, nextRun) {
|
||||
autoEnabled = enabled;
|
||||
$autoBadgeDot.toggleClass('is-on', !!enabled);
|
||||
$autoBadgeText.text(enabled ? s.auto_active : s.auto_inactive);
|
||||
$toggleBtn.text(enabled ? s.stop_auto : s.start_auto).toggleClass('aap-btn-outline', !!enabled).toggleClass('aap-btn-primary', !enabled);
|
||||
|
||||
if (enabled) {
|
||||
$autoModeWrap.hide();
|
||||
if (nextRun) $nextRunEl.html(s.next_run + ' <strong>' + nextRun + '</strong>').show();
|
||||
} else {
|
||||
$autoModeWrap.show();
|
||||
$nextRunEl.hide();
|
||||
}
|
||||
}
|
||||
|
||||
if (aapData.auto_enabled) updateAutoUI(true, aapData.next_run);
|
||||
|
||||
$toggleBtn.on('click', function () {
|
||||
var $btn = $(this).prop('disabled', true);
|
||||
var enable = !autoEnabled;
|
||||
|
||||
if (!enable) {
|
||||
// Parando o automático — salva configs antes
|
||||
feedback($autoStatus, '<em>' + s.wait + '</em>', 'pending');
|
||||
saveSettings().always(function () {
|
||||
$.post(ajaxurl, { action: 'aap_toggle_auto', nonce: aapData.nonce_toggle_auto, enable: 0 })
|
||||
.done(function (res) {
|
||||
if (res.success) {
|
||||
updateAutoUI(false, '');
|
||||
feedback($autoStatus, $('<span>').text(s.auto_paused).html(), 'pending');
|
||||
} else {
|
||||
feedback($autoStatus, s.error_prefix + ' ' + (res.data || s.error_start), 'error');
|
||||
}
|
||||
})
|
||||
.fail(function () { feedback($autoStatus, s.comm_error, 'error'); })
|
||||
.always(function () { $btn.prop('disabled', false); });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Iniciando o automático — valida o agendamento se necessário
|
||||
var mode = $modeSchedule.is(':checked') ? 'schedule' : 'now';
|
||||
var scheduledAt = 0;
|
||||
|
||||
if (mode === 'schedule') {
|
||||
var val = $scheduledAt.val();
|
||||
if (!val) {
|
||||
feedback($autoStatus, s.schedule_required, 'error');
|
||||
$btn.prop('disabled', false);
|
||||
return;
|
||||
}
|
||||
scheduledAt = Math.floor(new Date(val).getTime() / 1000);
|
||||
if (scheduledAt <= Math.floor(Date.now() / 1000)) {
|
||||
feedback($autoStatus, s.schedule_past, 'error');
|
||||
$btn.prop('disabled', false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
feedback($autoStatus, '<em>' + s.wait + '</em>', 'pending');
|
||||
|
||||
// Salva configs (incluindo o intervalo exibido aqui) antes de iniciar
|
||||
saveSettings().always(function () {
|
||||
$.post(ajaxurl, {
|
||||
action: 'aap_toggle_auto',
|
||||
nonce: aapData.nonce_toggle_auto,
|
||||
enable: 1,
|
||||
mode: mode,
|
||||
scheduled_at: scheduledAt,
|
||||
interval_value: $interval.val(),
|
||||
interval_unit: $unit.val(),
|
||||
})
|
||||
.done(function (res) {
|
||||
if (res.success) {
|
||||
updateAutoUI(res.data.enabled, res.data.next_run);
|
||||
feedback($autoStatus, res.data.enabled ? s.auto_started : s.auto_paused, res.data.enabled ? 'success' : 'pending');
|
||||
} else {
|
||||
feedback($autoStatus, s.error_prefix + ' ' + (res.data || s.error_start), 'error');
|
||||
}
|
||||
})
|
||||
.fail(function () { feedback($autoStatus, s.comm_error, 'error'); })
|
||||
.always(function () { $btn.prop('disabled', false); });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Log (banco de dados, paginado) ─────────────────────────────────────
|
||||
var $logBox = $('#rap-log-box');
|
||||
var $logStatus = $('#rap-log-status');
|
||||
var $logMore = $('#rap-log-more');
|
||||
var $logLevel = $('#rap-log-level');
|
||||
|
||||
var logPage = 1;
|
||||
var logFetching = false;
|
||||
var logTimer = null;
|
||||
var logIds = {};
|
||||
|
||||
if (pref('live', undefined) === false) {
|
||||
$('.aap-pref-live').prop('checked', false);
|
||||
}
|
||||
|
||||
function logRow(row) {
|
||||
var cls = row.level === 'error' ? 'erro' : row.level === 'warn' ? 'aviso' : 'info';
|
||||
return '<div class="aap-log-line" data-id="' + row.id + '">'
|
||||
+ '<span class="aap-log-time">' + $('<span>').text(row.time).html() + '</span>'
|
||||
+ '<span class="aap-log-level aap-log-level-' + cls + '">' + String(row.level).toUpperCase() + '</span>'
|
||||
+ '<span class="aap-log-msg">' + $('<span>').text(row.message).html() + '</span>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function appendRows(rows, prepend) {
|
||||
var html = '';
|
||||
|
||||
$.each(rows, function (_, row) {
|
||||
if (logIds[row.id]) return;
|
||||
logIds[row.id] = true;
|
||||
html += logRow(row);
|
||||
});
|
||||
|
||||
if (!html) return;
|
||||
|
||||
if (prepend) {
|
||||
$logBox.prepend(html);
|
||||
} else {
|
||||
$logBox.append(html);
|
||||
}
|
||||
}
|
||||
|
||||
function fetchLogs(page, mode) {
|
||||
if (logFetching || !$logBox.length) return;
|
||||
logFetching = true;
|
||||
|
||||
$.post(ajaxurl, {
|
||||
action: 'aap_fetch_logs',
|
||||
nonce: aapData.nonce_logs,
|
||||
page: page,
|
||||
per_page: aapData.log_per_page || 50,
|
||||
level: $logLevel.length ? $logLevel.val() : ''
|
||||
})
|
||||
.done(function (res) {
|
||||
if (!res.success) return;
|
||||
|
||||
var rows = res.data.rows || [];
|
||||
|
||||
if (mode === 'reset') {
|
||||
$logBox.empty();
|
||||
logIds = {};
|
||||
}
|
||||
|
||||
appendRows(rows, mode === 'new');
|
||||
|
||||
if (!$logBox.children().length) {
|
||||
$logBox.html('<span class="aap-log-empty">' + $('<span>').text(s.log_empty).html() + '</span>');
|
||||
} else {
|
||||
$logBox.find('.aap-log-empty').remove();
|
||||
}
|
||||
|
||||
$logMore.prop('hidden', !res.data.has_more);
|
||||
$logStatus.text(s.log_updated + ' ' + new Date().toLocaleTimeString());
|
||||
})
|
||||
.fail(function () {
|
||||
$logStatus.text(s.comm_error);
|
||||
})
|
||||
.always(function () {
|
||||
logFetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
function reloadLogs() {
|
||||
logPage = 1;
|
||||
fetchLogs(1, 'reset');
|
||||
}
|
||||
|
||||
function startLogPolling() {
|
||||
if (!$logBox.length) return;
|
||||
stopLogPolling();
|
||||
reloadLogs();
|
||||
if (pref('live', undefined)) {
|
||||
logTimer = setInterval(function () { fetchLogs(1, 'new'); }, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopLogPolling() {
|
||||
clearInterval(logTimer);
|
||||
logTimer = null;
|
||||
}
|
||||
|
||||
if ($logMore.length) {
|
||||
$logMore.on('click', function () {
|
||||
logPage += 1;
|
||||
fetchLogs(logPage, 'more');
|
||||
});
|
||||
}
|
||||
|
||||
if ($logLevel.length) {
|
||||
$logLevel.on('change', reloadLogs);
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (!$logBox.length) return;
|
||||
if (document.hidden) stopLogPolling(); else startLogPolling();
|
||||
});
|
||||
|
||||
if ($logBox.length) startLogPolling();
|
||||
});
|
||||
Reference in new issue
Block a user