Skip to content

CTS - MyAssessment UI changes#155

Open
rlorenzo wants to merge 654 commits intomainfrom
VPR-104-accessibility-audit-CTS-myassessments
Open

CTS - MyAssessment UI changes#155
rlorenzo wants to merge 654 commits intomainfrom
VPR-104-accessibility-audit-CTS-myassessments

Conversation

@rlorenzo
Copy link
Copy Markdown
Contributor

@rlorenzo rlorenzo commented Apr 20, 2026

Summary

Accessibility improvements scoped to the CTS MyAssessments page and the shared AssessmentBubble component. Split out from #137 so the page rework and bubble redesign can land as their own PR.

MyAssessments page

  • Heading hierarchy — promote the page title to <h1> and the "Entrustable Professional Activities" heading to <h2> (1.3.1 Info and Relationships)
  • Expanded layout — toggle buttons moved to the left edge of each EPA row; expanded details (date, clinician, service, rating) now align under the EPA name rather than starting at the row origin; comment extends to the full width of the content column
  • Expand/collapse labelsaria-label + aria-expanded on the global expand-all button and each per-EPA expand button; label adapts with state ("Expand details for…" / "Collapse details for…") (4.1.2 Name, Role, Value)
  • Details dialog close button — accessible aria-label="Close dialog" close icon added to the "Assessment Details" dialog header (2.1.1 Keyboard)
  • Details dialog rating pill — the rating in the "Assessment Details" dialog now renders as a color-matched pill (same styling as the expanded inline list) instead of a separate bubble plus loose label, simplifying the layout

AssessmentBubble component

  • Privacy-preserving aria-label — the rating dot no longer announces a numeric score; aria-label reads the descriptive rating name ("Trust with indirect supervision") for both the clickable and the decorative variants, and the numeric value is removed from inside the circle. Prevents screen readers from announcing a student's low rating aloud in shared spaces (4.1.2 Name, Role, Value)
  • Contrast & target size — rating circles became <button> elements with a stepped UC Davis blue palette (rgba(62,127,238,0.3)rgba(11,3,139,1)), a 1.5rem diameter for a larger touch/click target, a gold focus-visible ring, and 0.25rem horizontal spacing so adjacent bubbles never touch. Replaces the old low-contrast light blue fill (1.4.1 Use of Color, 1.4.11 Non-text Contrast)

Test Coverage

  • AssessmentBubble unit tests — new assessment-bubble.test.ts locks in the privacy contract (aria-label must never contain the numeric rating), the value → class mapping, click emission with the correct id, and the non-interactive span variant when id is omitted. 15 tests covering the full component surface.

Test plan

  • /CTS/MyAssessments renders with h1 title, h2 EPA section, and keyboard-accessible expand/collapse controls
  • Screen reader announces level names (not numeric scores) for bubbles in both clickable and decorative variants
  • AssessmentBubble still renders correctly in other CTS pages that use it (AssessmentEpa, AssessmentEpaEdit, AssessmentCompetency, ManageSessionCompetencies)
  • npm run testassessment-bubble.test.ts passes

Summary by CodeRabbit

  • New Features

    • Added styled level chip components for clearer assessment rating display
    • Restructured assessment details dialog with improved layout and navigation controls
  • Improvements

    • Enhanced visual styling of assessment bubbles with more explicit color definitions
    • Strengthened accessibility with improved ARIA labels and semantic markup
    • Refined expand/collapse functionality for better assessment view management

- Add dashboard page showing verification progress, instructor/course
  stats, and data hygiene alerts with department-scoped access
- Create AlertStates table to persist alert ignore/resolved state
- Add department verification breakdown and recent changes feed
- Include frontend/backend tests and system documentation PDFs
- Use explicit .Where() filter instead of implicit if-check in
  ApplyAlertStatesAsync foreach loop
- Remove always-true/false ternary conditions in test file by
  simplifying to direct calculations
- Add clickable department rows to navigate to filtered instructor list
- Show different layouts for open vs closed terms
- Highlight "No Department" with visual accent styling
- Only show verification overdue alerts for open terms
- Fix term classification when closedDate set without openedDate
- Extract shared composable for dialog state, CRUD, and reload
- Add EffortActionButtons, ZeroEffortBanner, CrossListedCoursesSection
- Unify CourseImportDialog with mode prop, delete self-service copy
- Fix button order on MyEffort (Import first, Add second)
- Eliminate post-mutation loading flash on MyEffort
rlorenzo added 6 commits May 4, 2026 14:39
- Cloudflare fronts vetmed.ucdavis.edu, so the proxy chain is
  User -> CF -> F5 -> app. Without trusting CF as a known proxy,
  UseForwardedHeaders ignores XFF and RemoteIpAddress stays at
  the CF edge - breaking every IP-based allowlist in the app,
  not just health checks.
- Fetches CF's published v4/v6 CIDRs from cloudflare.com at
  startup (cached for the process lifetime) and adds them to
  KnownIPNetworks. Falls back to a hardcoded snapshot on fetch
  failure so a CF blip during deploy doesn't break startup.
- Bumps ForwardLimit to 2 so the chain walk continues through
  both proxy hops (F5 and CF) and lands on the real client IP
  rather than stopping at the CF edge.
- /health: anonymous liveness for Jenkins probes.
- /health/detail: per-check JSON (UI format) gated by InternalAllowlist
  (developer IPs only). Intentionally not CAS-gated so it stays
  reachable when auth subsystems are degraded.
- /healthchecks: HealthChecks.UI dashboard, IP-gated to the same list.
- Database, disk, AWS SSM, LDAP, CAS, SMTP, and VMACs probes; campus-*
  external probes wrapped in adaptive polling so a healthy check is
  cached for an hour and a failing one re-probes every 5 min.
- The in-app HealthChecksUI collector polls /health/detail through
  the IP filter without widening the allowlist: a process-startup
  random token is stamped on every outbound poll by a delegating
  handler (UseApiEndpointDelegatingHandler), and the endpoint filter
  bypasses the IP check on a constant-time match. Token cycles per
  process, so it can't be replayed across deploys.
- Unit tests cover the constant-time matcher, header lookup
  (case-insensitive), and the delegating handler's stamp/replace/
  forward behaviors.
# Conflicts:
#	web/Classes/HealthChecks/AwsSsmHealthCheck.cs
#	web/Classes/HealthChecks/DiskSpaceHealthCheck.cs
#	web/Classes/HealthChecks/HealthCheckExtensions.cs
#	web/Classes/HealthChecks/HttpEndpointHealthCheck.cs
#	web/Classes/HealthChecks/LdapHealthCheck.cs
#	web/Classes/HealthChecks/SmtpHealthCheck.cs
#	web/Program.cs
#	web/Viper.csproj
#	web/wwwroot/js/healthchecks-ui-extras.js
@rlorenzo rlorenzo force-pushed the VPR-104-accessibility-audit-CTS-myassessments branch from daef548 to 6057b7e Compare May 5, 2026 20:13
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • review-ready

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 28d4fe5f-2fe6-4ca3-8072-2511f0cf1d77

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

AssessmentBubble is refactored from imperative ref/watch logic to computed-driven state. CSS classes are added for layout and level indicators. MyAssessments integrates the updated component and restructures the assessment details dialog with new chip-based level display.

Changes

AssessmentBubble Component & Integration

Layer / File(s) Summary
Component Props & Emit Signature
VueApp/src/CTS/components/AssessmentBubble.vue
Props changed from TypeScript generics to runtime schema with explicit defaults. Emit signature downgraded from typed "bubble-click": [id: number] to untyped array form.
Component Logic
VueApp/src/CTS/components/AssessmentBubble.vue
Replaced ref/watch-based bubbleClass mutation with computed properties for isValidValue and bubbleClass. Template switched from always-present q-icon to conditional button (when id exists) or non-interactive span (when undefined).
Styling
VueApp/src/CTS/assets/cts.css
Added .expandToggleCol, assessment bubble trigger/button/focus styles, and level-indicator chip classes (.levelChip with --1 through --5 modifiers). Updated assessmentBubble5_* hex color values; added .assessmentComment for italic styling.
MyAssessments Integration
VueApp/src/CTS/pages/MyAssessments.vue
Introduced anyExpanded computed to track expansion state. Simplified toggleExpandAll() to use computed boolean. Restructured Assessment Details dialog (added close button layout), replaced Assessment Bubble in rating display with styled levelChip span. Updated EPA list layout: added :level-name prop to bubble, expanded details now render levelChip per assessment with updated grid/spacing.
Tests
VueApp/src/CTS/__tests__/assessment-bubble.test.ts
Added comprehensive test suite validating: aria-label privacy (no numeric "rating X of Y" exposed, "open assessment details" suffix when id present), class-name contract (valueassessmentBubble5_{1..5} when maxValue=5), click behavior (button emission when id defined, no button/no emit when undefined), and empty bubble content.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~23 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "CTS - MyAssessment UI changes" is vague and overly broad, using generic phrasing that does not convey specific information about the primary change. Clarify the title to highlight the main change, such as "CTS: Convert AssessmentBubble to accessible button with privacy-preserving labels" or focus on the primary accessibility improvements.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch VPR-104-accessibility-audit-CTS-myassessments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@VueApp/src/CTS/__tests__/assessment-bubble.test.ts`:
- Around line 44-54: Add a new test in assessment-bubble.test.ts that covers the
accessibility edge case where levelName is empty or omitted: use createWrapper
with maxValue and value (e.g., maxValue: 5, value: 2) and set levelName: "" (and
optionally another case with levelName undefined) and assert that the
span[role="img"] aria-label is not an empty string and instead contains the
numeric fallback (match /\b2 of 5\b/i); name the test to describe the edge case
(e.g., "falls back to numeric label when levelName is empty") and mirror the
style of the existing tests so it fails if component produces aria-label="".

In `@VueApp/src/CTS/assets/cts.css`:
- Around line 96-119: Duplicate color definitions exist between
.assessmentBubble5_1–5 and .levelChip--1–5; consolidate by extracting the five
color pairs into CSS custom properties (e.g. --level-bg-1..5 and
--level-fg-1..5) and create shared modifier classes (e.g. .levelPalette--1..5)
that set background-color and color via those vars, then update
.assessmentBubble5_1..5 and .levelChip--1..5 to simply include the corresponding
shared modifier class (or reference the variables) so palette changes are
managed in one place; modify references in the stylesheet for the selectors
assessmentBubble5_* and levelChip--* accordingly.

In `@VueApp/src/CTS/components/AssessmentBubble.vue`:
- Line 27: The emit definition is untyped so TypeScript won't enforce that
"bubble-click" carries a numeric id; replace the untyped defineEmits in
AssessmentBubble.vue with a typed signature (e.g. use defineEmits<{ (e:
'bubble-click', id: number): void }>() or defineEmits<{ 'bubble-click': (id:
number) => void }>() ) so emitting "bubble-click" requires a number, and ensure
places that call emit("bubble-click", ...) still pass a number (matching
MyAssessments.vue's handleAssessmentClick(id: number)).
- Around line 64-69: The span with role="img" in AssessmentBubble.vue uses the
prop levelName which defaults to "" and thus renders aria-label="" (invalid per
WCAG); change the template so aria-label is only bound when levelName is
non-empty and set aria-hidden to true when levelName is falsy (e.g.
:aria-label="levelName || undefined" and :aria-hidden="!levelName") so the
element is treated as decorative when no name is provided; update bindings on
the <span> that uses role="img" and the levelName prop accordingly.
- Around line 48-63: Replace the raw <button> with a Quasar q-btn: keep the
v-if="id", type="button" (if desired), :aria-label using the same levelName
expression, `@click`="clickBubble", and add flat, padding="none", round so it
matches the reset; render the visual bubble inside q-btn (e.g., as the default
slot using the existing <span :class="['assessmentBubble', bubbleClass]"
aria-hidden="true" />) and keep the q-tooltip with its content and {{ props.text
}}; after changing to q-btn, remove the custom .assessmentBubbleTrigger reset
rule and the :focus-visible override from cts.css so Quasar’s built-in
focus/hover/ripple behavior is used instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ca3eb28b-1ab4-462c-b6dd-1d6df9777221

📥 Commits

Reviewing files that changed from the base of the PR and between bfe8cfa and 6057b7e.

📒 Files selected for processing (4)
  • VueApp/src/CTS/__tests__/assessment-bubble.test.ts
  • VueApp/src/CTS/assets/cts.css
  • VueApp/src/CTS/components/AssessmentBubble.vue
  • VueApp/src/CTS/pages/MyAssessments.vue

Comment on lines +44 to +54
it("uses levelName on the standalone span and does not expose the numeric value", () => {
const wrapper = createWrapper({
maxValue: 5,
value: 2,
levelName: "Trust with direct supervision",
})

const label = wrapper.get('span[role="img"]').attributes("aria-label")!
expect(label).toBe("Trust with direct supervision")
expect(label).not.toMatch(/\b2 of 5\b/i)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

No test for the accessibility edge case: span variant with missing/empty levelName.

The existing aria-label privacy suite covers a levelName-present span, but the component's default of levelName: "" produces aria-label="" on role="img" — a real a11y gap that is currently undetected by the suite.

✅ Proposed additional test
+        it("renders aria-hidden when levelName is absent on the standalone span", () => {
+            const wrapper = createWrapper({
+                maxValue: 5,
+                value: 2,
+                // levelName intentionally omitted — defaults to ""
+            })
+
+            const span = wrapper.get('span[role="img"]')
+            // Must not leave role="img" with an empty accessible name
+            expect(span.attributes("aria-label") ?? "").toBe("")
+            // Confirm the fix: element should be decorative when no name is available
+            expect(span.attributes("aria-hidden")).toBe("true")
+        })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@VueApp/src/CTS/__tests__/assessment-bubble.test.ts` around lines 44 - 54, Add
a new test in assessment-bubble.test.ts that covers the accessibility edge case
where levelName is empty or omitted: use createWrapper with maxValue and value
(e.g., maxValue: 5, value: 2) and set levelName: "" (and optionally another case
with levelName undefined) and assert that the span[role="img"] aria-label is not
an empty string and instead contains the numeric fallback (match /\b2 of 5\b/i);
name the test to describe the edge case (e.g., "falls back to numeric label when
levelName is empty") and mirror the style of the existing tests so it fails if
component produces aria-label="".

Comment on lines 96 to 119
.assessmentBubble5_1 {
color: rgba(62, 127, 238, 0.3);
background-color: rgba(62, 127, 238, 0.3);
color: #212529;
}

.assessmentBubble5_2 {
color: rgba(62, 127, 238, 0.7);
background-color: rgba(62, 127, 238, 0.7);
color: #212529;
}

.assessmentBubble5_3 {
color: rgba(62, 127, 238, 1);
background-color: rgba(62, 127, 238, 1);
color: #000;
}

.assessmentBubble5_4 {
color: rgba(0, 44, 175, 0.8);
background-color: rgba(0, 44, 175, 0.8);
color: #fff;
}

.assessmentBubble5_5 {
color: rgba(11, 3, 139, 1);
background-color: rgba(11, 3, 139, 1);
color: #fff;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Duplicate color palette across assessmentBubble5_* and levelChip--*.

All five background-color/color pairs in .assessmentBubble5_1–5 (lines 96–119) are identical to those in .levelChip--1–5 (lines 152–171). A future palette change needs to be applied in two places.

Consolidate with CSS custom properties and shared modifier classes:

♻️ Proposed refactor
+/* Level colour tokens */
+:root {
+    --level-1-bg: rgba(62, 127, 238, 0.3);  --level-1-fg: `#212529`;
+    --level-2-bg: rgba(62, 127, 238, 0.7);  --level-2-fg: `#212529`;
+    --level-3-bg: rgba(62, 127, 238, 1);    --level-3-fg: `#000`;
+    --level-4-bg: rgba(0, 44, 175, 0.8);    --level-4-fg: `#fff`;
+    --level-5-bg: rgba(11, 3, 139, 1);      --level-5-fg: `#fff`;
+}
+
+/* Shared level-colour modifier — consumed by both .assessmentBubble and .levelChip */
+.level--1 { background-color: var(--level-1-bg); color: var(--level-1-fg); }
+.level--2 { background-color: var(--level-2-bg); color: var(--level-2-fg); }
+.level--3 { background-color: var(--level-3-bg); color: var(--level-3-fg); }
+.level--4 { background-color: var(--level-4-bg); color: var(--level-4-fg); }
+.level--5 { background-color: var(--level-5-bg); color: var(--level-5-fg); }
+
 .assessmentBubble5_1 {
-    background-color: rgba(62, 127, 238, 0.3);
-    color: `#212529`;
+    background-color: var(--level-1-bg);
+    color: var(--level-1-fg);
 }
 /* … repeat for 5_2–5_5 … */

 .levelChip--1 {
-    background-color: rgba(62, 127, 238, 0.3);
-    color: `#212529`;
+    background-color: var(--level-1-bg);
+    color: var(--level-1-fg);
 }
 /* … repeat for --2–--5 … */

Also applies to: 152-171

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@VueApp/src/CTS/assets/cts.css` around lines 96 - 119, Duplicate color
definitions exist between .assessmentBubble5_1–5 and .levelChip--1–5;
consolidate by extracting the five color pairs into CSS custom properties (e.g.
--level-bg-1..5 and --level-fg-1..5) and create shared modifier classes (e.g.
.levelPalette--1..5) that set background-color and color via those vars, then
update .assessmentBubble5_1..5 and .levelChip--1..5 to simply include the
corresponding shared modifier class (or reference the variables) so palette
changes are managed in one place; modify references in the stylesheet for the
selectors assessmentBubble5_* and levelChip--* accordingly.

const emit = defineEmits<{
"bubble-click": [id: number]
}>()
const emit = defineEmits(["bubble-click"])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Untyped defineEmits removes TypeScript enforcement of the bubble-click payload.

defineEmits(["bubble-click"]) lets the compiler accept any payload (or no payload) for bubble-click. The call site in MyAssessments.vue binds handleAssessmentClick(id: number), so a future refactor that passes a non-number would compile silently and fail at runtime.

🛡️ Proposed fix: typed emit signature
-const emit = defineEmits(["bubble-click"])
+const emit = defineEmits<{
+    "bubble-click": [id: number]
+}>()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const emit = defineEmits(["bubble-click"])
const emit = defineEmits<{
"bubble-click": [id: number]
}>()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@VueApp/src/CTS/components/AssessmentBubble.vue` at line 27, The emit
definition is untyped so TypeScript won't enforce that "bubble-click" carries a
numeric id; replace the untyped defineEmits in AssessmentBubble.vue with a typed
signature (e.g. use defineEmits<{ (e: 'bubble-click', id: number): void }>() or
defineEmits<{ 'bubble-click': (id: number) => void }>() ) so emitting
"bubble-click" requires a number, and ensure places that call
emit("bubble-click", ...) still pass a number (matching MyAssessments.vue's
handleAssessmentClick(id: number)).

Comment on lines +48 to +63
<button
v-if="id !== undefined"
type="button"
class="assessmentBubbleTrigger"
:aria-label="levelName ? `${levelName}, open assessment details` : 'Open assessment details'"
@click="clickBubble"
>
<q-tooltip
style="white-space: pre-wrap"
class="text-body2"
>
<strong>Click to open details</strong>
<br />
{{ props.text }}
<span
:class="['assessmentBubble', bubbleClass]"
aria-hidden="true"
/>
<q-tooltip class="text-body2">
<div><strong>Click to open details</strong></div>
<div class="assessmentBubbleTooltipText">{{ props.text }}</div>
</q-tooltip>
</q-icon>
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Raw <button> violates the project guideline; replace with q-btn.

The template uses a plain <button> element that is then reset via .assessmentBubbleTrigger CSS. As per coding guidelines, Quasar components (q-btn, etc.) must be used instead of HTML elements.

A q-btn with flat + padding="none" + round achieves the same reset without bespoke CSS, and inherits the Quasar focus/hover/ripple contracts:

♻️ Proposed refactor
-    <button
+    <q-btn
         v-if="id !== undefined"
-        type="button"
-        class="assessmentBubbleTrigger"
+        flat
+        round
+        dense
+        padding="none"
         :aria-label="levelName ? `${levelName}, open assessment details` : 'Open assessment details'"
         `@click`="clickBubble"
     >
         <span
             :class="['assessmentBubble', bubbleClass]"
             aria-hidden="true"
         />
         <q-tooltip class="text-body2">
             <div><strong>Click to open details</strong></div>
             <div class="assessmentBubbleTooltipText">{{ props.text }}</div>
         </q-tooltip>
-    </button>
+    </q-btn>

With q-btn, the .assessmentBubbleTrigger reset rule in cts.css (lines 60–68) and the :focus-visible override (lines 78–82) can also be removed in favour of Quasar's built-in focus ring.

As per coding guidelines: "Use Quasar components (q-btn, q-dialog, q-banner, etc.) instead of HTML elements."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@VueApp/src/CTS/components/AssessmentBubble.vue` around lines 48 - 63, Replace
the raw <button> with a Quasar q-btn: keep the v-if="id", type="button" (if
desired), :aria-label using the same levelName expression, `@click`="clickBubble",
and add flat, padding="none", round so it matches the reset; render the visual
bubble inside q-btn (e.g., as the default slot using the existing <span
:class="['assessmentBubble', bubbleClass]" aria-hidden="true" />) and keep the
q-tooltip with its content and {{ props.text }}; after changing to q-btn, remove
the custom .assessmentBubbleTrigger reset rule and the :focus-visible override
from cts.css so Quasar’s built-in focus/hover/ripple behavior is used instead.

Comment on lines +64 to +69
<span
v-else
:class="['assessmentBubble', bubbleClass]"
role="img"
:aria-label="levelName"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

aria-label="" on role="img" when levelName is the empty-string default.

levelName defaults to "", so any consumer that omits the prop gets <span role="img" aria-label="">. WCAG 2.1 SC 1.1.1 requires a non-empty text alternative for non-decorative images; an empty label leaves the element named but meaningless to screen readers. Either require the prop, or fall back to aria-hidden when no name is available:

🛡️ Proposed fix
     <span
         v-else
         :class="['assessmentBubble', bubbleClass]"
-        role="img"
-        :aria-label="levelName"
+        v-bind="levelName
+            ? { role: 'img', 'aria-label': levelName }
+            : { 'aria-hidden': 'true' }"
     />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span
v-else
:class="['assessmentBubble', bubbleClass]"
role="img"
:aria-label="levelName"
/>
<span
v-else
:class="['assessmentBubble', bubbleClass]"
v-bind="levelName
? { role: 'img', 'aria-label': levelName }
: { 'aria-hidden': 'true' }"
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@VueApp/src/CTS/components/AssessmentBubble.vue` around lines 64 - 69, The
span with role="img" in AssessmentBubble.vue uses the prop levelName which
defaults to "" and thus renders aria-label="" (invalid per WCAG); change the
template so aria-label is only bound when levelName is non-empty and set
aria-hidden to true when levelName is falsy (e.g. :aria-label="levelName ||
undefined" and :aria-hidden="!levelName") so the element is treated as
decorative when no name is provided; update bindings on the <span> that uses
role="img" and the levelName prop accordingly.

rlorenzo added 15 commits May 5, 2026 19:08
- Delete web/Areas/Jobs/ and Quartz package refs from web/Viper.csproj
- Remove orphaned ScheduledTask/ScheduledTaskHistory models, DbSets,
  and ScheduledTaskLog utility (legacy ColdFusion VIPER still owns
  the underlying tables)
- Cloudflare fronts vetmed.ucdavis.edu, so the proxy chain is
  User -> CF -> F5 -> app. Without trusting CF as a known proxy,
  UseForwardedHeaders ignores XFF and RemoteIpAddress stays at
  the CF edge - breaking every IP-based allowlist in the app,
  not just health checks.
- Fetches CF's published v4/v6 CIDRs from cloudflare.com at
  startup (cached for the process lifetime) and adds them to
  KnownIPNetworks. Falls back to a hardcoded snapshot on fetch
  failure so a CF blip during deploy doesn't break startup.
- Bumps ForwardLimit to 2 so the chain walk continues through
  both proxy hops (F5 and CF) and lands on the real client IP
  rather than stopping at the CF edge.
- /health: anonymous liveness for Jenkins probes.
- /health/detail: per-check JSON (UI format) gated by InternalAllowlist
  (developer IPs only). Intentionally not CAS-gated so it stays
  reachable when auth subsystems are degraded.
- /healthchecks: HealthChecks.UI dashboard, IP-gated to the same list.
- Database, disk, AWS SSM, LDAP, CAS, SMTP, and VMACs probes; campus-*
  external probes wrapped in adaptive polling so a healthy check is
  cached for an hour and a failing one re-probes every 5 min.
- The in-app HealthChecksUI collector polls /health/detail through
  the IP filter without widening the allowlist: a process-startup
  random token is stamped on every outbound poll by a delegating
  handler (UseApiEndpointDelegatingHandler), and the endpoint filter
  bypasses the IP check on a constant-time match. Token cycles per
  process, so it can't be replayed across deploys.
- Unit tests cover the constant-time matcher, header lookup
  (case-insensitive), and the delegating handler's stamp/replace/
  forward behaviors.
- Add missing XML doc <param> tags on health-check constructors
- Move HttpClient/SmtpClient property init out of object initializers
  so exceptions during init don't leak the resource
- Drop redundant name qualifiers and using directive
# Conflicts:
#	web/Classes/CloudflareNetworks.cs
#	web/Classes/HealthChecks/AwsSsmHealthCheck.cs
#	web/Classes/HealthChecks/DiskSpaceHealthCheck.cs
#	web/Classes/HealthChecks/HealthCheckCollectorAuth.cs
#	web/Classes/HealthChecks/HealthCheckExtensions.cs
#	web/Classes/HealthChecks/SmtpHealthCheck.cs
#	web/Program.cs
#	web/Viper.csproj
- Add XML doc <summary> + <param> tags on health-check constructors
- Move HttpClient/SmtpClient property init out of object initializers
  so exceptions during init don't leak the resource
- Drop redundant name qualifiers and using directive
- Drop staff-name comments from InternalAllowlist IPs
# Conflicts:
#	web/Classes/HealthChecks/SmtpHealthCheck.cs
Introduce a shared accessibility layer used by every export surface
in this PR:

- WordAccessibilityHelper: PDF/UA-aligned tagging for OpenXML Word
  exports (semantic headings, list/table structure, doc properties,
  accessible alt text on images, modern v15 compatibility).
- PdfAccessibilityHelper: QuestPDF semantic helpers (SemanticHeader,
  SemanticTable, SemanticIgnore, accessibility metadata) plus the
  WithAccessibility extension that finalizes document properties.
- ExcelAccessibilityHelper: ClosedXML helpers for tagged tables,
  sanitized table names, and core workbook properties so the
  exports satisfy WCAG 2.1 + PDF/UA where applicable.

Each helper has unit-test coverage in test/Classes/Utilities to
keep the cross-cutting behavior pinned.
Make every export surface in the Students area produce accessible
documents:

- Photo Gallery: Word and PDF exports tagged for assistive tech;
  the inline PDF endpoints share validation with the JSON POST
  paths, View is constrained at the model boundary, and exports
  carry sanitized error messages instead of raw exception text.
- Emergency Contact: PDF (overview + detail) and Excel (overview +
  detail) exports emit a tagged structure tree with em-dash
  placeholders so empty cells still satisfy PDF/UA clause 7.2.

Adds JetBrains.Annotations so the JSON-bound DTO setters can be
marked [UsedImplicitly] without the format check fighting the
ReSharper audit.
Apply the shared accessibility tagging to every Effort report
export surface so PDFs, Excel workbooks, and tabular sheets
announce their structure to assistive tech:

- PDF reports: Merit (Detail/Summary/Average/Multi-Year),
  Evaluation (Detail/Summary), Teaching Activity (Individual/
  Grouped), Department Summary, School Summary, Clinical Effort,
  Clinical Schedule, and Scheduled Clinical Weeks now use
  semantic headers, tagged tables, and document-property
  metadata. A shared Effort PDF footer helper keeps page
  numbering consistent across reports.
- Excel reports: each workbook gets sanitized sheet names,
  promoted accessible tables, and core properties matching the
  PDF. Workbooks are wrapped in `using` declarations so a
  midstream failure can't leak the handle.
- Empty data preflight: each effort page wires a hasData
  predicate so PDF/Excel exports are blocked client-side before
  the controller would respond with NoContent.
Switch report PDF endpoints from POST/blob fetch to GET so the
browser's built-in viewer renders them and Save preserves the
filename. Empty-data preflight moves to the client because GET
responses cannot surface a 204 as a notification once the new
tab is open.

- ApiController.InlineFile: shared helper that sets
  Content-Disposition: inline plus private/no-store cache
  headers (PDFs may contain PII).
- use-report-page composable: standardizes URL-param sync, PDF
  export, Excel download, and document-title management for the
  10+ Effort report pages. Browser tab titles now match the
  page H1 ("Merit & Promotion Report - Average", etc.) and pick
  up the active term once a report loads.
- ExportToolbar: unified Print/PDF/Excel/Word button row with
  loading state and an optional inline search input.
- Brand colors: VueApp/src/config/colors.ts exposes the UC
  Davis palette with WCAG-AA contrast pairings; ReportLayout
  picks them up.
- ClinicalScheduler refactor: extract WeekItem/ScheduleAssignment/
  ScheduleSemester/ViewMode out of ScheduleView.vue into a
  sibling .ts module so plain tsc (used by lint) can resolve
  the cross-file type imports the composables rely on.
Split out from VPR-104-accessibility-audit-CTS. Rework AssessmentBubble
into a button/span pair with proper roles and labels, restyle the rating
bubbles as filled chips, and rebuild the MyAssessments layout with an
h1/h2 hierarchy, accessible expand/collapse controls, and a level-chip
dialog. Adds test coverage for AssessmentBubble.
Adds a five-way display toggle to the My Assessments page so
stakeholders can compare alternative visualizations side-by-side on
TEST before we commit to one. The styles are:

1. Original circles — pre-redesign legacy palette
2. Current bubbles — the existing post-redesign view
3. Abbreviation pills — DNP / TDS / TIS / IRS / SJL
4. Timeline — per-EPA voice thread with smooth progression chart
5. Bar — chronological supervision-context bar with legend

This is a temporary state. After the review pass on TEST, the
unselected designs (and the toggle itself) will be removed.
@rlorenzo rlorenzo force-pushed the VPR-104-accessibility-audit-CTS-myassessments branch from 6057b7e to 43056bc Compare May 7, 2026 02:31
rlorenzo added 3 commits May 6, 2026 21:46
- Color rating with a level-matching dot in the byline
- Cross-link a review and its progression-chart dot via hover/focus in
  both directions, with a halo on the active dot
- Grow the inline chart at wider viewports and label "click to expand"
- Spread same-day duplicate dots horizontally so both stay visible

Also extract two ScheduleView.vue types (ScheduleAssignment,
ScheduleSemester) into a sibling .ts module so plain .ts composables
can import them. env.d.ts's `*.vue` wildcard hides Vue SFC named
exports from non-Vue importers, which broke type checking on three
schedule composables. Drop unused module-level re-exports of helpers
that are only consumed via the composable's returned object.
…elopment

# Conflicts:
#	VueApp/src/CTS/assets/cts.css
#	VueApp/src/CTS/components/EpaProgressionChart.vue
#	VueApp/src/CTS/components/EpaVoiceThread.vue
#	VueApp/src/ClinicalScheduler/__tests__/normalize-schedule-semesters.test.ts
#	VueApp/src/ClinicalScheduler/components/ScheduleView.vue
#	VueApp/src/ClinicalScheduler/composables/use-bulk-deletion-logic.ts
#	VueApp/src/ClinicalScheduler/composables/use-delete-mode.ts
#	VueApp/src/ClinicalScheduler/composables/use-schedule-normalization.ts
… mobile

On a phone, the inline progression chart now renders above the
review list (instead of below) so a reader sees their trend before
reading individual feedback.

return await MapToUserInfoResultAsync(user, currentTerms);
}
catch (Exception ex)

return await MapToUserInfoResultAsync(user, currentTerms);
}
catch (Exception ex)
}
}
}
catch (Exception ex)
new("scope", "api_access")
};

var formContent = new FormUrlEncodedContent(formParams);
}
}
}
catch (Exception ex)
Comment on lines +483 to +486
catch (Exception ex)
{
// Log exception - instinct integration should not fail the entire request
}
Comment on lines +167 to +170
catch
{
return new List<string>();
}
Comment on lines +147 to +150
catch (Exception ex)
{
return null;
}
Comment on lines +121 to +124
catch (Exception ex)
{
return null;
}
public class UserInfoController : AreaController
{
public Classes.SQLContext.AAUDContext _aaud;
private UserInfoService _userInfo;
@rlorenzo
Copy link
Copy Markdown
Contributor Author

rlorenzo commented May 7, 2026

@bsedwards New changes to the timeline display. Example: https://secure-test.vetmed.ucdavis.edu/2/CTS/MyAssessments?student=19774

  1. Bigger graph, will expand if the screen is wide enough
  2. Adding "click to expand" reminder text on the graph
  3. If you hover over the review, it will highlight the dot on the graph and vice versa
  4. If a review is on the same day, show dots side by side
  5. Added a colored circle next to the rating

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.

5 participants