mirror of
https://github.com/honmashironeko/ProxyCat.git
synced 2026-10-28 19:30:53 +00:00
2551 lines
87 KiB
JavaScript
2551 lines
87 KiB
JavaScript
/**
|
||
* 模块名称:面板外壳与「代理配置 / 访问控制 / 运行日志」页(app.js)
|
||
* 功能描述:面板骨架与三个自持视图:侧栏导航与 hash 路由、主题切换、toast 通知中心、通用弹窗、剪贴板、
|
||
* 全局请求反馈与 ajax 错误兜底、代理配置页的表单与名单、运行日志页的筛选与访问统计、出口气泡与广告位。
|
||
* 职责边界:负责:面板外壳与配置 / 访问控制 / 运行日志三个页签。不负责:池页签的业务与代理列表
|
||
* (pool.js、proxies.js)、浮层组件与表格状态占位(tables.js)、快捷键面板(palette.js)。
|
||
* 关键依赖:jQuery;i18n.js 的 T() / onLangRender / initLang;common.js 的 escapeHtml 等工具;
|
||
* tables.js 的表格占位、抽屉与键盘函数;pool.js 与 proxies.js 的池函数(tables.js 反向
|
||
* 调用本文件的 _overlayOpen / _overlayClosed);脚本加载顺序由 index.html 固定。
|
||
* 已知限制:
|
||
* 1. 脚本顺序为 jquery→i18n→common→tables→app→pool→proxies→palette;typeof 守卫只覆盖少量可选模块调用。
|
||
* 2. 视图切换只有 switchTab 一个入口,首次进入无 hash 也要走一遍;hash 写入走 replaceState,applyHash 必须幂等。
|
||
* 3. 本文件自持 /api/status 与日志轮询,池状态轮询归 pool.js;后台轮询失败只计数,判据须精确路径(indexOf 会吞掉主动请求的失败)。
|
||
* 4. 按钮三态:flashSaved / flashFailed 必须在 btnLoad(btn, false) 之后调,否则被 .loading 守卫跳过。
|
||
* 5. 动态拼 HTML 的用户可控文本必须经 escapeHtml 或 .text() 写入;用户名与密码不得拼进内联 onclick。
|
||
* 6. 主题无存档时跟随系统且不写存档,仅手动切换才持久化;index.html 首帧内联脚本判据须与 initTheme 一致。
|
||
* 7. 密码列打码时可见文本是圆点,collectUsers 必须读 .user-pass 的 data-pass,否则会把掩码存回。
|
||
* 8. loadUsers 失败时若清空表格,随后的保存会用空表覆盖真实账号(等于关掉认证),须保留旧行。
|
||
* 9. 弹窗回调只有 _modalCallback 一个槽位,OK / 取消须先取出并清空再调用;回调里再开弹窗(如 addUser 的两步
|
||
* 输入)时,调用后清空会抹掉内层刚注册的回调,表现为确认后毫无反应。
|
||
*/
|
||
|
||
var _focusReturn = null;
|
||
|
||
function _focusablesIn(sel) {
|
||
return $(sel).find('a[href], button:not(:disabled), input:not(:disabled), ' +
|
||
'select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])')
|
||
.filter(':visible');
|
||
}
|
||
|
||
function _overlayOpen(sel, focusTarget, lockScroll) {
|
||
if (_focusReturn === null) _focusReturn = document.activeElement;
|
||
if (lockScroll) {
|
||
var sbw = window.innerWidth - document.documentElement.clientWidth;
|
||
$('body').addClass('modal-open');
|
||
if (sbw > 0) $('body').css('padding-right', sbw + 'px');
|
||
}
|
||
setTimeout(function () {
|
||
var $t = (focusTarget && $(focusTarget).length) ? $(focusTarget) : _focusablesIn(sel).first();
|
||
if ($t.length) $t.trigger('focus');
|
||
}, 60);
|
||
}
|
||
|
||
function _overlayClosed() {
|
||
if ($('.modal-overlay.show, #row-drawer.show').length) return;
|
||
$('body').removeClass('modal-open').css('padding-right', '');
|
||
if (_focusReturn && document.body.contains(_focusReturn)) $(_focusReturn).trigger('focus');
|
||
_focusReturn = null;
|
||
}
|
||
|
||
$(document).on('keydown', function (e) {
|
||
if (e.key !== 'Tab') return;
|
||
var $box = $('.modal-overlay.show, #row-drawer.show').last();
|
||
if (!$box.length) return;
|
||
var $items = _focusablesIn('#' + $box.attr('id'));
|
||
if (!$items.length) return;
|
||
var first = $items[0], last = $items[$items.length - 1];
|
||
if (e.shiftKey && (document.activeElement === first || !$box[0].contains(document.activeElement))) {
|
||
e.preventDefault(); last.focus();
|
||
} else if (!e.shiftKey && (document.activeElement === last || !$box[0].contains(document.activeElement))) {
|
||
e.preventDefault(); first.focus();
|
||
}
|
||
});
|
||
|
||
function openModal(id) {
|
||
$('#' + id).addClass('show');
|
||
_overlayOpen('#' + id, null, true);
|
||
}
|
||
|
||
function closeModal(id) {
|
||
$('#' + id).removeClass('show');
|
||
_overlayClosed();
|
||
}
|
||
|
||
function copyToClipboard(text, onDone, onFail) {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(onDone, function () {
|
||
_copyFallback(text, onDone, onFail);
|
||
});
|
||
return;
|
||
}
|
||
_copyFallback(text, onDone, onFail);
|
||
}
|
||
|
||
function _copyFallback(text, onDone, onFail) {
|
||
if (copyViaTextarea(text)) onDone();
|
||
else if (onFail) onFail();
|
||
else window.prompt(T().copy_manually, text);
|
||
}
|
||
|
||
var TOAST_LIMIT = 4;
|
||
var TOAST_TTL = { ok: 3200, info: 3800, warn: 5200, err: 7000 };
|
||
var _toastLive = [];
|
||
var logTimer = null;
|
||
|
||
function _toastType(t) {
|
||
if (t === 'error') return 'err';
|
||
if (t === 'warn') return 'warn';
|
||
if (t === 'info') return 'info';
|
||
return 'ok';
|
||
}
|
||
|
||
function _toastIcon(type) {
|
||
if (type === 'err') return 'fa-times-circle';
|
||
if (type === 'warn') return 'fa-exclamation-triangle';
|
||
if (type === 'info') return 'fa-info-circle';
|
||
return 'fa-check-circle';
|
||
}
|
||
|
||
function _toastDrop(entry) {
|
||
var at = _toastLive.indexOf(entry);
|
||
if (at >= 0) _toastLive.splice(at, 1);
|
||
clearTimeout(entry.timer);
|
||
entry.timer = null;
|
||
entry.el.classList.add('leaving');
|
||
setTimeout(function () {
|
||
if (entry.el.parentNode) entry.el.parentNode.removeChild(entry.el);
|
||
}, 220);
|
||
}
|
||
|
||
function _toastArm(entry) {
|
||
clearTimeout(entry.timer);
|
||
entry.timer = setTimeout(function () { _toastDrop(entry); }, entry.ttl);
|
||
}
|
||
|
||
function dismissToast() {
|
||
_toastLive.slice().forEach(_toastDrop);
|
||
}
|
||
|
||
function _toastRestartBar(el, ttl) {
|
||
var old = el.querySelector('.toast-bar');
|
||
if (!old) return;
|
||
var bar = old.cloneNode(false);
|
||
bar.style.animationDuration = ttl + 'ms';
|
||
old.parentNode.replaceChild(bar, old);
|
||
}
|
||
|
||
function toast(m, t) {
|
||
m = m || (t === 'error' ? T().operation_failed_bare : T().operation_success);
|
||
var type = _toastType(t);
|
||
var stack = document.getElementById('toast-stack');
|
||
if (!stack) return;
|
||
|
||
for (var i = 0; i < _toastLive.length; i++) {
|
||
var live = _toastLive[i];
|
||
if (live.type !== type || live.text !== m) continue;
|
||
live.count++;
|
||
live.el.querySelector('.toast-count').textContent = '×' + live.count;
|
||
_toastRestartBar(live.el, live.ttl);
|
||
_toastArm(live);
|
||
return;
|
||
}
|
||
|
||
var el = document.createElement('div');
|
||
el.className = 'toast ' + type;
|
||
el.innerHTML =
|
||
'<i class="fa ' + _toastIcon(type) + ' toast-ico"></i>' +
|
||
'<span class="toast-msg"></span>' +
|
||
'<span class="toast-count"></span>' +
|
||
'<button type="button" class="toast-x"><i class="fa fa-times"></i></button>' +
|
||
'<span class="toast-bar" style="animation-duration:' + TOAST_TTL[type] + 'ms"></span>';
|
||
el.querySelector('.toast-msg').textContent = m;
|
||
var close = el.querySelector('.toast-x');
|
||
close.setAttribute('aria-label', T().drawer_close);
|
||
|
||
var entry = { el: el, type: type, text: m, count: 1, ttl: TOAST_TTL[type], timer: null };
|
||
close.addEventListener('click', function () { _toastDrop(entry); });
|
||
el.addEventListener('mouseenter', function () {
|
||
clearTimeout(entry.timer);
|
||
entry.timer = null;
|
||
});
|
||
el.addEventListener('mouseleave', function () {
|
||
if (!entry.timer) _toastArm(entry);
|
||
});
|
||
|
||
stack.appendChild(el);
|
||
_toastLive.push(entry);
|
||
while (_toastLive.length > TOAST_LIMIT) _toastDrop(_toastLive[0]);
|
||
_toastArm(entry);
|
||
}
|
||
|
||
function btnLoad(btn, loading) {
|
||
btn.prop('disabled', loading).toggleClass('loading', loading);
|
||
}
|
||
|
||
function markSavedHint(btn) {
|
||
var bar = $(btn).closest('.cfg-actions');
|
||
if (!bar.length) return;
|
||
bar.find('.dirty-hint').hide();
|
||
bar.find('.saved-hint')
|
||
.removeAttr('data-idle-hint')
|
||
.html('<i class="fa fa-check-circle"></i> ' +
|
||
escapeHtml(T().saved_at_hint + T().sep_colon + new Date().toLocaleTimeString()))
|
||
.show();
|
||
}
|
||
|
||
function idleSavedHintHtml() {
|
||
return '<i class="fa fa-check-circle"></i> ' + escapeHtml(T().saved_at_hint);
|
||
}
|
||
|
||
function refreshIdleSavedHints() {
|
||
$('.saved-hint[data-idle-hint]').html(idleSavedHintHtml());
|
||
}
|
||
|
||
onLangRender(refreshIdleSavedHints);
|
||
|
||
function setActionsIdle(selector, idle) {
|
||
var bar = $(selector).closest('.cfg-actions');
|
||
if (!bar.length) return;
|
||
bar.find('button').attr('aria-disabled', idle ? 'true' : null);
|
||
}
|
||
|
||
var _stickySyncRaf = null;
|
||
|
||
function syncStickyBars() {
|
||
$('.cfg-actions.sticky').each(function () {
|
||
var bar = $(this);
|
||
if (!bar.is(':visible')) { bar.removeClass('stuck'); return; }
|
||
var offset = parseFloat(window.getComputedStyle(this).bottom) || 0;
|
||
bar.toggleClass('stuck',
|
||
this.getBoundingClientRect().bottom >= window.innerHeight - offset - 1);
|
||
});
|
||
}
|
||
|
||
function scheduleStickySync() {
|
||
if (_stickySyncRaf) return;
|
||
_stickySyncRaf = window.requestAnimationFrame(function () {
|
||
_stickySyncRaf = null;
|
||
syncStickyBars();
|
||
});
|
||
}
|
||
|
||
$(window).on('scroll resize', scheduleStickySync);
|
||
|
||
function flashSaved(btn) {
|
||
var el = $(btn);
|
||
if (!el.length || el.hasClass('loading')) return;
|
||
el.addClass('done');
|
||
setTimeout(function () { el.removeClass('done'); }, 1200);
|
||
}
|
||
|
||
function flashResult(btn, ok) {
|
||
var el = $(btn);
|
||
if (!el.length) return;
|
||
btnLoad(el, false);
|
||
if (ok) flashSaved(el); else flashFailed(el);
|
||
}
|
||
|
||
function flashFailed(btn) {
|
||
var el = $(btn);
|
||
if (!el.length || el.hasClass('loading')) return;
|
||
el.addClass('failed');
|
||
setTimeout(function () { el.removeClass('failed'); }, 1600);
|
||
}
|
||
|
||
|
||
var _modalCallback = null;
|
||
|
||
function showModal(opts) {
|
||
var o = opts || {};
|
||
$('#modal-icon').attr('class', 'modal-icon ' + (o.icon || 'info'));
|
||
$('#modal-icon i').attr('class', 'fa ' + (o.iconClass || 'fa-question-circle'));
|
||
$('#modal-title').text(o.title || '');
|
||
$('#modal-body').text(o.body || '');
|
||
var input = $('#modal-input');
|
||
if (o.input) {
|
||
input.show().val(o.value || '').attr('placeholder', o.placeholder || '');
|
||
setTimeout(function () { input.focus(); }, 150);
|
||
} else {
|
||
input.hide().val('');
|
||
}
|
||
$('#modal-cancel').toggle(!!o.showCancel);
|
||
$('#modal-ok').find('span').text(o.okText || T().modal_confirm);
|
||
$('#modal-cancel').find('span').text(o.cancelText || T().modal_cancel);
|
||
$('#modal-ok').toggleClass('bad', !!o.danger).toggleClass('pri', !o.danger);
|
||
$('#modal-overlay').addClass('show');
|
||
_modalCallback = o.callback || null;
|
||
_overlayOpen('#modal-overlay', o.input ? input : $('#modal-ok'), true);
|
||
|
||
$('#modal-ok').off('click').on('click', function () {
|
||
var val = o.input ? input.val().trim() : true;
|
||
closeModal('modal-overlay');
|
||
var cb = _modalCallback;
|
||
_modalCallback = null;
|
||
if (cb) cb(val);
|
||
});
|
||
$('#modal-cancel').off('click').on('click', function () {
|
||
closeModal('modal-overlay');
|
||
var cb = _modalCallback;
|
||
_modalCallback = null;
|
||
if (cb) cb(null);
|
||
});
|
||
|
||
input.off('keydown').on('keydown', function (e) {
|
||
if (e.key === 'Enter') $('#modal-ok').click();
|
||
if (e.key === 'Escape') $('#modal-cancel').click();
|
||
});
|
||
$(document).off('keydown.modal').on('keydown.modal', function (e) {
|
||
if (e.key === 'Escape' && !o.input) $('#modal-cancel').click();
|
||
});
|
||
}
|
||
|
||
function showConfirm(title, body, cb, opts) {
|
||
showModal({
|
||
icon: 'warning', iconClass: 'fa-exclamation-triangle',
|
||
title: title, body: body, showCancel: true,
|
||
danger: !!(opts && opts.danger),
|
||
okText: T().modal_confirm, cancelText: T().modal_cancel,
|
||
callback: function (ok) { if (cb) cb(ok === true); }
|
||
});
|
||
}
|
||
|
||
function showPrompt(title, body, value, placeholder, cb) {
|
||
showModal({
|
||
icon: 'info', iconClass: 'fa-pencil',
|
||
title: title, body: body, input: true,
|
||
value: value || '', placeholder: placeholder || '',
|
||
showCancel: true,
|
||
okText: T().modal_confirm, cancelText: T().modal_cancel,
|
||
callback: function (val) { if (cb) cb(val); }
|
||
});
|
||
}
|
||
|
||
|
||
function nextTheme() {
|
||
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||
}
|
||
|
||
function cycleTheme() {
|
||
setTheme(nextTheme());
|
||
}
|
||
|
||
function setTheme(t) {
|
||
document.documentElement.setAttribute('data-theme', t);
|
||
renderThemeLabel();
|
||
safeSetItem('proxycat-theme', t);
|
||
}
|
||
|
||
function renderThemeLabel() {
|
||
onLangRender(renderThemeLabel);
|
||
var isDark = nextTheme() === 'dark';
|
||
var icon = isDark ? 'fa-sun-o' : 'fa-moon-o';
|
||
$('#theme-btn i').attr('class', 'fa ' + icon);
|
||
$('#theme-label').text(isDark ? T().theme_toggle_light : T().theme_toggle_dark);
|
||
}
|
||
|
||
function initTheme() {
|
||
var saved = safeGetItem('proxycat-theme');
|
||
if (saved) { setTheme(saved); return; }
|
||
var prefersDark = window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches;
|
||
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
||
renderThemeLabel();
|
||
}
|
||
|
||
|
||
function _startLogRefresh() {
|
||
if (isPolling('log')) return;
|
||
updateLogs();
|
||
startPolling('log', updateLogs, 2000);
|
||
loadLogFiles();
|
||
_startDomainRefresh();
|
||
}
|
||
|
||
function _stopLogRefresh() {
|
||
stopPolling('log');
|
||
_stopDomainRefresh();
|
||
}
|
||
var TAB_STORAGE_KEY = 'proxycat-active-tab';
|
||
var CONFIG_GROUP_STORAGE_KEY = 'proxycat-config-group';
|
||
|
||
var VIEW_SLUGS = {
|
||
'tab-config': 'config',
|
||
'tab-pool': 'pool',
|
||
'tab-access': 'access',
|
||
'tab-logs': 'logs'
|
||
};
|
||
var POOL_VIEW_SLUGS = {
|
||
'pool-view-manage': 'manage',
|
||
'pool-view-settings': 'settings',
|
||
'pool-view-db': 'db'
|
||
};
|
||
var CONFIG_GROUPS = ['source', 'listen', 'exits', 'check', 'perf', 'logs', 'advanced'];
|
||
var VIEW_TITLE_KEYS = {
|
||
'tab-config': 'config_tab',
|
||
'tab-pool': 'pool_tab',
|
||
'tab-access': 'access_control_tab',
|
||
'tab-logs': 'logs_tab'
|
||
};
|
||
var POOL_VIEW_SUB_KEYS = {
|
||
'pool-view-manage': 'view_sub_pool_manage',
|
||
'pool-view-settings': 'view_sub_pool_settings',
|
||
'pool-view-db': 'view_sub_pool_db'
|
||
};
|
||
|
||
function currentHash() {
|
||
var tabId = $('.tab-body.show').attr('id') || 'tab-config';
|
||
if (tabId === 'tab-config') {
|
||
return '#/config/' + (safeGetItem(CONFIG_GROUP_STORAGE_KEY) || 'source');
|
||
}
|
||
if (tabId === 'tab-pool') {
|
||
var view = safeGetItem(POOL_VIEW_STORAGE_KEY);
|
||
return '#/pool/' + (POOL_VIEW_SLUGS[view] || 'manage');
|
||
}
|
||
return '#/' + (VIEW_SLUGS[tabId] || 'config');
|
||
}
|
||
|
||
function writeHash() {
|
||
var next = currentHash();
|
||
if (location.hash === next) return;
|
||
history.replaceState(null, '', location.pathname + location.search + next);
|
||
}
|
||
|
||
function parseHash() {
|
||
var m = /^#\/([a-z]+)(?:\/([a-z]+))?/.exec(location.hash || '');
|
||
if (!m) return null;
|
||
var tabId = null;
|
||
for (var k in VIEW_SLUGS) { if (VIEW_SLUGS[k] === m[1]) tabId = k; }
|
||
return tabId ? { tab: tabId, section: m[2] || null } : null;
|
||
}
|
||
|
||
function applyHash() {
|
||
var r = parseHash();
|
||
if (!r) return false;
|
||
if (r.tab === 'tab-config') {
|
||
switchTab('tab-config', { group: CONFIG_GROUPS.indexOf(r.section) >= 0 ? r.section : null });
|
||
} else if (r.tab === 'tab-pool') {
|
||
var view = null;
|
||
for (var k in POOL_VIEW_SLUGS) { if (POOL_VIEW_SLUGS[k] === r.section) view = k; }
|
||
switchTab('tab-pool', { view: view });
|
||
} else {
|
||
switchTab(r.tab);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function toggleSidebar(open) {
|
||
var isOpen = open === undefined ? !$('body').hasClass('sb-open') : !!open;
|
||
$('body').toggleClass('sb-open', isOpen);
|
||
$('#sidebar').toggleClass('open', isOpen);
|
||
}
|
||
|
||
function syncSidebarActive(tabId, view) {
|
||
$('#sb-nav .sb-item').removeClass('cur');
|
||
$('#sb-nav .sb-item[data-tab="' + tabId + '"]').addClass('cur');
|
||
if (tabId === 'tab-pool' && view) {
|
||
$('#sb-nav .sb-item[data-view="' + view + '"]').addClass('cur');
|
||
}
|
||
}
|
||
|
||
function renderViewHead() {
|
||
onLangRender(renderViewHead);
|
||
var tabId = $('.tab-body.show').attr('id') || 'tab-config';
|
||
var titleKey = VIEW_TITLE_KEYS[tabId] || 'title';
|
||
var subKey = 'view_sub_' + (VIEW_SLUGS[tabId] || 'config');
|
||
if (tabId === 'tab-pool') {
|
||
var view = safeGetItem(POOL_VIEW_STORAGE_KEY);
|
||
if (POOL_VIEW_SUB_KEYS[view]) subKey = POOL_VIEW_SUB_KEYS[view];
|
||
}
|
||
$('#view-title').attr('data-i18n', titleKey).text(T()[titleKey]);
|
||
$('#view-sub').text(T()[subKey]);
|
||
}
|
||
|
||
function switchTab(tabId, opts) {
|
||
var o = opts || {};
|
||
if (!$('#' + tabId).length) return;
|
||
|
||
$('.tab-body').removeClass('show');
|
||
$('#' + tabId).addClass('show');
|
||
$('body').attr('data-tab', tabId);
|
||
safeSetItem(TAB_STORAGE_KEY, tabId);
|
||
|
||
if (tabId === 'tab-config') {
|
||
showConfigGroup(o.group || safeGetItem(CONFIG_GROUP_STORAGE_KEY) || 'source');
|
||
}
|
||
|
||
var view = null;
|
||
if (tabId === 'tab-pool') {
|
||
view = o.view || safeGetItem(POOL_VIEW_STORAGE_KEY) || 'pool-view-manage';
|
||
switchPoolView(view);
|
||
}
|
||
|
||
syncSidebarActive(tabId, view);
|
||
renderViewHead();
|
||
|
||
if (tabId === 'tab-logs') _startLogRefresh(); else _stopLogRefresh();
|
||
if (tabId === 'tab-pool') {
|
||
startPoolPolling();
|
||
restoreFiltersFromStorage();
|
||
if (poolReady()) {
|
||
loadPlugins();
|
||
loadSources();
|
||
loadProxies();
|
||
}
|
||
} else {
|
||
stopPoolPolling();
|
||
}
|
||
|
||
writeHash();
|
||
toggleSidebar(false);
|
||
window.scrollTo(0, 0);
|
||
scheduleStickySync();
|
||
}
|
||
|
||
$(document).on('click', '.sb-item[data-tab]', function () {
|
||
switchTab($(this).data('tab'));
|
||
});
|
||
|
||
$(document).on('click', '.sb-item[data-view]', function () {
|
||
var view = $(this).data('view');
|
||
if ($('#tab-pool').hasClass('show')) {
|
||
switchPoolView(view);
|
||
toggleSidebar(false);
|
||
} else {
|
||
switchTab('tab-pool', { view: view });
|
||
}
|
||
});
|
||
|
||
function initSidebar() {
|
||
$(window).on('resize', function () {
|
||
if (window.innerWidth > 1024) toggleSidebar(false);
|
||
});
|
||
$(window).on('hashchange', function () { applyHash(); });
|
||
}
|
||
|
||
function restoreActiveView() {
|
||
if (applyHash()) return;
|
||
var saved = safeGetItem(TAB_STORAGE_KEY);
|
||
switchTab(saved && $('#' + saved).length ? saved : 'tab-config');
|
||
}
|
||
|
||
|
||
function controlService(a) {
|
||
if (a !== 'start') {
|
||
showConfirm(T().svc_confirm_title,
|
||
a === 'stop' ? T().svc_stop_confirm_body : T().svc_restart_confirm_body,
|
||
function (ok) { if (ok) _doControlService(a); });
|
||
return;
|
||
}
|
||
_doControlService(a);
|
||
}
|
||
|
||
function _doControlService(a) {
|
||
var btn = $('#svc-' + a), ok = false;
|
||
btnLoad(btn, true);
|
||
$.ajax({
|
||
url: appendToken('/api/service'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ action: a }),
|
||
success: function (r) {
|
||
if (r.status === 'success') { ok = true; updateStatusAndGauge(); toast(r.message, 'success'); }
|
||
else toast(r.message, 'error');
|
||
},
|
||
error: function (x) { toast(fillTemplate(T().operation_failed, jqErrorText(x, T().network_error)), 'error'); },
|
||
complete: function () { flashResult(btn, ok); }
|
||
});
|
||
}
|
||
|
||
function svcBtns(s) {
|
||
var r = s === 'running';
|
||
$('#svc-start').prop('disabled', r);
|
||
$('#svc-stop, #svc-restart').prop('disabled', !r);
|
||
}
|
||
|
||
function toggleExitPop() {
|
||
$('#exit-pop').toggleClass('show');
|
||
$('#tag-exits').toggleClass('on', $('#exit-pop').hasClass('show'));
|
||
}
|
||
|
||
function closeExitPop() {
|
||
$('#exit-pop').removeClass('show');
|
||
$('#tag-exits').removeClass('on');
|
||
}
|
||
|
||
$(document).on('mousedown', function (e) {
|
||
if (!$('#exit-pop').hasClass('show')) return;
|
||
if ($(e.target).closest('#exit-pop, #tag-exits').length) return;
|
||
closeExitPop();
|
||
});
|
||
|
||
function renderSidebarStatus(serviceRunning, poolRunning) {
|
||
$('#sb-svc-dot').attr('class', 'dot ' + (serviceRunning ? 'on' : 'off'));
|
||
$('#sb-svc-text').text(T().service_status[serviceRunning ? 'running' : 'stopped'] || (serviceRunning ? 'running' : 'stopped'));
|
||
$('#sb-pool-dot').attr('class', 'dot ' + (poolRunning ? 'on' : 'off'));
|
||
$('#sb-pool-text').text(T().service_status[poolRunning ? 'running' : 'stopped'] || (poolRunning ? 'running' : 'stopped'));
|
||
}
|
||
|
||
|
||
$(document).on('click', '[data-copy]:not([data-act])', function () {
|
||
var text = $(this).attr('data-copy') || '';
|
||
if (!text) return;
|
||
copyToClipboard(text, function () {
|
||
toast(T().addr_copied + T().sep_colon + text, 'success');
|
||
}, function () {
|
||
toast(T().copy_failed_manual, 'error');
|
||
});
|
||
});
|
||
|
||
|
||
function copyAddr(t) {
|
||
var e = t === 'http' ? $('#addr-http') : $('#addr-socks5');
|
||
var x = e.find('span').text();
|
||
var done = function () {
|
||
e.addClass('copied');
|
||
toast(T().addr_copied + T().sep_colon + x, 'success');
|
||
setTimeout(function () { e.removeClass('copied'); }, 1500);
|
||
};
|
||
copyToClipboard(x, done, function () {
|
||
toast(T().copy_failed_manual, 'error');
|
||
});
|
||
}
|
||
|
||
function collectUsers() {
|
||
var users = {};
|
||
$('#user-table tbody tr').each(function () {
|
||
var tds = $(this).find('td');
|
||
var $pass = tds.eq(1).find('.user-pass');
|
||
users[tds.eq(0).text()] = $pass.length ? ($pass.attr('data-pass') || '') : tds.eq(1).text();
|
||
});
|
||
return users;
|
||
}
|
||
|
||
function pickAuth() {
|
||
var users = collectUsers();
|
||
var names = Object.keys(users).filter(function (n) { return n; });
|
||
var name = names.length ? names[0] : '';
|
||
return (name && users[name]) ? name + ':' + users[name] + '@' : '';
|
||
}
|
||
|
||
function updAddr(c) {
|
||
var port = c.port || '1080', a = pickAuth();
|
||
$('#http-addr').text('http://' + a + '127.0.0.1:' + port);
|
||
$('#socks5-addr').text('socks5://' + a + '127.0.0.1:' + port);
|
||
}
|
||
|
||
|
||
|
||
function startGlobalPolling() {
|
||
if (isPolling('status')) return;
|
||
startPolling('status', updateStatusAndGauge, 1000);
|
||
}
|
||
|
||
function pauseGlobalPolling() {
|
||
stopPolling('status');
|
||
if (typeof _stopLogRefresh === 'function') _stopLogRefresh();
|
||
if (typeof stopPoolPolling === 'function') stopPoolPolling();
|
||
}
|
||
|
||
function exitRemainingText(e, cm) {
|
||
var parts = [];
|
||
if (cm === 'time' && e.expires_in !== null && e.expires_in !== undefined) {
|
||
parts.push(escapeHtml(Math.ceil(e.expires_in)) + T().seconds);
|
||
}
|
||
if ((cm === 'time' || cm === 'request_count')
|
||
&& e.requests_left !== null && e.requests_left !== undefined) {
|
||
parts.push(escapeHtml(e.requests_left) + T().times);
|
||
}
|
||
return parts.length ? parts.join(' · ') : '--';
|
||
}
|
||
|
||
function _sinceText(iso) {
|
||
if (!iso) return '';
|
||
var at = Date.parse(iso);
|
||
if (isNaN(at)) return '';
|
||
return fillTemplate(T().source_error_ago, fmtDuration((Date.now() - at) / 1000));
|
||
}
|
||
|
||
function renderExitRows(exits, cm) {
|
||
var $list = $('#exit-list'), $rows = $('#exit-rows');
|
||
if (!exits || !exits.length) {
|
||
$rows.html('<div class="exit-row exit-empty">' + escapeHtml(T().exit_no_data) + '</div>');
|
||
$list.show();
|
||
return;
|
||
}
|
||
$rows.html(exits.map(function (e) {
|
||
var load = e.capacity > 0
|
||
? escapeHtml(e.active) + '/' + escapeHtml(e.capacity)
|
||
: fillTemplate(T().exit_load_unlimited, e.active);
|
||
var flags = '';
|
||
if (e.suspect) {
|
||
flags += '<i class="fa fa-exclamation-triangle exit-flag" title="' +
|
||
escapeHtml(T().exit_suspect) + '"></i>';
|
||
}
|
||
if (e.failures > 0) {
|
||
flags += '<span class="exit-fails" title="' +
|
||
escapeHtml(fillTemplate(T().exit_failures, e.failures)) + '">' +
|
||
escapeHtml(e.failures) + '</span>';
|
||
}
|
||
var tip = e.requests_served
|
||
? fillTemplate(T().exit_served, e.requests_served)
|
||
: '';
|
||
return '<div class="exit-row"' + (tip ? ' title="' + escapeHtml(tip) + '"' : '') + '>' +
|
||
'<span class="exit-url">' + escapeHtml(e.url) + '</span>' + flags +
|
||
'<span class="exit-load">' + load + '</span>' +
|
||
'<span class="exit-left">' + exitRemainingText(e, cm) + '</span>' +
|
||
'</div>';
|
||
}).join(''));
|
||
$list.show();
|
||
}
|
||
|
||
function updateStatusAndGauge() {
|
||
onLangRender(updateStatusAndGauge);
|
||
$.get(appendToken('/api/status'), function (d) {
|
||
var s = d.service_status || 'stopped', r = s === 'running';
|
||
$('#svc-dot').attr('class', 'dot ' + (r ? 'on' : 'off'));
|
||
$('#svc-text').text(T().service_status[s] || s);
|
||
renderSidebarStatus(r, !!d.pool_running);
|
||
svcBtns(s);
|
||
var m = d.proxy_source_mode || 'local', modes = {
|
||
local: T().proxy_source_mode_local, api: T().proxy_source_mode_api, pool: T().proxy_source_mode_pool
|
||
};
|
||
$('#tag-source').text((modes[m] || m));
|
||
if (m === 'local') $('#tag-mode').show().text(T().proxy_mode[d.mode] || d.mode || '--');
|
||
else $('#tag-mode').hide();
|
||
var iv = parseInt(d.interval) || 0;
|
||
var cm = d.switch_countdown_mode || 'time';
|
||
var exitList = d.active_proxies || [];
|
||
var exitText = fillTemplate(
|
||
d.elastic_active ? T().upstream_exits_expanding : T().upstream_exits, exitList.length);
|
||
$('#tag-exits').show().text(exitText);
|
||
renderExitRows(exitList, cm);
|
||
if (cm === 'per_request') { $('#tag-interval').text(T().pool_per_request_swap); $('#tip-zero').show(); }
|
||
else if (cm === 'request_count') { $('#tag-interval').text((parseInt(d.request_interval) || 0) + ' ' + T().times); $('#tip-zero').hide(); }
|
||
else { $('#tag-interval').text(iv + T().seconds); $('#tip-zero').hide(); }
|
||
if (d.source_error && d.source_error.message) {
|
||
$('#tip-source-error-text').text(
|
||
fillTemplate(T().source_error_hint, d.source_error.message) +
|
||
_sinceText(d.source_error.at));
|
||
$('#tip-source-error').show();
|
||
} else {
|
||
$('#tip-source-error').hide();
|
||
}
|
||
updAddr(d);
|
||
|
||
if (cm === 'none') { $('#gauge-bar').css('width', '100%'); $('#next-switch').text('--'); return; }
|
||
if (cm === 'per_request') { $('#gauge-bar').css('width', '100%'); $('#next-switch').text(T().pool_per_request_swap); return; }
|
||
if (!exitList.length) {
|
||
$('#gauge-bar').css('width', '0%');
|
||
$('#next-switch').text(T().pool_waiting_exits);
|
||
return;
|
||
}
|
||
var tl = Math.max(0, d.time_left || 0);
|
||
if (tl <= 0) {
|
||
$('#gauge-bar').css('width', '100%');
|
||
$('#next-switch').text(T().pool_rotate_waiting);
|
||
return;
|
||
}
|
||
if (cm === 'request_count') {
|
||
var ri = parseInt(d.request_interval) || 0;
|
||
if (ri <= 0) { $('#gauge-bar').css('width', '100%'); $('#next-switch').text('--'); return; }
|
||
$('#next-switch').text(Math.ceil(tl) + ' ' + T().times);
|
||
$('#gauge-bar').css('width', Math.min(100, ((ri - tl) / ri) * 100) + '%');
|
||
return;
|
||
}
|
||
$('#next-switch').text(Math.ceil(tl) + ' ' + T().seconds);
|
||
$('#gauge-bar').css('width', (iv > 0 ? Math.min(100, ((iv - tl) / iv) * 100) : 0) + '%');
|
||
});
|
||
}
|
||
|
||
|
||
function setSourceMode(mode) {
|
||
$('#source-mode-group .source-mode-btn').removeClass('active');
|
||
$('#source-mode-group .source-mode-btn[data-mode="' + mode + '"]').addClass('active');
|
||
$('#proxy-source-mode').val(mode);
|
||
onSourceChange();
|
||
updateServerDirtyState();
|
||
}
|
||
|
||
function loadConfig(opts) {
|
||
var o = opts || {};
|
||
$.get(appendToken('/api/config'), function (d) {
|
||
if (!o.poolOnly) applyServerConfigToForm(d.server || {});
|
||
if (!o.serverOnly && typeof applyPoolConfigToForm === 'function') {
|
||
applyPoolConfigToForm(d.pool || {}, o.force);
|
||
}
|
||
});
|
||
}
|
||
|
||
function discardServerChanges() {
|
||
if (!serverFormDirty()) { toast(T().no_changes, 'error'); return; }
|
||
loadConfig({ serverOnly: true });
|
||
}
|
||
|
||
var SERVER_FORM_DEFAULTS = {
|
||
proxy_source_mode: 'local', proxy_file: 'ip.txt', mode: 'cycle',
|
||
api_proxy_url: '', proxy_username: '', proxy_password: '', pool_remote_url: '',
|
||
client_idle_timeout: '300',
|
||
max_pool_size: '500',
|
||
client_max_connections: '1000',
|
||
port: '1080', interval: '300', request_interval: '0', exit_count: '5',
|
||
test_url: 'https://www.baidu.com', ip_auth_priority: 'whitelist', log_level: 'INFO',
|
||
check_proxies_on_startup: 'True', check_proxies_on_use: 'True',
|
||
log_access_enabled: 'true', access_records_enabled: 'true', domain_stats_enabled: 'true',
|
||
access_records_real_ip_probe: 'false',
|
||
access_records_retention_days: '7', access_records_max_rows: '200000',
|
||
domain_stats_retention_days: '30',
|
||
exit_wait_timeout: '15', max_concurrent_per_proxy: '0',
|
||
auto_expand_enabled: 'true',
|
||
client_idle_timeout: '300', client_max_keepalive: '8',
|
||
client_max_connections: '1000', client_keepalive_expiry: '30',
|
||
max_concurrent_requests: '1000', max_pool_size: '500', buffer_size: '8192',
|
||
check_concurrency: '50', tunnel_idle_timeout: '10', web_port: '5001',
|
||
proxy_check_ttl: '60', check_cooldown: '10', proxy_failure_cooldown: '3',
|
||
switch_cooldown: '2', access_records_flush_interval: '5',
|
||
access_records_buffer_size: '20000', domain_stats_flush_interval: '15',
|
||
log_max_bytes: '10485760', log_backup_count: '3',
|
||
whitelist_file: 'whitelist.txt', blacklist_file: 'blacklist.txt',
|
||
bypass_whitelist_file: 'bypass_whitelist.txt', display_level: '1'
|
||
};
|
||
|
||
var SERVER_CHECKBOX_FIELDS = {
|
||
check_proxies_on_startup: true, check_proxies_on_use: true,
|
||
log_access_enabled: true, access_records_enabled: true,
|
||
access_records_real_ip_probe: true,
|
||
domain_stats_enabled: true, auto_expand_enabled: true
|
||
};
|
||
|
||
var _serverFormBaseline = null;
|
||
|
||
function _serverField(key) { return $('[name="' + key + '"]'); }
|
||
|
||
function _isServerCheckbox(key) { return SERVER_CHECKBOX_FIELDS[key] === true; }
|
||
|
||
function _normalizeDisplayLevel(value) {
|
||
var n = parseInt(value, 10);
|
||
if (isNaN(n) || n < 0) return SERVER_FORM_DEFAULTS.display_level;
|
||
return n >= 2 ? '2' : String(n);
|
||
}
|
||
|
||
function _writeServerField(key, rawValue) {
|
||
var missing = (rawValue === undefined || rawValue === null || rawValue === '');
|
||
var value = missing ? SERVER_FORM_DEFAULTS[key] : rawValue;
|
||
if (_isServerCheckbox(key)) {
|
||
_serverField(key).prop('checked', String(value).toLowerCase() === 'true');
|
||
return;
|
||
}
|
||
if (key === 'display_level') value = _normalizeDisplayLevel(value);
|
||
_serverField(key).val(key === 'log_level' ? String(value).toUpperCase() : value);
|
||
}
|
||
|
||
function _readServerField(key) {
|
||
if (_isServerCheckbox(key)) return _serverField(key).prop('checked').toString();
|
||
return _serverField(key).val();
|
||
}
|
||
|
||
function applyServerConfigToForm(c) {
|
||
var m = c.proxy_source_mode || 'local';
|
||
$('#source-mode-group .source-mode-btn').removeClass('active');
|
||
$('#source-mode-group .source-mode-btn[data-mode="' + m + '"]').addClass('active');
|
||
syncModeField(m);
|
||
for (var key in SERVER_FORM_DEFAULTS) _writeServerField(key, c[key]);
|
||
if ($('#proxy-auth').length) {
|
||
$('#proxy-auth').val(_joinProxyAuth(c.proxy_username, c.proxy_password));
|
||
}
|
||
syncProxyAuthHint();
|
||
onSourceChange(); onIntervalChange(); updAddr(c);
|
||
markServerFormClean();
|
||
updateLogLevelWarning();
|
||
}
|
||
|
||
function getSourceMode() { return _serverField('proxy_source_mode').val() || 'local'; }
|
||
|
||
function _splitProxyAuth(text) {
|
||
var raw = String(text === null || text === undefined ? '' : text);
|
||
var at = raw.indexOf(':');
|
||
if (at === -1) return { username: raw, password: '' };
|
||
return { username: raw.slice(0, at), password: raw.slice(at + 1) };
|
||
}
|
||
|
||
function _joinProxyAuth(username, password) {
|
||
var user = String(username || '');
|
||
return user ? (user + ':' + String(password || '')) : '';
|
||
}
|
||
|
||
function syncProxyAuthHint() {
|
||
var text = String($('#proxy-auth').val() || '');
|
||
$('#proxy-auth-warn').toggle(text !== '' && text.indexOf(':') === -1);
|
||
}
|
||
|
||
function collectServerConfig() {
|
||
var values = {};
|
||
for (var key in SERVER_FORM_DEFAULTS) {
|
||
if (!_serverField(key).length) continue;
|
||
values[key] = _readServerField(key);
|
||
}
|
||
if ($('#proxy-auth').length) {
|
||
var auth = _splitProxyAuth($('#proxy-auth').val());
|
||
values.proxy_username = auth.username;
|
||
values.proxy_password = auth.password;
|
||
}
|
||
var idle = parseInt(values.client_keepalive_expiry, 10);
|
||
values.client_idle_timeout = String(Math.max(isNaN(idle) ? 0 : idle, 30));
|
||
return values;
|
||
}
|
||
|
||
function serverConfigChanges() {
|
||
var current = collectServerConfig();
|
||
var changes = {};
|
||
for (var key in current) {
|
||
if (String(current[key]) !== String(_serverFormBaseline ? _serverFormBaseline[key] : undefined)) {
|
||
changes[key] = current[key];
|
||
}
|
||
}
|
||
return changes;
|
||
}
|
||
|
||
function markServerFormClean() {
|
||
_serverFormBaseline = collectServerConfig();
|
||
updateServerDirtyState();
|
||
}
|
||
|
||
function syncServerBaseline(keys) {
|
||
if (!_serverFormBaseline) return;
|
||
var current = collectServerConfig();
|
||
keys.forEach(function (key) {
|
||
_serverFormBaseline[key] = (key in current) ? current[key] : _readServerField(key);
|
||
});
|
||
updateServerDirtyState();
|
||
}
|
||
|
||
function serverFormDirty() {
|
||
return Object.keys(serverConfigChanges()).length > 0;
|
||
}
|
||
|
||
function updateServerDirtyState() {
|
||
var ready = _serverFormBaseline !== null;
|
||
var dirty = ready && serverFormDirty();
|
||
$('#server-dirty-hint').toggle(dirty);
|
||
$('#btn-save-config').toggleClass('dirty', dirty);
|
||
|
||
var saved = $('#server-saved-hint');
|
||
if (dirty) {
|
||
saved.hide();
|
||
} else if (ready) {
|
||
if (!saved.children().length) saved.html(idleSavedHintHtml()).attr('data-idle-hint', '1');
|
||
saved.show();
|
||
}
|
||
setActionsIdle('#btn-save-config', ready && !dirty);
|
||
|
||
if (!ready) return;
|
||
$('#config-nav .settings-nav-item').each(function () {
|
||
var group = $(this).data('group');
|
||
var groupDirty = $('#tab-config .settings-group[data-group="' + group + '"]')
|
||
.find('[name]').toArray().some(function (el) {
|
||
if (!(el.name in SERVER_FORM_DEFAULTS)) return false;
|
||
return String(_readServerField(el.name)) !== String(_serverFormBaseline[el.name]);
|
||
});
|
||
$(this).find('.settings-dirty-dot').toggle(groupDirty);
|
||
});
|
||
}
|
||
|
||
$(document).on('input change', '#tab-config [name]', updateServerDirtyState);
|
||
|
||
function updateLogLevelWarning() {
|
||
var level = String(_serverField('log_level').val() || 'INFO').toUpperCase();
|
||
var suppressesAccessLog = level !== 'DEBUG' && level !== 'INFO';
|
||
var recordsEnabled = _serverField('log_access_enabled').prop('checked');
|
||
$('#log-level-warning').toggle(suppressesAccessLog && recordsEnabled);
|
||
}
|
||
|
||
$(document).on('change', 'select[name="log_level"], input[name="log_access_enabled"]',
|
||
updateLogLevelWarning);
|
||
|
||
function showSettingsGroup(navSelector, group) {
|
||
var layout = $(navSelector).closest('.settings-layout');
|
||
layout.find('.settings-nav-item').removeClass('cur');
|
||
layout.find('.settings-nav-item[data-group="' + group + '"]').addClass('cur');
|
||
layout.find('.settings-group').removeClass('show');
|
||
layout.find('.settings-group[data-group="' + group + '"]').addClass('show');
|
||
scheduleStickySync();
|
||
}
|
||
|
||
function showConfigGroup(group) {
|
||
showSettingsGroup('#config-nav', group);
|
||
safeSetItem(CONFIG_GROUP_STORAGE_KEY, group);
|
||
writeHash();
|
||
}
|
||
|
||
$(document).on('click', '#config-nav .settings-nav-item', function () {
|
||
showConfigGroup($(this).data('group'));
|
||
});
|
||
|
||
$(window).on('beforeunload', function (e) {
|
||
if (serverFormDirty()) {
|
||
e.preventDefault();
|
||
e.originalEvent.returnValue = '';
|
||
return '';
|
||
}
|
||
});
|
||
|
||
function submitConfig(sections, btn, onSaved) {
|
||
if (btn) btnLoad(btn, true);
|
||
var ok = false;
|
||
$.ajax({
|
||
url: appendToken('/api/config'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify(sections),
|
||
success: function (r) {
|
||
if (r.status !== 'success') { toast(r.message, 'error'); return; }
|
||
ok = true;
|
||
toast(T().config_save_success, 'success');
|
||
if (btn) { btnLoad(btn, false); flashSaved(btn); markSavedHint(btn); }
|
||
if (typeof onSaved === 'function') onSaved();
|
||
if (r.applied && r.applied.proxy === false) {
|
||
toast(T().config_apply_failed, 'error');
|
||
}
|
||
if (r.applied && r.applied.pool === false) {
|
||
toast(T().pool_config_apply_failed, 'error');
|
||
}
|
||
if (r.port_changed) {
|
||
if (r.service_status === 'running') {
|
||
$.ajax({
|
||
url: appendToken('/api/service'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ action: 'restart' }),
|
||
success: function (rr) {
|
||
if (rr && rr.status === 'success') toast(rr.message, 'success');
|
||
else toast((rr && rr.message) || T().config_save_failed, 'error');
|
||
updateStatusAndGauge();
|
||
},
|
||
error: function (x) {
|
||
toast(fillTemplate(T().operation_failed, jqErrorText(x)), 'error');
|
||
updateStatusAndGauge();
|
||
}
|
||
});
|
||
} else {
|
||
toast(T().port_saved_stopped, 'success');
|
||
}
|
||
}
|
||
loadConfig({ poolOnly: !sections.server && !!sections.pool });
|
||
},
|
||
error: function (x) { toastServerConfigError(x); },
|
||
complete: function () {
|
||
if (btn && !ok) flashResult(btn, false);
|
||
}
|
||
});
|
||
}
|
||
|
||
function _configField(key) {
|
||
var named = _serverField(key);
|
||
if (named.length) return named;
|
||
var found = $();
|
||
$('#pool-form [data-pool-key]').each(function () {
|
||
if ($(this).data('pool-key') === key) { found = $(this); return false; }
|
||
});
|
||
return found;
|
||
}
|
||
|
||
function _configFieldLabel(field) {
|
||
return field.closest('.fg')
|
||
.find('.fg-label, .fg-check label').first().text();
|
||
}
|
||
|
||
function _revealServerField(key) {
|
||
var group = _configField(key).closest('.settings-group');
|
||
if (!group.length) return;
|
||
var nav = group.closest('.settings-layout').find('.settings-nav');
|
||
if (nav.length) showSettingsGroup('#' + nav.attr('id'), group.data('group'));
|
||
}
|
||
|
||
function toastServerConfigError(x) {
|
||
var details = null;
|
||
try { details = JSON.parse(x.responseText).details; } catch (e) { details = null; }
|
||
var reason = details && details.reason;
|
||
if (details && details.key && reason) {
|
||
var label = _configFieldLabel(_configField(details.key));
|
||
if (label) {
|
||
_revealServerField(details.key);
|
||
toast(fillTemplate(T().config_field_invalid, label + T().sep_colon + reason), 'error');
|
||
return;
|
||
}
|
||
}
|
||
toast(T().config_save_failed + T().sep_colon + jqErrorText(x), 'error');
|
||
}
|
||
|
||
function saveAllConfig() {
|
||
var changes = serverConfigChanges();
|
||
if (!Object.keys(changes).length) {
|
||
toast(T().no_changes, 'info');
|
||
return;
|
||
}
|
||
if (changes.port === undefined) {
|
||
submitConfig({ server: changes }, $('#btn-save-config'));
|
||
return;
|
||
}
|
||
showConfirm(T().config_port_restart_title, T().config_port_restart_body, function (ok) {
|
||
if (ok) submitConfig({ server: changes }, $('#btn-save-config'));
|
||
});
|
||
}
|
||
|
||
var MODE_FIELD_BY_SOURCE = {
|
||
local: {
|
||
label: 'run_mode_label', hint: 'run_mode_hint', doc: 'run_mode_doc',
|
||
options: [['cycle', 'cycle_mode'], ['loadbalance', 'loadbalance_mode']]
|
||
},
|
||
api: {
|
||
label: 'rotation_mode_label', hint: 'rotation_mode_hint', doc: 'rotation_mode_doc',
|
||
options: [['request', 'mode_request'], ['continuous', 'mode_continuous']]
|
||
}
|
||
};
|
||
MODE_FIELD_BY_SOURCE.pool = MODE_FIELD_BY_SOURCE.api;
|
||
|
||
function syncModeField(sourceMode) {
|
||
var spec = MODE_FIELD_BY_SOURCE[sourceMode] || MODE_FIELD_BY_SOURCE.api;
|
||
var $sel = $('#mode-select');
|
||
if (!$sel.length) return;
|
||
|
||
var allowed = spec.options.map(function (o) { return o[0]; });
|
||
var current = $sel.val();
|
||
if (allowed.indexOf(current) === -1) current = allowed[0];
|
||
|
||
$sel.html(spec.options.map(function (o) {
|
||
return '<option value="' + o[0] + '" data-i18n="' + o[1] + '">' +
|
||
escapeHtml(T()[o[1]] || o[1]) + '</option>';
|
||
}).join(''));
|
||
$sel.val(current);
|
||
|
||
$('#mode-label').attr('data-i18n', spec.label).text(T()[spec.label] || spec.label);
|
||
$('#mode-hint').attr('data-i18n', spec.hint).text(T()[spec.hint] || spec.hint);
|
||
$('#mode-tip').attr('data-i18n-title', spec.doc)
|
||
.attr('title', T()[spec.doc] || '');
|
||
}
|
||
|
||
function _visibilityClauseHolds(clause) {
|
||
clause = clause.trim();
|
||
if (!clause) return true;
|
||
var negate = clause.indexOf('!=') !== -1;
|
||
var splitAt = negate ? clause.indexOf('!=') : clause.indexOf('=');
|
||
var key = clause.slice(0, splitAt).trim();
|
||
var wanted = clause.slice(splitAt + (negate ? 2 : 1)).split('|')
|
||
.map(function (v) { return v.trim(); });
|
||
var field = _serverField(key);
|
||
if (!field.length) field = $('#pool-form [data-pool-key="' + key + '"]');
|
||
if (!field.length) return true;
|
||
var actual = field.prop('type') === 'checkbox'
|
||
? field.prop('checked').toString()
|
||
: String(field.val() === null || field.val() === undefined ? '' : field.val()).trim();
|
||
var hit = wanted.indexOf(actual) !== -1;
|
||
return negate ? !hit : hit;
|
||
}
|
||
|
||
function syncFieldVisibility() {
|
||
$('[data-visible-when]').each(function () {
|
||
var clauses = String($(this).attr('data-visible-when') || '').split(';');
|
||
var show = true;
|
||
for (var i = 0; i < clauses.length; i++) {
|
||
if (!_visibilityClauseHolds(clauses[i])) { show = false; break; }
|
||
}
|
||
$(this).toggle(show);
|
||
});
|
||
}
|
||
|
||
$(document).on('change', '#tab-config input, #tab-config select, #pool-form input, #pool-form select',
|
||
function () {
|
||
syncFieldVisibility();
|
||
syncProxyAuthHint();
|
||
});
|
||
|
||
function onSourceChange() {
|
||
var m = getSourceMode();
|
||
syncModeField(m);
|
||
syncFieldVisibility();
|
||
if (m === 'pool') updatePoolStatus();
|
||
if (m === 'api') loadApiCredentials();
|
||
}
|
||
|
||
function onIntervalChange() {
|
||
var seconds = parseInt($('input[name="interval"]').val()) || 0;
|
||
var requests = parseInt($('input[name="request_interval"]').val()) || 0;
|
||
$('#tip-zero').toggle(seconds === 0 && requests === 0);
|
||
}
|
||
|
||
|
||
function switchProxy() {
|
||
var btn = $('#switch-btn'); if (btn.prop('disabled')) return;
|
||
btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> <span>' + T().switching + '</span>');
|
||
$.get(appendToken('/api/switch_proxy'), function (d) {
|
||
if (d.status === 'success') { toast(d.message, 'success'); updateStatusAndGauge(); btn.prop('disabled', false).html('<i class="fa fa-refresh"></i> <span>' + T().manual_switch_btn + '</span>'); }
|
||
else if (d.cooldown) {
|
||
toast(d.message, 'error');
|
||
var rem = d.cooldown_remaining || 2;
|
||
btn.prop('disabled', true).html('<i class="fa fa-clock-o"></i> <span>' + Math.ceil(rem) + 's</span>');
|
||
var iv = setInterval(function () {
|
||
rem -= 0.1;
|
||
if (rem <= 0) { clearInterval(iv); btn.prop('disabled', false); btn.html('<i class="fa fa-refresh"></i> <span>' + T().manual_switch_btn + '</span>'); }
|
||
else btn.html('<i class="fa fa-clock-o"></i> <span>' + Math.ceil(rem) + 's</span>');
|
||
}, 100);
|
||
} else if (d.switching) {
|
||
toast(d.message, 'error');
|
||
btn.prop('disabled', false).html('<i class="fa fa-refresh"></i> <span>' + T().manual_switch_btn + '</span>');
|
||
} else {
|
||
toast(d.message, 'error');
|
||
updateStatusAndGauge();
|
||
btn.prop('disabled', false).html('<i class="fa fa-refresh"></i> <span>' + T().manual_switch_btn + '</span>');
|
||
}
|
||
}).fail(function () {
|
||
toast(T().switch_failed_network, 'error');
|
||
btn.prop('disabled', false).html('<i class="fa fa-refresh"></i> <span>' + T().manual_switch_btn + '</span>');
|
||
});
|
||
}
|
||
|
||
|
||
function loadLocalProxies() {
|
||
$.get(appendToken('/api/proxies'), function (d) {
|
||
var h = [], hs = [], s = [], f = [];
|
||
(d.proxies || []).forEach(function (p) {
|
||
if (p.indexOf('@') >= 0 || (p.indexOf('://') >= 0 && !p.match(/^(http|https|socks5):\/\/[^@]+:\d+$/))) f.push(p);
|
||
else if (p.indexOf('http://') === 0) h.push(p.replace('http://', ''));
|
||
else if (p.indexOf('https://') === 0) hs.push(p.replace('https://', ''));
|
||
else if (p.indexOf('socks5://') === 0) s.push(p.replace('socks5://', ''));
|
||
else h.push(p);
|
||
});
|
||
$('#http-proxy-list').val(h.join('\n')); $('#https-proxy-list').val(hs.join('\n'));
|
||
$('#socks5-proxy-list').val(s.join('\n')); $('#full-proxy-list').val(f.join('\n'));
|
||
});
|
||
}
|
||
|
||
function saveProxies() {
|
||
var b = $('#btn-save-proxy'); btnLoad(b, true);
|
||
var ok = false;
|
||
var a = [];
|
||
$('#http-proxy-list').val().split('\n').forEach(function (l) { if (l.trim()) a.push('http://' + l.trim()); });
|
||
$('#https-proxy-list').val().split('\n').forEach(function (l) { if (l.trim()) a.push('https://' + l.trim()); });
|
||
$('#socks5-proxy-list').val().split('\n').forEach(function (l) { if (l.trim()) a.push('socks5://' + l.trim()); });
|
||
$('#full-proxy-list').val().split('\n').forEach(function (l) { if (l.trim()) a.push(l.trim()); });
|
||
$.ajax({
|
||
url: appendToken('/api/proxies'), method: 'POST', contentType: 'application/json', data: JSON.stringify({ proxies: a }),
|
||
success: function (r) {
|
||
if (r.status === 'success') { ok = true; toast(T().proxy_save_success, 'success'); btnLoad(b, false); flashSaved(b); loadLocalProxies(); }
|
||
else toast(r.message, 'error');
|
||
},
|
||
error: function (x) { toast(T().proxy_save_failed + T().sep_colon + jqErrorText(x), 'error'); },
|
||
complete: function () { if (!ok) flashResult(b, false); }
|
||
});
|
||
}
|
||
|
||
function checkProxies() {
|
||
var b = $('#btn-check-proxy'); btnLoad(b, true);
|
||
var u = $('input[name="test_url"]').val() || 'https://www.baidu.com';
|
||
var ok = false;
|
||
$.get(appendToken('/api/check_proxies'), { test_url: u }, function (d) {
|
||
if (d.status === 'success') {
|
||
ok = true;
|
||
loadLocalProxies();
|
||
toast(fillTemplate(T().proxy_check_result, d.total), 'success');
|
||
} else {
|
||
toast(d.message, 'error');
|
||
}
|
||
}).fail(function (x) {
|
||
toast(T().proxy_check_failed + T().sep_colon + jqErrorText(x), 'error');
|
||
}).always(function () {
|
||
btnLoad(b, false);
|
||
if (!ok) flashResult(b, false);
|
||
});
|
||
}
|
||
|
||
|
||
function loadIpLists() {
|
||
$.get(appendToken('/api/ip_lists'), function (d) {
|
||
$('#whitelist').val((d.whitelist || []).join('\n'));
|
||
$('#blacklist').val((d.blacklist || []).join('\n'));
|
||
});
|
||
}
|
||
|
||
function saveIpLists(btn) {
|
||
var b = $(btn);
|
||
if (b.length) btnLoad(b, true);
|
||
var wl = $('#whitelist').val().split('\n').filter(function (l) { return l.trim(); });
|
||
var bl = $('#blacklist').val().split('\n').filter(function (l) { return l.trim(); });
|
||
$.ajax({
|
||
url: appendToken('/api/config'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ server: { ip_auth_priority: $('select[name="ip_auth_priority"]').val() } }),
|
||
success: function () {
|
||
syncServerBaseline(['ip_auth_priority']);
|
||
var saveWl = $.ajax({ url: appendToken('/api/ip_lists'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ type: 'whitelist', list: wl }) });
|
||
var saveBl = $.ajax({ url: appendToken('/api/ip_lists'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ type: 'blacklist', list: bl }) });
|
||
$.when(saveWl, saveBl).done(function () {
|
||
toast(T().ip_list_save_success, 'success');
|
||
if (b.length) { btnLoad(b, false); flashSaved(b); }
|
||
loadIpLists();
|
||
}).fail(function (x) {
|
||
toast(T().ip_list_save_failed, 'error');
|
||
if (b.length) flashResult(b, false);
|
||
});
|
||
},
|
||
complete: function () { if (b.length) btnLoad(b, false); }
|
||
});
|
||
}
|
||
|
||
|
||
function loadBypassWhitelist() {
|
||
$.get(appendToken('/api/bypass_whitelist'), function(d) {
|
||
$('#bypass-whitelist').val((d.list || []).join('\n'));
|
||
});
|
||
}
|
||
|
||
function saveBypassWhitelist(btn) {
|
||
var b = $(btn);
|
||
if (b.length) btnLoad(b, true);
|
||
var list = $('#bypass-whitelist').val().split('\n').filter(function(l) { return l.trim(); });
|
||
$.ajax({
|
||
url: appendToken('/api/bypass_whitelist'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ list: list }),
|
||
success: function(r) {
|
||
if (r.status === 'success') {
|
||
toast(T().bypass_save_success, 'success');
|
||
if (b.length) { btnLoad(b, false); flashSaved(b); }
|
||
loadBypassWhitelist();
|
||
} else toast(r.message || T().save_failed, 'error');
|
||
},
|
||
error: function() { toast(T().bypass_save_failed || T().save_failed, 'error'); },
|
||
complete: function() { if (b.length) btnLoad(b, false); }
|
||
});
|
||
}
|
||
|
||
|
||
var USER_PASS_MASK = '••••••••';
|
||
|
||
function toggleUserPassword($pass, btn) {
|
||
var masked = $pass.hasClass('masked');
|
||
if (masked) $pass.text($pass.attr('data-pass') || '').removeClass('masked');
|
||
else $pass.text(USER_PASS_MASK).addClass('masked');
|
||
var label = masked ? T().user_hide_password : T().user_show_password;
|
||
$(btn).find('i').attr('class', 'fa ' + (masked ? 'fa-eye-slash' : 'fa-eye'));
|
||
$(btn).attr('title', label).attr('aria-label', label);
|
||
}
|
||
|
||
function loadUsers() {
|
||
onLangRender(loadUsers);
|
||
beginTableLoad('#user-table tbody', 3, 2);
|
||
$.ajax({ url: appendToken('/api/users'), timeout: 15000 })
|
||
.done(function (d) {
|
||
if (d.status === 'error' || !d.users) {
|
||
showTableAlert('#user-table tbody', 3, d.message || T().users_load_failed, 'users');
|
||
return;
|
||
}
|
||
clearTableAlert('#user-table tbody');
|
||
var t = $('#user-table tbody').empty();
|
||
var users = d.users || {}, count = Object.keys(users).length;
|
||
if (count === 0) t.append(emptyRow(3, T().no_users));
|
||
$.each(users, function (u, p) {
|
||
var $row = $('<tr>');
|
||
$('<td>').append($('<span class="user-name">').text(u)).appendTo($row);
|
||
|
||
var $pass = $('<span class="user-pass masked">')
|
||
.attr('data-pass', p).text(USER_PASS_MASK);
|
||
var $passCell = $('<td>').append($pass).appendTo($row);
|
||
|
||
var $actions = $('<td class="user-actions">');
|
||
var $eye = $('<button type="button" class="icon-btn user-eye">')
|
||
.html('<i class="fa fa-eye"></i>')
|
||
.attr('title', T().user_show_password).attr('aria-label', T().user_show_password)
|
||
.on('click', function () { toggleUserPassword($pass, this); });
|
||
$('<button class="btn xs" type="button">').append('<i class="fa fa-edit"></i>')
|
||
.attr('title', T().edit_user_title).attr('aria-label', T().edit_user_title)
|
||
.on('click', function () { editUser(u); }).appendTo($actions);
|
||
$('<button class="btn xs bad" type="button">').append('<i class="fa fa-trash"></i>')
|
||
.attr('title', T().confirm_delete_title).attr('aria-label', T().confirm_delete_title)
|
||
.on('click', function () { deleteUser(u); }).appendTo($actions);
|
||
$actions.appendTo($row);
|
||
$row.appendTo(t);
|
||
$passCell.append($eye);
|
||
});
|
||
if (count > 0) $('#user-count').show().text(count + ' ' + T().user_count_label);
|
||
else $('#user-count').hide();
|
||
updAddr({});
|
||
})
|
||
.fail(function (x) {
|
||
showTableAlert('#user-table tbody', 3,
|
||
jqErrorText(x, T().network_error), 'users');
|
||
});
|
||
}
|
||
|
||
registerTableRetry('users', loadUsers);
|
||
|
||
function addUser() {
|
||
showPrompt(T().add_user_btn, T().enter_username, '', '', function (u) {
|
||
if (!u) return;
|
||
showPrompt(T().add_user_btn, T().enter_password, '', '', function (p) {
|
||
if (!p) return;
|
||
var o = collectUsers();
|
||
o[u] = p; saveUsers(o);
|
||
});
|
||
});
|
||
}
|
||
|
||
function editUser(u) {
|
||
showPrompt(T().edit_user_title, fillTemplate(T().edit_user_msg, u), '', '', function (p) {
|
||
if (!p) return;
|
||
var o = collectUsers();
|
||
if (u in o) o[u] = p;
|
||
saveUsers(o);
|
||
});
|
||
}
|
||
|
||
function deleteUser(u) {
|
||
showConfirm(T().confirm_delete_title, fillTemplate(T().confirm_delete_user, u), function (ok) {
|
||
if (!ok) return;
|
||
var o = collectUsers();
|
||
delete o[u];
|
||
saveUsers(o);
|
||
});
|
||
}
|
||
|
||
function saveUsers(o) {
|
||
$.ajax({
|
||
url: appendToken('/api/users'), method: 'POST', contentType: 'application/json', data: JSON.stringify({ users: o }),
|
||
success: function (r) { if (r.status === 'success') { toast(T().users_save_success, 'success'); loadUsers(); } else toast(r.message, 'error'); },
|
||
error: function (x) { toast(T().users_save_failed + T().sep_colon + jqErrorText(x), 'error'); }
|
||
});
|
||
}
|
||
|
||
|
||
function loadApiCredentials() {
|
||
onLangRender(loadApiCredentials);
|
||
$.get(appendToken('/api/api_credentials'), function (d) {
|
||
var sel = $('#api-credential-select').empty();
|
||
sel.append('<option value="">-- ' + T().api_no_selection + ' --</option>');
|
||
if (d.status === 'success' && d.sets) {
|
||
d.sets.forEach(function (s) {
|
||
sel.append('<option value="' + escapeHtml(s.name) + '">' + escapeHtml(s.name) +
|
||
(s.url ? ' · ' + escapeHtml(s.url) : '') + '</option>');
|
||
});
|
||
if (d.active_credential) sel.val(d.active_credential);
|
||
}
|
||
$('#btn-delete-credential').prop('disabled', !d.active_credential);
|
||
window._apiSets = d.sets || [];
|
||
});
|
||
}
|
||
|
||
function onCredentialSelect() {
|
||
var name = $('#api-credential-select').val();
|
||
$('#btn-delete-credential').prop('disabled', !name);
|
||
if (!name) return;
|
||
var sets = window._apiSets || [];
|
||
var target = sets.find(function (s) { return s.name === name; });
|
||
if (target) {
|
||
$('input[name="api_proxy_url"]').val(target.url || '');
|
||
$('#proxy-auth').val(_joinProxyAuth(target.username, target.password));
|
||
syncProxyAuthHint();
|
||
$.ajax({
|
||
url: appendToken('/api/api_credentials'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ action: 'set_active', name: name }),
|
||
success: function (r) {
|
||
if (r.status === 'success') {
|
||
syncServerBaseline(['api_proxy_url', 'proxy_username', 'proxy_password']);
|
||
updateStatusAndGauge();
|
||
toast(T().api_credential_switched + T().sep_colon + name, 'success');
|
||
} else {
|
||
toast(r.message || fillTemplate(T().operation_failed, name), 'error');
|
||
}
|
||
},
|
||
error: function () {
|
||
toast(fillTemplate(T().operation_failed, name), 'error');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function saveApiCredential() {
|
||
showPrompt(T().api_save_credential, T().api_enter_name, '', T().api_name_placeholder, function (name) {
|
||
if (!name) return;
|
||
$.ajax({
|
||
url: appendToken('/api/api_credentials'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ action: 'save', name: name,
|
||
url: $('input[name="api_proxy_url"]').val(),
|
||
username: _splitProxyAuth($('#proxy-auth').val()).username,
|
||
password: _splitProxyAuth($('#proxy-auth').val()).password
|
||
}),
|
||
success: function (r) { if (r.status === 'success') { toast(T().api_credential_saved, 'success'); loadApiCredentials(); } else toast(r.message, 'error'); },
|
||
error: function (x) { toast(T().request_failed + T().sep_colon + jqErrorText(x, T().network_error), 'error'); }
|
||
});
|
||
});
|
||
}
|
||
|
||
function deleteApiCredential() {
|
||
var name = $('#api-credential-select').val(); if (!name) return;
|
||
showConfirm(T().api_confirm_delete, fillTemplate(T().api_confirm_delete_msg, name), function (ok) {
|
||
if (!ok) return;
|
||
$.ajax({
|
||
url: appendToken('/api/api_credentials'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ action: 'delete', name: name }),
|
||
success: function (r) { if (r.status === 'success') { toast(T().api_credential_deleted, 'success'); loadApiCredentials(); } else toast(r.message, 'error'); },
|
||
error: function (x) { toast(T().request_failed + T().sep_colon + jqErrorText(x, T().network_error), 'error'); }
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
function controlPool(a) {
|
||
var b = $('#pool-' + a); btnLoad(b, true);
|
||
$.ajax({
|
||
url: appendToken('/api/pool/' + a), method: 'POST',
|
||
success: function (r) { if (r.status === 'success') toast(r.message, 'success'); else toast(r.message, 'error'); setTimeout(updatePoolStatus, 1500); },
|
||
error: function (x) { toast(fillTemplate(T().operation_failed, x.responseText), 'error'); },
|
||
complete: function () { btnLoad(b, false); }
|
||
});
|
||
}
|
||
|
||
|
||
var _logFollow = true;
|
||
var _logAuto = true;
|
||
var _logCategory = 'all';
|
||
var _logFile = '';
|
||
var _logFileCache = null;
|
||
var _logHasRendered = false;
|
||
|
||
var CATEGORY_LABELS = {
|
||
all: function () { return T().log_cat_all; },
|
||
main: function () { return T().log_cat_main; },
|
||
proxy: function () { return T().log_cat_proxy; },
|
||
access: function () { return T().log_cat_access; },
|
||
pool: function () { return T().log_cat_pool; },
|
||
error: function () { return T().log_cat_error; }
|
||
};
|
||
|
||
function setLogCategory(cat) {
|
||
_logCategory = cat;
|
||
$('#log-category').val(cat);
|
||
$('#log-cat-pills .log-cat-pill').removeClass('cur');
|
||
$('#log-cat-pills .log-cat-pill[data-cat="' + cat + '"]').addClass('cur');
|
||
loadLogFiles();
|
||
_updateAccessPanelVisibility();
|
||
updateLogs(true);
|
||
}
|
||
|
||
function setLogLevel(lv) {
|
||
$('#log-level').val(lv);
|
||
$('#log-level-pills .log-lv-pill').removeClass('cur');
|
||
$('#log-level-pills .log-lv-pill[data-lv="' + lv + '"]').addClass('cur');
|
||
updateLogs(true);
|
||
}
|
||
|
||
function toggleLogFollow() {
|
||
_logFollow = !_logFollow;
|
||
$('#log-follow-btn').toggleClass('on', _logFollow);
|
||
if (_logFollow) updateLogs(true);
|
||
}
|
||
|
||
function toggleLogAuto() {
|
||
_logAuto = !_logAuto;
|
||
$('#log-auto-btn').toggleClass('on', _logAuto);
|
||
if (_logAuto) updateLogs(true);
|
||
}
|
||
|
||
function onLogSourceChange() {
|
||
_logFile = $('#log-file-select').val() || '';
|
||
_logFileCache = null;
|
||
$('#log-container').toggleClass('showing-file', !!_logFile);
|
||
_updateAccessPanelVisibility();
|
||
updateLogs(true);
|
||
}
|
||
|
||
function loadLogFiles() {
|
||
onLangRender(loadLogFiles);
|
||
$.get(appendToken('/api/logs/files'), function (d) {
|
||
if (d.status !== 'success') return;
|
||
var sel = $('#log-file-select'), keep = _logFile;
|
||
sel.empty().append($('<option>').val('').text(T().log_source_live));
|
||
(d.files || []).forEach(function (f) {
|
||
var cat = CATEGORY_LABELS[f.category];
|
||
var label = (cat ? cat() : f.category) + ' · ' + f.name + ' · ' + fmtLogSize(f.size);
|
||
sel.append($('<option>').val(f.name).text(label));
|
||
});
|
||
if (keep && sel.find('option[value="' + keep + '"]').length) {
|
||
sel.val(keep);
|
||
} else if (_logFile) {
|
||
_logFile = '';
|
||
_logFileCache = null;
|
||
sel.val('');
|
||
$('#log-container').removeClass('showing-file');
|
||
_updateAccessPanelVisibility();
|
||
updateLogs(true);
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
function renderLogMessage(msg, kw) {
|
||
var text = (msg === null || msg === undefined) ? '' : String(msg);
|
||
if (!kw) return escapeHtml(text);
|
||
var key = String(kw).toLowerCase();
|
||
if (!key) return escapeHtml(text);
|
||
var out = '', i = 0, low = text.toLowerCase();
|
||
for (;;) {
|
||
var at = low.indexOf(key, i);
|
||
if (at < 0) { out += escapeHtml(text.slice(i)); break; }
|
||
out += escapeHtml(text.slice(i, at)) +
|
||
'<span class="log-hit">' + escapeHtml(text.slice(at, at + key.length)) + '</span>';
|
||
i = at + key.length;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function _applyLevelFilter(entries) {
|
||
var lv = $('#log-level').val();
|
||
if (!lv || lv === 'ALL') return entries;
|
||
if (lv === 'IMPORTANT') {
|
||
return entries.filter(function (e) {
|
||
return e.level === 'WARNING' || e.level === 'ERROR' || e.level === 'CRITICAL';
|
||
});
|
||
}
|
||
return entries.filter(function (e) { return e.level === lv; });
|
||
}
|
||
|
||
function parseLogLine(line) {
|
||
var m = String(line).match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) - ([A-Z]+) - ([\s\S]*)$/);
|
||
if (m) return { time: m[1], level: m[2], message: m[3] };
|
||
return { time: '', level: 'INFO', message: line };
|
||
}
|
||
|
||
function _renderLogEntries(entries, kw) {
|
||
var c = $('#log-container');
|
||
c.empty();
|
||
if (!entries.length) {
|
||
c.addClass('is-empty').append('<div class="empty-hint">' + escapeHtml(T().no_logs) + '</div>');
|
||
return;
|
||
}
|
||
c.removeClass('is-empty');
|
||
|
||
var prevKey = null, prevEl = null;
|
||
entries.forEach(function (l) {
|
||
var lvName = l.level || 'INFO';
|
||
var key = lvName + '|' + l.message;
|
||
if (prevEl && key === prevKey) {
|
||
var n = (prevEl.data('repeat') || 1) + 1;
|
||
prevEl.data('repeat', n).addClass('folded');
|
||
prevEl.find('.log-repeat').text('×' + n);
|
||
return;
|
||
}
|
||
var cat = CATEGORY_LABELS[l.category];
|
||
prevKey = key;
|
||
prevEl = $('<div class="log-line log-' + lvName + '">' +
|
||
(l.time ? '<span class="log-time">' + escapeHtml(l.time) + '</span>' : '') +
|
||
'<span class="log-level-badge ' + lvName + '">' + escapeHtml(lvName) + '</span>' +
|
||
(l.category ? '<span class="log-cat log-cat-' + escapeHtml(l.category) + '">' +
|
||
escapeHtml(cat ? cat() : l.category) + '</span>' : '') +
|
||
'<span class="log-repeat"></span>' +
|
||
'<span class="log-msg">' + renderLogMessage(l.message, kw) + '</span>' +
|
||
'<button type="button" class="log-copy" title="' + escapeHtml(T().copy_line) +
|
||
'" aria-label="' + escapeHtml(T().copy_line) + '"><i class="fa fa-copy"></i></button>' +
|
||
'</div>');
|
||
c.append(prevEl);
|
||
});
|
||
}
|
||
|
||
function updateLogs(force) {
|
||
onLangRender(updateLogs);
|
||
if (_logFile && !force) return;
|
||
if (!_logAuto && !force && _logHasRendered) return;
|
||
|
||
var kw = $('#log-search').val() || '';
|
||
var c = $('#log-container');
|
||
var wasAtBottom = c.scrollTop() + c.innerHeight() >= c[0].scrollHeight - 4;
|
||
var prevScroll = c.scrollTop();
|
||
|
||
function done(entries) {
|
||
_renderLogEntries(entries, kw);
|
||
_logHasRendered = true;
|
||
if (_logFollow && wasAtBottom) c.scrollTop(c[0].scrollHeight);
|
||
else c.scrollTop(prevScroll);
|
||
}
|
||
|
||
if (_logFile) {
|
||
var cacheFresh = _logFileCache && _logFileCache.name === _logFile &&
|
||
(Date.now() - _logFileCache.at) < 1000;
|
||
if (cacheFresh) {
|
||
done(_applyLevelFilter(_logFileCache.entries));
|
||
updateLogStats();
|
||
return;
|
||
}
|
||
$.get(appendToken('/api/logs/file?name=' + encodeURIComponent(_logFile) + '&lines=2000'), function (d) {
|
||
if (d.status !== 'success') { toast(d.message || T().log_file_read_failed, 'error'); return; }
|
||
_logFileCache = { name: _logFile, entries: (d.lines || []).map(parseLogLine), at: Date.now() };
|
||
done(_applyLevelFilter(_logFileCache.entries));
|
||
});
|
||
} else {
|
||
$.get(appendToken('/api/logs?category=' + encodeURIComponent(_logCategory) +
|
||
'&level=' + $('#log-level').val() +
|
||
'&search=' + encodeURIComponent(kw)), function (d) {
|
||
if (d.status !== 'success') return;
|
||
done(d.logs || []);
|
||
});
|
||
}
|
||
updateLogStats();
|
||
}
|
||
|
||
function updateLogStats() {
|
||
onLangRender(updateLogStats);
|
||
$.get(appendToken('/api/logs/stats'), function (d) {
|
||
if (d.status !== 'success') return;
|
||
var s = d.stats, lvs = s.levels || {}, cats = s.categories || {}, a = [];
|
||
a.push('<span class="log-stat-item total"><b>' + escapeHtml(T().log_total) + '</b> ' + s.total + '</span>');
|
||
['INFO', 'WARNING', 'ERROR', 'CRITICAL'].forEach(function (k) {
|
||
if (lvs[k] !== undefined) a.push('<span class="log-stat-item ' + k + '">' + k + ' ' + lvs[k] + '</span>');
|
||
});
|
||
Object.keys(CATEGORY_LABELS).forEach(function (k) {
|
||
if (k === 'all' || cats[k] === undefined) return;
|
||
var label = CATEGORY_LABELS[k];
|
||
a.push('<span class="log-stat-item log-stat-cat">' + escapeHtml(label ? label() : k) + ' ' + cats[k] + '</span>');
|
||
});
|
||
$('#log-stats').html(a.join(''));
|
||
_paintCategoryCounts(s);
|
||
});
|
||
}
|
||
|
||
function _paintCategoryCounts(s) {
|
||
var cats = (s && s.categories) || {};
|
||
$('#log-cat-pills .log-cat-pill').each(function () {
|
||
var cat = $(this).attr('data-cat');
|
||
var n = cat === 'all' ? s.total : cats[cat];
|
||
var $count = $(this).find('.pill-count');
|
||
if (n === undefined || n === null) { $count.remove(); return; }
|
||
if (!$count.length) $count = $('<span class="pill-count"></span>').appendTo(this);
|
||
$count.text(n);
|
||
});
|
||
}
|
||
|
||
$(document).on('click', '.log-copy', function () {
|
||
var $line = $(this).closest('.log-line');
|
||
var text = [
|
||
$line.find('.log-time').text(),
|
||
$line.find('.log-level-badge').text(),
|
||
$line.find('.log-msg').text()
|
||
].filter(function (v) { return v; }).join(' ');
|
||
copyToClipboard(text, function () {
|
||
toast(T().log_line_copied, 'success');
|
||
});
|
||
});
|
||
|
||
function exportLogs() {
|
||
var url = '/api/logs/export?';
|
||
if (_logFile) url += 'file=' + encodeURIComponent(_logFile);
|
||
else url += 'category=' + encodeURIComponent(_logCategory) +
|
||
'&level=' + $('#log-level').val() +
|
||
'&search=' + encodeURIComponent($('#log-search').val() || '');
|
||
window.open(appendToken(url), '_blank');
|
||
toast(T().log_export_started, 'success');
|
||
}
|
||
|
||
function onLogSearch() {
|
||
var v = $('#log-search').val(); $('#log-clear-btn').toggle(!!v);
|
||
clearTimeout(logTimer); logTimer = setTimeout(function () { updateLogs(true); }, 300);
|
||
}
|
||
|
||
function clearLogSearch() {
|
||
$('#log-search').val('');
|
||
$('#log-clear-btn').hide();
|
||
updateLogs(true);
|
||
}
|
||
|
||
function clearLogs() {
|
||
var label = CATEGORY_LABELS[_logCategory];
|
||
var scope = label ? label() : _logCategory;
|
||
showConfirm(T().confirm_clear_title,
|
||
T().confirm_clear_logs + T().sep_paren_open + scope + T().sep_paren_close, function (ok) {
|
||
if (!ok) return;
|
||
$.ajax({
|
||
url: appendToken('/api/logs/clear'), method: 'POST', contentType: 'application/json',
|
||
data: JSON.stringify({ category: _logCategory }),
|
||
success: function (r) {
|
||
if (r.status !== 'success') { toast(r.message || T().clear_logs_failed, 'error'); return; }
|
||
toast(T().logs_cleared, 'success');
|
||
$('#log-container').empty(); $('#log-stats').empty();
|
||
loadLogFiles();
|
||
},
|
||
error: function () { toast(T().clear_logs_failed, 'error'); }
|
||
});
|
||
}, { danger: true });
|
||
}
|
||
|
||
var _domainSearchTimer = null;
|
||
var _domainSort = 'last_seen';
|
||
var _domainOrder = 'desc';
|
||
var _upstreamRows = [];
|
||
var _domainPayload = null;
|
||
var _expandedUpstreams = {};
|
||
|
||
var _accessMode = 'summary';
|
||
var _recordsPage = 1;
|
||
var _recordsPageSize = 100;
|
||
var _recordsTotal = 0;
|
||
var _recordsAuto = true;
|
||
var _recordsSearchTimer = null;
|
||
var _recordsPayload = null;
|
||
var _recordsOptionsLoaded = false;
|
||
|
||
function _startDomainRefresh() {
|
||
if (isPolling('domain')) return;
|
||
_updateAccessPanelVisibility();
|
||
_refreshAccessPanel();
|
||
startPolling('domain', _refreshAccessPanel, 5000);
|
||
}
|
||
|
||
function _refreshAccessPanel() {
|
||
if (_accessMode === 'records') {
|
||
if (_recordsAuto) loadAccessRecords();
|
||
} else {
|
||
loadDomainStats();
|
||
}
|
||
}
|
||
|
||
function _stopDomainRefresh() {
|
||
stopPolling('domain');
|
||
}
|
||
|
||
function _updateAccessPanelVisibility() {
|
||
$('#log-cat-pills').toggle(!_logFile);
|
||
$('#log-clear-btn-wrap').toggle(!_logFile);
|
||
|
||
var show = _logCategory === 'access' && !_logFile;
|
||
var wasHidden = !$('#access-stats-panel').is(':visible');
|
||
$('#access-stats-panel').toggle(show);
|
||
$('#access-hint').toggle(!show && !_logFile);
|
||
if (show && wasHidden) {
|
||
if (_accessMode === 'records') _loadRecordOptions(function () { loadAccessRecords(); });
|
||
else loadDomainStats();
|
||
}
|
||
}
|
||
|
||
function showAccessStats() {
|
||
_logFile = '';
|
||
$('#log-file-select').val('');
|
||
$('#log-container').removeClass('showing-file');
|
||
setLogCategory('access');
|
||
}
|
||
|
||
|
||
function setAccessMode(mode) {
|
||
_accessMode = mode === 'records' ? 'records' : 'summary';
|
||
$('#access-mode-pills .log-lv-pill').removeClass('cur');
|
||
$('#access-mode-pills .log-lv-pill[data-access-mode="' + _accessMode + '"]').addClass('cur');
|
||
$('#access-summary-view').toggle(_accessMode === 'summary');
|
||
$('#access-records-view').toggle(_accessMode === 'records');
|
||
|
||
if (_accessMode === 'records') _loadRecordOptions(function () { loadAccessRecords(); });
|
||
else loadDomainStats();
|
||
}
|
||
|
||
function _loadRecordOptions(onReady) {
|
||
var done = onReady || function () {};
|
||
if (_recordsOptionsLoaded) { done(); return; }
|
||
|
||
$.ajax({
|
||
url: appendToken('/api/logs/records/options'),
|
||
success: function (d) {
|
||
if (d.status !== 'success') { done(); return; }
|
||
var html = '<option value="">' + escapeHtml(T().records_upstream_all) + '</option>';
|
||
(d.upstreams || []).forEach(function (item) {
|
||
html += '<option value="' + escapeHtml(item.value) + '">' +
|
||
escapeHtml(item.label || item.value) + ' (' + item.count + ')</option>';
|
||
});
|
||
$('#records-upstream').html(html);
|
||
_recordsOptionsLoaded = true;
|
||
done();
|
||
},
|
||
error: done
|
||
});
|
||
}
|
||
|
||
function _recordRangeParams() {
|
||
var since = $('#records-since').val() || '';
|
||
var until = $('#records-until').val() || '';
|
||
return '&since=' + encodeURIComponent(since) + '&until=' + encodeURIComponent(until);
|
||
}
|
||
|
||
function _recordsFilterParams() {
|
||
return '&outcome=' + encodeURIComponent($('#records-outcome').val() || '') +
|
||
'&upstream=' + encodeURIComponent($('#records-upstream').val() || '') +
|
||
'&search=' + encodeURIComponent($('#records-search').val() || '') +
|
||
_recordRangeParams();
|
||
}
|
||
|
||
function _toLocalInputValue(date) {
|
||
function pad(n) { return (n < 10 ? '0' : '') + n; }
|
||
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) +
|
||
'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||
}
|
||
|
||
function setRecordsRange(preset) {
|
||
var now = new Date();
|
||
var since;
|
||
if (preset === 'today') {
|
||
since = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||
} else {
|
||
var minutes = { '5m': 5, '1h': 60, '24h': 1440 }[preset] || 60;
|
||
since = new Date(now.getTime() - minutes * 60000);
|
||
}
|
||
|
||
$('#records-since').val(_toLocalInputValue(since));
|
||
$('#records-until').val('');
|
||
$('#access-records-view .log-lv-pill').removeClass('cur');
|
||
$('#access-records-view .log-lv-pill[data-records-range="' + preset + '"]').addClass('cur');
|
||
_recordsPage = 1;
|
||
loadAccessRecords();
|
||
}
|
||
|
||
function onRecordsRangeChange() {
|
||
$('#access-records-view .log-lv-pill').removeClass('cur');
|
||
_recordsPage = 1;
|
||
loadAccessRecords();
|
||
}
|
||
|
||
function onRecordsFilterChange() {
|
||
_recordsPage = 1;
|
||
loadAccessRecords();
|
||
}
|
||
|
||
function onRecordsSearch() {
|
||
clearTimeout(_recordsSearchTimer);
|
||
_recordsSearchTimer = setTimeout(function () {
|
||
_recordsPage = 1;
|
||
loadAccessRecords();
|
||
}, 300);
|
||
}
|
||
|
||
function toggleRecordsAuto() {
|
||
_recordsAuto = !_recordsAuto;
|
||
$('#records-auto-btn').toggleClass('on', _recordsAuto);
|
||
if (_recordsAuto) loadAccessRecords();
|
||
}
|
||
|
||
function _pauseRecordsAutoOnPaging() {
|
||
if (_recordsPage > 1 && _recordsAuto) {
|
||
_recordsAuto = false;
|
||
$('#records-auto-btn').removeClass('on');
|
||
}
|
||
}
|
||
|
||
function recordsPrevPage() {
|
||
if (_recordsPage <= 1) return;
|
||
_recordsPage -= 1;
|
||
_pauseRecordsAutoOnPaging();
|
||
loadAccessRecords();
|
||
}
|
||
|
||
function recordsNextPage() {
|
||
if (_recordsPage * _recordsPageSize >= _recordsTotal) return;
|
||
_recordsPage += 1;
|
||
_pauseRecordsAutoOnPaging();
|
||
loadAccessRecords();
|
||
}
|
||
|
||
function loadAccessRecords() {
|
||
onLangRender(loadAccessRecords);
|
||
if (_accessMode !== 'records') return;
|
||
if (!$('#access-stats-panel').is(':visible')) return;
|
||
|
||
var q = '/api/logs/records?limit=' + _recordsPageSize +
|
||
'&offset=' + ((_recordsPage - 1) * _recordsPageSize) +
|
||
_recordsFilterParams();
|
||
beginTableLoad('#records-tbody', 9, 6);
|
||
$.ajax({
|
||
url: appendToken(q),
|
||
timeout: 15000,
|
||
success: function (d) {
|
||
if (d.status !== 'success') { toast(d.message || '', 'error'); return; }
|
||
clearTableAlert('#records-tbody');
|
||
_recordsPayload = d;
|
||
_recordsTotal = d.total || 0;
|
||
_renderRecordTable();
|
||
},
|
||
error: function (x) {
|
||
showTableAlert('#records-tbody', 9,
|
||
jqErrorText(x, T().network_error) + T().sep_mid + T().stale_data_note, 'records');
|
||
}
|
||
});
|
||
}
|
||
|
||
registerTableRetry('records', loadAccessRecords);
|
||
|
||
function _renderRecordTable() {
|
||
var payload = _recordsPayload || {};
|
||
var tbody = $('#records-tbody');
|
||
tbody.empty();
|
||
|
||
if (payload.enabled === false) {
|
||
tbody.append(emptyRow(9, T().records_disabled));
|
||
} else if (!(payload.records || []).length) {
|
||
tbody.append(emptyRow(9, T().records_no_data));
|
||
} else {
|
||
payload.records.forEach(function (record) { tbody.append(_recordRow(record)); });
|
||
}
|
||
|
||
var counts = payload.outcome_counts || {};
|
||
var parts = [
|
||
T().records_summary_range + ' ' + (payload.total || 0) + ' ' + T().records_summary_unit,
|
||
T().access_outcome_success + ' ' + (counts.success || 0),
|
||
T().access_outcome_failure + ' ' + (counts.failure || 0),
|
||
T().access_outcome_aborted + ' ' + (counts.aborted || 0)
|
||
];
|
||
if (payload.pending) {
|
||
parts.push(fillTemplate(T().records_summary_pending, payload.pending));
|
||
}
|
||
if (payload.dropped) {
|
||
parts.push(fillTemplate(T().records_summary_dropped, payload.dropped));
|
||
}
|
||
$('#records-summary').text(parts.join(' · '));
|
||
|
||
var pages = Math.max(1, Math.ceil(_recordsTotal / _recordsPageSize));
|
||
$('#records-page-info').text(fillTemplate(
|
||
T().records_page_info, _recordsPage, pages));
|
||
$('#records-prev-btn').prop('disabled', _recordsPage <= 1);
|
||
$('#records-next-btn').prop('disabled', _recordsPage >= pages);
|
||
}
|
||
|
||
function _recordRow(record) {
|
||
var outcome = record.outcome || '';
|
||
return $('<tr data-id="' + escapeHtml(record.id) + '">' +
|
||
'<td class="cell-mono">' + escapeHtml(record.ts || '') + '</td>' +
|
||
'<td class="cell-mono">' + escapeHtml(record.client || '') + '</td>' +
|
||
'<td class="cell-mono">' + escapeHtml(record.target || '') + '</td>' +
|
||
'<td class="cell-mono">' + escapeHtml(record.upstream_label || record.upstream || '') + '</td>' +
|
||
'<td class="cell-mono">' + escapeHtml(record.real_ip || '--') + '</td>' +
|
||
'<td><span class="record-outcome ' + escapeHtml(outcome) + '">' +
|
||
escapeHtml(_outcomeLabel(outcome)) + '</span></td>' +
|
||
'<td class="cell-num">' + _statusCell(record.status_code) + '</td>' +
|
||
'<td class="cell-num">' + (record.elapsed_ms || 0) + ' ms</td>' +
|
||
'<td class="domain-reason" title="' + escapeHtml(record.reason || '') + '">' +
|
||
escapeHtml(record.reason || '--') + '</td></tr>');
|
||
}
|
||
|
||
function _statusCell(code) {
|
||
if (code === null || code === undefined) return '<span class="status-code">--</span>';
|
||
var cls = 'status-code';
|
||
if (code >= 200 && code < 300) cls += ' ok';
|
||
else if (code >= 300 && code < 400) cls += ' redirect';
|
||
else if (code >= 400 && code < 500) cls += ' client';
|
||
else if (code >= 500) cls += ' server';
|
||
return '<span class="' + cls + '">' + escapeHtml(code) + '</span>';
|
||
}
|
||
|
||
function _outcomeLabel(outcome) {
|
||
if (outcome === 'success') return T().access_outcome_success;
|
||
if (outcome === 'failure') return T().access_outcome_failure;
|
||
if (outcome === 'aborted') return T().access_outcome_aborted;
|
||
return outcome || '--';
|
||
}
|
||
|
||
function openRecordDrawer(id) {
|
||
var list = (_recordsPayload && _recordsPayload.records) || [];
|
||
var r = null;
|
||
for (var i = 0; i < list.length; i++) {
|
||
if (String(list[i].id) === String(id)) { r = list[i]; break; }
|
||
}
|
||
if (!r) return;
|
||
|
||
openRowDrawer({
|
||
title: r.target || '--',
|
||
subtitle: (r.ts || '') + ' · ' + _outcomeLabel(r.outcome),
|
||
fields: drawerSections([
|
||
[T().drawer_group_request, [
|
||
{ label: T().records_col_time, value: r.ts, mono: true },
|
||
{ label: T().records_col_client, value: r.client, mono: true },
|
||
{ label: T().records_col_target, value: r.target, mono: true, copy: true },
|
||
{ label: T().records_method, value: r.method || '' },
|
||
{ label: T().records_kind, value: r.kind || '' }
|
||
]],
|
||
[T().drawer_group_upstream, [
|
||
{ label: T().records_col_upstream, value: r.upstream_label || r.upstream, mono: true, copy: true },
|
||
{ label: T().records_col_real_ip, value: r.real_ip, mono: true }
|
||
]],
|
||
[T().drawer_group_result, [
|
||
{ label: T().records_col_outcome, value: _outcomeLabel(r.outcome) },
|
||
{ label: T().records_col_status, value: (r.status_code === null || r.status_code === undefined) ? '' : String(r.status_code), mono: true },
|
||
{ label: T().records_col_elapsed, value: (r.elapsed_ms || 0) + ' ms', mono: true },
|
||
{ label: T().records_col_reason, value: r.reason || '' }
|
||
]]
|
||
])
|
||
});
|
||
}
|
||
|
||
function initAccessRecordsTable() {
|
||
var table = $('#records-table');
|
||
if (!table.length) return;
|
||
table.on('click', 'tbody tr[data-id]', function () {
|
||
openRecordDrawer($(this).attr('data-id'));
|
||
});
|
||
attachTableKeyboard(table, {
|
||
onActivate: function (i) {
|
||
var row = table.find('tbody tr[data-id]').eq(i);
|
||
if (row.length) openRecordDrawer(row.attr('data-id'));
|
||
}
|
||
});
|
||
}
|
||
|
||
function exportAccessRecords() {
|
||
var url = '/api/logs/records/export?format=' + encodeURIComponent($('#records-format').val() || 'csv') +
|
||
_recordsFilterParams();
|
||
var limit = (_recordsPayload && _recordsPayload.export_limit) || 0;
|
||
|
||
if (limit && _recordsTotal > limit) {
|
||
showConfirm(
|
||
T().records_export_btn,
|
||
fillTemplate(T().records_export_truncated,
|
||
_recordsTotal, limit),
|
||
function (ok) {
|
||
if (!ok) return;
|
||
window.open(appendToken(url));
|
||
toast(T().log_export_started, 'success');
|
||
}
|
||
);
|
||
return;
|
||
}
|
||
window.open(appendToken(url));
|
||
toast(T().log_export_started, 'success');
|
||
}
|
||
|
||
function clearAccessRecords() {
|
||
showConfirm(T().confirm_clear_title,
|
||
T().confirm_clear_records,
|
||
function (ok) {
|
||
if (!ok) return;
|
||
$.ajax({
|
||
url: appendToken('/api/logs/records/clear'), method: 'POST',
|
||
success: function (r) {
|
||
if (r.status !== 'success') { toast(r.message || '', 'error'); return; }
|
||
toast(r.message || '', 'success');
|
||
_recordsPage = 1;
|
||
_recordsOptionsLoaded = false;
|
||
_loadRecordOptions(function () { loadAccessRecords(); });
|
||
},
|
||
error: function () { toast(T().clear_logs_failed, 'error'); }
|
||
});
|
||
}, { danger: true });
|
||
}
|
||
|
||
function sortDomainStats(field) {
|
||
if (_domainSort === field) _domainOrder = _domainOrder === 'desc' ? 'asc' : 'desc';
|
||
else { _domainSort = field; _domainOrder = 'desc'; }
|
||
loadDomainStats();
|
||
}
|
||
|
||
function onDomainSearch() {
|
||
clearTimeout(_domainSearchTimer);
|
||
_domainSearchTimer = setTimeout(loadDomainStats, 300);
|
||
}
|
||
|
||
function loadDomainStats() {
|
||
onLangRender(loadDomainStats);
|
||
if (!$('#access-stats-panel').is(':visible')) return;
|
||
var q = '/api/logs/domains?sort=' + _domainSort + '&order=' + _domainOrder +
|
||
'&limit=200&search=' + encodeURIComponent($('#domain-search').val() || '');
|
||
beginTableLoad('#domain-tbody', 7, 6);
|
||
$.ajax({ url: appendToken(q), timeout: 15000 })
|
||
.done(function (d) {
|
||
if (d.status !== 'success') return;
|
||
clearTableAlert('#domain-tbody');
|
||
_upstreamRows = d.proxies || [];
|
||
_domainPayload = d;
|
||
_renderDomainTable();
|
||
})
|
||
.fail(function (x) {
|
||
showTableAlert('#domain-tbody', 7,
|
||
jqErrorText(x, T().network_error) + T().sep_mid + T().stale_data_note, 'domains');
|
||
});
|
||
}
|
||
|
||
registerTableRetry('domains', loadDomainStats);
|
||
|
||
function _rateCell(row) {
|
||
var total = row.total || 0;
|
||
var rate = total ? Math.round((row.success / total) * 100) : 0;
|
||
var cls = total && row.failure ? (rate < 50 ? 'domain-rate bad' : 'domain-rate warn') : 'domain-rate';
|
||
return '<span class="' + cls + '">' + rate + '%</span>';
|
||
}
|
||
|
||
function _renderDomainTable() {
|
||
var payload = _domainPayload || {};
|
||
var tbody = $('#domain-tbody');
|
||
tbody.empty();
|
||
|
||
if (payload.enabled === false) {
|
||
tbody.append(emptyRow(7, T().domain_disabled));
|
||
} else if (!_upstreamRows.length) {
|
||
tbody.append(emptyRow(7, T().domain_no_data));
|
||
} else {
|
||
_upstreamRows.forEach(function (row) {
|
||
tbody.append(_upstreamRow(row));
|
||
var detail = _expandedUpstreams[row.upstream];
|
||
if (detail === undefined) return;
|
||
if (detail === 'loading') {
|
||
tbody.append(emptyRow(7, T().loading, 'domain-detail'));
|
||
return;
|
||
}
|
||
detail.forEach(function (d) { tbody.append(_domainDetailRow(d)); });
|
||
});
|
||
}
|
||
|
||
var s = payload.summary || {};
|
||
$('#domain-summary').text(
|
||
T().access_summary + ' ' + (payload.total || 0) +
|
||
' · ' + T().access_summary_hosts + ' ' + (s.hosts || 0) +
|
||
' · ' + T().domain_summary_ok + ' ' + (s.success || 0) +
|
||
' / ' + T().domain_summary_fail + ' ' + (s.failure || 0));
|
||
}
|
||
|
||
function _upstreamRow(row) {
|
||
var expanded = _expandedUpstreams[row.upstream] !== undefined;
|
||
var label = row.upstream_label || row.upstream;
|
||
return $('<tr class="domain-row' + (expanded ? ' expanded' : '') + '">' +
|
||
'<td class="cell-mono domain-toggle"><i class="fa fa-caret-' + (expanded ? 'down' : 'right') +
|
||
'"></i> <span class="domain-name">' + escapeHtml(label) + '</span>' +
|
||
'<span class="domain-count" title="' + escapeHtml(T().access_col_hosts) + '">' +
|
||
row.host_count + '</span></td>' +
|
||
'<td class="domain-ok cell-num">' + row.success + '</td>' +
|
||
'<td class="domain-fail cell-num">' + row.failure + '</td>' +
|
||
'<td class="cell-num">' + row.total + '</td>' +
|
||
'<td>' + _rateCell(row) + '</td>' +
|
||
'<td class="cell-mono">' + escapeHtml(row.last_seen || '--') + '</td>' +
|
||
'<td class="domain-reason" title="' + escapeHtml(row.last_error || '') + '">' +
|
||
escapeHtml(row.last_error || '--') + '</td></tr>')
|
||
.data('upstream', row.upstream);
|
||
}
|
||
|
||
function _domainDetailRow(d) {
|
||
return $('<tr class="domain-detail">' +
|
||
'<td class="cell-mono domain-name">' + escapeHtml(d.host) + '</td>' +
|
||
'<td class="domain-ok cell-num">' + d.success + '</td>' +
|
||
'<td class="domain-fail cell-num">' + d.failure + '</td>' +
|
||
'<td>' + d.total + '</td>' +
|
||
'<td>' + _rateCell(d) + '</td>' +
|
||
'<td class="cell-mono">' + escapeHtml(d.last_seen || '--') + '</td>' +
|
||
'<td class="domain-reason" title="' + escapeHtml(d.last_error || '') + '">' +
|
||
escapeHtml(d.last_error || '--') + '</td></tr>');
|
||
}
|
||
|
||
function toggleUpstreamRow(upstream) {
|
||
if (_expandedUpstreams[upstream] !== undefined) {
|
||
delete _expandedUpstreams[upstream];
|
||
_renderDomainTable();
|
||
return;
|
||
}
|
||
_expandedUpstreams[upstream] = 'loading';
|
||
_renderDomainTable();
|
||
$.ajax({
|
||
url: appendToken('/api/logs/domains?limit=200&upstream=' + encodeURIComponent(upstream)),
|
||
success: function (d) {
|
||
_expandedUpstreams[upstream] = (d.status === 'success' && d.domains) ? d.domains : [];
|
||
_renderDomainTable();
|
||
},
|
||
error: function () {
|
||
_expandedUpstreams[upstream] = [];
|
||
_renderDomainTable();
|
||
}
|
||
});
|
||
}
|
||
|
||
$(document).on('click', '.domain-row', function () {
|
||
toggleUpstreamRow($(this).data('upstream'));
|
||
});
|
||
|
||
function clearDomainStats() {
|
||
showConfirm(T().confirm_clear_title, T().confirm_clear_domains, function (ok) {
|
||
if (!ok) return;
|
||
$.ajax({
|
||
url: appendToken('/api/logs/domains/clear'), method: 'POST',
|
||
success: function (r) {
|
||
if (r.status !== 'success') { toast(r.message || '', 'error'); return; }
|
||
toast(r.message || T().domain_cleared, 'success');
|
||
_expandedUpstreams = {};
|
||
loadDomainStats();
|
||
},
|
||
error: function () { toast(T().clear_logs_failed, 'error'); }
|
||
});
|
||
}, { danger: true });
|
||
}
|
||
|
||
|
||
function checkVer() {
|
||
onLangRender(checkVer);
|
||
$.get(appendToken('/api/version'), function (d) {
|
||
if (d.status === 'success') {
|
||
$('#hdr-ver').text(d.current_version || 'ProxyCat');
|
||
if (d.is_latest) $('#ver-stat').html('<span class="ver-ok"> ' + T().latest_version + '</span>');
|
||
else $('#ver-stat').html('<span class="ver-new"> ' + T().new_version_available + ': ' + (d.latest_version || '') + '</span>');
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
var _ads = [];
|
||
var _adIndex = 0;
|
||
var _adCountdown = null;
|
||
var _adDismissed = false;
|
||
|
||
function loadAds() {
|
||
onLangRender(loadAds);
|
||
$.get(appendToken('/api/ads'), function (d) {
|
||
if (d.status !== 'success' || !d.ads || !d.ads.length) return;
|
||
_ads = d.ads;
|
||
_adIndex = 0;
|
||
_adDismissed = !!d.dismissed;
|
||
if (_adDismissed) {
|
||
showReopenBtn();
|
||
} else {
|
||
showAdBar();
|
||
renderAdPair(0);
|
||
startAdRotation();
|
||
}
|
||
}).fail(function () {
|
||
});
|
||
}
|
||
|
||
function renderAdCard(slot, ad) {
|
||
var s = String(slot);
|
||
if (!ad) {
|
||
$('#ad-card-' + s).hide();
|
||
return;
|
||
}
|
||
$('#ad-card-' + s).show();
|
||
$('#ad-title-' + s).text(ad.title || '');
|
||
$('#ad-body-' + s).text(ad.body || '');
|
||
|
||
if (ad.image_url) {
|
||
$('#ad-img-' + s).attr('src', ad.image_url).show()
|
||
.off('click').on('click', function (e) {
|
||
e.stopPropagation();
|
||
openImgLightbox(ad.image_url);
|
||
});
|
||
} else {
|
||
$('#ad-img-' + s).hide().attr('src', '').off('click');
|
||
}
|
||
|
||
if (ad.link_url) {
|
||
$('#ad-link-' + s).attr('href', ad.link_url).show();
|
||
$('#ad-link-text-' + s).text(ad.link_text || T().ad_link_default);
|
||
} else {
|
||
$('#ad-link-' + s).hide();
|
||
}
|
||
}
|
||
|
||
function renderAdPair(startIdx) {
|
||
if (!_ads.length) return;
|
||
_adIndex = startIdx;
|
||
var left = _ads[startIdx] || null;
|
||
var right = (_ads.length > startIdx + 1) ? _ads[startIdx + 1] : null;
|
||
|
||
renderAdCard(0, left);
|
||
renderAdCard(1, right);
|
||
|
||
var totalPairs = Math.ceil(_ads.length / 2);
|
||
if (totalPairs > 1) {
|
||
var curPair = Math.floor(startIdx / 2);
|
||
var dots = '';
|
||
for (var i = 0; i < totalPairs; i++) {
|
||
dots += '<span class="ad-dot' + (i === curPair ? ' active' : '') +
|
||
'" data-index="' + (i * 2) + '"></span>';
|
||
}
|
||
$('#ad-pagination').html(dots).show();
|
||
$('#ad-pagination .ad-dot').off('click').on('click', function () {
|
||
var i = parseInt($(this).data('index'));
|
||
if (i !== _adIndex) {
|
||
resetAdCountdown();
|
||
renderAdPair(i);
|
||
startAdRotation();
|
||
}
|
||
});
|
||
} else {
|
||
$('#ad-pagination').empty().hide();
|
||
}
|
||
|
||
var dt = 15;
|
||
if (left) dt = left.display_time || 15;
|
||
if (right) dt = Math.max(dt, right.display_time || 15);
|
||
updateAdTimer(dt);
|
||
}
|
||
|
||
function updateAdTimer(seconds) {
|
||
$('#ad-timer').text(seconds + 's');
|
||
}
|
||
|
||
function startAdRotation() {
|
||
resetAdCountdown();
|
||
if (!_ads.length) return;
|
||
var left = _ads[_adIndex] || null;
|
||
var right = (_ads.length > _adIndex + 1) ? _ads[_adIndex + 1] : null;
|
||
var remaining = 15;
|
||
if (left) remaining = left.display_time || 15;
|
||
if (right) remaining = Math.max(remaining, right.display_time || 15);
|
||
_adCountdown = setInterval(function () {
|
||
remaining--;
|
||
if (remaining <= 0) {
|
||
clearInterval(_adCountdown);
|
||
var next = (_adIndex + 2) % _ads.length;
|
||
renderAdPair(next);
|
||
startAdRotation();
|
||
} else {
|
||
updateAdTimer(remaining);
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function resetAdCountdown() {
|
||
clearInterval(_adCountdown);
|
||
}
|
||
|
||
function showAdBar() {
|
||
$('#ad-bar').removeClass('hidden').show();
|
||
$('#ad-reopen-btn').hide();
|
||
_adDismissed = false;
|
||
$.post(appendToken('/api/ads/reopen'));
|
||
}
|
||
|
||
function hideAdBar() {
|
||
$('#ad-bar').addClass('hidden');
|
||
setTimeout(function () {
|
||
if (_adDismissed) {
|
||
$('#ad-bar').hide();
|
||
showReopenBtn();
|
||
}
|
||
}, 260);
|
||
}
|
||
|
||
function showReopenBtn() {
|
||
$('#ad-reopen-btn').show();
|
||
}
|
||
|
||
|
||
function openImgLightbox(src) {
|
||
if (!src) return;
|
||
$('#img-lightbox-img').attr('src', src);
|
||
$('#img-lightbox').addClass('show');
|
||
}
|
||
|
||
function closeImgLightbox() {
|
||
$('#img-lightbox').removeClass('show');
|
||
}
|
||
|
||
$(document).on('keydown', function (e) {
|
||
if (e.key === 'Escape' && $('#img-lightbox').hasClass('show')) {
|
||
closeImgLightbox();
|
||
}
|
||
});
|
||
|
||
function closeAd() {
|
||
resetAdCountdown();
|
||
_adDismissed = true;
|
||
$.post(appendToken('/api/ads/dismiss'));
|
||
hideAdBar();
|
||
}
|
||
|
||
function reopenAd() {
|
||
if (!_ads.length) {
|
||
loadAds();
|
||
return;
|
||
}
|
||
_adIndex = 0;
|
||
renderAdPair(0);
|
||
showAdBar();
|
||
startAdRotation();
|
||
}
|
||
|
||
|
||
function scrollToTop() {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
|
||
var POLL_FAIL_THRESHOLD = 3;
|
||
var _pollFailCount = 0;
|
||
|
||
function setConnBanner(lost) {
|
||
var el = $('#conn-banner');
|
||
if (!lost) {
|
||
el.removeClass('show');
|
||
return;
|
||
}
|
||
$('#conn-banner-text').text(T().conn_lost);
|
||
el.addClass('show');
|
||
}
|
||
|
||
var _activeAjax = 0;
|
||
|
||
function syncGlobalProgress() {
|
||
$('#global-progress').toggleClass('on', _activeAjax > 0);
|
||
}
|
||
|
||
function retryConnection() {
|
||
_pollFailCount = 0;
|
||
setConnBanner(false);
|
||
updateStatusAndGauge();
|
||
startGlobalPolling();
|
||
if ($('#tab-pool').hasClass('show')) startPoolPolling();
|
||
if ($('#tab-logs').hasClass('show')) _startLogRefresh();
|
||
}
|
||
|
||
$(function () {
|
||
initTheme(); initLang(); loadConfig(); loadLocalProxies(); loadIpLists(); loadBypassWhitelist(); loadUsers(); checkVer(); initSidebar(); loadAds();
|
||
|
||
restoreActiveView();
|
||
if (typeof initFieldTips === 'function') initFieldTips();
|
||
if (typeof initTableDensity === 'function') initTableDensity();
|
||
if (typeof initProxyTableKeyboard === 'function') initProxyTableKeyboard();
|
||
if (typeof initAccessRecordsTable === 'function') initAccessRecordsTable();
|
||
if (typeof restoreActiveTasks === 'function') restoreActiveTasks();
|
||
|
||
var $scrollBtn = $('#scroll-top-btn');
|
||
$(window).on('scroll', function () {
|
||
$scrollBtn.toggleClass('visible', window.scrollY > window.innerHeight * 0.6);
|
||
});
|
||
|
||
startGlobalPolling();
|
||
|
||
document.addEventListener('visibilitychange', function () {
|
||
if (document.hidden) {
|
||
pauseGlobalPolling();
|
||
} else {
|
||
startGlobalPolling();
|
||
if ($('#tab-logs').hasClass('show')) _startLogRefresh();
|
||
if ($('#tab-pool').hasClass('show')) startPoolPolling();
|
||
}
|
||
});
|
||
|
||
var _POLLED_PATHS = ['/api/status', '/api/logs', '/api/logs/stats',
|
||
'/api/logs/domains', '/api/logs/records',
|
||
'/api/pool/status'];
|
||
function isBackgroundPoll(settings) {
|
||
if (!settings || !settings.url) return false;
|
||
var path = String(settings.url).split('?')[0];
|
||
if (_POLLED_PATHS.indexOf(path) >= 0) return true;
|
||
return /^\/api\/pool\/tasks\/[^/]+$/.test(path);
|
||
}
|
||
|
||
$(document).ajaxSend(function (event, xhr, settings) {
|
||
if (isBackgroundPoll(settings)) return;
|
||
_activeAjax++;
|
||
syncGlobalProgress();
|
||
});
|
||
$(document).ajaxComplete(function (event, xhr, settings) {
|
||
if (isBackgroundPoll(settings)) return;
|
||
_activeAjax = Math.max(0, _activeAjax - 1);
|
||
syncGlobalProgress();
|
||
});
|
||
$(document).ajaxError(function (event, xhr, settings) {
|
||
if (isBackgroundPoll(settings)) {
|
||
_pollFailCount++;
|
||
if (_pollFailCount >= POLL_FAIL_THRESHOLD) setConnBanner(true);
|
||
return;
|
||
}
|
||
var path = settings && settings.url ? String(settings.url).split('?')[0] : '';
|
||
if (path.indexOf('/api/pool/') === 0 || path === '/api/users') return;
|
||
var msg = T().request_failed + T().sep_colon + jqErrorText(xhr, T().network_error);
|
||
toast(msg, 'error');
|
||
});
|
||
$(document).ajaxSuccess(function (event, xhr, settings) {
|
||
if (!isBackgroundPoll(settings)) return;
|
||
_pollFailCount = 0;
|
||
setConnBanner(false);
|
||
});
|
||
|
||
});
|