Skip to content

#549 Manager Daily Digest - #717

Open
b-at-neu wants to merge 10 commits into
devfrom
549-manager-daily-digest
Open

b-at-neu wants to merge 10 commits into
devfrom
549-manager-daily-digest

Conversation

@b-at-neu

@b-at-neu b-at-neu commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #549

SESSION REQUIRED: touches CLAUDE.md — a dispatched agent can't edit it

Summary

  • Adds two Vercel Cron routes — daily and weekly manager digests — each a static, single-purpose endpoint rather than one param-dispatched job runner.
  • Daily windows since each manager's own last digest (any status; a failed run isn't retried), falling back to 24h for a manager's first-ever digest — gap-safe against a missed/delayed cron fire, and in steady daily operation indistinguishable from a fixed 24h window. Reports new applications per managed position.
  • Weekly is a pure reminder, not a recap: it has no time window of its own and reports every application still short of a terminal status (not accepted/rejected/withdrawn/draft) across the manager's positions, as a status-count box per status (colored to match the in-app badge palette), plus their currently-open positions as context. Sent only when that total is greater than zero. This is a design change from the originally-agreed plan (which specified a "new this week" total plus a full status breakdown including terminal decisions) — see the follow-up comment on Manager Daily Digest #549 recording why and what shipped instead.
  • Idempotency has no new column: the weekly gate checks for an EmailLog row already existing in the current org week; the daily window is itself the idempotency mechanism (see above).
  • Renames EmailTemplateKey.manager_digest (shipped in Email Foundation And Send Log #547, never written) to manager_daily_digest and adds manager_weekly_digest.
  • Appends both routes to CLAUDE.md's API-route allowlist (rewritten by Ingest Resend Delivery Events #569) with the bounding clause that they are not a general job runner.

Changes

  • prisma/schema.prisma, prisma/migrations/20260911222818_manager_digest_cadences/ — enum rename + add, hand-written ALTER TYPE ... RENAME VALUE / ADD VALUE (data-preserving).
  • lib/dates.tspreviousOrgDay, currentOrgWeekStart (org calendar-date arithmetic, DST-safe); previousOrgWeek was added then removed once the weekly digest stopped needing a week-bounded query.
  • lib/constants.ts — renamed/added EMAIL_TEMPLATE_VALUES/EMAIL_TEMPLATE_LABELS entries, MANAGER_DIGEST_SEND_SPACING_MS, DAILY_DIGEST_LOOKBACK_MS, reworded complained ("recipient" not "applicant").
  • lib/utils.tsgetEmailLogDescription's two bounce sentences reworded to "recipient".
  • lib/types.tsManagerDigestPosition, DailyDigestRecipient (since: Date), WeeklyDigestStatusCount, WeeklyDigestRecipient (asOfDay: string, unresolved-only statusCounts).
  • prisma/data/digests.ts — new; getDailyDigestRecipients fetches raw application rows and buckets per manager by their own since cutoff (can't pre-aggregate until each manager's cutoff is known); getWeeklyDigestRecipients tallies only UNRESOLVED_APPLICATION_STATUSES, no time bound. Both scoped by Position.managers only (never the admin-short-circuiting buildReviewablePositionWhere).
  • lib/email/templates.tsmanagerDailyDigestEmail (names the exact since-instant), managerWeeklyDigestEmail (table-based colored stat-box grid per status, matching the in-app badge palette), MANAGER_EMAIL_FOOTER.
  • lib/email/manager-digests.ts — new; dispatchDailyManagerDigests/dispatchWeeklyManagerDigests, the second named swallow site (per-recipient try/catch, spaced by MANAGER_DIGEST_SEND_SPACING_MS).
  • lib/cron.tsrejectUnauthorizedCron, the bearer-secret check both routes call before any DB access.
  • app/api/cron/manager-daily-digest/route.ts, app/api/cron/manager-weekly-digest/route.ts — new.
  • vercel.json — the two crons entries.
  • CLAUDE.md, docs/ENGINEERING.md, docs/PERMISSIONS.md, docs/WORKFLOWS.md, README.md, .env.example — allowlist, logging swallow site, route-access note, XC-10 workflow entry + PM-8/AD-12 clauses, CRON_SECRET docs.
  • tests/unit/dates.test.ts, tests/unit/email-templates.test.ts, tests/db/manager-digests.test.ts — new coverage; tests/db/email-log-queries.test.ts, tests/unit/utils.test.ts — updated for the renamed enum value and reworded copy.

Testing plan

  • Set CRON_SECRET in the Vercel preview env and redeploy (also needs RESEND_API_KEY/RESEND_FROM_EMAIL, already set).
  • curl -i <preview>/api/cron/manager-daily-digest with no header, then a wrong one → 401 {"error":"Unauthorized"} both times; repeat for …/manager-weekly-digest; /emails shows no new rows.
  • As a manager M with two open positions, submit two applications to the first and one to the second.
  • curl -H "Authorization: Bearer $CRON_SECRET" <preview>/api/cron/manager-daily-digest{"sent":1,...}; M gets one email, subject 3 new applications across 2 positions, both positions listed with the right counts, body naming the exact since-instant.
  • Click each position link → /manage/applications?positionId=… with the new submissions on top.
  • Re-run the same curl immediately → {"sent":0,...,"skipped":1} and no second email.
  • Submit one more application, re-run → this time it is included (proves the gap-safe "since last digest" window, not a calendar-day exclusion).
  • A second manager N with no new activity gets nothing on the daily run.
  • With N having some applications still applied/reviewing (any age) and none accepted/rejected/withdrawn/draft, curl … /api/cron/manager-weekly-digest → one email with a stat box per unresolved status and the open-positions list.
  • Re-run the weekly curl → skipped: 1; re-run the daily one → unaffected by the weekly row.
  • Give N only terminal-status applications (accepted/rejected) → the weekly run sends nothing to N.
  • /emails as an admin → Manager daily digest/Manager weekly digest rows addressed to M/N; both filterable by template; neither appears in any application's Email history.
  • Deactivate M, submit another application, re-run both → no digest for M.
  • Read both emails in a phone client and Gmail web — rows stay readable at ~320px; each link text reads meaningfully on its own; the weekly stat boxes render (or gracefully degrade to square, undecorated boxes in Outlook).

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test — 781 passed (42 files), run against a local throwaway Postgres container since the worktree's default port 5432 was occupied by an unrelated container
  • npm run build — pass; both cron routes appear as dynamic (ƒ) routes

Notes

  • Both the daily-window and weekly-content redesigns were direct operator feedback during review, not part of the original agreed plan — see the follow-up comment on Manager Daily Digest #549 for the record of what changed and why.
  • Vercel Cron only fires on production deployments, so previews are verified by the manual curls above.
  • Cron schedules are UTC, so the local send time shifts an hour across DST (08:00 EDT / 07:00 EST) — deliberate; unaffected by the windowing change, since the daily window is now relative to each manager's own last digest rather than the org calendar.
  • A failed digest is not retried — the daily window and weekly gate both key off the last EmailLog row (any status), so a run that errors just gets picked up in full by the next one for the daily digest, or waits a full week for the weekly one; the failed row on /emails is the record either way.
  • Genuinely simultaneous calls are the unguarded case; Vercel Cron doesn't overlap runs and the bearer secret bounds who else can call, so no advisory lock was added.
  • submittedAt, not ApplicationStatusEvent, is the "received" source for the daily digest — refreshed on a withdrawn → applied resubmission, excludes since-withdrawn rows, unaffected by a reviewer overriding a status back to applied.

@b-at-neu b-at-neu added the claude Will be worked on by Claude label Sep 12, 2026
@b-at-neu b-at-neu self-assigned this Sep 12, 2026
@vercel

vercel Bot commented Sep 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
aplio Ready Ready Preview Sep 12, 2026 3:28pm UTC

@b-at-neu b-at-neu added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 12, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 1 · needs revision

3 open — 3 🟡 Low (see inline)

Comment thread prisma/data/digests.ts Outdated
Comment thread prisma/data/digests.ts Outdated
Comment thread lib/email/manager-digests.ts
Comment thread lib/email/templates.ts Outdated
@b-at-neu b-at-neu added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Sep 12, 2026
@b-at-neu
b-at-neu force-pushed the 549-manager-daily-digest branch from 577c376 to 9699d22 Compare September 12, 2026 04:04
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-L1, R1-L2, R1-L3 · 9699d22

@b-at-neu b-at-neu added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed revising Revise agent working (in-flight) ready for review PR ready for review agent labels Sep 12, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 2 · needs revision

1 open — 1 🔴 Critical (see inline)

Comment thread lib/email/templates.ts Outdated
@b-at-neu b-at-neu added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Sep 12, 2026
b-at-neu and others added 7 commits September 12, 2026 00:13
manager_digest shipped in #547 but nothing ever wrote it. Renaming it
now and adding manager_weekly_digest is data-preserving since the
column is still unused.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
previousOrgDay, currentOrgWeekStart and previousOrgWeek do their
arithmetic on the org calendar date, never a resolved instant, so a
DST transition inside the shifted range can't shift the day count.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
getDailyDigestRecipients/getWeeklyDigestRecipients compute recipients
from EmailLog alone (no new column) — a manager already digested
inside the current calendar day/week is dropped and counted as
skipped, so a repeat cron fire is a no-op.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Two static routes, not one route with a mode param — each names and
runs exactly the one digest its path names, so it stays a falsifiable
allowlist line rather than a job runner. Both reject any request whose
Authorization header isn't the CRON_SECRET bearer token before
touching the database.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Appends both cron routes to CLAUDE.md's allowlist per #569's shape,
and updates ENGINEERING/PERMISSIONS/WORKFLOWS for the new swallow
site, the machine-authenticated route, and the two digest workflows.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The weekly email no longer tallies "new applications this week" — a
manager gets it only when they have applications still short of a
terminal status, with a status-count box per status (color-coded like
the in-app badges) instead of a text recap. previousOrgWeek has no
remaining caller and is removed along with it.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Weekly digest redesign (operator feedback)

Per direct feedback from the operator (not a formal review cycle):

  • The weekly email no longer counts "new applications this week." It now sends only when a manager has applications still short of a terminal status (accepted/rejected/withdrawn/draft excluded), regardless of how long they've been outstanding — a pure "you have review work waiting" reminder.
  • Status counts now render as colored stat boxes (matching the in-app StatCard/badge palette: blue for applied/reached_out/interview_scheduled, amber for reviewing) instead of a plain text list.
  • Subject is now "N applications awaiting your review".
  • Confirmed unchanged: weekly still sends Mondays at 13:00 UTC (9am ET); daily digest still windows to the previous org calendar day by design (a submission from "just now" won't appear until the next day's run).
  • previousOrgWeek (lib/dates.ts) had no remaining caller after this change and was removed along with its tests.

74284bc on top of the earlier c72e616 fix. All 780 tests pass, prettier/eslint/tsc clean.

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 12, 2026
@b-at-neu b-at-neu added reviewing Review agent working (in-flight) revising Revise agent working (in-flight) and removed ready for review PR ready for review agent reviewing Review agent working (in-flight) labels Sep 12, 2026
Replaces the previous-org-calendar-day window with a gap-safe one
anchored to each manager's own last digest (24h fallback for a
first-ever digest). A missed or delayed cron fire can no longer drop
an application, and in steady daily operation the window is still
always ~24h. The email now names the exact since-instant rather than
a calendar day, since the window can span more than a day after a gap.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Daily digest windowing redesign (operator feedback)

Per direct feedback, the daily digest no longer windows to the previous org calendar day. It now windows since each manager's own last digest (any status, success or failure — a failed run still isn't retried, same philosophy as before), falling back to DAILY_DIGEST_LOOKBACK_MS (24h) for a manager's first-ever digest.

  • Gap-safe: a missed or delayed cron fire can no longer drop an application permanently — the next run just looks back further for that manager.
  • In steady daily operation this is indistinguishable from the old ~24h window.
  • No more same-day exclusion: an application submitted minutes ago now shows up on the very next call, rather than requiring a calendar-day rollover — this was the operator's original testing friction.
  • The "gate" is no longer a separate boolean — a manager whose window (since their last digest) yields zero new applications just has an empty digest, whether that's genuinely no activity or a repeat call moments after a successful send. Both collapse into skipped.
  • Email copy now names the exact since-instant (formatInstant, e.g. "since Mar 9, 2026, 8:00 AM EST") instead of a single calendar day, since the window can span more than a day after a gap.

0bf327a on top of 74284bc/c72e616. All 781 tests pass, prettier/eslint/tsc clean.

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 12, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 4 · needs revision

2 open — 1 🟠 Medium, 1 🟡 Low (see inline)

Comment thread prisma/data/digests.ts
Comment thread lib/email/templates.ts
@b-at-neu b-at-neu added the needs revision Review found issues that need fixing label Sep 12, 2026
@b-at-neu b-at-neu added revising Revise agent working (in-flight) and removed ready for review PR ready for review agent needs revision Review found issues that need fixing labels Sep 12, 2026
@b-at-neu b-at-neu mentioned this pull request Sep 12, 2026
40 tasks
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 4

fixed R4-M1, R4-L1 · skipped R3-L1 (moot — flagged code removed in 74284bc) · 7e025be

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 12, 2026
@b-at-neu b-at-neu added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 12, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 5 · approved

0 open

@b-at-neu b-at-neu added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Review passed, ready to merge claude Will be worked on by Claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Manager Daily Digest

1 participant