#775 Show Managers Activity When Their Position Is Opened - #776
Conversation
Records every position status change, mirroring ApplicationStatusEvent, so the activity panel can later show "position opened" items. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wraps the status write in a transaction with a compare-and-swap on the prior status, so a concurrent change surfaces a clear error instead of silently overwriting, and writes a PositionStatusEvent per real move. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
getRecentPositionOpenings reads PositionStatusEvent rows scoped like the existing application feed; getActivityGroups merges them in by time and caps the combined reviewer group at 10. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A row with an href renders as a Link wrapped in SheetClose so opening it also closes the panel; plain application rows are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers scoping (manager, other manager, applicant, admin), reopen vs close, returning to draft, and merge-and-cap behavior in getActivityGroups, plus the event write itself in updatePositionStatus. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 1 · needs revision
3 open — 3 🟡 Low (see inline)
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 1 · operator addendum · needs revision
1 open (operator) + the 3 🟡 Low inline findings (R1-L1, R1-L2, R1-L3) from the Cycle 1 review above. Address all four in this revision.
R1-M1 (Medium, operator scope addition): show position closes in the activity panel as well as opens.
- Manual close: when a position's status changes to
closed(admin or manager, per the existing transition rules), show "<title> was closed". This comes from thePositionStatusEventrows this PR already writes. - Deadline close: when an
openposition'sclosesAtpasses, show "<title> closed". This isn't a status change, so there's no event. Derive it in the activity query fromclosesAt(≤ now, inside the feed's recency window), usingclosesAtas the row's timestamp. Don't write events on a schedule and don't add a cron. - Scope: exactly the same as the "opened" rows. Listed managers see them in "Positions you manage", admins in "All positions". Merge them by time with the other rows, keep the 10-item cap, and link them to the position like the "opened" rows.
- No duplicates: a position closed manually must not also get a deadline-close row. The same goes for one whose
closesAtpassed after it had already been closed. - Tests and docs:
tests/db/coverage for both close kinds, the scoping (other managers and applicants don't see them) and the no-duplicate rule. Update the copy constants (POSITION_ACTIVITY_SENTENCE), thedocs/WORKFLOWS.mdXC-10 entry, and the #775 wording indocs/PERMISSIONS.mdif it only mentions opens.
Add manual and deadline-close activity rows, and trim three over-long comments flagged for the same discipline rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Revision — Cycle 1fixed R1-M1, R1-L1, R1-L2, R1-L3 · c958549 |
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 3 · approved
1 open — 1 🟡 Low (see inline)
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 3 · operator addendum · needs revision
1 open (operator scope addition). Also fold in R3-L1, the 3-line comment above getRecentPositionDeadlineCloses, from the Cycle 3 review above.
R3-M1 (Medium, operator scope addition, recorded on #775 as "Second amendment: deletions"): show deletions of published positions in the activity panel.
- When an admin deletes a position whose status was
openorclosedat deletion (notdraft), show "<title> was deleted".- Derive it from
deletedAt, the soft-delete stamp set bydeletePosition. - No new event and no schema change, unless deriving it proves unsound.
- Derive it from
- Not linked: the position page no longer exists.
- Scoping: the same as the other position rows (listed managers in "Positions you manage", admins in "All positions"). The reviewable-position filters exclude deleted positions, so the query needs a scoped lookup that includes them. Don't loosen any existing filter.
- Drafts: deleted drafts produce no row.
- Earlier rows: the position's opened and closed rows drop out after deletion, as today. Merge by time and keep the 10-item cap.
- Tests and docs:
tests/db/coverage for the published-vs-draft rule, the scoping (other managers and applicants don't see it), and the unlinked row;- a copy constant;
docs/WORKFLOWS.mdXC-10;docs/PERMISSIONS.mdif it lists the activity rows.
Adds the deletion row for the activity panel (R3-M1, second amendment in #775): a published position deleted while open/closed shows an unlinked "was deleted" item, derived from deletedAt with no schema change. Fixes the scope gap where deleting a manager's only position would drop their reviewer group before the notification could show. Also compresses the deadline-close comment flagged as R3-L1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Revision — Cycle 3fixed R3-M1, R3-L1 · 37a975d |
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 5 · approved
2 open — 1 🟡 Low, 1 ⚪ Nit (see inline)
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 5 · operator addendum · needs revision
1 open (operator change, recorded on #775). Also fold in R5-L1 and R5-N1 from the Cycle 5 review above.
Operator note on process: this is past the usual 5-cycle cap. The operator is directing the change, so it continues rather than escalating to needs human.
R5-M1 (Medium, operator decision): deleting a position must not remove its earlier activity rows.
Today the opened, closed and deadline-close queries filter out deleted positions (deletedAt: null in the scoping), so deleting a position removes its history from the feed. There is also a test asserting that rows drop out. Once a row has appeared, deletion must never remove it.
- Opened and closed rows from
PositionStatusEventfor a position that's later deleted stay. Render them unlinked, since the page no longer exists, keeping the position title. - Deadline-close rows a position had already produced before deletion stay too, unlinked. A deadline that passes after deletion produces nothing.
- Scoping: unchanged in who sees them (listed managers and admins), via the same kind of scoped lookup that
buildDeletedPositionWhereuses. Don't loosenbuildReviewablePositionWhere/PUBLISHED_POSITION_WHEREfor other callers. - Tests: replace the "earlier rows drop out" test with one asserting they persist and are unlinked. Keep the other deletion tests.
- Docs: update
docs/WORKFLOWS.mdXC-10 (anddocs/PERMISSIONS.mdif it says rows drop out). - Out of scope: unchanged. Rows still age out through the recency window and the 10-item cap.
Deletion no longer erases a position's earlier activity history: opened, closed and deadline-close rows now survive deletion, rendered unlinked. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Revision — Cycle 5fixed R5-M1, R5-L1, R5-N1 · ece313a |
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 7 · approved
5 open — 5 🟡 Low (see inline)
| return user.isAdmin ? base : { ...base, managers: { some: { id: user.id } } }; | ||
| } | ||
|
|
||
| // Same admin/manager split, but omits buildReviewablePositionWhere's deletedAt: |
There was a problem hiding this comment.
R7-L1 🟡 Low — comment discipline (ENGINEERING.md §7): 3-line comment above buildPositionHistoryWhere, past the "two only rarely, never three" cap (this same pattern was flagged and fixed as R1-L1/R1-L2/R1-L3/R3-L1 earlier in this PR). Fix: compress to one line, e.g. // Like buildReviewablePositionWhere, but keeps deleted rows so history outlives deletion.
| reviewed: ActivityItem[]; | ||
| }; | ||
|
|
||
| // Matches getRecentPositionStatusEvents's select in prisma/data/positions.ts. |
There was a problem hiding this comment.
R7-L2 🟡 Low — comment discipline: 3-line comment above PositionStatusActivity. Fix: cut to one line — the type's own fields and the getRecentPositionStatusEvents name already carry most of this.
| }; | ||
| }>; | ||
|
|
||
| // Matches getRecentPositionDeadlineCloses's select — no event backs this row, |
There was a problem hiding this comment.
R7-L3 🟡 Low — comment discipline: 3-line comment above PositionDeadlineCloseActivity. Fix: compress to one line.
| // Excludes a deadline lapsing only after deletion — Prisma can't compare two | ||
| // columns of one row in a where, so this runs post-fetch. | ||
| return positions | ||
| .filter((p) => p.deletedAt === null || p.closesAt! <= p.deletedAt) |
There was a problem hiding this comment.
R7-L4 🟡 Low — p.closesAt! is a non-null assertion (a cast), while the three analogous cases added in prisma/data/activity.ts in this same PR use a type-predicate to narrow instead (per the "narrow, not cast" comments there). Consider the same pattern here for consistency: (p): p is typeof p & { closesAt: Date } => p.closesAt !== null.
| where: { | ||
| ...buildPositionHistoryWhere(reviewer), | ||
| status: 'open', | ||
| closesAt: { gte: cutoff, lte: now }, |
There was a problem hiding this comment.
R7-L5 🟡 Low — by-design tradeoff, flagging for visibility: this row is derived from the position's current closesAt, not stored. If closesAt is later cleared/extended after a deadline-close row has already appeared, the row silently disappears on the next read — unlike the persisted opened/closed/deleted rows, which per the operator's amendment must never disappear once shown. Worth confirming this exception is intentional, or noting it explicitly alongside that rule in WORKFLOWS.md.
|
Operator decision: accepted. If an admin extends or clears a position's close date after its deadline-close row has appeared, that row disappears. Extending the deadline effectively reopens the position. This is the one deliberate exception to "activity rows never disappear", which still holds for opened, closed and deleted rows. |
Closes #775
Summary
PositionStatusEventtable (mirroringApplicationStatusEvent) recording every position status transition, written in the same transaction as the status write, with a compare-and-swap that also catches a concurrent status change.Changes
prisma/schema.prisma/ migrationadd_position_status_event: newPositionStatusEventmodel plus back-relations onPositionandUser. No backfill — a position already open has no truthful opened-at time.prisma/actions/position-actions.ts:updatePositionStatusnow runs insideprisma.$transaction, compare-and-swaps on the position's just-read status, and writes aPositionStatusEventfor every real transition (none on a same-status save). A concurrent status change now returns a clear{ error: POSITION_STATUS_CHANGED_ERROR }instead of the old blocked-unpublish message.lib/constants.ts:POSITION_STATUS_CHANGED_ERROR,POSITION_ACTIVITY_SENTENCE(copy keyed by the status a position opened from).prisma/data/positions.ts:getRecentPositionOpenings(reviewer, take)— readsto: 'open'events scoped identically to the existing reviewer application feed (buildReviewablePositionWhere).prisma/data/activity.ts:getActivityGroupsfetches openings alongside applications, merges by time, and caps the combined reviewer group at 10. Opening rows get no self-filter — the admin who opened a position still sees it.lib/types.ts:ActivityItemgains an optionalhref; newPositionOpeningActivitypayload type.components/features/activity-feed.tsx: a row withhrefrenders as aLinkwrapped inSheetClose asChild, so clicking it navigates and closes the panel; plain application rows are unchanged.tests/helpers/fixtures.ts: cleans uppositionStatusEventrows before the position delete (FK).tests/db/authorization.test.ts,tests/db/position-transitions.test.ts: new coverage (see Testing plan).docs/WORKFLOWS.md(XC-10, PM-4, AD intro),docs/PERMISSIONS.md: document the new event, its copy, the "just changed" error, and that it's not counted as archive-resetting activity.Testing plan
npm run prettier:check,npm run eslint:check,npm run tsc:check,npm run test(844/844 passing, including newtests/dbcases).hrefto the position (tests/db/authorization.test.ts).scope: 'none'and no reviewer group at all.PositionStatusEventwith the correctfrom/to/changedById; a same-status save and a blocked transition write none (tests/db/position-transitions.test.ts).Manual verification (per the plan's Testing section) still recommended in the preview deploy:
/positions/<id>and the Sheet closes; keyboard (Tab + Enter) does the same with a visible focus ring, including at a 375px viewport.Automated checks
run-prettier-check,run-linting-check,run-tsc-check,run-commit-message-check— all expected to pass locally; CI will confirm.Notes
docs/WORKFLOWS.mdXC-10.PositionStatusEventrows are deliberately not read byisPositionActive/positionActivitySelect— they don't reset the archive window; documented explicitly indocs/PERMISSIONS.mdto avoid a later ticket assuming otherwise.🤖 Generated with Claude Code