- 原生 fnOS 应用(CGI 入口 + Python 守护进程),依赖 python312 运行时 - 三平台签到:TRAE 每日 200 积分、WorkBuddy 7 天循环、Qoder 每日 100 Credits - WorkBuddy 网页授权(OAuth 设备流)、TRAE icubeAuth 解密 Token、Qoder Cosy 设备头 - 多时段调度(默认 00:05/09:00/12:30/19:00)、启动补跑、幂等签到+回查确认 - 多平台 Webhook 通知(企业微信/钉钉/飞书/Bark/Server酱/PushPlus 自动识别) - Qoder 双站点 Token 隔离自动探测与修复 - 前端:仪表盘/账号管理/签到记录/设置四个页签,30s 轮询+变化检测+隐藏暂停 - 测试:34 后端 + 25 E2E + 9 前端回归 + 真实时钟调度测试
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();
|
||
})();
|