[1.7.0] — 2026-05-03
A 28-commit hardening + UX + feature-completion pass driven by QA r5. Three security layers landed (XSS sanitization, CSP, input validation), every missing CRUD endpoint was filled in, the parent-project bootstrap is now fully automated, and the UI gained 9 new pages — Activity, redesigned Deep Research, plus 7 sidebar-grouped modes (project / training / followup / batch / outreach / interview-prep / patterns) covering 100% of parent's modes/. Test coverage went from 73 to 201, across 23 test files, plus 23 comprehensive Playwright e2e steps.
🔒 Security
fix(cv): sanitize CV markdown to block stored XSS in preview(FIX-C10) —PUT /api/cvnow strips<script>,<iframe>,<object>,<embed>,<style>,<form>,<svg>,on*=event handlers, andjavascript:/vbscript:/data:text/htmlURIs before writingcv.md. Body capped at 1 MB (413 on overflow). Client-sideUI.md()was rewritten to escape every byte before any markdown transformation runs, so raw HTML can never reachinnerHTML. Linkhrefattributes are validated against an allowlist of safe schemes (http/https/mailto/tel/relative +data:imageonly). 17 new tests across the strip helper and HTTP round-trips.fix(server): add CSP and baseline security headers(FIX-L2) — every response now carriesX-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: same-origin. When the server binds beyond loopback (HOST≠127.0.0.1/::1/localhost), a strictContent-Security-Policyis layered on top:default-src 'self',script-src 'self'(nounsafe-inline), Google Fonts whitelisted,connect-src 'self'blocks XSS exfiltration. Inlineonclickhandlers inindex.htmlandrouter.jswere moved toaddEventListenerto keep the strict CSP intact. 8 new tests gating CSP across 5 differentHOSTvalues.fix(api): tighten pipeline URL validator(FIX-M7) —POST /api/pipelineused to accept"not-a-url"and persist it. NowisValidJobUrl()rejects bare strings, inputs <10 or >2000 chars, whitespace-containing URLs, non-http(s)schemes, and loopback hostnames (localhost/127.0.0.1/::1). Folds in FIX-M3 + FIX-M6 (return 400 on invalid, plus adedupedflag on success).fix(server): actually load .env so HH_USER_AGENT / GEMINI_API_KEY hints work— previously the runtime told users to "set HH_USER_AGENT in .env" but the server never read that file, so following the instruction did nothing. Adds a 35-line zero-dependency dotenv loader (server/lib/dotenv.mjs) wired in at the top ofserver/index.mjs. Process-env values set on the command line still win, so existing CI overrides aren't shadowed. Parent's.env.examplenow includes a documentedHH_USER_AGENTblock with a real-Chrome User-Agent example. 6 new tests.fix(api): sanitize JD before prompt assembly(FIX-M5) —POST /api/evaluatestrips ANSI escapes, control bytes, inline<script>tags, and trims whitespace before either calling Gemini or echoing the prompt back. 50 KB length cap. The 50-char minimum runs against the sanitized text, so prompt-injection attempts that look long enough but consist mostly of escapes fail-fast with 400.fix(health): mask Node version + project root when HOST!=loopback(FIX-M1) —/api/healthno longer fingerprints the host on LAN-exposed deployments. Loopback responses keep the values for local diagnostics.
✨ New features
feat: 7 new sidebar modes + grouped sidebar(FIX-C8) — covers 100% of the parent'smodes/directory with no UI gaps. New routes:#/project(portfolio project advisor),#/training(course / cert evaluation),#/followup(per-application cadence),#/batch(parallel URL processor),#/contacto(LinkedIn outreach drafter),#/interview-prep(stage-specific prep),#/patterns(rejection-pattern analyzer). All seven share a single config-driven view factory (public/js/views/mode-page.js) and a single generic endpointPOST /api/mode/:slug— adding a new mode in the future is one config row + one i18n block. Sidebar reorganized into 6 groups: Sourcing / Decision / Application / Networking / Analytics / Setup. 18 nav items total. 12 new tests intests/modes-endpoints.test.mjs.fix: bootstrap parent deps + russian_portals defaults(FIX-C4 + C9 + C12 + H2) —bin/start.shnow installs the parent'snode_modules(js-yaml, playwright, jsdom) ANDnpx playwright install chromiumon fresh clones, so/api/stream/scan,/pdf, and/livenesswork end-to-end out of the box.createApp()probesportals.ymlon every boot — if therussian_portals:block is missing, appends a documented default with comments. Idempotent: the second boot is a no-op. 3 new tests.fix: disable 9 dead portal slugs in template + health-check script(FIX-C3) —templates/portals.example.ymlnow ships with Ada / Factorial / Tinybird / Weights & Biases / Travelperk / Clarity AI / Forto / Vinted / Runway flaggedenabled: false(each entry has an inline reason comment). New installs scan 87 alive companies instead of 96. Newweb-ui/scripts/portals-health-check.mjsHEAD-probes every enabledcareers_urland reports DEAD entries with a suggested patch list (JSON output via--json). 3 new tests.feat(activity): user-action log + Activity sidebar page— every state-changing API request is captured todata/activity.jsonl(timestamp, action verb, target, success flag, optional detail). New sidebar entry Activity with action-prefix chip filters (pipeline / cv / jd / evaluate / scan / stream / script), action ✓/✗ badges, and refresh button. Auto-rotates at 5 MB. 10 new tests covering middleware, read filters, corrupt-line tolerance, and the recursion guard forGET /api/activityitself.feat(deep): view Deep Research in browser + saved-results archive— the Deep Research page now (a) runs the prompt through Gemini live when{ run: true }andGEMINI_API_KEYis set, persisting output tointerview-prep/{slug}.md; (b) lists every saved deep-research file as clickable cards with relative timestamps; (c) renders results as Markdown with 📋 Copy / ⬇ Download .md / ↗ Open in tab actions per result. New REST surface:GET /api/interview-prep,GET /api/interview-prep/:name,DELETE /api/interview-prep/:name. 7 new tests.feat(cv): generate + download PDF in browser, with PDF archive— new 📄 Generate PDF button on the CV page streams/api/stream/pdfin a modal console. OnERR_MODULE_NOT_FOUND/playwrighterrors, it surfaces a copy-pasteable bootstrap command. New "Generated PDFs" section auto-loads after each successful run, listing everyoutput/*.pdfwith ↗ Open and ⬇ Download buttons. New REST surface:GET /api/output/pdfs,GET /api/output/pdfs/:name. 6 new tests.feat(api): POST /api/tracker — append rows from the UI(FIX-H8) — append a canonical row todata/applications.mdfrom the browser. Validates company + role, normalizes status againsttemplates/states.yml, auto-increments zero-padded#, dedups by company+role (case-insensitive), pipe-escapes notes so the markdown table doesn't fracture. Bootstraps the table when the file is empty. 6 new tests.feat(api): DELETE /api/jds/:name(FIX-H4) — remove saved JDs without shelling out. Path-traversal characters are stripped before any filesystem touch; the parameter must end in.txt. 5 new tests, including../../etc/passwdrefusal.feat(api): POST /api/evaluate/test-gemini(FIX-H7) — smoke-test endpoint that runs a 50-char dummy JD throughgemini-eval.mjsso the user can verify the API key works without sitting through a real evaluation. Returns{ ok, code, sampleLength, sample }.
🐛 Bug fixes
fix(router): catch-all 404 view + i18n coverage guard(FIX-C7) — unknown hash routes used to silently fall back to the dashboard, masking typos and broken bookmarks. Now#/totally-random-xyzrenders a dedicated 404 page that quotes the bad path back and links to the dashboard. The 404 view is registered inside the router IIFE itself so it cannot collide with any user route. Newtests/i18n-coverage.test.mjsrunsi18n.jsinside avm.Contextwith a stubwindow, exposes the privateDICT, and asserts every one of the 173+ keys × 8 locales is populated and non-empty. 4 new router tests.fix(router): alias #/profile → settings(FIX-C2) — the internal route name issettings(withnav.settingsrendering "Profile") but external links and muscle memory go to#/profile. Now both addresses reach the same view, and the sidebar nav-item lights up either way. 2 new tests.fix(health): unify Health/Doctor + flag template profiles(FIX-C6 + FIX-H6) — Health and Doctor were two different sources of truth. Now/api/healthexposes everything Doctor reports (parent-deps, Playwright, dirs, profile-customized,HH_USER_AGENT). TheProfile customizedcheck detects placeholder names (Jane Smith,Alex Doe,John Doe,Your Name,Test User) and explicit YAML parse errors. 4 new tests.fix(scan): warn on query↔negative collisions in RU config(FIX-H3) — whenportals.ymlships with"PHP"intitle_filter.negativewhile the queries target Senior PHP, every match gets filtered and the user sees zero results.loadConfig()now computes awarningsarray;runRuScan()emits each warning as an SSE stderr line before the scan starts. 2 new tests verify the shipped defaults stay PHP-friendly out of the box.fix(scan): warn when HH_USER_AGENT is unset(FIX-H1) — the/scanpage probes/api/healthand shows a yellow warning card above the action row whenHH_USER_AGENTis empty, so users know about the hh.ru 403 before they click RU scan.fix(api): warn when POST /api/jds slug had unsafe chars stripped(FIX-M2) — slug normalization that strips dangerous characters now returns awarningfield; pure case/whitespace cleanup stays silent. Empty result after sanitization returns 400.fix(ui): clear global search on route change + button spinners(FIX-M4 + FIX-L1) — the global-search input is cleared onhashchange(with a guard for active typing). NewUI.withSpinner(button, fn)helper wires loading state, ARIA, and double-click prevention into every async button click. Already adopted on Doctor / Verify / sync-check / Save CV / Normalize / Dedup / Merge buttons.fix(ui): make sidebar scrollable so 18 nav items always reach the footer— the grouped sidebar from FIX-C8 overflowed shorter viewports; bottom items (Activity / Health) were clipped..sidebarnow hasoverflow-y: autowith thin custom-styled scrollbars (WebKit + Firefox). Footer stays pinned via the existingmargin-top: auto.fix(ui): empty modal-title placeholder(FIX-H9) — the hardcoded English"Title"string inindex.htmlis gone, closing the brief race window where it was visible during modal open.
🌐 i18n
- 173+ translation keys × 8 supported locales (
en,es,pt-BR,ko,ja,ru,zh-CN,zh-TW). New keys added across all locales for: 404 page, activity log, deep research, PDF flow, security warnings, tracker mutation, apply rename. Coverage is now enforced bytests/i18n-coverage.test.mjs— every key must have a non-empty value in every supported locale or CI fails.
⚙️ DevOps
- Test count: 73 → 201 (+128 tests across 23 test files). The single remaining failing test (
runEnScan: dry-run end-to-end across multiple sources) is a pre-existing flake dependent on Greenhouse/Ashby/Lever live API responses. - Comprehensive Playwright e2e (
tests/e2e-comprehensive.mjs, 23 steps): walks the full user journey — CV save → preview → PDF generation → all 7 new modes → tracker filters → activity log → 404 → modal ESC → sidebar scroll → Ctrl-K focus → search clear → profile alias → language persistence. - GitHub Actions (
.github/workflows/):ci.yml— unit + integration tests on Node 18/20/22 matrix, plus i18n coverage gate (every key × 8 locales must be non-empty), plus the full Playwright e2e on every PR.ai-review.yml— Claude Code AI review on every PR. Maintainers retain merge authority; Claude only suggests. Skip viaskip-ai-reviewlabel.release.yml— auto-publish a GitHub Release when av*.*.*tag is pushed; release notes are sliced fromCHANGELOG.mdso all 8 language variants stay the canonical source.
- CSP-friendly UI: all inline
onclickhandlers removed fromindex.htmlandrouter.js. The strictscript-src 'self'policy is now enforceable without breaking any feature.
📦 New REST endpoints
| Method | Path | Purpose |
|---|---|---|
GET
| /api/activity
| List user-action events, newest first |
GET
| /api/interview-prep
| List saved Deep Research files |
GET
| /api/interview-prep/:name
| Read a single Deep Research file |
DELETE
| /api/interview-prep/:name
| Remove a Deep Research file |
GET
| /api/output/pdfs
| List generated PDFs |
GET
| /api/output/pdfs/:name
| Stream a PDF as an attachment |
POST
| /api/tracker
| Append a row to applications.md
|
DELETE
| /api/jds/:name
| Remove a saved JD |
POST
| /api/evaluate/test-gemini
| Smoke-test the Gemini API key |
POST
| /api/mode/:slug
| Generic prompt builder for the 7 new modes (project / training / followup / batch / contacto / interview-prep / patterns) |