Conversation
… into Development
- 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
# Conflicts: # web/Viper.csproj
- 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
daef548 to
6057b7e
Compare
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAssessmentBubble 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. ChangesAssessmentBubble Component & Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~23 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
VueApp/src/CTS/__tests__/assessment-bubble.test.tsVueApp/src/CTS/assets/cts.cssVueApp/src/CTS/components/AssessmentBubble.vueVueApp/src/CTS/pages/MyAssessments.vue
| 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) | ||
| }) |
There was a problem hiding this comment.
🧹 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="".
| .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; | ||
| } |
There was a problem hiding this comment.
🧹 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"]) |
There was a problem hiding this comment.
🧹 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.
| 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)).
| <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> |
There was a problem hiding this comment.
🛠️ 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.
| <span | ||
| v-else | ||
| :class="['assessmentBubble', bubbleClass]" | ||
| role="img" | ||
| :aria-label="levelName" | ||
| /> |
There was a problem hiding this comment.
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.
| <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.
- 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.
6057b7e to
43056bc
Compare
- 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) |
| catch (Exception ex) | ||
| { | ||
| // Log exception - instinct integration should not fail the entire request | ||
| } |
| catch | ||
| { | ||
| return new List<string>(); | ||
| } |
| catch (Exception ex) | ||
| { | ||
| return null; | ||
| } |
| catch (Exception ex) | ||
| { | ||
| return null; | ||
| } |
| public class UserInfoController : AreaController | ||
| { | ||
| public Classes.SQLContext.AAUDContext _aaud; | ||
| private UserInfoService _userInfo; |
|
@bsedwards New changes to the timeline display. Example: https://secure-test.vetmed.ucdavis.edu/2/CTS/MyAssessments?student=19774
|
Summary
Accessibility improvements scoped to the CTS MyAssessments page and the shared
AssessmentBubblecomponent. Split out from #137 so the page rework and bubble redesign can land as their own PR.MyAssessments page
<h1>and the "Entrustable Professional Activities" heading to<h2>(1.3.1 Info and Relationships)aria-label+aria-expandedon 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)aria-label="Close dialog"close icon added to the "Assessment Details" dialog header (2.1.1 Keyboard)AssessmentBubblecomponent<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 goldfocus-visiblering, 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
AssessmentBubbleunit tests — newassessment-bubble.test.tslocks 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 whenidis omitted. 15 tests covering the full component surface.Test plan
/CTS/MyAssessmentsrenders with h1 title, h2 EPA section, and keyboard-accessible expand/collapse controlsAssessmentBubblestill renders correctly in other CTS pages that use it (AssessmentEpa,AssessmentEpaEdit,AssessmentCompetency,ManageSessionCompetencies)npm run test—assessment-bubble.test.tspassesSummary by CodeRabbit
New Features
Improvements