Skip to content

feat(session): move the SPA session poll off the legacy CFM - #306

Open
rlorenzo wants to merge 1 commit into
feature/session-timeout-endpointfrom
feature/session-timeout-spa
Open

feat(session): move the SPA session poll off the legacy CFM#306
rlorenzo wants to merge 1 commit into
feature/session-timeout-endpointfrom
feature/session-timeout-spa

Conversation

@rlorenzo

@rlorenzo rlorenzo commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #304, which is stacked on #302. Review this one third. GitHub stack #305. Base is feature/session-timeout-endpoint, so this diff shows only the SPA work.

Migrates the Vue SPA's session timeout component off the legacy ColdFusion endpoint and onto the VIPER 2 endpoint added in #304.

Why this one matters

#304 only moved the Razor side. SessionTimeout.vue is a parallel implementation of the same component and was still calling public/timeout/seconds_until_timeout_v2.cfm?id=<loginId>, the exact unauthenticated endpoint #304 exists to replace. So until this merges:

  • the login-id-in-a-query-string read hole stays open for every SPA user, and
  • legacy VIPER 1 cannot be decommissioned, because the SPA still depends on it at runtime.

With this merged, nothing in web/ or VueApp/src/ references the CFM. Retiring seconds_until_timeout_v2.cfm becomes a separate, coordinated change in the legacy repo. Note that seconds_until_timeout.cfm (v1) must stay, since VIPER 1 uses it for its own timeout.

Deliberate deviation: plain fetch, not useFetch()

CLAUDE.md says use the service layer plus useFetch(). I am not doing that here, and it is worth a reviewer's attention.

It is not the envelope. fetchWrapper only unwraps when r.success !== undefined (ViperFetch.ts:137-141), so the envelope-less payload from the new endpoint passes through fine. useFetch() would work.

The reason is error handling. useFetch() routes every failure through errorHandler.handleError, which writes to the global error store (ErrorHandler.ts:13). For this component that is wrong twice over:

  • The poll is a silent background check every five minutes. A transient network blip would raise a global error banner at the user, who neither asked for nor can act on it.
  • On 401/403 it fires handleAuthError, which would kick in at exactly the moment our own dialog is trying to tell the user their session expired and offer them a Log in button. The two would fight.

The file already used plain fetch, so this is not a regression, but it is a conscious exception rather than an oversight. Happy to revisit if you would rather add a non-reporting variant to the service layer.

Also carried over

This file had the same defects #302 fixed on the Razor side, so the same fixes apply:

  • Non-OK responses are rejected instead of being passed into the success handler.
  • A failed extend renders a StatusBanner type="error" inside the dialog, and leaves the dialog up so the user can retry. StatusBanner is the SPA equivalent of the Razor q-banner per DESIGN.md, and type="error" is assertive (role="alert") by default.
  • The poll's dead try/catch, which could not catch an async rejection, is replaced with a real .catch that reschedules. Previously a rejected poll scheduled no follow-up, so session checking stopped for the life of the page.
  • The warning stands down by itself when the session is extended elsewhere, instead of leaving a stale "your session will expire at ..." on screen forever.
  • Midnight renders as "12:05 AM" rather than "0:05 AM", and the formatter is now one function instead of being duplicated in both handlers.

extendSession's target is unchanged on purpose. It uses VITE_VIPER_HOME, which is already /2/ in .env.test and .env.production, so it was never affected by the PathBase bug in #302.

Verification

Command Result
npm run lint -- --fix VueApp/src/components/SessionTimeout.vue No issues; TypeScript, fallow and jscpd all clean
npm run verify:build Build succeeded, 0 errors
npm run test:frontend 1121 passed (88 files), 0 failed
npm run test:backend 2710 passed, 0 failed

Not verified locally. There is no existing test for this component and I did not add one; it is DOM-and-timer heavy and the valuable assertions need the real endpoint. The end-to-end path (SPA polling /2/api/sessionTimeout under the real PathBase, against a real database) needs a TEST deploy. Local dev has no PathBase, so the /2 prefix cannot be exercised here.

@rlorenzo

rlorenzo commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Session timeout polling now uses the API session-timeout endpoint. The component retries failed checks, handles session extensions, formats expiration times correctly, and displays extension failures without closing the dialog.

Changes

Session timeout flow

Layer / File(s) Summary
Polling and timeout state
VueApp/src/components/SessionTimeout.vue
Polling uses direct fetch requests with HTTP error handling. Failed checks reschedule silently. The component hides stale warnings and formats midnight expiration times correctly.
Session extension and feedback
VueApp/src/components/SessionTimeout.vue
Successful extensions reset polling and warning state. Failed extensions keep the dialog open and show an error banner. Reset logic clears the failure state.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SessionTimeoutVue
  participant PollingTimer
  participant SessionTimeoutAPI
  PollingTimer->>SessionTimeoutVue: trigger session check
  SessionTimeoutVue->>SessionTimeoutAPI: fetch session-timeout status
  SessionTimeoutAPI-->>SessionTimeoutVue: return status or HTTP error
  SessionTimeoutVue->>PollingTimer: schedule next check
  SessionTimeoutVue->>SessionTimeoutAPI: request session extension
  SessionTimeoutAPI-->>SessionTimeoutVue: return extension result
  SessionTimeoutVue->>PollingTimer: reset polling after success
  SessionTimeoutVue-->>SessionTimeoutVue: show error banner after failure
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: moving SPA session polling from the legacy CFM endpoint.
Description check ✅ Passed The description directly explains the endpoint migration, related fixes, rationale, and verification results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/session-timeout-spa

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/components/SessionTimeout.vue`:
- Around line 32-33: The session lifecycle requests in
VueApp/src/components/SessionTimeout.vue at lines 32-33 and 58-59 both require
changes: replace the raw fetch calls in the polling and session-extension flows
with a shared silent useFetch or service-layer method for sessionTimeout and
RefreshSession. Ensure this path suppresses global error and authentication
handling while preserving the existing API response contract.
- Around line 68-70: Update the failed-session-extension state in SessionTimeout
so the open dialog includes an accessible close q-btn wired to
hideSessionTimeoutWarning, while preserving the StatusBanner text. Ensure the
Log in action is visible alongside the existing Retry/Refresh action when
sessionExtendFailed is true, and add Playwright coverage for keyboard navigation
plus both actions.
🪄 Autofix

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 Plus

Run ID: 41e27369-f9d1-41fd-ad3b-45d95b33e500

📥 Commits

Reviewing files that changed from the base of the PR and between 6e97020 and 7c677c8.

📒 Files selected for processing (1)
  • VueApp/src/components/SessionTimeout.vue

Comment thread VueApp/src/components/SessionTimeout.vue Outdated
Comment thread VueApp/src/components/SessionTimeout.vue

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Updates the Vue SPA’s session-timeout polling to use the VIPER 2 /api/sessionTimeout endpoint (instead of the legacy ColdFusion seconds_until_timeout_v2.cfm), aligning the SPA with the already-migrated Razor implementation and removing the legacy dependency/security hole.

Changes:

  • Replaced the legacy CFM poll URL (with loginId in the query string) with a VIPER 2 API poll based on VITE_API_URL.
  • Improved polling resiliency by rejecting non-OK responses and ensuring failures still reschedule future checks.
  • Added in-dialog error UI for failed session extension attempts via StatusBanner, and centralized the expiry time formatter.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@rlorenzo
rlorenzo force-pushed the feature/session-timeout-spa branch from 7c677c8 to e956bcb Compare August 8, 2026 02:34
@codecov-commenter

codecov-commenter commented Aug 8, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 1.46kB (0.06%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
viper-frontend-esm 2.29MB 1.46kB (0.06%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: viper-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/quasar.client-*.js (New) 475.39kB 475.39kB 100.0% 🚀
assets/GenericError-*.css 818 bytes 208.71kB 0.39%
assets/GenericError-*.js -473.35kB 58.18kB -89.05%
assets/schedule-*.js -12 bytes 54.94kB -0.02%
assets/TermManagement-*.js -8 bytes 52.37kB -0.02%
assets/SortableList-*.js -1 bytes 45.53kB -0.0%
assets/CourseDetail-*.js -5 bytes 39.35kB -0.01%
assets/PhotoGallery-*.js -23 bytes 36.1kB -0.06%
assets/InstructorEdit-*.js -7 bytes 28.59kB -0.02%
assets/InstructorList-*.js -1 bytes 25.96kB -0.0%
assets/StaffDashboard-*.js 37 bytes 25.55kB 0.15%
assets/effort-*.js 35 bytes 23.68kB 0.15%
assets/Files-*.js -6 bytes 22.54kB -0.03%
assets/CrossListedCoursesSection-*.js -1 bytes 22.29kB -0.0%
assets/ContentBlockEdit-*.js -12 bytes 20.63kB -0.06%
assets/MultiYearReport-*.js -4 bytes 18.8kB -0.02%
assets/EmergencyContactForm-*.js -6 bytes 17.2kB -0.03%
assets/AuditList-*.js 38 bytes 17.1kB 0.22%
assets/AuditList-*.js 39 bytes 5.16kB 0.76%
assets/MyAssessments-*.js 38 bytes 16.56kB 0.23%
assets/CourseList-*.js -11 bytes 15.88kB -0.07%
assets/EffortTypeList-*.js -2 bytes 15.35kB -0.01%
assets/ManageLinkCollections-*.js -1 bytes 15.21kB -0.01%
assets/CmsHome-*.js 4 bytes 11.38kB 0.04%
assets/AuditLogPage-*.js -2 bytes 11.26kB -0.02%
assets/EmergencyContactList-*.js 3 bytes 11.07kB 0.03%
assets/CourseImportDialog-*.js -1 bytes 10.83kB -0.01%
assets/permission-*.js 30 bytes 9.78kB 0.31%
assets/ManageBundleCompetencies-*.js 34 bytes 8.9kB 0.38%
assets/ReportFilterForm-*.js -2 bytes 8.86kB -0.02%
assets/LeftNavEdit-*.js -1 bytes 8.68kB -0.01%
assets/StudentClassYear-*.js 38 bytes 8.49kB 0.45%
assets/cts-*.js 34 bytes 8.36kB 0.41%
assets/MyEffort-*.js -2 bytes 8.21kB -0.02%
assets/LeftNavMenus-*.js -8 bytes 8.0kB -0.1%
assets/ImportFiles-*.js -2 bytes 7.62kB -0.03%
assets/EmergencyContactView-*.js 1 bytes 7.61kB 0.01%
assets/ContentBlocks-*.js 38 bytes 7.6kB 0.5%
assets/ManageSessionCompetencies-*.js 39 bytes 6.88kB 0.57%
assets/AssessmentList-*.js 39 bytes 6.85kB 0.57%
assets/ManageCompetencies-*.js 39 bytes 6.65kB 0.59%
assets/ContentBlockHistory-*.js 40 bytes 6.59kB 0.61%
assets/UnitList-*.js -2 bytes 6.34kB -0.03%
assets/CompetenciesBundleReport-*.js 38 bytes 6.11kB 0.63%
assets/use-*.js 41 bytes 890 bytes 4.83%
assets/use-*.js 45 bytes 6.05kB 0.75%
assets/use-*.js 1 bytes 664 bytes 0.15%
assets/use-*.js 1 bytes 377 bytes 0.27%
assets/WebReports-*.js -6 bytes 6.02kB -0.1%
assets/BulkEncrypt-*.js 37 bytes 5.48kB 0.68%
assets/StudentSelect-*.js 40 bytes 5.46kB 0.74%
assets/ClinicalEffort-*.js -2 bytes 5.33kB -0.04%
assets/ManageEpas-*.js 39 bytes 5.23kB 0.75%
assets/ManageBundles-*.js 34 bytes 5.0kB 0.68%
assets/TermSelection-*.js -2 bytes 4.53kB -0.04%
assets/ManageLevels-*.js 35 bytes 4.51kB 0.78%
assets/FileAuditLog-*.js -1 bytes 4.3kB -0.02%
assets/AssessmentEpaEdit-*.js 1 bytes 3.79kB 0.03%
assets/ManageCourseCompetencies-*.js 40 bytes 3.71kB 1.09%
assets/StudentClassYearImport-*.js 39 bytes 3.67kB 1.07%
assets/BiorenderStudents-*.js 40 bytes 3.6kB 1.12%
assets/ManageMilestones-*.js 34 bytes 3.56kB 0.97%
assets/EffortDialogShell-*.js 34 bytes 3.43kB 1.0%
assets/ContentDiffDialog-*.js -2 bytes 3.39kB -0.06%
assets/StatusIcon-*.js -1 bytes 3.24kB -0.03%
assets/students-*.js 5 bytes 3.2kB 0.16%
assets/ScheduledCliWeeks-*.js -1 bytes 3.07kB -0.03%
assets/AssessmentCompetency-*.js 1 bytes 3.0kB 0.03%
assets/ManageDomains-*.js 35 bytes 2.79kB 1.27%
assets/PercentAssignTypeList-*.js -1 bytes 2.72kB -0.04%
assets/LevelSelect-*.js 41 bytes 2.65kB 1.57%
assets/PercentAssignTypeInstructors-*.js -1 bytes 2.44kB -0.04%
assets/CtsHome-*.js 41 bytes 2.3kB 1.82%
assets/DateRangeFilter-*.js 1 bytes 2.09kB 0.05%
assets/ManageRoles-*.js 36 bytes 1.95kB 1.88%
assets/PermissionSelector-*.js 40 bytes 1.38kB 2.99%
assets/RichTextEditor-*.js 1 bytes 1.33kB 0.08%
assets/ModifiedStamp-*.js 1 bytes 1.28kB 0.08%
assets/ReportDeptTabs-*.js -3 bytes 1.12kB -0.27%
assets/InstructorPageShell-*.js -5 bytes 993 bytes -0.5%
assets/CAHFSAuth-*.js 41 bytes 673 bytes 6.49% ⚠️
assets/EditButton-*.js 1 bytes 529 bytes 0.19%
assets/StatusBadge-*.js 41 bytes 464 bytes 9.69% ⚠️
assets/StatusBanner-*.js (Deleted) -1.73kB 0 bytes -100.0% 🗑️
assets/StatusBanner-*.css (Deleted) -831 bytes 0 bytes -100.0% 🗑️

Files in assets/GenericError-*.js:

  • ./src/components/SessionTimeout.vue → Total Size: 146 bytes

@codecov-commenter

codecov-commenter commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.84%. Comparing base (f8e26f4) to head (c9cd016).

Files with missing lines Patch % Lines
VueApp/src/components/SessionTimeout.vue 0.00% 33 Missing ⚠️
Additional details and impacted files
@@                         Coverage Diff                          @@
##           feature/session-timeout-endpoint     #306      +/-   ##
====================================================================
- Coverage                             41.85%   41.84%   -0.01%     
====================================================================
  Files                                   987      987              
  Lines                                 49518    49526       +8     
  Branches                               5843     5847       +4     
====================================================================
  Hits                                  20726    20726              
- Misses                                27882    27890       +8     
  Partials                                910      910              
Flag Coverage Δ
backend 39.91% <ø> (ø)
frontend 58.06% <0.00%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
VueApp/src/components/SessionTimeout.vue 0.00% <0.00%> (ø)

@rlorenzo
rlorenzo force-pushed the feature/session-timeout-spa branch from e956bcb to d5226aa Compare August 8, 2026 06:10
@rlorenzo
rlorenzo force-pushed the feature/session-timeout-spa branch 2 times, most recently from 6081219 to 281d071 Compare August 8, 2026 18:08
@rlorenzo
rlorenzo force-pushed the feature/session-timeout-spa branch from 281d071 to 2f32839 Compare August 10, 2026 03:27
@rlorenzo
rlorenzo requested a lite review from Copilot August 10, 2026 05:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Suppressed comments (3)

VueApp/src/components/SessionTimeout.vue:60

  • fetch(viperHome + "RefreshSession") assumes VITE_VIPER_HOME always ends with a trailing slash. If it is configured as "/2" (no trailing slash), this becomes "/2RefreshSession" and the refresh button will never work on that environment.
    fetch(viperHome + "RefreshSession")
        .then((r) => (r.ok ? r.json() : Promise.reject(new Error("RefreshSession returned " + r.status))))

VueApp/src/components/SessionTimeout.vue:9

  • sessionTimeoutUrl is built by direct string concatenation and relies on VITE_API_URL always ending with a trailing slash. If someone configures it as "/api" (no trailing slash), the fetch will go to "/apisessionTimeout" and silently fail/reschedule forever.

This issue also appears on line 59 of the same file.

const sessionTimeoutUrl = `${import.meta.env.VITE_API_URL}sessionTimeout`

VueApp/src/components/SessionTimeout.vue:53

  • The reschedule delay uses the magic number 300000 (ms). Using a seconds/minutes expression makes this easier to audit and prevents unit mixups during future edits.
            sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, 300000)

- Point the poll at the VIPER 2 endpoint via VITE_API_URL, dropping the
  CFM URL that passed the login id as an unauthenticated query parameter
- Keep plain fetch rather than useFetch, which reports every failure
  through the global error store: that would banner a silent five minute
  poll and fire the auth handler while our dialog offers a log in
- Carry over the Razor fixes, since this file had the same defects:
  reject non-OK responses, report a failed extend in a StatusBanner
  while offering both Refresh Session and Log in,
  reschedule after a failed poll rather than stopping for the life of
  the page, stand the warning down when the session is extended
  elsewhere, and render midnight as 12 AM rather than 0 AM
@rlorenzo
rlorenzo force-pushed the feature/session-timeout-spa branch from 2f32839 to c9cd016 Compare August 10, 2026 05:56
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.

3 participants