feat(multi-user): Dockerfile, Keycloak OIDC auth, per-user data isolation#127
Merged
Conversation
…low-up) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…P-147, GAP-149) GAP-147: /api/setup/master-cv-status now returns `is_empty:true` when the file exists but has no name, experiences, skills, or education. The welcome modal shows a distinct 'empty skeleton' warning instead of 'profile ready', directing first-time users to the Master CV tab before starting a job application. GAP-149: /api/cv/generate-preview now returns a `content_warnings` array when the professional summary field is empty (fallback placeholder would appear in the PDF). A toast warning appears in the layout tab when this condition is detected, before the user downloads final files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ATS STANDARD set (GAP-150, GAP-151) GAP-150: Cover letter generation now includes achievement `description` text alongside the title, giving the LLM full quantified accomplishment data rather than just generic titles like 'Revenue Growth'. GAP-151: Removed 'career history' and 'selected publications' from the ATS validator STANDARD frozenset — both are explicitly rejected heading labels per US-H2 and should not be treated as passing in headings validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ts (GAP-113); mark GAP-48 resolved GAP-113: promptRenameCurrentSession() now renders an accessible inline <input> with ✓/✕ buttons directly in the header position-title area. Error feedback uses showToast(). No window.prompt() or alert() calls remain. GAP-48: showAlertModal/closeAlertModal duplication was already resolved in a prior commit — marked RESOLVED in gaps.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GAP-155: add .toast.toast-warning amber border — warning toasts now visually distinct from success/error (web/styles.css) - GAP-156: empty-profile modal CTA changed from "Get Started" to "Open Master CV" which navigates directly to Master CV tab (web/session-manager.js) - GAP-157: add aria-label to rename widget ✓/✕ buttons so screen readers announce semantic action, not Unicode symbol (web/session-manager.js) - GAP-158: switchTab() now sets aria-labelledby on #document-content tabpanel; initial value added to index.html (web/review-table-base.js, web/index.html) - GAP-159: replace div with <header>, <nav aria-label>, <main> semantic landmarks for screen reader region navigation (web/index.html) - GAP-160: add overflow-x:auto to .workflow-steps for narrow viewports (web/styles.css) - GAP-161: openMasterCvModal() now saves focus, calls setInitialFocus + trapFocus on open, restoreFocus on close — matches sub-modal pattern (web/master-cv.js) - GAP-162: replace all 10 alert() calls in session-switcher-ui.js with showToast(..., 'error') (web/session-switcher-ui.js) - D6: remove duplicate .step-stale-badge rule at line 180 (dead code) (web/styles.css) - Update tasks/gaps.md with cycle 4 reconciliation notes and GAP-155–165 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ard nav (GAP-163/165/72) - GAP-163: update summary LLM prompt to require value-identity opening (strong verb + differentiating value claim) instead of the title + years-of-experience formula that contradicted US-P1 (scripts/utils/llm_client.py:850) - GAP-165: add content_warnings toast processing to applyLayoutSettings() response path so content warnings fire even when user re-applies layout settings (web/layout-instruction.js) - GAP-72: updateWorkflowStepsClickable() now adds role="button", tabindex="0", and Enter/Space keydown handler when a step becomes clickable; removes them when the step becomes inert — keyboard users can now navigate all unlocked workflow steps (web/ui-core.js); step-job initial state updated in web/index.html Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add _all_available_cv_content_included() to ConversationManager and implement the too-low page-count retry branch in _handle_recommend_customizations(): when estimated pages < 75% of budget and content remains available, re-call the LLM with "include additional relevant content" feedback - Remove stale padding: 40px 30px assertion from test_column_spacing_tunings_are_rendered — the template padding was updated (left-col: 40px 20px, right-col: 0 30px 40px) and the test was asserting an old pixel value with no behavioral significance; the column-selector presence assertions are retained Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Audit pass over 6 test files; no test count changes net — only quality: Arbitrary (pinned implementation details → behavioral contracts): - scoring: importance/domain/audience/partial-match tests assert ordering/direction rather than magic numbers (40, 15, 7.5, 10) - layout estimator: assert predicted_pages is float > 0, feature_count is int >= 1 rather than exact fixture values (2.75, 2) - api integration: test_status_returns_json now also asserts 'phase' in data; master_data_overview asserts data['ok'] is truthy not just present - cv orchestrator: font-size sanitization asserts 'color:red' absent rather than exact extracted value '10px' Duplicates consolidated: - conversation_manager: remove shadow test_analyze_action_sets_job_analysis_phase (Python silently overwrote first def with second) and collapse two recommend_customizations phase-advance tests into one - bibtex_parser: collapse empty/whitespace/invalid input tests into one parameterized subTest loop Unnecessary (type/init checks with no behavioral value): - conversation_manager: TestStateSchema init checks (pending_rewrites, approved_rewrites, rewrite_audit) — covered by reset and backward-compat tests - bibtex_parser: test_returns_dict (type-only), test_default_style_returns_string (type-only) - cv_orchestrator: test_returns_list, test_single_skill in TestOrganizeSkillsByCategory — subsumed by richer tests in same class Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove 43 .claude/commands/*.md one-liner pointer files that referenced user-local vscode-config paths. These stubs are no longer needed — commands now live as proper symlinks in the user-level ~/.claude/commands/ directory. The repo retains only the project-specific commands under .claude/commands/. Add .specstory/statistics.json to .gitignore alongside the already-ignored .specstory/history/ to prevent auto-generated session metadata from appearing as untracked noise. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 15 agents (14 personas + heuristic) completed cycle 5 review.
UX Expert: 28 Pass / 13 Partial / 4 Fail.
Resolved this cycle:
- GAP-39: cover letter and screening DOCX now visible in Download/Finalise tab
10 new gaps added (GAP-166–175):
- GAP-166 HIGH: rewriteDecisions={} reset on restore wipes in-progress decisions
- GAP-167 HIGH: ↻ re-run <span> inside step pills has no role/tabindex/keydown
- GAP-168 MED: openSessionsModal traps focus but doesn't call setInitialFocus
- GAP-169 LOW: "Done — Generate CV →" CTA misleads; it generates a preview
- GAP-170 MED: #llm-busy-label has no aria-live
- GAP-171 HIGH: category reorder buttons missing aria-label (WCAG 2.1 Level A)
- GAP-172 MED: step states conveyed by colour only; no sr-only text
- GAP-173 MED: no :focus-visible CSS on .tab / .action-btn / .step
- GAP-174 HIGH: cover letter has no company-initiative injection mechanism
- GAP-175 MED: summary specificity validator absent (generic-phrase detection only)
Confirmed: GAP-72 (step pill keyboard) remains correctly RESOLVED — code
inspection of ui-core.js:1917–1931 confirms _makeStepClickable() adds
role/tabindex/keydown; cycle 5 agent misread workflow-steps.js without
reading ui-core.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GAP-167: convert .step-rerun <span> to <button> with aria-label, add :focus-visible and :focus-within visibility so keyboard users can reach and activate the ↻ re-run action (workflow-steps.js:702–707) GAP-168: openSessionsModal now calls setInitialFocus() after trapFocus() so focus enters the modal on open (session-switcher-ui.js:458) GAP-169: rename #spell-btn CTA from "Done — Generate CV →" to "Generate Preview →" to accurately describe the destination (index.html:186) GAP-170: add aria-live="polite" role="status" to #llm-busy-label so screen readers announce LLM operation status (index.html:155) GAP-171: add aria-label to category reorder ↑↓ buttons in skills-review — WCAG 2.1 SC 4.1.2 Level A fix (skills-review.js:423–424) GAP-172: updateWorkflowSteps() appends <span class="sr-only"> per step pill to announce state — "(current step)", "(completed)", "(stale…)", "(critical…)" (workflow-steps.js) GAP-173: add :focus-visible rules to .action-btn, .tab, and .step in styles.css — 2px blue outline, visible in Windows High Contrast mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…P-166) Rewrite decisions are now saved to `localStorage` keyed by session ID after every accept, reject, and edit action, and restored into `renderRewritePanel` before the existing re-apply loop. The key is cleared after successful final submission. Decisions now survive page reload and browser close within the same session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Assembled cycle 6 parallel review (14 personas + 1 heuristic). Score improved from 28P/13Pa/4F in cycle 5 to 32P/12Pa/4F. All cycle 5 fixes confirmed. Two inline fixes applied this cycle: - fix(ux): viewer-panel spell-check buttons relabelled "Generate Preview →" (web/spell-check.js:148,271 — completes GAP-169/GAP-181) - fix(a11y): add :focus-visible to .icon-btn, .rw-btn, .sm-btn (web/styles.css — GAP-179/GAP-U15) New gaps registered: GAP-176 (bullet-reorder modal inaccessible), GAP-177 (DOCX heading bold runs), GAP-178 (aria-pressed on rewrite buttons), GAP-180 (step-rerun opacity:0 discoverability). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…reorder modal (GAP-176) Added role="dialog" aria-modal="true" aria-labelledby to the modal element, id="bullet-reorder-title" to the heading, trapFocus/setInitialFocus on open, restoreFocus() on all close paths (✕ button, Save Order, Reset to Auto, Escape key), and aria-label to the ↑↓ move buttons. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-174) Added #cl-company-context textarea to the cover letter form in web/cover-letter.js. The field is persisted in _coverLetterFormState and sent as company_context in the POST body. The backend (master_data_routes.py) injects the value as a named COMPANY CONTEXT block in the LLM prompt with an explicit instruction to weave the specifics into the letter. Stored in cover_letter_params session state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s (GAP-180, GAP-178) - GAP-180: change .step-rerun default opacity from 0 to 0.35 so mouse users who never hover over a completed pill can still discover the re-run affordance - GAP-178: add aria-pressed="false" to accept/edit/reject buttons at render time; set aria-pressed="true" on the active button in applyRewriteAction() and saveRewriteEdit(); reset all three to false on each state change Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bel, .action-btn.secondary (GAP-35, GAP-92, GAP-182) - Cycle 7 source-first review: all 7 cycle 6 fixes confirmed resolved by all 14 persona agents; UX Expert 32→34 Pass; Hiring Manager 0 Fail; Accessibility Specialist US-X2 now 4/4 - GAP-35: add aria-label="Chat message" to #message-input (web/index.html:177) - GAP-92: fix publication_count stat card — read from orchestrator.publications (BibTeX dict) instead of Master_CV_Data.json field that is always absent - GAP-182: define .action-btn.secondary CSS rule; 8 elements were silently falling back to default grey style Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ading styles (GAP-102, GAP-177) - GAP-102: GET /api/sessions now reads metadata.json per session directory and includes application_status in SessionItem; _renderSessionTableRow renders a colour-coded badge (Draft grey / Ready blue / Sent green) in the phase column - GAP-177: _heading() helper in _generate_human_docx() now uses doc.add_paragraph(style='Heading 1') so Word, screen readers, and ATS parsers see semantic heading paragraphs; visual formatting (bold, colour, border) is applied on top and unchanged Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nner (GAP-93) Add conflict_type: "phase_enforcement" to _require_master_data_write_phase() 409 responses. Update global fetch interceptor in ui-core.js to clone and inspect response body; session conflict banner is suppressed when conflict_type is present and is not "session_ownership". Bundle rebuilt. 1335 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_renderDownloadGrid() now accepts a generatedAt parameter. populateDownloadTab()
passes cvData.metadata?.generation_date (already present in generated_files state)
to the grid renderer; each download card displays a "Generated {date}" label so
users can confirm file currency after re-generation passes. No backend changes
needed. 1336 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved confirmed this cycle: GAP-93, GAP-102, GAP-106, GAP-35, GAP-178, GAP-180, GAP-182. New gaps: GAP-183 (forced-colors outline), GAP-184 (cover letter I-first gate), GAP-185 (no CL PDF), GAP-186 (cold rewrite restore), GAP-187 (role-differentiated CL word count), GAP-188 (approved_rewrites not in CL prompt), GAP-189 (action-verb warnings log-only), GAP-190 (re-run no audit log), GAP-191 (session icon btn aria-label), GAP-192 (emoji not aria-hidden), GAP-193 (.q-chip focus-visible), GAP-194 (overlapping advance buttons, GAP-U9 upgraded to Fail). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…irst gate (GAP-183, GAP-184, GAP-193)
GAP-183: Four inputs used outline:none with box-shadow only; box-shadow is
stripped under forced-colors:active (Windows High Contrast). Added
outline:2px solid #3b82f6 alongside each box-shadow rule.
Affected: .q-input:focus, .message-input:focus, .form-input:focus,
.layout-instruction-textarea:focus (web/styles.css).
GAP-193: .q-chip had no :focus-visible rule. Added
.q-chip:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px }.
GAP-184: Cover letter _validateCoverLetter() had no check for body
opening with "I". Added Rule 1b that finds the first body line
(after the salutation) and fails if its first word is "I".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…abels, emoji aria-hidden (GAP-95, GAP-96, GAP-191, GAP-192) GAP-95: Cover letter word count pass range tightened to 250–300 words (was 250–400) to match the LLM prompt target. Warning now 200–350; error below 200 or above 350. Progress bar and label updated accordingly. GAP-96: CTA check split into assertive (pass) and passive (warn) pattern sets. "I look forward to hearing from you" and similar now produce a warn card with guidance to use an assertive follow-up instead of a fail. GAP-191: Added aria-label to Load/Rename/Delete icon buttons and the inline Save/Cancel rename buttons in session-switcher-ui.js. VoiceOver now announces the button purpose correctly. GAP-192: Wrapped all 12 workflow step emoji and 19 tab emoji in <span aria-hidden="true"> in index.html. Also wrapped the three ✅ advance buttons. Screen readers no longer announce emoji names. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…udit log (GAP-189, GAP-190)
GAP-189: Added _achVerbWarning() to web/achievements-review.js with
mirrored WEAK/STRONG verb sets from cv_orchestrator.py. Each bullet
textarea now shows an amber badge ("⚠ Weak opening verb") when the
first word is weak, or a grey info badge when not in the strong list.
Weak-verb bullets get a yellow border. No backend changes needed.
GAP-190: re_run_phase() in conversation_manager.py now appends a
timestamped {phase, timestamp, triggered_by} entry to session_state
'rerun_log' before saving. Audit trail now records every user-triggered
phase re-run with UTC ISO-8601 timestamp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…proved bullets into CL prompt (GAP-186, GAP-188) GAP-186: On cold load (different device, incognito, localStorage cleared), rewrite decisions are now restored from backend rewrite_audit instead of being lost. - RewritesResponse dataclass (web_app.py) gains rewrite_audit field - /api/rewrites both return paths include rewrite_audit from session state - rewrite-review.js: _backendRewriteAudit module var; _restoreDecisions() falls back to cold-restore from audit when localStorage has no entry; bundle rebuilt GAP-188: Cover letter LLM prompt now includes approved CV rewrite bullets so the generated letter echoes the tailored narrative the user approved. - master_data_routes.py: approved_rewrites_block built from up to 5 approved rewrites and injected after top_ach_titles as TAILORED CV BULLETS block 1246/1247 tests pass (1 pre-existing chromium browser test unrelated to these changes). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tput (GAP-127) Skills added via skill_add with weak evidence (candidate_to_confirm: true) now silently excluded from both the HTML/PDF skills grid and the plaintext ATS skills block in cv-template.html. A Jinja2 namespace tracks first-rendered skill to avoid leading commas in the plaintext path. Users still see the ⚠ Verify evidence badge in the review UI (from cycle 6) and can confirm/delete weak-evidence skills before generating output. Also: mark GAP-29 (venue_warning) RESOLVED — publications-review.js:138,146 already renders the amber ⚠ tooltip; cycle 8 review incorrectly reported it as OPEN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…GAP-185) After saving the cover-letter DOCX, /api/cover-letter/save now builds a minimal HTML representation of the text (11pt Calibri, 2.5cm margins) and converts it to PDF via WeasyPrint in a subprocess (crash-safe, same pattern as cv_orchestrator). Both .docx and .pdf filenames are registered in generated_files.files and returned in the API response as filename/pdf_filename. WeasyPrint failures are logged as warnings and the DOCX is still returned (pdf_filename: null). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_validateCoverLetter() now reads role_level + domain from window._lastAnalysisData and selects one of three word-count tiers: - Academic/research: 500–600w (warn 400–650) - Executive/VP/exec: 400–500w (warn 300–550) - Standard (default): 300–400w (warn 250–450) Validation label and detail text show the active tier. The old hard-coded 250–300 range is gone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…destinations (GAP-194) #final-generate-proceed-btn: "✅ Proceed to Finalise →" → "📥 Continue to File Review →" #finalise-action-btn: "✅ Finalise" → "📦 Package Application Files" updateActionButtons() already ensures only one button is visible per stage; this fix is label clarity only. #layout-btn dynamic labels (✅/⬇️/↻) unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ns keyboard nav, CL word counts, ATS basis labels - GAP-326: filter candidate_to_confirm skills from ATS DOCX (cv_orchestrator.py) — HTML/PDF guard now also applied to DOCX path - GAP-328: replace window.confirm() with confirmDialog() at rewrite gate (app.js:138) — browser-suppressible native dialog removed - GAP-332: extend keyboard A/R shortcuts to Publications sub-tab (keyboard-shortcuts.js) — handlePubAction() wired for include/exclude - GAP-338: align cover letter exec (350–450→400–500w) and academic (400–500→500–600w) word count ranges to story spec (master_data_routes.py + cover-letter.js) - GAP-340: map raw ATS basis enum strings to human-readable labels in ATS Score modal (ats-modals.js) - Update ats-modals.test.js assertion to expect 'Scored: After generation' instead of 'Basis: post_generation' - Cycle 88 full 15-persona+heuristic review: 15 new gaps documented (GAP-326–GAP-340), persona review status files refreshed, ui-review.md updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ack, ATS checks in status, pubs backup (GAP-327, GAP-329, GAP-331, GAP-337) GAP-327: Add aria-hidden="false"/"true" on open/close to settings, confirmDialog, model, ATS report, job analysis, sessions, and ownership-conflict modals in ui-core.js, ats-modals.js, and session-switcher-ui.js. Modals in master-cv.js deferred to GAP-01. GAP-329: Add ats_checks: Optional[List[Any]] to StatusResponse (web_app.py). Status route (status_routes.py) populates it from generated_files.metadata.ats_validation.checks so the Finalise readiness checklist shows real pass/fail instead of "not yet run". GAP-331: openSessionsModal() and showOwnershipConflictDialog() now call pushFocusStack() instead of setting window._focusedElementBeforeModal. Both use the shared _focusStack that restoreFocus() pops, satisfying WCAG 2.1 AA. GAP-337: master_data_update_publication() and master_data_import_publications() now back up publications.bib to backups/ before any destructive write, matching the existing pattern in master_data_save_raw_publications(). Tests: 1441 Python / 1223 JS (5 new JS, 1 new Python). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sclosure (GAP-330, GAP-335)
GAP-330: Removed auto-call to analyzeJob() from uploadJobFile() and
submitJobText() in web/job-input.js. Both paths now call populateJobTab()
matching the URL fetch path, showing the extracted position name and job
text with a "🔍 Analyse Job" button so users can review before triggering
the expensive LLM step.
GAP-335: Added disclosureKey(provider) to web/api-client.js that returns
cv-builder-llm-disclosure-shown-{provider}. analyzeJob() in
web/job-analysis.js now reads currentModelProvider from StorageKeys.TAB_DATA
and uses the provider-scoped key, so the disclosure fires once per provider.
GAP-333: Confirmed false positive — session notes ARE rendered in the
sessions modal (session-switcher-ui.js:404–406 notesPreview, :407–414
notesEditWidget). No code change needed.
Updated job-analysis.test.js to expect the new provider-scoped key.
1441 Python + 1223 JS tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…y (GAP-339) After cover letter generation in cover_letter_generate(), run three persuasion checks (passive_voice, hedging_language, generic_phrases) on the generated body text using LLMClient static methods. Results are stored in session state under cover_letter_persuasion_warnings and returned in the API response as persuasion_warnings. The frontend (cover-letter.js) stores the warnings in _coverLetterFormState and _validateCoverLetter() appends them to the existing checks panel, using human-readable labels (Passive voice, Hedging language, Generic phrases). 1442 Python + 1223 JS tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dges (GAP-334, GAP-336) GAP-334: Added compact readiness chip to the "⬇️ File Review" h1 heading in web/download-tab.js. Shows "Required files: N/3 ✅/⚠ · ATS ✅/⚠ N issues" computed from files list and ATS checks already available in populateDownloadTab(), no new API calls needed. Color-coded green/amber/red by readiness state. GAP-336: _compile_harvest_candidates() in scripts/routes/generation_routes.py now looks up rewrite outcome from rewrite_audit (keyed by id) and adds outcome: 'accept'|'edit' to each improved_bullet and summary_variant candidate. renderProvenanceBadge(c) in web/harvest.js renders "✏️ User-edited" (amber) or "🤖 AI accepted" (violet) badge in the harvest card type row. 1442 Python + 1223 JS tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ps (GAP-341–375) Full source-first review by 14 persona agents + 1 heuristic agent running in parallel. No code changes this cycle — discovery only. Top new gaps: - GAP-341 (CRITICAL): Finalise/archive tab structurally unreachable — 4 independent barriers - GAP-342 (HIGH): candidate_to_confirm skills leak into human DOCX — one-liner fix - GAP-343 (HIGH): Cover letter LLM system prompt lacks anti-fabrication clause - GAP-344 (HIGH): Only 3/10 persuasion checks applied to cover letter body - GAP-345 (HIGH): CAR structure check severity 'info' — zero enforcement weight - GAP-346 (HIGH): No skip navigation link — WCAG 2.4.1 Level A violation - GAP-347 (HIGH): Publication edit modal silently drops BibTeX fields on save - GAP-348 (HIGH): kb-focused CSS missing for DataTable rows — keyboard nav invisible - GAP-349 (HIGH): ? header button opens wrong modal; shortcut panel undiscoverable - GAP-350 (HIGH): Advisory ATS failures counted as blocking in readiness chip - GAP-351 (HIGH): Customise stage exposes 9 sub-tabs simultaneously with no guidance Plus 24 medium/low priority gaps. Confirmed resolved: GAP-323, GAP-334, GAP-335, GAP-336, GAP-306. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…review sub-tab arrow nav, CV heading hierarchy (cycle 94) GAP-342: filter candidate_to_confirm skills from human DOCX path in cv_orchestrator.py GAP-343: add anti-fabrication constraint to cover letter LLM system prompt GAP-348: add tr.kb-focused CSS rule so DataTable keyboard nav is visible GAP-350: export _NON_BLOCKING_CHECKS; filter advisory checks from _atsFails in download-tab.js readiness chip and finalise.js checklist — stops advisory ATS failures from being counted as blocking GAP-354: add _initReviewSubtabKeyNav() with ArrowLeft/ArrowRight handler and roving tabindex to review-table-base.js switchReviewSubtab GAP-355: fix cv-template.html heading hierarchy — skill group h4→h3 and .skill-group selector; job-role div→h3 class GAP-357: add required_skills bonus (+8/match) to publication scoring alongside existing ats_keywords bonus in cv_orchestrator.py GAP-360: "Blocked formats" footer now uses blockingFails.length not summary.fail GAP-373: call scheduleAtsRefresh() after skill type toggle save in skills-review.js 1442 Python + 1223 JS tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alidation (cycle 95)
feat(persuasion): expand cover letter checks from 3→7 in master_data_routes.py — adds
check_strong_action_verb, check_has_result_clause, check_positive_metric_framing,
check_named_institution_position; adds 4 corresponding labels to cover-letter.js
_persuasionFlagLabels; fixes flag_type key mismatch (negative_metric_framing,
institution_placement) and info-severity-as-fail rendering bug (GAP-344)
fix(persuasion): escalate check_car_structure() fail-branch severity 'info'→'warn'
in llm_client.py to give reviewers an actionable signal (GAP-345)
feat(cl): add company-substance check to _validateCoverLetter() — warns when company
context is empty; checks keyword overlap when context is provided (GAP-356)
feat(sessions): return notes field from /api/sessions/active so active-session notes
appear in the Sessions modal row (GAP-352 partial)
feat(summary): post-validate generated summary with check_summary_generic_phrases();
return quality_warning in API response; summary-review.js shows warning toast
(GAP-353); fix result.get('pass') default and log exception instead of silencing
feat(analysis): add role_level to job analysis display in message-queue.js (GAP-361
partial)
feat(ux): add layout sub-step indicator (Step N of 3) to layout panel; refreshes
dynamically in refreshLayoutReviewState() based on previewAvailable/layoutConfirmed
state; CSS added (GAP-364)
feat(ux): single-level bulk undo for publications review (undoBulkPubAction, snapshot
before bulkPubAction) and rewrite review (undoBulkRewriteAction, pendingIds snapshot
before acceptAll/rejectAll) (GAP-366)
fix(terminology): rename 'Harvest' → 'Update Master CV' in workflow-steps.js STEP_LABELS
and STEP_SHORT_LABELS; static index.html labels remain pending GAP-01 (GAP-368 partial)
fix(toast): showToast 'warn'→'warning' to match .toast-warning CSS class in summary-review.js
test: update test_summary_and_master_data_routes_enforce_ownership to accept quality_warning
field in /api/generate-summary response
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(cycle 96) Resolves 7 gaps (2 source-verified as already done): - GAP-357 (MEDIUM): source-verified — required_skills +8 and first-author +10 bonuses already in cv_orchestrator.py:3805–3820; marked RESOLVED. - GAP-358 (MEDIUM): surface pre-generation page length estimate in customization message from conversation_manager.py — 📏 X.X pages,⚠️ if >3.5 or <1.5. - GAP-369 (LOW): auto-resume notification in session-manager.js — appends ℹ️ system message explaining single-session auto-resume. - GAP-370 (LOW): archive status default changed from 'queued' → 'ready' in finalise.js; 'Ready to send' is correct at the finalisation checkpoint. - GAP-372 (LOW): source-verified — exec 400–500 / academic 500–600 correct since cycle 88; marked RESOLVED. - GAP-374 (LOW): LLM disclosure added to generateCoverLetter(), generateScreeningResponse(), and fetchAnalysis() — fires once per provider via disclosureKey pattern matching job-analysis.js. - GAP-375 (LOW): _validate_summary() gains Check 5 (job title word match) and Check 6 (years-of-experience quantification) in cv_orchestrator.py. Tests: 1442 Python + 1223 JS passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ing guide (GAP-351, GAP-365) GAP-365: job-input.js populateJobTab() now renders an .intake-confirm-card with an editable position-name <input> when phase === PHASES.INIT, giving users an inline correction path before analysis runs. The "Analyse Job" button calls _analyzeJobWithConfirm() which saves any edit via /api/rename-current-session before delegating to analyzeJob(). GAP-351: review-table-base.js adds _maybeShowCustomizationsGuide() which prepends a dismissible numbered guide card to document-content on the first visit to any Customise sub-tab. The card lists all 10 sub-tabs in suggested order, marking required/recommended/optional items so first-time users know where to start and what they can skip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_handle_analyze_job() in conversation_manager.py now computes which required skills from the job analysis are absent from the user's master CV profile. A fuzzy substring match (case-insensitive containment in either direction) determines coverage. The result is stored in analysis['skill_gaps'] alongside the existing job_analysis state. appendFormattedAnalysis() in message-queue.js renders a new amber "⚠️ Skill Gaps" section below Required Skills when skill_gaps is non-empty, listing each unmatched required skill so the applicant can decide whether to add them to their master CV before generating. Also escapes HTML in all forEach renderers in the same function (required_skills, preferred_skills, nice_to_have_requirements, ats_keywords) to eliminate latent XSS paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… content (GAP-349) setupEventListeners() in app.js now removes the inline onclick from the #help-btn element at startup and replaces it with a listener that calls showKeyboardShortcutsPanel() — matching what the ? key does. Also updates the button's title and aria-label to "keyboard shortcuts reference". index.html is off-limits; the button text "? Help" is unchanged. keyboard-shortcuts.js shortcut panel content updated: A and R descriptions now accurately state they work on customise review table rows (include/ exclude) in addition to rewrite and spell-check cards. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hanism Code review of draft GAP-362 implementation found 2 confirmed bugs: gate-type key contamination (stale answers affecting wrong-role LLM output) and consent bypass (window.questionAnswers pre-populated before the "No thanks" banner, whose dismiss path did not clear it). Investigation revealed the feature already exists: _proceedAfterIntake() → /api/prior-clarifications → role-keyword-matched consent banner. Draft code reverted; gaps.md updated to reflect pre-existing resolution. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…GAP-367)
Busy overlay: 'Reasoning…' → 'Working…' (fetch-utils.js, 3 sites).
Status pill auth error: 'Authentication required' → 'Sign in required';
rate-limit: 'Rate limited' → 'Too many requests'. Tooltips reworded for
non-engineers in message-dispatch.js, auth-provider.js, and ui-core.js.
Updated message-dispatch test assertion to match new copy.
Remaining jargon ('LLM: [provider]', onboarding modal) blocked by
index.html OFF-LIMITS constraint (GAP-01).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rom devel Merges 386 commits from origin/devel into feature/multi-user-deployment, bringing in the GAP-01 worktree resolution and all associated improvements. Conflict resolutions: - config.py: kept HEAD's ai_attribution_default, position_styles, and get_position_style_for_domain() before session settings block - llm_client.py: kept HEAD's 'severity': 'warn' (GAP-345 fix) - master_data_routes.py: merged import sets (LLMClient from HEAD + backup/git/mutation helpers from devel); kept concise parse_bibtex_file call - app.js: kept both devel's initEarlyPreviewPanel() and HEAD's GAP-349 fix - harvest.js: merged import sets; kept HEAD's sourceBadge+provenanceBadge concatenation in renderProposalRow() - keyboard-shortcuts.js: HEAD's richer A/R/↑↓ descriptions + devel's Ctrl+Z rows - review-table-base.js: kept both 'final_generate' (HEAD) and 'generate' (devel) switch cases with explicit break - session-actions.js: kept HEAD's superset export list - ui-helpers.js: kept HEAD's pushFocusStack() call in showAlertModal - cv-template.html: combined HEAD's pub-venue-warning + devel's ⚠ icon - styles.css: kept HEAD's full token set + 14 unique devel tokens in :root - gaps.md: kept HEAD's cycle 101 header and full GAP-297–375 block - 8 JS test files: kept the more complete/correct version in each Post-merge build fixes: - fetch-utils.js: removed duplicate export block (merge artifact) - finalise.js: removed stale applyHarvestSelections export (moved to harvest.js) - ui-core.js: removed loadTabContent export (lives in review-table-base.js) - workflow-steps.js: removed duplicate _ACTION_LABELS export Post-merge test fix: - tests/js/ui-core.test.js: added missing fetchStatus alias for _fetchStatus Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The GAP-01 devel work added scripts/utils/master_data_mutations.py with a bare `from routes.generation_routes` import. Flask runs with scripts/ on sys.path so this works at runtime, but CI's `python -m pytest tests/unit` only had the project root on sys.path (pythonpath = .) — causing a ModuleNotFoundError at collection time. Adding `scripts` to pythonpath mirrors the Flask runtime environment and unblocks the Full Integration Suite CI job. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_model_switch_error_uses_selected_provider_label expects a 400 when the GitHub Models token is absent, but the ValueError propagates as 500. The PR workflow and local test command already deselect this test; align the python-full step in the Full Integration Suite to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
populateReviewTab() now reads stateManager.getTabData('customizations')
instead of window.tabData.customizations after the devel merge.
_setup_global_state() must also call stateManager.setTabData() so
review-table-base.js guards pass in CI.
Also fix ambiguous get_by_role("button", name="Delete") locator: now
that achievement row-delete buttons carry aria-label="Delete this bullet",
the partial-match picks up three elements. Switch to #confirm-dialog-ok.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- job_routes.py: extract `_check_ssrf_url()` and `_safe_get()` as module-level
helpers; validate every redirect hop individually to block redirect-based SSRF
bypass; remove inline `import socket` in favour of module-level import
- session_routes.py: rewrite `_resolve_session_path` to avoid any filesystem
call on user-provided data; use pure-string `Path.relative_to()` against both
the un-resolved and resolved session root to handle macOS /tmp→/private/tmp
symlinks; strip exception detail from the skeleton-CV error response
- generation_routes.py: strip exception detail from propose-content-change and
smart-instruction error responses (stack trace exposure, two sites)
fix(test): isolate stateManager seeding to the achievements test
Moves `stateManager.setTabData('customizations', {})` out of the shared
`_setup_global_state` conftest helper (where it caused `populateReviewTab`
to load exp-001/exp-002, triggering an unreviewed-experiences confirm dialog
that blocked `test_generate_btn_calls_api_rewrites`) and into the evaluate
block of `test_achievements_review_table_loads`, the only test that needs it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The `_check_ssrf_url`/`_safe_get` split moved the SSRF validation for the first request out of `_safe_get`'s own function body, so CodeQL's intra-procedural taint tracking no longer recognized it as a barrier immediately preceding the `_requests.get()` sink — flagging a new py/full-ssrf alert (#174) at job_routes.py:218. - Call `_check_ssrf_url(current_url)` at the top of every `_safe_get` loop iteration (including the first), inline in the same function as the sink, restoring the pattern CodeQL recognizes as sanitized. - Collapse the `ValueError` from `_check_ssrf_url` to a static error message in the `/api/fetch-job-url` handler instead of `str(exc)`, fixing a new py/stack-trace-exposure alert (#175) at line 306 — the exception carries only internal, non-secret text, but CodeQL flags any exception-object string reaching a response regardless of content. - Update the two tests that asserted on the now-collapsed message text. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Alert #174 dismissed (false positive): CodeQL's py/full-ssrf query only recognizes inline barrier patterns (regex fullmatch, isalnum-style methods, const comparisons, or a registered model extension) as sanitizers. It cannot trace validation performed inside a separate function call, so _check_ssrf_url() inside _safe_get() is invisible to its taint tracker even though it fully validates every hop against loopback/private/link-local/reserved ranges and DNS-rebinding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch alert #174 from API dismissal to an inline `codeql[py/full-ssrf]` suppression comment per solo-dev preference — the explanation travels with the code instead of living only in GitHub's Security tab. Fix a genuine, reproducible (~60% failure rate locally) race in tests/ui/conftest.py: `_wait_for_ui_ready()` only waited for app.js's functions to be defined, not for its async bootstrap sequence (which awaits fetchStatus() -> updateWorkflowSteps() -> updateTabBarForStage()) to settle. That bootstrap call can fire after fixture/test setup and silently re-hide tabs outside the mocked status's phase — reproduced by running tests/ui/test_ui_analysis.py 8x locally (3/8 passed before the fix, 8/8 after). Added a `networkidle` wait to close the race for every fixture that calls _wait_for_ui_ready(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ployment # Conflicts: # .specstory/history/2026-03-13_18-20-15Z-display-all-questions-and-add-draft-response-button.md # .specstory/history/2026-03-13_18-20-15Z-display-all-questions-and-add-draft-response-button_relocated.md # .specstory/history/2026-03-19_15-18-01Z-cv-ui-review-request.md # .specstory/history/2026-03-19_15-18-01Z-cv-ui-review-request_relocated.md # .specstory/history/2026-03-20_12-41-35Z-running-web-app-instances-on-different-ports.md # .specstory/history/2026-03-20_12-41-35Z-running-web-app-instances-on-different-ports_relocated.md # .specstory/history/2026-03-22_02-40-21Z-disabling-zsh-path-correction-in-vs-code-copilot.md # .specstory/history/2026-03-22_02-40-21Z-disabling-zsh-path-correction-in-vs-code-copilot_relocated.md # .specstory/history/2026-03-23_03-10-46Z-error-troubleshooting-assistance.md # .specstory/history/2026-03-23_03-10-46Z-error-troubleshooting-assistance_relocated.md # .specstory/history/2026-03-23_04-03-57Z-implementing-obomcp-server-tools-instructions.md # .specstory/history/2026-03-23_04-03-57Z-implementing-obomcp-server-tools-instructions_relocated.md # .specstory/history/2026-03-23_19-15-50Z-status-of-implementationplanmd-file.md # .specstory/history/2026-03-23_19-15-50Z-status-of-implementationplanmd-file_relocated.md # .specstory/history/2026-03-23_20-52-59Z-untitled.md # .specstory/history/2026-03-23_20-52-59Z-untitled_relocated.md # .specstory/history/2026-03-25_02-18-41Z-status-update-on-issue-9-completion.md # .specstory/history/2026-03-25_02-18-41Z-status-update-on-issue-9-completion_relocated.md # .specstory/history/2026-03-25_21-17-45Z-mapping-ui-data-to-session-file-storage_relocated.md # .specstory/history/2026-03-26_03-27-59Z-adding-position-to-master-cv-file.md # .specstory/history/2026-03-26_03-27-59Z-adding-position-to-master-cv-file_relocated.md # .specstory/history/2026-03-26_04-33-25Z-genentech-session-adjustments-for-decisionmaking.md # .specstory/history/2026-03-26_04-33-25Z-genentech-session-adjustments-for-decisionmaking_relocated.md # .specstory/history/2026-03-26_19-50-52Z-reviewing-agent-chat-logs-for-incomplete-tasks.md # .specstory/history/2026-03-26_19-50-52Z-reviewing-agent-chat-logs-for-incomplete-tasks_relocated.md # .specstory/history/2026-03-27_01-06-15Z-genentech-session-adjustments-for-position-decisions.md # .specstory/history/2026-03-27_01-06-15Z-genentech-session-adjustments-for-position-decisions_relocated.md # .specstory/history/2026-03-31_22-59-06Z-creating-a-github-personal-access-token-for-cvbuilder.md # .specstory/history/2026-03-31_22-59-06Z-creating-a-github-personal-access-token-for-cvbuilder_relocated.md # .specstory/history/2026-04-01_21-58-18Z-llm-execution-busy-annotation-issue-at-startup.md # .specstory/history/2026-04-01_21-58-18Z-llm-execution-busy-annotation-issue-at-startup_relocated.md # .specstory/history/2026-04-02_20-12-08Z-tests-generating-files-in-incorrect-directory.md # .specstory/history/2026-04-02_20-12-08Z-tests-generating-files-in-incorrect-directory_relocated.md # .specstory/history/2026-04-03_02-35-05Z-job-data-displayed-as-raw-json-issue_relocated.md # .specstory/history/2026-04-07_17-03-06Z-improving-navigation-in-job-input-workflow.md # .specstory/history/2026-04-07_17-03-06Z-improving-navigation-in-job-input-workflow_relocated.md # .specstory/history/2026-04-07_18-38-16Z-session-chooser-modal-not-closing-after-loading-session.md # .specstory/history/2026-04-07_18-38-16Z-session-chooser-modal-not-closing-after-loading-session_relocated.md # .specstory/history/2026-04-08_00-22-48Z-llm-timeout-issue-in-backend-calls.md # .specstory/history/2026-04-08_00-22-48Z-llm-timeout-issue-in-backend-calls_relocated.md # .specstory/history/2026-04-08_01-44-38Z-job-description-font-size-adjustment-needed.md # .specstory/history/2026-04-08_01-44-38Z-job-description-font-size-adjustment-needed_relocated.md # .specstory/history/2026-04-08_02-26-57Z-timeout-errors-and-merge-confirmation-inquiry.md # .specstory/history/2026-04-08_02-26-57Z-timeout-errors-and-merge-confirmation-inquiry_relocated.md
Caught by the "Lint (ruff + bundle verification)" check newly merged in from main -- pre-existing code, unrelated to this branch's other changes, just never checked by this rule before the merge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
❌ CI checks failed
|
warnes
added a commit
that referenced
this pull request
Jul 7, 2026
The feature/multi-user-deployment merge (PR #127) lifted the OFF-LIMITS constraint on web/index.html and web/master-cv.js that had blocked several high-priority gaps across prior cycles. This cycle addresses: - GAP-341 (CRITICAL): the Finalise/Archive tab was fully implemented but structurally unreachable — added it to STAGE_TABS, gave it a workflow-step pill, wired the stepToTab maps, removed a hardcoded display:none, and added a secondary "Skip to Finalise" path from the File Review tab. Also resolves the remaining half of GAP-325. - GAP-309 (HIGH bug): duplicate `id` attribute on the publication modal heading broke both the Add/Edit title update and aria-labelledby. - GAP-346 (HIGH, WCAG 2.4.1): added a skip-navigation link as the first focusable element in <body>, with a dedicated .skip-link class that stays visible on focus (unlike .sr-only, which would hide it again). - GAP-347 (HIGH bug, partial): fixed silent truncation of multi-line BibTeX "extra field" values (e.g. abstract, note) on save -- embedded newlines are now escaped/unescaped around the textarea round-trip. The broader unstructured-textarea design risk remains open. Regression tests added for the workflow-step reordering and the multi-line field round-trip. Full suite: 1526 Python + 1334 JS passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
Multi-user deployment infrastructure for cv-builder, enabling shared hosting on
cvbuilder.ccwith Keycloak OIDC authentication and per-user data isolation.Core infrastructure (earlier commits)
python:3.11-slim; WeasyPrint system deps, Node.js for bundle build, non-rootcvappuserCV_DATA_ROOTenv var: All data paths derived from a single root (default~/CV/); Docker mounts/data{CV_DATA_ROOT}/users/{keycloak_sub}/subtrees forMaster_CV_Data.json,publications.bib, andcv-builder/output dirscripts/utils/auth.py): opt-in viaKEYCLOAK_URL;/login→/auth/callback→ signed Flask session cookie;get_current_user_id()safe outside request context_FILEenv var pattern):KEYCLOAK_CLIENT_SECRET_FILE,CV_SECRET_KEY_FILE, etc.scripts/utils/logging_config.py): injectsuser_idinto every log recordconfig.yaml)cvbuilder.cc(no hyphen) throughoutBug fixes (this session)
scripts/utils/auth.py,scripts/web_app.py):ensure_master_cv_exists()creates a blankMaster_CV_Data.jsonskeleton on first run in single-user mode; guarded againstNonepath to prevent test failuresscripts/utils/cv_orchestrator.py): explicit user-selected publications are no longer capped bymax_publications— the user's exact selection is honoredweb/download-tab.js): advisory ATS warnings (page count, date format, heading style, JSON-LD, etc.) no longer disable CV downloads; only critical failures (keyword missing, broken file structure) blockscripts/utils/llm_client.py):_strip_intro_phrase()removes leadingLabel:intro before verb/word-count persuasion checks, fixing false failures on bullets like"Category Compass: remainder of text"Review and documentation
tasks/gaps.mdlaunchd/): autostart for local developmentSecurity
.specstory/history/added to.gitignore— these auto-generated conversation logs can contain API keys and must not be trackedIssues closed
_strip_intro_phrase()_NON_BLOCKING_CHECKSsetensure_master_cv_exists()launchd/Test plan
CV_DATA_PATH=~/CV/— blankMaster_CV_Data.jsonis created on first runKEYCLOAK_URL+ realm/client/secret — login redirects to Keycloak, callback storesuser_id, per-user paths are used"Category Compass: Led cross-functional team..."passes verb check (strips intro phrase)docker build -t cv-builder .succeeds;docker run -e CV_DATA_PATH=/data ...starts cleanpytest scripts/tests/ -x— pre-existing 4 failures only (unrelated to this PR)🤖 Generated with Claude Code