Skip to content

VPR-59: Migrate CMS from VIPER 1 to VIPER 2#242

Closed
rlorenzo wants to merge 40 commits into
mainfrom
VPR-59-migrate-cms
Closed

VPR-59: Migrate CMS from VIPER 1 to VIPER 2#242
rlorenzo wants to merge 40 commits into
mainfrom
VPR-59-migrate-cms

Conversation

@rlorenzo

@rlorenzo rlorenzo commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the legacy VIPER 1 CMS to VIPER 2 as a new API + Vue SPA: file
management, content blocks, left-nav menus, and link collections, plus the
supporting trash lifecycle and scheduler wiring.

What's included

  • Files: management API + UI (upload, edit, encrypt, bulk-encrypt, import
    from the legacy VIPER webroot, VIPER 2 download links, ZIP download, audit
    trail, download rate limiting, user photo endpoints).
  • Content blocks: management UI with version history and diffs; inline file
    upload that stages files client-side and only uploads on Save, with per-file
    conflict resolution (rename / overwrite / reuse) and rollback on a failed save.
  • Left-nav menus & link collections: management with a unified reorder UX.
  • Trash lifecycle: soft-delete with owner-scoped trash (non-admins see only
    files they deleted; admins see the whole trash) and a 30-day auto-purge
    scheduled job that restores legacy retention.
  • Hub & navigation: redesigned CMS home with recent activity, CMS left nav,
    and access for granular-permission users.
  • Cross-cutting: HTML sanitizer service, accessibility and responsive polish
    across CMS pages.

Trash retention / auto-purge (deployment-sensitive)

This branch adds CmsTrashPurgeScheduledJob (recurring-job id cms:trash-purge,
cron 0 3 * * * — daily at 3:00 AM Pacific). When enabled it permanently
deletes
CMS files that were soft-deleted more than 30 days ago (DB record +
bytes on disk), matching the legacy VIPER 1 30-day purge routine.

The legacy VIPER 1 scheduled task performs the same 30-day purge. To keep both
from purging in parallel, the job is gated by a config flag and ships
disabled
: it stays registered and fires on cron, but no-ops until the flag is
turned on. It is only enabled after the legacy job is retired.

  • Flag: Cms:TrashPurgeEnabled (config / SSM). Absent or false = disabled.
  • Defaulted to false in appsettings.json, so every environment is off until
    it explicitly opts in. Unlike a Hangfire dashboard tweak, this survives app
    restarts and IIS app-pool recycles.

Deployment steps

  1. Deploy as usual (merge to Development → TEST; then main → PROD). No action
    needed to keep the purge off — it ships disabled (Cms:TrashPurgeEnabled is
    false by default). The legacy VIPER 1 task stays the sole purge.
  2. (Optional) Confirm in the Hangfire dashboard at /scheduler/dashboard
    (requires the SVMSecure.CATS.scheduledJobs RAPS permission) that the
    cms:trash-purge runs log "disabled … skipping" (Hangfire.Console output).

Cutover (once the legacy job is turned off)

  1. Disable/remove the legacy VIPER 1 scheduled purge task.
  2. Set Cms:TrashPurgeEnabled = true (via SSM in deployed envs) and
    restart/redeploy so the app picks up the new config value.
  3. Verify: use Trigger now on cms:trash-purge once and check the console
    output confirms a real run and the purged-file count. From then on the daily
    3:00 AM run is the sole purge.

To roll back to the legacy purge at any time, set the flag back to false and
restart — no code change or dashboard surgery required.

Testing

  • npm run test (backend + frontend), lint, and build verification pass
    (enforced by the pre-commit hook).
  • UI flows exercised with Playwright MCP.

rlorenzo added 25 commits July 1, 2026 20:19
- New /api/cms/files endpoints: paged list with filters, get, folders,
  multipart upload, edit with permission/person deltas, encrypt toggle,
  file replacement, soft delete/restore, admin permanent delete, and a
  queryable fileAudit log matching legacy audit actions
- CmsFileCrypto implements ColdFusion-compatible AES-128-ECB with
  UU-encoded per-file keys so both systems can read each other's
  encrypted files during coexistence; CMS.cs decrypt now delegates to it
- CmsFileStorageService validates folders against the storage root
  (containment + top-level whitelist), uploads via temp-then-move, and
  reproduces legacy _0.._999 collision renaming
- Encrypt/decrypt-in-place writes are atomic (temp file + move) to
  avoid corrupting files on interrupted writes
- Files page at /CMS/ManageFiles (not /CMS/Files, which is the MVC
  download handler): server-paged list with folder/status/search/
  encrypted filters, upload and edit dialogs, soft delete/restore
- Shared PermissionSelector and PersonSelector backed by new
  /api/cms/options endpoints, since the RAPS permission list requires
  RAPS roles + 2FA and Directory search is too heavy for type-ahead
- File audit log page with action/user/date/search filters and
  per-file filtering via ?fileGuid=
- CMS home cards shown per user permission
- ViperFetch gains postForm/putForm for multipart uploads
- Extend /api/CMS/content: status/system/section/search filters,
  get-by-id, version history list and retrieval, restore, admin-gated
  permanent delete, content-only PATCH, file attach/detach, and
  ModifiedOn-based conflict detection (409 on stale saves)
- Fix history semantics to match legacy: store the previous version
  with its original author/date (was writing the new content, and
  create wrote a history row with block id 0)
- List endpoint projects without the Content column to avoid loading
  every block body; attached file GUIDs are validated on save
- UI: content block list page and editor (QEditor, settings sidebar,
  permissions, file attachment via search, version history with
  restore-by-save, conflict dialog)
- New /api/cms/left-navs (ManageNavigation): menu CRUD with cascade
  delete, and a batch item save that adds/updates/deletes/reorders in
  one call, replacing the legacy DataTables editor flow
- Fix nav display filter: items with no permissions are now visible to
  any logged-in user (legacy behavior); previously they were hidden
  from rendered menus on Layout/Home/Students pages
- Validate left nav item URLs with SafeUrlAttribute
- UI: menu list page and item editor with drag/arrow reordering and
  per-item permission selectors; shared EditButton/ModifiedStamp
  components extracted from list pages
- POST /api/cms/files/import moves files from the legacy VIPER webroot
  (CMS:LegacyWebrootPath) into the managed store with per-file results;
  the original path is stored as oldURL so existing links keep working,
  matching legacy move-and-track semantics; source paths are contained
  to the webroot and removed only after a fully successful import
- POST /api/cms/files/bulk-encrypt encrypts unencrypted files in place
  with per-file results; a failed key save rolls the file back to
  plaintext so disk and DB never disagree, and batch failures clear the
  change tracker so one bad file cannot poison the rest
- UI: Import page (paths textarea keeps failures for retry) and Bulk
  Encrypt page (multi-select over unencrypted files); list pages now
  surface fetch errors instead of showing silent empty tables
- /api/cms/photos serves user photos to any authenticated user by
  mailId, loginId, or iamId (legacy userPhoto.cfc parity), resolving
  ids through AAUD
- altPhoto=true serves the alternate ProfilePhotos/{iamId}.jpg when
  present; invalid or traversal-shaped ids fall back to the default
  photo without touching disk outside the photo roots
- ID-card photo lookup, caching, and the nopic fallback reuse the
  Students area PhotoService rather than duplicating the pipeline
- ZIP requests get a strict per-user token bucket (archives are
  assembled in memory); single files get a generous sliding window
- Buckets key on CAS login, falling back to the proxy-resolved client
  IP for anonymous requests; rejections return 429 with Retry-After
- Limits are configurable under CMS:DownloadRateLimit; invalid values
  fall back to defaults instead of failing downloads
- Left nav now mirrors legacy CMS (Home, content blocks, files, left-nav
  sections, permission-gated); nav, hub cards, and page titles share
  "Manage X" wording, and /CMS/ canonicalizes to /CMS/Home so the Home
  link highlights
- Hub: permission-filtered 2x2 tool cards plus a recent-activity rail
  (recently edited blocks/files deep-link to their editors via existing
  list APIs); zero-permission visitors get an info banner instead of a
  blank page
- Import and Bulk Encrypt moved off the hub into a File Tools menu on
  Manage Files; the Upload File nav link opens the upload dialog via
  ?upload=1
- Shell a11y (all areas): visible focus ring on flat card actions, left
  nav items announce as links, drawer landmark labeled, new "Skip to
  section menu" link, and in-page anchor navigation no longer steals
  focus or scroll position
- Shared router: scroll position survives query-only navigation, hash
  links scroll to their target; createSpaRouter.ts renamed to
  create-spa-router.ts (kebab convention) with new unit tests
- Workspace anchors now use the brand link color and dense table action
  buttons keep a 24px minimum hit area (base.css, applies app-wide)
- Icon-only status and result indicators get screen-reader text; links
  that open new windows announce it
- Loading buttons use the design-system spinner+label slot; deleted
  banner on content block edit uses StatusBanner; sidebar/card headings
  are real h2s
- Create buttons match nav wording (Add Content Block, Add Left-Nav
  Menu, Add Collection); removed the duplicate Audit Log button on
  Manage Files
- Hand-rolled "file(s)" plurals replaced with inflect(); inline styles
  extracted to classes
- File URLs now point at the VIPER2 download handler (/CMS/Files,
  permission-checked, serves public files anonymously) instead of the
  ColdFusion /cms/files path; host keeps non-default ports and the URL
  respects PathBase on TEST/PROD
- Manage Files: Old URL column collapses to a compact link (full URL in
  tooltip) that resolves through the download handler; filters sync to
  the query string for shareable deep links; new "Public only" toggle
  (isPublic was already supported by the API)
- Folder filter lists disk folders unioned with folders that exist only
  on file records (e.g. VMDEVSignatures in dev), with an explicit "All"
  default; upload destinations keep the disk-only allow-list since the
  first path segment is a security check
- Hub cards render as equal-height tiles via a CSS grid with 1fr rows
Matches the legacy UI and exercises the real resolution chain old links
take (VIPER 1 404 handler -> oldURL lookup) instead of calling the CMS
handler lookup directly.
- Replace the MakeUnique flag with a check-name endpoint plus explicit
  FileName/Overwrite request fields, so an upload detects a name
  conflict up front and prompts to rename or overwrite
- Add an import dry-run preview that reports the resulting names and
  any auto-renames without moving files, sharing path validation with
  the real import so it can't promise what import would reject
- Reconcile the on-disk file back to its pre-save state when a metadata
  save fails across create, update, import, and bulk-encrypt
- Require a replacement upload to keep the original file's extension so
  a stored .pdf can't end up holding .png bytes
- Add per-folder file counts for the filter dropdowns, and extract
  shared BreadcrumbHeading and StatusIcon components
- Surface validation and save errors in an inline banner beside the
  submit button on the content block, left-nav, file, and link forms,
  replacing transient toasts that scrolled out of view.
- Guard unsaved edits on those forms: route-leave prompts on the pages
  and confirm-on-close on the dialogs, via useUnsavedChanges.
- Make Add Left-Nav Menu a settings-only modal opened from the list
  (and the nav via ?add=1) that routes to the page for item editing;
  a shared LeftNavMenuSettingsFields backs both the modal and page.
- Auto-enable Public access when a content block's System is set to
  "Public", with an inline notice, since AllowPublicAccess is the only
  field that actually gates unauthenticated visibility.
- Add a "Public only" filter to the content block list, backed by a new
  isPublic parameter on the list endpoint and service.
- Replace "Back to ..." buttons with breadcrumb headings and align the
  page titles and entry labels to "Add" (including "Add File").
- Default the System filter to an explicit "All" on the menu and block
  lists.
- List pages (Files, Content Blocks, Left Nav, Audit, Bulk Encrypt)
  collapse to stacked cards below the mobile breakpoint instead of
  scrolling wide tables
- a11y: give the rich-text editor an accessible name, mark the System
  select required, and make the Link card keyboard-reachable via its
  anchor
- Align the file and Effort audit-trail filter layouts (outlined
  controls; search and Clear Filters on their own row); this accounts
  for the Effort/AuditList change in a CMS commit
- Replace remaining hard-coded colors with brand tokens and move inline
  dialog widths to shared classes
- Cross-block edit history page (linked from the CMS home) with
  per-version diffs: diff a saved version against its predecessor, or
  diff the editor's current draft against any past version. Diffs use
  htmldiff.net and are re-sanitized server-side so only ins/del markup
  reaches the browser.
- Restrict the public content-block lookup to active blocks so
  soft-deleted content is no longer served to unauthenticated callers.
- Frontend: Vitest/@vue/test-utils suites for CMS utils, components,
  dialogs, selectors, list pages, and the content-history and diff views
- Backend: controller tests for files, content, left-nav, link
  collections, and user photos, plus content-block permission filtering
- Add SanitizeDiff cases proving the diff sanitizer strips XSS while
  keeping the ins/del change markers
PostLinkCollection, CreateLinkCollectionTagCategory, and PostLink passed
a local variable name to CreatedAtAction instead of an action method, so
the 201 Location header URL generation would fail at runtime through
routing. Point each at its existing collection GET action; these
resources have no get-by-id endpoint to target.
…tions

- Extract a shared, accessible SortableList (drag + up/down, "just
  moved" highlight, live-region announcements, reduced-motion fallback)
  to replace the duplicated per-page drag handlers
- Track left-nav menu settings and items as independently dirty/saved
  sections; rename "Revert" to "Discard Item Changes"
- Add a per-row Delete to link-collection links
- Restore endpoints return 204 instead of 200 with an empty body, so the
  client no longer reports a false "failed to restore" (files and blocks)
- Deleting a link collection now removes its links, link tags, and tag
  categories first, fixing a foreign-key 500
- Creating a tag category returns the generated id and validates the id
  and route/body, so the follow-up order save no longer 400s or 500s
- Content-block list filters initialize from and sync to the URL,
  matching the Files list (deep-linkable views)
- Snapshot a file's original bytes before an overwrite or replacement
  and restore them if the database save fails, so a failed create or
  edit can't orphan the new bytes or destroy the original.
- If the restore itself fails, keep the backup and log its path for
  recovery instead of deleting the only remaining copy.
- Import preview reserves each planned name so sources sharing a base
  name preview the unique names the import will actually assign.
- Treats "//host" as off-site and blocks it; backslash variants
  ("/\", "\\", "\/") normalize to the same bypass and are rejected
  too. Ordinary relative paths still validate.
- The /CMS/Home route and the /CMS/ root redirect both accept any
  granular CMS permission (Files, Content Blocks, Left Nav, ...), not
  just base SVMSecure.CMS, reading one shared set so the guard and the
  canonicalization can't drift.
- Adopt the shared StatusBadge over raw q-badge and an ad-hoc palette,
  and replace hard-coded px/hex with rem and brand tokens.
- Improve a11y (SR-hidden status icons, grouped/labelled left-nav rows)
  and keep button labels visible while loading.
- ContentBlock watches only its name prop (drops a duplicate fetch),
  renders null-safe, and shares heading styles via the global sheet.
- ContentBlockEdit skips the section-path/file APIs for create-only
  users so they don't fire requests that can only 403; the left-nav
  list now surfaces load errors.
- Add an inline uploader to the content-block editor: files stage
  client-side and only upload on Save, using the block's folder and
  permissions, with per-file conflict resolution (rename/overwrite/reuse)
  and rollback of newly created files if the save then fails
- Make the block's VIPER section path required on create and read-only
  after, sourced from a new content-scoped /content/folders endpoint so
  create-only users can pick a folder without AllFiles
- Restore legacy 30-day trash retention: a daily CmsTrashPurgeScheduledJob
  permanently deletes files (record + disk) past the cutoff, detaching
  content-block links first and tolerating per-file disk failures. Gated
  by a Cms:TrashPurgeEnabled flag (default off) so it ships disabled until
  the legacy VIPER 1 purge is retired and survives app restarts
- Scope the trash to its owner for non-admins (restore and list match on
  ModifiedBy, failing closed) and open a Trash entry on the hub; surface
  the purge date on deleted files
- Drop the System and Order columns from the content-blocks list; polish
  responsive list headers, stacked permission chips, and link a11y
@codecov-commenter

codecov-commenter commented Jul 2, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 122.19kB (5.75%) ⬆️⚠️, exceeding the configured threshold of 5%.

Bundle name Size Change
viper-frontend-esm 2.25MB 122.19kB (5.75%) ⬆️⚠️

Affected Assets, Files, and Routes:

view changes for bundle: viper-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/GenericError-*.js 563 bytes 539.09kB 0.1%
assets/vendor-*.js -25 bytes 271.77kB -0.01%
assets/GenericError-*.css 1.3kB 204.67kB 0.64%
assets/TermManagement-*.js 6 bytes 52.39kB 0.01%
assets/schedule-*.js 5 bytes 49.73kB 0.01%
assets/SortableList-*.js (New) 45.64kB 45.64kB 100.0% 🚀
assets/CourseDetail-*.js -7 bytes 39.32kB -0.02%
assets/PhotoGallery-*.js 3 bytes 36.08kB 0.01%
assets/InstructorEdit-*.js 2 bytes 28.56kB 0.01%
assets/InstructorList-*.js -7 bytes 25.91kB -0.03%
assets/effort-*.js 12 bytes 23.18kB 0.05%
assets/CrossListedCoursesSection-*.js -4 bytes 22.29kB -0.02%
assets/Files-*.js (New) 19.83kB 19.83kB 100.0% 🚀
assets/ClinicianScheduleView-*.js -1 bytes 18.91kB -0.01%
assets/MultiYearReport-*.js 1 bytes 18.75kB 0.01%
assets/ContentBlockEdit-*.js (New) 17.68kB 17.68kB 100.0% 🚀
assets/AuditList-*.js 28 bytes 17.34kB 0.16%
assets/EmergencyContactForm-*.js 5 bytes 17.16kB 0.03%
assets/RotationScheduleView-*.js -1 bytes 16.97kB -0.01%
assets/MyAssessments-*.js -5 bytes 16.47kB -0.03%
assets/ManageLinkCollections-*.js -42.48kB 15.13kB -73.73%
assets/_plugin-*.js -2 bytes 13.24kB -0.02%
assets/dist-*.js 4.97kB 11.97kB 70.89% ⚠️
assets/CourseImportDialog-*.js 1 bytes 10.83kB 0.01%
assets/CmsHome-*.js 9.71kB 10.02kB 3102.56% ⚠️
assets/cts-*.js 1 bytes 8.33kB 0.01%
assets/LeftNavMenus-*.js (New) 8.01kB 8.01kB 100.0% 🚀
assets/LeftNavEdit-*.js (New) 7.98kB 7.98kB 100.0% 🚀
assets/ContentBlocks-*.js (New) 7.41kB 7.41kB 100.0% 🚀
assets/ImportFiles-*.js (New) 7.12kB 7.12kB 100.0% 🚀
assets/ManageCompetencies-*.js -1 bytes 6.61kB -0.02%
assets/UnitList-*.js 1 bytes 6.35kB 0.02%
assets/WebReports-*.js -467 bytes 5.9kB -7.33%
assets/ContentBlockHistory-*.js (New) 5.54kB 5.54kB 100.0% 🚀
assets/use-*.js (New) 823 bytes 823 bytes 100.0% 🚀
assets/BulkEncrypt-*.js (New) 5.34kB 5.34kB 100.0% 🚀
assets/FileAuditLog-*.js (New) 4.5kB 4.5kB 100.0% 🚀
assets/cms-*.js 2.05kB 3.42kB 149.53% ⚠️
assets/ContentDiffDialog-*.js (New) 3.34kB 3.34kB 100.0% 🚀
assets/StatusIcon-*.js (New) 2.16kB 2.16kB 100.0% 🚀
assets/ManageLinkCollections-*.css -2.05kB 2.02kB -50.48%
assets/DateRangeFilter-*.js (New) 1.96kB 1.96kB 100.0% 🚀
assets/SortableList-*.css (New) 1.62kB 1.62kB 100.0% 🚀
assets/ContentBlockEdit-*.css (New) 1.51kB 1.51kB 100.0% 🚀
assets/LeftNavMenuSettingsFields-*.js (New) 1.5kB 1.5kB 100.0% 🚀
assets/LeftNavEdit-*.css (New) 1.25kB 1.25kB 100.0% 🚀
assets/PermissionSelector-*.js (New) 987 bytes 987 bytes 100.0% 🚀
assets/ListCardField-*.js (New) 924 bytes 924 bytes 100.0% 🚀
assets/ContentDiffDialog-*.css (New) 803 bytes 803 bytes 100.0% 🚀
assets/ModifiedStamp-*.js (New) 735 bytes 735 bytes 100.0% 🚀
assets/CmsHome-*.css (New) 577 bytes 577 bytes 100.0% 🚀
assets/EditButton-*.js (New) 528 bytes 528 bytes 100.0% 🚀
assets/ContentBlock-*.js 14 bytes 498 bytes 2.89%
assets/BreadcrumbHeading-*.js (New) 448 bytes 448 bytes 100.0% 🚀
assets/file-*.js (New) 160 bytes 160 bytes 100.0% 🚀
assets/WebReports-*.css -204 bytes 137 bytes -59.82%
assets/LeftNavMenuSettingsFields-*.css (New) 105 bytes 105 bytes 100.0% 🚀
assets/ImportFiles-*.css (New) 104 bytes 104 bytes 100.0% 🚀
assets/ContentBlockHistory-*.css (New) 103 bytes 103 bytes 100.0% 🚀
assets/FileAuditLog-*.css (New) 100 bytes 100 bytes 100.0% 🚀
assets/StatusIcon-*.css (New) 82 bytes 82 bytes 100.0% 🚀
assets/ContentBlock-*.css (Deleted) -99 bytes 0 bytes -100.0% 🗑️

Files in assets/CmsHome-*.js:

  • ./src/CMS/components/ActivityRow.vue → Total Size: 226 bytes

Files in assets/ContentDiffDialog-*.js:

  • ./src/CMS/components/ContentDiffDialog.vue → Total Size: 244 bytes

Files in assets/StatusIcon-*.js:

  • ./src/CMS/components/DeleteRestoreButtons.vue → Total Size: 168 bytes

Files in assets/DateRangeFilter-*.js:

  • ./src/CMS/components/DateRangeFilter.vue → Total Size: 153 bytes

Files in assets/ContentBlock-*.js:

  • ./src/CMS/components/ContentBlock.vue → Total Size: 144 bytes

@codecov-commenter

codecov-commenter commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.20699% with 355 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.21%. Comparing base (ae61c44) to head (13b8ecc).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
VueApp/src/CMS/pages/ManageLinkCollections.vue 33.84% 42 Missing and 1 partial ⚠️
web/Areas/CMS/Controllers/CMSOptionsController.cs 0.00% 41 Missing ⚠️
VueApp/src/CMS/pages/ContentBlockEdit.vue 83.66% 18 Missing and 15 partials ⚠️
VueApp/src/CMS/pages/Files.vue 80.71% 23 Missing and 4 partials ⚠️
web/Areas/CMS/Controllers/CMSFilesController.cs 82.91% 20 Missing and 7 partials ⚠️
web/Areas/CMS/Controllers/CMSContentController.cs 79.27% 13 Missing and 10 partials ⚠️
VueApp/src/CMS/components/FileFormDialog.vue 86.92% 10 Missing and 10 partials ⚠️
VueApp/src/components/SortableList.vue 71.42% 13 Missing and 5 partials ⚠️
VueApp/src/CMS/pages/ContentBlocks.vue 84.09% 12 Missing and 2 partials ⚠️
VueApp/src/CMS/pages/LeftNavMenus.vue 80.82% 12 Missing and 2 partials ⚠️
... and 17 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #242      +/-   ##
==========================================
+ Coverage   44.58%   48.21%   +3.63%     
==========================================
  Files         896      969      +73     
  Lines       51733    56298    +4565     
  Branches     4834     5509     +675     
==========================================
+ Hits        23063    27145    +4082     
- Misses      28098    28372     +274     
- Partials      572      781     +209     
Flag Coverage Δ
backend 47.44% <76.65%> (+2.75%) ⬆️
frontend 57.44% <82.57%> (+15.08%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Comment thread test/CMS/CMSFilesControllerTests.cs Fixed
Comment thread test/CMS/CmsFileServiceTests.cs Fixed
Comment thread web/Areas/CMS/Services/CmsFileImportService.cs Fixed
Comment thread web/Areas/CMS/Services/CmsFileCrypto.cs Fixed
Comment thread test/CMS/CMSContentPermissionTests.cs Fixed
Comment thread test/CMS/CMSContentPermissionTests.cs Fixed
Comment thread test/CMS/CMSFilesControllerTests.cs Fixed
Comment thread test/CMS/CmsFileServiceTests.cs Fixed
Comment thread web/Areas/CMS/Services/CmsContentBlockService.cs Fixed
Comment thread web/Areas/CMS/Services/CmsContentBlockService.cs Fixed
Comment thread web/Areas/CMS/Services/CmsFileImportService.cs Fixed
- Treat both '/' and '\' as separators for legacy import lines and
  user-supplied file/folder names; traversal and invalid-name checks no
  longer rely on host-OS path APIs (Linux CI let "..\" and "<>" through)
- Build test path expectations with Path.Join instead of hardcoded '\'
- Clear ReSharper/CodeQL findings in the file services: specific catch
  filter on the bulk-encrypt save, disposed test streams, redundant
  qualifiers and suppressions
rlorenzo added 14 commits July 1, 2026 22:03
- Drop always-true null checks and dead coalesces flagged by NRT
  contracts; flatten EF search conditions CodeQL called too complex
- [JsonRequired] on LeftNavItemEdit ints so under-posted JSON is a 400
  instead of silently creating items or clearing headers
- Test hygiene: dispose form-file streams, remove redundant suppressions
  and qualifiers, pass TestContext cancellation token
- New useServerTable/useUrlFilteredTable composables own the shared
  QTable pagination + URL-synced filter plumbing, removing the clone
  groups jscpd flagged vs main (delta now +0)
- Split high-complexity save/upload functions into small helpers;
  fallow suppressions remain only for template-size synthetics
- GET content/fn/{friendlyName} projected raw ContentBlock entities:
  attached files exposed their AES key and server path, plus full
  unsanitized ContentHistory and permission rows, to anonymous callers
- Return ContentBlockDto (same consumer fields); regression test asserts
  the payload carries no key/filePath/contentHistories
- X-Content-Type-Options: nosniff on all /CMS/Files responses
- html/htm/xhtml/svg force Content-Disposition: attachment (keyed off
  the stored file path, not the user-supplied fn) so an uploaded page
  cannot render in the app origin; html stays on the allow-list for
  legacy files
- Align DTO MaxLengths to DB columns (overlong input was a 500 at
  SaveChanges); validate each permission string via MaxLengthEach
- Reject unknown item ids on batch save (409) instead of silently
  re-creating concurrently deleted items; duplicate ids 400 from the
  service; friendly-name uniqueness enforced case-insensitively
- DateRangeHelper no longer overflows on DateTime.MaxValue.Date filters
- CmsNavMenu takes IUserHelper via constructor like LeftNavMenu
- EF.Parameter on the attached-file GUID Contains check
- Document the legacy-parity CATS.Admin gate (verified in CF source)
- 75 new Vitest specs: content-block save/conflict flows, inline
  upload commit paths, file dialog conflicts, table composables,
  CmsHome personas, list page actions
- Worst-covered files now 76-100% lines; patch coverage ~60% -> ~85%
- Mirrors the files list: ApiPagination envelope, whitelisted sort
  columns, count-before-page; list no longer loads every block client-
  side and the rows-per-page All option is gone
- ContentBlocks.vue now uses useServerTable like its sibling lists;
  card-mode breakpoint standardized to lt.sm
- Link-collection edit dialog now guards unsaved tag edits on close,
  matching the link dialog and the DESIGN.md close-guard pattern
- Filter bars unified to dense outlined across the five list pages
- Tag colors moved off semantic status roles to a categorical palette;
  diff tints and focus ring tokenized; content-block headings get a
  descending scale; px to rem; deep-watcher and stale ?upload=1
  query-sync race fixed
- loadBlocks sorted a default (alphabetical, 50-row) page client-side,
  so recent edits past the first page never appeared; request
  sortBy=modifiedOn perPage=5 server-side like loadFiles, with a spec
  pinning the params
- Drop the bespoke field-error pill on the link form, residual px in
  InlineFileUpload, and the text-h6 that weakened the rail heading
- Hub boxes now match the left nav's three groups in its order, with
  Link Collections inside Content Blocks and the nav's Add File /
  Add Left-Nav Menu deep-links as card actions
- Warn file managers when trashed files purge within 7 days (new
  deletedOn sort on the files list backs it); recently deleted files
  join the activity rail
- Rail items link to their edit history / audit trail and open the
  latest change as an inline diff (live content vs newest saved
  version via the editor's POST diff contract); shared
  useContentDiffViewer replaces three parallel viewer implementations
- Row actions fade in on hover/focus, stay visible on touch
- The rail's row markup (actions cluster, verb caption, hover reveal)
  pushed RecentActivity's template past the complexity threshold; the
  row is now its own component and the shared item types live in
  CMS/types
@rlorenzo

rlorenzo commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@rlorenzo

rlorenzo commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by a 6-PR stack so each slice fits under GitHub Copilot's 20,000-line review limit: #245 (files) -> #246 (content blocks + left nav) -> #247 (import/photos/rate limiting) -> #248 (hub/a11y/UX) -> #249 (responsive/diffs/tests) -> #250 (hardening + review fixes, final slice). Note this PR's branch holds the stale pre-rebase history (the local branch was rebased onto main on 2026-06-23 and not force-pushed); the stack branches carry the current, test-verified history. Closing in favor of the stack.

@rlorenzo rlorenzo closed this Jul 2, 2026
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.

4 participants