Skip to content

feat: dashboard needs-attention counts and lifecycle activation emails - #270

Open
JoachimLK wants to merge 9 commits into
mainfrom
feat/dashboard-needs-attention-and-lifecycle-emails
Open

feat: dashboard needs-attention counts and lifecycle activation emails#270
JoachimLK wants to merge 9 commits into
mainfrom
feat/dashboard-needs-attention-and-lifecycle-emails

Conversation

@JoachimLK

@JoachimLK JoachimLK commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Two unrelated bodies of work that had accumulated uncommitted, kept as separate commits.

1. Dashboard: surface what actually needs attention (17f6068)

"To Review" now counts unopened applicants, not the new stage. The old count stayed high long after every application in it had been read, so the tile could never be worked down to zero — and moving a candidate along the pipeline is a separate decision from having looked at them. It now uses the per-user read receipts introduced in #266, matching the per-job badges on /jobs. Clicking through opens the list filtered to the same set (?viewed=unviewed), backed by a new viewed filter on the applications API and a matching control on the list, so the number and the rows behind it can't disagree.

Unanswered candidate replies — a new dashboard card for threads whose newest message is inbound. Derived from message order rather than candidateConversation.unreadCount: nothing in the product ever clears that counter, so a badge built on it could only grow. "Newest message is inbound" self-clears the moment anything goes out on the thread.

Application detail tabs are addressable (?tab=inbox), validated against a shared list so a hand-edited value can't land the component in a state with no content. The reply card links straight into the thread.

2. Lifecycle activation emails (2c35b1b)

Emits org.created and job.posted to Resend Automations, which owns the delay, the branching, and the copy for the "signed up, never opened a role" nudge.

This is deliberately not built on notificationOutbox. That engine reports workspace activity — something happened, come look. This one chases an absence, and an absence has no moment to enqueue at; the waiting lives in a Resend Wait-for-Event step instead. It also means the copy is editable without a deploy, which matters while we still don't know what the email should say.

  • Off by default. LIFECYCLE_EMAILS_ENABLED fails closed, unlike NOTIFICATIONS_ENABLED — enabling it ships user email addresses to the Resend account behind RESEND_API_KEY, which is only ever right on hosted.
  • Fire-and-forget. Neither org creation nor job creation may fail or slow down because a marketing automation was unreachable. Failures log under category: lifecycle_event.
  • Skips demo workspaces, the demo account, and reserved @example.com addresses (sample applicants can only hard bounce, and bounces score against the sending domain).
  • isTest rides in the job.posted payload rather than gating the emit — whether a test role counts as activation is a copy decision, so it belongs in the automation's condition.

Not done in this PR

The automation itself doesn't exist in Resend yet, so events currently go nowhere. Nothing sends until both the automation is built and the env var is set.

Testing

928 unit tests pass; vue-tsc clean. New: tests/unit/lifecycle-events.test.ts (8 cases), tests/unit/application-detail-tabs.test.ts; extended tests/unit/application-views.test.ts.

Known gap: server/utils/unansweredReplies.ts has no test. The selectDistinctOn ordering in it is the kind of thing that breaks quietly.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added dashboard tracking and filtering for viewed and unviewed applications.
    • Replaced Quick Actions with an Unanswered Replies queue showing candidate messages and job details.
    • Added direct links to application inbox conversations and preserved selected detail tabs.
    • Added optional lifecycle email events for workspace creation and job posting.
    • Added optional salary details to job creation and previews.
    • Added per-board job feed delivery status and caching improvements.
  • Documentation
    • Updated configuration guidance and changelog entries covering lifecycle emails and product updates.

JoachimLK and others added 2 commits August 11, 2026 09:20
Replace the "To Review" tile's `new`-stage count with per-user read
receipts, and add the two things the dashboard was silent about.

* "To Review" now counts applicants the signed-in user has never opened,
  matching the per-job badges on /jobs. The `new` stage stayed high long
  after every application in it had been read, so the tile could not be
  worked down to zero. Clicking through opens the list filtered to the
  same set (`?viewed=unviewed`), which the applications API and a new
  Viewed filter on the list now support, so the number and the rows
  behind it cannot disagree.

* Unanswered candidate replies: threads whose newest message is inbound.
  Derived from message order rather than `candidateConversation.unread
  Count`, because nothing in the product clears that counter — a badge
  built on it could only ever grow. "Newest message is inbound" clears
  itself the moment anything goes out on the thread.

* Application detail tabs are addressable (`?tab=inbox`), validated
  against a shared list so a hand-edited value cannot land the component
  in a state with no content. The reply card links straight into the
  thread rather than the overview.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first step of the funnel is "signed up, never opened a role", which
is an absence — there is no moment at which it happens, so it cannot be
enqueued the way `notificationOutbox` enqueues workspace activity.
Instead we emit the event that starts the clock (`org.created`) and the
one that stops it (`job.posted`), and a Wait-for-Event step in Resend
decides whether anyone is mailed. The copy and the timing live in the
Resend dashboard, editable without a deploy — at this stage that is the
point, since we do not yet know what the email should say.

Off by default. Unlike NOTIFICATIONS_ENABLED, LIFECYCLE_EMAILS_ENABLED
fails closed: a recruiter notification is mail the operator's own users
asked for, but a lifecycle event ships a user's address to *our* Resend
account, which is the wrong default for anyone self-hosting.

Emits are fire-and-forget and swallow their own failures — neither
creating a workspace nor opening a role may fail, or even slow down,
because a marketing automation was unreachable. A dropped event costs
one nudge email; a durable queue for that would be a much larger change
than the funnel currently justifies. Failures log under
`category: lifecycle_event`.

Demo workspaces, the demo account, and reserved (`@example.com`)
addresses are skipped — the shared demo org would otherwise enter the
funnel as a signup that never posted a role, and sample applicants can
only ever hard bounce against the sending domain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@railway-app

railway-app Bot commented Aug 11, 2026

Copy link
Copy Markdown

🚅 Deployed to the reqcore-pr-270 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Web Aug 12, 2026 at 8:30 am

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@JoachimLK, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 35a15464-3c31-4b8f-86b9-6c689bbd5a01

📥 Commits

Reviewing files that changed from the base of the PR and between 4835006 and 94adc71.

📒 Files selected for processing (16)
  • app/composables/useBillingStatus.ts
  • app/composables/useChatbot.ts
  • app/composables/useChatbotQuota.ts
  • ee/server/api/candidate-messages/send.post.ts
  • ee/server/utils/candidate-message-allowance.ts
  • server/api/chatbot/chat.post.ts
  • server/utils/ai/budget.ts
  • server/utils/ai/platformConfig.ts
  • server/utils/ai/resolveProvider.ts
  • server/utils/billing/usage.ts
  • server/utils/interview-conversation.ts
  • shared/billing.ts
  • tests/unit/ai-budget-ledgers.test.ts
  • tests/unit/ai-provider-grandfathered.test.ts
  • tests/unit/candidate-messaging.test.ts
  • tests/unit/chatbot-plan-gate.test.ts
📝 Walkthrough

Walkthrough

The change adds per-user viewed and unviewed application filtering, dashboard metrics for unviewed applications, an unanswered-replies queue, Inbox deep links, optional Resend Automations events, job compensation fields, and feed delivery tracking.

Changes

Dashboard application workflows

Layer / File(s) Summary
Application view filtering
server/utils/schemas/application.ts, server/utils/applicationViews.ts, server/api/applications/index.get.ts, app/composables/useApplications.ts, tests/unit/application-views.test.ts
Applications support viewed and unviewed filters. Shared read-receipt conditions provide application and dashboard counts.
Unanswered reply dashboard data
server/utils/unansweredReplies.ts, server/api/dashboard/stats.get.ts, app/composables/useDashboard.ts
The dashboard retrieves latest inbound candidate replies, reply counts, and unviewed application counts.
Dashboard filters and reply deep links
app/components/ApplicationsList.vue, shared/application-detail-tabs.ts, app/components/ApplicationDetail.vue, app/pages/dashboard/applications/[id].vue, app/pages/dashboard/index.vue, tests/unit/application-detail-tabs.test.ts, CHANGELOG.md
Viewed filters persist in URLs and saved views. Reply rows link to the application Inbox tab. The dashboard replaces Quick Actions with an Unanswered Replies queue.

Lifecycle email events

Layer / File(s) Summary
Lifecycle event configuration and emission
server/utils/env.ts, server/utils/lifecycle/events.ts, tests/unit/lifecycle-events.test.ts, .env.example, CHANGELOG.md
Resend Automations events use explicit feature gating, demo and address suppression, structured logging, error handling, and fire-and-forget delivery.
Organization and job event wiring
server/utils/auth.ts, server/api/jobs/index.post.ts
Organization creation emits orgCreated. Successful job creation emits jobPosted with event metadata.

Job compensation and feed distribution

Layer / File(s) Summary
Job compensation entry and submission
app/pages/dashboard/jobs/new.vue, app/composables/useJobs.ts, app/components/ApplicationBuilderPreview.vue, shared/sample-job.ts, e2e/critical-flows/job-creation.spec.ts, .npmrc, Dockerfile
The job wizard supports optional compensation with validation, draft restoration, formatted inputs, submission mapping, preview display, and end-to-end coverage.
Feed delivery storage and retention
server/database/schema/app.ts, server/database/migrations/*, server/routes/jobs.xml.ts, server/utils/jobFeed.ts, server/tasks/retention-cleanup.ts
Feed requests record fetch metadata and included jobs. Cleanup removes feed-fetch records older than 90 days.
Board delivery status
server/api/jobs/[id]/promote.get.ts, server/utils/jobFeed.ts, app/components/JobPromotePanel.vue, tests/unit/feed-delivery.test.ts
The API computes per-board delivery states, and the promotion panel displays aggregate and board-level delivery status.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DashboardPage
  participant DashboardStats
  participant ApplicationsEndpoint
  participant ApplicationViews
  DashboardPage->>DashboardStats: request dashboard counts and replies
  DashboardStats->>ApplicationViews: count unviewed applications
  DashboardPage->>ApplicationsEndpoint: request viewed or unviewed applications
  ApplicationsEndpoint->>ApplicationViews: apply read-receipt condition
  ApplicationViews-->>DashboardPage: filtered applications and counts
Loading
sequenceDiagram
  participant JobWizard
  participant JobsAPI
  participant JobsFeed
  participant FeedFetch
  participant JobPromotePanel
  JobWizard->>JobsAPI: submit compensation fields
  JobsAPI-->>JobWizard: create job
  JobsFeed->>FeedFetch: record board fetch metadata
  JobsFeed->>JobsFeed: mark included jobs
  JobPromotePanel->>JobsAPI: request delivery status
  JobsAPI-->>JobPromotePanel: return board delivery states
Loading
sequenceDiagram
  participant OrganizationCreation
  participant JobCreation
  participant LifecycleEvents
  participant ResendAutomations
  OrganizationCreation->>LifecycleEvents: emit org.created
  JobCreation->>LifecycleEvents: emit job.posted
  LifecycleEvents->>ResendAutomations: submit eligible event
  ResendAutomations-->>LifecycleEvents: return delivery result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary dashboard and lifecycle activation email changes and follows Conventional Commits format.
Description check ✅ Passed The description clearly explains both workstreams, rationale, testing, known gaps, and deferred automation, so it is sufficiently complete.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-needs-attention-and-lifecycle-emails

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/components/ApplicationsList.vue (1)

410-417: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include viewed when comparing saved-view settings.

A saved view that differs only by viewed appears clean. The UI can then hide the unsaved change and prevent the user from updating the saved view.

Proposed fix
 function settingsEqual(a: ApplicationsViewSettings, b: ApplicationsViewSettings) {
   return a.status === b.status
+    && a.viewed === b.viewed
     && a.jobId === b.jobId
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/ApplicationsList.vue` around lines 410 - 417, Update
settingsEqual to compare the viewed property in addition to the existing status,
jobId, sort, propertyFilters, and visibleColumns fields, so changes to viewed
mark the saved view as modified.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/components/ApplicationDetail.vue`:
- Around line 52-53: Update ApplicationDetail’s reactive state so detailTab
stays synchronized with changes to props.initialTab, including same-application
route query navigation. Add a watcher for props.initialTab alongside the
existing applicationId watcher and assign the new value to detailTab.

In `@app/components/ApplicationsList.vue`:
- Around line 159-176: Replace the independent filter watchers around
activeStatus and activeViewed with one watcher that observes both state values
and route changes, then updates query.status and query.viewed together,
including removing either key when its value is undefined. Ensure the combined
router.replace uses the latest route query so changing one filter does not
overwrite the other.

---

Outside diff comments:
In `@app/components/ApplicationsList.vue`:
- Around line 410-417: Update settingsEqual to compare the viewed property in
addition to the existing status, jobId, sort, propertyFilters, and
visibleColumns fields, so changes to viewed mark the saved view as modified.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f37f712f-a730-49e3-9bbb-caf310e8b2b8

📥 Commits

Reviewing files that changed from the base of the PR and between 63a2968 and 2c35b1b.

📒 Files selected for processing (21)
  • .env.example
  • CHANGELOG.md
  • app/components/ApplicationDetail.vue
  • app/components/ApplicationsList.vue
  • app/composables/useApplications.ts
  • app/composables/useDashboard.ts
  • app/pages/dashboard/applications/[id].vue
  • app/pages/dashboard/index.vue
  • server/api/applications/index.get.ts
  • server/api/dashboard/stats.get.ts
  • server/api/jobs/index.post.ts
  • server/utils/applicationViews.ts
  • server/utils/auth.ts
  • server/utils/env.ts
  • server/utils/lifecycle/events.ts
  • server/utils/schemas/application.ts
  • server/utils/unansweredReplies.ts
  • shared/application-detail-tabs.ts
  • tests/unit/application-detail-tabs.test.ts
  • tests/unit/application-views.test.ts
  • tests/unit/lifecycle-events.test.ts

Comment on lines +52 to +53
type DetailTab = ApplicationDetailTab
const detailTab = ref<DetailTab>(props.initialTab)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

ast-grep outline 'app/pages/dashboard/applications/[id].vue' --items all
rg -n -C 5 'ApplicationDetail|initialTab|parseApplicationDetailTab|route\.query\.tab' \
  'app/pages/dashboard/applications/[id].vue'

Repository: reqcore-inc/reqcore

Length of output: 1488


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

fd -i 'ApplicationDetail.vue' .
fd -i 'application-detail-tabs' .
printf '\n--- component outline ---\n'
ast-grep outline "$(fd -i -t f 'ApplicationDetail.vue' . | head -n 1)" --items all
printf '\n--- relevant component references ---\n'
component="$(fd -i -t f 'ApplicationDetail.vue' . | head -n 1)"
rg -n -C 8 'initialTab|detailTab|applicationId|watch\(' "$component"
printf '\n--- tab parser and usages ---\n'
tabfile="$(fd -i -t f 'application-detail-tabs' . | head -n 1)"
cat -n "$tabfile"
rg -n -C 5 'initial-tab|initialTab|route\.query\.tab|parseApplicationDetailTab' app shared 2>/dev/null || true

Repository: reqcore-inc/reqcore

Length of output: 32309


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

page = Path("app/pages/dashboard/applications/[id].vue").read_text()
component = Path("app/components/ApplicationDetail.vue").read_text()

assert "const initialTab = computed(() => parseApplicationDetailTab(route.query.tab))" in page
assert ':initial-tab="initialTab"' in page
assert "const detailTab = ref<DetailTab>(props.initialTab)" in component

watchers = re.findall(
    r"watch\(\s*(?:\(\)\s*=>\s*)?([^,\n]+),",
    component,
)
assert any("props.applicationId" in watcher for watcher in watchers)
assert not any("props.initialTab" in watcher for watcher in watchers)

print("page initialTab is derived from route.query.tab and passed as a prop")
print("detailTab is initialized from props.initialTab")
print("the existing reset watcher observes props.applicationId")
print("no watcher observes props.initialTab")
print("same-application query changes can therefore leave detailTab unchanged")
PY

Repository: reqcore-inc/reqcore

Length of output: 437


Synchronize detailTab with initialTab.

When same-application navigation changes route.query.tab, initialTab updates but detailTab does not. The watcher at Lines 642-646 observes only props.applicationId. Add a watcher for props.initialTab.

Proposed fix
 const detailTab = ref<DetailTab>(props.initialTab)
+
+watch(() => props.initialTab, (tab) => {
+  detailTab.value = tab
+})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type DetailTab = ApplicationDetailTab
const detailTab = ref<DetailTab>(props.initialTab)
type DetailTab = ApplicationDetailTab
const detailTab = ref<DetailTab>(props.initialTab)
watch(() => props.initialTab, (tab) => {
detailTab.value = tab
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/ApplicationDetail.vue` around lines 52 - 53, Update
ApplicationDetail’s reactive state so detailTab stays synchronized with changes
to props.initialTab, including same-application route query navigation. Add a
watcher for props.initialTab alongside the existing applicationId watcher and
assign the new value to detailTab.

Comment on lines +159 to +176
const initialViewed = VIEWED_OPTIONS.includes(route.query.viewed as any)
? (route.query.viewed as ViewedFilter)
: undefined
const activeViewed = useState<ViewedFilter | undefined>(`app-filter-viewed${scopeSuffix}`, () => initialViewed)
if (initialViewed !== undefined) {
activeViewed.value = initialViewed
}

watch(activeViewed, (next) => {
const query = { ...route.query }
if (next) {
query.viewed = next
}
else {
delete query.viewed
}
router.replace({ query })
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 'activeStatus|activeViewed|route\.query\.(status|viewed)|router\.replace|clearAllFilters|applySettings' \
  app/components/ApplicationsList.vue

Repository: reqcore-inc/reqcore

Length of output: 9982


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- component imports and filter setup ---'
sed -n '1,190p' app/components/ApplicationsList.vue

printf '%s\n' '--- settings and view application logic ---'
sed -n '350,445p' app/components/ApplicationsList.vue

printf '%s\n' '--- route/filter references in the component ---'
rg -n -C 4 'useRoute|useRouter|route\.query|watch\(|useState|clearAllFilters|applySettings' app/components/ApplicationsList.vue

Repository: reqcore-inc/reqcore

Length of output: 15095


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- dependency versions ---'
rg -n '"(vue|vue-router|nuxt)"' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true

printf '%s\n' '--- deterministic watcher/query model ---'
python3 - <<'PY'
from copy import deepcopy

def status_write(route_query, status):
    query = deepcopy(route_query)
    if status:
        query["status"] = status
    else:
        query.pop("status", None)
    return query

def viewed_write(route_query, viewed):
    query = deepcopy(route_query)
    if viewed:
        query["viewed"] = viewed
    else:
        query.pop("viewed", None)
    return query

cases = [
    ("both added", {}, "new", "unviewed"),
    ("both changed", {"status": "old", "viewed": "viewed"}, "new", "unviewed"),
    ("both cleared", {"status": "new", "viewed": "unviewed"}, None, None),
]
for name, route, status, viewed in cases:
    first = status_write(route, status)
    second = viewed_write(route, viewed)
    print(name)
    print("  status watcher submits:", first)
    print("  viewed watcher submits:", second)
    print("  last submitted query:", second)
PY

Repository: reqcore-inc/reqcore

Length of output: 2274


🌐 Web query:

Vue Router 4 router.replace returns Promise navigation asynchronous current route update documentation

💡 Result:

In Vue Router 4, the router.replace method (like router.push) returns a Promise that allows you to wait until the navigation is fully finished [1][2]. Key technical details regarding this Promise: 1. Asynchronous Resolution: The Promise resolves once the navigation successfully completes or fails [3]. Navigation includes the processing of all asynchronous navigation guards, such as those that resolve async route components or perform data fetching [4]. 2. Success vs. Failure: To handle navigation failures (e.g., cancelling a navigation via next(false)), the Promise resolves to either undefined (on success) or a NavigationFailure object (if the navigation was aborted or cancelled) [3]. 3. Rejection: The Promise only rejects if an unhandled error occurs during the navigation process (e.g., an error inside a navigation guard) [3]. In such cases, the error will also be triggered through the router.onError hook [3]. Because the Promise resolves rather than rejects when a navigation is simply cancelled, you can await the call safely without triggering "Uncaught (in promise)" errors for expected navigation failures [3]. By checking the returned value or catching errors specifically, you can differentiate between successful, cancelled, and failed navigations [3].

Citations:


Synchronize status and viewed filters in one route-query update.

Watch route changes and update both activeStatus and activeViewed, including undefined. Replace the two independent router.replace watchers with one watcher that writes both query parameters. Separate updates can submit stale route.query snapshots and discard the other filter change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/ApplicationsList.vue` around lines 159 - 176, Replace the
independent filter watchers around activeStatus and activeViewed with one
watcher that observes both state values and route changes, then updates
query.status and query.viewed together, including removing either key when its
value is undefined. Ensure the combined router.replace uses the latest route
query so changing one filter does not overwrite the other.

CI rejected the lockfile with "Missing: oxc-parser@0.144.0 from lock
file" — an optional peer of unctx@3.0.0 (`oxc-parser >=0.140.0`) that
the lock pins at 0.128.0.

The lock is not wrong so much as generated under different rules: with
`legacy-peer-deps=true`, npm skips peer resolution entirely and never
records that peer, and that flag has been set in a personal ~/.npmrc
rather than in the repo. Every local `npm install` therefore produced a
lock that a runner with no user npmrc refuses.

Moving the setting into the repo makes resolution a property of the
project instead of of whoever last ran npm install, so CI, Docker builds
and every contributor resolve the same tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@JoachimLK JoachimLK changed the title Dashboard needs-attention counts + lifecycle activation emails feat: dashboard needs-attention counts and lifecycle activation emails Aug 11, 2026
The image build ran `npm ci` against a lockfile generated under
`legacy-peer-deps`, but only `package*.json` was copied in — so the
resolution mode never reached it and the build failed on an optional
peer of unctx (`Missing: oxc-parser@0.144.0 from lock file`), the same
way CI did before the repo npmrc landed.

Reproduced against node:22.22-alpine (npm 10.9.8): fails without the
file, succeeds with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-270 August 11, 2026 07:36 Destroyed
Introduces salary input fields to the job builder, including support for
ranges, currency codes, and payment units. Adds validation to ensure
salary details are complete and logically sound before publishing.
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-270 August 11, 2026 08:18 Destroyed
- Add `feed_fetch` table to log feed requests per board
- Add `last_included_in_feed_at` to `job` table
- Implement `boardDeliveryState` logic to verify job presence in pulls
- Add UI components for per-board delivery status
- Add cleanup task for `feed_fetch` logs
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-270 August 12, 2026 07:28 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/database/schema/app.ts`:
- Around line 154-166: Replace the global lastIncludedInFeedAt field with a
per-fetch job receipt relation: in server/database/schema/app.ts:154-166 remove
the global evidence, and in server/database/schema/app.ts:858-893 add a receipt
table linking feed_fetch.id to job.id with board-fetch and job lookup indexes.
In server/database/migrations/0067_lame_rachel_grey.sql:1-13 create the table,
foreign keys, and indexes; in server/routes/jobs.xml.ts:153-162 retain included
job IDs and in server/routes/jobs.xml.ts:214-230 insert the fetch and receipts
transactionally. Update boardDeliveryState in server/utils/jobFeed.ts:253-271
and the promotion queries in server/api/jobs/[id]/promote.get.ts:120-147 to use
the latest board fetch receipt, marking delivery only when that fetch includes
the job.

In `@server/routes/jobs.xml.ts`:
- Around line 217-218: Remove the raw IP assignment from the public feed request
logging object in the route handling this code, including the getRequestIP call.
Retain the verified board identifier and fetch-time fields needed for delivery
status, and do not add derived IP diagnostics unless an existing documented
retention mechanism is available.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9163e245-e200-472c-bf8a-ab0af69af8a0

📥 Commits

Reviewing files that changed from the base of the PR and between 2c35b1b and 4835006.

📒 Files selected for processing (17)
  • .npmrc
  • Dockerfile
  • app/components/ApplicationBuilderPreview.vue
  • app/components/JobPromotePanel.vue
  • app/composables/useJobs.ts
  • app/pages/dashboard/jobs/new.vue
  • e2e/critical-flows/job-creation.spec.ts
  • server/api/jobs/[id]/promote.get.ts
  • server/database/migrations/0067_lame_rachel_grey.sql
  • server/database/migrations/meta/0067_snapshot.json
  • server/database/migrations/meta/_journal.json
  • server/database/schema/app.ts
  • server/routes/jobs.xml.ts
  • server/tasks/retention-cleanup.ts
  • server/utils/jobFeed.ts
  • shared/sample-job.ts
  • tests/unit/feed-delivery.test.ts

Comment on lines +154 to +166
/**
* Last time this job was actually emitted into a served `/jobs.xml` response.
*
* Syndication is pull-based: nothing is ever posted, aggregators fetch the
* feed on their own schedule. So "this role is on the boards" is not
* something publishing can assert — the only evidence that exists is that a
* board asked for the feed and this job was in what we handed back.
*
* Stamped with the same clock as the `feed_fetch` row written for that
* request, which is what lets `boardDeliveryState` compare the two: a job
* whose timestamp is older than a board's last fetch was *not* in that pull.
*/
lastIncludedInFeedAt: timestamp('last_included_in_feed_at'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Persist per-board job receipts instead of one global inclusion timestamp.

A later fetch from Board B updates job.lastIncludedInFeedAt. The classifier then marks Board A as delivered when Board A's latest pull excluded the job. This occurs when eligibility changes between the two pulls.

  • server/database/schema/app.ts#L154-L166: replace the global lastIncludedInFeedAt evidence with a per-fetch job receipt relation.
  • server/database/schema/app.ts#L858-L893: add a receipt table that links feed_fetch.id to job.id, with indexes for board-fetch and job lookups.
  • server/database/migrations/0067_lame_rachel_grey.sql#L1-L13: create the receipt table, foreign keys, and indexes in this migration.
  • server/routes/jobs.xml.ts#L153-L162: retain included IDs for receipt insertion.
  • server/routes/jobs.xml.ts#L214-L230: insert the fetch row and its job receipts in one transaction.
  • server/utils/jobFeed.ts#L253-L271: classify a board as delivered only when that board's latest fetch has a receipt for the job.
  • server/api/jobs/[id]/promote.get.ts#L120-L147: query the latest fetch and receipt for each board.
📍 Affects 5 files
  • server/database/schema/app.ts#L154-L166 (this comment)
  • server/database/schema/app.ts#L858-L893
  • server/database/migrations/0067_lame_rachel_grey.sql#L1-L13
  • server/routes/jobs.xml.ts#L153-L162
  • server/routes/jobs.xml.ts#L214-L230
  • server/utils/jobFeed.ts#L253-L271
  • server/api/jobs/[id]/promote.get.ts#L120-L147
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/database/schema/app.ts` around lines 154 - 166, Replace the global
lastIncludedInFeedAt field with a per-fetch job receipt relation: in
server/database/schema/app.ts:154-166 remove the global evidence, and in
server/database/schema/app.ts:858-893 add a receipt table linking feed_fetch.id
to job.id with board-fetch and job lookup indexes. In
server/database/migrations/0067_lame_rachel_grey.sql:1-13 create the table,
foreign keys, and indexes; in server/routes/jobs.xml.ts:153-162 retain included
job IDs and in server/routes/jobs.xml.ts:214-230 insert the fetch and receipts
transactionally. Update boardDeliveryState in server/utils/jobFeed.ts:253-271
and the promotion queries in server/api/jobs/[id]/promote.get.ts:120-147 to use
the latest board fetch receipt, marking delivery only when that fetch includes
the job.

Comment thread server/routes/jobs.xml.ts
Comment on lines +217 to +218
userAgent: getRequestHeader(event, 'user-agent')?.slice(0, 512) ?? null,
ipAddress: getRequestIP(event, { xForwardedFor: true }) ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not retain raw requester IP addresses for public feed requests.

This route logs untagged browser requests. ipAddress can therefore contain personal data for users who are not job-board crawlers. Delivery status only needs a verified board identifier and fetch time.

Remove raw IP storage. If diagnostics are required, store a minimized derived value with documented retention.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/routes/jobs.xml.ts` around lines 217 - 218, Remove the raw IP
assignment from the public feed request logging object in the route handling
this code, including the getRequestIP call. Retain the verified board identifier
and fetch-time fields needed for delivery status, and do not add derived IP
diagnostics unless an existing documented retention mechanism is available.

Introduce `tierUsesFreeAllowances` to normalize AI usage metering across
the `free` and `grandfathered` tiers. This enables `grandfathered`
workspaces to utilize platform AI with a bounded cost, correcting a
long-standing issue where many legacy orgs lacked AI access due to
mandatory BYOK requirements.
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-270 August 12, 2026 07:51 Destroyed
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-270 August 12, 2026 07:57 Destroyed
Introduces `GRANDFATHERED_CONVERSATION_CAP_START` to allow grandfathered
orgs to adopt the Free tier's candidate conversation limit without being
retroactively blocked for legacy activity. Updates the allowance logic
to
filter conversation counts based on this timestamp.

Additionally, removes the hard exclusion of grandfathered orgs from
platform AI usage. These orgs now resolve to the platform provider by
default, with their usage metered against the Free tier allowance rather
than being restricted to BYOK.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant