Features - Fighter90/career-ops-ui GitHub Wiki

Features

Every view in the SPA (public/js/views/*.js), what it does, and the API routes backing it. Routes live in server/lib/routes/<module>.mjs β€” see API Reference for the full contracts.

Core pipeline

#/ β€” Dashboard

Command center: application counts, status histogram, average score, recent applications/pipeline URLs, last report, quick-action tiles. Backed by GET /api/dashboard and GET /api/health.

#/scan β€” Portal scanner

Zero-token, in-process scanning of 94 sources (89 EN + 5 RU). Source dropdown built dynamically from GET /api/scan/sources; results stream over GET /api/stream/scan?source=ats|regional|both (SSE, multi-phase done events); results table from GET /api/scan-results; RU config via GET /api/scan/regional/config. Supports dryRun=1 and per-company narrowing. A β—Ž fit badge marks postings matching your two-pager. v1.129.1: a Seniority filter facet (lead/staff/senior/mid/junior/intern, auto-populated from results like the Country facet, via job-facets.js), a Seniority badge column, and a zero-token Age freshness column (today/Nd).

#/pipeline β€” URL inbox

Pending JD URLs from data/pipeline.md with a split-pane preview. GET/POST/DELETE /api/pipeline (URL gated by isValidJobUrl, file-locked) + the SSRF-safe GET /api/pipeline/preview?url=. The server-side auto-pipeline (evaluate β†’ report β†’ tracker) streams via the auto-pipeline module's SSE endpoint.

#/evaluate β€” Offer evaluation

Paste a JD β†’ 0.0–5.0 evaluation across five dimensions plus a holistic global score. POST /api/evaluate (live with a key, manual prompt otherwise; save:true persists the JD under jds/). Smoke tests: POST /api/evaluate/test-gemini / test-anthropic.

#/tracker β€” Application tracker

Parses data/applications.md. GET /api/tracker (optional ?page=&pageSize=&status= pagination + whole-history funnel), POST /api/tracker (dedup by company+role). v1.118.0: canonical Hired status with a celebratory badge and a πŸŽ‰ job-landed banner; funnel and conversion charts count Hired as advanced through every stage. v1.129.1 (parent web/ port): the status vocabulary is no longer hardcoded β€” server/lib/states.mjs reads templates/states.yml live (with a CI fallback), so POST folds any label/id/alias (Spanish/legacy, e.g. contratadaβ†’Hired) to its canonical label and the funnel buckets by canonical status; score cells use a finer 4-tier tone (public/js/lib/score-tone.js). Company logos on ATS-hosted rows resolve via a nameβ†’domain map (company-logo.js domainFromName()). v1.131.0 (parent web/ /pipeline port): the funnel-chip bar + status dropdown become a CRM stage-tab strip β€” an All tab plus one tab per canonical status (Evaluated Β· Applied Β· Responded Β· Interview Β· Offer Β· Rejected Β· Discarded Β· SKIP Β· Hired), each with a live count including zero-count stages so the full funnel is always visible; the active tab drives the filter. The tab set comes from the new read-only GET /api/tracker/stages (canonical labels in order + an alias-fold map, from server/lib/states.mjs β€” the client never hardcodes the whitelist), bucketed by the pure public/js/lib/tracker-stages.js (window.TrackerStages); tabs are accessible (role tablist/tab, counts in the accessible name) and rows show a brand logo when logos are enabled.

"Still live?" classification tiers (v1.237.1). server/lib/liveness-core.mjs decides in a fixed precedence, and the order is the whole design β€” not an implementation detail:

http_gone β†’ bot_challenge β†’ access_blocked β†’ server_error β†’ expired_url β†’ expired_body (HARD) β†’ redirected_off_posting β†’ apply_control_visible β†’ expired_body_soft (SOFT) β†’ listing_page β†’ insufficient_content β†’ no_apply_control

  • HARD patterns beat a visible Apply button, so only unambiguous copy belongs there β€” e.g. /this (?:job|role|position)(?: listing)? is closed\b(?!-)/i. The \b(?!-) is a compound-adjective guard: \b alone rejects closedown but still matches real prose like "This role is closed-loop control of the platform".
  • SOFT patterns lose to a visible Apply button. /\bjob expired\b/i lives here because the classifier receives the whole page's text, so it false-fires on a "Similar jobs" carousel entry, a "Hide job expired" filter chip or a footer FAQ. A bare "JOB EXPIRED" banner with no apply control still expires, via expired_body_soft.
  • Why the asymmetry: a false expired is written to scan history as skipped_expired and then dedup-filters that real job out of every later scan, indefinitely, unless scan_history.recheck_after_days is set. A false "still live" costs one wasted re-check. Put a pattern in the HARD tier only when it cannot appear as page chrome on a live posting.

#/reports β€” Evaluation reports

Lists and renders reports/*.md. GET /api/reports, GET /api/reports/:slug.

#/batch β€” Batch evaluate

Parallel batch evaluations via the batch route module.

#/deep β€” Deep company research

POST /api/deep { company, role?, run? } β€” live via the provider cascade (persists to interview-prep/), or a manual prompt.

#/apply, #/cover, #/contacto, #/training, #/project, #/patterns, #/followup β€” Mode pages

Generic surfaces over the parent's slash-command modes via POST /api/mode/:slug (allowlist: batch, contacto, followup, interview-prep, patterns, project, training). The apply checklist includes a knock-out pre-scan (visa/degree/salary disqualifiers). #/followup additionally opens with a cadence board β€” per-application urgency (πŸ”΄/🟠/🟑/πŸ”΅) from GET /api/followup (relays the parent's followup-cadence.mjs) and a Seed button (POST /api/followup/seed).

CV & documents

#/cv β€” CV editor

GET /api/cv, PUT /api/cv (sanitized via stripDangerousMarkdown, 1 MB cap).

#/cv-studio β€” CV Studio

Four AI/deterministic tools: Humanize (POST /api/cv-studio/humanize β€” voice-match rewrite), Tailor (POST /api/cv-studio/tailor β€” JD-tailored CV + cover letter behind a recruiter-checklist gate), Add to CV (POST /api/cv-studio/add-entry β€” URL or pasted text β†’ ATS bullets, suggestions only, SSRF-guarded), plus two pure client libs: cv-diagnostics.js (deterministic rΓ©sumΓ© score) and cv-privacy.js (in-browser PII masking). No server writes.

PDF / DOCX export

GET /api/stream/pdf (spawns the parent's generate-pdf.mjs, requires Playwright), GET /api/output/pdfs + download, and POST /api/export/docx (dependency-free server/lib/docx.mjs, stateless).

Interview & career toolkit

#/interview-prep

Company briefs in interview-prep/*.md: GET/DELETE /api/interview-prep* + generation via POST /api/mode/interview-prep.

#/mock-interview

Live mock interview (v1.92.0): turn/save/sessions endpoints in the interview route module.

#/networking

Networking planner (v1.91.0) β€” writes user-layer networking/net-*.md.

#/career-plan

GET/PUT /api/career-plan (user-layer config/career-plan.md) + POST /api/career-plan/generate (AI plan from CV+profile+two-pager+memory, horizon + focus).

#/orientation

POST /api/orientation/generate β€” career-orientation profile (archetype vectors, roles, strengths, working style); reflection-not-test framing, no writes.

#/two-pager

Candidate two-pager: GET/PUT /api/two-pager (config/two-pager.yml) + POST /api/two-pager/draft ({run:true} auto-fills the form via the live cascade). Inlined into eval prompts; powers the scan β—Ž badge.

#/memory

About-me note: GET/PUT /api/memory + suggest β€” writes config/memory.md, inlined into bundleProjectContext so it reaches every AI request.

Statistics & observability

#/stats β€” Statistics (five tabs)

  1. AI market report β€” POST /api/stats/market (salary/market report by target roles + region + currency).
  2. My pipeline β€” own-pipeline analytics from the tracker.
  3. Target-role trend β€” POST /api/stats/snapshot (append data/role-stats.jsonl) + GET /api/stats/trend.
  4. Rejection patterns β€” GET /api/stats/patterns (read-only relay of the parent's analyze-patterns.mjs, incl. per-ATS-vendor advance rate).
  5. Lifetime (v1.118.0) β€” GET /api/stats/lifetime (parent stats.mjs: lifetime funnel, scanner totals, portal coverage) + GET /api/stats/salary-gap (salary-gap.mjs: desired vs advertised vs actual compensation). Zero-token shell-outs, fail-soft {available:false} without the parent.

Markdown/PDF/DOCX export via the shared report-export.js (also used by career-plan, orientation, two-pager).

#/usage + sidebar usage HUD

GET /api/usage rolls up data/llm-usage.jsonl into per-provider token totals + estimated USD over 24h/7d/30d/all. The usage-hud.js widget pins a live-refreshing <tokens> Β· <cost> meter to the sidebar bottom.

#/activity

GET /api/activity?limit=&type= β€” the append-only journal of every state-changing request (secrets redacted).

#/health

GET /api/health β€” liveness + setup readiness (Node version, parent resolution, keys, Playwright) + GET /api/status/providers (LLM readiness, no secrets).

Configuration & help

#/config β€” App Settings

Parent .env round-trip: GET/POST /api/config (only KNOWN_KEYS, secrets masked). Includes the AI CLI tools tab (GET /api/cli-detect β€” read-only PATH scan for Claude Code/Cursor/Codex/Gemini/OpenCode/Copilot/Qwen/Antigravity/Grok Build/Kimi β€” 10 tools, v1.127.0 re-added Cursor per parent #2115; never executes a found binary) and the OpenRouter model dropdown (GET /api/openrouter/models).

#/portals

Portal config viewer (GET /api/portals) + POST /api/portals/health β€” probes each tracked company's careers_url via SSRF-safe safeGet to flag dead ATS slugs (read-only).

#/docs-assistant + floating Ask-the-docs button

POST /api/docs-assistant/ask β€” answers how-to questions grounded ONLY in the in-app help guide (docs/help/<lang>.md); keyword retrieval picks top sections; the model answers from them or says the guide doesn't cover it. Never reads CV/profile/tracker. The docs-fab.js launcher floats on every page.

Help (#/help)

GET /api/help/:lang β€” the full in-app guide, 31 H2 / 119 H3 sections, translated into all 17 locales.

Extras

  • In-app bug reporter (bug-report.js) β€” privacy-floored diagnostic snapshot β†’ preview β†’ pre-filled GitHub issue. No server route.
  • Company logos β€” GET /api/logo?domain= proxies a company's favicon from its own domain (SSRF-safe, size-capped, LRU-cached, opt-in).
  • Runners β€” POST /api/run/{doctor,verify,normalize,dedup,merge,sync-check,reconcile} buffered shell-outs to parent maintenance scripts; GET /api/stream/{scan,liveness,pdf} streaming ones.
⚠️ **GitHub.com Fallback** ⚠️