/// // The CHECKING project. A different Apps Script project, installed by a different // account, with read-only access to the hiring workbook and the journal. It never // sends a welcome letter; it emails the people named in its alerts. import { composeMorningSummary, emptyCheckState, markNotified, renderAlertNote, runCheck, type CheckResult, type CheckState } from "../core/monitor"; import { planTriggers } from "../core/triggers"; import type { CheckConfig } from "../core/types"; import { authorizationStatus, formatter, installExactly, prop, readGrid, readJournal, tab, TABS } from "./common"; const MANAGED = ["runCheck", "morningSummary"]; function config(): CheckConfig { return { staleAfterMinutes: Number(prop("STALE_AFTER_MINUTES", "40")), pendingAfterMinutes: Number(prop("PENDING_AFTER_MINUTES", "60")), contacts: { technicalOwner: prop("TECHNICAL_OWNER"), hiringCoordinator: prop("HIRING_COORDINATOR") }, }; } function loadState(): CheckState { const raw = PropertiesService.getScriptProperties().getProperty("CHECK_STATE"); return raw ? (JSON.parse(raw) as CheckState) : emptyCheckState(); } function saveState(st: CheckState) { PropertiesService.getScriptProperties().setProperty("CHECK_STATE", JSON.stringify(st)); } function check(state: CheckState): CheckResult { const r = runCheck( { readHiring: () => readGrid(tab(prop("HIRING_BOOK_ID"), TABS.hiring)), readJournal: () => readJournal(prop("OPS_BOOK_ID")), }, config(), new Date(), state, formatter(Session.getScriptTimeZone()), ); PropertiesService.getScriptProperties().setProperty( "LAST_CHECK", JSON.stringify({ at: r.checkedAt, status: r.status, alerts: r.alerts.length }), ); return r; } function withLock(f: () => T): T | { skipped: string } { const lock = LockService.getScriptLock(); if (!lock.tryLock(20000)) return { skipped: "another check is running" }; try { return f(); } finally { lock.releaseLock(); } } /** * Every 15 minutes: send each open alert whose note has not been accepted yet. * An alert counts as notified only after MailApp accepted its note; a failed send is retried next time. */ export function periodicCheck() { return withLock(() => { const r = check(loadState()); let state: CheckState = { firstSeen: r.firstSeen, notified: r.notified }; const fmt = formatter(Session.getScriptTimeZone()); const emailed: string[] = []; const failed: string[] = []; for (const a of r.toNotify) { const note = renderAlertNote(a, r.checkedAt, fmt); try { MailApp.sendEmail({ to: prop("ALERT_TO"), subject: note.subject, body: note.body }); state = markNotified(state, [a.key], new Date().toISOString()); emailed.push(a.kind); } catch (e) { failed.push(`${a.kind}: ${(e as Error).message}`); } saveState(state); } saveState(state); return { status: r.status, alerts: r.alerts.map((a) => a.kind), emailed, failed }; }); } /** Once a day: the summary. It never marks an alert as notified. */ export function dailySummary() { return withLock(() => { const r = check(loadState()); saveState({ firstSeen: r.firstSeen, notified: r.notified }); const s = composeMorningSummary(r, formatter(Session.getScriptTimeZone())); MailApp.sendEmail({ to: prop("SUMMARY_TO", prop("ALERT_TO")), subject: s.subject, body: s.body }); return { status: r.status }; }); } export function install() { const desired = [ { handler: "runCheck", kind: "every-minutes" as const, value: Number(prop("CHECK_EVERY_MINUTES", "15")) }, { handler: "morningSummary", kind: "daily-at-hour" as const, value: Number(prop("SUMMARY_HOUR", "7")) }, ]; return installExactly(desired, (existing, installed) => planTriggers(existing, desired, MANAGED, undefined, installed)); } export function verify() { return { authorization: authorizationStatus(), triggers: ScriptApp.getProjectTriggers().map((t) => t.getHandlerFunction()).join(", ") || "none", lastCheck: PropertiesService.getScriptProperties().getProperty("LAST_CHECK") ?? "never", }; }