Skip to content

feat: add Next.js web UI dashboard (web-ui/)#679

Open
gauravlochab wants to merge 15 commits into
santifer:mainfrom
gauravlochab:feat/web-ui
Open

feat: add Next.js web UI dashboard (web-ui/)#679
gauravlochab wants to merge 15 commits into
santifer:mainfrom
gauravlochab:feat/web-ui

Conversation

@gauravlochab
Copy link
Copy Markdown

@gauravlochab gauravlochab commented May 17, 2026

Summary

Adds a fully functional web dashboard for career-ops at web-ui/. Reads all existing data files (applications.md, pipeline.md, follow-ups.md, reports/, profile.yml) via a local Express API — no changes to existing files, no data migration needed.

  • 6 pages: Dashboard, Tracker, Pipeline Inbox, Follow-ups, Interview Prep, Analytics
  • Express API (server.mjs) on port 3099 — parses all career-ops markdown/YAML files into JSON
  • Next.js 16 (App Router, server components) + shadcn/ui + Tailwind CSS v4
  • Search across 1500+ pipeline URLs by company/role
  • Markdown rendering for story bank and reports
  • Status tabs for all 8 canonical states in the tracker
  • Zero impact on existing CLI/TUI workflow — the web-ui is a read layer on top

How to run

cd web-ui
pnpm install
pnpm dev        # starts API on :3099 + Next.js on :3030

Opens at http://localhost:3030/dashboard

Screenshots

Dashboard with 4 stat cards, best-to-act-on panel, and recent activity:

  • Total Applications / In Progress / Offers / Avg Score
  • Applications Tracker with tab-based status filter + search
  • Pipeline Inbox: search 1500+ URLs, color-coded platform badges (Greenhouse/Ashby/Lever)
  • Analytics: by status, score range, top companies

Test plan

  • cd web-ui && pnpm install && pnpm dev starts without errors
  • /dashboard loads with real data from data/applications.md
  • /dashboard/tracker shows applications grouped by status
  • /dashboard/pipeline search filters by company/role
  • /dashboard/interview renders story bank as formatted markdown
  • API server at :3099/api/applications returns valid JSON
  • No existing career-ops files modified

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • In-app pipeline search; full web dashboard (Dashboard, Pipeline, Tracker, Analytics, Follow-ups, Users, Settings), auth (Login/Register), Eval UI, and 404/500 pages; user/profile forms and interactive charts/tables.
  • Improvements

    • Richer viewer rendering and wrapping, improved batch/headless selection, optional location filtering for scans, tighter role deduping, default Gemini model upgraded, CV template layout tweaks.
  • Documentation

    • Consolidated agent/mode docs, English translations, onboarding and update guidance, clarified data ownership.
  • Chores

    • Version bump to 1.8.0, funding entry added, web UI scaffolding and tooling integrated.

gauravlochab and others added 2 commits May 17, 2026 13:49
Adds a full-featured web dashboard at `web-ui/` built on Next.js 16
(App Router) + shadcn/ui + Tailwind CSS v4. Reads all existing
career-ops data files via a local Express API — no migration needed.

## What's included

- `web-ui/server.mjs` — Express API on port 3099 that parses
  applications.md, pipeline.md, follow-ups.md, reports/, profile.yml,
  and interview-prep/ into JSON endpoints
- `web-ui/lib/api.ts` — TypeScript client with typed interfaces for
  Application, PipelineItem, FollowUp, Profile
- 6 dashboard pages:
  - `/dashboard` — welcome, 4 stat cards, best-to-act-on, recent activity
  - `/dashboard/tracker` — 8 status tabs, search, score badges
  - `/dashboard/pipeline` — search across 1500+ URLs, platform detection
  - `/dashboard/followups` — urgency-coded follow-up table
  - `/dashboard/interview` — rendered markdown story bank + company files
  - `/dashboard/analytics` — by status, score range, top companies

## How to run

```bash
cd web-ui && pnpm install && pnpm dev
```

Opens at http://localhost:3030. Reads data from the parent career-ops
directory automatically (no config needed if run from within the repo).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Welcome to career-ops, @gauravlochab! Thanks for your first PR.

A few things to know:

  • Tests will run automatically — check the status below
  • Make sure you've linked a related issue (required for features)
  • Read CONTRIBUTING.md if you haven't

We'll review your PR soon. Join our Discord if you have questions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Consolidates agent docs into AGENTS.md, hardens the system updater, refactors scan to provider plugins with optional location filtering, adds Terminal UI search/viewer improvements and tests, and adds a full Next.js + Express web UI (pages, components, API).

Changes

Agent docs, updater, and integrity

Layer / File(s) Summary
Agent docs consolidation and SKILL redirect
AGENTS.md, CLAUDE.md, GEMINI.md, .claude/skills/career-ops/SKILL.md, DATA_CONTRACT.md
Centralizes agent instructions in AGENTS.md, points CLAUDE/GEMINI to it, redirects local SKILL to .agents, and adjusts writing-samples ownership.
Updater hardening and integrity checks
update-system.mjs, test-all.mjs
Adds .agents/ bootstrap, VERSION parsing helper, safer apply()/rollback() with violated-user-path tracking and revert, and AGENTS.md integrity tests.
Repo metadata
.github/FUNDING.yml, .github/workflows/test.yml, VERSION
Adds funding entry, bumps Go version in CI to 1.26, and updates project VERSION.

Batch & scanning

Layer / File(s) Summary
Batch prompt and runner model flag
batch/batch-prompt.md, batch/batch-runner.sh
Loads _profile.md and config/profile.yml into batch prompts and adds --model/MODEL support in the runner.
Provider-based scan and location filtering
scan.mjs, modes/scan.md, templates/portals.example.yml
Refactors scanner to plugin providers, adds provider detection/resolve, new location_filter allow/block semantics, and persists location in scan-history.tsv.
Tracker merge tweaks and CV template CSS
merge-tracker.mjs, templates/cv-template.html, package.json
Tightens role token matching (keep short acronyms), adjusts CSS grid for certifications, and bumps package version + dotenv.
Gemini eval updates
gemini-eval.mjs, GEMINI.md, README.md
Defaults to gemini-2.5-flash, injects config/profile.yml/modes/_profile.md into prompts, redacts API key in errors, and simplifies score extraction.

Terminal dashboard (TUI) improvements

Layer / File(s) Summary
Pipeline search behavior & tests
dashboard/internal/ui/screens/pipeline.go, dashboard/internal/ui/screens/pipeline_test.go
Adds in-screen search state/handlers, live filtering, adjusted layout/scroll, preserve search across reloads, and comprehensive tests for interactions and regressions.
Viewer rendering overhaul & tests
dashboard/internal/ui/screens/viewer.go, dashboard/internal/ui/screens/viewer_test.go
Switches to precomputed renderedLines, rebuild on resize, new table rendering via lipgloss/table, inline tokenizer, and tests verifying wrapping, punctuation handling, and placeholders.
TUI deps and docs
dashboard/go.mod, docs/ARCHITECTURE.md
Promotes x/ansi dep and updates architecture doc text/diagram to generic headless workers.

Web UI: Next.js + Express

Layer / File(s) Summary
Project scaffolding & configs
web-ui/package.json, web-ui/tsconfig.json, web-ui/next.config.ts, web-ui/postcss.config.mjs, web-ui/components.json, web-ui/.eslintrc, web-ui/.prettierrc, web-ui/.gitignore, web-ui/app/globals.css
Adds web-ui package, TypeScript/Next config, PostCSS, shadcn config, lint/format rules, gitignore, and theme/global CSS tokens.
Root layout, providers, and core components
web-ui/app/layout.tsx, web-ui/components/providers.tsx, web-ui/components/layout/*, web-ui/components/site-header.tsx, web-ui/components/app-sidebar.tsx
Adds RootLayout, Theme provider, header, sidebar, search, logo, and nav building blocks.
Guest pages
web-ui/app/(guest)/layout.tsx, web-ui/app/(guest)/404-page/*, web-ui/app/(guest)/500-page/*, web-ui/app/(guest)/login/*, web-ui/app/(guest)/register/*
Implements guest layout with 404/500 and auth pages (metadata and components).
Dashboard pages and data
web-ui/app/dashboard/*, web-ui/app/dashboard/data.json
Adds dashboard pages: home, analytics, pipeline, tracker, followups, interview, settings, users; server-side data fetching via APIs and data.json fixtures.
API client, utils, routes
web-ui/lib/api.ts, web-ui/lib/utils.ts, web-ui/lib/routes-config.tsx, web-ui/hooks/*
Defines domain types, client fetch wrappers, helpers, route config, and mobile detection hook.
Express API and proxy
web-ui/server.mjs, web-ui/proxy.ts
Express server exposing markdown-backed JSON endpoints and PATCH/POST handlers; root redirect handler for Next.
UI primitives and composite components
web-ui/components/ui/*, web-ui/components/*
Large library of shadcn-style primitives (button, input, dialog, table, form helpers, charts, etc.) and higher-order composites (data-table, section-cards, users table, icons, nav components).

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant NextApp as Next.js App
  participant API as web-ui Express API
  participant FS as Repo Files (markdown/reports)

  Browser->>NextApp: Request SSR /dashboard
  NextApp->>API: GET /api/applications|pipeline|followups
  API->>FS: Read markdown/YAML (applications.md, pipeline.md, reports)
  FS-->>API: Parsed JSON
  API-->>NextApp: JSON responses
  NextApp-->>Browser: Rendered HTML
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 76

Caution

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

⚠️ Outside diff range comments (3)
test-all.mjs (1)

229-231: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use exact allowlist matching in the personal-data scan.

Line 229 uses substring matching (file.includes(a)), which can accidentally whitelist unrelated files whose paths merely contain an allowed filename token. That creates false negatives in leak detection.

Proposed fix
-      if (allowedFiles.some(a => file.includes(a))) continue;
+      const normalizedFile = file.replace(/\\/g, '/');
+      if (allowedFiles.includes(normalizedFile)) continue;
🤖 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 `@test-all.mjs` around lines 229 - 231, The allowlist currently uses substring
matching (allowedFiles.some(a => file.includes(a))) which can falsely whitelist
paths; change it to exact matching by comparing the filename or normalized path
segments instead (e.g., replace file.includes(a) with path.basename(file) === a
or with an exact path equality/endsWith check after normalizing). Ensure you
require/import path if needed and update the condition that uses allowedFiles
and the file variable so only exact filename or exact normalized path matches
are treated as allowed.
batch/batch-prompt.md (1)

195-205: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the required batch verification header line.

The report header template is missing the required marker. Please include this exact line in the header block:

**Verification:** unconfirmed (batch mode)

As per coding guidelines: batch/**/*.md must “mark report header with **Verification:** unconfirmed (batch mode)” when Playwright is unavailable.

🤖 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 `@batch/batch-prompt.md` around lines 195 - 205, Add the required verification
header line to the report header block: insert the exact line "**Verification:**
unconfirmed (batch mode)" into the header section near the existing metadata
(the block containing "# Evaluación: {Empresa} — {Rol}", "**Fecha:**",
"**Arquetipo:**", "**Score:**", "**Legitimacy:**", "**URL:**", "**PDF:**",
"**Batch ID:**") so the header includes that required marker when Playwright is
unavailable.
scan.mjs (1)

192-196: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle missing data/pipeline.md before reading.

appendToPipeline calls readFileSync(PIPELINE_PATH, "utf-8") without checking existence, so first-run/partial setups will crash instead of degrading gracefully.

Suggested fix
 function appendToPipeline(offers) {
   if (offers.length === 0) return;
-
-  let text = readFileSync(PIPELINE_PATH, 'utf-8');
+  if (!existsSync(PIPELINE_PATH)) {
+    writeFileSync(PIPELINE_PATH, '# Pipeline\n\n## Pendientes\n\n## Procesadas\n', 'utf-8');
+  }
+  let text = readFileSync(PIPELINE_PATH, 'utf-8');
As per coding guidelines "`**/*.mjs`: Check for command injection, path traversal, and SSRF. Ensure scripts handle missing data/ directories gracefully."
🤖 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 `@scan.mjs` around lines 192 - 196, The function appendToPipeline currently
calls readFileSync(PIPELINE_PATH, 'utf-8') without verifying the file exists,
causing a crash on first-run or missing data; update appendToPipeline to check
for the file (using fs.existsSync or try/catch around fs.readFileSync) and if
missing create a sensible default file or set text to an empty string before
appending, then write back to PIPELINE_PATH; ensure you reference PIPELINE_PATH
and use safe path handling (no user-controlled paths) when creating the file.
🤖 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 `@AGENTS.md`:
- Around line 85-116: The AGENTS.md fragment has markdownlint violations
(MD022/MD031/MD040) due to missing blank lines around headings and fenced blocks
and an unspecified fenced-block language for the TSV; update the Step 1/Step
2/Step 3/Step 4 headings and any adjacent fenced blocks by inserting a blank
line before and after each heading and fence, and add a language identifier
(e.g., "tsv" or "text") to the fenced TSV block so the linter recognizes it;
search for the headings "Step 1: CV (required)", "Step 2: Profile (required)",
"Step 3: Portals (recommended)", and "Step 4: Tracker" plus the TSV fenced block
in AGENTS.md to apply these changes.

In `@CLAUDE.md`:
- Around line 1-2: The file CLAUDE.md currently starts with a non-H1 line
("`@AGENTS.md`"), causing markdownlint MD041; prepend a top-level heading (H1) as
the very first line (for example "# CLAUDE" or "# Claude Code") so the file
begins with an H1 before the existing "`@AGENTS.md`" include, satisfying the
linter.

In `@DATA_CONTRACT.md`:
- Line 21: The change reclassifies a file under a user-owned directory by
marking `writing-samples/README.md` as system-owned while `writing-samples/*`
remains user-owned; revert that by keeping ownership consistent: either make
`writing-samples/README.md` user-owned (keep `writing-samples/*` as-is) or move
the system-owned README into a true system directory, and update the
DATA_CONTRACT entry for `writing-samples/*` and `writing-samples/README.md` so
parent and child paths have consistent ownership.

In `@gemini-eval.mjs`:
- Around line 52-63: The PATHS object currently omits article-digest.md so the
evaluator only loads cv.md; update the PATHS constant (key name like
articleDigest or articleDigestMd) to include join(ROOT, 'article-digest.md') and
then modify the evaluation-loading logic that reads cv.md (the same code
referenced around the block that handles PATHS) to also read article-digest.md
at evaluation time so metrics are sourced from both files per the metrics-source
contract.

In `@GEMINI.md`:
- Around line 1-2: Add a top-level H1 heading at the start of GEMINI.md to
satisfy MD041 (first-line-heading) before the existing include directive
"@./AGENTS.md"; update the file so the first line is a Markdown H1 (e.g., "#
Gemini") followed by the existing include line to keep the include intact and
resolve the lint error.

In `@modes/pipeline.md`:
- Line 17: Add a language tag to the opening code fence that currently lacks one
(the standalone ``` fence) so markdownlint MD040 is satisfied, and ensure the
bash code block around the lines containing the bash fence (the ```bash ... ```
block) is separated by blank lines above and below it to satisfy MD031; update
the two offending fences to use ```<language> for the first and to add a blank
line before and after the existing ```bash block.

In `@modes/scan.md`:
- Around line 112-120: The docs claim the location decision from location_filter
in portals.yml is persisted as a 7th column in scan-history.tsv, but the file's
write format/schema/example still shows only 6 columns; update the documentation
and any examples to consistently describe the 7-column contract (include the new
7th "location_decision" column), and update any writer/parser references to the
scan-history.tsv format to read/write that 7th column; ensure references to
location_filter, portals.yml, and scan-history.tsv are synchronized so readers
and implementers know the exact column order and that the 7th column contains
the persisted location decision.

In `@README.md`:
- Line 160: Update the README entry that currently reads "**Free tier:** Both
options work without billing. Native CLI uses Google OAuth; the API script uses
`gemini-2.5-flash` (15 RPM, 1M tokens/day free)." to avoid hardcoded Gemini
quotas: either remove the specific numbers or replace them with a qualifier like
"Free tier limits apply—see official docs for current quotas" and add links to
the official Gemini rate limits and pricing pages (e.g.,
ai.google.dev/gemini-api/docs/rate-limits and
ai.google.dev/gemini-api/docs/pricing or the Gemini CLI quota page) so readers
are pointed to authoritative, up-to-date quota information.

In `@test-all.mjs`:
- Line 287: The call that unconditionally reads AGENTS.md via readFile
(assigning to agents) should handle a missing file gracefully; wrap the readFile
invocation (where agents is set) in a presence check or try/catch and if the
file is absent or read fails, set agents to null/empty and emit a non-fatal
warning so the rest of test-all.mjs can continue to produce the normal summary;
update any downstream logic that assumes agents is a string to tolerate
null/empty (e.g., in validation or summary functions).

In `@web-ui/app/`(guest)/404-page/page.tsx:
- Line 36: Update the image alt text in the 404 page component to accurately
describe the image for assistive tech and page context: replace the misleading
"Login visual" alt on the image element in web-ui/app/(guest)/404-page/page.tsx
with a concise, descriptive label such as "404 error illustration" or "Page not
found illustration" (or a brief phrase describing the specific graphic used),
ensuring the alt reflects the 404/page-not-found context rather than login.
- Around line 26-29: The CTA buttons in web-ui/app/(guest)/404-page/page.tsx
(the Button elements alongside ArrowRight) are not wired to any action; update
them so the primary Button navigates to the home route and the ghost Button goes
to a support destination. Implement this by replacing the static Buttons with
Link-wrapped Buttons (next/link) or adding a click handler using
useRouter().push('/') for the home Button and useRouter().push('/support') or an
external mailto/support URL for the Contact support Button; ensure the Button
components (and ArrowRight icon) remain intact and preserve accessibility
attributes like aria-label.

In `@web-ui/app/`(guest)/500-page/page.tsx:
- Line 28: The image alt text on the 500 error page is incorrect (currently
alt="Login visual"); update the Image/JSX element in the page component (the
<Image ... alt=... /> in page.tsx) so the alt describes the server-error
context—e.g., change to "500 error illustration" or "Server error illustration"
to accurately convey the image for assistive tech.

In `@web-ui/app/`(guest)/login/page.tsx:
- Around line 62-64: The "Forgot your password?" Link in the login page
currently uses a placeholder href="#" which is a dead target; update the Link
element in web-ui/app/(guest)/login/page.tsx (the anchor with text "Forgot your
password?") to point to a real recovery route such as "/forgot-password" or
remove/hide the Link until that route/component exists so production navigation
isn't broken.

In `@web-ui/app/`(guest)/register/page.tsx:
- Around line 124-126: Replace the plain anchor used for internal navigation
with Next.js client-side Link: in the register page component (page.tsx) locate
the anchor element with href="/login" and swap it to use the Next.js Link
component, add the import for Link from "next/link", and preserve the existing
className ("text-primary hover:underline") and link text so the appearance and
behavior remain the same.
- Around line 65-80: The password input and submit CTA are using sign-in
semantics; update the Input component with id/name "password" to use
autoComplete="new-password" (replace current-password) and change the Button
text from "Sign in" to a registration-appropriate label such as "Create account"
(or "Register") so the Input (id/name="password") and the Button(type="submit")
reflect registration semantics.

In `@web-ui/app/dashboard/followups/page.tsx`:
- Line 60: The empty-state paragraph currently renders the hint string "Add
follow-ups via the API or edit <code className='bg-muted px-1
rounded'>data/follow-ups.md</code>"; update that JSX in the page component so
the code element shows the correct repository file name "follow-ups.md" (i.e.,
replace "data/follow-ups.md" with "follow-ups.md" in the <p
className="text-sm">...</p> element) to match the repo/PR contract.

In `@web-ui/app/dashboard/interview/page.tsx`:
- Around line 47-49: The list item div rendering the file name (the element with
className containing "rounded-md px-3 py-2 text-sm hover:bg-muted cursor-pointer
transition-colors capitalize") is styled as clickable but has no interaction;
either remove the interactive affordances or make it interactive. Fix by editing
that element in page.tsx: if rows are informational only, remove
"cursor-pointer" and "hover:bg-muted" (and optionally "transition-colors") from
the className; if they should be clickable, convert the div into an interactive
element (e.g., a button or Link) and add the appropriate onClick or href handler
to the component that renders the file name (the same element that calls
f.replace(...)). Ensure accessibility by using a semantic element and keyboard
support when making it interactive.
- Around line 10-31: The parsed HTML from marked (storyHTML) is injected via
dangerouslySetInnerHTML without sanitization, so update the component to
sanitize the output before rendering: after creating storyHTML from
marked(storyBank) call a sanitizer (e.g., DOMPurify.sanitize or sanitize-html)
to produce a safeStoryHTML and pass that to dangerouslySetInnerHTML instead; add
the necessary import for the sanitizer and ensure you sanitize any HTML produced
by marked(storyBank) (references: storyBank, marked, storyHTML,
dangerouslySetInnerHTML).

In `@web-ui/app/dashboard/pipeline/page.tsx`:
- Around line 21-24: The search filter currently calls toLowerCase() on
i.company and i.role which can be undefined; update the predicate in
pending.filter to guard these fields (e.g., use (i.company ?? '').toLowerCase(),
(i.role ?? '').toLowerCase(), and likewise ensure i.url is safely handled) or
use optional chaining with a default string so the comparison against query
never throws; modify the predicate that references i.company, i.role, i.url and
query to coerce missing fields to '' before calling toLowerCase().

In `@web-ui/app/dashboard/settings/layout.tsx`:
- Around line 18-33: The sidebar items with titles "Account", "Appearance",
"Notifications", and "Display" currently use placeholder href="#" links; remove
any entries that don't have implemented routes from the settings items array in
layout.tsx or replace their href="#" with the real route paths if available,
otherwise mark them disabled (e.g., add a disabled flag or render as
non-clickable) so the Settings sidebar only contains working navigation (locate
the array of items in layout.tsx and update the objects for "Account",
"Appearance", "Notifications", "Display").

In `@web-ui/app/dashboard/settings/profile-form.tsx`:
- Line 115: The Link in profile-form.tsx currently points to a likely-broken
template route "/examples/forms"; update the Link href to the app's correct
email/settings route (e.g., replace "/examples/forms" with the real route like
"/settings/email" or the appropriate route constant), ensuring the Link
component in the profile-form.tsx file points to a valid route used elsewhere in
the app.
- Line 73: The onSubmit handler for ProfileFormValues is currently empty,
leaving the "Update profile" action non-functional; implement submission logic
in the onSubmit function (or disable the submit button until wired) by
validating form data, calling the profile update API or parent callback (e.g.,
updateProfile or props.onUpdateProfile), handling loading state and showing
success/error feedback, and ensure errors are surfaced to the form (set form
errors) so the UI at the Update profile button and any other submit points at
the same component (see onSubmit and related handlers around lines where
onSubmit is declared) behave correctly.

In `@web-ui/app/dashboard/settings/sidebar-nav.tsx`:
- Line 29: The active-state check using strict equality on pathname fails for
nested routes; in the ternary that compares pathname === item.href (inside the
sidebar-rendering component where item.href and pathname are used), replace that
equality with a startsWith check that also handles root/trailing-slash edge
cases, e.g. use (pathname === item.href ||
pathname.startsWith(item.href.endsWith('/') ? item.href : item.href + '/')) so
child paths like /dashboard/settings/profile keep the active "bg-muted" class.

In `@web-ui/app/dashboard/tracker/page.tsx`:
- Around line 15-16: Validate the incoming tab value before casting to
CanonicalStatus: check that tab is one of the allowed status keys (e.g., exists
in STATUS_LABELS or in a canonicalStatuses array) and only then set activeTab =
tab as CanonicalStatus; otherwise default to "evaluated". Add a small type-guard
(isCanonicalStatus) or runtime check used wherever activeTab is created and
before any access like STATUS_LABELS[activeTab].toLowerCase() to avoid calling
toLowerCase on undefined.
- Around line 22-25: The filter expression on tabApps can throw when a.notes is
undefined; update the predicate used in the filter (the arrow function that
accesses a.company, a.role, a.notes and the query) to guard notes with a
null-safe check such as using optional chaining or defaulting (e.g., use
a.notes?.toLowerCase().includes(query) or (a.notes ||
'').toLowerCase().includes(query)) so the search won't throw when notes is
missing; ensure the rest of the comparisons still lower-case both sides
(company, role and query) as they currently do.

In `@web-ui/app/dashboard/users/data-table.tsx`:
- Around line 320-327: The CommandItem onSelect handlers for the
status/plan/role filter lists are currently empty; wire them to update the table
filters by calling table.getColumn("status")?.setFilterValue(...),
table.getColumn("plan_name")?.setFilterValue(...), and
table.getColumn("role")?.setFilterValue(...) respectively (toggle the selected
value: clear if already selected, set if new) and then close the picker (e.g.,
setOpen(false) or the equivalent state used for that dropdown). Locate the
CommandItem usages (the one iterating statuses, the one iterating plans, and the
one iterating roles) and replace the empty onSelect with logic that retrieves
the currentValue parameter, computes the new filter value ("" or currentValue)
and calls the appropriate table.getColumn(... )?.setFilterValue(newValue), then
close the control.
- Around line 51-59: The User type is missing the fields used by the table and
filter; update the exported User type to include name: string, email: string,
and role: string, and then fix the "name" column accessor so it either uses a
computed accessor (accessorFn) that returns `${row.firstName} ${row.lastName}`
or ensure the data you feed the table populates a name field; also update the
search/filter logic that references "name" to match whichever approach you
choose (computed accessor or actual name property) so filtering and cells render
correctly.

In `@web-ui/app/dashboard/users/data.json`:
- Around line 4-400: The users fixture contains realistic names/emails in the
JSON array objects (keys "id","name","email") which are pseudo-PII; replace
those with synthetic values (e.g., set "name" to "User <id>" and "email" to
"user+<id>`@example.com`") or switch to generating demo users at runtime using a
deterministic fake generator, and remove any realistic avatar/email patterns so
the dataset contains no real-looking personal identifiers.

In `@web-ui/app/dashboard/users/page.tsx`:
- Around line 20-23: The getUsers function currently calls fs.readFile and
JSON.parse without protection; wrap the body of getUsers in a try/catch, catch
file system errors from fs.readFile (e.g., missing file) and JSON parsing errors
from JSON.parse, and return a safe fallback (e.g., an empty array) or rethrow a
descriptive error; update the implementation of getUsers to use
path.join(process.cwd(), "app/dashboard/users/data.json") inside the try and
ensure errors are logged or handled before returning so the page doesn't crash.

In `@web-ui/app/globals.css`:
- Around line 1-3: The `@import` rules must appear before any `@plugin` in
globals.css; move `@import` "tailwindcss" and `@import` "tw-animate-css" to the very
top of the file (with "tailwindcss" first), and place `@plugin`
"`@tailwindcss/typography`" after those imports so Tailwind is initialized before
plugins register utilities.

In `@web-ui/components/anchor.tsx`:
- Around line 23-25: The active-state comparison doesn't normalize props.href
when it's an object; update the isMatch logic to call props.href.toString() (or
String(props.href)) in both branches so object hrefs are normalized before
comparing (i.e., use props.href.toString() for the non-absolute branch and keep
the split("/")[1] usage for the absolute branch), and guard against
null/undefined href if necessary.

In `@web-ui/components/app-sidebar.tsx`:
- Line 47: The Tailwind important modifier is using v3 syntax in the
SidebarMenuButton usage; update the className on the SidebarMenuButton component
(the string "data-[slot=sidebar-menu-button]:!p-1.5") to use Tailwind v4 syntax
by moving the exclamation mark to the end of the utility (e.g.,
"data-[slot=sidebar-menu-button]:p-1.5!"), leaving the rest of the class value
unchanged.
- Around line 48-52: Replace the plain anchor used for internal navigation with
Next.js client-side Link: in the SidebarMenuButton block that currently has <a
href="/dashboard">, import Link from "next/link" (if not already) and render
Link with the asChild prop (matching how NavMain uses Link) so the existing
inner div and span are preserved as the link child; this enables client-side
navigation to "/dashboard" without a full page reload while keeping the same
styles and structure.

In `@web-ui/components/chart-area-interactive.tsx`:
- Around line 178-182: The ToggleGroup is passing onValueChange directly to
setTimeRange which allows an empty string to be set; change the handler in the
chart-area-interactive component so that onValueChange uses a wrapper that only
updates state when the new value is one of the allowed union ("90d" | "30d" |
"7d") and otherwise ignores it (or keeps the previous timeRange); reference
ToggleGroup, timeRange and setTimeRange and implement the check inside the
onValueChange callback to guard against empty or invalid values.
- Line 183: Update the Tailwind important-modifier syntax in the className
string inside the ChartAreaInteractive component: replace the v3-style `:!px-4`
token with the v4-style `:px-4!` so the class becomes `className="hidden
*:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex"` (locate the className
on the JSX element in chart-area-interactive.tsx around the toggle group).
Ensure no other occurrences of `:!` remain in the file and run the Tailwind
build to verify the updated selector works.
- Around line 154-155: The code is creating dates with new Date(item.date) and
new Date("2024-06-30") which are parsed as UTC but rendered in local time
causing off-by-one-day errors in US timezones; update all occurrences (the local
variables like date and referenceDate in chart-area-interactive.tsx and the
other uses at the locations referenced) to parse ISO dates with an explicit UTC
timestamp (e.g. replace new Date("YYYY-MM-DD") with new
Date("YYYY-MM-DDT00:00:00Z") or new Date(item.date + "T00:00:00Z")), or
alternatively import and use a library parser such as date-fns parseISO/utc or
Day.js (parseISO(item.date) or dayjs.utc(item.date)) and normalize to
UTC/startOfDay before doing comparisons and rendering so filtering and labels
are consistent across timezones.

In `@web-ui/components/data-table.tsx`:
- Around line 405-427: The mobile Select is not connected to the Tabs state so
changing the select doesn't update the tab content; fix this by making the Tabs
controlled (use a shared state value and onValueChange) and bind that state to
the Select (set Select's value and onValueChange to the same handlers).
Specifically, create a state like selectedView and pass it as value and
onValueChange to the Tabs component (where Tabs currently has
defaultValue="outline"), and also set Select's value and onValueChange (the
SelectTrigger/SelectValue pair) to use selectedView so both controls stay in
sync.
- Around line 725-795: The form inside TableCellViewer reuses static IDs (e.g.,
"header", "type", "status", "target", "limit", "reviewer") causing
duplicate-id/accessibility issues; change to row-scoped unique IDs by appending
the item id or index (e.g., `${item.id}-header`) wherever the Label htmlFor and
the control id are set (Input id, SelectTrigger id) and ensure SelectTrigger,
Input and Label references (in TableCellViewer) use the same generated id so
labels correctly map to their controls.

In `@web-ui/components/icon.tsx`:
- Line 8: Add the missing React namespace imports at the top of the affected
files so the type annotations resolve: add either `import React from "react"` or
`import * as React from "react"` to web-ui/components/icon.tsx (used by the
IconType alias: React.ComponentType and React.SVGProps) and to
ui/collapsible.tsx (used by React.FC and React.ComponentProps) so TypeScript
strict mode can find the React types.

In `@web-ui/components/layout/header.tsx`:
- Around line 77-79: Replace the non-focusable <figure> that wraps the avatar
image (src `/images/avatars/1.png`) with a focusable control by using a <button
type="button"> (move the "cursor-pointer" class to the button), attach the
existing onClick menu-toggle handler to that button, and add accessible
attributes such as aria-haspopup="true" and aria-expanded={...} (bound to your
menu open state); ensure the img remains inside the button and existing styling
is preserved so keyboard users can focus and activate the account dropdown.
- Around line 58-64: In header.tsx update the two external Link elements that
use target="_blank" (the Link wrapping the Get Pro button with href
"https://shadcnuikit.com/pricing" and the Link wrapping Learn More with href
"https://shadcnuikit.com/") to also include rel="noopener noreferrer" to prevent
window.opener exposure; locate the Link nodes inside the Button components in
the header component and add rel="noopener noreferrer" to each external Link
with target="_blank".

In `@web-ui/components/layout/logo.tsx`:
- Line 13: Replace the placeholder template branding text "Shadcn UI Kit" and
the Badge contents ("Free") in the Logo component (logo.tsx) with the
project-specific branding strings; locate the JSX that renders the text and the
<Badge> element in the Logo component and update the visible text to the correct
project name and badge label (e.g., "Career Ops Dashboard" and an appropriate
badge) so the component no longer displays the template copy.

In `@web-ui/components/layout/search.tsx`:
- Around line 44-53: The dynamic icon lookup can produce undefined and crash
rendering; in the search component replace the unchecked use of icons[item.icon]
(currently assigned to LucideIcon with a // `@ts-expect-error`) with a safe guard:
look up the icon (icons[item.icon]) only if item.icon is truthy and the key
exists, remove the ts-expect-error, and render a fallback (e.g., null or a
default icon) when the lookup returns undefined; update the CommandItem render
to conditionally render <LucideIcon .../> only when LucideIcon is a valid
component to prevent runtime errors.

In `@web-ui/components/layout/sidebar.tsx`:
- Around line 90-96: The external <Link> elements in the sidebar component (the
Link with href="https://shadcnuikit.com/pricing" inside the Get Pro Button and
the Link with href="https://shadcnuikit.com/" inside the Learn More Button) use
target="_blank" but lack rel attributes; update those Link elements to include
rel="noopener noreferrer" to restore proper tab isolation and security when
opening new tabs.

In `@web-ui/components/nav-main.tsx`:
- Line 30: The active-route check in nav-main.tsx is too loose; update the
isActive logic to only mark a route active when pathname exactly equals item.url
or when pathname starts with item.url followed by a path boundary (i.e.,
item.url + "/") — keep the existing special-case exclusion for "/dashboard" and
reference the isActive variable, pathname, and item.url when making the change.

In `@web-ui/components/nav-secondary.tsx`:
- Around line 30-35: Replace the plain anchor in the SidebarMenuButton render
with Next.js client-side routing: import Link from 'next/link' and, inside the
SidebarMenuButton block (where <a href={item.url}> is used), render a Link for
internal routes (e.g., URLs starting with '/') passing the same children
(<item.icon /> and <span>{item.title}</span>) and keep an <a> fallback for
external URLs (e.g., starting with 'http' or non-rooted paths). Ensure the code
uses the same unique symbols (SidebarMenuButton, item.url, item.icon,
item.title) and preserves semantics/props when switching to Link so internal
navigation uses client-side transitions and external links remain anchors.

In `@web-ui/components/nav-user.tsx`:
- Around line 54-55: The AvatarFallback currently hardcodes "CN"; change it to
compute initials from the user's name and use that value in AvatarFallback
instances. In the nav-user component locate where AvatarFallback is rendered
(the AvatarFallback JSX in the NavUser component) and replace the literal "CN"
with a small helper that derives initials from user.name (split by whitespace,
take the first letters of the first and last word, uppercase them, handle
single-word names by taking first letter, and provide a safe default like "?" if
name is missing). Ensure you use the same initials variable for both
AvatarFallback usages so both places reflect the current user's name.

In `@web-ui/components/section-cards.tsx`:
- Line 25: The stat label "Applied + Interview" is inconsistent with the values
being calculated because the displayed count includes `responded` and `total` is
labeled as “evaluations”; update the UI to make labels match the data by either
(A) changing the `sub` string to include `responded` (e.g., "Applied + Interview
+ Responded") and renaming the `total` label where used to "Evaluations" (or a
clearer term), or (B) change the count logic (where `responded` is included) to
match the existing "Applied + Interview" label; locate and adjust the `sub`
property, the `responded` usage, and the `total` label in the section-cards
component to keep labels and computed values consistent.
- Around line 11-12: The average score calculation currently uses total (all
apps) but should use only evaluated apps; update the avgScore computation to
first derive the evaluatedApps = apps.filter(a => a.status === "evaluated") (or
reuse the existing evaluated count) and compute avgScore by reducing over
evaluatedApps and dividing by evaluatedApps.length, returning "—" when there are
zero evaluated apps; replace any other identical calculations (e.g., the same
logic around avgScore in the block referenced by apps/filter and evaluated) so
the metric consistently reflects only evaluated roles.

In `@web-ui/components/site-header.tsx`:
- Around line 7-19: PAGE_TITLES in SiteHeader is missing entries for existing
dashboard routes, causing usePathname() to fall back to "Dashboard" for valid
pages like "/dashboard/users" and "/dashboard/settings"; update the PAGE_TITLES
Record to include explicit mappings for all dashboard routes (e.g.,
"/dashboard/users" -> "Users", "/dashboard/settings" -> "Settings" and any other
existing routes) so that the computed title variable in SiteHeader reflects the
correct page names, or alternatively implement a fallback that derives a
humanized title from pathname if you want to avoid hardcoding every route.

In `@web-ui/components/ui/calendar.tsx`:
- Around line 12-13: The import for DayButton is declared as a type-only import
but DayButton is used at runtime (in the object literal and with typeof); remove
the leading "type" so DayButton is imported as a runtime value. Update the
import statement that currently reads something like `type DayButton` to a
normal import of DayButton, leaving any other true type-only imports as-is, so
the references to DayButton (the object literal and the typeof usage) resolve at
runtime.

In `@web-ui/components/ui/carousel.tsx`:
- Around line 99-104: You register both events with api.on("reInit", onSelect)
and api.on("select", onSelect) but only remove the "select" listener; update the
cleanup to unsubscribe both events by calling api?.off("reInit", onSelect) and
api?.off("select", onSelect) (or the equivalent removal API) so the onSelect
handler is removed for both "reInit" and "select" to avoid listener leaks and
duplicate calls.

In `@web-ui/components/ui/chart.tsx`:
- Around line 237-241: The conditional rendering hides zero because it uses a
truthy check; update the check around the JSX that renders
{item.value.toLocaleString()} to explicitly test for null/undefined (e.g.,
item.value != null or item.value !== undefined) so 0 is rendered correctly,
matching the pattern used elsewhere (see usages of item.value handling in this
component).
- Around line 82-100: The injected CSS built in the style block using THEMES and
the id variable and the colorConfig map is unsafe: escape or sanitize the
selector fragment derived from id (used in the `[data-chart=${id}]` selector)
and validate/whitelist/normalize colors from colorConfig before embedding them
into the CSS string to prevent CSS-injection; implement a small utility (e.g.,
escapeCssIdentifier or isValidCssColor) and use it where id and itemConfig.color
/ itemConfig.theme are read, and remove the falsy guard that suppresses numeric
zeros (the current `item.value &&` check) so that 0 values render correctly (use
an explicit null/undefined check instead of treating 0 as false).

In `@web-ui/components/ui/collapsible.tsx`:
- Line 3: Add the missing React import by importing React (e.g., import * as
React from "react") at the top of the file so the type references like
React.ComponentProps used with CollapsiblePrimitive.Root,
CollapsiblePrimitive.CollapsibleTrigger and
CollapsiblePrimitive.CollapsibleContent resolve correctly; also confirm the
Radix export names (CollapsiblePrimitive.Root,
CollapsiblePrimitive.CollapsibleTrigger,
CollapsiblePrimitive.CollapsibleContent) match the installed radix-ui package
and adjust the import path/identifiers if the package exports a different
namespace or named members.

In `@web-ui/components/ui/form.tsx`:
- Around line 28-30: The context initializations use a non-null empty object
which makes the undefined-provider guard unreachable and allows
fieldContext.name and itemContext.id to be accessed when the provider is
missing; change the contexts (e.g., FormFieldContext and the FormItemContext) to
be created with an explicit undefined default (createContext<... |
undefined>(undefined)) and update any consumer code (the places that read
fieldContext.name and itemContext.id) to validate the context is present and
throw a clear error if undefined (e.g., in the FormField and FormItem consumer
hooks/components), so consumers cannot read properties when mounted outside
their provider.

In `@web-ui/components/ui/input-group.tsx`:
- Around line 71-76: The click handler on the InputGroup wrapper only focuses
elements found by querySelector("input"), so clicking the addon won't focus a
textarea (e.g., InputGroupTextarea). Update the onClick callback in
ui/input-group.tsx to search for both inputs and textareas (for example using
querySelector("input, textarea") or a more general focusable selector) and call
focus() on the found element; keep the early return when the click originates on
a button unchanged.

In `@web-ui/components/ui/kbd.tsx`:
- Around line 18-26: KbdGroup's props are typed as React.ComponentProps<"div">
but the component renders a <kbd>, causing a type mismatch; change the prop type
to React.ComponentProps<"kbd"> (i.e., update the KbdGroup signature) so
TypeScript validates props against the actual rendered element (ensure the
identifier KbdGroup and its prop spread {...props} remain unchanged).

In `@web-ui/components/ui/navigation-menu.tsx`:
- Around line 1-3: This file is missing the "use client" directive required for
React client components; add the literal line "use client" as the very first
line of web-ui/components/ui/navigation-menu.tsx (before any imports) so the
NavigationMenu wrapper that imports NavigationMenuPrimitive and cva runs as a
client component; ensure the directive appears above the existing imports (e.g.,
before the imports of React, NavigationMenuPrimitive, and cva) and save.

In `@web-ui/components/ui/pagination.tsx`:
- Around line 9-10: The import of Button is currently type-only which breaks its
later runtime use with typeof; change the import to bring Button in as a value
(remove the TypeScript-only `type` qualifier) so the binding for Button is
available at runtime, e.g. update the import that currently reads `import {
buttonVariants, type Button } from "`@/components/ui/button`"` to import Button as
a value so the `typeof Button` usage elsewhere in this file resolves correctly;
ensure only purely type-only imports keep `type` if needed.

In `@web-ui/components/ui/popover.tsx`:
- Line 4: The import for the Radix Popover is wrong: replace the current import
of PopoverPrimitive from "radix-ui" with a namespace import from
"`@radix-ui/react-popover`" (e.g., import * as PopoverPrimitive from
"`@radix-ui/react-popover`") and update any usages of PopoverPrimitive
accordingly; also add "`@radix-ui/react-popover`": "^1.0.0" to web-ui/package.json
dependencies and run install so the package is available.

In `@web-ui/components/ui/sidebar.tsx`:
- Around line 97-109: The keyboard handler in the React.useEffect (handleKeyDown
listening for SIDEBAR_KEYBOARD_SHORTCUT and calling toggleSidebar) should ignore
events when focus is inside editable controls; update handleKeyDown to
early-return if the event target is an input, textarea, select, or an element
with isContentEditable (check event.target instanceof HTMLElement and test
tagName or isContentEditable), and also consider elements contained within those
(use closest on the target). This prevents Ctrl/Cmd+B from toggling the sidebar
while typing in form fields or contenteditable areas.
- Line 4: Import and usage of Slot are incorrect: change the import from
SlotPrimitive to importing Slot directly from "`@radix-ui/react-slot`" (i.e.
import Slot from '`@radix-ui/react-slot`') and replace all uses of
SlotPrimitive.Slot with the Slot component so asChild code paths work properly;
search for the symbol SlotPrimitive and update each occurrence to use Slot as a
component (also ensure any props like asChild are passed to Slot).

In `@web-ui/components/ui/slider.tsx`:
- Around line 16-23: The current fallback for _values uses [min, max] which
forces a two-thumb range when no value/defaultValue is provided; change the
fallback to a single-thumb default [min] so single-value sliders render
correctly (update the React.useMemo that computes _values and the identical
logic at the other occurrence to return [min] instead of [min, max] when neither
value nor defaultValue are arrays). Ensure you keep the same dependency array
and only adjust the fallback value.

In `@web-ui/components/ui/sonner.tsx`:
- Line 33: Replace the inline type assertion "as React.CSSProperties" by
importing the CSSProperties type from React and using that imported name;
specifically, add "import { CSSProperties } from 'react'" at the top of
web-ui/components/ui/sonner.tsx (or the module where the assertion appears) and
change the assertion from "as React.CSSProperties" to "as CSSProperties" (search
for occurrences of "React.CSSProperties" in the file or project and apply the
same replacement to standardize types).

In `@web-ui/eslint.config.mjs`:
- Around line 9-11: Delete the legacy .eslintrc file so ESLint uses the flat
config in eslint.config.mjs (the defineConfig([...], extends:
[...nextCoreWebVitals]) entry); remove the dead .eslintrc from the repo and
commit the deletion so there is no duplicate/ambiguous ESLint configuration.

In `@web-ui/lib/api.ts`:
- Around line 80-121: Wrap the network calls in getApplications, getPipeline,
getFollowUps, getReport, getProfile, getStoryBank, and getInterviewFiles in
try/catch (or extract a shared fetch helper) so network/DNS/refused errors are
caught and the existing fallback values are returned instead of allowing
exceptions to bubble; ensure the catch returns the same defaults currently used
for non-OK responses ([], null, {}, "# Story Bank\n\nNo stories yet.", etc.) and
that successful responses still call r.json().

In `@web-ui/lib/routes-config.tsx`:
- Line 20: Several menu entries in the routes configuration use incorrect href
strings (e.g., the href property currently set to "/dashboard/default") causing
navigation 404s; update the href values on the route objects in
routes-config.tsx (the objects that contain the href property) to exactly match
the app's actual route paths as defined by the router (replace
"/dashboard/default" and the other mismatched href values referenced in the file
with the correct routes such as the real dashboard path, user path, etc.), and
verify by cross-checking each href against the app's route definitions and
running the app to ensure no 404s from menu clicks.

In `@web-ui/lib/utils.ts`:
- Around line 16-22: Extract the inline parameter type for generateMeta into a
named interface (e.g., GenerateMetaParams) and update the function signature to
accept that interface instead of the inline object; specifically, declare
interface GenerateMetaParams { title: string; description: string } and change
the generateMeta parameter to use GenerateMetaParams to conform to the project
style and reuse the type elsewhere.
- Around line 24-25: The page metadata currently appends the hardcoded suffix
"Shadcn UI Kit Free Dashboard Template" to titles (see the title: `${title} -
Shadcn UI Kit Free Dashboard Template` entry in web-ui/lib/utils.ts); replace
this template-specific string with a project-wide constant (e.g., SITE_NAME or
SITE_TITLE) or a configurable env value and use `${title} - ${SITE_NAME}` (or
just title when appropriate) so page titles use the correct site branding; add
the constant near other globals and update any callers that rely on the old
hardcoded suffix.

In `@web-ui/next.config.ts`:
- Around line 5-13: The remotePatterns array in next.config.ts currently allows
all hosts ("**") and unencrypted HTTP; update the remotePatterns entries to list
only explicit trusted hostnames and use protocol: "https" (remove the http entry
and the wildcard hostnames). Edit the remotePatterns array (the objects
currently with protocol and hostname keys) to replace hostname: "**" with
concrete hostnames you trust (e.g., your CDN and backend domains) and ensure
protocol is "https" for each entry so only encrypted origins are permitted.

In `@web-ui/package.json`:
- Line 41: Replace the non-deterministic dependency entry for "radix-ui" in
package.json (currently set to "latest") with a specific, tested semver value
(for example "radix-ui": "^0.x.y") to ensure reproducible installs; update the
dependency string in package.json and run a fresh install and lockfile update
(npm/yarn/pnpm) so package-lock.json / yarn.lock reflects the pinned version,
and ensure any CI/builder uses the updated lockfile.

In `@web-ui/README.md`:
- Around line 1-58: The README currently documents an unrelated product ("Shadcn
UI Kit") instead of this repo's web UI; replace the entire README.md content
with project-specific docs: change the title/header from "Shadcn UI Kit" to the
project name (career-ops web UI), remove all promotional/proprietary content
(the "Get Lifetime Access (PRO)" section and preview images), and add concise
sections for project purpose, local setup (install, build, start commands),
required environment variables, testing/linting commands, deployment notes, and
contribution guidelines; update any top-anchor/back-to-top links to match the
new headings so contributors can quickly run and contribute to the web-ui.

In `@web-ui/server.mjs`:
- Around line 192-195: The markdown table row builder writes raw user fields
into cols (e.g., cols[5], cols[8]) and returns '| ' + cols.join(' | ') + ' |',
which lets pipe characters and newlines in values (like notes) break parsing;
before assigning to cols (where status and notes are set) sanitize/escape
table-breaking characters by replacing '|' with '\|' and converting newlines to
a single-space or escaped '\n' sequence (apply the same escaping wherever other
fields are written, including the similar block around line 208) so the
generated markdown row remains intact.
- Around line 12-13: The global app.use(cors()) call allows any origin and must
be restricted because this server exposes unauthenticated write routes (PATCH
/api/applications/:num and POST /api/followups). Replace the global permissive
CORS with a scoped CORS policy: remove or change app.use(cors()) and instead
apply cors with explicit options (allowed origin list, allowed methods, and
credentials policy) only to the routes that need it or apply stricter options
for write-capable endpoints (e.g., use cors({ origin:
['https://your-trusted-origin.com'], methods: ['POST','PATCH'] }) when mounting
the handlers for the PATCH /api/applications/:num and POST /api/followups
routes); update any usage around express.json() as needed so route-specific
middleware ordering remains correct.
- Around line 175-178: The route handler for
app.get('/api/interview-file/:name') directly joins req.params.name into a
filesystem path (path.join(ROOT, 'interview-prep', req.params.name)), allowing
path traversal; fix it by normalizing/resolving the requested path and rejecting
any path outside the intended directory: decode and validate req.params.name,
compute resolved = path.resolve(path.join(ROOT, 'interview-prep'), safeName) (or
use path.resolve(ROOT, 'interview-prep', req.params.name)) and then ensure
path.relative(path.join(ROOT, 'interview-prep'), resolved) does not start with
'..' (or verified resolved.startsWith(expectedDir)), returning 404/400 if
validation fails, and only then read the file; also reject suspicious characters
(null bytes) and empty names to be safe.

---

Outside diff comments:
In `@batch/batch-prompt.md`:
- Around line 195-205: Add the required verification header line to the report
header block: insert the exact line "**Verification:** unconfirmed (batch mode)"
into the header section near the existing metadata (the block containing "#
Evaluación: {Empresa} — {Rol}", "**Fecha:**", "**Arquetipo:**", "**Score:**",
"**Legitimacy:**", "**URL:**", "**PDF:**", "**Batch ID:**") so the header
includes that required marker when Playwright is unavailable.

In `@scan.mjs`:
- Around line 192-196: The function appendToPipeline currently calls
readFileSync(PIPELINE_PATH, 'utf-8') without verifying the file exists, causing
a crash on first-run or missing data; update appendToPipeline to check for the
file (using fs.existsSync or try/catch around fs.readFileSync) and if missing
create a sensible default file or set text to an empty string before appending,
then write back to PIPELINE_PATH; ensure you reference PIPELINE_PATH and use
safe path handling (no user-controlled paths) when creating the file.

In `@test-all.mjs`:
- Around line 229-231: The allowlist currently uses substring matching
(allowedFiles.some(a => file.includes(a))) which can falsely whitelist paths;
change it to exact matching by comparing the filename or normalized path
segments instead (e.g., replace file.includes(a) with path.basename(file) === a
or with an exact path equality/endsWith check after normalizing). Ensure you
require/import path if needed and update the condition that uses allowedFiles
and the file variable so only exact filename or exact normalized path matches
are treated as allowed.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8756800c-1890-4e2a-bc06-6a9df01409ff

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1f3a3 and 472522e.

⛔ Files ignored due to path filters (22)
  • web-ui/app/favicon.ico is excluded by !**/*.ico
  • web-ui/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • web-ui/public/github.png is excluded by !**/*.png
  • web-ui/public/images/404.svg is excluded by !**/*.svg
  • web-ui/public/images/500.svg is excluded by !**/*.svg
  • web-ui/public/images/avatars/1.png is excluded by !**/*.png
  • web-ui/public/images/avatars/10.png is excluded by !**/*.png
  • web-ui/public/images/avatars/11.png is excluded by !**/*.png
  • web-ui/public/images/avatars/12.png is excluded by !**/*.png
  • web-ui/public/images/avatars/2.png is excluded by !**/*.png
  • web-ui/public/images/avatars/3.png is excluded by !**/*.png
  • web-ui/public/images/avatars/4.png is excluded by !**/*.png
  • web-ui/public/images/avatars/5.png is excluded by !**/*.png
  • web-ui/public/images/avatars/6.png is excluded by !**/*.png
  • web-ui/public/images/avatars/7.png is excluded by !**/*.png
  • web-ui/public/images/avatars/8.png is excluded by !**/*.png
  • web-ui/public/images/avatars/9.png is excluded by !**/*.png
  • web-ui/public/images/cover.png is excluded by !**/*.png
  • web-ui/public/logo.png is excluded by !**/*.png
  • web-ui/public/preview.png is excluded by !**/*.png
  • web-ui/public/preview2.png is excluded by !**/*.png
  • web-ui/public/seo.jpg is excluded by !**/*.jpg
📒 Files selected for processing (146)
  • .claude/skills/career-ops/SKILL.md
  • .claude/skills/career-ops/SKILL.md
  • .github/FUNDING.yml
  • .github/workflows/test.yml
  • AGENTS.md
  • CLAUDE.md
  • DATA_CONTRACT.md
  • GEMINI.md
  • README.md
  • VERSION
  • batch/batch-prompt.md
  • batch/batch-runner.sh
  • dashboard/go.mod
  • dashboard/internal/ui/screens/pipeline.go
  • dashboard/internal/ui/screens/pipeline_test.go
  • dashboard/internal/ui/screens/viewer.go
  • dashboard/internal/ui/screens/viewer_test.go
  • docs/ARCHITECTURE.md
  • gemini-eval.mjs
  • merge-tracker.mjs
  • modes/_shared.md
  • modes/auto-pipeline.md
  • modes/batch.md
  • modes/contacto.md
  • modes/deep.md
  • modes/ja/_shared.md
  • modes/oferta.md
  • modes/ofertas.md
  • modes/pdf.md
  • modes/pipeline.md
  • modes/project.md
  • modes/pt/_shared.md
  • modes/ru/_shared.md
  • modes/scan.md
  • package.json
  • scan.mjs
  • templates/cv-template.html
  • templates/portals.example.yml
  • test-all.mjs
  • update-system.mjs
  • web-ui/.eslintrc
  • web-ui/.gitignore
  • web-ui/.prettierrc
  • web-ui/README.md
  • web-ui/app/(guest)/404-page/page.tsx
  • web-ui/app/(guest)/500-page/page.tsx
  • web-ui/app/(guest)/layout.tsx
  • web-ui/app/(guest)/login/page.tsx
  • web-ui/app/(guest)/register/page.tsx
  • web-ui/app/dashboard/analytics/page.tsx
  • web-ui/app/dashboard/data.json
  • web-ui/app/dashboard/followups/page.tsx
  • web-ui/app/dashboard/interview/page.tsx
  • web-ui/app/dashboard/layout.tsx
  • web-ui/app/dashboard/page.tsx
  • web-ui/app/dashboard/pipeline/page.tsx
  • web-ui/app/dashboard/settings/layout.tsx
  • web-ui/app/dashboard/settings/page.tsx
  • web-ui/app/dashboard/settings/profile-form.tsx
  • web-ui/app/dashboard/settings/sidebar-nav.tsx
  • web-ui/app/dashboard/tracker/page.tsx
  • web-ui/app/dashboard/users/data-table.tsx
  • web-ui/app/dashboard/users/data.json
  • web-ui/app/dashboard/users/page.tsx
  • web-ui/app/globals.css
  • web-ui/app/layout.tsx
  • web-ui/components.json
  • web-ui/components/anchor.tsx
  • web-ui/components/app-sidebar.tsx
  • web-ui/components/chart-area-interactive.tsx
  • web-ui/components/data-table.tsx
  • web-ui/components/icon.tsx
  • web-ui/components/layout/header.tsx
  • web-ui/components/layout/logo.tsx
  • web-ui/components/layout/search.tsx
  • web-ui/components/layout/sidebar.tsx
  • web-ui/components/nav-main.tsx
  • web-ui/components/nav-secondary.tsx
  • web-ui/components/nav-user.tsx
  • web-ui/components/providers.tsx
  • web-ui/components/section-cards.tsx
  • web-ui/components/site-header.tsx
  • web-ui/components/ui/accordion.tsx
  • web-ui/components/ui/alert-dialog.tsx
  • web-ui/components/ui/alert.tsx
  • web-ui/components/ui/aspect-ratio.tsx
  • web-ui/components/ui/avatar.tsx
  • web-ui/components/ui/badge.tsx
  • web-ui/components/ui/breadcrumb.tsx
  • web-ui/components/ui/button-group.tsx
  • web-ui/components/ui/button.tsx
  • web-ui/components/ui/calendar.tsx
  • web-ui/components/ui/card.tsx
  • web-ui/components/ui/carousel.tsx
  • web-ui/components/ui/chart.tsx
  • web-ui/components/ui/checkbox.tsx
  • web-ui/components/ui/collapsible.tsx
  • web-ui/components/ui/command.tsx
  • web-ui/components/ui/context-menu.tsx
  • web-ui/components/ui/dialog.tsx
  • web-ui/components/ui/drawer.tsx
  • web-ui/components/ui/dropdown-menu.tsx
  • web-ui/components/ui/empty.tsx
  • web-ui/components/ui/field.tsx
  • web-ui/components/ui/form.tsx
  • web-ui/components/ui/hover-card.tsx
  • web-ui/components/ui/input-group.tsx
  • web-ui/components/ui/input-otp.tsx
  • web-ui/components/ui/input.tsx
  • web-ui/components/ui/item.tsx
  • web-ui/components/ui/kbd.tsx
  • web-ui/components/ui/label.tsx
  • web-ui/components/ui/menubar.tsx
  • web-ui/components/ui/navigation-menu.tsx
  • web-ui/components/ui/pagination.tsx
  • web-ui/components/ui/popover.tsx
  • web-ui/components/ui/progress.tsx
  • web-ui/components/ui/radio-group.tsx
  • web-ui/components/ui/scroll-area.tsx
  • web-ui/components/ui/select.tsx
  • web-ui/components/ui/separator.tsx
  • web-ui/components/ui/sheet.tsx
  • web-ui/components/ui/sidebar.tsx
  • web-ui/components/ui/skeleton.tsx
  • web-ui/components/ui/slider.tsx
  • web-ui/components/ui/sonner.tsx
  • web-ui/components/ui/spinner.tsx
  • web-ui/components/ui/switch.tsx
  • web-ui/components/ui/table.tsx
  • web-ui/components/ui/tabs.tsx
  • web-ui/components/ui/textarea.tsx
  • web-ui/components/ui/toggle-group.tsx
  • web-ui/components/ui/toggle.tsx
  • web-ui/components/ui/tooltip.tsx
  • web-ui/eslint.config.mjs
  • web-ui/hooks/use-mobile.ts
  • web-ui/hooks/use-mobile.tsx
  • web-ui/lib/api.ts
  • web-ui/lib/routes-config.tsx
  • web-ui/lib/utils.ts
  • web-ui/next.config.ts
  • web-ui/package.json
  • web-ui/postcss.config.mjs
  • web-ui/proxy.ts
  • web-ui/server.mjs
  • web-ui/tsconfig.json

Comment thread CLAUDE.md
Comment on lines +1 to +2
@AGENTS.md
<!-- Add anything Claude Code specific that other agents don't need -->
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a top-level heading to satisfy markdownlint MD041.

Line 1 is not an H1, which can fail markdown linting in CI.

Proposed fix
+ # CLAUDE
+
  `@AGENTS.md`
  <!-- Add anything Claude Code specific that other agents don't need -->
📝 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
@AGENTS.md
<!-- Add anything Claude Code specific that other agents don't need -->
# CLAUDE
`@AGENTS.md`
<!-- Add anything Claude Code specific that other agents don't need -->
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 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 `@CLAUDE.md` around lines 1 - 2, The file CLAUDE.md currently starts with a
non-H1 line ("`@AGENTS.md`"), causing markdownlint MD041; prepend a top-level
heading (H1) as the very first line (for example "# CLAUDE" or "# Claude Code")
so the file begins with an H1 before the existing "`@AGENTS.md`" include,
satisfying the linter.

Comment thread DATA_CONTRACT.md
| `data/scan-history.tsv` | Your scan history |
| `data/follow-ups.md` | Your follow-up history |
| `writing-samples/*` | Your personal writing samples for style calibration |
| `writing-samples/*` | Your personal writing samples for style calibration (except `writing-samples/README.md`, which is system-owned documentation delivered by updates) |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not reclassify a previously user-layer path into system-layer ownership.

This change makes writing-samples/README.md system-owned while the parent writing-samples/* path is user data. That weakens the contract and can permit updater writes inside a user-layer directory.

As per coding guidelines: DATA_CONTRACT.md: “This file defines system vs user file boundaries. Changes here are critical — reject if user-layer files are being reclassified.”

Also applies to: 65-65

🤖 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 `@DATA_CONTRACT.md` at line 21, The change reclassifies a file under a
user-owned directory by marking `writing-samples/README.md` as system-owned
while `writing-samples/*` remains user-owned; revert that by keeping ownership
consistent: either make `writing-samples/README.md` user-owned (keep
`writing-samples/*` as-is) or move the system-owned README into a true system
directory, and update the DATA_CONTRACT entry for `writing-samples/*` and
`writing-samples/README.md` so parent and child paths have consistent ownership.

Comment thread modes/pipeline.md
3. **If there are 3+ pending URLs**, launch agents in parallel (Agent tool with `run_in_background`) to maximize speed.
4. **At the end**, show summary table:

```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix markdownlint fence issues (MD040, MD031).

Line 17 code fence needs a language tag, and the bash fence around Lines 54-56 should be surrounded by blank lines to satisfy markdownlint.

Also applies to: 54-56

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@modes/pipeline.md` at line 17, Add a language tag to the opening code fence
that currently lacks one (the standalone ``` fence) so markdownlint MD040 is
satisfied, and ensure the bash code block around the lines containing the bash
fence (the ```bash ... ``` block) is separated by blank lines above and below it
to satisfy MD031; update the two offending fences to use ```<language> for the
first and to add a blank line before and after the existing ```bash block.

Comment thread modes/scan.md
Comment on lines +112 to +120
6b. **Filtrar por ubicación (opcional)** usando `location_filter` de `portals.yml`:
- Si el bloque `location_filter` está ausente, todas las ubicaciones pasan (comportamiento por defecto)
- Ubicación vacía en una oferta → pasa (no penalizar datos faltantes)
- Cualquier keyword de `block` presente → rechazar (precedencia sobre allow)
- `allow` vacío → pasa (ya superó block)
- `allow` no vacío → debe coincidir al menos una keyword
- Todas las coincidencias son case-insensitive substring
- La ubicación se persiste como 7ª columna en `scan-history.tsv` para auditoría posterior

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align scan-history.tsv contract updates across the full workflow.

Line 119 says location decision is persisted as a 7th column, but the write format and schema/example in this file still describe 6 columns. This creates an ambiguous data contract for writers/parsers.

📝 Proposed doc alignment
-  b. Registrar en `scan-history.tsv`: `{url}\t{date}\t{query_name}\t{title}\t{company}\tadded`
+  b. Registrar en `scan-history.tsv`: `{url}\t{date}\t{query_name}\t{title}\t{company}\tadded\t{location_decision}`

-`data/scan-history.tsv` trackea TODAS las URLs vistas:
+`data/scan-history.tsv` trackea TODAS las URLs vistas (incluye decisión de ubicación):

-url	first_seen	portal	title	company	status
-https://...	2026-02-10	Ashby — AI PM	PM AI	Acme	added
+url	first_seen	portal	title	company	status	location_decision
+https://...	2026-02-10	Ashby — AI PM	PM AI	Acme	added	allow_match
🤖 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 `@modes/scan.md` around lines 112 - 120, The docs claim the location decision
from location_filter in portals.yml is persisted as a 7th column in
scan-history.tsv, but the file's write format/schema/example still shows only 6
columns; update the documentation and any examples to consistently describe the
7-column contract (include the new 7th "location_decision" column), and update
any writer/parser references to the scan-history.tsv format to read/write that
7th column; ensure references to location_filter, portals.yml, and
scan-history.tsv are synchronized so readers and implementers know the exact
column order and that the 7th column contains the persisted location decision.

Comment thread README.md
```

> **Free tier:** Both options work without billing. Native CLI uses Google OAuth; the API script uses `gemini-2.0-flash` (15 RPM, 1M tokens/day free).
> **Free tier:** Both options work without billing. Native CLI uses Google OAuth; the API script uses `gemini-2.5-flash` (15 RPM, 1M tokens/day free).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What are the current free-tier limits and default free model for Gemini API and Gemini CLI, according to official Google documentation?

💡 Result:

As of May 17, 2026, the free-tier limits and model access for the Gemini API and Gemini CLI are structured as follows: Gemini API Free Tier The Gemini API free tier is designed for developers and small projects [1][2]. It provides free access to input and output tokens, but it is subject to specific rate limits and model restrictions [1][2]. - Model Access: The free tier is restricted to Flash models (e.g., Gemini 2.5 Flash and Flash-Lite) [3]. Access to Pro models typically requires a paid API key or a subscription [3]. - Rate Limits: Limits are project-based and can be viewed in Google AI Studio [4][5]. While specific limits vary by model, they are generally lower than those in paid tiers [4][5][6]. - Data Usage: Content submitted via the free tier may be used by Google to improve its products [1][2]. Gemini CLI Free Tier The Gemini CLI offers a free tier that depends on the authentication method used [7][8]. - Authentication via Google Account (Gemini Code Assist for individuals): This method allows for 1,000 model requests per user per day and 60 model requests per user per minute [7][9][8]. - Authentication via Gemini API Key (Unpaid): This method is limited to 250 model requests per user per day and 10 model requests per user per minute, with access restricted to Flash models only [7][8]. - Vertex AI (Express Mode): This offers a trial period (typically 90 days) before billing must be enabled, with variable quotas and model access specific to the account [7][8]. Note: Usage limits and model availability are subject to change [10]. Developers are encouraged to check the official Google AI Studio and Gemini API documentation for the most current information [4][5][10].

Citations:


Consider removing or qualifying hardcoded Gemini quotas in README.

Line 160 specifies 15 RPM and 1M tokens/day, but official Google documentation indicates these limits are "project-based," "vary by model," and "subject to change." These values are not clearly confirmed in the official pricing or rate-limits pages. Either link to the official Gemini API rate limits and Gemini CLI quota page instead, or replace specific numbers with a note like "Free tier limits apply—see official docs for current quotas."

🤖 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 `@README.md` at line 160, Update the README entry that currently reads "**Free
tier:** Both options work without billing. Native CLI uses Google OAuth; the API
script uses `gemini-2.5-flash` (15 RPM, 1M tokens/day free)." to avoid hardcoded
Gemini quotas: either remove the specific numbers or replace them with a
qualifier like "Free tier limits apply—see official docs for current quotas" and
add links to the official Gemini rate limits and pricing pages (e.g.,
ai.google.dev/gemini-api/docs/rate-limits and
ai.google.dev/gemini-api/docs/pricing or the Gemini CLI quota page) so readers
are pointed to authoritative, up-to-date quota information.

items: [
{
title: "Dashboard",
href: "/dashboard/default",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Several configured hrefs do not match existing app routes.

These paths map to non-existent pages, so menu navigation will 404.

Proposed fix
-        href: "/dashboard/default",
+        href: "/dashboard",
@@
-      { title: "Users", href: "/dashboard/pages/users", icon: "Users" },
+      { title: "Users", href: "/dashboard/users", icon: "Users" },
@@
-        href: "/dashboard/pages/settings",
+        href: "/dashboard/settings",
@@
-          { title: "404", href: "/pages/error/404" },
-          { title: "500", href: "/pages/error/500" }
+          { title: "404", href: "/404-page" },
+          { title: "500", href: "/500-page" }

Also applies to: 23-23, 26-26, 43-44

🤖 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 `@web-ui/lib/routes-config.tsx` at line 20, Several menu entries in the routes
configuration use incorrect href strings (e.g., the href property currently set
to "/dashboard/default") causing navigation 404s; update the href values on the
route objects in routes-config.tsx (the objects that contain the href property)
to exactly match the app's actual route paths as defined by the router (replace
"/dashboard/default" and the other mismatched href values referenced in the file
with the correct routes such as the real dashboard path, user path, etc.), and
verify by cross-checking each href against the app's route definitions and
running the app to ensure no 404s from menu clicks.

Comment thread web-ui/lib/utils.ts
Comment on lines +16 to +22
export function generateMeta({
title,
description,
}: {
title: string;
description: string;
}): Metadata {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use an interface for generateMeta params.

Please extract the inline object shape at Lines 19-22 into an interface and use it in the function signature for consistency across TypeScript files.

As per coding guidelines: "**/*.ts: Prefer interface for defining object shapes in TypeScript".

🤖 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 `@web-ui/lib/utils.ts` around lines 16 - 22, Extract the inline parameter type
for generateMeta into a named interface (e.g., GenerateMetaParams) and update
the function signature to accept that interface instead of the inline object;
specifically, declare interface GenerateMetaParams { title: string; description:
string } and change the generateMeta parameter to use GenerateMetaParams to
conform to the project style and reuse the type elsewhere.

Comment thread web-ui/lib/utils.ts Outdated
Comment thread web-ui/next.config.ts
Comment on lines +5 to +13
remotePatterns: [
{
protocol: "https",
hostname: "**"
},
{
protocol: "http",
hostname: "**"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

❓ Verification inconclusive

For Next.js Image Optimization (v16), what are the documented security implications of using images.remotePatterns with hostname "**" and protocol "http"?


Using remotePatterns with hostname "" and protocol "http" in Next.js is strongly discouraged because it effectively disables the security protections provided by the Image Optimization API [1][2]. The primary security implications include: 1. Unrestricted Image Optimization: The Next.js Image Optimization API is a server-side process that fetches, resizes, and optimizes images [3][1]. By allowing any hostname and the insecure "http" protocol, you permit the API to fetch images from any source on the internet [1][2]. Malicious actors can exploit this to force your server to perform resource-intensive operations on arbitrary images, potentially leading to Denial of Service (DoS) attacks or increased infrastructure costs [4][1]. 2. Exposure to Malicious Content: Allowing arbitrary remote sources means your application may inadvertently serve malicious files [3][1]. While Next.js validates that the requested source is an image, SVGs are a notable risk because they can contain embedded scripts, event handlers, or other malicious payloads that could execute in the context of your users' browsers [3]. 3. Protocol Downgrade and Interception: Using "http" instead of "https" exposes the image fetching process to Man-in-the-Middle (MitM) attacks [1]. Because the communication is unencrypted, an attacker could intercept or modify the image data in transit before it reaches your server for optimization [1]. 4. Bypassing Intended Restrictions: The remotePatterns configuration is designed to ensure that only images from trusted, verified sources are processed [1][2]. Using wildcards like "" negates this "allow-list" approach, effectively turning your image optimization endpoint into an open proxy for external content [4][1]. Official documentation explicitly warns that omitting specific protocol, port, or hostname configurations is not recommended because it may allow malicious actors to optimize URLs you did not intend [1][2]. It is best practice to be as specific as possible, defining only the exact domains and protocols required by your application [1][2].

Citations:


Restrict remotePatterns to specific trusted hosts and HTTPS only.

Lines 8 and 12 allow all hosts with the wildcard "**", and line 11 permits unencrypted HTTP. This configuration disables Next.js Image Optimization security protections, allowing malicious actors to exploit your server for resource-intensive operations (DoS attacks, cost inflation), expose you to malicious content (particularly SVGs with embedded scripts), and expose image fetches to man-in-the-middle interception. Specify explicit trusted hostnames and enforce HTTPS.

🔒 Proposed hardening
 const nextConfig: NextConfig = {
   images: {
     remotePatterns: [
-      {
-        protocol: "https",
-        hostname: "**"
-      },
-      {
-        protocol: "http",
-        hostname: "**"
-      }
+      { protocol: "https", hostname: "avatars.githubusercontent.com" },
+      { protocol: "https", hostname: "images.company.com" }
     ]
   }
 };
📝 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
remotePatterns: [
{
protocol: "https",
hostname: "**"
},
{
protocol: "http",
hostname: "**"
}
remotePatterns: [
{ protocol: "https", hostname: "avatars.githubusercontent.com" },
{ protocol: "https", hostname: "images.company.com" }
]
🤖 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 `@web-ui/next.config.ts` around lines 5 - 13, The remotePatterns array in
next.config.ts currently allows all hosts ("**") and unencrypted HTTP; update
the remotePatterns entries to list only explicit trusted hostnames and use
protocol: "https" (remove the http entry and the wildcard hostnames). Edit the
remotePatterns array (the objects currently with protocol and hostname keys) to
replace hostname: "**" with concrete hostnames you trust (e.g., your CDN and
backend domains) and ensure protocol is "https" for each entry so only encrypted
origins are permitted.

Comment thread web-ui/README.md Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment thread AGENTS.md
Comment on lines +85 to +116
#### Step 1: CV (required)
If `cv.md` is missing, ask:
> "I don't have your CV yet. You can either:
> 1. Paste your CV here and I'll convert it to markdown
> 2. Paste your LinkedIn URL and I'll extract the key info
> 3. Tell me about your experience and I'll draft a CV for you
>
> Which do you prefer?"

Create `cv.md` from whatever they provide. Make it clean markdown with standard sections (Summary, Experience, Projects, Education, Skills).

#### Step 2: Profile (required)
If `config/profile.yml` is missing, copy from `config/profile.example.yml` and then ask:
> "I need a few details to personalize the system:
> - Your full name and email
> - Your location and timezone
> - What roles are you targeting? (e.g., 'Senior Backend Engineer', 'AI Product Manager')
> - Your salary target range
>
> I'll set everything up for you."

Fill in `config/profile.yml` with their answers. For archetypes and targeting narrative, store the user-specific mapping in `modes/_profile.md` or `config/profile.yml` rather than editing `modes/_shared.md`.

#### Step 3: Portals (recommended)
If `portals.yml` is missing:
> "I'll set up the job scanner with 45+ pre-configured companies. Want me to customize the search keywords for your target roles?"

Copy `templates/portals.example.yml` → `portals.yml`. If they gave target roles in Step 2, update `title_filter.positive` to match.

#### Step 4: Tracker
If `data/applications.md` doesn't exist, create it:
```markdown
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix markdownlint violations to keep docs checks clean.

markdownlint-cli2 is already flagging heading/fence formatting here (MD022/MD031/MD040). Add required blank lines around headings/fences and specify a language for the TSV fenced block to avoid CI/doc-lint noise.

Also applies to: 140-140, 293-295

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 85-85: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 96-96: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 108-108: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 114-114: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 116-116: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 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 `@AGENTS.md` around lines 85 - 116, The AGENTS.md fragment has markdownlint
violations (MD022/MD031/MD040) due to missing blank lines around headings and
fenced blocks and an unspecified fenced-block language for the TSV; update the
Step 1/Step 2/Step 3/Step 4 headings and any adjacent fenced blocks by inserting
a blank line before and after each heading and fence, and add a language
identifier (e.g., "tsv" or "text") to the fenced TSV block so the linter
recognizes it; search for the headings "Step 1: CV (required)", "Step 2: Profile
(required)", "Step 3: Portals (recommended)", and "Step 4: Tracker" plus the TSV
fenced block in AGENTS.md to apply these changes.

Comment thread gemini-eval.mjs
Comment on lines 52 to 63
const PATHS = {
// Primary evaluation logic lives in these two mode files
shared: join(ROOT, 'modes', '_shared.md'),
oferta: join(ROOT, 'modes', 'oferta.md'),
shared: join(ROOT, 'modes', '_shared.md'),
oferta: join(ROOT, 'modes', 'oferta.md'),
// Canonical skill path referenced in Issue #344
evaluate: join(ROOT, '.claude', 'skills', 'career-ops', 'SKILL.md'),
cv: join(ROOT, 'cv.md'),
reports: join(ROOT, 'reports'),
tracker: join(ROOT, 'data', 'applications.md'),
evaluate: join(ROOT, '.claude', 'skills', 'career-ops', 'SKILL.md'),
cv: join(ROOT, 'cv.md'),
profile: join(ROOT, 'modes', '_profile.md'),
profileYml: join(ROOT, 'config', 'profile.yml'),
reports: join(ROOT, 'reports'),
tracker: join(ROOT, 'data', 'applications.md'),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include article-digest.md in evaluation context to satisfy metrics-source contract.

The evaluator currently loads cv.md but not article-digest.md, so metrics can drift from the required source-of-truth pair.

Proposed fix
 const PATHS = {
   shared:      join(ROOT, 'modes', '_shared.md'),
   oferta:      join(ROOT, 'modes', 'oferta.md'),
   evaluate:    join(ROOT, '.claude', 'skills', 'career-ops', 'SKILL.md'),
   cv:          join(ROOT, 'cv.md'),
+  articleDigest: join(ROOT, 'article-digest.md'),
   profile:     join(ROOT, 'modes', '_profile.md'),
   profileYml:  join(ROOT, 'config', 'profile.yml'),
   reports:     join(ROOT, 'reports'),
   tracker:     join(ROOT, 'data', 'applications.md'),
 };
@@
 const cvContent      = readFile(PATHS.cv,          'cv.md');
+const articleDigest  = readFile(PATHS.articleDigest, 'article-digest.md');
 const profileContent = readFile(PATHS.profile,     'modes/_profile.md');
 const profileYml     = readFile(PATHS.profileYml,  'config/profile.yml');
@@
 ${cvContent}
 
+═══════════════════════════════════════════════════════
+ARTICLE DIGEST METRICS (article-digest.md)
+═══════════════════════════════════════════════════════
+${articleDigest}
+
 ═══════════════════════════════════════════════════════
 CANDIDATE PROFILE & TARGETS (config/profile.yml)
 ═══════════════════════════════════════════════════════
 ${profileYml}
As per coding guidelines: “Hardcode NO metrics in evaluation logic; read all metrics from `cv.md` and `article-digest.md` at evaluation time”.

Also applies to: 179-217

🤖 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 `@gemini-eval.mjs` around lines 52 - 63, The PATHS object currently omits
article-digest.md so the evaluator only loads cv.md; update the PATHS constant
(key name like articleDigest or articleDigestMd) to include join(ROOT,
'article-digest.md') and then modify the evaluation-loading logic that reads
cv.md (the same code referenced around the block that handles PATHS) to also
read article-digest.md at evaluation time so metrics are sourced from both files
per the metrics-source contract.

Comment thread GEMINI.md
Comment on lines +1 to +2
@./AGENTS.md
<!-- Add anything Gemini specific that other agents don't need -->
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a top-level heading to satisfy markdown linting.

Line 1 currently starts with an include directive, which triggers MD041 (first-line-heading). Add an H1 before the include so docs lint stays clean.

💡 Suggested patch
+# Gemini
+
 @./AGENTS.md
 <!-- Add anything Gemini specific that other agents don't need -->
📝 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
@./AGENTS.md
<!-- Add anything Gemini specific that other agents don't need -->
# Gemini
@./AGENTS.md
<!-- Add anything Gemini specific that other agents don't need -->
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 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 `@GEMINI.md` around lines 1 - 2, Add a top-level H1 heading at the start of
GEMINI.md to satisfy MD041 (first-line-heading) before the existing include
directive "@./AGENTS.md"; update the file so the first line is a Markdown H1
(e.g., "# Gemini") followed by the existing include line to keep the include
intact and resolve the lint error.

Comment thread test-all.mjs
console.log('\n9. AGENTS.md integrity');

const claude = readFile('CLAUDE.md');
const agents = readFile('AGENTS.md');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail AGENTS integrity checks gracefully when the file is missing.

Line 287 reads AGENTS.md unconditionally. If the file is absent, the script throws before producing the normal summary output.

Proposed fix
-const agents = readFile('AGENTS.md');
+if (!fileExists('AGENTS.md')) {
+  fail('Missing system file: AGENTS.md');
+}
+const agents = fileExists('AGENTS.md') ? readFile('AGENTS.md') : '';
🤖 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 `@test-all.mjs` at line 287, The call that unconditionally reads AGENTS.md via
readFile (assigning to agents) should handle a missing file gracefully; wrap the
readFile invocation (where agents is set) in a presence check or try/catch and
if the file is absent or read fails, set agents to null/empty and emit a
non-fatal warning so the rest of test-all.mjs can continue to produce the normal
summary; update any downstream logic that assumes agents is a string to tolerate
null/empty (e.g., in validation or summary functions).

Comment on lines +65 to +80
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="w-full"
placeholder="Password"
/>
</div>
</div>

<div>
<Button type="submit" className="w-full">
Sign in
</Button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix register form semantics (new-password) and CTA copy.

On a registration flow, autoComplete="current-password" and "Sign in" are incorrect. Use new-password and a registration action label.

Suggested fix
                 <Input
                   id="password"
                   name="password"
                   type="password"
-                  autoComplete="current-password"
+                  autoComplete="new-password"
                   required
                   className="w-full"
                   placeholder="Password"
                 />
...
             <div>
               <Button type="submit" className="w-full">
-                Sign in
+                Create account
               </Button>
             </div>
🤖 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 `@web-ui/app/`(guest)/register/page.tsx around lines 65 - 80, The password
input and submit CTA are using sign-in semantics; update the Input component
with id/name "password" to use autoComplete="new-password" (replace
current-password) and change the Button text from "Sign in" to a
registration-appropriate label such as "Create account" (or "Register") so the
Input (id/name="password") and the Button(type="submit") reflect registration
semantics.

Comment thread web-ui/lib/api.ts
Comment on lines +80 to +121
export async function getApplications(): Promise<Application[]> {
const r = await fetch(`${BASE}/api/applications`, { cache: "no-store" })
if (!r.ok) return []
return r.json()
}

export async function getPipeline(): Promise<PipelineItem[]> {
const r = await fetch(`${BASE}/api/pipeline`, { cache: "no-store" })
if (!r.ok) return []
return r.json()
}

export async function getFollowUps(): Promise<FollowUp[]> {
const r = await fetch(`${BASE}/api/followups`, { cache: "no-store" })
if (!r.ok) return []
return r.json()
}

export async function getReport(num: string): Promise<{ content: string } | null> {
const r = await fetch(`${BASE}/api/report/${num}`, { cache: "no-store" })
if (!r.ok) return null
return r.json()
}

export async function getProfile(): Promise<Profile> {
const r = await fetch(`${BASE}/api/profile`, { cache: "no-store" })
if (!r.ok) return {}
return r.json()
}

export async function getStoryBank(): Promise<string> {
const r = await fetch(`${BASE}/api/storybank`, { cache: "no-store" })
if (!r.ok) return "# Story Bank\n\nNo stories yet."
const d = await r.json()
return d.content
}

export async function getInterviewFiles(): Promise<string[]> {
const r = await fetch(`${BASE}/api/interview-files`, { cache: "no-store" })
if (!r.ok) return []
return r.json()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Catch network errors in fetch helpers, not just non-OK responses.

All helpers currently return fallbacks only when r.ok is false; a network/refused/DNS error throws before that and will bubble up. Wrap each call (or a shared helper) in try/catch so the documented safe defaults are actually guaranteed.

Suggested pattern
+async function safeJson<T>(url: string, fallback: T): Promise<T> {
+  try {
+    const r = await fetch(url, { cache: "no-store" })
+    if (!r.ok) return fallback
+    return (await r.json()) as T
+  } catch {
+    return fallback
+  }
+}
+
 export async function getApplications(): Promise<Application[]> {
-  const r = await fetch(`${BASE}/api/applications`, { cache: "no-store" })
-  if (!r.ok) return []
-  return r.json()
+  return safeJson<Application[]>(`${BASE}/api/applications`, [])
 }
🤖 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 `@web-ui/lib/api.ts` around lines 80 - 121, Wrap the network calls in
getApplications, getPipeline, getFollowUps, getReport, getProfile, getStoryBank,
and getInterviewFiles in try/catch (or extract a shared fetch helper) so
network/DNS/refused errors are caught and the existing fallback values are
returned instead of allowing exceptions to bubble; ensure the catch returns the
same defaults currently used for non-OK responses ([], null, {}, "# Story
Bank\n\nNo stories yet.", etc.) and that successful responses still call
r.json().

Comment thread web-ui/package.json
"marked": "^12.0.0",
"next": "16.1.0",
"next-themes": "^0.3.0",
"radix-ui": "latest",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Check the package.json file for the radix-ui dependency
cat -n web-ui/package.json | grep -A 2 -B 2 "radix-ui"

Repository: santifer/career-ops

Length of output: 511


🏁 Script executed:

# Search for all imports from radix-ui in the codebase
rg "from\s+['\"]radix-ui['\"]" web-ui/ -A 2

Repository: santifer/career-ops

Length of output: 7047


🏁 Script executed:

# Also check for `@radix-ui` imports
rg "from\s+['\"]`@radix-ui`" web-ui/ -A 2

Repository: santifer/career-ops

Length of output: 329


🏁 Script executed:

# List the structure of web-ui to understand the project layout
fd -t f -e json web-ui/package.json && wc -l web-ui/package.json

Repository: santifer/career-ops

Length of output: 86


🌐 Web query:

radix-ui npm package exports components unified vs individual @radix-ui packages

💡 Result:

Radix UI offers two primary ways to consume its primitives: via a unified package or via individual scoped packages [1][2]. Unified Package (radix-ui) The radix-ui package acts as a single entry point that bundles the various Radix Primitives [3][1]. It is designed to simplify dependency management and help prevent version conflicts or duplication [1][2]. The package is tree-shakeable, meaning that despite being a unified bundle, you should only ship the components you actually use in your application [1][2]. As of February 2026, tools like shadcn/ui have moved toward using this unified package to provide a cleaner dependency structure in package.json [4]. Individual Packages (@radix-ui/react-*) Alternatively, you can install and import primitives individually (e.g., @radix-ui/react-dialog, @radix-ui/react-dropdown-menu) [1][2]. This approach provides granular control over which specific packages are installed [1]. When using this method, it is recommended to update all Radix packages together to avoid duplication of shared dependencies and to keep bundle sizes optimized [1][2]. Considerations While the unified package simplifies installation, some users have reported concerns regarding dependency bloat if the unified package does not stay perfectly synchronized with the latest versions of the individual primitives [5]. Additionally, the unified package's exports map has been noted to cause issues with certain Node.js resolution patterns, such as resolving package.json files, due to its use of a wildcard export [6]. Note: Radix Themes is a separate, pre-styled library and is consumed via its own package, @radix-ui/themes [7].

Citations:


Pin radix-ui to a specific tested version, not latest.

Line 41 uses "radix-ui": "latest", which makes builds non-reproducible and risks unexpected breaking changes. Since ~30 UI component files depend on this package, pin to a known-good version (e.g., "radix-ui": "^0.x.y") to ensure deterministic installs.

🤖 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 `@web-ui/package.json` at line 41, Replace the non-deterministic dependency
entry for "radix-ui" in package.json (currently set to "latest") with a
specific, tested semver value (for example "radix-ui": "^0.x.y") to ensure
reproducible installs; update the dependency string in package.json and run a
fresh install and lockfile update (npm/yarn/pnpm) so package-lock.json /
yarn.lock reflects the pinned version, and ensure any CI/builder uses the
updated lockfile.

Comment thread web-ui/server.mjs
Comment on lines +12 to +13
app.use(cors())
app.use(express.json())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict CORS for write-capable local endpoints.

app.use(cors()) enables any origin, while this server exposes unauthenticated write routes (PATCH /api/applications/:num, POST /api/followups). A malicious site visited in the same browser can attempt cross-origin writes to localhost.

Suggested fix
 const app = express()
-app.use(cors())
+const allowedOrigin = process.env.WEB_UI_ORIGIN || 'http://localhost:3030'
+app.use(cors({ origin: allowedOrigin, methods: ['GET', 'PATCH', 'POST'] }))
 app.use(express.json())

Also applies to: 181-211

🤖 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 `@web-ui/server.mjs` around lines 12 - 13, The global app.use(cors()) call
allows any origin and must be restricted because this server exposes
unauthenticated write routes (PATCH /api/applications/:num and POST
/api/followups). Replace the global permissive CORS with a scoped CORS policy:
remove or change app.use(cors()) and instead apply cors with explicit options
(allowed origin list, allowed methods, and credentials policy) only to the
routes that need it or apply stricter options for write-capable endpoints (e.g.,
use cors({ origin: ['https://your-trusted-origin.com'], methods:
['POST','PATCH'] }) when mounting the handlers for the PATCH
/api/applications/:num and POST /api/followups routes); update any usage around
express.json() as needed so route-specific middleware ordering remains correct.

Comment thread web-ui/server.mjs
Comment on lines +175 to +178
app.get('/api/interview-file/:name', (req, res) => {
const file = path.join(ROOT, 'interview-prep', req.params.name)
if (!fs.existsSync(file)) return res.status(404).json({ error: 'File not found' })
res.json({ content: fs.readFileSync(file, 'utf8') })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Block path traversal in /api/interview-file/:name.

req.params.name is joined directly into a filesystem path. Inputs like ../../config/profile.yml can escape interview-prep and expose arbitrary files.

Suggested fix
 app.get('/api/interview-file/:name', (req, res) => {
-  const file = path.join(ROOT, 'interview-prep', req.params.name)
+  const baseDir = path.join(ROOT, 'interview-prep')
+  const candidate = path.resolve(baseDir, req.params.name)
+  if (!candidate.startsWith(baseDir + path.sep) || !candidate.endsWith('.md')) {
+    return res.status(400).json({ error: 'Invalid file name' })
+  }
+  const file = candidate
   if (!fs.existsSync(file)) return res.status(404).json({ error: 'File not found' })
   res.json({ content: fs.readFileSync(file, 'utf8') })
 })
🤖 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 `@web-ui/server.mjs` around lines 175 - 178, The route handler for
app.get('/api/interview-file/:name') directly joins req.params.name into a
filesystem path (path.join(ROOT, 'interview-prep', req.params.name)), allowing
path traversal; fix it by normalizing/resolving the requested path and rejecting
any path outside the intended directory: decode and validate req.params.name,
compute resolved = path.resolve(path.join(ROOT, 'interview-prep'), safeName) (or
use path.resolve(ROOT, 'interview-prep', req.params.name)) and then ensure
path.relative(path.join(ROOT, 'interview-prep'), resolved) does not start with
'..' (or verified resolved.startsWith(expectedDir)), returning 404/400 if
validation fails, and only then read the file; also reject suspicious characters
(null bytes) and empty names to be safe.

Comment thread web-ui/server.mjs
Comment on lines +192 to +195
if (status !== undefined) cols[5] = status.charAt(0).toUpperCase() + status.slice(1)
if (notes !== undefined) cols[8] = notes
updated = true
return '| ' + cols.join(' | ') + ' |'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape table-breaking characters before writing markdown rows.

notes/other fields are written directly into pipe-delimited markdown rows. | or newlines from user input will corrupt applications.md / follow-ups.md parsing.

Suggested fix
+function sanitizeCell(value) {
+  return String(value ?? '')
+    .replace(/\|/g, '\\|')
+    .replace(/\r?\n/g, ' ')
+    .trim()
+}
...
-    if (notes !== undefined) cols[8] = notes
+    if (notes !== undefined) cols[8] = sanitizeCell(notes)
...
-  const row = `| ${num} | ${company} | ${role} | ${appliedDate || today} | ${today} | ${nextAction || 'Follow up'} | ${dueDate || ''} | ${notes || ''} |`
+  const row = `| ${num} | ${sanitizeCell(company)} | ${sanitizeCell(role)} | ${sanitizeCell(appliedDate || today)} | ${today} | ${sanitizeCell(nextAction || 'Follow up')} | ${sanitizeCell(dueDate || '')} | ${sanitizeCell(notes || '')} |`

Also applies to: 208-208

🤖 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 `@web-ui/server.mjs` around lines 192 - 195, The markdown table row builder
writes raw user fields into cols (e.g., cols[5], cols[8]) and returns '| ' +
cols.join(' | ') + ' |', which lets pipe characters and newlines in values (like
notes) break parsing; before assigning to cols (where status and notes are set)
sanitize/escape table-breaking characters by replacing '|' with '\|' and
converting newlines to a single-space or escaped '\n' sequence (apply the same
escaping wherever other fields are written, including the similar block around
line 208) so the generated markdown row remains intact.

gauravlochab and others added 2 commits May 17, 2026 17:38
- section-cards: exclude zero-score apps from avg score calculation;
  fix copy to "Applied + Responded + Interview"; correct note to show
  count of scored roles
- nav-user: derive avatar initials from user.name instead of hardcoded "CN"
- layout/logo: replace bundui branding with "career-ops"
- nav-secondary: use Next.js Link instead of bare <a> for internal links
- layout/header: add rel="noopener noreferrer" to external target="_blank" links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- package.json: rename from "shadcn-ui-kit-free" to "career-ops-web-ui"
- lib/utils.ts: update page title template to "career-ops"
- layout/header.tsx: remove "Get Pro" upsell card and unused imports
- layout/sidebar.tsx: remove "Get Pro" upsell card and unused imports
- README.md: replace bundui README with career-ops web UI docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
web-ui/components/layout/header.tsx (2)

48-64: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Template promo branding is still visible in the header drawer.

The card text and outbound links still reference Shadcn UI Kit, which conflicts with the career-ops dashboard branding and can confuse users.

🤖 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 `@web-ui/components/layout/header.tsx` around lines 48 - 64, The header drawer
still contains template promo branding and external Shadcn links (CardTitle,
CardDescription, Button asChild with Link and LockIcon) that conflict with our
career-ops branding; update the CardTitle and CardDescription text to use
career-ops phrasing, replace the outbound hrefs (the Link components used inside
the Buttons) with the correct career-ops URLs or internal routes, and adjust the
CTA labels (e.g., "Get Pro", "Learn More") to match our product terminology so
the drawer reflects our branding and points to our domains instead of
shadcnuikit.com.

75-80: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a focusable control for the account menu trigger.

DropdownMenuTrigger asChild currently wraps a <figure>, which is not keyboard-focusable by default and blocks keyboard users from opening this menu.

♿ Suggested fix
-          <DropdownMenuTrigger asChild>
-            <figure className="cursor-pointer">
-              <img src={`/images/avatars/1.png`} className="h-10 w-10" alt="..." />
-            </figure>
-          </DropdownMenuTrigger>
+          <DropdownMenuTrigger asChild>
+            <button type="button" className="cursor-pointer" aria-label="Open account menu">
+              <img src="/images/avatars/1.png" className="h-10 w-10" alt="User avatar" />
+            </button>
+          </DropdownMenuTrigger>
#!/bin/bash
# Verify trigger element type used under DropdownMenuTrigger in header component
rg -n -C2 'DropdownMenuTrigger asChild|<figure className="cursor-pointer"|<button type="button"' web-ui/components/layout/header.tsx
🤖 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 `@web-ui/components/layout/header.tsx` around lines 75 - 80, The account menu
trigger currently uses a non-focusable <figure> inside DropdownMenuTrigger
asChild; replace the <figure className="cursor-pointer"> with a focusable
control such as <button type="button" className="cursor-pointer"
aria-label="Account menu"> and keep the <img ... /> inside so keyboard users can
focus and open the DropdownMenu; update any styling/classes as needed on the
button and ensure the img alt remains meaningful. Reference:
DropdownMenuTrigger, figure, img, DropdownMenu.
🤖 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.

Duplicate comments:
In `@web-ui/components/layout/header.tsx`:
- Around line 48-64: The header drawer still contains template promo branding
and external Shadcn links (CardTitle, CardDescription, Button asChild with Link
and LockIcon) that conflict with our career-ops branding; update the CardTitle
and CardDescription text to use career-ops phrasing, replace the outbound hrefs
(the Link components used inside the Buttons) with the correct career-ops URLs
or internal routes, and adjust the CTA labels (e.g., "Get Pro", "Learn More") to
match our product terminology so the drawer reflects our branding and points to
our domains instead of shadcnuikit.com.
- Around line 75-80: The account menu trigger currently uses a non-focusable
<figure> inside DropdownMenuTrigger asChild; replace the <figure
className="cursor-pointer"> with a focusable control such as <button
type="button" className="cursor-pointer" aria-label="Account menu"> and keep the
<img ... /> inside so keyboard users can focus and open the DropdownMenu; update
any styling/classes as needed on the button and ensure the img alt remains
meaningful. Reference: DropdownMenuTrigger, figure, img, DropdownMenu.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a744593-8f93-48f5-9cf3-357ccbb6966a

📥 Commits

Reviewing files that changed from the base of the PR and between 472522e and c98534c.

📒 Files selected for processing (5)
  • web-ui/components/layout/header.tsx
  • web-ui/components/layout/logo.tsx
  • web-ui/components/nav-secondary.tsx
  • web-ui/components/nav-user.tsx
  • web-ui/components/section-cards.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
web-ui/package.json (1)

41-41: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin radix-ui to a specific version instead of "latest".

Using "latest" makes builds non-reproducible and risks breaking changes. Pin to a tested semver range (e.g., "radix-ui": "^1.0.0") to ensure deterministic installs.

🔒 Proposed fix
-    "radix-ui": "latest",
+    "radix-ui": "^1.0.0",
🤖 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 `@web-ui/package.json` at line 41, The dependency "radix-ui" is pinned to
"latest" which makes installs non-deterministic; update the package.json
dependency entry for "radix-ui" to a tested semver version or range (for example
"^1.0.0" or the exact version you validated), then run the package manager to
update the lockfile and commit the updated package.json and lockfile; locate the
"radix-ui" entry in package.json to make this change and ensure CI passes with
the new pinned version.
web-ui/components/layout/header.tsx (1)

50-53: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace non-focusable <figure> with a keyboard-accessible trigger.

The <figure> element is not keyboard-accessible by default, preventing keyboard and screen-reader users from opening the account dropdown. Radix UI's DropdownMenuTrigger with asChild expects a focusable child element.

♿ Proposed accessibility fix
         <DropdownMenu>
           <DropdownMenuTrigger asChild>
-            <figure className="cursor-pointer">
-              <img src={`/images/avatars/1.png`} className="h-10 w-10" alt="..." />
-            </figure>
+            <button type="button" className="cursor-pointer border-0 bg-transparent p-0">
+              <img src={`/images/avatars/1.png`} className="h-10 w-10 rounded-full" alt="User account menu" />
+            </button>
           </DropdownMenuTrigger>
🤖 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 `@web-ui/components/layout/header.tsx` around lines 50 - 53, The account
dropdown trigger uses <DropdownMenuTrigger asChild> but wraps a non-focusable
<figure>, breaking keyboard access; replace the <figure> with a focusable
element such as a <button type="button"> (preserving the existing
className="cursor-pointer" and sizing classes) and add an accessible label
(e.g., aria-label="Open account menu") so DropdownMenuTrigger, asChild, and the
<img> inside become keyboard- and screen-reader-accessible; ensure you keep
visual styling (reset default button styles if needed) and remove the
non-focusable <figure>.
web-ui/lib/utils.ts (1)

16-22: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract inline parameter type into a named interface.

The inline object type should be replaced with a named interface per coding guidelines.

♻️ Proposed refactor
+interface GenerateMetaParams {
+  title: string;
+  description: string;
+}
+
-export function generateMeta({
-  title,
-  description,
-}: {
-  title: string;
-  description: string;
-}): Metadata {
+export function generateMeta({
+  title,
+  description,
+}: GenerateMetaParams): Metadata {
   return {

As per coding guidelines: "**/*.ts: Prefer interface for defining object shapes in TypeScript".

🤖 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 `@web-ui/lib/utils.ts` around lines 16 - 22, Replace the inline parameter type
in generateMeta with a named interface: define and export an interface (e.g.,
GenerateMetaParams) with title: string and description: string, then update the
generateMeta function signature to accept (params: GenerateMetaParams) or
destructure as ({ title, description }: GenerateMetaParams); ensure the new
interface name is used in the function declaration and any callers/imports are
updated accordingly.
🤖 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 `@web-ui/components/layout/header.tsx`:
- Line 52: Replace the non-descriptive alt text on the avatar image in
header.tsx (the <img src={`/images/avatars/1.png`} className="h-10 w-10"
alt="..." /> element) with a meaningful description such as "User avatar" or
"Account menu" (or interpolate the current user's name if available) so screen
readers convey the image purpose; update the alt attribute value accordingly in
the JSX for the avatar image.

In `@web-ui/components/layout/sidebar.tsx`:
- Around line 25-28: The Anchor element uses a redundant key expression
(key={item.title + item.href}) even though the parent already provides a unique
key; remove the concatenated key prop from the Anchor (or replace it with a
single stable identifier like item.href if a key is required) to simplify and
avoid duplication—locate the Anchor component instance and update its key usage
accordingly (referencing Anchor and item.title/item.href).

In `@web-ui/package.json`:
- Line 39: The package.json dependency "next": "16.1.0" is outdated; update the
Next.js version string for the Next dependency (the "next" entry in
package.json) to the latest stable release (e.g., "16.2.6"), then reinstall
dependencies (npm/yarn/pnpm install) and run the app/tests to verify
compatibility with React 19.2.3; ensure package-lock.json / yarn.lock is
committed after the update.

---

Duplicate comments:
In `@web-ui/components/layout/header.tsx`:
- Around line 50-53: The account dropdown trigger uses <DropdownMenuTrigger
asChild> but wraps a non-focusable <figure>, breaking keyboard access; replace
the <figure> with a focusable element such as a <button type="button">
(preserving the existing className="cursor-pointer" and sizing classes) and add
an accessible label (e.g., aria-label="Open account menu") so
DropdownMenuTrigger, asChild, and the <img> inside become keyboard- and
screen-reader-accessible; ensure you keep visual styling (reset default button
styles if needed) and remove the non-focusable <figure>.

In `@web-ui/lib/utils.ts`:
- Around line 16-22: Replace the inline parameter type in generateMeta with a
named interface: define and export an interface (e.g., GenerateMetaParams) with
title: string and description: string, then update the generateMeta function
signature to accept (params: GenerateMetaParams) or destructure as ({ title,
description }: GenerateMetaParams); ensure the new interface name is used in the
function declaration and any callers/imports are updated accordingly.

In `@web-ui/package.json`:
- Line 41: The dependency "radix-ui" is pinned to "latest" which makes installs
non-deterministic; update the package.json dependency entry for "radix-ui" to a
tested semver version or range (for example "^1.0.0" or the exact version you
validated), then run the package manager to update the lockfile and commit the
updated package.json and lockfile; locate the "radix-ui" entry in package.json
to make this change and ensure CI passes with the new pinned version.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6d6d4912-3e9a-4ec6-b1ee-e7bc79c0a7fb

📥 Commits

Reviewing files that changed from the base of the PR and between c98534c and aa48c49.

📒 Files selected for processing (5)
  • web-ui/README.md
  • web-ui/components/layout/header.tsx
  • web-ui/components/layout/sidebar.tsx
  • web-ui/lib/utils.ts
  • web-ui/package.json

<DropdownMenu>
<DropdownMenuTrigger asChild>
<figure className="cursor-pointer">
<img src={`/images/avatars/1.png`} className="h-10 w-10" alt="..." />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Provide a descriptive alt text for the avatar image.

The current alt text "..." is not descriptive and fails to convey the purpose of the image to screen reader users. Use a meaningful description like "User account" or "Account menu".

♿ Proposed fix
-              <img src={`/images/avatars/1.png`} className="h-10 w-10" alt="..." />
+              <img src={`/images/avatars/1.png`} className="h-10 w-10" alt="User account" />
📝 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
<img src={`/images/avatars/1.png`} className="h-10 w-10" alt="..." />
<img src={`/images/avatars/1.png`} className="h-10 w-10" alt="User account" />
🤖 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 `@web-ui/components/layout/header.tsx` at line 52, Replace the non-descriptive
alt text on the avatar image in header.tsx (the <img
src={`/images/avatars/1.png`} className="h-10 w-10" alt="..." /> element) with a
meaningful description such as "User avatar" or "Account menu" (or interpolate
the current user's name if available) so screen readers convey the image
purpose; update the alt attribute value accordingly in the JSX for the avatar
image.

Comment on lines +25 to +28
<Anchor
href={item.href}
key={item.title + item.href}
activeClassName="!bg-primary text-primary-foreground">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Simplify the key attribute.

The key prop on line 27 concatenates item.title + item.href, but the parent already provides a unique key prop at the same level. This creates a redundant key expression.

♻️ Optional simplification
     <Anchor
       href={item.href}
-      key={item.title + item.href}
       activeClassName="!bg-primary text-primary-foreground">
📝 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
<Anchor
href={item.href}
key={item.title + item.href}
activeClassName="!bg-primary text-primary-foreground">
<Anchor
href={item.href}
activeClassName="!bg-primary text-primary-foreground">
🤖 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 `@web-ui/components/layout/sidebar.tsx` around lines 25 - 28, The Anchor
element uses a redundant key expression (key={item.title + item.href}) even
though the parent already provides a unique key; remove the concatenated key
prop from the Anchor (or replace it with a single stable identifier like
item.href if a key is required) to simplify and avoid duplication—locate the
Anchor component instance and update its key usage accordingly (referencing
Anchor and item.title/item.href).

Comment thread web-ui/package.json
"js-yaml": "^4.1.0",
"lucide-react": "^0.379.0",
"marked": "^12.0.0",
"next": "16.1.0",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Next.js 16.1.0 exists and get latest versions
echo "=== Checking Next.js versions ==="
npm view next versions --json | jq -r '.[]' | grep "^16\." | tail -5

echo -e "\n=== Latest Next.js version ==="
npm view next version

echo -e "\n=== Checking compatibility with React 19.2.3 ==="
npm view next@16.1.0 peerDependencies

Repository: santifer/career-ops

Length of output: 527


Consider updating Next.js to the latest stable version.

Next.js 16.1.0 is a valid released version and is compatible with React 19.2.3 based on its peer dependencies (^19.0.0). However, version 16.1.0 is outdated—the current latest stable version is 16.2.6. Updating to the latest patch version ensures you have the latest bug fixes and security patches.

🤖 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 `@web-ui/package.json` at line 39, The package.json dependency "next": "16.1.0"
is outdated; update the Next.js version string for the Next dependency (the
"next" entry in package.json) to the latest stable release (e.g., "16.2.6"),
then reinstall dependencies (npm/yarn/pnpm install) and run the app/tests to
verify compatibility with React 19.2.3; ensure package-lock.json / yarn.lock is
committed after the update.

- server.mjs: POST /api/evaluate spawns `claude -p <url>` as a
  subprocess in the career-ops root; GET /api/evaluate/:id/stream
  serves SSE so the UI can tail output in real time
- app/dashboard/evaluate/page.tsx: client page with URL input,
  live log stream via EventSource, and "View in Tracker" CTA on done
- app/dashboard/page.tsx: add dashed quick-link to Evaluate page
- components/app-sidebar.tsx: add "Evaluate a Job" nav item
- components/site-header.tsx: add page title for /dashboard/evaluate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
web-ui/server.mjs (3)

177-180: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Block path traversal in /api/interview-file/:name.

req.params.name is joined directly into a filesystem path, so ../ traversal can escape interview-prep and read arbitrary files.

As per coding guidelines "**/*.mjs: Check for command injection, path traversal, and SSRF. Ensure scripts handle missing data/ directories gracefully."

🤖 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 `@web-ui/server.mjs` around lines 177 - 180, The GET handler for
'/api/interview-file/:name' joins req.params.name directly into a filesystem
path (using path.join with ROOT and 'interview-prep'), allowing path traversal;
fix by resolving and validating the target path before reading: compute const
base = path.resolve(ROOT, 'interview-prep'), compute const target =
path.resolve(base, req.params.name) and ensure target.startsWith(base +
path.sep) (or equals base) to reject any '../' escaping, return 400/404 if
validation fails, and handle missing directory/file checks gracefully before
calling fs.readFileSync; also consider restricting names to a safe basename
pattern (no path separators) as an additional safeguard.

14-15: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict CORS for write-capable endpoints.

Global app.use(cors()) allows any origin, while this server exposes unauthenticated mutating routes. That enables cross-origin writes from a malicious page opened in the same browser context.

As per coding guidelines "**/*.mjs: Check for command injection, path traversal, and SSRF. Ensure scripts handle missing data/ directories gracefully."

🤖 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 `@web-ui/server.mjs` around lines 14 - 15, Global app.use(cors()) opens CORS
for all origins; change it to restrict cross-origin access for mutating routes
by removing or replacing the global app.use(cors()) and instead apply CORS with
a strict origin/options object only on read-only routes or explicitly on safe
endpoints, and ensure write-capable handlers (e.g., any POST/PUT/DELETE route
handlers) do not use permissive CORS — apply cors(options) per-route or wrap
read routes with cors(options) while leaving mutating routes either
same-origin-only or protected behind CSRF/auth checks; update the usage around
app.use(cors()) and any route definitions that perform writes accordingly.

194-197: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape table-breaking characters before writing markdown rows.

status/notes/company/role/... are written raw into pipe-delimited markdown rows. | and newlines can corrupt applications.md and follow-ups.md structure.

As per coding guidelines "**/*.mjs: Check for command injection, path traversal, and SSRF. Ensure scripts handle missing data/ directories gracefully."

Also applies to: 210-211

🤖 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 `@web-ui/server.mjs` around lines 194 - 197, The markdown row writer is
inserting raw fields (status, notes, company, role, etc.) into the
pipe-delimited cols array which lets '|' and newlines break the table; sanitize
each field before assigning to cols by replacing pipe characters with '\|' and
removing or normalizing newline characters (e.g., replace '\n'/'\r' with a
single space) — implement a small helper (e.g., escapeTableCell) and use it
where cols[] is set (refer to cols, status, notes assignments and the code
around the return building the '| ' + cols.join(' | ') + ' |' string), and apply
the same sanitization at the other locations mentioned (around lines 210-211).
web-ui/components/app-sidebar.tsx (1)

50-53: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Use next/link for internal dashboard navigation.

Using <a href="/dashboard"> here triggers a full page load instead of client-side navigation.

Suggested patch
+import Link from "next/link"
...
-              <a href="/dashboard">
+              <Link href="/dashboard">
                 <div className="flex size-6 items-center justify-center rounded bg-foreground text-background text-xs font-bold">C</div>
                 <span className="text-base font-semibold">career-ops</span>
-              </a>
+              </Link>
🤖 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 `@web-ui/components/app-sidebar.tsx` around lines 50 - 53, Replace the plain
anchor with Next's client-side Link: import Link from 'next/link' at the top of
the component (app-sidebar.tsx) and change the anchor block (<a
href="/dashboard">...</a>) to use <Link href="/dashboard">...children...</Link>,
preserving the inner div and span and their classes so styling remains the same;
ensure the Link wraps the same content and remove the original <a> to enable
client-side navigation for the dashboard link.
🤖 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 `@web-ui/server.mjs`:
- Around line 186-188: The PATCH handler currently calls fs.readFileSync on the
computed path (const file = path.join(ROOT, 'data', 'applications.md')) without
checking existence, which causes an uncaught exception if the file is missing;
update the handler to first check for the file (e.g., fs.existsSync or fs.stat)
or wrap readFileSync in a try/catch, and if the file is absent return a
controlled API error (400/404 with JSON) or initialize an empty lines array
before proceeding, ensuring the rest of the logic that uses lines and updated
behaves correctly and logs the condition.
- Around line 228-251: The spawned Claude subprocess created via spawn('claude',
['-p', prompt, ...]) lacks protections: add input sanitization for prompt,
verify ROOT exists before spawning, enforce global/max concurrent job limit
(check and increment/decrement a counter or limit Map keyed by jobId), cap job
log buffer size inside push (truncate or drop old lines from job.lines and stop
writing to clients after a max line count), and implement a per-process runtime
timeout that sends a kill to child if exceeded and sets job.error; also watch
for excessive stderr spam and limit handling similarly, and ensure child is
killed and job.clients are closed in all error/timeout branches. Use the symbols
spawn, child, push, job, jobs, job.clients, and child.on('close') to locate and
make these changes.

---

Duplicate comments:
In `@web-ui/components/app-sidebar.tsx`:
- Around line 50-53: Replace the plain anchor with Next's client-side Link:
import Link from 'next/link' at the top of the component (app-sidebar.tsx) and
change the anchor block (<a href="/dashboard">...</a>) to use <Link
href="/dashboard">...children...</Link>, preserving the inner div and span and
their classes so styling remains the same; ensure the Link wraps the same
content and remove the original <a> to enable client-side navigation for the
dashboard link.

In `@web-ui/server.mjs`:
- Around line 177-180: The GET handler for '/api/interview-file/:name' joins
req.params.name directly into a filesystem path (using path.join with ROOT and
'interview-prep'), allowing path traversal; fix by resolving and validating the
target path before reading: compute const base = path.resolve(ROOT,
'interview-prep'), compute const target = path.resolve(base, req.params.name)
and ensure target.startsWith(base + path.sep) (or equals base) to reject any
'../' escaping, return 400/404 if validation fails, and handle missing
directory/file checks gracefully before calling fs.readFileSync; also consider
restricting names to a safe basename pattern (no path separators) as an
additional safeguard.
- Around line 14-15: Global app.use(cors()) opens CORS for all origins; change
it to restrict cross-origin access for mutating routes by removing or replacing
the global app.use(cors()) and instead apply CORS with a strict origin/options
object only on read-only routes or explicitly on safe endpoints, and ensure
write-capable handlers (e.g., any POST/PUT/DELETE route handlers) do not use
permissive CORS — apply cors(options) per-route or wrap read routes with
cors(options) while leaving mutating routes either same-origin-only or protected
behind CSRF/auth checks; update the usage around app.use(cors()) and any route
definitions that perform writes accordingly.
- Around line 194-197: The markdown row writer is inserting raw fields (status,
notes, company, role, etc.) into the pipe-delimited cols array which lets '|'
and newlines break the table; sanitize each field before assigning to cols by
replacing pipe characters with '\|' and removing or normalizing newline
characters (e.g., replace '\n'/'\r' with a single space) — implement a small
helper (e.g., escapeTableCell) and use it where cols[] is set (refer to cols,
status, notes assignments and the code around the return building the '| ' +
cols.join(' | ') + ' |' string), and apply the same sanitization at the other
locations mentioned (around lines 210-211).
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 63548c4a-132e-4c50-b011-1a265b3a545a

📥 Commits

Reviewing files that changed from the base of the PR and between aa48c49 and fd7a05d.

📒 Files selected for processing (5)
  • web-ui/app/dashboard/evaluate/page.tsx
  • web-ui/app/dashboard/page.tsx
  • web-ui/components/app-sidebar.tsx
  • web-ui/components/site-header.tsx
  • web-ui/server.mjs

Comment thread web-ui/server.mjs
Comment thread web-ui/server.mjs Outdated
Comment on lines +228 to +251
const child = spawn('claude', ['-p', prompt, '--output-format', 'text', '--dangerously-skip-permissions'], {
cwd: ROOT,
env: { ...process.env },
})

const push = (line) => {
job.lines.push(line)
for (const client of job.clients) {
client.write(`data: ${JSON.stringify({ line })}\n\n`)
}
}

child.stdout.on('data', d => d.toString().split('\n').filter(Boolean).forEach(push))
child.stderr.on('data', d => d.toString().split('\n').filter(Boolean).forEach(l => push(`⚠ ${l}`)))

child.on('close', (code) => {
job.done = true
job.error = code !== 0 ? `Process exited with code ${code}` : null
const msg = job.error ? `data: ${JSON.stringify({ done: true, error: job.error })}\n\n`
: `data: ${JSON.stringify({ done: true })}\n\n`
for (const client of job.clients) { client.write(msg); client.end() }
job.clients.clear()
setTimeout(() => jobs.delete(jobId), 10 * 60 * 1000)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add runtime and memory guards for spawned evaluation jobs.

claude subprocesses have no timeout, job count limit, or log-buffer cap. A hung/noisy process can cause unbounded memory growth and resource exhaustion.

As per coding guidelines "**/*.mjs: Check for command injection, path traversal, and SSRF. Ensure scripts handle missing data/ directories gracefully."

🤖 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 `@web-ui/server.mjs` around lines 228 - 251, The spawned Claude subprocess
created via spawn('claude', ['-p', prompt, ...]) lacks protections: add input
sanitization for prompt, verify ROOT exists before spawning, enforce global/max
concurrent job limit (check and increment/decrement a counter or limit Map keyed
by jobId), cap job log buffer size inside push (truncate or drop old lines from
job.lines and stop writing to clients after a max line count), and implement a
per-process runtime timeout that sends a kill to child if exceeded and sets
job.error; also watch for excessive stderr spam and limit handling similarly,
and ensure child is killed and job.clients are closed in all error/timeout
branches. Use the symbols spawn, child, push, job, jobs, job.clients, and
child.on('close') to locate and make these changes.

gauravlochab and others added 2 commits May 17, 2026 18:15
- Pass prompt via stdin instead of as a positional arg — eliminates
  the "no stdin data received" warning
- Prepend HEADLESS MODE instruction so career-ops uses WebFetch for
  verification instead of launching Playwright (no browser popups)
- Strip ANSI escape codes from stdout/stderr before sending to UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion mismatch

Radix UI generates random IDs (e.g. radix-_R_...) for DropdownMenuTrigger
and other primitives. When rendered in a server component the server and
client produce different IDs, causing a React hydration mismatch warning.
Marking the layout as a client component ensures IDs are only generated
once, on the client.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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 `@web-ui/server.mjs`:
- Around line 98-103: The parseFollowUps() function currently creates
follow-ups.md when missing; instead remove the fs.writeFileSync call and make
parseFollowUps() return an empty array (or throw a controlled error) when the
file doesn't exist; also check that the data directory exists (the path
constructed by path.join(ROOT, 'data', 'follow-ups.md') and fs.existsSync for
ROOT/data) and if the directory itself is missing return the same empty
result/error so no read path mutates the filesystem—leave file creation to
explicit user actions.
- Around line 51-55: The function enrichFromReport uses app.reportPath directly
with path.join(ROOT, app.reportPath) allowing path traversal; validate and
constrain app.reportPath to the reports directory before reading. Resolve the
final path (e.g., using path.resolve or equivalent) and compute an allowed base
like path.resolve(ROOT, 'reports'); reject reportPath values that are absolute
or that resolve to a path outside the allowed base (check that
resolvedPath.startsWith(allowedBase + path.sep) or equals allowedBase) and
return app without reading if invalid; keep the rest of enrichFromReport
(reportFile, fs.existsSync, fs.readFileSync) unchanged when the path is
validated.
- Around line 220-230: The current loose check using /^https?:\/\/.+/ allows
newline/extra-prompt injection and SSRF; before creating job/randomUUID and
before calling spawn('claude', ...) you must strictly validate and sanitize the
incoming url: trim it, construct new URL(url) and verify round-trip equality
(sanitizedUrl === url || compare normalized forms), ensure protocol is http or
https, reject if hostname is localhost, 127.0.0.1, ::1 or in private CIDR ranges
(10.*, 172.16-31.*, 192.168.*) or resolves to those addresses, and reject URLs
with embedded credentials or control/newline characters; only after these checks
create the job object and build the Claude prompt using the sanitizedUrl
variable so no injected text can reach the spawned child.
- Around line 230-255: The current spawn call for Claude assigns to child but
lacks an 'error' listener, so spawn failures will crash the process and leave
job entries in jobs; add a child.on('error', err => { set job.done = true; set
job.error to a descriptive message including err.message; notify all job.clients
with the same SSE-style `data` payload used in the 'close' handler (done: true
and error), end each client, clear job.clients, remove the job from jobs
(setTimeout as in close or immediate), and log the error }); ensure this handler
is attached immediately after creating the child (before writing to stdin) so
missing binary/startup errors are handled gracefully.
- Around line 183-212: The PR adds two mutation routes that modify data
files—app.patch('/api/applications/:num') and app.post('/api/followups')—which
violates the read-only contract; remove or disable these handlers so no
fs.writeFileSync/fs.appendFileSync or parseFollowUps-based mutations happen in
this PR. Replace the handlers with a 405/403 JSON response (or delete them
entirely) and add a comment referencing that any web-editing behavior must be
introduced only with an explicit data-contract change; if edits are intended,
instead gate them behind an explicit FEATURE_FLAG or admin-only check and
document the contract change.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 08230fe9-b1b9-44e5-805f-fa7a8f1ff69e

📥 Commits

Reviewing files that changed from the base of the PR and between fd7a05d and ed36f3a.

📒 Files selected for processing (2)
  • web-ui/app/dashboard/layout.tsx
  • web-ui/server.mjs

Comment thread web-ui/server.mjs
Comment on lines +51 to +55
function enrichFromReport(app) {
if (!app.reportPath) return app
const reportFile = path.join(ROOT, app.reportPath)
if (!fs.existsSync(reportFile)) return app
const head = fs.readFileSync(reportFile, 'utf8').slice(0, 2000)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Constrain reportPath to reports/ before reading it.

reportPath comes from markdown data and is joined directly into a filesystem path. A value like ../../config/profile.yml or an absolute path will escape the reports directory and make /api/applications read arbitrary local files.

🤖 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 `@web-ui/server.mjs` around lines 51 - 55, The function enrichFromReport uses
app.reportPath directly with path.join(ROOT, app.reportPath) allowing path
traversal; validate and constrain app.reportPath to the reports directory before
reading. Resolve the final path (e.g., using path.resolve or equivalent) and
compute an allowed base like path.resolve(ROOT, 'reports'); reject reportPath
values that are absolute or that resolve to a path outside the allowed base
(check that resolvedPath.startsWith(allowedBase + path.sep) or equals
allowedBase) and return app without reading if invalid; keep the rest of
enrichFromReport (reportFile, fs.existsSync, fs.readFileSync) unchanged when the
path is validated.

Comment thread web-ui/server.mjs
Comment on lines +98 to +103
function parseFollowUps() {
const file = path.join(ROOT, 'data', 'follow-ups.md')
if (!fs.existsSync(file)) {
fs.writeFileSync(file, '# Follow-ups\n\n| # | Company | Role | Applied Date | Last Contact | Next Action | Due Date | Notes |\n|---|---------|------|--------------|--------------|-------------|----------|-------|\n')
return []
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't create follow-ups.md from a read path.

A simple GET of /api/followups mutates data/follow-ups.md when the file is missing, which breaks the read-only contract for the dashboard. It also still throws if data/ itself is absent. Return an empty result or a controlled error here, and leave file creation to an explicit user action. Based on learnings Data contract: User Layer (NEVER auto-updated) includes cv.md, config/profile.yml, modes/_profile.md, article-digest.md, portals.yml, data/*, reports/*, output/*, interview-prep/*.

🤖 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 `@web-ui/server.mjs` around lines 98 - 103, The parseFollowUps() function
currently creates follow-ups.md when missing; instead remove the
fs.writeFileSync call and make parseFollowUps() return an empty array (or throw
a controlled error) when the file doesn't exist; also check that the data
directory exists (the path constructed by path.join(ROOT, 'data',
'follow-ups.md') and fs.existsSync for ROOT/data) and if the directory itself is
missing return the same empty result/error so no read path mutates the
filesystem—leave file creation to explicit user actions.

Comment thread web-ui/server.mjs
Comment thread web-ui/server.mjs
Comment on lines +220 to +230
const { url } = req.body
if (!url || !/^https?:\/\/.+/.test(url)) return res.status(400).json({ error: 'Invalid URL' })

const jobId = randomUUID()
const job = { lines: [], done: false, error: null, clients: new Set() }
jobs.set(jobId, job)

// Headless mode: skip Playwright (no browser), use WebFetch fallback for verification.
// Prompt is piped via stdin so claude -p doesn't wait for stdin data.
const prompt = `HEADLESS MODE: You are running as a background worker spawned from the web UI. Playwright/browser is NOT available — use WebFetch for job posting verification and mark the report header with **Verification:** unconfirmed (batch mode). Do NOT open any browser windows or attempt interactive login.\n\nEvaluate this job posting: ${url}`
const child = spawn('claude', ['-p', '--output-format', 'text', '--dangerously-skip-permissions'], {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Harden url validation before building the Claude prompt.

/^https?:\/\/.+/ only checks a prefix, so inputs like https://example.com\nIgnore previous instructions... still pass and inject extra prompt text. It also accepts loopback/private-network URLs, which turns the WebFetch step into an SSRF primitive. Parse with new URL(url), require an exact round-trip, and reject localhost/private destinations before spawning the worker. As per coding guidelines **/*.mjs: Check for command injection, path traversal, and SSRF. Ensure scripts handle missing data/ directories gracefully.`

🤖 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 `@web-ui/server.mjs` around lines 220 - 230, The current loose check using
/^https?:\/\/.+/ allows newline/extra-prompt injection and SSRF; before creating
job/randomUUID and before calling spawn('claude', ...) you must strictly
validate and sanitize the incoming url: trim it, construct new URL(url) and
verify round-trip equality (sanitizedUrl === url || compare normalized forms),
ensure protocol is http or https, reject if hostname is localhost, 127.0.0.1,
::1 or in private CIDR ranges (10.*, 172.16-31.*, 192.168.*) or resolves to
those addresses, and reject URLs with embedded credentials or control/newline
characters; only after these checks create the job object and build the Claude
prompt using the sanitizedUrl variable so no injected text can reach the spawned
child.

Comment thread web-ui/server.mjs
Comment on lines +230 to +255
const child = spawn('claude', ['-p', '--output-format', 'text', '--dangerously-skip-permissions'], {
cwd: ROOT,
env: { ...process.env },
})
child.stdin.write(prompt)
child.stdin.end()

const push = (line) => {
job.lines.push(line)
for (const client of job.clients) {
client.write(`data: ${JSON.stringify({ line })}\n\n`)
}
}

const stripAnsi = s => s.replace(/\x1B\[[0-9;]*m/g, '')
child.stdout.on('data', d => d.toString().split('\n').filter(Boolean).forEach(l => push(stripAnsi(l))))
child.stderr.on('data', d => d.toString().split('\n').filter(Boolean).forEach(l => push(`⚠ ${stripAnsi(l)}`)))

child.on('close', (code) => {
job.done = true
job.error = code !== 0 ? `Process exited with code ${code}` : null
const msg = job.error ? `data: ${JSON.stringify({ done: true, error: job.error })}\n\n`
: `data: ${JSON.stringify({ done: true })}\n\n`
for (const client of job.clients) { client.write(msg); client.end() }
job.clients.clear()
setTimeout(() => jobs.delete(jobId), 10 * 60 * 1000)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle spawn failures explicitly.

If claude is missing or the process cannot start, ChildProcess emits error, not close. Without an error handler this can crash the API process and leave the job stuck in jobs forever.

🤖 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 `@web-ui/server.mjs` around lines 230 - 255, The current spawn call for Claude
assigns to child but lacks an 'error' listener, so spawn failures will crash the
process and leave job entries in jobs; add a child.on('error', err => { set
job.done = true; set job.error to a descriptive message including err.message;
notify all job.clients with the same SSE-style `data` payload used in the
'close' handler (done: true and error), end each client, clear job.clients,
remove the job from jobs (setTimeout as in close or immediate), and log the
error }); ensure this handler is attached immediately after creating the child
(before writing to stdin) so missing binary/startup errors are handled
gracefully.

gauravlochab and others added 7 commits May 17, 2026 19:50
- Dashboard: days-in-search counter, "Your next actions" derived section
  with urgency colouring, recommendation text on Best-to-act-on cards,
  days-ago display in Recent Activity
- Evaluate: named stage pills (Fetching → Analyzing → Scoring → Report →
  Tracker), summary card on completion with score and fit verdict,
  prefill URL from ?url= query param (Pipeline Evaluate button)
- Pipeline: Evaluate + Skip action buttons per card, PATCH /api/pipeline
  endpoint to toggle done/undone in pipeline.md
- Tracker: inline status dropdown (writes to applications.md on change),
  report drawer with full markdown + quick facts strip, report click
  target on "Report #N" link
- Analytics: conversion funnel with conversion rates, weekly cadence
  sparkline (last 8 weeks), actionable insight banner
- Follow-ups: "+ Add follow-up" form, "+ Track" button on suggestions
  (auto-sets due date 7d out), overdue/due-soon counts in subtitle
- Interview Prep: "Practice random question" mode with STAR+R framework,
  interactive company file viewer, active-interview banner linking to
  tracker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Drop `import { icons } from 'lucide-react'` barrel import in search.tsx
  and sidebar.tsx — was pulling all ~1400 icons into every page bundle (30MB pkg)
- Move @faker-js/faker (16MB), recharts (5.2MB), @dnd-kit/* (2MB),
  @MDXEditor, @tanstack/react-table, embla-carousel-react, axios,
  date-fns, react-day-picker, input-otp to devDependencies — none are
  used in the dashboard; they were bundled into production unnecessarily
- Switch all lib/api.ts fetches from cache:"no-store" to next.revalidate
  (10s for app data, 30s for story bank/files, 60s for profile) — prevents
  re-parsing markdown files on every single page request
- Remove "use client" from app/dashboard/layout.tsx — layout is a pure
  wrapper; marking it client-side was forcing all dashboard children out
  of server rendering. AppSidebar/SiteHeader remain "use client" as needed
- Wrap useSearchParams() in <Suspense> in evaluate/page.tsx — fixes the
  prerender error that caused the build to fail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t controls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLI mode (modes/recruiter-find.md): generates ≤300-char LinkedIn
connection notes, follow-up messages, and search queries from a
recruiter profile URL, job URL, or inbound DM. Supports 3 scenarios:
A (profile forwarded), B (job found / find recruiter), C (DM reply).

Web UI changes:
- New /dashboard/recruiter-find page with scenario tabs, SSE streaming,
  structured success card with copy buttons and Log as Sent form
- Fix follow-ups schema: 9-column format (type, appNumber, channel,
  contact, dateSent) with backward compat for legacy rows
- Update followups/page.tsx and followup-client.tsx to new schema
- Add Recruiter Find nav item to sidebar
- Add rfJobs endpoints to server.mjs (POST /api/recruiter-find,
  GET /api/recruiter-find/:jobId/stream)
- Add RecruiterScenario type, SCENARIO_LABELS, ARCHETYPE_COLORS to api.ts
- Update AGENTS.md dispatch table with recruiter-find trigger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- web-ui/README.md: add all 8 pages, key features section, full API endpoint table
- README.md: add Web UI to features table and project structure tree
- docs/SETUP.md: add Web UI Dashboard optional setup section
- docs/ARCHITECTURE.md: add web-ui architecture section with Express/Next.js details

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… follow-up, not list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug fixes:
- B1a: ReportDrawer uses useEffect; shows loading/error states
- B1b: Evaluate page auto-marks pipeline item done on success
- B1c: Pipeline done-items now show Restore button

Interactions:
- Add URL form on Pipeline page (POST /api/pipeline)
- DeleteFollowUpButton on follow-ups table (DELETE /api/followups/:num)
- Sort follow-ups by days/date/company via URL param
- Analytics funnel labels link to tracker tab
- 5 new sidebar nav items

New server routes (server.mjs):
- POST /api/pipeline, DELETE /api/followups/:num
- POST /api/scan + GET /api/scan/:jobId/stream
- GET /api/patterns
- POST /api/batch
- POST /api/pdf/:num + GET /api/pdf/:jobId/stream

New pages (all pass pnpm build):
- /dashboard/scanner — trigger scan.mjs with SSE streaming + stage pills
- /dashboard/batch — multi-select pipeline items, parallel evaluate
- /dashboard/insights — fetch /api/patterns, display funnel/blockers/recs
- /dashboard/pdf — list no-PDF apps, streaming PDF generation per row
- /dashboard/compare — two-dropdown side-by-side report comparison

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Feature table: 8 pages → 13 pages
- File listing: update page count and add scanner/batch/insights/pdf/compare
- web-ui/README.md: add 5 new page entries in routes table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant