Skip to content

feat(multi-user): Dockerfile, Keycloak OIDC auth, per-user data isolation#127

Merged
warnes merged 733 commits into
mainfrom
feature/multi-user-deployment
Jul 7, 2026
Merged

feat(multi-user): Dockerfile, Keycloak OIDC auth, per-user data isolation#127
warnes merged 733 commits into
mainfrom
feature/multi-user-deployment

Conversation

@warnes

@warnes warnes commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Multi-user deployment infrastructure for cv-builder, enabling shared hosting on cvbuilder.cc with Keycloak OIDC authentication and per-user data isolation.

Core infrastructure (earlier commits)

  • Dockerfile + pip requirements: Production container based on python:3.11-slim; WeasyPrint system deps, Node.js for bundle build, non-root cvapp user
  • CV_DATA_ROOT env var: All data paths derived from a single root (default ~/CV/); Docker mounts /data
  • Per-user isolation: {CV_DATA_ROOT}/users/{keycloak_sub}/ subtrees for Master_CV_Data.json, publications.bib, and cv-builder/ output dir
  • Keycloak OIDC (scripts/utils/auth.py): opt-in via KEYCLOAK_URL; /login/auth/callback → signed Flask session cookie; get_current_user_id() safe outside request context
  • Docker Swarm secrets (_FILE env var pattern): KEYCLOAK_CLIENT_SECRET_FILE, CV_SECRET_KEY_FILE, etc.
  • RequestContextFilter (scripts/utils/logging_config.py): injects user_id into every log record
  • Startup guard: refuses to start with a clear error if no data path is configured (neither env var nor config.yaml)
  • Domain correction: cvbuilder.cc (no hyphen) throughout

Bug fixes (this session)

  • GAP-36 onboarding (scripts/utils/auth.py, scripts/web_app.py): ensure_master_cv_exists() creates a blank Master_CV_Data.json skeleton on first run in single-user mode; guarded against None path to prevent test failures
  • Publications cap (scripts/utils/cv_orchestrator.py): explicit user-selected publications are no longer capped by max_publications — the user's exact selection is honored
  • Non-blocking ATS checks (web/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) block
  • Persuasion intro-phrase (scripts/utils/llm_client.py): _strip_intro_phrase() removes leading Label: intro before verb/word-count persuasion checks, fixing false failures on bullets like "Category Compass: remainder of text"

Review and documentation

  • 15-persona parallel cvUiReview (2026-06-18): 19 new GAP entries (GAP-124 through GAP-142) added to tasks/gaps.md
  • macOS launchd agent (launchd/): autostart for local development

Security

  • .specstory/history/ added to .gitignore — these auto-generated conversation logs can contain API keys and must not be tracked
  • History rewritten to remove previously committed specstory files that contained secrets

Issues closed

Issue Title Resolution
#102 Persuasion verb check fires on "Category:" intro phrases Fixed via _strip_intro_phrase()
#103 Page-length ATS warning blocks PDF download Fixed via _NON_BLOCKING_CHECKS set
#105 First run fails without Master_CV_Data.json Fixed via ensure_master_cv_exists()
#106 Explicit publication selection capped by max_publications Fixed by removing cap on explicit-selection path
#122 launchd autostart for local development Implemented in launchd/
#125 User-selected publications capped incorrectly Fixed (same as #106)

Test plan

  • Single-user mode: start with CV_DATA_PATH=~/CV/ — blank Master_CV_Data.json is created on first run
  • Auth mode: set KEYCLOAK_URL + realm/client/secret — login redirects to Keycloak, callback stores user_id, per-user paths are used
  • Download tab: ATS advisory warnings shown but do not block download buttons
  • Persuasion check: bullet "Category Compass: Led cross-functional team..." passes verb check (strips intro phrase)
  • Publications: explicitly accepting >N publications passes all selected through without cap
  • Docker: docker build -t cv-builder . succeeds; docker run -e CV_DATA_PATH=/data ... starts clean
  • Test suite: pytest scripts/tests/ -x — pre-existing 4 failures only (unrelated to this PR)

🤖 Generated with Claude Code

warnes and others added 30 commits June 18, 2026 18:20
…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>
warnes and others added 15 commits July 6, 2026 20:26
…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>
Comment thread scripts/routes/generation_routes.py Fixed
Comment thread scripts/routes/generation_routes.py Fixed
warnes and others added 4 commits July 7, 2026 11:54
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>
Comment thread scripts/routes/job_routes.py Dismissed
Comment thread scripts/routes/job_routes.py Fixed
warnes and others added 5 commits July 7, 2026 15:32
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>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

❌ CI checks failed

Re-run after fixing the issues above. See CONTRIBUTING.md for local test commands.

@warnes warnes merged commit 426a153 into main Jul 7, 2026
15 checks passed
@warnes warnes deleted the feature/multi-user-deployment branch July 7, 2026 22:10
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants