mise ~/.config/mise/config.toml tools: crush@0.91.0

feat(api): add React Email rendering and PNG heatmap endpoint
This commit is contained in:
Prad Nukala committed 2026-08-25 10:33:04 -04:00
1 parent 6b9a259ec7
commit 881530a808
8 files changed
+1058 -168

No files matched your search

+74 -120
View File
@@ -1,10 +1,13 @@
/**
* The daily digest email — built from D1 + the bundled schedule, sent at
* 8 AM ET via the send_email binding. Plain inline-styled HTML that renders
* clean in Gmail mobile; always sent with a text alternative.
* 8 AM ET via the send_email binding. This module owns data only: it turns
* D1 rows into a `DigestData` and hands it to email.tsx, which renders both
* the HTML and the plain-text alternative from that single tree (React Email
* `render`), so the two can never drift.
*
* Retrieval rules: review and drill lines carry number + difficulty only —
* never the topic, never a solution link. The learning day's core list is
* Retrieval rules: review and drill rows carry number + difficulty only —
* never the topic, never a solution link. `DigestRow` has no field for
* either, so the rule holds by construction. The learning day's core list is
* the only labeled section. Reviews + drills ≤ 6, reviews first, overflow
* simply stays due (oldest tomorrow). Sunday is a two-line rest note.
*
@@ -26,62 +29,58 @@ import {
streak,
weekdayOf,
} from "./srs.ts";
import { type DigestData, type DigestRow, renderDigest } from "./email.tsx";
import { signLink } from "./links.ts";
const DAILY_CAP = 6;
function cap(difficulty: string): string {
return difficulty[0]!.toUpperCase() + difficulty.slice(1);
}
export interface Digest {
subject: string;
html: string;
text: string;
}
/** One-tap pass/fail anchor pair for a problem line. */
async function tapLinks(env: Env, lc: number, date: string): Promise<string> {
const pass = await signLink(env.LINK_KEY, lc, "pass", date);
const fail = await signLink(env.LINK_KEY, lc, "fail", date);
const url = (r: string, sig: string) =>
`${env.PUBLIC_URL}/log?p=${lc}&r=${r}&d=${date}&sig=${sig}`;
return (
`<a href="${url("pass", pass)}" style="color:#1a7f37;font-weight:bold">pass</a>` +
` · <a href="${url("fail", fail)}" style="color:#cf222e;font-weight:bold">fail</a>`
);
/** A problem row plus its signed one-tap pass/fail URLs. */
async function tapRow(env: Env, p: ProblemRow, date: string, staged: boolean): Promise<DigestRow> {
const [pass, fail] = await Promise.all([
signLink(env.LINK_KEY, p.lc_number, "pass", date),
signLink(env.LINK_KEY, p.lc_number, "fail", date),
]);
const base = `${env.PUBLIC_URL}/log?p=${p.lc_number}&d=${date}&r=`;
return {
lc: p.lc_number,
difficulty: p.difficulty,
...(staged ? { stage: p.stage } : {}),
passUrl: `${base}pass&sig=${pass}`,
failUrl: `${base}fail&sig=${fail}`,
};
}
export async function buildDigest(env: Env, date: string): Promise<Digest> {
/** Everything the email needs, read straight out of D1 and the schedule. */
export async function collectDigest(env: Env, date: string): Promise<DigestData> {
const db = env.DB;
const day = campaignDay(date);
const week = campaignWeek(date);
const header = `${prettyDate(date)} — Day ${day}/${CAMPAIGN_DAYS} · Week ${week}`;
const rest = weekdayOf(date) === 0;
if (weekdayOf(date) === 0) {
return {
subject: `SRS — ${prettyDate(date)} — rest day`,
html: `<p><strong>${header}</strong></p><p>Rest day. Nothing is due. Overdue reviews moved to Monday.</p>`,
text: `${header}\nRest day. Nothing is due. Overdue reviews moved to Monday.`,
};
}
// Reviews take the cap first; drills fill whatever is left.
const due = rest ? [] : await dueReviews(db, date);
const capped = due.slice(0, DAILY_CAP);
const drills = rest ? [] : await pickDrills(db, date, DAILY_CAP - capped.length);
const due = await dueReviews(db, date);
const reviews = due.slice(0, DAILY_CAP);
const carried = due.length - reviews.length;
const drills = await pickDrills(db, date, DAILY_CAP - reviews.length);
const currentStreak = await streak(db, date);
const data: DigestData = {
day: prettyDate(date),
progress: `Day ${campaignDay(date)} of ${CAMPAIGN_DAYS} · Week ${campaignWeek(date)}`,
streak: await streak(db, date),
rest,
reviews: await Promise.all(capped.map((p) => tapRow(env, p, date, true))),
carried: due.length - capped.length,
drills: await Promise.all(drills.map((p) => tapRow(env, p, date, false))),
yesterday: { total: 0, failed: [] },
heatmapUrl: `${env.PUBLIC_URL}/chart/heatmap.png`,
progressUrl: `${env.DOCS_URL}/progress`,
};
const htmlParts: string[] = [];
const textParts: string[] = [];
htmlParts.push(
`<h2 style="margin:0 0 4px">${header}</h2>`,
`<p style="margin:0 0 16px;color:#57606a">🔥 streak: ${currentStreak} day${currentStreak === 1 ? "" : "s"}</p>`,
);
textParts.push(header, `streak: ${currentStreak}`);
// New topic — the only labeled section.
const topicIssue = SCHEDULE[date];
// New topic — the only section allowed to name problems and link them.
const topicIssue = rest ? undefined : SCHEDULE[date];
if (topicIssue !== undefined) {
const topic = await db
.prepare("SELECT name FROM topics WHERE issue = ?")
@@ -93,57 +92,14 @@ export async function buildDigest(env: Env, date: string): Promise<Digest> {
)
.bind(topicIssue)
.all<ProblemRow>();
const links = core
.map(
(p) =>
`<a href="https://github.com/${env.REPO}/issues/${p.issue}">LC ${p.lc_number}</a>${p.stage === "new" ? "" : " ✓"}`,
)
.join(" · ");
htmlParts.push(
`<h3 style="margin:16px 0 4px">New topic: ${topic?.name ?? `#${topicIssue}`}</h3>`,
`<p style="margin:0">Core: ${links}</p>`,
);
textParts.push(`New topic: ${topic?.name ?? topicIssue} — core: ${core.map((p) => `LC ${p.lc_number}`).join(", ")}`);
}
// Reviews due — unlabeled lines, one-tap links.
htmlParts.push(`<h3 style="margin:16px 0 4px">Reviews due (${reviews.length})</h3>`);
textParts.push(`Reviews due (${reviews.length})`);
if (reviews.length) {
htmlParts.push(
`<p style="margin:0 0 4px;color:#57606a">Solve each from scratch. Do not open your old solution first.</p>`,
);
for (const p of reviews) {
htmlParts.push(
`<p style="margin:2px 0">LC ${p.lc_number} — ${cap(p.difficulty)} — stage ${p.stage} — ${await tapLinks(env, p.lc_number, date)}</p>`,
);
textParts.push(`- LC ${p.lc_number} — ${cap(p.difficulty)} — stage ${p.stage}`);
}
if (carried > 0) {
htmlParts.push(`<p style="margin:4px 0;color:#57606a">${carried} more carried to tomorrow (cap ${DAILY_CAP}).</p>`);
textParts.push(`${carried} more carried to tomorrow.`);
}
} else {
htmlParts.push(`<p style="margin:0">None.</p>`);
textParts.push("none");
}
// Blind drills — unlabeled.
htmlParts.push(`<h3 style="margin:16px 0 4px">Blind drills (${drills.length})</h3>`);
textParts.push(`Blind drills (${drills.length})`);
if (drills.length) {
htmlParts.push(
`<p style="margin:0 0 4px;color:#57606a">No topic given. Name the pattern out loud before you code.</p>`,
);
for (const p of drills) {
htmlParts.push(
`<p style="margin:2px 0">LC ${p.lc_number} — ${cap(p.difficulty)} — ${await tapLinks(env, p.lc_number, date)}</p>`,
);
textParts.push(`- LC ${p.lc_number} — ${cap(p.difficulty)}`);
}
} else {
htmlParts.push(`<p style="margin:0">None today.</p>`);
textParts.push("none");
data.topic = {
name: topic?.name ?? `#${topicIssue}`,
core: core.map((p) => ({
lc: p.lc_number,
url: `https://github.com/${env.REPO}/issues/${p.issue}`,
solved: p.stage !== "new",
})),
};
}
// Saturday: the review issue already exists (created at midnight ET).
@@ -153,40 +109,38 @@ export async function buildDigest(env: Env, date: string): Promise<Digest> {
.bind(isoWeek(date))
.first<{ issue: number }>();
if (gate) {
htmlParts.push(
`<h3 style="margin:16px 0 4px">Saturday gate</h3>`,
`<p style="margin:0"><a href="https://github.com/${env.REPO}/issues/${gate.issue}">Review — Week ${campaignWeek(date)} (#${gate.issue})</a> — timed, blind, then close the issue.</p>`,
);
textParts.push(`Saturday gate: https://github.com/${env.REPO}/issues/${gate.issue}`);
data.gate = {
week: campaignWeek(date),
url: `https://github.com/${env.REPO}/issues/${gate.issue}`,
};
}
}
// Footer: yesterday's log, gate rate to date, progress page.
const yesterday = addDays(date, -1);
// Footer: yesterday's log summarised (failures named — they are the
// actionable part) and the gate rate to date.
const { results: logged } = await db
.prepare("SELECT lc_number, kind, result FROM attempts WHERE date = ? ORDER BY id")
.bind(yesterday)
.all<{ lc_number: number; kind: string; result: string }>();
.prepare("SELECT lc_number, result FROM attempts WHERE date = ? ORDER BY id")
.bind(addDays(date, -1))
.all<{ lc_number: number; result: string }>();
data.yesterday = {
total: logged.length,
failed: logged.filter((a) => a.result !== "pass").map((a) => a.lc_number),
};
const lastGate = await db
.prepare("SELECT pass_rate FROM gates WHERE pass_rate IS NOT NULL ORDER BY week DESC LIMIT 1")
.first<{ pass_rate: number }>();
const footerBits = [
logged.length
? `Yesterday: ${logged.map((a) => `LC ${a.lc_number} ${a.result === "pass" ? "✅" : "❌"}`).join(" · ")}`
: "Yesterday: no attempts logged.",
lastGate ? `Gate pass rate: ${Math.round(lastGate.pass_rate * 100)}%` : "No gates scored yet.",
];
htmlParts.push(
`<hr style="border:none;border-top:1px solid #d0d7de;margin:16px 0">`,
`<p style="margin:0;color:#57606a;font-size:13px">${footerBits.join(" · ")} · <a href="${env.DOCS_URL}/progress">progress page</a></p>`,
);
textParts.push(...footerBits, `progress: ${env.DOCS_URL}/progress`);
if (lastGate) data.gateRate = lastGate.pass_rate;
const load = reviews.length + drills.length;
return data;
}
export async function buildDigest(env: Env, date: string): Promise<Digest> {
const data = await collectDigest(env, date);
const { html, text } = await renderDigest(data);
return {
subject: `SRS — ${prettyDate(date)} — ${load} on deck`,
html: `<div style="font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;max-width:600px;margin:0 auto;padding:12px">${htmlParts.join("\n")}</div>`,
text: textParts.join("\n"),
subject: `(Day ${campaignDay(date)}/${CAMPAIGN_DAYS}) LeetCode Daily Digest`,
html,
text,
};
}