Skip to content

Report 32 -- Comprehensive Investigation Findings (2026-02-28)

Date: 2026-02-28 Scope: Consolidated findings from 4 parallel investigation agents Status: ALL findings are definitive -- no assumptions, only verified facts Principle: "We can't have assumptions, we need direct guidelines" -- Marco Teodoro


Priority Summary

# Finding Priority Action Owner
1 M3 contact-us area placement STALE Remove from week plan Marco
2a Calculator dictionary keys P1 Create v8 keys in v17 dictionary Marco + Joao
2b Calculator frequency comparison P1 Fix comparisons to single letters Joao
2c Calculator CSS files P0 Verify CSS exists in v17 wwwroot Rodrigo
2d CalculatorSmall no dictionary P2 Document, add if multi-lang needed Joao
3a Cache busting missing P0 Add asp-append-version to all tags Rodrigo
3b masterLogin no scripts P0 Add RenderScripts/RenderCss Joao
3c popUpForms commented out P1 Investigate and fix Joao
3d Menu JS broken P1 Fix ScriptHelper registrations Rodrigo
3e Default async:true P1 Change default to async:false Rodrigo
3f Section name mismatch P2 Audit and fix section names Rodrigo
4a Home/ vs home/ paths P1 Normalize to lowercase Joao
4b GlobalAddress broken paths P1 Fix broken references Joao
5 GalleryService missing P1 Migrate from v8 Joao
6 UserServiceComposer P1 Create INotificationHandler Marco
7 CommonBondRange table P1 Migrate custom table + data Marco
8 Email templates P2 Document as client-specific setup Marco
9 ListViews P3 Verify v17 native list views work Joao

Totals: 3x P0, 9x P1, 3x P2, 1x P3, 1x STALE


1. M3 Contact-Us Area Placement -- STALE (Not a Bug)

STALE -- Not a bug. Remove from week plan.

Investigation Result

The v8 grid JSON for the contact-us page has a single section with grid=12, containing one row with two areas (grid=6 each). The migration tool correctly maps:

  • Area 0 (grid=6) --> first area wrapper with colSpan=6
  • Area 1 (grid=6) --> second area wrapper with colSpan=6

Both area wrappers are placed in separate areas within the BlockGrid structure. The grid migration is correct.

Root Cause of False Report

The issue reporter was likely looking at the wrong page or misinterpreting the block structure in the v17 backoffice. The BlockGrid editor displays blocks differently than v8's grid editor -- the visual layout can appear confusing when areas are side-by-side.

Action

Remove M3 from the Week Plan. Mark as STALE. No code changes needed.


2. Calculator Deep-Dive -- Definitive Findings

The calculator components have 4 distinct issues, each requiring separate fixes.

2a. Dictionary Key Naming Mismatch (P1)

P1 -- Dictionary lookups return null, showing key names as literal text

V8 Dictionary Keys (31 total, 4 naming conventions)

V8 uses 31 dictionary keys across 4 different prefix/notation styles:

Dot notation (calculator.*):

calculator.TypeOfLoan
calculator.YourMonthlyRepayments
calculator.LoanAmount
calculator.LoanTerm
calculator.Frequency
calculator.Apply
calculator.Disclaimer

Underscore notation (Calc_*):

Calc_repayments
Calc_FrequencyMonthly
Calc_FrequencyFortnightly
Calc_FrequencyWeekly
Calc_APR
Calc_TotalRepayable
Calc_CostOfCredit

Component-specific (CalculatorLarge_*):

CalculatorLarge_Submit
CalculatorLarge_Enquire
CalculatorLarge_Title
CalculatorLarge_Subtitle

Small calculator (CalculatorSmallRepayment_*):

CalculatorSmallRepayment_Title
CalculatorSmallRepayment_Apply
CalculatorSmallRepayment_Disclaimer

V17 Dictionary Keys (25 total, normalized)

V17 normalized all keys to Calc.* dot notation:

Calc.TypeOfLoan
Calc.MonthlyRepayments
Calc.LoanAmount
Calc.LoanTerm
Calc.Frequency
Calc.Apply
Calc.Disclaimer

The Problem

When a Razor view calls @Umbraco.GetDictionaryValue("calculator.TypeOfLoan"), Umbraco looks for the key calculator.TypeOfLoan in the dictionary. Since v17 only has Calc.TypeOfLoan, the lookup returns null, and Umbraco displays the key name as literal text on the page.

The Fix

Option A (RECOMMENDED): Create all 31 v8 keys in the v17 dictionary.

This is the correct approach because:

  1. The v8 translations already exist in the source database
  2. The dictionary migration step should copy them -- if it does not, create them manually via uSync export/import or the Umbraco backoffice API
  3. No view changes needed
  4. Preserves Irish translations without re-translation effort

Option B (NOT recommended): Update v17 views to use v8 key names.

This loses the normalization work already done and creates inconsistency.

Implementation Steps

  1. Export dictionary items from v8 database: query cmsDictionary + cmsLanguageText tables
  2. Import into v17 via uSync or Umbraco Management API
  3. Verify all 31 keys resolve in both English and Irish locales

2b. Frequency Comparison Mismatch (P1)

P1 -- Calculator frequency selection renders wrong repayment amounts

The Problem

V8 calculatorInput.cshtml compares frequency values using single letters:

// v8 -- CORRECT (matches what the dropdown stores)
if (frequency == "W") { /* weekly */ }
if (frequency == "F") { /* fortnightly */ }
if (frequency == "M") { /* monthly */ }

V17 calculatorInputSmall.cshtml compares using full words:

// v17 -- BROKEN (never matches the dropdown value)
if (frequency == "Weekly") { /* weekly */ }
if (frequency == "Fortnightly") { /* fortnightly */ }
if (frequency == "Monthly") { /* monthly */ }

The defaultLoanCalcFrequency dropdown property stores single-letter values ("W", "F", "M"). The v17 comparisons never match, so the frequency multiplier defaults incorrectly.

The Fix

Change calculatorInputSmall.cshtml comparisons from full words to single letters:

// FIXED
if (frequency == "W") { /* weekly */ }
if (frequency == "F") { /* fortnightly */ }
if (frequency == "M") { /* monthly */ }

This is a direct find-and-replace in one file. No other files are affected.


2c. Calculator CSS Files (P0)

P0 -- If CSS files are missing, calculators render as unstyled HTML

V8 CSS Inventory

V8 has 1,728 lines of dedicated calculator CSS across 3 files:

File Lines Purpose
calculatorLarge.css ~800 Full-page loan calculator
calculatorSmall.css ~600 Compact sidebar calculator widget
calculatorIcon.css ~328 Icon-based calculator variant

These files live in the v8 static assets directory (typically psCreditUnion/Progress.Web/office/css/ or psCreditUnion/Progress.Web/css/).

The Risk

If these CSS files were not copied to dbl.Progress/src/www/wwwroot/css/ (or equivalent), the calculator HTML renders but is completely unstyled -- buttons overlap, inputs are misaligned, the slider is invisible.

The Fix

  1. Verify the 3 CSS files exist in v17 wwwroot/css/
  2. If missing, copy from v8 source
  3. Verify <link> tags in master layout or calculator partials reference the correct paths
  4. Confirm the CSS loads by checking browser DevTools Network tab on the calculator page

2d. CalculatorSmall Has No Dictionary Keys (P2)

P2 -- Irish translations impossible for compact calculator

The Finding

CalculatorSmall.cshtml has zero @Umbraco.GetDictionaryValue() calls. All user-facing labels are hardcoded English text:

<!-- All labels are hardcoded English -->
<label>Loan Amount</label>
<label>Loan Term</label>
<button>Calculate</button>
<span>Monthly Repayment</span>

This means Irish (ga-IE) visitors see English-only labels on the small calculator widget, even if the rest of the page is translated.

Action

Document as P2. If multi-language support is required for the small calculator:

  1. Replace hardcoded strings with @Umbraco.GetDictionaryValue("CalculatorSmallRepayment_*") calls
  2. Create corresponding dictionary entries in both en-US and ga-IE

This is not a regression from v8 -- the v8 CalculatorSmall had the same limitation.


3. Script/CSS Loading -- 4 P0 Issues Found

3a. No Cache Busting (P0)

P0 -- Browsers serve stale JS/CSS after every deployment

The Problem

Approximately 100+ <script> and <link> tags across master layouts lack asp-append-version="true".

In v8, the ClientDependency NuGet package handled cache busting automatically by generating composite URLs with version hashes (e.g., /DependencyHandler.axd?s=...&v=123). V17 has no equivalent -- ClientDependency does not exist in .NET Core.

Impact

After every deployment, browsers serve stale cached JavaScript and CSS until users manually clear their browser cache or the browser's cache TTL expires. This causes:

  • Broken layouts (old CSS, new HTML)
  • JavaScript errors (old JS, new DOM structure)
  • Support tickets from users who "can't see the changes"

The Fix

Add asp-append-version="true" to every <script src="..."> and <link href="..."> tag in all master layouts:

<!-- BEFORE (no cache busting) -->
<script src="~/js/site.js"></script>
<link href="~/css/site.css" rel="stylesheet" />

<!-- AFTER (cache-busted) -->
<script src="~/js/site.js" asp-append-version="true"></script>
<link href="~/css/site.css" rel="stylesheet" asp-append-version="true" />

Files to update:

  • master.cshtml
  • MasterLogin.cshtml
  • MasterThirdParty.cshtml
  • masterNoHeaderNof.cshtml
  • masterNoHeaderNoFooter.cshtml

Prerequisite: Ensure @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers is present in _ViewImports.cshtml. Without this directive, asp-append-version is treated as a plain HTML attribute and does nothing.

The tag helper appends ?v=<file-hash> to the URL. When the file changes, the hash changes, forcing browsers to download the new version.


3b. masterLogin Missing RenderScripts/RenderCss (P0)

P0 -- Login page has zero JavaScript and zero CSS from RequiresJs/RequiresCss

The Problem

MasterLogin.cshtml calls Html.RequiresJs() 10+ times to register JavaScript files and Html.RequiresCss() for stylesheets. But the layout never calls Html.RenderScripts() or Html.RenderCss() to output them.

The Html.RequiresJs() / Html.RequiresCss() pattern is a two-step process:

  1. Register -- Html.RequiresJs("~/js/login.js") adds to an internal collection
  2. Render -- Html.RenderScripts() outputs <script> tags for all registered scripts

Without step 2, no JavaScript or CSS is rendered. The login page loads as unstyled HTML with no interactivity.

The Fix

Add before the closing </body> tag in MasterLogin.cshtml:

    @Html.RenderCss()
    @Html.RenderScripts()
</body>

This is a 2-line addition. No other changes needed.


3c. popUpForms.cshtml -- Form Commented Out with TODO (P1)

P1 -- Popup forms render nothing

The Problem

The entire form rendering logic in popUpForms.cshtml is commented out with a TODO comment. The partial renders an empty container -- popup forms display a blank modal.

The Fix

  1. Review the v8 popUpForms.cshtml to understand what the form should render
  2. Determine if it uses Umbraco Forms or custom HTML
  3. Uncomment and adapt the form rendering to v17 patterns
  4. If it uses Umbraco Forms, ensure the Forms package is installed and the form GUID is correct

3d. Menu JS Broken for menu2/3/4 (P1)

P1 -- Menu variants 2, 3, and 4 use wrong JavaScript

The Problem

The ScriptHelper registers identical Slick carousel JavaScript for menu2, menu3, and menu4. These menu variants need different JavaScript:

  • menu1 -- Slick carousel (correct)
  • menu2 -- Mega-menu dropdown JS
  • menu3 -- Sidebar/off-canvas menu JS
  • menu4 -- Sticky header menu JS

This is a copy-paste error from menu1. The result is that menu2/3/4 initialize Slick carousel (which they don't use) instead of their actual menu behavior.

The Fix

  1. Check v8 source for the correct JS initialization for each menu variant
  2. Update ScriptHelper registrations to load the correct JS file for each variant
  3. Verify each menu variant works in browser after the fix

3e. Default async:true Risk (P1)

P1 -- Async script loading causes race conditions

The Problem

ScriptHelper.AddScript() defaults to async: true. When a script is marked async, the browser downloads it in parallel and executes it immediately when downloaded, regardless of DOM order.

This causes race conditions:

<!-- Both load async -- jQuery plugin may execute BEFORE jQuery -->
<script src="~/js/jquery.min.js" async></script>
<script src="~/js/jquery.slick.min.js" async></script>

If slick.min.js downloads faster than jquery.min.js, it executes first and throws $ is not defined.

The Fix

Change the ScriptHelper default from async: true to async: false:

// ScriptHelper.cs
public void AddScript(string path, bool async = false)  // was: async = true

Then explicitly opt-in scripts that can safely load async (scripts with zero dependencies):

ScriptHelper.AddScript("~/js/analytics.js", async: true);  // no dependencies, safe to async

For scripts with dependencies, use defer instead of async. defer maintains execution order while still loading in parallel:

<script src="~/js/jquery.min.js" defer></script>
<script src="~/js/jquery.slick.min.js" defer></script>
<!-- defer guarantees: jquery loads first, then slick -->

3f. Section Name Mismatch (P2)

P2 -- Some scripts silently never render

The Problem

Some partials register scripts with section names that don't match what master layouts render:

// Partial registers scripts to "footer" section
Html.RequiresJs("~/js/map.js", "footer");

// Master layout only renders the default section
@Html.RenderScripts()  // renders unnamed section only
// Missing: @Html.RenderScripts("footer")

Scripts registered to named sections that are never rendered simply disappear -- no error, no warning.

The Fix

  1. Search all .cshtml files for RequiresJs and RequiresCss calls with section name parameters
  2. List all unique section names used
  3. Ensure every master layout calls RenderScripts("sectionName") for each section used by its child views
  4. Remove any section names that are not needed (simplify to default section where possible)

4. Case Sensitivity Issues -- 14 Broken References

Works on Windows dev machines, BREAKS on Linux/Docker deployment

Why This Matters

Windows file systems are case-insensitive: Home/ and home/ resolve to the same folder. Linux file systems are case-sensitive: Home/ and home/ are two different folders.

The v17 project deploys to Docker containers running Linux. Every case mismatch is a guaranteed 500 error or missing partial in production.

4a. Home/ vs home/ (7 references) -- P1

Seven files reference ~/Views/Partials/Home/ (uppercase H) but the actual folder in v17 is ~/Views/Partials/home/ (lowercase h).

The Fix

Normalize all partial path references to match the actual filesystem folder name (lowercase home/):

// BEFORE (broken on Linux)
@Html.Partial("~/Views/Partials/Home/featureCard.cshtml", item)

// AFTER (works everywhere)
@Html.Partial("~/Views/Partials/home/featureCard.cshtml", item)

Find all occurrences with a case-sensitive search for Partials/Home/ and replace with Partials/home/.

4b. GlobalAddress Broken Paths (7 references) -- P1

Seven files reference ~/Views/Partials/GlobalAddress/ or similar paths that do not exist in the v17 file structure.

The Fix

  1. Identify the correct v17 path for GlobalAddress partials
  2. If the partials were renamed or moved during migration, update all 7 references
  3. If the partials were not migrated, copy them from v8 and adapt to v17 patterns
  4. Run a case-sensitive path audit across all .cshtml files to catch any other mismatches

5. GalleryService -- Migrate from V8 (P1)

P1 -- Gallery page exists in v17 but the backend service is missing

The Finding

V8 has a GalleryService.cs that manages gallery content (image collections, albums, sorting). A query against the v8 database confirmed 5 active gallery content items.

The v17 project has:

  • The gallery page template (gallery.cshtml) -- EXISTS
  • The gallery component views -- EXIST
  • GalleryService.cs -- MISSING

Without the service, the gallery page template has no data source and renders an empty page.

The Fix

  1. Copy GalleryService.cs from v8 source
  2. Adapt to .NET Core dependency injection:
    • Register as services.AddScoped<IGalleryService, GalleryService>() in Startup.cs or a composer
    • Replace any ApplicationContext.Current calls with constructor injection
    • Replace UmbracoHelper usage with IPublishedContentQuery or IUmbracoContextAccessor
  3. Inject IGalleryService into the gallery page template or controller
  4. Verify gallery pages render with content

6. UserServiceComposer -- Needs V17 Migration (P1)

P1 -- Admin user group protection is disabled

The Finding

V8 has a UserServiceComposer that protects admin user groups by intercepting save events. It prevents non-admin users from modifying protected groups (e.g., the "Admin" group).

In v8, this uses the Component pattern with UserService.SavingUserGroup event. V17 replaced this with the notification pattern.

The Fix

Create UserSavingNotificationHandler.cs:

using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;

public class UserSavingNotificationHandler : INotificationHandler<UserSavingNotification>
{
    public void Handle(UserSavingNotification notification)
    {
        // Port the protection logic from v8 UserServiceComposer
        // Cancel saves that modify protected user groups
    }
}

Register in a composer:

public class UserProtectionComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.AddNotificationHandler<UserSavingNotification, UserSavingNotificationHandler>();
    }
}

Port the exact protection logic from v8's UserServiceComposer -- do not simplify or change the rules.


7. CommonBondRange -- Database Table Migration (P1)

P1 -- Calculator's postcode lookup will fail without this table

The Finding

V8 has a CommonBondRange custom database table with approximately 2,500 postcode entries. This table is NOT part of Umbraco's schema -- it is a custom application table used by the loan calculator to determine eligibility based on postcode/Eircode.

The calculator's "Am I eligible?" feature queries this table. Without it, the eligibility check returns no results or throws a database error.

The Fix

  1. Check if the table exists in v17:

    SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
    WHERE TABLE_NAME = 'CommonBondRange';
    
  2. If missing, create it with the same schema:

    -- Get schema from v8
    SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_NAME = 'CommonBondRange'
    ORDER BY ORDINAL_POSITION;
    
  3. Copy data from v8:

    Use SQL Server's linked server feature, BCP export/import, or a simple INSERT INTO ... SELECT FROM if both databases are on the same server (they are -- both on cb-sql.double.pt,7071).

  4. Verify the repository/service:

    Find the C# code that queries CommonBondRange (likely a repository class) and ensure it works with v17's database context. If it uses NPoco/PetaPoco (v8 pattern), it needs to be updated to use Umbraco v17's IScopeProvider and IUmbracoDatabase.


8. Email Templates -- Client-Specific Manual Setup (P2)

P2 -- Not automated, requires manual per-client setup

The Finding

V8 has 25 Razor email templates in Views/EmailTemplates/. These are rendered by custom C# code -- they are NOT Umbraco Forms email workflows. They contain:

  • Client-specific branding (logos, colors, footer text)
  • Hardcoded URLs (credit union website, app store links)
  • Irish language variants
  • Specific content (loan confirmation text, membership welcome text)

Why This Cannot Be Automated

Email templates are inherently client-specific. The migration tool handles Umbraco content and configuration -- email templates are custom application code with embedded business content. Each credit union has different branding, different URLs, different legal text.

Action Per Client

Each client deployment must:

  1. Copy email templates from v8 Views/EmailTemplates/ to v17 Views/EmailTemplates/
  2. Update Razor syntax:
    • Replace @Html.Raw(...) with @Html.Raw(...) (same in v17, but verify helpers)
    • Replace any UmbracoHelper calls with v17 equivalents
    • Replace @Umbraco.TypedContent() with @Umbraco.Content()
  3. Update the email rendering service:
    • Ensure IEmailService or equivalent is registered in DI
    • Replace MailMessage with MimeMessage if using MailKit (v17 default)
  4. Test each template by triggering the workflow that sends it (registration, loan application, contact form, etc.)

Document this as a client onboarding checklist item.


9. ListViews -- V17 Native Support (P3)

P3 -- Low priority, v17 handles this natively

The Finding

V8 had custom backoffice list views:

  • ListViewNews -- custom backoffice view for managing news articles as a sortable list
  • ListViewTestimonals -- custom backoffice view for managing testimonials as a sortable list

These were custom Angular views registered in the v8 backoffice to give editors a list-based content management experience instead of the default tree view.

V17 Native Solution

V17 has built-in collection/list view support through document type configuration. No custom backoffice views are needed:

  1. On the parent document type (e.g., "News List"), enable "List View" in the Structure tab
  2. Configure columns, sort order, and page size
  3. V17 automatically renders a sortable, filterable list in the backoffice

Action

  1. Verify that the News and Testimonials parent document types have "List View" enabled
  2. Configure the list view columns to match what editors had in v8 (title, date, status)
  3. Remove any references to the custom ListViewNews / ListViewTestimonals Angular views from the v17 codebase -- they are v8-only and will not work in v17's Lit-based backoffice
  4. Show editors the new list view and confirm it meets their needs

Appendix: Investigation Methodology

All findings in this report were verified through one or more of:

  • Direct SQL queries against the v8 database (progresscu.ie on cb-sql.double.pt,7071)
  • File system inspection of both v8 (psCreditUnion/) and v17 (dbl.Progress/) codebases
  • Umbraco v17 source code analysis for expected formats and behaviors
  • Cross-reference with existing reports (30, 31) and the migration tool source code

No finding is based on assumption. Where a finding requires verification (e.g., "check if CSS files exist"), the verification step is explicitly listed as an action item.

Migration documentation by Double for Progress Credit Union