Skip to content

#775 Show Managers Activity When Their Position Is Opened - #776

Merged
cielbellerose merged 9 commits into
devfrom
775-show-managers-activity-when-position-opened
Sep 25, 2026
Merged

cielbellerose merged 9 commits into
devfrom
775-show-managers-activity-when-position-opened

Conversation

@cielbellerose

Copy link
Copy Markdown
Collaborator

Closes #775

Summary

  • Managers now see an activity-panel item when a position they manage is opened or reopened — the ticket's core ask, built on the reviewer group added in #617 Move Recent Activity Into A Global Activity Panel #771.
  • Adds a PositionStatusEvent table (mirroring ApplicationStatusEvent) 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 / migration add_position_status_event: new PositionStatusEvent model plus back-relations on Position and User. No backfill — a position already open has no truthful opened-at time.
  • prisma/actions/position-actions.ts: updatePositionStatus now runs inside prisma.$transaction, compare-and-swaps on the position's just-read status, and writes a PositionStatusEvent for 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) — reads to: 'open' events scoped identically to the existing reviewer application feed (buildReviewablePositionWhere).
  • prisma/data/activity.ts: getActivityGroups fetches 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: ActivityItem gains an optional href; new PositionOpeningActivity payload type.
  • components/features/activity-feed.tsx: a row with href renders as a Link wrapped in SheetClose asChild, so clicking it navigates and closes the panel; plain application rows are unchanged.
  • tests/helpers/fixtures.ts: cleans up positionStatusEvent rows 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 new tests/db cases).
  • Admin opens a draft position with manager M → M's activity panel shows the "was opened" item with an href to the position (tests/db/authorization.test.ts).
  • A manager of a different position does not see the item.
  • An applicant on the position (not a manager) sees scope: 'none' and no reviewer group at all.
  • Admin sees the item under "All positions" (no self-filter).
  • Reopen (open → closed → open) shows "was reopened"; the close transition alone produces no item.
  • Returning an opened position to draft (no applications) drops the item for the manager.
  • An application newer than an opening sorts above it in the merged, 10-item-capped reviewed group.
  • Each legal status transition writes exactly one PositionStatusEvent with the correct from/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:

  • As M, open the panel, click the "was opened" row → navigates to /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.
  • Vercel preview deployment (subject to the Neon branch budget).

Notes

  • Plan called for the reviewer group's cap comment to move from "per group" to "newest 10 across both kinds" for the merged reviewer group — done in docs/WORKFLOWS.md XC-10.
  • PositionStatusEvent rows are deliberately not read by isPositionActive/positionActivitySelect — they don't reset the archive window; documented explicitly in docs/PERMISSIONS.md to avoid a later ticket assuming otherwise.

🤖 Generated with Claude Code

cielbellerose and others added 6 commits September 24, 2026 18:16
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>
@cielbellerose cielbellerose added the claude Will be worked on by Claude label Sep 24, 2026
@cielbellerose cielbellerose self-assigned this Sep 24, 2026
@vercel

vercel Bot commented Sep 24, 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 24, 2026 11:44pm UTC

@cielbellerose cielbellerose 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 24, 2026

@cielbellerose cielbellerose 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 lib/types.ts Outdated
Comment thread prisma/data/positions.ts Outdated
Comment thread prisma/actions/position-actions.ts Outdated
@cielbellerose cielbellerose added needs revision Review found issues that need fixing and removed reviewing Review agent working (in-flight) labels Sep 24, 2026

@cielbellerose cielbellerose 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 · 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 the PositionStatusEvent rows this PR already writes.
  • Deadline close: when an open position's closesAt passes, show "<title> closed". This isn't a status change, so there's no event. Derive it in the activity query from closesAt (≤ now, inside the feed's recency window), using closesAt as 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 closesAt passed 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), the docs/WORKFLOWS.md XC-10 entry, and the #775 wording in docs/PERMISSIONS.md if it only mentions opens.

@cielbellerose cielbellerose added revising Revise agent working (in-flight) and removed needs revision Review found issues that need fixing labels Sep 24, 2026
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>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-M1, R1-L1, R1-L2, R1-L3 · c958549

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 24, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 24, 2026

@cielbellerose cielbellerose 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 3 · approved

1 open — 1 🟡 Low (see inline)

Comment thread prisma/data/positions.ts Outdated
@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 24, 2026

@cielbellerose cielbellerose 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 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 open or closed at deletion (not draft), show "<title> was deleted".
    • Derive it from deletedAt, the soft-delete stamp set by deletePosition.
    • No new event and no schema change, unless deriving it proves unsound.
  • 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.md XC-10;
    • docs/PERMISSIONS.md if it lists the activity rows.

@cielbellerose cielbellerose added revising Revise agent working (in-flight) and removed needs revision Review found issues that need fixing labels Sep 24, 2026
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>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 3

fixed R3-M1, R3-L1 · 37a975d

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 24, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 24, 2026

@cielbellerose cielbellerose 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

2 open — 1 🟡 Low, 1 ⚪ Nit (see inline)

Comment thread tests/db/authorization.test.ts
Comment thread prisma/data/activity.ts Outdated
@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 24, 2026

@cielbellerose cielbellerose 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 · 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 PositionStatusEvent for 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 buildDeletedPositionWhere uses. Don't loosen buildReviewablePositionWhere / PUBLISHED_POSITION_WHERE for 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.md XC-10 (and docs/PERMISSIONS.md if it says rows drop out).
  • Out of scope: unchanged. Rows still age out through the recency window and the 10-item cap.

@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed approved Review passed, ready to merge needs revision Review found issues that need fixing labels Sep 24, 2026
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>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 5

fixed R5-M1, R5-L1, R5-N1 · ece313a

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 24, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 24, 2026

@cielbellerose cielbellerose 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 7 · approved

5 open — 5 🟡 Low (see inline)

Comment thread lib/auth/scopes.ts
return user.isAdmin ? base : { ...base, managers: { some: { id: user.id } } };
}

// Same admin/manager split, but omits buildReviewablePositionWhere's deletedAt:

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.

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.

Comment thread lib/types.ts
reviewed: ActivityItem[];
};

// Matches getRecentPositionStatusEvents's select in prisma/data/positions.ts.

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.

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.

Comment thread lib/types.ts
};
}>;

// Matches getRecentPositionDeadlineCloses's select — no event backs this row,

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.

R7-L3 🟡 Low — comment discipline: 3-line comment above PositionDeadlineCloseActivity. Fix: compress to one line.

Comment thread prisma/data/positions.ts
// 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)

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.

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.

Comment thread prisma/data/positions.ts
where: {
...buildPositionHistoryWhere(reviewer),
status: 'open',
closesAt: { gte: cutoff, lte: now },

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.

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.

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 24, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

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.

@cielbellerose
cielbellerose merged commit b604ad4 into dev Sep 25, 2026
14 of 19 checks passed
@cielbellerose
cielbellerose deleted the 775-show-managers-activity-when-position-opened branch September 25, 2026 00:02

This branch was successfully deployed

1 active deployment
Preview — ece313a2 Deployed Sep 24, 2026 by vercel[bot]
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.

Show Managers Activity When Their Position Is Opened

1 participant