486 lines
16 KiB
JavaScript
486 lines
16 KiB
JavaScript
|
|
const BOOL_FIELDS = [
|
|||
|
|
"use_llm",
|
|||
|
|
"use_description_variation",
|
|||
|
|
"auto_fill_enabled",
|
|||
|
|
];
|
|||
|
|
const NUMBER_FIELDS = [
|
|||
|
|
"gitlab_board_id",
|
|||
|
|
"auto_fill_hour",
|
|||
|
|
"auto_fill_minute",
|
|||
|
|
"max_hours_per_day",
|
|||
|
|
"hour_step",
|
|||
|
|
"compbase_default_days",
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const SECRET_INPUTS = new Set(["gitlab_token", "xai_api_key", "compbase_password"]);
|
|||
|
|
|
|||
|
|
function fillSettingsForm(data) {
|
|||
|
|
const form = document.getElementById("settingsForm");
|
|||
|
|
for (const el of form.querySelectorAll("[name]")) {
|
|||
|
|
const name = el.name;
|
|||
|
|
if (name === "dates_file_content") continue;
|
|||
|
|
if (BOOL_FIELDS.includes(name)) {
|
|||
|
|
el.checked = Boolean(data[name]);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
// Never put masked secrets back into password fields (looks like a real token)
|
|||
|
|
if (SECRET_INPUTS.has(name)) {
|
|||
|
|
el.value = "";
|
|||
|
|
el.placeholder = data[`${name}_set`]
|
|||
|
|
? "已設定(重新貼上才會更新)"
|
|||
|
|
: "貼上 token";
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (data[name] !== undefined && data[name] !== null) {
|
|||
|
|
el.value = data[name];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (data.pts_username) {
|
|||
|
|
document.getElementById("ptsUsername").value = data.pts_username;
|
|||
|
|
}
|
|||
|
|
if (data.teams_username) {
|
|||
|
|
document.getElementById("teamsUsername").value = data.teams_username;
|
|||
|
|
}
|
|||
|
|
document.getElementById("teamsEnabled").checked = Boolean(data.teams_calendar_enabled);
|
|||
|
|
|
|||
|
|
const gitlabHint = document.getElementById("gitlabTokenHint");
|
|||
|
|
if (gitlabHint) {
|
|||
|
|
gitlabHint.textContent = data.gitlab_token_set
|
|||
|
|
? "Token 已儲存。若無法使用請重新產生並貼上新的 Personal Access Token(權限至少 api)。"
|
|||
|
|
: "到 GitLab → Preferences → Access Tokens 建立,勾選 api。";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function collectSettingsPayload(form) {
|
|||
|
|
const payload = {};
|
|||
|
|
for (const el of form.querySelectorAll("[name]")) {
|
|||
|
|
const name = el.name;
|
|||
|
|
if (name === "dates_file_content") continue;
|
|||
|
|
if (BOOL_FIELDS.includes(name)) {
|
|||
|
|
payload[name] = el.checked;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (NUMBER_FIELDS.includes(name)) {
|
|||
|
|
const num = el.value === "" ? null : Number(el.value);
|
|||
|
|
if (num !== null && !Number.isNaN(num)) payload[name] = num;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
const value = el.value.trim();
|
|||
|
|
if (name === "gitlab_milestone_title") {
|
|||
|
|
payload[name] = value;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (value !== "") payload[name] = value;
|
|||
|
|
}
|
|||
|
|
payload.teams_calendar_enabled = document.getElementById("teamsEnabled").checked;
|
|||
|
|
return payload;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderAuthStatus(data) {
|
|||
|
|
const ptsEl = document.getElementById("ptsAuthStatus");
|
|||
|
|
if (ptsEl) {
|
|||
|
|
ptsEl.textContent = data.has_pts_session ? "已連線" : "未連線";
|
|||
|
|
ptsEl.className = `badge ${data.has_pts_session ? "ok" : "warn"}`;
|
|||
|
|
}
|
|||
|
|
const teams = data.teams_calendar || data.graph_calendar || {};
|
|||
|
|
const teamsEl = document.getElementById("teamsAuthStatus");
|
|||
|
|
if (teamsEl) {
|
|||
|
|
if (!data.teams_calendar_enabled) {
|
|||
|
|
teamsEl.textContent = "未啟用";
|
|||
|
|
teamsEl.className = "badge";
|
|||
|
|
} else if (teams.connected) {
|
|||
|
|
teamsEl.textContent = teams.auth_mode === "graph_api" ? "Graph API" : "已連線";
|
|||
|
|
teamsEl.className = "badge ok";
|
|||
|
|
} else {
|
|||
|
|
teamsEl.textContent = "未連線";
|
|||
|
|
teamsEl.className = "badge warn";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function refreshStatus() {
|
|||
|
|
try {
|
|||
|
|
const data = await api("/api/status");
|
|||
|
|
renderAuthStatus(data);
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error(err);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadSettings() {
|
|||
|
|
const [settings, dates] = await Promise.all([
|
|||
|
|
api("/api/settings"),
|
|||
|
|
api("/api/dates-file"),
|
|||
|
|
]);
|
|||
|
|
fillSettingsForm(settings);
|
|||
|
|
document.getElementById("datesFileContent").value = dates.content || "";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveSettings(event) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
const statusEl = document.getElementById("settingsStatus");
|
|||
|
|
statusEl.textContent = "儲存中…";
|
|||
|
|
statusEl.className = "settings-status";
|
|||
|
|
const form = document.getElementById("settingsForm");
|
|||
|
|
const payload = collectSettingsPayload(form);
|
|||
|
|
const datesContent = document.getElementById("datesFileContent").value;
|
|||
|
|
try {
|
|||
|
|
await api("/api/settings", {
|
|||
|
|
method: "PUT",
|
|||
|
|
body: JSON.stringify(payload),
|
|||
|
|
});
|
|||
|
|
await api("/api/dates-file", {
|
|||
|
|
method: "PUT",
|
|||
|
|
body: JSON.stringify({ content: datesContent }),
|
|||
|
|
});
|
|||
|
|
await loadSettings();
|
|||
|
|
await refreshStatus();
|
|||
|
|
statusEl.textContent = "已儲存";
|
|||
|
|
statusEl.className = "settings-status ok";
|
|||
|
|
} catch (err) {
|
|||
|
|
statusEl.textContent = err.message;
|
|||
|
|
statusEl.className = "settings-status err";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function ptsLogin() {
|
|||
|
|
const msg = document.getElementById("ptsLoginMsg");
|
|||
|
|
const username = document.getElementById("ptsUsername").value.trim();
|
|||
|
|
const password = document.getElementById("ptsPassword").value;
|
|||
|
|
msg.textContent = "登入中…";
|
|||
|
|
msg.className = "settings-status";
|
|||
|
|
try {
|
|||
|
|
const data = await api("/api/pts/login", {
|
|||
|
|
method: "POST",
|
|||
|
|
body: JSON.stringify({ username, password, remember: true }),
|
|||
|
|
});
|
|||
|
|
document.getElementById("ptsPassword").value = "";
|
|||
|
|
msg.textContent = data.message || "登入成功";
|
|||
|
|
msg.className = "settings-status ok";
|
|||
|
|
await refreshStatus();
|
|||
|
|
} catch (err) {
|
|||
|
|
msg.textContent = err.message;
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function ptsLogout() {
|
|||
|
|
const msg = document.getElementById("ptsLoginMsg");
|
|||
|
|
try {
|
|||
|
|
await api("/api/pts/logout", { method: "POST" });
|
|||
|
|
msg.textContent = "已清除 session";
|
|||
|
|
msg.className = "settings-status";
|
|||
|
|
await refreshStatus();
|
|||
|
|
} catch (err) {
|
|||
|
|
msg.textContent = err.message;
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let _mfaPreviewTimer = null;
|
|||
|
|
let _mfaMetaTimer = null;
|
|||
|
|
|
|||
|
|
function vncPageUrl(port) {
|
|||
|
|
const host = window.location.hostname || "localhost";
|
|||
|
|
const p = port || 6080;
|
|||
|
|
// autoconnect + scale to fit modal
|
|||
|
|
return `http://${host}:${p}/vnc.html?autoconnect=1&resize=scale&reconnect=1`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openTeamsMfaModal(view) {
|
|||
|
|
const modal = document.getElementById("teamsMfaModal");
|
|||
|
|
if (!modal) return;
|
|||
|
|
modal.classList.remove("hidden");
|
|||
|
|
const port = (view && view.vnc_port) || 6080;
|
|||
|
|
const interactive = !!(view && view.interactive);
|
|||
|
|
const link = document.getElementById("teamsVncLink");
|
|||
|
|
const frame = document.getElementById("teamsVncFrame");
|
|||
|
|
const url = vncPageUrl(port);
|
|||
|
|
if (link) {
|
|||
|
|
link.href = url;
|
|||
|
|
link.style.display = interactive ? "" : "none";
|
|||
|
|
}
|
|||
|
|
if (frame) {
|
|||
|
|
if (interactive) {
|
|||
|
|
frame.src = url;
|
|||
|
|
frame.style.display = "";
|
|||
|
|
} else {
|
|||
|
|
frame.removeAttribute("src");
|
|||
|
|
frame.style.display = "none";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const hint = document.getElementById("teamsMfaHint");
|
|||
|
|
if (hint) {
|
|||
|
|
hint.innerHTML = interactive
|
|||
|
|
? "帳密送出後,請在下方<strong>互動畫面</strong>完成驗證(可點數字、輸入驗證碼)。瀏覽器最多保持約 <strong>10 分鐘</strong>,完成前不會關掉。"
|
|||
|
|
: "請在跳出的 <strong>Chromium 視窗</strong>完成 MFA。右側會顯示即時截圖(若有)。";
|
|||
|
|
}
|
|||
|
|
const status = document.getElementById("teamsMfaStatus");
|
|||
|
|
if (status) {
|
|||
|
|
status.textContent = (view && view.message) || "登入中…";
|
|||
|
|
status.className = "settings-status";
|
|||
|
|
}
|
|||
|
|
startMfaPreviewPoll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeTeamsMfaModal() {
|
|||
|
|
stopMfaPreviewPoll();
|
|||
|
|
const modal = document.getElementById("teamsMfaModal");
|
|||
|
|
if (modal) modal.classList.add("hidden");
|
|||
|
|
const frame = document.getElementById("teamsVncFrame");
|
|||
|
|
if (frame) {
|
|||
|
|
frame.removeAttribute("src");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function startMfaPreviewPoll() {
|
|||
|
|
stopMfaPreviewPoll();
|
|||
|
|
const img = document.getElementById("teamsMfaShot");
|
|||
|
|
const meta = document.getElementById("teamsMfaShotMeta");
|
|||
|
|
const status = document.getElementById("teamsMfaStatus");
|
|||
|
|
const tick = () => {
|
|||
|
|
if (img) {
|
|||
|
|
img.src = `/api/teams/mfa-preview?t=${Date.now()}`;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
tick();
|
|||
|
|
_mfaPreviewTimer = setInterval(tick, 1500);
|
|||
|
|
_mfaMetaTimer = setInterval(async () => {
|
|||
|
|
try {
|
|||
|
|
const view = await api("/api/teams/browser-view");
|
|||
|
|
if (status && view.login_message) {
|
|||
|
|
status.textContent = view.login_message;
|
|||
|
|
}
|
|||
|
|
if (meta) {
|
|||
|
|
const parts = [];
|
|||
|
|
if (view.mfa_preview_label) parts.push(view.mfa_preview_label);
|
|||
|
|
if (view.mfa_page_url) parts.push(view.mfa_page_url.replace(/^https?:\/\//, "").slice(0, 48));
|
|||
|
|
meta.textContent = parts.join(" · ") || "更新中";
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
/* ignore */
|
|||
|
|
}
|
|||
|
|
}, 2000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function stopMfaPreviewPoll() {
|
|||
|
|
if (_mfaPreviewTimer) {
|
|||
|
|
clearInterval(_mfaPreviewTimer);
|
|||
|
|
_mfaPreviewTimer = null;
|
|||
|
|
}
|
|||
|
|
if (_mfaMetaTimer) {
|
|||
|
|
clearInterval(_mfaMetaTimer);
|
|||
|
|
_mfaMetaTimer = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function teamsLogin() {
|
|||
|
|
const msg = document.getElementById("teamsLoginMsg");
|
|||
|
|
const username = document.getElementById("teamsUsername").value.trim();
|
|||
|
|
const password = document.getElementById("teamsPassword").value;
|
|||
|
|
if (!username || !password) {
|
|||
|
|
msg.textContent = "請輸入 Microsoft 帳號與密碼";
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
msg.textContent = "登入中…(請在 MFA 畫面完成驗證)";
|
|||
|
|
msg.className = "settings-status";
|
|||
|
|
document.getElementById("teamsEnabled").checked = true;
|
|||
|
|
|
|||
|
|
let view = {};
|
|||
|
|
try {
|
|||
|
|
view = await api("/api/teams/browser-view");
|
|||
|
|
} catch {
|
|||
|
|
view = { interactive: false, vnc_port: 6080 };
|
|||
|
|
}
|
|||
|
|
openTeamsMfaModal(view);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await api("/api/settings", {
|
|||
|
|
method: "PUT",
|
|||
|
|
body: JSON.stringify({ teams_calendar_enabled: true }),
|
|||
|
|
});
|
|||
|
|
const data = await api("/api/teams/login", {
|
|||
|
|
method: "POST",
|
|||
|
|
body: JSON.stringify({ username, password, remember: true, headless: false }),
|
|||
|
|
});
|
|||
|
|
document.getElementById("teamsPassword").value = "";
|
|||
|
|
msg.textContent = data.message || "登入成功";
|
|||
|
|
msg.className = "settings-status ok";
|
|||
|
|
const st = document.getElementById("teamsMfaStatus");
|
|||
|
|
if (st) {
|
|||
|
|
st.textContent = data.message || "登入成功";
|
|||
|
|
st.className = "settings-status ok";
|
|||
|
|
}
|
|||
|
|
await refreshStatus();
|
|||
|
|
setTimeout(closeTeamsMfaModal, 1500);
|
|||
|
|
} catch (err) {
|
|||
|
|
msg.textContent = err.message;
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
const st = document.getElementById("teamsMfaStatus");
|
|||
|
|
if (st) {
|
|||
|
|
st.textContent = err.message;
|
|||
|
|
st.className = "settings-status err";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function teamsLogout() {
|
|||
|
|
const msg = document.getElementById("teamsLoginMsg");
|
|||
|
|
try {
|
|||
|
|
await api("/api/teams/logout", { method: "POST" });
|
|||
|
|
msg.textContent = "已中斷";
|
|||
|
|
msg.className = "settings-status";
|
|||
|
|
await refreshStatus();
|
|||
|
|
} catch (err) {
|
|||
|
|
msg.textContent = err.message;
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function escapeHtml(text) {
|
|||
|
|
return String(text ?? "")
|
|||
|
|
.replace(/&/g, "&")
|
|||
|
|
.replace(/</g, "<")
|
|||
|
|
.replace(/>/g, ">")
|
|||
|
|
.replace(/"/g, """);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function initTeamsTestDates() {
|
|||
|
|
const today = new Date().toISOString().slice(0, 10);
|
|||
|
|
const startEl = document.getElementById("teamsTestStart");
|
|||
|
|
const endEl = document.getElementById("teamsTestEnd");
|
|||
|
|
if (startEl && !startEl.value) startEl.value = today;
|
|||
|
|
if (endEl && !endEl.value) endEl.value = today;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function teamsShowStatus() {
|
|||
|
|
const msg = document.getElementById("teamsTestMsg");
|
|||
|
|
const debug = document.getElementById("teamsTestDebug");
|
|||
|
|
try {
|
|||
|
|
const data = await api("/api/teams/status");
|
|||
|
|
if (msg) {
|
|||
|
|
msg.textContent = data.connected
|
|||
|
|
? `連線中(${data.auth_mode || "session"})`
|
|||
|
|
: "未連線或 session 過期,請先登入 Teams";
|
|||
|
|
msg.className = `settings-status ${data.connected ? "ok" : "err"}`;
|
|||
|
|
}
|
|||
|
|
if (debug) debug.textContent = JSON.stringify(data, null, 2);
|
|||
|
|
} catch (err) {
|
|||
|
|
if (msg) {
|
|||
|
|
msg.textContent = err.message;
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function teamsTestMeetings() {
|
|||
|
|
const msg = document.getElementById("teamsTestMsg");
|
|||
|
|
const summary = document.getElementById("teamsTestSummary");
|
|||
|
|
const list = document.getElementById("teamsTestList");
|
|||
|
|
const debug = document.getElementById("teamsTestDebug");
|
|||
|
|
const start = document.getElementById("teamsTestStart")?.value;
|
|||
|
|
const end = document.getElementById("teamsTestEnd")?.value || start;
|
|||
|
|
const skip = document.getElementById("teamsTestSkipHolidays")?.checked !== false;
|
|||
|
|
|
|||
|
|
if (!start) {
|
|||
|
|
if (msg) {
|
|||
|
|
msg.textContent = "請選開始日期";
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg) {
|
|||
|
|
msg.textContent = "測試中…(可能開瀏覽器做 MFA)";
|
|||
|
|
msg.className = "settings-status";
|
|||
|
|
}
|
|||
|
|
if (list) list.innerHTML = "";
|
|||
|
|
if (summary) summary.innerHTML = "";
|
|||
|
|
if (debug) debug.textContent = "請求中…";
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const q = new URLSearchParams({
|
|||
|
|
start_date: start,
|
|||
|
|
end_date: end,
|
|||
|
|
skip_holidays: skip ? "true" : "false",
|
|||
|
|
debug: "true",
|
|||
|
|
});
|
|||
|
|
const data = await api(`/api/teams/meetings?${q.toString()}`);
|
|||
|
|
|
|||
|
|
if (summary) {
|
|||
|
|
summary.innerHTML = [
|
|||
|
|
`<span class="pill"><span class="pill-label">會議</span><span class="pill-value">${data.total_count ?? 0}</span></span>`,
|
|||
|
|
`<span class="pill"><span class="pill-label">時數</span><span class="pill-value">${data.total_hours ?? 0}h</span></span>`,
|
|||
|
|
`<span class="pill"><span class="pill-label">天數</span><span class="pill-value">${data.workday_count ?? 0}</span></span>`,
|
|||
|
|
`<span class="pill"><span class="pill-label">連線</span><span class="pill-value">${data.connected ? "是" : "否"}</span></span>`,
|
|||
|
|
].join("");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const days = (data.days || []).filter((d) => d.count > 0);
|
|||
|
|
if (list) {
|
|||
|
|
if (!days.length) {
|
|||
|
|
list.innerHTML = `<p class="hint">${escapeHtml(data.fetch_hint || data.error || "0 筆會議")}</p>`;
|
|||
|
|
} else {
|
|||
|
|
list.innerHTML = days
|
|||
|
|
.map((day) => {
|
|||
|
|
const rows = (day.meetings || [])
|
|||
|
|
.map(
|
|||
|
|
(m) =>
|
|||
|
|
`<li class="plan-row"><span class="meeting-hours">${escapeHtml(String(m.hours))}h</span>` +
|
|||
|
|
`<span class="meeting-time">${escapeHtml(m.time_label || "")}</span>` +
|
|||
|
|
`<span class="meeting-subject">${escapeHtml(m.subject || "")}</span></li>`
|
|||
|
|
)
|
|||
|
|
.join("");
|
|||
|
|
return `<div class="plan-day"><div class="plan-day-head"><strong>${escapeHtml(day.date)}</strong>` +
|
|||
|
|
`<span class="muted-inline">${day.count} 場 · ${day.hours}h</span></div>` +
|
|||
|
|
`<ul class="meeting-items">${rows}</ul></div>`;
|
|||
|
|
})
|
|||
|
|
.join("");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg) {
|
|||
|
|
if (data.error) {
|
|||
|
|
msg.textContent = data.error;
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
} else if ((data.total_count || 0) > 0) {
|
|||
|
|
msg.textContent = `成功:${data.total_count} 場會議 · ${data.total_hours}h`;
|
|||
|
|
msg.className = "settings-status ok";
|
|||
|
|
} else {
|
|||
|
|
msg.textContent = data.fetch_hint || "完成但 0 筆會議";
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (debug) debug.textContent = JSON.stringify(data, null, 2);
|
|||
|
|
} catch (err) {
|
|||
|
|
if (msg) {
|
|||
|
|
msg.textContent = err.message;
|
|||
|
|
msg.className = "settings-status err";
|
|||
|
|
}
|
|||
|
|
if (debug) debug.textContent = err.message;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.getElementById("settingsForm").addEventListener("submit", saveSettings);
|
|||
|
|
document.getElementById("ptsLoginBtn").addEventListener("click", ptsLogin);
|
|||
|
|
document.getElementById("ptsLogoutBtn").addEventListener("click", ptsLogout);
|
|||
|
|
document.getElementById("teamsLoginBtn").addEventListener("click", teamsLogin);
|
|||
|
|
document.getElementById("teamsLogoutBtn").addEventListener("click", teamsLogout);
|
|||
|
|
document.getElementById("teamsTestBtn")?.addEventListener("click", () => {
|
|||
|
|
teamsTestMeetings().catch(console.error);
|
|||
|
|
});
|
|||
|
|
document.getElementById("teamsStatusBtn")?.addEventListener("click", () => {
|
|||
|
|
teamsShowStatus().catch(console.error);
|
|||
|
|
});
|
|||
|
|
document.getElementById("teamsMfaClose")?.addEventListener("click", () => {
|
|||
|
|
// Only hide preview; login request keeps running until MFA done or timeout
|
|||
|
|
closeTeamsMfaModal();
|
|||
|
|
});
|
|||
|
|
document.getElementById("teamsMfaShot")?.addEventListener("error", () => {
|
|||
|
|
/* 404 until first snapshot — ignore */
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
initTeamsTestDates();
|
|||
|
|
loadSettings().then(refreshStatus).catch(console.error);
|