Feat/multi-step-job-creation-form#44
Conversation
…, pipeline management, recruitment tools, and security compliance - Introduced AI Candidate Ranking (Glass Box) feature with detailed implementation plans and design notes. - Added Local AI via Ollama for privacy-focused AI model execution. - Implemented Resume Parsing functionality to automate candidate data extraction from resumes. - Created Candidate Portal for self-service application status checks and updates. - Developed Email Notifications system for key hiring events. - Established Application Tracking and Candidate Profiles for better management of candidate data. - Launched Kanban Pipeline Board for visual tracking of candidates through hiring stages. - Enhanced Job Management with structured workflows and CRUD capabilities. - Built Public Job Board for SEO-friendly job listings and application submissions. - Implemented GDPR Data Management features for compliance with data protection regulations. - Established API Rate Limiting and Server-Proxied Documents for enhanced security and data privacy.
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a Feature Catalog page and content collection with many catalog docs, a client-side Giscus comments component, a three-step job-creation wizard, dashboard swipe UI enhancements, header links to /catalog, runtime config/prerender updates, and deletes the Tailwind v4 agent skill SKILL.md. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CatalogPage
participant ContentAPI
participant TreeBuilder
participant Sidebar
participant GiscusWidget
User->>CatalogPage: Request /catalog
CatalogPage->>ContentAPI: fetchAll('catalog')
ContentAPI-->>CatalogPage: catalogItems
CatalogPage->>TreeBuilder: buildTree(catalogItems)
TreeBuilder-->>CatalogPage: hierarchicalNodes
CatalogPage-->>User: render outline + stats
User->>CatalogPage: Click node
CatalogPage->>Sidebar: loadFeatureDetails(nodeId)
Sidebar->>Sidebar: render metadata + markdown
Sidebar->>GiscusWidget: init(term=nodeId)
GiscusWidget-->>Sidebar: display discussion
Sidebar-->>User: show sidebar
sequenceDiagram
participant User
participant JobWizard
participant Validator
participant Server
participant QuestionsAPI
User->>JobWizard: Open /dashboard/jobs/new
JobWizard->>JobWizard: show Step 1
User->>JobWizard: Fill Step1, Click Next
JobWizard->>Validator: validateStep1(data)
Validator-->>JobWizard: ok
JobWizard->>JobWizard: show Step 2 (questions)
User->>JobWizard: add/edit questions
JobWizard->>QuestionsAPI: (deferred) queue question posts
JobWizard->>JobWizard: show Step 3 (targeting)
User->>JobWizard: Click Create
JobWizard->>Validator: validateStep1(data)
Validator-->>JobWizard: ok
JobWizard->>Server: POST /api/jobs (job payload)
Server-->>JobWizard: createdJob
JobWizard->>QuestionsAPI: POST /api/jobs/{id}/questions for each question
QuestionsAPI-->>JobWizard: createdQuestions
JobWizard-->>User: navigate to /dashboard/jobs
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (1)
content/catalog/platform/deployment/index.md (1)
33-33: Avoid hardcoding cloud pricing in evergreen docs.The
~€5/monthfigure will age quickly and can become inaccurate. Consider replacing it with “check current provider pricing” or adding an “as of” date.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@content/catalog/platform/deployment/index.md` at line 33, The line listing "Hetzner Cloud CX23 — 2 vCPU, 4GB RAM (~€5/month)" hardcodes a price that will become outdated; update that entry (the text containing "Hetzner Cloud CX23 — 2 vCPU, 4GB RAM (~€5/month)") to remove the static price and either replace it with a neutral note like "check current provider pricing" or append an "as of <YYYY-MM-DD>" qualifier so the doc remains accurate over time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/components/GiscusComments.vue`:
- Around line 23-25: The Giscus widget fails because the script element in
GiscusComments.vue sets empty data-repo-id and data-category-id; populate those
values from runtime config instead: add giscusRepoId and giscusCategoryId to
your nuxt.config.ts runtimeConfig (using the GitHub repo node ID and Discussions
category node ID), then update the script.setAttribute calls in the component
(the script element created in GiscusComments.vue) to read
processRuntimeConfig.giscusRepoId and processRuntimeConfig.giscusCategoryId (or
use useRuntimeConfig()) so the data-repo-id and data-category-id are non-empty
and the widget can initialize.
In `@app/pages/catalog/index.vue`:
- Around line 352-365: The expand toggle is only clickable via the SVG inside
the row, blocking keyboard users; update the template so the expand control is
its own focusable button element (not just a clickable SVG inside the parent
<button>) and wire its handlers to accessible attributes and events: create a
dedicated toggle button (with aria-expanded bound to isExpanded(feature.path),
aria-controls pointing to the children region, and a visible focusable element)
that calls toggleExpand(feature.path) on click and on key activation
(Enter/Space), while keeping the row click on selectFeature(feature); reference
the existing selectFeature, isExpanded, toggleExpand, and feature.children
symbols when making the change.
- Around line 327-330: The category toggle buttons need ARIA state so screen
readers know whether the branch is open: update the button with a bound
aria-expanded attribute that reflects the current open state (e.g.,
:aria-expanded="isExpanded(category.path)" or
:aria-expanded="expanded.has(category.path)" depending on your state shape) and
add aria-controls pointing to the associated submenu id (e.g.,
:aria-controls="'category-'+category.path") while ensuring the submenu element
has the matching id; apply the same change to the other toggle button usages
(the one using toggleExpand(category.path) at the second location) and ensure
the component exposes an isExpanded helper or reads the same expanded state used
by toggleExpand to keep the attributes in sync.
- Around line 431-434: The icon-only close button that calls closeSidebar lacks
an accessible name; update the <button> that uses `@click`="closeSidebar" to
include an explicit accessible label (e.g., add aria-label="Close sidebar" or
bind a localized label via :aria-label="t('closeSidebar')"), or add visually
hidden text inside the button for screen readers, ensuring the label clearly
identifies the action and references the existing closeSidebar handler.
- Line 509: The external anchor element that opens in a new tab (the <a ...>
anchor with text "Giscus" and target="_blank") is missing rel attributes; update
that anchor to include rel="noopener noreferrer" (and audit other
target="_blank" anchors in the same component to add the same rel attributes) to
prevent tab-nabbing and ensure consistent external-link handling.
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 512-514: The help text paragraph that reads "Our AI can help you
write a compelling job description. Click the magic wand icon in the editor."
(the <p> with classes "text-xs text-brand-700 dark:text-brand-300
leading-relaxed") references a non-existent feature; either remove that <p>
entirely or replace it with accurate guidance that matches the actual
description textarea (the job description textarea/component) — e.g., a neutral
tip about best practices for writing descriptions or a note linking to available
help — and ensure the copy matches existing UI features (do not mention a magic
wand or AI unless you add that editor feature).
- Around line 79-82: The computed canGoNext currently calls validateStep1(),
which mutates errors.value and causes side effects during reactivity; make the
computed pure by creating a non-mutating predicate (e.g., isStep1Valid or
validateStep1Pure) that returns boolean without touching errors.value and use
that in canGoNext, while keeping the existing validateStep1() (which sets
errors.value) for use inside nextStep() and handleSubmit() where showing errors
is intended; update calls in nextStep/handleSubmit to call validateStep1() and
update canGoNext to call the new pure predicate.
- Line 269: The static hint "80 characters left" is wrong; update the template
to show a live remaining count by adding a computed property (e.g.
titleCharsRemaining) that returns 200 - form.value.title.length (matching the
Zod schema max 200) and replace the hardcoded text with the computed value (use
titleCharsRemaining and form.value.title.length as needed); ensure the computed
is imported from Vue and referenced in the template where the current <p v-else>
is rendered.
- Around line 172-177: The "Save draft" button in new.vue currently has no click
handler; add a handler (e.g., `@click`="saveDraft") on the button and implement a
saveDraft method in this component (or setup() return) that persists the current
form state (call the existing job save API or local draft API, or emit an event)
and provides UX feedback (success/error). If you don't want saving now, remove
the button entirely to avoid a dead control; reference the button text "Save
draft" and the component new.vue when making the change.
In `@content/catalog/ai-intelligence/ai-ranking/index.md`:
- Around line 30-37: The table row claiming "EU AI Act compliant" is a
definitive legal claim; update the "EU AI Act compliant" cell in the table (the
row header "EU AI Act compliant") to a cautious phrasing such as "Designed for
compliance (pending legal review)" or "Aims for compliance — subject to formal
validation" to reflect that compliance is a goal pending legal/compliance review
rather than a confirmed fact.
In `@content/catalog/ai-intelligence/index.md`:
- Line 8: The sentence "Applirank eliminates that risk" overstates legal
certainty; update the copy so it avoids absolute guarantees — replace the phrase
"Applirank eliminates that risk" (in the sentence starting "This is not just a
design preference — the [EU AI Act]...") with a mitigated formulation such as
"Applirank helps mitigate that risk" or "Applirank reduces that risk" and ensure
surrounding wording remains grammatically correct and consistent with the
document's tone.
In `@content/catalog/collaboration/interview-scheduling/index.md`:
- Line 20: Fix the missing blank line after the "## Considerations" section
heading so the heading and its paragraph are separated: locate the "##
Considerations" heading in the document (the line starting with "##
Considerations") and insert a single newline/empty line between that heading and
the following paragraph text ("this is a complex feature...") so the heading
renders correctly.
In `@content/catalog/security-compliance/gdpr/index.md`:
- Around line 22-24: Update the "Right to deletion" bullet so it no longer
implies cross-tenant removal; replace the phrase "Remove all candidate data
across all organizations, including S3 documents" (under the "Right to deletion"
heading) with wording that scopes deletion to the requesting controller/tenant
only and references deletion of that tenant's copies (including that tenant's S3
objects) while preserving tenant boundaries and authorization checks.
In `@content/catalog/security-compliance/index.md`:
- Around line 6-8: Update the absolute claim that "input validation on every
endpoint" to accurately reflect implementation gaps by scoping it to validated
inputs (e.g., "request body and query parameter validation on most write
endpoints") and explicitly call out exceptions: list the read-only GET endpoints
(documents/[id]/preview.get.ts, documents/[id]/download.get.ts,
dashboard/stats.get.ts, applications/[id].get.ts, candidates/[id].get.ts,
jobs/[id].get.ts, jobs/[id]/questions/[questionId].delete.ts) and note that
auth/[...all].ts delegates validation to an external auth library;
alternatively, add a short note that validation coverage varies and where to
find the authoritative list. Ensure the revised sentence replaces the current
absolute line and includes a pointer to the exception list or documentation.
In `@content/catalog/security-compliance/rate-limiting/index.md`:
- Around line 14-19: The doc incorrectly says the in-memory limiter is "global"
— clarify that server/utils/rateLimit.ts implements a per-instance (in-memory)
sliding-window limiter applied via the server middleware to API routes, and
update the prose to state this limitation (limits are enforced per process and
can be bypassed across multiple instances); also suggest recommended
alternatives (use a centralized store like Redis/Memcached or an API
gateway/load‑balancer with shared rate-limiting) or note that a distributed rate
limiter is required for multi-node deployments.
---
Nitpick comments:
In `@content/catalog/platform/deployment/index.md`:
- Line 33: The line listing "Hetzner Cloud CX23 — 2 vCPU, 4GB RAM (~€5/month)"
hardcodes a price that will become outdated; update that entry (the text
containing "Hetzner Cloud CX23 — 2 vCPU, 4GB RAM (~€5/month)") to remove the
static price and either replace it with a neutral note like "check current
provider pricing" or append an "as of <YYYY-MM-DD>" qualifier so the doc remains
accurate over time.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (38)
.agents/skills/tailwind-v4-skill/SKILL.mdapp/components/GiscusComments.vueapp/pages/blog/[...slug].vueapp/pages/blog/index.vueapp/pages/catalog/index.vueapp/pages/dashboard/jobs/new.vueapp/pages/index.vueapp/pages/roadmap.vuecontent.config.tscontent/catalog/ai-intelligence/ai-ranking/index.mdcontent/catalog/ai-intelligence/index.mdcontent/catalog/ai-intelligence/local-ai/index.mdcontent/catalog/ai-intelligence/resume-parsing/index.mdcontent/catalog/collaboration/candidate-portal/index.mdcontent/catalog/collaboration/email-notifications/index.mdcontent/catalog/collaboration/index.mdcontent/catalog/collaboration/interview-scheduling/index.mdcontent/catalog/collaboration/team-comments/index.mdcontent/catalog/mobile-support/index.mdcontent/catalog/mobile-support/responsive-dashboard/index.mdcontent/catalog/pipeline-management/application-tracking/index.mdcontent/catalog/pipeline-management/candidate-profiles/index.mdcontent/catalog/pipeline-management/index.mdcontent/catalog/pipeline-management/job-management/index.mdcontent/catalog/pipeline-management/kanban-board/index.mdcontent/catalog/platform/dashboard/index.mdcontent/catalog/platform/deployment/index.mdcontent/catalog/platform/index.mdcontent/catalog/platform/multi-tenant/index.mdcontent/catalog/recruitment-tools/custom-application-forms/index.mdcontent/catalog/recruitment-tools/document-storage/index.mdcontent/catalog/recruitment-tools/index.mdcontent/catalog/recruitment-tools/public-job-board/index.mdcontent/catalog/security-compliance/gdpr/index.mdcontent/catalog/security-compliance/index.mdcontent/catalog/security-compliance/rate-limiting/index.mdcontent/catalog/security-compliance/server-proxied-documents/index.mdnuxt.config.ts
💤 Files with no reviewable changes (1)
- .agents/skills/tailwind-v4-skill/SKILL.md
| <button | ||
| class="rounded-md p-1.5 text-white/40 transition hover:bg-white/[0.06] hover:text-white" | ||
| @click="closeSidebar" | ||
| > |
There was a problem hiding this comment.
Icon-only close button needs an accessible name.
Without an explicit label, screen readers announce an unnamed button.
Suggested change
<button
+ type="button"
+ aria-label="Close feature details"
class="rounded-md p-1.5 text-white/40 transition hover:bg-white/[0.06] hover:text-white"
`@click`="closeSidebar"
>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/catalog/index.vue` around lines 431 - 434, The icon-only close
button that calls closeSidebar lacks an accessible name; update the <button>
that uses `@click`="closeSidebar" to include an explicit accessible label (e.g.,
add aria-label="Close sidebar" or bind a localized label via
:aria-label="t('closeSidebar')"), or add visually hidden text inside the button
for screen readers, ensuring the label clearly identifies the action and
references the existing closeSidebar handler.
| </h4> | ||
| <p class="text-[12px] text-white/30 mb-3"> | ||
| Sign in with GitHub to comment. Discussions are powered by | ||
| <a href="https://giscus.app" target="_blank" class="text-brand-400 hover:underline">Giscus</a>. |
There was a problem hiding this comment.
Add rel on external link opened in a new tab.
Use rel="noopener noreferrer" consistently for _blank links.
Suggested change
- <a href="https://giscus.app" target="_blank" class="text-brand-400 hover:underline">Giscus</a>.
+ <a href="https://giscus.app" target="_blank" rel="noopener noreferrer" class="text-brand-400 hover:underline">Giscus</a>.📝 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.
| <a href="https://giscus.app" target="_blank" class="text-brand-400 hover:underline">Giscus</a>. | |
| <a href="https://giscus.app" target="_blank" rel="noopener noreferrer" class="text-brand-400 hover:underline">Giscus</a>. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/catalog/index.vue` at line 509, The external anchor element that
opens in a new tab (the <a ...> anchor with text "Giscus" and target="_blank")
is missing rel attributes; update that anchor to include rel="noopener
noreferrer" (and audit other target="_blank" anchors in the same component to
add the same rel attributes) to prevent tab-nabbing and ensure consistent
external-link handling.
|
|
||
| Applirank's AI features follow the **Glass Box** principle: every AI decision must be explainable and auditable. When the system ranks a candidate, it shows you exactly which skills matched, why the score was assigned, and what data drove the recommendation. | ||
|
|
||
| This is not just a design preference — the [EU AI Act](https://artificialintelligenceact.eu/) classifies employment decisions as high-risk AI, meaning opaque ranking algorithms create real legal liability. Applirank eliminates that risk by making AI reasoning fully visible. |
There was a problem hiding this comment.
Avoid absolute legal guarantees (“eliminates that risk”).
Line 8 overstates legal certainty. Prefer “reduces risk” or “helps mitigate risk” to avoid compliance/marketing liability.
📝 Suggested wording
-Applirank eliminates that risk by making AI reasoning fully visible.
+Applirank helps mitigate that risk by making AI reasoning fully visible.📝 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.
| This is not just a design preference — the [EU AI Act](https://artificialintelligenceact.eu/) classifies employment decisions as high-risk AI, meaning opaque ranking algorithms create real legal liability. Applirank eliminates that risk by making AI reasoning fully visible. | |
| This is not just a design preference — the [EU AI Act](https://artificialintelligenceact.eu/) classifies employment decisions as high-risk AI, meaning opaque ranking algorithms create real legal liability. Applirank helps mitigate that risk by making AI reasoning fully visible. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@content/catalog/ai-intelligence/index.md` at line 8, The sentence "Applirank
eliminates that risk" overstates legal certainty; update the copy so it avoids
absolute guarantees — replace the phrase "Applirank eliminates that risk" (in
the sentence starting "This is not just a design preference — the [EU AI
Act]...") with a mitigated formulation such as "Applirank helps mitigate that
risk" or "Applirank reduces that risk" and ensure surrounding wording remains
grammatically correct and consistent with the document's tone.
|
|
||
| Interview scheduling is one of the most tedious parts of recruiting. Coordinators spend hours going back and forth between candidates and interviewers to find available slots. An integrated scheduler reduces this to a few clicks. | ||
|
|
||
| ## Considerationsthis is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API. |
There was a problem hiding this comment.
Fix missing space after section heading.
There's a missing line break between the "## Considerations" heading and the paragraph text, causing them to run together.
📝 Proposed fix
-## Considerationsthis is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API.
+## Considerations
+
+This is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API.📝 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.
| ## Considerationsthis is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API. | |
| ## Considerations | |
| This is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API. |
🧰 Tools
🪛 LanguageTool
[grammar] ~20-~20: Ensure spelling is correct
Context: ...duler reduces this to a few clicks. ## Considerationsthis is a complex feature with many edge cas...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@content/catalog/collaboration/interview-scheduling/index.md` at line 20, Fix
the missing blank line after the "## Considerations" section heading so the
heading and its paragraph are separated: locate the "## Considerations" heading
in the document (the line starting with "## Considerations") and insert a single
newline/empty line between that heading and the following paragraph text ("this
is a complex feature...") so the heading renders correctly.
| - **Data export** — Generate a JSON/CSV bundle of all data associated with a candidate (profile, applications, documents, question responses) | ||
| - **Right to deletion** — Remove all candidate data across all organizations, including S3 documents | ||
| - **Consent tracking** — Record when and how consent was given for data processing |
There was a problem hiding this comment.
Avoid cross-tenant deletion semantics in the GDPR plan.
Line 23 currently implies erasing candidate data across all organizations. That would violate tenant boundaries and can create authorization/compliance issues. Scope deletion to the requesting controller/tenant only.
Suggested wording update
-- **Right to deletion** — Remove all candidate data across all organizations, including S3 documents
+- **Right to deletion** — Remove all candidate data for the requesting organization/workspace, including related S3 documents
+- **Tenant isolation** — Process deletion per controller/tenant; never allow one tenant’s request to erase another tenant’s data🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@content/catalog/security-compliance/gdpr/index.md` around lines 22 - 24,
Update the "Right to deletion" bullet so it no longer implies cross-tenant
removal; replace the phrase "Remove all candidate data across all organizations,
including S3 documents" (under the "Right to deletion" heading) with wording
that scopes deletion to the requesting controller/tenant only and references
deletion of that tenant's copies (including that tenant's S3 objects) while
preserving tenant boundaries and authorization checks.
| Security in an ATS isn't optional — you're handling people's personal data, resumes, and employment history. Applirank ships with production-ready security defaults: API rate limiting, comprehensive security headers, server-proxied document access, and input validation on every endpoint. | ||
|
|
||
| Every security feature is on by default. You don't have to remember to enable it. |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Read-only reconnaissance: inspect endpoint definitions vs validation usage.
echo "=== Endpoint handlers ==="
fd -e ts -e js server | xargs rg -nP 'defineEventHandler|eventHandler|router\.(get|post|put|delete|patch)' -C2
echo "=== Validation patterns ==="
fd -e ts -e js server | xargs rg -nP 'readValidatedBody|getValidatedQuery|safeParse|\.parse\(|z\.object\(' -C2Repository: reqcore-inc/applirank
Length of output: 34902
Scope the input validation claim to reflect actual implementation gaps.
The claim "input validation on every endpoint" overstates coverage. Several endpoints lack explicit validation for route parameters:
- Read-only GET endpoints:
documents/[id]/preview.get.ts,documents/[id]/download.get.ts,dashboard/stats.get.ts,applications/[id].get.ts,candidates/[id].get.ts,jobs/[id].get.ts,jobs/[id]/questions/[questionId].delete.ts - The
auth/[...all].tshandler delegates validation to an external auth library without visible guardrails
Either add validation guardrails to these endpoints and document them consistently, or update the documentation to qualify which endpoints have validation (e.g., "request body and query parameter validation" rather than "every endpoint"). The claims as written are too absolute to maintain reliably.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@content/catalog/security-compliance/index.md` around lines 6 - 8, Update the
absolute claim that "input validation on every endpoint" to accurately reflect
implementation gaps by scoping it to validated inputs (e.g., "request body and
query parameter validation on most write endpoints") and explicitly call out
exceptions: list the read-only GET endpoints (documents/[id]/preview.get.ts,
documents/[id]/download.get.ts, dashboard/stats.get.ts,
applications/[id].get.ts, candidates/[id].get.ts, jobs/[id].get.ts,
jobs/[id]/questions/[questionId].delete.ts) and note that auth/[...all].ts
delegates validation to an external auth library; alternatively, add a short
note that validation coverage varies and where to find the authoritative list.
Ensure the revised sentence replaces the current absolute line and includes a
pointer to the exception list or documentation.
| Global per-IP rate limiting on all `/api` endpoints with stricter thresholds for authentication and write operations. | ||
|
|
||
| ## What it does | ||
|
|
||
| - In-memory sliding window rate limiter (`server/utils/rateLimit.ts`) | ||
| - Applied globally via server middleware to all API routes |
There was a problem hiding this comment.
Avoid calling an in-memory limiter “global” across deployments.
Line 14 and Line 18 describe global coverage, but an in-memory limiter is enforced per instance. In multi-instance deployments, limits can be bypassed by spreading requests across nodes.
📝 Suggested wording update
-Global per-IP rate limiting on all `/api` endpoints with stricter thresholds for authentication and write operations.
+Per-instance per-IP rate limiting on all `/api` endpoints, with stricter thresholds for authentication and write operations.
-- In-memory sliding window rate limiter (`server/utils/rateLimit.ts`)
-- Applied globally via server middleware to all API routes
+- In-memory sliding window rate limiter (`server/utils/rateLimit.ts`)
+- Applied via server middleware to all API routes in each app instance
+- For true cluster-wide/global enforcement, back the limiter with a shared store (e.g., Redis)📝 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.
| Global per-IP rate limiting on all `/api` endpoints with stricter thresholds for authentication and write operations. | |
| ## What it does | |
| - In-memory sliding window rate limiter (`server/utils/rateLimit.ts`) | |
| - Applied globally via server middleware to all API routes | |
| Per-instance per-IP rate limiting on all `/api` endpoints, with stricter thresholds for authentication and write operations. | |
| ## What it does | |
| - In-memory sliding window rate limiter (`server/utils/rateLimit.ts`) | |
| - Applied via server middleware to all API routes in each app instance | |
| - For true cluster-wide/global enforcement, back the limiter with a shared store (e.g., Redis) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@content/catalog/security-compliance/rate-limiting/index.md` around lines 14 -
19, The doc incorrectly says the in-memory limiter is "global" — clarify that
server/utils/rateLimit.ts implements a per-instance (in-memory) sliding-window
limiter applied via the server middleware to API routes, and update the prose to
state this limitation (limits are enforced per process and can be bypassed
across multiple instances); also suggest recommended alternatives (use a
centralized store like Redis/Memcached or an API gateway/load‑balancer with
shared rate-limiting) or note that a distributed rate limiter is required for
multi-node deployments.
…ume and cover letter
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
app/pages/dashboard/jobs/new.vue (2)
258-274: Partial failure risk: questions may fail after job creation.If
createJobsucceeds but posting questions fails, the job exists without its questions.Promise.allrejects on first failure, leaving the system in a partial state. Consider:
- Catching question-posting errors separately and showing a warning (job created, questions partially saved)
- Or using
Promise.allSettledto attempt all questions and report individual failuresOption: Handle partial question failures gracefully
if (applicationForm.value.questions.length > 0 && created?.id) { - await Promise.all( + const results = await Promise.allSettled( applicationForm.value.questions.map((question, index) => ( $fetch(`/api/jobs/${created.id}/questions`, { method: 'POST', body: { label: question.label, type: question.type, description: question.description || undefined, required: question.required, options: question.options || undefined, displayOrder: index, }, }) - )), + )) ) + const failures = results.filter((r) => r.status === 'rejected') + if (failures.length > 0) { + console.warn(`${failures.length} question(s) failed to save`) + // Optionally show user warning but still navigate + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 258 - 274, The current block that posts applicationForm.value.questions after job creation uses Promise.all which will abort on the first failed $fetch and can leave the job without some or all questions; replace Promise.all with Promise.allSettled when mapping over applicationForm.value.questions (and keep the POST to /api/jobs/${created.id}/questions), then inspect the settled results to detect failures and surface a concise warning to the user (e.g., "Job created but X questions failed to save") while logging individual errors for debugging; alternatively, wrap each $fetch in its own try/catch and collect failures to report the same way—ensure you reference the questions mapping and created?.id when implementing this change.
221-235: Consider case-insensitive duplicate skill detection.The current check
!findCandidates.value.skills.includes(skill)is case-sensitive, so "Vue" and "vue" would both be added. Consider normalizing for comparison:Optional: Case-insensitive check
.forEach((skill) => { - if (!findCandidates.value.skills.includes(skill)) { + const normalizedSkill = skill.toLowerCase() + const isDuplicate = findCandidates.value.skills.some( + (s) => s.toLowerCase() === normalizedSkill + ) + if (!isDuplicate) { findCandidates.value.skills.push(skill) } })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 221 - 235, The addSkillFromInput function currently checks duplicates case-sensitively; update the duplicate check to compare case-insensitively by normalizing both the incoming skill and existing entries (e.g., use skillLower = skill.toLowerCase() and check !findCandidates.value.skills.some(s => s.toLowerCase() === skillLower)) before pushing to findCandidates.value.skills so "Vue" and "vue" are treated as duplicates while preserving the pushed value format you prefer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 162-169: The object spread into
applicationForm.value.questions[index] makes the id field become optional; fix
it by explicitly preserving the original id when replacing the question object:
read the existing question (applicationForm.value.questions[index]) and include
its id in the new object construction so id remains required (e.g., set id:
existingQuestion.id) while assigning label, type, description, required, and
options to retain type safety for the questions array.
- Around line 184-189: The swap in moveQuestion is failing TS because
list[index] and list[targetIndex] may be typed as possibly undefined; update the
swap to assert non-null (safe after the bounds check) or guard with explicit
local variables: compute const a = list[index]!, const b = list[targetIndex]!
(or return early if either is undefined) and then assign list[index] = b;
list[targetIndex] = a; reference moveQuestion, applicationForm.value.questions,
list, index and targetIndex when making the change.
---
Nitpick comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 258-274: The current block that posts
applicationForm.value.questions after job creation uses Promise.all which will
abort on the first failed $fetch and can leave the job without some or all
questions; replace Promise.all with Promise.allSettled when mapping over
applicationForm.value.questions (and keep the POST to
/api/jobs/${created.id}/questions), then inspect the settled results to detect
failures and surface a concise warning to the user (e.g., "Job created but X
questions failed to save") while logging individual errors for debugging;
alternatively, wrap each $fetch in its own try/catch and collect failures to
report the same way—ensure you reference the questions mapping and created?.id
when implementing this change.
- Around line 221-235: The addSkillFromInput function currently checks
duplicates case-sensitively; update the duplicate check to compare
case-insensitively by normalizing both the incoming skill and existing entries
(e.g., use skillLower = skill.toLowerCase() and check
!findCandidates.value.skills.some(s => s.toLowerCase() === skillLower)) before
pushing to findCandidates.value.skills so "Vue" and "vue" are treated as
duplicates while preserving the pushed value format you prefer.
…wipe component - Implemented search feature to filter candidates by name and email. - Updated candidate selection logic to work with filtered applications. - Enhanced candidate detail view with additional information and improved layout. - Added transition labels and classes for better status management. - Improved navigation and action buttons for better user experience.
There was a problem hiding this comment.
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/pages/dashboard/jobs/[id]/swipe.vue (1)
104-120:⚠️ Potential issue | 🟠 MajorIndex can go out of bounds when search filters the list.
The watcher at line 104 adjusts
currentIndexwhenfocusedApplicationschanges, but there's no equivalent logic whenfilteredApplicationsshrinks due to a search. If the user hascurrentIndex = 5and then searches to get 3 results,filteredApplications[5]returnsundefined, causing the detail panel to show "No candidates" while the candidate list displays matches.🐛 Proposed fix: Add a watcher on filteredApplications
watch(focusedApplications, () => { if (focusedApplications.value.length === 0) { currentIndex.value = 0 return } if (currentIndex.value >= focusedApplications.value.length) { currentIndex.value = focusedApplications.value.length - 1 } }, { immediate: true }) +watch(filteredApplications, () => { + if (filteredApplications.value.length === 0) { + currentIndex.value = 0 + return + } + if (currentIndex.value >= filteredApplications.value.length) { + currentIndex.value = filteredApplications.value.length - 1 + } +}) + watch(focusStatus, () => { currentIndex.value = 0 searchTerm.value = '' })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/`[id]/swipe.vue around lines 104 - 120, Add a watcher for filteredApplications that mirrors the focusedApplications watcher: when filteredApplications.value is empty set currentIndex.value = 0; otherwise if currentIndex.value >= filteredApplications.value.length clamp it to filteredApplications.value.length - 1 so currentSummary (computed from filteredApplications[currentIndex]) never becomes undefined; reference the existing currentIndex, filteredApplications and currentSummary symbols and follow the same { immediate: true } behavior as the focusedApplications watcher.
♻️ Duplicate comments (1)
app/pages/dashboard/jobs/new.vue (1)
804-809:⚠️ Potential issue | 🟡 MinorHelp text still references non-existent magic wand feature.
This was previously marked as addressed, but the text "Click the magic wand icon in the editor" remains. No such icon exists in the description textarea. This may have been inadvertently reintroduced.
Suggested fix
<div v-if="currentStep === 1" class="p-6 rounded-xl bg-brand-50 dark:bg-brand-900/20 border border-brand-100 dark:border-brand-900/30"> <h4 class="text-sm font-semibold text-brand-900 dark:text-brand-100 mb-2">Need help?</h4> <p class="text-xs text-brand-700 dark:text-brand-300 leading-relaxed"> - Our AI can help you write a compelling job description. Click the magic wand icon in the editor. + Write a clear, detailed description to attract qualified candidates. </p> </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 804 - 809, The help text inside the template block shown when currentStep === 1 still mentions a non-existent "magic wand" icon; update the paragraph text in app/pages/dashboard/jobs/new.vue (the <div v-if="currentStep === 1"> block and specifically the <p> help paragraph) to remove or replace the "Click the magic wand icon in the editor" phrase with an accurate instruction (e.g., point to the real AI helper or a generic "use the AI assistant in the editor" or simply remove that clause) so the UI text matches existing editor controls.
🧹 Nitpick comments (4)
app/pages/dashboard/jobs/new.vue (2)
688-706: No limit on skills array in Step 3.Users can add unlimited skills. Consider adding a reasonable cap (e.g., 20) with feedback when the limit is reached, similar to the 5-question limit in Step 2.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 688 - 706, The skills list (findCandidates.skills) currently has no upper bound; update the add logic in the addSkillFromInput method to enforce a max (e.g., MAX_SKILLS = 20) and return/display a user-facing message when the cap is reached (disable adding on `@keydown.enter` and `@blur` and avoid pushing new skills if length >= MAX_SKILLS). Also update the template to show a small feedback element near the input (e.g., "You can add up to 20 skills") and optionally disable or hide the add button behavior when findCandidates.skills.length >= MAX_SKILLS; keep removeSkill(idx) unchanged so users can delete to free space. Ensure the limit constant and checks are added where addSkillFromInput and the input event handlers are defined.
566-569: GripVertical icon suggests drag-and-drop but only buttons exist.The grip icon typically indicates draggable elements, but reordering is only possible via the up/down buttons. This could confuse users expecting drag functionality.
Consider either removing the grip icon or implementing actual drag-and-drop reordering.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 566 - 569, The GripVertical icon (GripVertical) suggests drag-and-drop but the UI only supports reordering via the existing up/down buttons (e.g., the move-up/move-down handlers), so either remove the GripVertical element to avoid misleading users or implement real drag-and-drop: integrate a drag library (like Vue.Draggable), wrap the jobs list in the draggable container, add handlers (onDragStart/onEnd or `@update`) that call the existing reorder logic or update the jobs array directly, and ensure the same persistence/validation used by the move-up/move-down methods is invoked after reordering.app/pages/dashboard/jobs/[id]/swipe.vue (2)
251-260: Consider handling edge cases for very recent or future timestamps.When
minsis 0 or negative (clock skew, future date), this returns "0m ago" or a negative value. A small guard would improve UX.♻️ Suggested improvement
function timeAgo(date: string | Date) { const diff = Date.now() - new Date(date).getTime() const mins = Math.floor(diff / 60_000) + if (mins < 1) return 'Just now' if (mins < 60) return `${mins}m ago` const hrs = Math.floor(mins / 60) if (hrs < 24) return `${hrs}h ago` const days = Math.floor(hrs / 24) if (days < 30) return `${days}d ago` return new Date(date).toLocaleDateString() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/`[id]/swipe.vue around lines 251 - 260, The timeAgo function should guard against zero or negative diffs and clearly handle future timestamps: in the timeAgo implementation, detect when mins <= 0 and return a friendly "just now" (or similar) string, and for negative diffs (new Date(date) is in the future) optionally format as "in Xm" / "in Xh" or clamp to "just now" to avoid showing "0m ago" or negative values; update the logic inside the timeAgo function to check the computed diff/mins and branch accordingly before returning minutes/hours/days or the date string.
829-848: Consider adding aria-labels for navigation buttons.The navigation buttons use icons with text, which is generally accessible, but adding
aria-labelattributes would improve screen reader support, especially since the button state (e.g., "Previous candidate, 1 of 5") provides useful context.♿ Optional accessibility enhancement
<button :disabled="currentIndex === 0" + :aria-label="`Previous candidate`" class="flex flex-1 cursor-pointer items-center justify-center gap-1 rounded-lg border border-surface-200 px-2.5 py-1.5 text-xs text-surface-600 transition-colors hover:bg-surface-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-surface-700 dark:text-surface-400 dark:hover:bg-surface-800" `@click`="goToPreviousCard" >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/`[id]/swipe.vue around lines 829 - 848, Add descriptive aria-label attributes to the Prev and Next navigation buttons in the template to improve screen reader context: update the button elements that call goToPreviousCard and goToNextCard to include aria-labels like "Previous candidate, {currentIndex + 1} of {filteredApplications.length}" and "Next candidate, {currentIndex + 1} of {filteredApplications.length}" respectively (use the existing currentIndex and filteredApplications bindings to build the dynamic label); also ensure the disabled state remains intact (the existing :disabled binding can stay) so screen readers know when the control is inactive.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 203-207: The preview applicationLink computed property returns a
URL with a placeholder ID ("xxxxxxxx") which can confuse users if copied; update
the UI to prevent copying invalid links by either disabling the
copy-to-clipboard control until the job is created or by adding a clear
tooltip/label indicating the link is a non-functional preview. Locate the
applicationLink computed property and the copy button component used near the
preview display, then wire the copy button's disabled state or tooltip to a new
reactive flag (e.g., jobCreated or isPreview) derived from the form/job creation
status so the button is disabled or shows a tooltip when applicationLink
contains the placeholder.
- Around line 258-274: The current use of Promise.all when posting
applicationForm.value.questions for created?.id can leave some question POSTs
failed while others succeed and the user is still navigated away; change the
logic to use Promise.allSettled on the array returned from
applicationForm.value.questions.map(...) and then inspect the settled results to
collect any failures; if any entries are rejected, surface an error (e.g., set
an error state or show a notification) and avoid navigating to /dashboard/jobs
(or retry the failed posts), otherwise proceed normally—update the code
references around the Promise.all call and the created?.id handling to implement
this settled-result check and user-facing error handling.
---
Outside diff comments:
In `@app/pages/dashboard/jobs/`[id]/swipe.vue:
- Around line 104-120: Add a watcher for filteredApplications that mirrors the
focusedApplications watcher: when filteredApplications.value is empty set
currentIndex.value = 0; otherwise if currentIndex.value >=
filteredApplications.value.length clamp it to filteredApplications.value.length
- 1 so currentSummary (computed from filteredApplications[currentIndex]) never
becomes undefined; reference the existing currentIndex, filteredApplications and
currentSummary symbols and follow the same { immediate: true } behavior as the
focusedApplications watcher.
---
Duplicate comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 804-809: The help text inside the template block shown when
currentStep === 1 still mentions a non-existent "magic wand" icon; update the
paragraph text in app/pages/dashboard/jobs/new.vue (the <div v-if="currentStep
=== 1"> block and specifically the <p> help paragraph) to remove or replace the
"Click the magic wand icon in the editor" phrase with an accurate instruction
(e.g., point to the real AI helper or a generic "use the AI assistant in the
editor" or simply remove that clause) so the UI text matches existing editor
controls.
---
Nitpick comments:
In `@app/pages/dashboard/jobs/`[id]/swipe.vue:
- Around line 251-260: The timeAgo function should guard against zero or
negative diffs and clearly handle future timestamps: in the timeAgo
implementation, detect when mins <= 0 and return a friendly "just now" (or
similar) string, and for negative diffs (new Date(date) is in the future)
optionally format as "in Xm" / "in Xh" or clamp to "just now" to avoid showing
"0m ago" or negative values; update the logic inside the timeAgo function to
check the computed diff/mins and branch accordingly before returning
minutes/hours/days or the date string.
- Around line 829-848: Add descriptive aria-label attributes to the Prev and
Next navigation buttons in the template to improve screen reader context: update
the button elements that call goToPreviousCard and goToNextCard to include
aria-labels like "Previous candidate, {currentIndex + 1} of
{filteredApplications.length}" and "Next candidate, {currentIndex + 1} of
{filteredApplications.length}" respectively (use the existing currentIndex and
filteredApplications bindings to build the dynamic label); also ensure the
disabled state remains intact (the existing :disabled binding can stay) so
screen readers know when the control is inactive.
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 688-706: The skills list (findCandidates.skills) currently has no
upper bound; update the add logic in the addSkillFromInput method to enforce a
max (e.g., MAX_SKILLS = 20) and return/display a user-facing message when the
cap is reached (disable adding on `@keydown.enter` and `@blur` and avoid pushing new
skills if length >= MAX_SKILLS). Also update the template to show a small
feedback element near the input (e.g., "You can add up to 20 skills") and
optionally disable or hide the add button behavior when
findCandidates.skills.length >= MAX_SKILLS; keep removeSkill(idx) unchanged so
users can delete to free space. Ensure the limit constant and checks are added
where addSkillFromInput and the input event handlers are defined.
- Around line 566-569: The GripVertical icon (GripVertical) suggests
drag-and-drop but the UI only supports reordering via the existing up/down
buttons (e.g., the move-up/move-down handlers), so either remove the
GripVertical element to avoid misleading users or implement real drag-and-drop:
integrate a drag library (like Vue.Draggable), wrap the jobs list in the
draggable container, add handlers (onDragStart/onEnd or `@update`) that call the
existing reorder logic or update the jobs array directly, and ensure the same
persistence/validation used by the move-up/move-down methods is invoked after
reordering.
| const applicationLink = computed(() => { | ||
| const base = `${requestUrl.protocol}//${requestUrl.host}` | ||
| const slugBase = slugifyTitle(form.value.title) || 'new-job' | ||
| return `${base}/jobs/${slugBase}-xxxxxxxx/apply` | ||
| }) |
There was a problem hiding this comment.
Preview link with placeholder may confuse users if copied.
applicationLink generates a URL with xxxxxxxx as a placeholder ID. The UI at line 481 mentions it's a preview, but allowing copy-to-clipboard for an invalid URL could mislead users.
Consider disabling the copy button until the job is created, or adding a tooltip clarifying the link isn't functional yet.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/dashboard/jobs/new.vue` around lines 203 - 207, The preview
applicationLink computed property returns a URL with a placeholder ID
("xxxxxxxx") which can confuse users if copied; update the UI to prevent copying
invalid links by either disabling the copy-to-clipboard control until the job is
created or by adding a clear tooltip/label indicating the link is a
non-functional preview. Locate the applicationLink computed property and the
copy button component used near the preview display, then wire the copy button's
disabled state or tooltip to a new reactive flag (e.g., jobCreated or isPreview)
derived from the form/job creation status so the button is disabled or shows a
tooltip when applicationLink contains the placeholder.
| if (applicationForm.value.questions.length > 0 && created?.id) { | ||
| await Promise.all( | ||
| applicationForm.value.questions.map((question, index) => ( | ||
| $fetch(`/api/jobs/${created.id}/questions`, { | ||
| method: 'POST', | ||
| body: { | ||
| label: question.label, | ||
| type: question.type, | ||
| description: question.description || undefined, | ||
| required: question.required, | ||
| options: question.options || undefined, | ||
| displayOrder: index, | ||
| }, | ||
| }) | ||
| )), | ||
| ) | ||
| } |
There was a problem hiding this comment.
Partial failure in Promise.all leaves questions in inconsistent state.
If one question POST fails, Promise.all rejects immediately while other requests may still be in-flight. The job is already created (line 251), so the user is navigated to /dashboard/jobs unaware that some questions weren't saved.
Consider using Promise.allSettled to capture individual failures and report them:
Proposed fix using Promise.allSettled
if (applicationForm.value.questions.length > 0 && created?.id) {
- await Promise.all(
+ const results = await Promise.allSettled(
applicationForm.value.questions.map((question, index) => (
$fetch(`/api/jobs/${created.id}/questions`, {
method: 'POST',
body: {
label: question.label,
type: question.type,
description: question.description || undefined,
required: question.required,
options: question.options || undefined,
displayOrder: index,
},
})
)),
)
+ const failures = results.filter((r) => r.status === 'rejected')
+ if (failures.length > 0) {
+ console.error('Some questions failed to save:', failures)
+ // Optionally show a warning to the user before navigating
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/dashboard/jobs/new.vue` around lines 258 - 274, The current use of
Promise.all when posting applicationForm.value.questions for created?.id can
leave some question POSTs failed while others succeed and the user is still
navigated away; change the logic to use Promise.allSettled on the array returned
from applicationForm.value.questions.map(...) and then inspect the settled
results to collect any failures; if any entries are rejected, surface an error
(e.g., set an error state or show a notification) and avoid navigating to
/dashboard/jobs (or retry the failed posts), otherwise proceed normally—update
the code references around the Promise.all call and the created?.id handling to
implement this settled-result check and user-facing error handling.
… enhance job question update handling
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (9)
app/pages/dashboard/jobs/new.vue (6)
261-277:⚠️ Potential issue | 🟠 MajorPartial failure in
Promise.allstill leaves questions in inconsistent state.If one question POST fails,
Promise.allrejects immediately while other requests may still be in-flight or succeed. The job is already created, so the user is navigated away unaware that some questions weren't saved.Suggested fix using Promise.allSettled
if (applicationForm.value.questions.length > 0 && created?.id) { - await Promise.all( + const results = await Promise.allSettled( applicationForm.value.questions.map((question, index) => ( $fetch(`/api/jobs/${created.id}/questions`, { method: 'POST', body: { label: question.label, type: question.type, description: question.description || undefined, required: question.required, options: question.options || undefined, displayOrder: index, }, }) )), ) + const failures = results.filter((r) => r.status === 'rejected') + if (failures.length > 0) { + console.error('Some questions failed to save:', failures) + // Consider showing a warning toast before navigating + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 261 - 277, Replace the Promise.all call that posts applicationForm.value.questions for created?.id with Promise.allSettled, inspect each result, and handle failures explicitly: collect failed question uploads (reference applicationForm.value.questions and created?.id), retry or surface an error to the user and prevent silent navigation away if any question POSTs failed; ensure successful responses are kept and failed ones are reported/logged so the UI can show which questions need retrying rather than leaving the job in a partially-updated state.
321-327:⚠️ Potential issue | 🟠 Major"Save draft" button still has no functionality.
This button lacks a click handler, so clicking it does nothing. Either implement draft saving or remove the button to avoid user confusion.
Option: Remove until implemented
- <button - type="button" - class="px-4 py-2 text-sm font-medium text-surface-700 dark:text-surface-300 bg-white dark:bg-surface-900 border border-surface-300 dark:border-surface-700 rounded-lg hover:bg-surface-50 dark:hover:bg-surface-800 transition-colors" - > - Save draft - </button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 321 - 327, The "Save draft" button in the new.vue template has no click handler and therefore does nothing; either remove the button or wire it up to an action: add a `@click` handler (e.g. `@click`="saveDraft") on the button and implement a saveDraft method in the component's script that performs draft persistence (calling your existing save method or an API, e.g. reuse createJob/updateJob logic or emit an event), or remove the button markup entirely until the draft flow (saveDraft) is implemented to avoid confusing users.
113-116:⚠️ Potential issue | 🟠 MajorComputed property
canGoNextstill has side effects.
validateStep1()mutateserrors.value, but it's called inside a computed property. This causes validation errors to appear/disappear reactively during render cycles, which is unexpected behavior.Suggested fix: separate validation check from error-setting
+const isStep1Valid = computed(() => { + const result = formSchema.safeParse(form.value) + return result.success +}) + const canGoNext = computed(() => { - if (currentStep.value === 1) return validateStep1() + if (currentStep.value === 1) return isStep1Valid.value return true })Keep
validateStep1()for use innextStep()andhandleSubmit()where showing errors is intended.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 113 - 116, The computed canGoNext calls validateStep1(), which mutates errors.value during render; extract a pure validator (e.g., isStep1Valid or validateStep1Silent) that returns boolean without touching errors.value and use that in the computed property canGoNext, while keeping the existing validateStep1() (which sets errors.value) for use inside nextStep() and handleSubmit() where showing errors is intended; update references so canGoNext -> isStep1Valid and nextStep()/handleSubmit() still call validateStep1().
419-419:⚠️ Potential issue | 🟡 MinorHardcoded character count is still misleading.
The text "80 characters left" is static and doesn't reflect actual input length. The Zod schema allows 200 characters (line 93), but this hint suggests a different limit.
Suggested fix: Dynamic character count
Add a computed property:
const titleCharsRemaining = computed(() => 200 - form.value.title.length)Then update the template:
- <p v-else class="mt-1.5 text-xs text-surface-500">80 characters left. No special characters.</p> + <p v-else class="mt-1.5 text-xs text-surface-500">{{ titleCharsRemaining }} characters left.</p>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` at line 419, The "80 characters left" hint is hardcoded and inconsistent with the Zod 200-char rule; add a computed property (e.g., titleCharsRemaining = computed(() => Math.max(0, 200 - (form.value.title?.length || 0)))) and replace the static paragraph text with a binding that displays the dynamic count and optionally pluralizes or clamps at 0; update references to use form.value.title length so the UI always reflects the Zod max length of 200.
206-210:⚠️ Potential issue | 🟡 MinorPreview link with placeholder ID can mislead users.
The
applicationLinkcomputed generates a URL withxxxxxxxxas a placeholder. While the UI mentions it's a "preview", the copy button allows copying an invalid URL that won't work.Consider disabling the copy button until the job is created, or adding a tooltip clarifying the link isn't functional yet.
Also applies to: 493-500
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 206 - 210, The preview URL produced by the applicationLink computed uses a fake "xxxxxxxx" ID and can be copied as if valid; change applicationLink (computed) to return either a non-copyable placeholder (empty string or a clearly non-URL value) when there is no real job id (e.g., check form.value.id or job.id/persisted flag) and return the real URL only when the job id exists, and then update the copy button's disabled binding to be true when the job is not yet created (disable copy if !form.value.id) and add a tooltip on the copy button explaining "Preview — link not active until job is created" so users cannot copy an invalid URL; locate and modify the applicationLink computed and the copy button component usage to implement these checks and the tooltip.
807-812:⚠️ Potential issue | 🟡 MinorHelp text still references non-existent feature.
The tip mentions "Click the magic wand icon in the editor" but the description textarea (lines 464-470) has no such icon. This was previously flagged and marked as addressed, but the text remains.
Suggested fix
<p class="text-xs text-brand-700 dark:text-brand-300 leading-relaxed"> - Our AI can help you write a compelling job description. Click the magic wand icon in the editor. + Write a clear, detailed description to attract qualified candidates. </p>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 807 - 812, The help text inside the v-if="currentStep === 1" panel still references a non-existent "magic wand icon" in the editor; either update the copy or add the missing UI. Fix by editing the help panel (the div with v-if="currentStep === 1") to remove or replace "Click the magic wand icon in the editor" with accurate guidance (e.g., "Use the AI assist button above the editor" or "Use the GenerateDescription action") or, if you intend to provide that feature, implement the icon/button inside the description textarea component so the copy matches; ensure the change touches the help panel text and/or the description textarea UI so they stay consistent.app/pages/catalog/index.vue (3)
441-446:⚠️ Potential issue | 🟡 MinorClose button still needs an accessible name.
The icon-only close button lacks an
aria-label, leaving screen reader users unaware of its purpose.Suggested fix
<button + type="button" + aria-label="Close feature details" class="rounded-md p-1.5 text-white/40 transition hover:bg-white/[0.06] hover:text-white" `@click`="closeSidebar" >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/catalog/index.vue` around lines 441 - 446, The close icon button in the template that calls closeSidebar (the <button `@click`="closeSidebar"> containing the <X /> icon) lacks an accessible name; add an aria-label (e.g., aria-label="Close sidebar") or aria-labelledby that references a visible label so screen readers announce its purpose, and ensure the label text is localized if the app uses i18n; update only the button attributes—do not change the click handler or icon component.
367-372:⚠️ Potential issue | 🟠 MajorSub-feature expand toggle is still mouse-only.
The toggle action is bound via
@click.stopon the SVG icon inside the button. Keyboard users who tab to the button and press Enter/Space will triggerselectFeature(feature)instead of expanding. They cannot independently expand sub-features.Consider wrapping the chevron in its own focusable button:
Suggested change
- <component - v-if="feature.children.length" - :is="isExpanded(feature.path) ? ChevronDown : ChevronRight" - class="size-3.5 text-white/25 shrink-0" - `@click.stop`="toggleExpand(feature.path)" - /> + <button + v-if="feature.children.length" + type="button" + :aria-expanded="isExpanded(feature.path)" + :aria-label="isExpanded(feature.path) ? 'Collapse sub-features' : 'Expand sub-features'" + class="rounded p-0.5 hover:bg-white/[0.06] transition" + `@click.stop`="toggleExpand(feature.path)" + > + <component + :is="isExpanded(feature.path) ? ChevronDown : ChevronRight" + class="size-3.5 text-white/25" + /> + </button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/catalog/index.vue` around lines 367 - 372, The chevron toggle is only bound to `@click` on the SVG so keyboard users trigger selectFeature when pressing Enter/Space on the parent button; wrap the ChevronDown/ChevronRight icon in its own focusable element (a button) and move the `@click.stop`="toggleExpand(feature.path)" there, add keyboard handlers (e.g., `@keydown.space.prevent` and `@keydown.enter.prevent`) that call toggleExpand(feature.path) to support Space/Enter, ensure the handler stops propagation so selectFeature(feature) on the row/button isn't invoked, and add appropriate ARIA attributes (aria-expanded bound to isExpanded(feature.path) and aria-controls referencing the sub-feature region) to the new toggle button; locate these changes around the component using isExpanded, toggleExpand, selectFeature, feature.path, ChevronDown and ChevronRight.
519-519:⚠️ Potential issue | 🟡 MinorAdd
relattribute to external Giscus link.The link opens in a new tab but lacks
rel="noopener noreferrer".Suggested fix
- <a href="https://giscus.app" target="_blank" class="text-brand-400 hover:underline">Giscus</a>. + <a href="https://giscus.app" target="_blank" rel="noopener noreferrer" class="text-brand-400 hover:underline">Giscus</a>.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/catalog/index.vue` at line 519, The external anchor for "Giscus" that uses target="_blank" should include rel="noopener noreferrer" to prevent reverse tabnabbing and leak of window.opener; update the <a href="https://giscus.app" target="_blank" ...>Giscus</a> element to add rel="noopener noreferrer" (keeping the existing class and target attributes intact).
🧹 Nitpick comments (4)
app/pages/catalog/index.vue (2)
395-413: Sub-feature buttons missingtype="button".For consistency with other buttons in this file and to prevent accidental form submissions if this component is ever nested differently, add
type="button".Suggested fix
<button v-for="sub in feature.children" :key="sub.path" + type="button" class="flex w-full items-center gap-3 px-5 py-2.5 pl-20 text-left transition hover:bg-white/[0.03]"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/catalog/index.vue` around lines 395 - 413, The sub-feature <button> elements lack an explicit type attribute which can cause accidental form submissions; update the buttons rendered in the v-for (the ones using selectFeature(sub), binding :key="sub.path", and referencing selectedNode and getStatusConfig) to include type="button" on each <button> so they behave as plain clickable controls rather than submit buttons.
118-124: Auto-expand logic may not trigger on subsequent data updates.The
watchEffectonly auto-expands categories whenexpandedPaths.value.size === 0. IfallItemsis refetched or changes later, new categories won't be auto-expanded because the set is no longer empty.If this is intentional (only expand on initial load), consider using
watchwith{ once: true }for clarity:Suggested change
-watchEffect(() => { - if (tree.value.length && expandedPaths.value.size === 0) { - for (const node of tree.value) { - expandedPaths.value.add(node.path) - } - } -}) +watch(tree, (nodes) => { + if (nodes.length) { + for (const node of nodes) { + expandedPaths.value.add(node.path) + } + } +}, { once: true })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/catalog/index.vue` around lines 118 - 124, The current watchEffect auto-expansion (watchEffect) only runs when expandedPaths is empty, so subsequent updates to tree/allItems won't expand new categories; decide intended behavior and change accordingly: if you want a one-time initial expand, replace the watchEffect with a watch on tree (use { immediate: true }) and stop the watcher after the first successful expansion (call the stop function) so intent is explicit; if you want to re-expand on every tree update, keep watching tree but remove the expandedPaths.value.size === 0 guard so new node.path entries are added whenever tree changes (refer to watchEffect, tree, and expandedPaths to locate the logic).app/pages/dashboard/jobs/new.vue (1)
701-707: Skills input may add duplicates when blurring after Enter.The input has both
@keydown.enter.prevent="addSkillFromInput"and@blur="addSkillFromInput". If a user types a skill and presses Enter (which adds it and clears the input), then clicks elsewhere,@blurfires on an empty input - this is harmless due to the early return. However, if they type and click away without pressing Enter, it works correctly.The current implementation is safe, but consider removing the
@blurhandler if Enter is the expected interaction to avoid confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/dashboard/jobs/new.vue` around lines 701 - 707, Remove the `@blur`="addSkillFromInput" binding from the input element to avoid double-triggering addSkillFromInput (the handler tied to `@keydown.enter.prevent`="addSkillFromInput"); alternatively if blur behavior must be kept, update addSkillFromInput to deduplicate and ignore empty/unchanged values (use the addSkillFromInput function and the skills collection to check for existing entries before pushing). Ensure references to addSkillFromInput remain consistent after the change.nuxt.config.ts (1)
73-76: Empty defaults for "required" Giscus config may cause silent failures.The comments indicate these fields are "required for the comments widget", but the defaults are empty strings. If the environment variables are not set, GiscusComments.vue will likely render nothing or fail silently without warning.
Consider either:
- Logging a warning during build when these are empty, or
- Having the GiscusComments component show an informative placeholder when config is missing
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@nuxt.config.ts` around lines 73 - 76, The giscusRepoId and giscusCategoryId in nuxt.config.ts are defaulting to empty strings which can silently break the comments widget; update nuxt.config.ts to detect when process.env.NUXT_PUBLIC_GISCUS_REPO_ID or process.env.NUXT_PUBLIC_GISCUS_CATEGORY_ID are missing and emit a clear build-time warning (e.g., using console.warn) naming giscusRepoId/giscusCategoryId, and/or change their default from '' to null/undefined so GiscusComments.vue can explicitly detect missing config and render a helpful placeholder; also update GiscusComments.vue to check for missing giscusRepoId/giscusCategoryId and show an informative message instructing to set the env vars.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/pages/catalog/index.vue`:
- Around line 540-546: The footer GitHub anchor (the <a> element that currently
has href="https://github.com/applirank/applirank" and target="_blank") is
missing the rel attribute; update that anchor to include rel="noopener
noreferrer" to match the nav GitHub link and prevent reverse tabnabbing by
adding rel="noopener noreferrer" to the same <a> element in
app/pages/catalog/index.vue.
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 591-626: The action buttons are only revealed on hover (opacity-0
group-hover:opacity-100) which makes them inaccessible to keyboard users; update
the parent container or the buttons to also reveal on focus by adding
focus-within:opacity-100 to the parent div (the container around the buttons) or
focus:opacity-100 on each button so keyboard tabbing triggers visibility, ensure
the interactive handlers (moveQuestion, editingQuestion assignment, showAddForm
toggle, handleDeleteQuestion) still point to the same elements and that disabled
states for the move buttons (index checks against
applicationForm.questions.length) remain unaffected.
---
Duplicate comments:
In `@app/pages/catalog/index.vue`:
- Around line 441-446: The close icon button in the template that calls
closeSidebar (the <button `@click`="closeSidebar"> containing the <X /> icon)
lacks an accessible name; add an aria-label (e.g., aria-label="Close sidebar")
or aria-labelledby that references a visible label so screen readers announce
its purpose, and ensure the label text is localized if the app uses i18n; update
only the button attributes—do not change the click handler or icon component.
- Around line 367-372: The chevron toggle is only bound to `@click` on the SVG so
keyboard users trigger selectFeature when pressing Enter/Space on the parent
button; wrap the ChevronDown/ChevronRight icon in its own focusable element (a
button) and move the `@click.stop`="toggleExpand(feature.path)" there, add
keyboard handlers (e.g., `@keydown.space.prevent` and `@keydown.enter.prevent`) that
call toggleExpand(feature.path) to support Space/Enter, ensure the handler stops
propagation so selectFeature(feature) on the row/button isn't invoked, and add
appropriate ARIA attributes (aria-expanded bound to isExpanded(feature.path) and
aria-controls referencing the sub-feature region) to the new toggle button;
locate these changes around the component using isExpanded, toggleExpand,
selectFeature, feature.path, ChevronDown and ChevronRight.
- Line 519: The external anchor for "Giscus" that uses target="_blank" should
include rel="noopener noreferrer" to prevent reverse tabnabbing and leak of
window.opener; update the <a href="https://giscus.app" target="_blank"
...>Giscus</a> element to add rel="noopener noreferrer" (keeping the existing
class and target attributes intact).
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 261-277: Replace the Promise.all call that posts
applicationForm.value.questions for created?.id with Promise.allSettled, inspect
each result, and handle failures explicitly: collect failed question uploads
(reference applicationForm.value.questions and created?.id), retry or surface an
error to the user and prevent silent navigation away if any question POSTs
failed; ensure successful responses are kept and failed ones are reported/logged
so the UI can show which questions need retrying rather than leaving the job in
a partially-updated state.
- Around line 321-327: The "Save draft" button in the new.vue template has no
click handler and therefore does nothing; either remove the button or wire it up
to an action: add a `@click` handler (e.g. `@click`="saveDraft") on the button and
implement a saveDraft method in the component's script that performs draft
persistence (calling your existing save method or an API, e.g. reuse
createJob/updateJob logic or emit an event), or remove the button markup
entirely until the draft flow (saveDraft) is implemented to avoid confusing
users.
- Around line 113-116: The computed canGoNext calls validateStep1(), which
mutates errors.value during render; extract a pure validator (e.g., isStep1Valid
or validateStep1Silent) that returns boolean without touching errors.value and
use that in the computed property canGoNext, while keeping the existing
validateStep1() (which sets errors.value) for use inside nextStep() and
handleSubmit() where showing errors is intended; update references so canGoNext
-> isStep1Valid and nextStep()/handleSubmit() still call validateStep1().
- Line 419: The "80 characters left" hint is hardcoded and inconsistent with the
Zod 200-char rule; add a computed property (e.g., titleCharsRemaining =
computed(() => Math.max(0, 200 - (form.value.title?.length || 0)))) and replace
the static paragraph text with a binding that displays the dynamic count and
optionally pluralizes or clamps at 0; update references to use form.value.title
length so the UI always reflects the Zod max length of 200.
- Around line 206-210: The preview URL produced by the applicationLink computed
uses a fake "xxxxxxxx" ID and can be copied as if valid; change applicationLink
(computed) to return either a non-copyable placeholder (empty string or a
clearly non-URL value) when there is no real job id (e.g., check form.value.id
or job.id/persisted flag) and return the real URL only when the job id exists,
and then update the copy button's disabled binding to be true when the job is
not yet created (disable copy if !form.value.id) and add a tooltip on the copy
button explaining "Preview — link not active until job is created" so users
cannot copy an invalid URL; locate and modify the applicationLink computed and
the copy button component usage to implement these checks and the tooltip.
- Around line 807-812: The help text inside the v-if="currentStep === 1" panel
still references a non-existent "magic wand icon" in the editor; either update
the copy or add the missing UI. Fix by editing the help panel (the div with
v-if="currentStep === 1") to remove or replace "Click the magic wand icon in the
editor" with accurate guidance (e.g., "Use the AI assist button above the
editor" or "Use the GenerateDescription action") or, if you intend to provide
that feature, implement the icon/button inside the description textarea
component so the copy matches; ensure the change touches the help panel text
and/or the description textarea UI so they stay consistent.
---
Nitpick comments:
In `@app/pages/catalog/index.vue`:
- Around line 395-413: The sub-feature <button> elements lack an explicit type
attribute which can cause accidental form submissions; update the buttons
rendered in the v-for (the ones using selectFeature(sub), binding
:key="sub.path", and referencing selectedNode and getStatusConfig) to include
type="button" on each <button> so they behave as plain clickable controls rather
than submit buttons.
- Around line 118-124: The current watchEffect auto-expansion (watchEffect) only
runs when expandedPaths is empty, so subsequent updates to tree/allItems won't
expand new categories; decide intended behavior and change accordingly: if you
want a one-time initial expand, replace the watchEffect with a watch on tree
(use { immediate: true }) and stop the watcher after the first successful
expansion (call the stop function) so intent is explicit; if you want to
re-expand on every tree update, keep watching tree but remove the
expandedPaths.value.size === 0 guard so new node.path entries are added whenever
tree changes (refer to watchEffect, tree, and expandedPaths to locate the
logic).
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 701-707: Remove the `@blur`="addSkillFromInput" binding from the
input element to avoid double-triggering addSkillFromInput (the handler tied to
`@keydown.enter.prevent`="addSkillFromInput"); alternatively if blur behavior must
be kept, update addSkillFromInput to deduplicate and ignore empty/unchanged
values (use the addSkillFromInput function and the skills collection to check
for existing entries before pushing). Ensure references to addSkillFromInput
remain consistent after the change.
In `@nuxt.config.ts`:
- Around line 73-76: The giscusRepoId and giscusCategoryId in nuxt.config.ts are
defaulting to empty strings which can silently break the comments widget; update
nuxt.config.ts to detect when process.env.NUXT_PUBLIC_GISCUS_REPO_ID or
process.env.NUXT_PUBLIC_GISCUS_CATEGORY_ID are missing and emit a clear
build-time warning (e.g., using console.warn) naming
giscusRepoId/giscusCategoryId, and/or change their default from '' to
null/undefined so GiscusComments.vue can explicitly detect missing config and
render a helpful placeholder; also update GiscusComments.vue to check for
missing giscusRepoId/giscusCategoryId and show an informative message
instructing to set the env vars.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
app/components/GiscusComments.vueapp/pages/catalog/index.vueapp/pages/dashboard/jobs/new.vuenuxt.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/components/GiscusComments.vue
| <a | ||
| href="https://github.com/applirank/applirank" | ||
| target="_blank" | ||
| class="inline-flex items-center gap-1 text-white/50 hover:text-white transition" | ||
| > | ||
| GitHub <ArrowUpRight class="size-3" /> | ||
| </a> |
There was a problem hiding this comment.
Footer GitHub link is missing rel attribute.
This target="_blank" link lacks rel="noopener noreferrer", inconsistent with the nav GitHub link at line 248.
Suggested fix
<a
href="https://github.com/applirank/applirank"
target="_blank"
+ rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-white/50 hover:text-white transition"
>📝 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.
| <a | |
| href="https://github.com/applirank/applirank" | |
| target="_blank" | |
| class="inline-flex items-center gap-1 text-white/50 hover:text-white transition" | |
| > | |
| GitHub <ArrowUpRight class="size-3" /> | |
| </a> | |
| <a | |
| href="https://github.com/applirank/applirank" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| class="inline-flex items-center gap-1 text-white/50 hover:text-white transition" | |
| > | |
| GitHub <ArrowUpRight class="size-3" /> | |
| </a> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/catalog/index.vue` around lines 540 - 546, The footer GitHub anchor
(the <a> element that currently has
href="https://github.com/applirank/applirank" and target="_blank") is missing
the rel attribute; update that anchor to include rel="noopener noreferrer" to
match the nav GitHub link and prevent reverse tabnabbing by adding rel="noopener
noreferrer" to the same <a> element in app/pages/catalog/index.vue.
| <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"> | ||
| <button | ||
| type="button" | ||
| :disabled="index === 0" | ||
| class="rounded p-1 text-surface-400 hover:text-surface-600 dark:hover:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors disabled:opacity-30" | ||
| title="Move up" | ||
| @click="moveQuestion(index, 'up')" | ||
| > | ||
| <ChevronUp class="size-4" /> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| :disabled="index === applicationForm.questions.length - 1" | ||
| class="rounded p-1 text-surface-400 hover:text-surface-600 dark:hover:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors disabled:opacity-30" | ||
| title="Move down" | ||
| @click="moveQuestion(index, 'down')" | ||
| > | ||
| <ChevronDown class="size-4" /> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class="rounded p-1 text-surface-400 hover:text-surface-600 dark:hover:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors" | ||
| title="Edit" | ||
| @click="editingQuestion = q; showAddForm = false" | ||
| > | ||
| <Pencil class="size-4" /> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class="rounded p-1 text-surface-400 hover:text-danger-600 dark:hover:text-danger-400 hover:bg-danger-50 dark:hover:bg-danger-950 transition-colors" | ||
| title="Delete" | ||
| @click="handleDeleteQuestion(q.id)" | ||
| > | ||
| <Trash2 class="size-4" /> | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
Action buttons hidden on hover are inaccessible to keyboard users.
The move/edit/delete buttons are hidden with opacity-0 group-hover:opacity-100. Keyboard users navigating via Tab won't see these buttons appear since hover doesn't trigger on focus.
Consider adding focus-within:opacity-100 to the parent or using focus:opacity-100 on buttons:
Suggested fix
- <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
+ <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity shrink-0">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/dashboard/jobs/new.vue` around lines 591 - 626, The action buttons
are only revealed on hover (opacity-0 group-hover:opacity-100) which makes them
inaccessible to keyboard users; update the parent container or the buttons to
also reveal on focus by adding focus-within:opacity-100 to the parent div (the
container around the buttons) or focus:opacity-100 on each button so keyboard
tabbing triggers visibility, ensure the interactive handlers (moveQuestion,
editingQuestion assignment, showAddForm toggle, handleDeleteQuestion) still
point to the same elements and that disabled states for the move buttons (index
checks against applicationForm.questions.length) remain unaffected.
Summary
Type of change
Validation
DCO
Signed-off-by) viagit commit -sSummary by CodeRabbit
New Features
Documentation
Navigation
Removed