///
// 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",
};
}