Skip to content

#706 Show Position Deadlines On Draft Applications - #707

Open
cielbellerose wants to merge 7 commits into
devfrom
706-show-position-deadlines-on-draft-applications
Open

cielbellerose wants to merge 7 commits into
devfrom
706-show-position-deadlines-on-draft-applications

Conversation

@cielbellerose

Copy link
Copy Markdown
Collaborator

Closes #706

Summary

  • Adds a Deadline column (and matching mobile card line) to the My Applications table, and a deadline slot in the dashboard's My Applications widget, so a draft's position deadline is finally visible on the applicant's own surfaces.
  • Introduces getDeadlineInfo (lib/utils.ts) — a pure helper that classifies a position's window into upcoming / distant / soon / urgent / past and derives the countdown text from a single diffMs, so the tier and the displayed text can never disagree.
  • Floats at-risk drafts (closing within 7 days) to the top: client-side in the table (pre-sorted rows, sort-key stays null so a header click still takes over), and query-side in getRecentMyApplications (merges a bounded at-risk query with the existing recency query) — the dashboard widget's take 3 was silently hiding an old draft with an approaching deadline.
  • Adds getClosingSoonDraftCount and a N closing soon segment to the widget's subtitle.

Changes

  • lib/constants.tsDEADLINE_SOON_DAYS (7), DEADLINE_URGENT_HOURS (48): sole owners of the urgency boundaries, shared by the helper and the query.
  • lib/types.tsDeadlineTier, DeadlineInfo.
  • lib/utils.tsgetDeadlineInfo(position, now), placed directly below getPositionDateInfo (deliberately not merged into it — see the "Notes" section).
  • components/features/deadline-indicator.tsx — new; the only place a deadline is styled, full (table/card) and compact (widget) variants.
  • components/features/my-applications-table.tsxDeadline column, now prop, at-risk pre-sort, mobile card line.
  • app/(main)/(auth)/applications/page.tsx — resolves now server-side and passes it down (avoids a hydration mismatch near a tier boundary).
  • prisma/data/applications.tsbuildAtRiskDraftWhere (shared), the at-risk float in getRecentMyApplications, new getClosingSoonDraftCount.
  • components/features/my-applications-widget.tsx — deadline in the trailing slot for drafts, N closing soon subtitle segment, one shared now.
  • tests/unit/utils.test.ts — tier/countdown/boundary coverage for getDeadlineInfo.
  • tests/db/my-applications-deadline-ordering.test.ts — new; float order, dedupe, take cap, exclusions (past-due, not-yet-open, unpublished, soft-deleted), and count parity.
  • docs/WORKFLOWS.md — AP-10 (Deadline column, tiers, at-risk float) and AP-1 (widget trailing slot + N closing soon).

Deliberately not changed: getPositionDateInfo / PositionDateLine. Widening its emphasis union with a "closing soon" tier would repaint every position card and detail page amber/red, which this ticket doesn't ask for. getDeadlineInfo owns applicant urgency instead; both still classify through the shared getPositionAvailability, so they can never disagree about the underlying window state, only about how to present it.

Testing plan

  • /applications shows a Deadline cell for every row: a draft closing in ~12 hours gets a red badge ("Closes in N hours"), one closing in ~3 days gets an amber badge ("Closes in N days"), one closing in ~30 days shows a plain muted "Closes ", one with no closesAt shows "—", one past its close date shows a red "Closed " badge (with Continue still available), and one not yet open shows a muted "Opens " with no badge.
  • A submitted application's deadline always renders as plain muted text/date, never a badge, regardless of how close the date is.
  • At-risk drafts (soon/urgent) sort to the top of the table, nearest deadline first, ahead of more-recently-touched rows; a not-yet-open or past-due draft never floats.
  • Clicking any column header (Position, Status, Applied, Deadline) takes over sorting and the at-risk float does not reassert itself; ?sort=/?dir= round-trips on reload.
  • Narrower than md, the stacked mobile card shows the same deadline/tiering on its own line.
  • Hovering a deadline shows the exact instant and timezone in the tooltip.
  • The dashboard's My Applications widget lists an old-but-at-risk draft ahead of more-recent applications, with a compact "Nh left"/"Nd left" badge, and the subtitle appends N closing soon when at least one at-risk draft exists.
  • Submitting or deleting the last at-risk draft and reloading / drops the count and removes the closing soon segment.
  • An applicant with no applications still gets the table's and widget's existing empty states; an applicant with applications but no at-risk drafts sees the widget's prior ordering/subtitle unchanged.
  • Both the amber (warning) and red (destructive) badges stay legible in light and dark mode.
  • Unit tests cover getDeadlineInfo's tier boundaries (48h inclusive/exclusive, 7d inclusive/exclusive, past, upcoming, null-date) and singular/plural countdown text.
  • DB test covers getRecentMyApplications's float order, dedupe of a row qualifying for both queries, the take cap, and exclusion of past-due/not-yet-open/unpublished/soft-deleted positions from the float, plus getClosingSoonDraftCount parity.

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test:unit — pass (409 tests)
  • tests/db/my-applications-deadline-ordering.test.ts — added but not executed in this environment: this worktree's Postgres port (5432) was already occupied by an unrelated container and I could not safely provision a second local database to run the db test project. The test logic was traced manually against the implementation (see PR description); please run npm run test (or the db project alone) in CI/review to confirm.

Notes

  • The countdown is only as fresh as the render; both surfaces are per-request dynamic and revalidated after every application mutation, so a long-lived open tab is the only staleness window — the alternative (a client-side ticking timer) would be a useEffect this codebase avoids.
  • formatCountdown in lib/utils.ts (the login OTP mm:ss timer) is unrelated and untouched.

🤖 Generated with Claude Code

@cielbellerose cielbellerose added the claude Will be worked on by Claude label Sep 11, 2026
@cielbellerose cielbellerose self-assigned this Sep 11, 2026
@vercel

vercel Bot commented Sep 11, 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 18, 2026 2:51pm 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 11, 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

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

Comment thread components/features/deadline-indicator.tsx
Comment thread prisma/data/applications.ts Outdated
Comment thread lib/utils.ts Outdated
Comment thread lib/utils.ts
@cielbellerose cielbellerose 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 11, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

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

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

0 open

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

Copy link
Copy Markdown
Collaborator Author

Revision requested — UX feedback from review of the preview

Three changes, all in components/features/deadline-indicator.tsx and
components/features/my-applications-widget.tsx. Item 3 reverses a decision
from the approved plan — read its note before implementing.

1. Closes vs Closed is not distinguishable enough

On /applications, a future deadline and a past one currently differ by a
single character in identical styling. For any row where emphasizeUrgency is
false (every submitted row), distant and past both fall through to the same
mutedLine🗓 Closes Mar 28 next to 🗓 Closed Mar 02, same icon, same
muted colour, same weight. Scanning the column, they read as the same thing.

Make the past state unmistakable without shouting on rows the user can't act
on. Options worth considering — pick one and say why:

  • a different icon for past (the calendar-clock currently reads "scheduled",
    not "over")
  • clearer wording than a one-letter delta, e.g. Closed Mar 2 vs
    Closes Mar 28Closed · Mar 2 / Due Mar 28, or past-tense phrasing
    that can't be misread at a glance
  • a distinct (still muted) treatment for past — e.g. strikethrough or a
    deliberately dimmer token

Whatever you choose has to hold for submitted rows too, which is where the
ambiguity actually bites — those never get the urgency branch.

2. Widget: the deadline belongs to the left of the status badge

In ApplicationList, the row is currently
title (flex-1) · ApplicationStatusBadge · deadline (trailing), so the
deadline sits to the right of the Draft tag. Move the deadline before the
status badge.

The reason is the whole point of the ticket: someone with an unsubmitted draft
needs the closing date to land in the same glance as the word "Draft", not
pushed to the far edge of the row. Reorder for all rows so the column
rhythm stays consistent; urgency styling still applies only to drafts.

Keep the layout resilient — the row is flex flex-wrap, and the title holds
min-w-0 flex-1 truncate, so verify a long position title still truncates
rather than pushing the deadline off-row at narrow widths.

3. The countdown should be text, not a Badge

soon and urgent currently render <Badge variant="warning"> /
<Badge variant="destructive">. Drop the badge. The countdown should read as
emphasised text — red and/or bold — not as a pill. Two filled tags in one row
(status + deadline) competes with the status badge and reads heavier than it
should for what is a piece of metadata.

This reverses the approved plan, and it reopens a real accessibility
constraint — solve it, don't re-discover it.
The badges were chosen precisely
because plain coloured text failed AA:

  • --warning is oklch(0.7 0.15 85); as text on bg-card it measures
    ≈2.7:1 — far below AA. Every existing text-warning use in the repo is
    icon-only for this reason (warning-callout.tsx,
    profile-completeness-banner.tsx, position-date-line.tsx).
  • --destructive is oklch(0.55 0.22 28) and is identical in both themes
    (app/globals.css:75 and :120), while --card inverts from
    oklch(1 0 0) to oklch(0.19 0 0). So text-destructive on a card is
    ≈5.4:1 in light mode but only ≈3.4:1 in dark mode — passing in one
    theme, failing in the other.
  • Bold does not rescue it: WCAG's 3:1 large-text threshold needs ≥18.66px for
    bold, and these surfaces are text-sm (14px) and text-xs (12px).

So the fix is a text-safe colour, most likely a new theme-aware token
(a --destructive/--warning variant that lightens in dark mode, following
the per-theme pattern already in globals.css) rather than reusing a token
that was only ever validated as a fill or an icon. Weight and an icon can carry
additional emphasis, but the colour has to pass on its own at 12px and 14px, in
both themes.

State the computed ratios for whatever you land on, for light and dark, at both
sizes — and add a dark-mode contrast check to the manual test matrix.

Unchanged

Everything else stands: the single-diffMs helper, getDeadlineInfo taking a
PositionWindow and gating urgency on getPositionAvailability, the
server-resolved now, the sort.key-stays-null pre-sort, the two
take-bounded queries plus the separate count, and LocalTime wrapping every
rendered date so the exact instant stays in the tooltip. Do not touch
getPositionDateInfo or PositionDateLine — position cards must not change
appearance.

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

Copy link
Copy Markdown
Collaborator Author

Revision — UX feedback follow-up

fixed all 3 items (past-deadline distinctness, deadline-before-badge ordering, Badge → AA-safe text tokens) · 2591a05

New tokens --warning-text / --destructive-text (app/globals.css): light 5.53:1 / 5.43:1, dark 5.66:1 / 5.36:1 against --card at 12px/14px (WCAG AA normal-text threshold, 4.5:1).

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

1 open — 1 🟡 Low (see inline)

Comment thread components/features/my-applications-table.tsx Outdated
@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 11, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author
Screenshot 2026-09-11 at 6 22 34 PM

@cielbellerose

Copy link
Copy Markdown
Collaborator Author
Screenshot 2026-09-18 at 10 06 43 AM

@cielbellerose cielbellerose added refresh branch Rebase onto base and force-push to trigger a fresh preview deployment refreshing Revise agent refreshing the branch (in-flight) and removed refresh branch Rebase onto base and force-push to trigger a fresh preview deployment labels Sep 18, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Pipeline Escalation

Rebasing 706-show-position-deadlines-on-draft-applications onto origin/dev produced an ambiguous conflict that autonomous resolution cannot safely resolve.

components/features/my-applications-table.tsx

Both sides modified the same action column cell inside buildColumns — not separate, non-overlapping sections.

dev side (f110d13 — "#709 drop unlabelled placeholder dashes from mobile cards") changed the action cell to render an em-dash placeholder for applications in a terminal decision status, instead of the action buttons:

{
  key: 'action',
  header: 'Action',
  cell: (a) =>
    TERMINAL_DECISION_STATUSES.includes(a.status) ? (
      <span className="text-muted-foreground text-sm" aria-hidden="true"></span>
    ) : (
      <div className="flex items-center gap-2">
        <MyApplicationPrimaryAction application={a} />
        <MyApplicationRowActions
          applicationId={a.id}
          status={a.status}
          positionTitle={a.position.title}
        />
      </div>
    ),
},

PR side (63902ed — "#706 show position deadlines on draft applications") independently restructured buildColumns — reordering/re-adding the status, applied, and a new deadline column, and rewriting the action cell back to always render the action buttons, with no TERMINAL_DECISION_STATUSES guard:

{
  key: 'action',
  header: 'Action',
  cell: (a) => (
    <div className="flex items-center gap-2">
      <MyApplicationPrimaryAction application={a} />
      <MyApplicationRowActions
        applicationId={a.id}
        status={a.status}
        positionTitle={a.position.title}
      />
    </div>
  ),
},

A second, smaller conflict at the closing of buildColumns (brace/bracket placement plus the PR's new atRiskDeadlineDate helper function) is entangled with the same hunk and can't be resolved independently of the first.

Why this isn't auto-resolvable

Taking "theirs" (the PR side) outright would silently drop the terminal-decision-status placeholder logic that #709 landed on dev after this PR branched — a real, recently-shipped UX behavior, not dead code. Taking "ours" (dev) would drop this PR's deadline column and restructuring. A correct merge requires re-applying the TERMINAL_DECISION_STATUSES guard on top of the PR's rebuilt action cell, which is a judgment call about intent best made by the original author rather than inferred mechanically.

Recommendation

The author (or a human reviewer) should rebase manually, keeping both the new deadline column/atRiskDeadlineDate helper from this PR and the TERMINAL_DECISION_STATUSES em-dash guard from dev in the merged action cell.

@cielbellerose cielbellerose added needs human Pipeline escalation: 3 review cycles without convergence and removed approved Review passed, ready to merge refreshing Revise agent refreshing the branch (in-flight) labels Sep 18, 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.

Human Direction — Cycle 1

Operator-authored, not a review-agent pass. Three items: one rebase decision and two scope changes now recorded in the #706 body ("Scope addendum"). Re-read that issue body before starting.

1. Rebase conflict — resolution authorized: keep both sides

The escalation in the previous comment is resolved. In components/features/my-applications-table.tsx, origin/dev's f110d13 (#709) wrapped the action column cell in a TERMINAL_DECISION_STATUSES guard rendering an aria-hidden em-dash for terminal rows; this PR independently rewrote that cell inside the new buildColumns(now) factory without the guard.

The two changes do not conflict in intent — combine them. Keep this PR's buildColumns(now) structure, the new deadline column, and the atRiskDeadlineDate helper, and wrap the action cell's body in dev's guard so it reads:

cell: (a) =>
  TERMINAL_DECISION_STATUSES.includes(a.status) ? (
    <span className="text-muted-foreground text-sm" aria-hidden="true"></span>
  ) : (
    <div className="flex items-center gap-2">
      <MyApplicationPrimaryAction application={a} />
      <MyApplicationRowActions
        applicationId={a.id}
        status={a.status}
        positionTitle={a.position.title}
      />
    </div>
  ),

Keep dev's TERMINAL_DECISION_STATUSES import. Also carry over the rest of f110d13 that landed in this file — it deleted the draft mobile card's bare dash, and that deletion must survive the rebase. Verify against git show f110d13 -- components/features/my-applications-table.tsx rather than assuming this hunk is the whole of it. If any other hunk in the rebase is genuinely ambiguous, escalate that one as normal — this authorization covers the action-cell collision only.

2. Restore the widget row order (regression introduced by this PR)

components/features/my-applications-widget.tsx moved <ApplicationStatusBadge> to after the trailing <span>, changing the row from title · badge · date to title · date · badge. Not requested, and it contradicts the widget mock in the issue. Put the badge back immediately after the title link; the DeadlineIndicator / LocalTime trailing slot stays where it is on the right.

3. Add the close date to the application detail page

app/(main)/(auth)/applications/[id]/page.tsx:82-95 — append the deadline as a fourth segment of the meta line, after the View position link, for drafts only. Reuse DeadlineIndicator with variant="full" and emphasizeUrgency; resolve now in the page (server component) and pass it in. No change to getMyApplication — its select already carries position.status, opensAt and closesAt. Submitted and terminal statuses keep today's line unchanged.

Update docs/WORKFLOWS.md for the detail-page deadline in the same PR, and add coverage for the draft/non-draft split on that page.

The full acceptance criteria are in the #706 body, including the four added ones.

@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed needs human Pipeline escalation: 3 review cycles without convergence needs revision Review found issues that need fixing labels Sep 18, 2026
cielbellerose and others added 6 commits September 18, 2026 10:45
Adds a Deadline column/mobile line and dashboard-widget trailing slot
with amber/red urgency badges for at-risk drafts, and floats at-risk
drafts ahead of recency order in both the table and
getRecentMyApplications so a stale-but-closing-soon draft can't be
hidden by a take-bounded query.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Distinguish past deadlines (icon + dimmer muted line), move the
deadline before the status badge in the widget, and replace the
soon/urgent/past Badge with AA-safe text tokens (--warning-text /
--destructive-text) instead of the fill-only --warning/--destructive.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deadline column header already names the field, so drop the redundant
Closes/Closed prefix (upcoming keeps Opens, which is load-bearing);
render soon/urgent as the compact Nd/Nh-left countdown everywhere
instead of a longer table-only form. Removes the now-unused
DeadlineInfo.countdown field and its tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Restore the widget's title-badge-trailing order and add the draft
deadline segment to the application detail page per the addendum.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes out three still-open review threads from earlier cycles
(R4-L1, R4-L2, R5-L1) that were never trimmed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1 (Human Direction)

fixed rebase-authorized action-cell merge, widget row order regression, detail-page deadline segment (scope addendum) · fixed R4-L1, R4-L2, R5-L1 (leftover long comments) · rebase: components/features/my-applications-table.tsx (combined per authorized resolution — kept buildColumns structure, wrapped action cell in TERMINAL_DECISION_STATUSES guard) · 298d505, db005d0

follow-up: no automated render-test coverage added for the detail page's draft/non-draft split — this repo has no component-render test infra (no testing-library dependency), so the split relies on the already-unit-tested getDeadlineInfo/DeadlineIndicator tiering plus the manual testing-plan step; flagging in case a future ticket wants to add that infra.

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude Will be worked on by Claude ready for review PR ready for review agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show Position Deadlines On Draft Applications

1 participant