feat: UI gaps batch — labels, disclosure, schema validation, layout UX#121
Closed
feat: UI gaps batch — labels, disclosure, schema validation, layout UX#121
Conversation
…JSON dump - Add `_json_ld_blobs_to_markdown()` to job_routes.py that dynamically renders all JSON-LD blob fields as human-readable markdown with no hard dependency on any specific field like `description` - Heading keys (title, name) become ## headers; meta fields (hiringOrganization, jobLocation, employmentType, datePosted, baseSalary) render as bold inline/block metadata; body fields (qualifications, responsibilities, skills, description, etc.) render as ### sections; all remaining fields append as bold key-value pairs - HTML content in field values is stripped to plain text via BeautifulSoup before embedding in the markdown output - Remove `json_ld_text` tracking (description-only fallback); both the SPA-noise branch and the hybrid branch now call `_json_ld_blobs_to_markdown` so no fields are dropped - Fix nested-dict inline rendering: when a sub-field renders to multiple lines, insert a newline before the content instead of concatenating inline - Add `_is_section_header()` guard in `_infer_position_name` so generic markdown labels (Description, Qualifications, etc.) are not mistaken for position titles when JSON-LD blobs have no title/name field - Update test assertion from exact-string match to substring containment to match the enriched markdown output format
- Rewrote test_ui_auth.py for the 4-step wizard/pill model: TestLlmStatusPill and TestModelWizard replace the defunct TestAuthBadge/TestAuthModal classes; full wizard integration tests navigate to the copilot-oauth auth step - Deleted TestSessionPicker (4 tests) -- .load-item-row intentionally removed - Split stale test_generate_calls_api_rewrites_then_action into two accurate tests reflecting the current rewrite-then-user-continues flow - Fixed _fetchAndDisplayLayoutPreview() in layout-instruction.js: guard with GENERATION_PHASES so passive restore (idle/confirmed) never calls markPreviewGenerated(), preventing phase regression in browser-restore tests - Rebuilt bundle.js after layout-instruction.js changes chore(tooling): add Playwright test runner helpers - Add "test:ui" npm script (conda run -n cvgen pytest tests/ui/) - Add test_ui.sh executable script with pass-through pytest args
…markPreviewGenerated calls
Implements the plan in .github/prompts/plan-stepPillNavStateModel.prompt.md.
Bug fix: navigating back to an earlier tab via the tab bar or a step pill
previously showed a confirmation modal whose "Proceed" callback called
switchTab() -- but there was no visible path to return to the active step.
The browsing-away amber ring now serves as the return affordance.
Core changes
------------
* Remove the tab-click guard from ui-core.js setupEventListeners() --
tab clicks go directly to switchTab(), no modal.
* Remove _showReRunConfirmModal calls from handleStepClick back-nav path --
clicking a completed step pill now browses directly, no modal.
* Change confirmReRunPhase to call backToPhase(step) instead of reRunPhase(step)
-- the hover rerun button repositions the phase pointer and lets the user
press the action button rather than triggering a silent LLM re-run.
Visual model (two independent signals)
---------------------------------------
Fill = app state (blue=active, green=completed, amber=stale, red=stale-critical)
Ring = view cursor (blue solid ring on viewed step, amber pulsing ring on
active pill while browsing elsewhere)
CSS additions (styles.css)
.step.viewing -- solid 2px blue ring; transition 0.2s
.step.browsing-away -- pulsing amber ring via @Keyframes browsing-pulse
inset box-shadows removed from .step.stale/.step.stale-critical (background
colour is sufficient; avoids compound box-shadow conflict with ring)
JS additions (workflow-steps.js)
_getStepTooltip() -- pure state->tooltip-text function
_updateViewingIndicator() -- applies .viewing/.browsing-away classes and
Bootstrap 5 tooltips to each step pill; exported
updateWorkflowSteps() -- calls _updateViewingIndicator at end; handles
status.stale_steps to apply .stale class
handleStepClick() -- simplified: no modals, direct switchTab()
review-table-base.js
switchTab() calls _updateViewingIndicator(tab) after setCurrentTab()
Backend (Phase D)
-----------------
* StatusResponse dataclass gains stale_steps: List[str]
* status route populates stale_steps from conversation.state
* back_to_phase() computes stale_steps = all steps after target phase;
stored in session state so fetchStatus() reflects it immediately
Tests: 1058/1058 JS, 1154/1155 Python (pre-existing Playwright Copilot-auth
browser test unrelated to this change)
feat(ui): step-pill two-signal nav state model
…fore analysis Session modal (fix lost due to concurrent agent edits): - loadSessionAndCloseModal() now clears dismissDisabled before calling closeSessionsModal(), so clicking Load in the startup-required sessions modal always dismisses it even when opened with required:true. Job URL fetch: - fetchJobFromURL() now switches to the job tab and renders the fetched content via populateJobTab() instead of immediately calling analyzeJob(). - populateJobTab() shows a primary "Analyze Job" button when phase is still 'init', giving the user a chance to review the content first. - Confirmation message tells the user to click Analyze Job to continue. Tests: increase default CV_SERVER_STARTUP_TIMEOUT from 15→30s in tests/ui/conftest.py to match the timeout used in recent test runs.
- Correct JSDoc block ordering in layout-instruction.js: _buildPreviewPayload doc now precedes its function; the _fetchAndDisplayLayoutPreview strategy docstring now sits directly above that function instead of above the helper (orphaned by insertion) - Remove unused `import pytest` from tests/ui/test_ui_auth.py (leftover from deleted pytest.skip() pattern in the old TestAuthModal class; new wizard tests assert directly, so pytest is not needed) - Make test_ui.sh portable: replace hardcoded Caskroom Python path (/usr/local/Caskroom/miniconda/base/envs/cvgen/bin/python) with `conda run -n cvgen python` (consistent with npm run test:ui) All 102 Playwright UI tests pass.
…nd_customizations analyze_job must advance to JOB_ANALYSIS, not CUSTOMIZATION. CUSTOMIZATION is only correct after recommend_customizations runs. If the backend jumped to CUSTOMIZATION here, a server restart between analysis and recommendations would restore the session with an empty Customise tab (the frontend _resolveRestoredPhase guard in session-manager.js compensated, but the root cause was in the backend). recommend_customizations must advance to CUSTOMIZATION, not REWRITE_REVIEW. The user reviews recommendations in the Customise tab before proceeding to the Rewrite tab. Premature REWRITE_REVIEW caused a restart to drop the user into an empty Rewrite tab, skipping Customise entirely. Correct phase sequence: analyze_job -> JOB_ANALYSIS recommend_customizations -> CUSTOMIZATION user advances -> REWRITE_REVIEW Restore two regression tests deleted by the concurrent agent: test_analyze_action_sets_job_analysis_phase test_recommend_customizations_action_sets_customization_phase The _resolveRestoredPhase frontend guards added in PR #88 are intentionally kept as defence-in-depth; the backend fix + frontend guard together are more robust than either alone. Fixes were originally present in the dev stash (stash@{0}) before being overwrote by concurrent agent edits during PR #88 work.
fix(review): address 3 code review findings from post-refactor review
… layout (#70) Issue #70: EXPERIENCE section should flow after Achievements when space allows, rather than always starting on a forced second page. Root cause: the cv-template used two separate #page-one / #page-two <div> elements, each with their own float context. The rigid two-div structure forced a hard CSS page-break between them, making it impossible for content to flow naturally from the first logical page to subsequent print pages. Fix: merge the two page divs into a single #cv-body div with one unified left-col (contact -> education -> awards -> certs -> languages -> skills) and one right-col (header -> summary -> achievements -> experience -> publications). Both columns now flow across print pages naturally. CSS changes: - Screen: replace #page-one / #page-two per-div selectors with #cv-body selectors; left-col uses align-self: stretch; right-col uses flex: 1 - Print: float-based two-column layout on #cv-body with box-decoration-break: clone on the sidebar for repeating background; add break-inside: avoid on .section so whole sections move to the next page rather than splitting - Remove: forced page-break-before: always on #page-two, fixed 11in min-height constraints, and the two-float-context mismatch - Update inline JS page-break marker injection to target #cv-body Test changes: - TestOptionalSidebarFields: updated 3 tests to check #cv-body selectors and new print CSS rules instead of deprecated #page-one / #page-two rules - TestExperiencePageFlow: updated all 6 tests to use cv-body slice instead of page-two; replaced the first-page/second-page heading test with a document-order assertion (achievements before experience in HTML order) - 88 tests pass: test_cv_template.py + test_benchmark_cv_render.py + test_template_renderer.py; also test_layout_digest.py + test_ats_generation.py
When a job description is fetched from a URL, the session now exposes
that URL through /api/status so the frontend can link back to the
original posting.
Changes:
- Add `job_url: Optional[str]` field to StatusResponse dataclass
- Wire `conversation.state.get("job_url")` into the StatusResponse
construction in status_routes.py
- In populateJobTab(), wrap the position-title <h1> in an <a> link
(color:inherit, text-decoration:none) when job_url is present
- Add a subtitle line below the <h1> showing the full URL with CSS
overflow-ellipsis truncation and a title= tooltip for hover; both
links open in a new tab with rel="noopener noreferrer"
- Rebuild web/bundle.js
No URL display is shown when the job was pasted rather than fetched.
- Advance to JOB_ANALYSIS (not CUSTOMIZATION) after analyze_job in both the web _execute_action path and the CLI loop; CUSTOMIZATION is only the correct phase once recommend_customizations has run - Advance to CUSTOMIZATION (not REWRITE_REVIEW) after recommend_customizations; REWRITE_REVIEW is only set by the /api/cv/rewrites route after the user clicks through the Customise tab - Add _resolveRestoredPhase guards: if restored phase is customization or rewrite_review but no customizations exist in the session, fall back to job_analysis so restarts mid-workflow place the user at the right step - Export _resolveRestoredPhase for testability - Add 2 Python regression tests and 7 JS tests covering all guard branches fix(job-input): improve HTML-to-text conversion for job descriptions - Preserve line breaks from <br> and block-level elements before extracting plain text in _json_ld_blobs_to_markdown, preventing word run-on from inline-concatenated HTML - In _renderJobText detect raw HTML stored in older sessions and sanitize directly via DOMPurify instead of running through marked, avoiding double-parse corruption while keeping XSS protection on both code paths
Fixes two bugs exposed when loading a prior session:
1. Two different question sets on session restore
- analyze_job now uses a JSON-schema prompt so questions arrive as
structured data in one LLM call; conversation_history (human-readable)
and state[post_analysis_questions] (structured objects) are populated
from the same parsed result, eliminating the regex-divergence mismatch.
- Add _parse_json_questions_response() with markdown-fence stripping,
json.loads fast path, and bracket-depth fallback for robustness.
2. Missing chat history on session restore
- fetch_job_url and load_job_file now append a user message to
conversation_history so job submissions appear in the restored log.
- do_action() calls _save_session() immediately after _execute_action()
so history is persisted even if client never calls
/api/post-analysis-responses.
3. Collapsible long messages in chat
- Add _makeCollapsibleContent() in message-queue.js: messages exceeding
8 lines or 480 chars show a preview with a Show N more lines toggle.
- Add backtick inline-code rendering alongside existing bold/italic.
- Add .collapsible-msg, .msg-preview, .msg-full, .msg-toggle-btn CSS.
4. Update test_analyze_action_includes_extracted_questions_in_context to
mock the LLM response as JSON rather than free-form numbered text.
…rser The layout-instruction path in apply_layout_instruction() used a bare json.loads() call with no fallback. Any model response wrapped in a markdown fence (```json ... ```) would crash with JSONDecodeError rather than being gracefully recovered. Changes: - Wrap json.loads() in try/except json.JSONDecodeError; fall through to self.llm._parse_json_response() which handles bracket-scan extraction - Extend the outer except handler to also catch ValueError (raised by _parse_json_response when no JSON container is found), returning the same parse_error dict that json.JSONDecodeError produced before - Add regression test: markdown-fenced layout response must parse successfully via the bracket-scan fallback - Update test_invalid_json_response_includes_raw_response to mock _parse_json_response.side_effect = ValueError, reflecting the real method's behaviour for un-parseable text Part of: feat/structured-json-output (Phase 1 of 4)
…r OpenAI providers
Adds json_mode: bool = False to the LLMClient.chat() abstract method and all
concrete implementations, then enables native response_format enforcement on
providers that support it.
Provider behaviour:
- OpenAIClient (and subclasses GroqClient, GitHubModelsClient, CopilotClient):
pass response_format={"type": "json_object"} to the API when json_mode=True,
constraining output at the API level rather than relying on prompt text alone
- CopilotOAuthClient: same — adds response_format to the raw JSON payload
- AnthropicClient, GeminiClient, CopilotSdkClient, LocalLLMClient, StubLLMClient:
accept json_mode param but ignore it; enforcement remains prompt-only for
providers that do not support a native JSON mode parameter
- call_llm() convenience wrapper gains the same param and passes it through
Internal call sites updated to pass json_mode=True:
- analyze_job_description() — returns structured job-analysis dict
- recommend_customizations() — returns structured recommendations dict
- rank_publications_for_job() — returns structured publication ranking list
- _propose_rewrites_via_chat() — returns structured rewrite proposals list
Call sites intentionally NOT updated (non-JSON responses):
- generate_professional_summary() — plain text
- rewrite_achievement() — plain text
- semantic_match() — numeric score
- convert_to_bibtex() — raw BibTeX text
Part of: feat/structured-json-output (Phase 2 of 4)
Add runtime structured output validation for the three heavy LLM calls:
- New `scripts/utils/llm_response_models.py`:
- `JobAnalysisResponse` (10 required fields + optional `reasoning`)
- `CustomizationResult` with nested `ExperienceRecommendation`,
`SkillRecommendation`, `AchievementRecommendation`, `SuggestedAchievement`
- `PublicationRankingItem` (cite_key, relevance_score, confidence, etc.)
- New `LLMClient._validate_with_repair()` helper:
- Validates a parsed dict against a Pydantic v2 BaseModel
- On `ValidationError`, extracts missing/invalid field paths and issues a
targeted one-shot repair prompt to the LLM (with json_mode=True)
- Re-validates the repaired response; re-raises if still invalid
- Wired validation into three callers in `llm_client.py`:
- `analyze_job_description` → validates with `JobAnalysisResponse`
- `recommend_customizations` → validates with `CustomizationResult`
- `rank_publications_for_job` → validates each array item with
`PublicationRankingItem`; invalid items after repair are skipped with a
warning rather than crashing the full ranking call
All 278 tests pass (pytest tests/test_llm_client.py
tests/test_layout_instructions.py tests/test_cv_orchestrator.py
tests/test_conversation_manager.py).
…ions Wire json_mode=True into the structured clarifying-questions chat call so API providers (OpenAI, GitHub Models, CopilotOAuth) enforce JSON output at the protocol level rather than relying only on prompt instructions. - `conversation_manager.py`: add `json_mode=True` to `self.llm.chat()` call that generates post-analysis clarifying questions (the call that feeds `_parse_json_questions_response`) - Remove dead `_extract_structured_questions()` method — it parsed numbered free-form text but was never called; the flow has used `_parse_json_questions_response` (JSON-first) since the structured-questions prompt was introduced - `tests/test_conversation_manager.py`: remove two tests that directly exercised the deleted `_extract_structured_questions` helper; retain the remaining test asserting phase transitions (61 tests pass) All non-browser tests pass: 1248/1250 (2 pre-existing Playwright timeouts).
Four findings from systematic code review; three resolved with code changes, one accepted as intentional design. Finding 1 — CopilotSdkClient ignores json_mode (FIXED): scripts/utils/llm_client.py: When json_mode=True, prepend a hard system instruction 'Respond with valid JSON only. No prose, no markdown fences.' before forwarding messages to any-llm. The copilotsdk provider converts messages to a flat prompt and has no response_format API param; the system message provides equivalent enforcement. Finding 2 — JobAnalysisResponse / CustomizationResult all-optional fields (DESIGN ACCEPTED): scripts/utils/llm_response_models.py: Added docstring comments explaining that all fields intentionally have defaults — type safety without breaking partial responses. Empty fields degrade gracefully in the downstream workflow. Finding 3 — No tests for _validate_with_repair() repair path (FIXED): tests/test_llm_client.py: Added TestValidateWithRepair class with 3 tests covering all code paths — immediate success, repair-on-failure, and ValidationError re-raise on persistent failure. Finding 4 — confidence field not validated against enum (ACCEPTED): scripts/utils/llm_response_models.py: Added inline comments '# expected: high | medium | low' to all four confidence fields. Kept as plain str for flexibility since confidence is display-only. CodeQL: 15 pre-existing findings in routes/ (path-injection, SSRF, stack-trace-exposure) — none in the modified files. All tests: 279 passed.
Security fixes (CodeQL): - auth_routes: remove stack trace exposure from HTTP 500 responses in set-model endpoint; add logger.warning for auth poll thread failures - job_routes: add DNS resolution check to block DNS-rebinding SSRF attacks on bare hostnames - session_routes: replace user-supplied path concatenation with a server-side enumeration helper (_resolve_session_path) to prevent path-injection vulnerabilities across load, delete, rename, restore, and trash endpoints Test fixes (Playwright): - test_web_ui_workflow: increase page-ready timeout (5 s to 15 s) to avoid intermittent startup failures on slow CI runners - mock_responses: add missing structured_output field to mock analysis fixture so phase-advance assertions pass Logging improvements: - auth_routes: log Copilot auth poll failures at WARNING level before storing error string for UI (background thread was completely silent on server logs) - job_routes: existing _requests.RequestException and bare Exception handlers already use logger.exception; Timeout/ConnectionError are user-expected conditions returning 400/500 with instructions - session_routes: silent except-pass clauses in listing loops are intentional (skip corrupt session.json); outer handlers already use logger.exception
…utput feat(conversation): structured JSON output with Pydantic validation and self-repair
Backend: - auth_routes: add _persist_provider_model_to_config() -- atomic YAML write (tmp+backup) so model/provider changes in the UI survive restart - llm_client: forward llm_request_timeout config value to OpenAI, Anthropic, and Gemini API calls (guards against hanging requests) - run_codeql.sh: add --custom-only mode to run .github/codeql/ queries against the VS Code extension's cached DB (~1-5 min vs ~15 min) Frontend: - job-analysis.js: skip extractStructuredQuestionsFromAssistantText when structured questions already present from API response - job-input.js: add FORBID_ATTR:['style'] to DOMPurify to strip inline CSS from Word/JSON-LD job descriptions - message-queue.js: use display='block' (not '') in show-more toggle - session-manager.js: trigger background testCurrentModel() health check after session restore and loadSessionFile - utils.js: guard cleanJsonResponse against non-string input Tests: - test_llm_client: patch utils.config.get_config in Anthropic and Gemini chat tests to isolate them from real config.yaml timeout value CodeQL queries: add 13 custom queries under .github/codeql/ covering unlogged exceptions, swallowed exceptions, exception detail in responses, SSRF path traversal, hardcoded secrets, LLM calls without timeout, master-data writes, Flask route inventory, and more
Replace silent `except: pass` blocks with structured log messages, add a slow-state badge to the LLM busy overlay, update the default provider/model, tighten CodeQL queries, and remove a dead workspace entry. Logging improvements - auth_routes: log non-critical provider/model persistence failure at DEBUG - generation_routes: log materialization failure and git-commit exceptions at WARNING - master_data_routes: log failed backup restoration at WARNING - session_routes: log unreadable session files at DEBUG; scan errors at WARNING - status_routes: log skipped unreadable sessions during clarification search at DEBUG - conversation_manager: log skipped session files and layout digest failures - copilot_auth: log unreadable token cache at DEBUG - session_registry: log timestamp parse failures at DEBUG UI — LLM busy overlay - Add "Taking longer than usual" pill badge that appears in slow-state overlay - Badge is hidden by default; shown only when the `.slow` class is applied Config - Switch default_provider to copilot-sdk and default_model to gpt-5-mini CodeQL - llm-call-without-timeout: exclude provider chat() impls that manage timeout internally via config, eliminating misleading false positives - master-data-write-outside-window: remove cv_data from master-var predicate (always a local rendering copy, never the master dict) - path-traversal: add isBarrier for _resolve_session_path, _resolve_backup_path, and safe_join helpers; narrow sink to os.path.join only Chore - Remove dead clem-diagrams entry from cv-builder.code-workspace
Add 15 new tests covering: - _getStepTooltip: all tooltip states (upcoming, active+viewing, browsing-away, completed+viewing/not-viewing, stale-critical, stale) - _updateViewingIndicator: viewing class applied, other pills cleared, browsing-away on active pill, no browsing-away when viewing active pill, tab alias mappings (questions→analysis, exp-review→customizations), unknown tab clears all rings
…firmed - Remove previewAvailable guard from the fresh-render path so generation_state.preview_html is always populated before confirm-layout, covering the legacy generate_cv action that writes files to disk without updating backend generation state - Collapse the now-redundant recovery path into the fresh-render path; passive restore is used only for confirmed/final_complete sessions or when generate-preview is unavailable (no job_analysis yet) - Regenerate web/bundle.js
…et leakage .specstory/history/ files can contain API keys from chat sessions. Also untrack .specstory/statistics.json which was previously committed.
…rvive tab navigation Agent-Logs-Url: https://github.com/Warnes-Innovations/cv-builder/sessions/ad3a0147-1320-484e-abdc-a603588e6fae Co-authored-by: warnes <6144863+warnes@users.noreply.github.com> # Conflicts: # web/bundle.js
… sub-sections (closes #95)
…ents, ats-modal, trash, publications)
Replace the bespoke 11-pattern substring list in cv_orchestrator.py with
a centralised prompt_safety module backed by the llm-sanitizer library.
Changes
-------
scripts/utils/prompt_safety.py (new)
- Lazy Scanner singleton (thread-safe module-level initialisation).
- scan_text_for_injection(text, min_risk) — union of llm-sanitizer rule
scan and supplementary substring check; covers zero-width chars, base64
payloads, homoglyphs, data-exfil patterns AND cv-builder-specific phrases
(system prompt, agent instruction, etc.) that must be detected in plain-
text DOM fragments (comment/hidden-element text) where surrounding HTML
syntax is absent.
- sanitize_instruction_text(text) — strips injection content via
llm-sanitizer redact() then a word-boundary regex pass; returns
(cleaned_text, findings) in the legacy cv-builder dict format.
- scan_for_safety_ale - scan_for_safety_ale - scan_for_safety_ale - scan_for_safety_ale - scan_for_safety_ale - scan_for_safety_ale - sls/cv_orchestrator.py
- Remove _LAYOUT_AGENT_INSTRUCTION_PATTERNS constant.
- _sanitize_layout_instruction_text: replace manual regex loop with
sanitize_instruction_text(); return shape unchanged.
- _sanitize_layout_context_html, _sanitize_layout_instruction_htm - _sanitize_layout_context_html, _sanitize_layout_instruction_htm - _sa
s/r s/y. s text is extracted; log a warning if indicators found; include
safety_alert in the JSON respons safety_alert in the JSON respons safety_alert in the JSON y.py (new)
- 21 unit tests covering scan_text_for_injection, sanitize_ - 21 unit tests covering scan_text_for_injection, sanitize_ - dth-char detection test.
All 343 existing Python tests pass; 21 new tests added and passing.
- add __env to repository ignore rules - prevent accidental commits of local API key environment file - keep environment guidance in workspace while excluding sensitive content
The llm-sanitizer package is installed locally via `pip install -e` but is not yet published to PyPI and cannot be added to scripts/requirements.txt. CI was failing with an ImportError at collection time because `scripts/utils/prompt_safety.py` imported directly from `llm_sanitizer.*`. Changes: - Wrap the three `llm_sanitizer` imports in a try/except ImportError block and set `_HAS_LLM_SANITIZER = True/False`. - Guard the scanner-based passes in `scan_text_for_injection`, `sanitize_instruction_text`, and `scan_for_safety_alert` so they short-circuit gracefully when the library is absent. - The fast substring pass in `scan_text_for_injection` always runs, preserving injection detection for the most common cv-builder cases without the library. - `min_risk` parameter type changed from `RiskLevel` (import-time default) to `Any = None`, resolved to `RiskLevel.high` at call time when the library is available. Local test: 306 passed (CI-equivalent command).
- Correct persona tally consistency in the rollup - Add technical persona gap synthesis and updated totals - Normalize markdown formatting for clean lint diagnostics
…date requirements - Remove optional import logic and guards from prompt_safety.py - Restore direct imports for RiskLevel, ScanResult, redact, Scanner - Add llm-sanitizer>=0.1.0 to requirements.txt for CI and local installs - Update UI browser restore test for new layout review state - Minor workspace config tweak for uv All Python tests pass (306/306) in cvgen environment.
…V tab, and first-run onboarding - GAP-49 (spell-check confirm gate): add _confirmProceedToGenerate() modal before CV generation in both empty and full spell-check submit paths; shows current ATS score and staleness warning before proceeding - GAP-30 (cover letter opening style): add formal/hook/narrative opening style selector to cover letter form; _OPENING_GUIDANCE dict in backend drives per-style LLM prompt; opening_style persisted in session state - GAP-41 (Master CV tab pre-job): expose 'master' tab in STAGE_TABS.job so Master CV editor is accessible before job description is entered - GAP-36 (first-run onboarding): session-free /api/setup/master-cv-status and /api/setup/create-master-cv endpoints; showOnboardingModal() / onboardingCreateEmptyProfile() JS functions; onboarding modal HTML in index.html; 7 new JS tests covering all onboarding paths - fix(tests/ui): change Playwright goto wait_until from networkidle to load across all 8 conftest fixtures to eliminate a pre-existing flaky timeout; _wait_for_ui_ready() already ensures JS readiness
Enhance the onboarding modal (introduced in GAP-36) into a general welcome/orientation screen shown on every startup until explicitly dismissed. Both variants now share a complete workflow overview. Changes: - web/index.html: restructure onboarding modal body with shared 3-step workflow section (colored numbered badges: build master profile -> target job -> harvest improvements) and variant-specific CTA banners (green for present, amber for missing); "Don't show again" checkbox on present footer - web/session-manager.js: add _setWelcomeSection(), maybeShowWelcomeModal() (checks localStorage dismissal flag, queries /api/setup/master-cv-status, shows appropriate section), and closeWelcomeModal() (hides overlay, optionally persists dismissal flag); update showOnboardingModal(); export 2 new symbols - web/app.js: call maybeShowWelcomeModal() in init() after ensureSessionContext - web/bundle.js: regenerated (2717.9 KB) - tests/js/session-manager.test.js: update DOM fixtures; update showOnboardingModal test to assert section visibility; add 7 new tests covering maybeShowWelcomeModal and closeWelcomeModal; fix localStorage cleanup with try/catch Closes GAP-37
master_data_routes.py omitted certifications from the /api/master-data/full
response, causing the Certifications section in the Master CV editor to
always render empty despite correct data in Master_CV_Data.json.
Changes:
- scripts/routes/master_data_routes.py: add
"certifications": master.get('certifications', []) to master_data_full()
response payload
Closes GAP-42
Replace the non-functional undoInstruction() stub with snapshot-based undo. Before each layout instruction is applied, the current HTML and instruction list are pushed onto an in-memory undo stack (capped at 20 entries). When Undo is pressed, the last snapshot is popped and restored: preview iframe, stateManager CV artifacts, and instruction history are all rolled back. Changes: - web/layout-instruction.js: add module-level _layoutUndoStack / _UNDO_STACK_MAX; push snapshot in submitLayoutInstruction() before API call; replace stub undoInstruction() with stack-pop restore logic - web/bundle.js: regenerated (2719.1 KB) - tests/js/layout-instruction.test.js: add submitLayoutInstruction to imports; rewrite undoInstruction describe block with 4 tests covering empty-stack no-op, snapshot restore of HTML and instructions, system message, and absent-layoutInstructions guard Closes GAP-25
Resolves the 79-record ambiguous set from the initial /specstoryOrganize pass (55 unique files after cross-workspace duplicates were identified). Changes: - Deleted 21 empty/stub sessions (untitled sessions, "File editing session" placeholders, and near-empty sessions under 300B) - Moved 8 personal-research sessions (diabetic neuropathy knowledge system work) to ~/CV/llm-history/ instead of a code repo - Resolved 23 cross-workspace duplicate groups: kept the canonical copy in the correct repo, deleted or moved the duplicates accordingly - Routed 3 single-copy files to their correct repos (vscode-config) - Annotated all kept copies with specstory-relocated metadata
…ons"; GAP-29 venue warning GAP-28: cv-template.html conditionally rendered "Publications" when no count suffix was needed. Heading now always reads "Selected Publications"; the count suffix (n) is appended only when some publications were omitted. GAP-29: _format_publications() in cv_orchestrator.py never set venue_warning, so the .pub-venue-warn icon in the template was never triggered. Now sets venue_warning to a descriptive message for any entry missing both journal and booktitle fields; entries with a venue continue to receive an empty string. Changes: - templates/cv-template.html: always emit "Selected Publications"; move count suffix outside the conditional so it only appears when pubs were omitted - scripts/utils/cv_orchestrator.py: add venue_warning field to every entry in _format_publications() based on presence of journal or booktitle Closes GAP-28 Closes GAP-29
…ublications" for all Update cv-template.html and requirements to reflect the correct heading rule: - "Selected Publications" when a subset of accepted publications is shown - "Publications" when all accepted publications are shown - Count suffix (N) never appears in generated documents Also fix stale evidence in review-status/hiring-manager.md where two rows still described venue_warning as unimplemented (fixed by GAP-29, ad9edf0). Changes: - templates/cv-template.html: conditional heading in both HTML section and ATS plain-text block - tasks/user-story-hiring-manager.md: US-M7 item 5 and acceptance criteria updated to specify the subset/full distinction and prohibit count suffix - tasks/gaps.md: GAP-24 description and GAP-28 status updated; count context removed from recommended resolution - tasks/ui-review.md: GAP-28 finding marked CLOSED - tasks/review-status/hiring-manager.md: heading and venue-warning rows updated to Pass/Partial with accurate evidence Amends behavior introduced in ad9edf0.
…compute time
Previously, ats_score (from POST /api/cv/ats-score) and validation_results
(from GET /api/ats-validate) were only written to metadata.json during
POST /api/finalise. If finalise was never called, those values were lost.
Fix: add a _try_patch_metadata() helper to both generation_routes.py and
review_routes.py. After each value is computed and stored in session state,
the helper immediately patches it into the session's metadata.json file.
The helper is fire-and-forget — exceptions are logged as warnings but never
propagated, so no existing error paths change.
Affected routes:
- POST /api/cv/ats-score → patches {"ats_score": score} into metadata.json
- GET /api/ats-validate → patches {"validation_results": {...}} into metadata.json
showAlertModal and closeAlertModal were defined in both ui-core.js and ui-helpers.js. The ui-core.js versions targeted non-existent element IDs (alert-modal, alert-title, alert-message) and created a second hidden overlay dynamically, making them effectively dead code. The ui-helpers.js versions are canonical: they target the correct HTML elements (alert-modal-overlay, alert-modal-title, alert-modal-message) and integrate with the focus-trap helpers (trapFocus, restoreFocus, setInitialFocus) exported by ui-core.js. Changes: - Remove showAlertModal + closeAlertModal function bodies and export from ui-core.js - Remove stale "also defined in ui-core.js" comment from ui-helpers.js - Rebuild web/bundle.js
…cknowledgement Require the user to expand the persuasion warnings panel and click "Acknowledged" before the "Submit All Decisions" button becomes enabled and before submitRewriteDecisions() will proceed. Changes: - updateRewriteTally(): disable submit button when persuasionWarningsAcknowledged is false (in addition to the existing pending > 0 check) - Acknowledged button onclick: call updateRewriteTally() after setting the flag so the submit button re-evaluates immediately - submitRewriteDecisions(): hard guard — shows an alert and returns early if warnings have not been acknowledged (defence in depth) - Add setPersuasionWarningsAcknowledged() setter for testability - Update rewrite-review.test.js: set acknowledged=true in the "enables submit button when no pending cards remain" test - Rebuild web/bundle.js
… Review tab Backend: - POST /api/cover-letter/save now appends the generated cover letter filename to generated_files['files'] in session state so it is registered for download and visible to the frontend. - POST /api/screening/save does the same for the screening responses DOCX. Frontend (download-tab.js / bundle.js): - _collectDownloadableFiles() recognises CoverLetter_* and Screening_Responses_* filename prefixes and renders them with appropriate descriptions instead of the generic DOCX label. Both artefacts are now served by the existing /api/download/<filename> route via the generated_files['files'] list lookup.
…abels
SESSION_PHASE_LABELS in utils.js now maps every backend phase enum to a
human-readable title-case string (e.g. 'rewrite_review' → 'Rewrite Review',
'init' → 'Getting Started') rather than lowercase abbreviations.
session-manager.js: loadSessionFile restore message now reads from
SESSION_PHASE_LABELS via an inline lookup instead of embedding the raw
enum value (e.g. '(rewrite_review)' → '(Rewrite Review)').
Tests updated to match:
- session-manager.test.js: restore-message expectation already set to
'(Rewrite Review)' — now passes.
- session-switcher.test.js: formatSessionPhaseLabel expectations updated
from short abbreviated values to full canonical labels ('Rewrite Review',
'Layout Review').
1108/1108 JS tests pass.
On the first analyzeJob() call, append a system message informing the user that submitted content is sent to the configured LLM provider. The flag is persisted in localStorage (StorageKeys.LLM_DISCLOSURE_SHOWN) so the message shows only once per browser profile. Changes: - api-client.js: add LLM_DISCLOSURE_SHOWN key to StorageKeys. - job-analysis.js: import StorageKeys; check+set flag at top of analyzeJob() before any fetch. - tests/js/job-analysis.test.js: two new tests — shows disclosure on first call; suppresses it when flag already set. - tests/js/api-client.test.js: update key-count assertion 5 → 6 and add assertion for new LLM_DISCLOSURE_SHOWN key. 1111/1111 JS tests pass.
…ter_data_routes._save_master The _save_master helper in master_data_routes.py now mirrors the behaviour already present in web_app.py: 1. Creates a timestamped backup before writing (unchanged). 2. Calls validate_master_data() on the in-memory dict immediately after the write. 3. If validation fails, restores the backup and raises ValueError so the calling route can surface the error rather than silently persisting invalid data. 91 Python master-data tests pass.
session-switcher-ui.js:
- Session row button text: 'Delete' → 'Move to Trash'
- Button title attribute: 'Delete session' → 'Move session to Trash'
- Saved-sessions section note: 'rename, or delete saved work' →
'rename, or move saved work to Trash'
Permanent-deletion labels in the Trash view ('Delete Forever',
'Permanently delete…') are intentionally unchanged — they accurately
describe an irreversible action on already-trashed items.
1111/1111 JS tests pass.
…trol layout-instruction.js: - Add `pxToPt(px)` helper (exported): converts CSS px to typographic pt using 96 px/in × 72 pt/in convention (1px = 0.75pt), rounded to 1dp - Rename label from 'Base font size (px):' to 'Base font size:' — unit now appears inline in the adjacent pt-display span - Add `<span id="font-size-pt-display">` next to the input showing the current value as 'Npx (M.m pt)' - Wire `input` event on base-font-size-input to update the span live - On session-state restore, update the pt span alongside the input value - Default display: 13 px (9.8 pt) tests/js/layout-instruction.test.js: - Import pxToPt - Add 5-case `pxToPt` describe block covering default value, round numbers, minimum, and rounding-to-1dp behaviour 1116/1116 JS tests pass.
master_data_routes.py (cover_letter_generate): - Prompt instruction changed from '~300–400 words' to '~250–300 words' The front-end validation range (250–400) is intentionally left unchanged in this change — see GAP-HM-04 for planned role-differentiated validation. 40/40 cover-letter Python tests pass.
cv-builder.code-workspace: - Add ../linkedown as a new sibling-repo workspace folder entry
| p.write_text(json.dumps(skeleton, indent=2), encoding="utf-8") | ||
| except OSError as exc: | ||
| logger.exception("Failed to create skeleton Master_CV_Data.json") | ||
| return jsonify({"ok": False, "error": str(exc)}), 500 |
| resolved = candidate.resolve() | ||
| resolved.relative_to(resolved_root) | ||
| return resolved | ||
| candidate_resolved = candidate.resolve() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes 18 UI/UX gap items from the
session_20260420_ui-gap-reviewOBO session.Changes
Frontend
a5fc40a)fd7c2a4)12462bf)ad9edf0,f501e1e)showAlertModalfromui-core.js(01c10c9)732a431)76bee01)126747b)5839b72)e8e0ee3)ceec3bd)Backend
certificationstoGET /api/master-data/full(345bd66)validation_resultstometadata.jsonat compute time (065a88e)_save_master(e95e7ba)e0212e3)Chore
3e0bae8)linkedownworkspace folder (a5d47c1)Test Results