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 ? "帳密送出後,請在下方互動畫面完成驗證(可點數字、輸入驗證碼)。瀏覽器最多保持約 10 分鐘,完成前不會關掉。" : "請在跳出的 Chromium 視窗完成 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, """); } 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 = [ `會議${data.total_count ?? 0}`, `時數${data.total_hours ?? 0}h`, `天數${data.workday_count ?? 0}`, `連線${data.connected ? "是" : "否"}`, ].join(""); } const days = (data.days || []).filter((d) => d.count > 0); if (list) { if (!days.length) { list.innerHTML = `
${escapeHtml(data.fetch_hint || data.error || "0 筆會議")}
`; } else { list.innerHTML = days .map((day) => { const rows = (day.meetings || []) .map( (m) => `