513 lines
22 KiB
JavaScript
513 lines
22 KiB
JavaScript
/* AgentCheckin frontend — vanilla JS, no dependencies */
|
||||
|
|
(function () {
|
|||
|
|
"use strict";
|
|||
|
|
|
|||
|
|
var state = null;
|
|||
|
|
var pollTimer = null;
|
|||
|
|
var oauthState = null;
|
|||
|
|
var oauthTimer = null;
|
|||
|
|
var logTimer = null;
|
|||
|
|
|
|||
|
|
var $ = function (id) { return document.getElementById(id); };
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- helpers
|
|||
|
|
function toast(msg, kind) {
|
|||
|
|
var t = $("toast");
|
|||
|
|
t.textContent = msg;
|
|||
|
|
t.className = "toast" + (kind ? " " + kind : "");
|
|||
|
|
clearTimeout(t._timer);
|
|||
|
|
t._timer = setTimeout(function () { t.classList.add("hidden"); }, 3200);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function api(method, path, body) {
|
|||
|
|
var opts = { method: method, headers: {} };
|
|||
|
|
if (body !== undefined && body !== null) {
|
|||
|
|
opts.headers["Content-Type"] = "application/json";
|
|||
|
|
opts.body = JSON.stringify(body);
|
|||
|
|
}
|
|||
|
|
return fetch("api" + path, opts).then(function (r) {
|
|||
|
|
return r.json().catch(function () { return {}; });
|
|||
|
|
}).then(function (d) {
|
|||
|
|
if (d && d.error) throw new Error(d.error);
|
|||
|
|
return d;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function esc(s) {
|
|||
|
|
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
|||
|
|
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var STATUS = {
|
|||
|
|
ok: { label: "签到成功", cls: "ok", icon: "✅" },
|
|||
|
|
ok_unverified: { label: "已领取·待确认", cls: "ok_unverified", icon: "✅" },
|
|||
|
|
already: { label: "今日已签", cls: "already", icon: "☑️" },
|
|||
|
|
inactive: { label: "未开放签到", cls: "inactive", icon: "➖" },
|
|||
|
|
fail: { label: "签到失败", cls: "fail", icon: "❌" },
|
|||
|
|
no_session: { label: "需重新授权", cls: "no_session", icon: "🔒" },
|
|||
|
|
no_auth: { label: "缺少凭据", cls: "no_auth", icon: "🔒" },
|
|||
|
|
retry_later: { label: "风控冷却", cls: "retry_later", icon: "⏳" }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function platformName(p) { return p === "trae" ? "TRAE" : (p === "qoder" ? "Qoder" : "WorkBuddy"); }
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- tabs
|
|||
|
|
document.querySelectorAll(".tab").forEach(function (btn) {
|
|||
|
|
btn.addEventListener("click", function () {
|
|||
|
|
document.querySelectorAll(".tab").forEach(function (b) { b.classList.remove("active"); });
|
|||
|
|
document.querySelectorAll(".tabpage").forEach(function (p) { p.classList.remove("active"); });
|
|||
|
|
btn.classList.add("active");
|
|||
|
|
$("tab-" + btn.dataset.tab).classList.add("active");
|
|||
|
|
if (btn.dataset.tab === "logs") loadLogs();
|
|||
|
|
if (btn.dataset.tab === "accounts") renderAccounts();
|
|||
|
|
if (btn.dataset.tab === "settings") openSettingsTab();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- dashboard
|
|||
|
|
function renderTop() {
|
|||
|
|
var alive = state && state.daemon_alive;
|
|||
|
|
var chip = $("daemon-chip");
|
|||
|
|
chip.innerHTML = '<span class="dot"></span>守护进程 ' + (alive ? "运行中" : "已停止");
|
|||
|
|
chip.className = "chip " + (alive ? "chip-green" : "chip-red");
|
|||
|
|
var nr = $("nextrun-chip");
|
|||
|
|
nr.textContent = "下次签到 " + ((state && state.next_run_at) || "—");
|
|||
|
|
nr.className = "chip " + (state && state.next_run_at ? "chip-blue" : "chip-gray");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderCards() {
|
|||
|
|
var wrap = $("dash-cards");
|
|||
|
|
var accounts = (state && state.accounts) || [];
|
|||
|
|
if (!accounts.length) {
|
|||
|
|
wrap.innerHTML = '<div class="panel" style="grid-column:1/-1;text-align:center;color:var(--muted)">' +
|
|||
|
|
"暂无账号,请先到「账号管理」添加 TRAE / WorkBuddy 账号</div>";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
wrap.innerHTML = accounts.map(function (a) {
|
|||
|
|
var st = a.today && a.today.status ? STATUS[a.today.status] : null;
|
|||
|
|
var statusHtml = st
|
|||
|
|
? '<span class="status-chip ' + st.cls + '">' + st.icon + " " + st.label + "</span>"
|
|||
|
|
: '<span class="status-chip pending">⏳ 今日未签</span>';
|
|||
|
|
var credits = "";
|
|||
|
|
if (a.last_result && typeof a.last_result.credits === "number") {
|
|||
|
|
credits = "剩余积分 <b>" + a.last_result.credits + "</b>";
|
|||
|
|
} else if (a.last_result && a.last_result.points) {
|
|||
|
|
credits = "本次 +<b>" + a.last_result.points + "</b>";
|
|||
|
|
}
|
|||
|
|
var msg = a.today && a.today.message ? esc(a.today.message) : "";
|
|||
|
|
return (
|
|||
|
|
'<div class="acct-card' + (a.enabled ? "" : " disabled") + '">' +
|
|||
|
|
'<div class="acct-head"><span class="platform-badge ' + esc(a.platform) + '">' + platformName(a.platform) + "</span>" +
|
|||
|
|
'<span class="acct-name">' + esc(a.name) + "</span></div>" +
|
|||
|
|
'<div class="acct-status">' + statusHtml + (msg ? '<span class="muted">' + msg + "</span>" : "") + "</div>" +
|
|||
|
|
'<div class="acct-meta">' + (credits ? "<span>" + credits + "</span>" : "") +
|
|||
|
|
(a.last_result ? "<span>最近运行 " + esc(a.last_result.time || "") + "</span>" : "") +
|
|||
|
|
(!a.enabled ? "<span>已禁用</span>" : "") + "</div>" +
|
|||
|
|
'<div class="acct-actions">' +
|
|||
|
|
'<button class="btn btn-sm" data-act="run" data-id="' + esc(a.id) + '">立即签到</button>' +
|
|||
|
|
'<button class="btn btn-sm" data-act="test" data-id="' + esc(a.id) + '">测连接</button>' +
|
|||
|
|
"</div></div>"
|
|||
|
|
);
|
|||
|
|
}).join("");
|
|||
|
|
|
|||
|
|
wrap.querySelectorAll("button[data-act]").forEach(function (btn) {
|
|||
|
|
btn.addEventListener("click", function () {
|
|||
|
|
if (btn.dataset.act === "run") runCheckin(btn.dataset.id, btn);
|
|||
|
|
else testAccount(btn.dataset.id, btn);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var lastStateJson = "";
|
|||
|
|
function refreshState(force) {
|
|||
|
|
return api("GET", "/state").then(function (d) {
|
|||
|
|
var json = JSON.stringify(d);
|
|||
|
|
if (!force && json === lastStateJson) return; // 数据没变,跳过重渲染
|
|||
|
|
lastStateJson = json;
|
|||
|
|
state = d;
|
|||
|
|
renderTop();
|
|||
|
|
renderCards();
|
|||
|
|
}).catch(function (e) { /* keep old state on transient errors */ });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- run / test
|
|||
|
|
var runningBtn = null;
|
|||
|
|
function runCheckin(accountId, btn) {
|
|||
|
|
if (btn) { btn.disabled = true; btn.textContent = "执行中…"; runningBtn = btn; }
|
|||
|
|
api("POST", "/checkin", accountId ? { account_id: accountId } : {})
|
|||
|
|
.then(function (d) {
|
|||
|
|
if (d.mode === "trigger") {
|
|||
|
|
toast("已触发签到,守护进程正在执行…");
|
|||
|
|
waitForRun(accountId);
|
|||
|
|
} else {
|
|||
|
|
toast("签到完成", "ok");
|
|||
|
|
refreshState();
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.catch(function (e) { toast(e.message, "err"); })
|
|||
|
|
.finally(function () {
|
|||
|
|
if (runningBtn) { runningBtn.disabled = false; runningBtn.textContent = "立即签到"; runningBtn = null; }
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function waitForRun(accountId) {
|
|||
|
|
// poll until today_results contains a fresh entry for the target
|
|||
|
|
var started = Date.now();
|
|||
|
|
var before = JSON.stringify((state && state.today_results) || {});
|
|||
|
|
(function poll() {
|
|||
|
|
if (Date.now() - started > 90000) { refreshState(); return; }
|
|||
|
|
refreshState().then(function () {
|
|||
|
|
var after = JSON.stringify((state && state.today_results) || {});
|
|||
|
|
var done = after !== before && (!accountId || (state.today_results || {})[accountId]);
|
|||
|
|
if (done) {
|
|||
|
|
var r = accountId ? state.today_results[accountId] : null;
|
|||
|
|
if (r) toast(platformName(r.platform) + " " + r.name + ":" + (r.message || r.status), r.status === "fail" || r.status === "no_session" ? "err" : "ok");
|
|||
|
|
else toast("签到完成", "ok");
|
|||
|
|
} else {
|
|||
|
|
setTimeout(poll, 3000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
})();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function testAccount(id, btn) {
|
|||
|
|
btn.disabled = true; btn.textContent = "检测中…";
|
|||
|
|
api("POST", "/accounts/test", { id: id })
|
|||
|
|
.then(function (d) {
|
|||
|
|
var parts = [];
|
|||
|
|
if (d.refresh) parts.push("续期:" + d.refresh);
|
|||
|
|
if (typeof d.checked_in === "boolean") parts.push("今日" + (d.checked_in ? "已签" : "未签"));
|
|||
|
|
if (d.enable === false) parts.push("活动未开放");
|
|||
|
|
if (d.credits && typeof d.credits.remain === "number") parts.push("剩余积分 " + d.credits.remain);
|
|||
|
|
else if (typeof d.credits === "number") parts.push("积分 " + d.credits);
|
|||
|
|
if (d.hint) parts.push(d.hint);
|
|||
|
|
if (d.error) parts.push("错误:" + d.error);
|
|||
|
|
toast(parts.join(";") || "检测完成", d.error ? "err" : "ok");
|
|||
|
|
})
|
|||
|
|
.catch(function (e) { toast(e.message, "err"); })
|
|||
|
|
.finally(function () { btn.disabled = false; btn.textContent = "测连接"; });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- accounts tab
|
|||
|
|
function renderAccounts() {
|
|||
|
|
var wrap = $("account-table-wrap");
|
|||
|
|
var accounts = (state && state.accounts) || [];
|
|||
|
|
if (!accounts.length) {
|
|||
|
|
wrap.innerHTML = '<p class="muted">暂无账号,点击右上角「添加账号」。</p>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var rows = accounts.map(function (a) {
|
|||
|
|
var st = a.today && a.today.status ? STATUS[a.today.status] : null;
|
|||
|
|
return "<tr>" +
|
|||
|
|
"<td><span class='platform-badge " + esc(a.platform) + "'>" + platformName(a.platform) + "</span></td>" +
|
|||
|
|
"<td>" + esc(a.name) + (a.need_reauth ? ' <span class="status-chip no_session">需重新授权</span>' : "") +
|
|||
|
|
(a.platform === "qoder" ? '<div class="muted" style="font-size:11px">' + esc((a.auth && a.auth.domain) || "") + "</div>" : "") + "</td>" +
|
|||
|
|
"<td>" + esc(a.auth.accessToken || "") + "</td>" +
|
|||
|
|
"<td>" + (st ? st.icon + " " + st.label : "⏳ 今日未签") + "</td>" +
|
|||
|
|
"<td>" + (a.last_result && typeof a.last_result.credits === "number" ? a.last_result.credits : "—") + "</td>" +
|
|||
|
|
"<td>" + (a.enabled ? "启用" : "禁用") + "</td>" +
|
|||
|
|
'<td class="actions">' +
|
|||
|
|
'<button class="btn btn-sm" data-a2="run" data-id="' + esc(a.id) + '">签到</button>' +
|
|||
|
|
(a.platform === "qoder" ? '<button class="btn btn-sm" data-a2="fixdomain" data-id="' + esc(a.id) + '" title="自动探测 Token 所属站点并切换">修复站点</button>' : "") +
|
|||
|
|
'<button class="btn btn-sm" data-a2="toggle" data-id="' + esc(a.id) + '">' + (a.enabled ? "禁用" : "启用") + "</button>" +
|
|||
|
|
'<button class="btn btn-sm btn-danger" data-a2="del" data-id="' + esc(a.id) + '">删除</button>' +
|
|||
|
|
"</td></tr>";
|
|||
|
|
}).join("");
|
|||
|
|
wrap.innerHTML = '<table class="tbl"><thead><tr><th>平台</th><th>名称</th><th>Token</th><th>今日</th><th>积分</th><th>状态</th><th>操作</th></tr></thead><tbody>' + rows + "</tbody></table>";
|
|||
|
|
wrap.querySelectorAll("button[data-a2]").forEach(function (btn) {
|
|||
|
|
btn.addEventListener("click", function () {
|
|||
|
|
var id = btn.dataset.id, act = btn.dataset.a2;
|
|||
|
|
if (act === "run") runCheckin(id, btn);
|
|||
|
|
else if (act === "fixdomain") {
|
|||
|
|
btn.disabled = true; btn.textContent = "探测中…";
|
|||
|
|
api("POST", "/accounts/fix_domain", { id: id })
|
|||
|
|
.then(function (d) {
|
|||
|
|
if (d.ok) { toast("已切换到 " + d.domain, "ok"); refreshState(); }
|
|||
|
|
else { toast(d.error || "探测失败", "err"); }
|
|||
|
|
})
|
|||
|
|
.catch(function (e) { toast(e.message, "err"); })
|
|||
|
|
.finally(function () { btn.disabled = false; btn.textContent = "修复站点"; });
|
|||
|
|
}
|
|||
|
|
else if (act === "toggle") {
|
|||
|
|
var acc = state.accounts.find(function (x) { return x.id === id; });
|
|||
|
|
api("POST", "/accounts/update", { id: id, enabled: !acc.enabled })
|
|||
|
|
.then(function () { refreshState(); }).catch(function (e) { toast(e.message, "err"); });
|
|||
|
|
} else if (act === "del") {
|
|||
|
|
if (!confirm("确定删除该账号?")) return;
|
|||
|
|
api("POST", "/accounts/delete", { id: id })
|
|||
|
|
.then(function () { toast("已删除", "ok"); refreshState(); })
|
|||
|
|
.catch(function (e) { toast(e.message, "err"); });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- add modal
|
|||
|
|
function openModal() { $("modal-add").classList.remove("hidden"); }
|
|||
|
|
function closeModal(id) { $(id).classList.add("hidden"); stopOauth(); $("add-msg").textContent = ""; }
|
|||
|
|
|
|||
|
|
document.querySelectorAll("[data-close]").forEach(function (b) {
|
|||
|
|
b.addEventListener("click", function () { closeModal(b.dataset.close); });
|
|||
|
|
});
|
|||
|
|
$("modal-add").addEventListener("click", function (e) {
|
|||
|
|
if (e.target === this) closeModal("modal-add");
|
|||
|
|
});
|
|||
|
|
$("btn-add-account").addEventListener("click", openModal);
|
|||
|
|
|
|||
|
|
document.querySelectorAll(".mtab").forEach(function (btn) {
|
|||
|
|
btn.addEventListener("click", function () {
|
|||
|
|
document.querySelectorAll(".mtab").forEach(function (b) { b.classList.remove("active"); });
|
|||
|
|
document.querySelectorAll(".mtabpage").forEach(function (p) { p.classList.remove("active"); });
|
|||
|
|
btn.classList.add("active");
|
|||
|
|
$("mtab-" + btn.dataset.mtab).classList.add("active");
|
|||
|
|
stopOauth();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function addMsg(msg, ok) {
|
|||
|
|
var m = $("add-msg");
|
|||
|
|
m.textContent = msg;
|
|||
|
|
m.className = "msg " + (ok ? "ok" : "err");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$("btn-wb-manual-add").addEventListener("click", function () {
|
|||
|
|
var token = $("wb-token").value.trim();
|
|||
|
|
if (!token) return addMsg("请填写 Access Token", false);
|
|||
|
|
api("POST", "/accounts/add_wb_manual", {
|
|||
|
|
name: $("wb-name").value.trim(),
|
|||
|
|
access_token: token,
|
|||
|
|
refresh_token: $("wb-refresh").value.trim(),
|
|||
|
|
domain: $("wb-domain").value
|
|||
|
|
}).then(function () {
|
|||
|
|
toast("WorkBuddy 账号已添加", "ok");
|
|||
|
|
closeModal("modal-add"); refreshState();
|
|||
|
|
}).catch(function (e) { addMsg(e.message, false); });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$("btn-trae-add").addEventListener("click", function () {
|
|||
|
|
var token = $("trae-token").value.trim();
|
|||
|
|
if (!token) return addMsg("请填写 Access Token", false);
|
|||
|
|
api("POST", "/accounts/add_trae", {
|
|||
|
|
name: $("trae-name").value.trim(),
|
|||
|
|
access_token: token,
|
|||
|
|
refresh_token: $("trae-refresh").value.trim()
|
|||
|
|
}).then(function () {
|
|||
|
|
toast("TRAE 账号已添加", "ok");
|
|||
|
|
closeModal("modal-add"); refreshState();
|
|||
|
|
}).catch(function (e) { addMsg(e.message, false); });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
$("btn-qoder-add").addEventListener("click", function () {
|
|||
|
|
var token = $("qoder-token").value.trim();
|
|||
|
|
if (!token) return addMsg("请填写 Access Token", false);
|
|||
|
|
if (token.length < 10) return addMsg("Access Token 仅 " + token.length + " 字符,明显不完整,请重新复制", false);
|
|||
|
|
var rt0 = $("qoder-refresh").value.trim();
|
|||
|
|
if (rt0 && rt0.length < 10) return addMsg("Refresh Token 仅 " + rt0.length + " 字符,明显不完整,请重新复制", false);
|
|||
|
|
var dev = {};
|
|||
|
|
var map = {
|
|||
|
|
"qoder-machine-id": "machineId",
|
|||
|
|
"qoder-machine-token": "machineToken",
|
|||
|
|
"qoder-machine-code": "machineCode",
|
|||
|
|
"qoder-machine-type": "machineType",
|
|||
|
|
"qoder-version": "version",
|
|||
|
|
};
|
|||
|
|
Object.keys(map).forEach(function (id) {
|
|||
|
|
var v = $(id).value.trim();
|
|||
|
|
if (v) dev[map[id]] = v;
|
|||
|
|
});
|
|||
|
|
api("POST", "/accounts/add_qoder", {
|
|||
|
|
name: $("qoder-name").value.trim(),
|
|||
|
|
access_token: token,
|
|||
|
|
refresh_token: $("qoder-refresh").value.trim(),
|
|||
|
|
domain: $("qoder-domain").value,
|
|||
|
|
device: dev
|
|||
|
|
}).then(function () {
|
|||
|
|
toast("Qoder 账号已添加", "ok");
|
|||
|
|
closeModal("modal-add"); refreshState();
|
|||
|
|
}).catch(function (e) { addMsg(e.message, false); });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---- workbuddy oauth flow
|
|||
|
|
function stopOauth() {
|
|||
|
|
if (oauthTimer) { clearInterval(oauthTimer); oauthTimer = null; }
|
|||
|
|
oauthState = null;
|
|||
|
|
$("oauth-box").classList.add("hidden");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$("btn-oauth-begin").addEventListener("click", function () {
|
|||
|
|
this.disabled = true;
|
|||
|
|
api("POST", "/wb_oauth_begin", {})
|
|||
|
|
.then(function (d) {
|
|||
|
|
oauthState = d.state;
|
|||
|
|
$("oauth-url").value = d.login_url;
|
|||
|
|
$("oauth-box").classList.remove("hidden");
|
|||
|
|
$("oauth-status").textContent = "等待登录…(链接 " + Math.round(d.ttl / 60) + " 分钟内有效)";
|
|||
|
|
oauthTimer = setInterval(pollOauth, 2000);
|
|||
|
|
})
|
|||
|
|
.catch(function (e) { addMsg(e.message, false); })
|
|||
|
|
.finally(function () { this.disabled = false; }.bind(this));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$("btn-oauth-copy").addEventListener("click", function () {
|
|||
|
|
var input = $("oauth-url");
|
|||
|
|
input.select();
|
|||
|
|
var done = function () { toast("链接已复制", "ok"); };
|
|||
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|||
|
|
navigator.clipboard.writeText(input.value).then(done, function () { document.execCommand("copy"); done(); });
|
|||
|
|
} else { document.execCommand("copy"); done(); }
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function pollOauth() {
|
|||
|
|
if (!oauthState) return;
|
|||
|
|
api("GET", "/wb_oauth_poll?state=" + encodeURIComponent(oauthState))
|
|||
|
|
.then(function (d) {
|
|||
|
|
if (d.status === "done") {
|
|||
|
|
stopOauth();
|
|||
|
|
toast("WorkBuddy 账号已绑定", "ok");
|
|||
|
|
closeModal("modal-add");
|
|||
|
|
refreshState();
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.catch(function (e) {
|
|||
|
|
$("oauth-status").textContent = e.message;
|
|||
|
|
stopOauth();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- logs
|
|||
|
|
function loadLogs() {
|
|||
|
|
api("GET", "/logs?lines=300").then(function (d) {
|
|||
|
|
$("log-view").textContent = (d.lines || []).join("\n") || "暂无日志";
|
|||
|
|
$("log-view").scrollTop = $("log-view").scrollHeight;
|
|||
|
|
}).catch(function () {
|
|||
|
|
$("log-view").textContent = "日志加载失败";
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
$("btn-refresh-logs").addEventListener("click", loadLogs);
|
|||
|
|
setInterval(function () {
|
|||
|
|
if (document.hidden) return;
|
|||
|
|
if ($("log-autorefresh").checked && $("tab-logs").classList.contains("active")) loadLogs();
|
|||
|
|
}, 15000);
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- settings
|
|||
|
|
// Local editing copy: initialised from server state ONLY when the tab is
|
|||
|
|
// opened (and there are no unsaved edits). The 10s state poll must never
|
|||
|
|
// overwrite it, otherwise in-progress edits are silently discarded.
|
|||
|
|
var settings = null;
|
|||
|
|
var settingsDirty = false;
|
|||
|
|
|
|||
|
|
function openSettingsTab() {
|
|||
|
|
if (!settingsDirty) {
|
|||
|
|
settings = JSON.parse(JSON.stringify(
|
|||
|
|
(state && state.settings) || { schedule_times: [], webhook_url: "", notify_on: [] }));
|
|||
|
|
}
|
|||
|
|
renderSettings();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderSettings() {
|
|||
|
|
// renders ONLY from the local copy — never re-reads from `state` here
|
|||
|
|
settings = settings || { schedule_times: [], webhook_url: "", notify_on: [] };
|
|||
|
|
var chips = $("time-chips");
|
|||
|
|
chips.innerHTML = "";
|
|||
|
|
(settings.schedule_times || []).forEach(function (t) {
|
|||
|
|
var el = document.createElement("span");
|
|||
|
|
el.className = "time-chip";
|
|||
|
|
el.innerHTML = esc(t) + '<button title="删除">×</button>';
|
|||
|
|
el.querySelector("button").addEventListener("click", function () {
|
|||
|
|
settings.schedule_times = settings.schedule_times.filter(function (x) { return x !== t; });
|
|||
|
|
settingsDirty = true;
|
|||
|
|
renderSettings();
|
|||
|
|
});
|
|||
|
|
chips.appendChild(el);
|
|||
|
|
});
|
|||
|
|
$("webhook-url").value = settings.webhook_url || "";
|
|||
|
|
$("webhook-type").value = settings.webhook_type || "auto";
|
|||
|
|
$("notify-fail").checked = (settings.notify_on || []).indexOf("fail") >= 0;
|
|||
|
|
$("notify-success").checked = (settings.notify_on || []).indexOf("success") >= 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$("btn-add-time").addEventListener("click", function () {
|
|||
|
|
var v = $("new-time").value;
|
|||
|
|
if (!v) { toast("请先选择要添加的时间", "err"); return; }
|
|||
|
|
settings.schedule_times = settings.schedule_times || [];
|
|||
|
|
if (settings.schedule_times.indexOf(v) >= 0) {
|
|||
|
|
toast("该时间已存在", "err");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
settings.schedule_times.push(v);
|
|||
|
|
settings.schedule_times.sort();
|
|||
|
|
settingsDirty = true;
|
|||
|
|
renderSettings();
|
|||
|
|
$("new-time").value = "";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$("btn-save-settings").addEventListener("click", function () {
|
|||
|
|
var times = (settings.schedule_times || []).slice().sort();
|
|||
|
|
if (!times.length) { $("settings-msg").textContent = "至少保留一个签到时间点"; $("settings-msg").className = "msg err"; return; }
|
|||
|
|
var notify = [];
|
|||
|
|
if ($("notify-fail").checked) notify.push("fail");
|
|||
|
|
if ($("notify-success").checked) notify.push("success");
|
|||
|
|
api("POST", "/settings", {
|
|||
|
|
schedule_times: times,
|
|||
|
|
webhook_url: $("webhook-url").value.trim(),
|
|||
|
|
webhook_type: $("webhook-type").value,
|
|||
|
|
notify_on: notify
|
|||
|
|
}).then(function () {
|
|||
|
|
settingsDirty = false;
|
|||
|
|
$("settings-msg").textContent = "已保存";
|
|||
|
|
$("settings-msg").className = "msg ok";
|
|||
|
|
toast("设置已保存", "ok");
|
|||
|
|
refreshState();
|
|||
|
|
}).catch(function (e) {
|
|||
|
|
$("settings-msg").textContent = e.message;
|
|||
|
|
$("settings-msg").className = "msg err";
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$("btn-test-notify").addEventListener("click", function () {
|
|||
|
|
var url = $("webhook-url").value.trim();
|
|||
|
|
if (!url) { $("settings-msg").textContent = "请先填写 Webhook 地址"; $("settings-msg").className = "msg err"; return; }
|
|||
|
|
// save first so the daemon/notify uses the new url, then fire a real run
|
|||
|
|
var notify = [];
|
|||
|
|
if ($("notify-fail").checked) notify.push("fail");
|
|||
|
|
if ($("notify-success").checked) notify.push("success");
|
|||
|
|
api("POST", "/settings", { webhook_url: url, webhook_type: $("webhook-type").value, notify_on: notify })
|
|||
|
|
.then(function () {
|
|||
|
|
settingsDirty = false;
|
|||
|
|
return api("POST", "/checkin", {});
|
|||
|
|
})
|
|||
|
|
.then(function () {
|
|||
|
|
$("settings-msg").textContent = "已触发一次签到,通知将随结果发送";
|
|||
|
|
$("settings-msg").className = "msg ok";
|
|||
|
|
waitForRun(null);
|
|||
|
|
})
|
|||
|
|
.catch(function (e) {
|
|||
|
|
$("settings-msg").textContent = e.message;
|
|||
|
|
$("settings-msg").className = "msg err";
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- run all
|
|||
|
|
$("btn-run-all").addEventListener("click", function () { runCheckin(null, null); });
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------- boot
|
|||
|
|
var STATE_POLL_MS = 30000;
|
|||
|
|
function boot() {
|
|||
|
|
refreshState(true);
|
|||
|
|
renderAccounts();
|
|||
|
|
pollTimer = setInterval(function () {
|
|||
|
|
if (document.hidden) return; // 页面/iframe 不可见时暂停轮询
|
|||
|
|
refreshState().then(renderAccountsIfVisible);
|
|||
|
|
}, STATE_POLL_MS);
|
|||
|
|
document.addEventListener("visibilitychange", function () {
|
|||
|
|
if (!document.hidden) refreshState(true).then(renderAccountsIfVisible);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function renderAccountsIfVisible() {
|
|||
|
|
if ($("tab-accounts").classList.contains("active")) renderAccounts();
|
|||
|
|
}
|
|||
|
|
boot();
|
|||
|
|
})();
|