Status: Phases 0–6 complete;
PR #2640
merged into imp/blazor-hybrid 2026-09-29. Phase 7 withdrawn (2026-09-24) — handed to
Jose Castro's overlapping AB#2877/2878/2879 work. Phase 8 not started and likely superseded by the
same work — check with Jose before starting it. Phase 9 (obsolete-code/doc cleanup) in progress,
worktree AB2855-DynFeedCleanup01 / branch chore/AB2855-dyn-feed-cleanup-01. Phase 10 not started.
Date: 2026-09-15
Scope: Dynamic feed loading, page hosting, template/content adapters, RCL/component packaging, CMS authoring, search rendering, and favorites rendering
Related: Dynamic Content Admin and Mobile Architecture expands section 4.6–4.10 into the full CMS/mobile boundary.
Update (2026-09-24) — pivot to a single consolidated integration branch: the original §7.1 plan (one PR per phase, stacked against
imp/blazor-hybrid) was superseded before any of those PRs beyond baseline landed. Instead,imp/blazor-hybridpicked up an unrelated large change first — PR #2543, the Service Layer Decoupling integration, which obsoleted the monolithicIAuthenticationinterface in favor ofISessionState/IAuthTokenProvider/ICredentialStore/IAuthSession— so every already-built phase branch (all of which predate that PR) needed reconciling against currentimp/blazor-hybridbefore any of them could land cleanly. Rather than doing that reconciliation once per phase PR, a new worktree/branchAB2845-DynFeedIntegration01/chore/AB2845-dyn-feed-integration-01was created from latestimp/blazor-hybrid, and every phase branch (0 through 6) was merged into it in sequence, resolving conflicts once each — see §7.1 for the merge log and conflict details. That integration branch is the current source of truth; it will go toimp/blazor-hybridas one PR rather than nine. Phase 7 (search rendering) was originally merged in too, but was withdrawn and dropped from the integration branch the same day — see Phase 7's own section for why. Phases 8–10 are otherwise unaffected by this pivot and remain to be built the normal way once the consolidated PR lands.Update (2026-09-21): Progressive per-container loading landed on
feat/ProgressiveContainerLoading(merged here) ahead of this plan's phases — see the new §2.1 and the callouts in §3.1, §4.1, Phase 1, and Phase 2 below.IItemsQueries/DynamicListFactoryregistration is still blocked as originally documented (§8 risk row unchanged). Phase 5'sContentResolutionContext.Usershould sequence after the auth-port split described in Service Layer Decoupling Plan §9 — that document currently references aDynamicContentHost.razorcomponent that does not exist at this branch's tip; treat that specific claim as stale until re-verified, but the sequencing recommendation (resolvers should adoptISessionStateonce that plan introduces it, rather than the plan's currentUserInfo?) still applies. Resolved 2026-09-24 — PR #2543 merged intoimp/blazor-hybrid, landing theISessionState/IAuthTokenProvidersplit; the integration branch above builds on it directly.
Restructure dynamic content consumption so that JSON, search results, and favorites can use the same presentation pipeline and reusable Razor component library (RCL) components.
The refactor has two related goals:
The intended result is one shared dynamic page host and one shared item-to-RCL pipeline, with localization and feature-specific behavior expressed as optional policies.
The refactor must also establish a package contract for the web-based CMS. The CMS should author against versioned component capabilities and payload schemas, while the mobile app remains the runtime that executes adapters, resolves data, and renders RCL components.
The current dynamic feed path is:
PageDto
-> DynamicPageOrchestrator
-> PageFactory / ContainerFactory
-> ItemComponentFactory
-> ComponentView
-> RCL component
Template adapters are dispatched through IFeedTemplateContentFactory. They return ItemDto objects, but many adapters also own feature-specific behavior such as:
The current page layer duplicates the dynamic loading shell across components such as:
DynamicFeedOrchestratorPage;MecHomePage;MecCommitteesPage;LocalizedDynamicPageLoader already centralizes page selection and language behavior, but it also performs the common page loading and orchestration work that should be reusable by non-localized pages.
Search and favorites have partial reuse of RCL components, but their data paths still contain source-specific hydration and rendering decisions. A logical item can therefore take different paths depending on whether it came from a feed, search, or favorites.
The current payload documentation already identifies Admin-only fields that are not represented consistently in the renderer, including token overrides, roster tokens, view-all tokens, and icon styling tokens. These fields need to become part of an explicit authoring contract rather than remaining undocumented additions to the JSON bag.
Branch note (2026-09-22): this machinery lives on
feat/ProgressiveContainerLoading, which is not yet merged intoimp/blazor-hybrid— confirmed viagit merge-base --is-ancestor origin/feat/ProgressiveContainerLoading origin/imp/blazor-hybrid. Any phase whose code referencesContainerResolution,ResolveProgressivelyAsync, orDynamicFeedRenderHelpersmust branch fromfeat/ProgressiveContainerLoading(or a descendant), not fromimp/blazor-hybriddirectly, until that branch merges — see the Phase 1 PR (§7.1) for the concrete example of this tripping up a branch-from-imp/blazor-hybridworktree.
Before Phase 1 began, LocalizedDynamicPageLoader and DynamicPageOrchestrator gained a second,
progressive loading path alongside the original all-at-once LoadAsync/ResolveAsync:
ILocalizedDynamicPageLoader.LoadProgressivelyAsync returns a ProgressivePageLoadResult
(ShellComponents + IReadOnlyList<Task<ContainerResolution>> ContainerTasks) so a page can paint
immediately with per-container loading skeletons instead of waiting for every container.DynamicFeedRenderHelpers.ApplyProgressivelyAsync consumes those tasks as they complete (in
completion order, matched by ComponentViewModel.Id, not list position) and swaps each skeleton
for its resolved content, with CancellationToken support so a superseded load (pull-to-refresh,
a background cache refresh) stops mutating an orphaned collection.WatchForStuckContainers gives up on a container still in a Retrying state after
RetryGiveUpDelay (15s) and shows its empty state, independent of any longer-running background
retry.StackContainerViewModel.SourceUnavailable distinguishes "the source failed to load" from
"the source loaded and is genuinely empty" so the UI can render different empty states.This is currently wired into DynamicFeedOrchestratorPage, AlpaIHomePage, MecHomePage,
MecCommitteesPage, MecCommitteeDetailPage, and LecPage directly against
LocalizedDynamicPageLoader/DynamicPageOrchestrator. It predates and is not yet expressed by the
non-localized IDynamicPageLoader this plan proposes (§4.1) or the shared page host (Phase 2) — both
need to carry a progressive variant, not just the plan's original synchronous
DynamicPageLoadResult, or this behavior regresses when those pages migrate.
The default path is DynamicPageLoader. LocalizedDynamicPageLoader is an optional page-selection
policy that selects the appropriate page before delegating to the common loader. Both paths converge
on the same orchestrator, resolver pipeline, ViewModel mapping, and RCL components.
Route or caller
-> DynamicPageRequest
-> DynamicPageLoader
^
|
LocalizedDynamicPageLoader
(optional page-selection policy)
-> DynamicPageOrchestrator
-> ItemResolutionPipeline
-> ItemComponentFactory
-> ComponentView
-> reusable RCL component
DynamicPageLoader owns the common workflow:
PageDto by page key;DynamicPageOrchestrator;LocalizedDynamicPageLoader owns only localization policy:
Localization must not be required by ordinary dynamic pages.
A shared dynamic page host owns:
ComponentView.Feature pages should provide route and policy inputs only. They should not duplicate the page rendering shell.
Feature content resolvers own:
They must not own Razor markup, page shells, or decisions that belong to ComponentView.
The shared item pipeline owns:
Introduce a neutral result type:
public sealed record DynamicPageLoadResult(
bool PageFound,
string RequestedPageKey,
string SelectedPageId,
IReadOnlyList<ComponentViewModel> Components,
string? Title,
string? TitleToken);
Add a common loader contract:
public interface IDynamicPageLoader
{
Task<DynamicPageLoadResult> LoadAsync(
string pageKey,
bool forceRefresh = false,
CancellationToken cancellationToken = default);
}
DynamicPageLoader should be the default path. LocalizedDynamicPageLoader should use the same result type and delegate final page loading to the common implementation.
IDynamicPageLoader must also expose a progressive counterpart mirroring
ILocalizedDynamicPageLoader.LoadProgressivelyAsync (§2.1) — e.g. a LoadProgressivelyAsync
returning a DynamicPageLoadResult-shaped ShellComponents/ContainerTasks pair — since that path
already ships on several pages this plan intends to migrate onto the common loader. Treating it as a
later add-on risks a regression where migrated pages lose per-container skeletons and
SourceUnavailable handling that non-migrated pages keep.
Keep stable routes such as /mec/committees, but resolve them to dynamic page keys through a route policy rather than a dedicated page component.
Example requests:
/mec
-> {mec}-home
-> non-localized unless the route requires otherwise
/mec/committees
-> {mec}-committees
-> localized
/dynamic-feed-orchestrator?page={ual-media}
-> ual-media
-> non-localized
Introduce a request model or resolver capable of carrying page-level policy such as:
ItemDto initially as the intermediate contractDo not replace ItemDto with a new presentation model in the first migration. The existing ItemComponentFactory and ComponentView pipeline already provides the desired RCL boundary.
First make all sources produce consistent ItemDto values. Introduce a separate ResolvedContentItem only if ItemDto cannot represent the common contract without feature leakage.
The current factory contract receives only TemplateDto. This causes the orchestrator to copy outer-item metadata back onto factory-created items.
Evolve the contract toward a context object:
public sealed record ContentResolutionContext(
ItemDto SourceItem,
TemplateDto Template,
UserInfo? User,
string? PageId,
string? ContainerType);
Use the existing IFeedTemplateContentFactory name initially unless the broader contract makes IContentResolver clearer during implementation.
Use inheritance only for page-level policies. Use composition for template resolution, mapping, and shared stamping.
The preferred dependency direction is:
LocalizedDynamicPageLoader
-> DynamicPageLoader
-> DynamicPageOrchestrator
-> content resolvers
Avoid separate orchestrator subclasses that duplicate visibility, token, favorite, or component dispatch behavior.
The CMS needs a versioned component catalog package, not a copy of the mobile application's adapters or compiled RCL assemblies.
The package should contain:
itemType, containerType, and contentType;The package must not contain mobile runtime implementation code such as:
The boundary is:
CMS component package
-> validates and authors JSON
-> publishes versioned PageDto content
Mobile runtime
-> loads PageDto content
-> runs adapters/resolvers
-> maps to ViewModels
-> renders RCL components
RCL components, ViewModels, adapters, and styles remain part of the mobile runtime package. The CMS package describes the public contract those runtime assets implement.
The admin tool does need its own authoring-time data adapter layer. This is separate from the mobile runtime adapters. It lets the CMS preview and configure dynamic content such as documents, categories, or other non-member-specific records by calling approved external APIs.
An authoring-time adapter should provide:
documents;The admin adapter must run through the admin backend or a controlled gateway. Browser code should not receive mobile service credentials or call arbitrary URLs supplied by an administrator.
The first authoring-time adapters should target non-member-specific sources, for example:
The admin tool must not attempt to resolve member-context data such as:
Those remain mobile runtime capabilities. The CMS can configure their resolver definitions and preview a neutral placeholder or explicitly supplied test context, but it must never require a real member account to author or preview a page.
The two adapter layers should share schemas and resolver keys where possible, while keeping implementations separate:
CMS authoring adapter
-> approved admin API/gateway
-> normalized preview data
-> CMS component canvas
Mobile runtime resolver
-> mobile/application port
-> member-aware data and visibility policy
-> ItemDto / ViewModel
-> RCL component
The component catalog should mark each resolver capability as one of:
Public: no member context required;AdminAuthorized: requires admin service permissions;MemberContext: resolved only by the mobile runtime;Hybrid: authoring can use neutral/sample data, but mobile runtime applies member-specific filtering.MemberContext capabilities must not be exposed as executable authoring adapters in the CMS.
Create a registry as the single source of truth for the supported authoring surface. Each entry should identify:
ComponentDefinition
key
packageVersion
wireType
renderType
category
payloadSchema
editableProperties
allowedChildren
requiredCapabilities
dataCapabilities
themeTokens
assets
favoriteSupport
deprecation
The registry should be consumable in two forms:
The registry must not be inferred by scanning Razor filenames. Razor tags can fail silently, and file names do not describe JSON compatibility, required services, or authoring constraints.
The catalog should distinguish between:
CardHero, CardSmall, ContactCard, or List;CMS, mobile, or both);Stack, Grid, or Carousel.For example, Committee should not be presented to the CMS as if it were a visual component. The
CMS should author a generic list or contact/card composition with a resolver configuration that
the mobile runtime knows how to execute. This keeps committee-specific data policy out of the
RCL contract and allows the same visual composition to be used by search and featured lists.
Styles should be packaged in layers:
The CMS should select token names and approved values rather than author arbitrary CSS. This keeps the app's layout and interaction behavior stable while still allowing site customization.
Every token used by a payload should be resolvable by the runtime. Unknown tokens should produce a diagnostic and a documented fallback, not a broken page. Token catalogs need their own version and compatibility checks.
Use independent versions for:
Published content should record the catalog version it was authored against. The mobile runtime should advertise the catalog versions and capabilities it supports. Publishing should be blocked when content requires an unsupported component, property, token, resolver, or interaction.
Compatibility rules should define:
DocumentHero to HeroCard;Full inventory: Phase 0 Baseline.
Exit criteria: The current behavior and migration targets are documented, and baseline tests pass. Met.
Tests (UnitTest/DynamicPageLoaderTests.cs):
Exit criteria: Existing generic and localized routes behave as before, with one shared result type and no duplicated orchestration logic. Met — dotnet build ALPAMobile.sln clean, dotnet test 2175/2175 passed (2167 baseline + 8 new), 2026-09-22.
Exit criteria: At least one existing dynamic page uses the shared host without visual or navigation regression. Met — MecHomePage migrated onto DynamicContentHost; dotnet build clean, dotnet test 2175/2175 passed, 2026-09-22. Not yet validated live on-device (see repo testing policy — UI validation is still owed before this is fully trusted for a visible route).
Scope note: DynamicFeedOrchestratorPage's FixedContent slot (hotel-directory filter bar) isn't supported by the host yet — that page stays on its own markup until a later phase widens the host or the hotel-directory case gets its own solution. Only MecHomePage was migrated in this phase.
MecCommitteesPageExit criteria: /mec/committees is served by the generic host and no longer requires a feature-specific Razor page. Code-complete — dotnet build clean, dotnet test 2175/2175 passed, 2026-09-22. Not yet device-validated (narrow/wide layout comparison, platform UI suites).
New reusable piece: LocalizedDynamicRoutePage (Pages/DynamicFeed/) — generalizes the MecHomePage/MecCommitteesPage load-orchestration shape (localized progressive load, pull-to-refresh, hydration-language reactivity, document-catalog-ready reconciliation) behind a PageKeySuffix parameter, so any future stable {mec}-{suffix} route (Phase 4 candidates) can become a one-line shim instead of duplicating this logic again.
Migrate one page at a time:
DynamicContentHost has no slot for page-level overlay content above the component loop.global-home-loggedout against the global site, not alpa-home) with its own background reconcile, diagnostic-mode simulated-latency testing path, and CSS modifier (.alpa-home--loggedout — real, load-bearing styling: background art, full-width LOGIN button, navy icon-mask tint, confirmed in alpa-components.css).DynamicPageOrchestrator.ResolveAsync directly (not the progressive/shell path every host-migrated page uses), with its own WatchForStuckContainers call after every reassignment — a different resolution shape than DynamicContentHost/LocalizedDynamicRoutePage assume.homepreview.feed1 element id is asserted on by name in UITests.Shared/BlazorWalkthroughTests.cs (4 call sites) — changing the host's generic markup would break the live UI test suite, which this environment has no way to run and re-verify.MecHomePage/LecPage: real per-page business logic and test-asserted DOM, not page-selection policy. Forcing a migration here to close out a checklist item would trade a verified-working page for an unverifiable one — no simulator/UI-test access in this environment to catch a regression.Retain dedicated pages only when they contain behavior that cannot be expressed by page metadata, route policy, or reusable RCL components, such as maps, document viewers, complex forms, or interactive search state.
Exit criteria: CMS-driven pages use one host; dedicated pages have an explicit reason to remain. Met — every candidate page from Phase 0 was assessed. MecHomePage and LecPage use DynamicContentHost; AlpaIHomePage and LecPage stay dedicated pages with concrete, source-confirmed reasons (not just "too hard to check"); LecRosterPage was never in scope. dotnet build clean, dotnet test 2175/2175 passed for the LecPage migration, 2026-09-22. Not device/UI-suite validated (see repo testing policy) — this is a stated limitation of this environment, not a skipped step.
Sequencing: the Service Layer Decoupling Plan is introducing narrower auth ports (
ISessionState,IAuthTokenProvider,ICredentialStore,IAuthSession) in its own Phase 5 and recommends this plan's resolvers consumeISessionStateonce that port exists, rather than theUserInfo?field below. Resolved (2026-09-23): that plan's own risk table explicitly anticipates this plan buildingContentResolutionContext.UseronIAuthenticationbefore its Phase 5 lands ("Dynamic-feed plan buildsContentResolutionContext.UseronIAuthenticationbefore Phase 5" — mitigation: "Sequence rule in §9"). That is the expected order, not a race condition to avoid — build onIAuthentication/UserInfo?now; theISessionStateswap is a planned fast-follow once PR #2543 (the integration PR carrying that port intoimp/blazor-hybrid) merges — it was still Active, blocked on an unrelated logout content-paint defect, as of 2026-09-23.
Exit criteria: Factories receive the information they need directly and no longer depend on implicit orchestrator post-processing. Mostly met — the context now exists and reaches every factory; the "no longer depend on implicit post-processing" half is intentionally deferred (see the unchecked item above) since forcing it now would remove working behavior for no benefit. dotnet build clean, dotnet test 2178/2178 passed (2175 baseline + 3 new), 2026-09-23. Not device-validated.
Candidate resolvers include:
Exit criteria: Feature resolvers fetch and map content, while the shared pipeline controls presentation metadata and component dispatch. Partially met — the two behaviors that were actually duplicated (render-type stamping, favorite-flag default) are now shared; action/route resolution and loading/empty-result behavior were already centralized elsewhere and needed no further extraction. dotnet build clean, dotnet test 2178/2178 passed (identical count to Phase 5 — a pure, behavior-preserving refactor, confirmed by zero test changes needed), 2026-09-23. Not device-validated.
Withdrawn — handed to Jose Castro's AB#2877/2878/2879 stack. While this phase's documents/links work was in flight, Jose independently started the same alignment goal — search results (and, per AB#2879, favorites) rendering with the feed's own look — via a different mechanism:
DynamicPageOrchestrator.ResolveTemplateFieldsInPlace(item, template, userInfo)applied atCoveoSearchResultFactory.BuildFeedComponentVm, shared at the orchestrator level, plus a reworkedItemComponentFactoryimage/glyph-policy pipeline (WithDefaultImagePolicy/WithDefaultGlyphPolicy) and a sharedFeedItemContent.ShownTexthelper. His stack (PRs 2557→2558→2559→2560→2565, all Active againstimp/blazor-hybridas of 2026-09-24) touches the exact same files this phase's documents/links work touched (CoveoSearchResultFactory.cs,ItemComponentFactory.cs,DynamicPageOrchestrator.cs). Rather than land two competing implementations of the same goal, this phase is withdrawn: PR #2550 (documents pilot) was abandoned, the links sub-migration (never PR'd) is left uncommitted in its own worktree, and thedocuments/linksmerge commits were dropped from the consolidated integration branch (chore/AB2845-dyn-feed-integration-01), which now ends at Phase 6. The findings below (design notes, theDocumentItem.ParsedPublishDatebug, per-content-type investigation) are kept for reference — some of it may still be useful if Jose's approach doesn't end up covering documents/links, but it is not active work.Original Phase 7 content (historical — withdrawn, see above)
Scoping note (2026-09-23): this phase is architecturally larger than Phases 1–6 —
ALPAMobile.Infrastructure(where every search extractor lives) is not allowed to referenceALPAMobile.Presentation(whereDocumentHeroFactory/ItemComponentFactorylive), a rule the codebase already documents and works around per content type (seeDocumentSearchExtractor.OpenRoute's route-literal duplication, andMenuItemSearchExtractor's tap-timeDeepLinkResolverresolution indocs/detail/SEARCH-CONTENT-ONBOARDING.md). Piloted one result type (documents) first rather than migrating all four at once, given no simulator/live-search access in this environment to catch a subtle rendering mismatch.
- [~] Migrate contacts, documents, committees, and links incrementally. (documents and links done — PRs pending; contacts and committees investigated and deliberately deferred — see below)
Per-type findings (2026-09-23): investigated all four before writing code for any of them. Only documents and links turned out to be safely migratable without a larger design change:
- Documents — done. See design note below.
- Links (
ResourceLinkSearchExtractor) — done.CoveoSearchResultFactorybuilds aCardSmallViewModelfromTitle+NotificationContentRouter.NormalizeHttpUrl(ClickUri), the exact same two-field mappingMemberResourcesPage.razor.csalready builds inline for this identical rawAppLinkdata — so a resource link's search result renders as the same card shape as its Member Resources row. Deliberately does not reuseLinkCardFactory: that factory resolves hrefs throughDeepLinkResolver/FavoriteKey.NormalizeLinkRawId, built for internal ALPA route/favorite conventions, whereas a Resource Link'sAppLink.URLis a plain author-entered external URL — confirmed by the extractor's own remarks and bySearchPage.OpenResultAsyncalready normalizing this exact data withNormalizeHttpUrl, notDeepLinkResolver. No new raw-JSON field needed:Title/Routewere already carried bySearchIndexEntry, gated on the existingContentTypeName/Sourcediscriminator ("Resource Link") rather than adding a new field for what's already a stable, tested string.- Committees (
CommitteeSearchExtractor) and contacts (Representative/Company Contact extractors) — deferred, not migrated. Both don't render throughItemComponentFactory/ComponentViewat all today; they deliberately open a search-only "contact card modal" (SearchIndexEntry.OpensContactModal) because their real MEC destination page was retired under AB#2500. The extractor's own comment says this is interim, pending "the MEC surface coming back as dynamic-feed-hydrated RCL components" — separate, larger work outside this refactor's scope. Migrating either now would mean either building a second, throwaway RCL-based rendering path ahead of that larger effort, or reverse-engineering interim UI parity with the modal — neither is a safe, proportionate change here.Design note — why documents don't use
FeedItemJson: the existingFeedItemJson/BuildFeedComponentVmmechanism (§2.1-adjacent, AB#2561) only works for content that's alreadyItemDto-shaped in the dynamic-feed cache. Documents aren't dynamic-feed content —DocumentSearchExtractorreads aDocumentItemfrom the documents API cache, not aSiteDto. Building anItemDtofor it inDocumentSearchExtractor(Infrastructure) would mean duplicatingDocumentHeroFactory's (Presentation) mapping logic in a second location with no compiler-enforced way to keep them in sync — a materially larger duplication than the one-lineOpenRouteliteral the codebase already tolerates. Instead:DocumentSearchExtractorserializes the rawDocumentIteminto a newSearchIndexEntry.DocumentJsonfield (threaded throughRawCoveoSearchResultItem.DocumentJson), andCoveoSearchResultFactory(Presentation) deserializes it and callsDocumentHeroFactory.Create(DocumentItem)directly — the same pure, already-shared mapper Favorites uses. Same shape as theMenuItemSearchExtractor/DeepLinkResolverprecedent already in the codebase (defer the Presentation-dependent step to Presentation, carry only raw data through Infrastructure), extended from a route string to a full card.Bug found and fixed during the documents work:
DocumentItem.ParsedPublishDateis a computed property that throwsFormatExceptionon the default emptyPublishDatestring.JsonSerializer.Serializeevaluates every public getter, so serializing aDocumentItemwith no publish date (a real, confirmed-live condition) crashed the whole document's indexing. Fixed with a fail-softTrySerializeDocumentwrapper inDocumentSearchExtractor— a row that can't serialize keeps its existing Title/Metadata/Route fields (search still works) and just skips the reconstructed-card enhancement, same "skip the enhancement, never the row" rule used everywhere else in this pipeline. This is a pre-existing latent bug inDocumentItemthat this phase's new serialization path exposed, not a bug in the new code itself — caught by the existingDocumentSearchExtractortest suite failing after this phase's change, not by manual review. This bug fix is worth porting to Jose's approach independently of the rest of this phase, since it's a real latent crash unrelated to which rendering approach wins.Exit criteria: Search results reuse the same RCL component and visual contract as feed results wherever the content type is shared. Partially met — documents and links now render through real, already-shared mappers instead of bespoke
SearchResultRowViewModelfields; committees and contacts remain on the interimOpensContactModalshape, deliberately not touched, pending the separate MEC-surface-retirement work.dotnet buildclean,dotnet test2187/2187 passed (2183 baseline + 4 new for links), 2026-09-23. Device-validated for both: deployed to a running Android emulator for documents and again for links (adb-confirmed process running each time, screenshots captured — including theHomeSkeletonloading state rendering correctly for the links build). Neither specific search-rendering path was exercised signed-in with a live query — that requires real ALPA credentials not available in this environment; clearing app data to force a fresh search index lost an already-authenticated test session that couldn't be reproduced.Phase 8: Unify favorites rendering — likely superseded, needs a check-in before starting
Not started, and probably shouldn't be started as written. Jose's AB#2879 ("Favorites: open offline and keep the look of the card that was hearted," PR #2560, Active) already does a large part of what this phase describes — new
IFavoriteLookStore/FavoriteLookStore, and rework ofFavoriteToggleContext/FavoriteLooksso a favorited card keeps the same look it had when hearted. Talk to Jose before picking this phase up: either his work already covers it and this phase should be struck, or there's a narrower remaining slice worth defining once his stack lands.Phase 8: Unify favorites rendering
Exit criteria: Favorites reuse the same ViewModels and RCL components as other content sources without losing favorite identity or navigation behavior.
Phase 9: Remove obsolete code and document the architecture
Exit criteria: No feature-specific page or adapter remains without a documented responsibility. Met — obsolete-code audit, doc updates, and the
ComponentViewdispatch-test gap are all closed.Phase 10: Package the CMS authoring contract
The first package should cover the existing dynamic-feed surface, including:
PageDto,ContainerDto, andItemDtopayload schemas;Grid,Stack,Carousel, andButtonGroupcontainers;- card, button, list, contact, document, and empty-state visual components;
- supported template/content resolver definitions;
- typography, surface, icon, image, and layout token references;
- favorite capability metadata;
- legacy discriminator aliases currently accepted by
ItemComponentFactory.Exit criteria: The admin tool can validate and preview a page using the published package, and the mobile runtime can reject or safely fall back from content that requires unsupported package capabilities.
6. Testing Strategy
Unit tests
Add or update tests for:
DynamicPageLoadersuccess, not-found, error, refresh, and cancellation behavior;- localized loader delegation and language fallback;
- route-to-page-key resolution;
- generic page host result handling;
- component catalog schema validity and unique wire discriminators;
- catalog coverage for every
ItemComponentFactoryandContainerFactorydiscriminator;- catalog/runtime compatibility and unsupported-capability rejection;
- token catalog coverage and unknown-token fallback behavior;
- preview fixture validity against the published payload schemas;
- authoring adapter parameter and normalized-response validation;
- admin adapter authorization, timeout, caching, and failure behavior;
- enforcement that CMS preview never requests real member profile, MEC, LEC, or favorite data;
- resolver context propagation;
- unknown content types and missing factories;
- empty results and visibility filtering;
- favorite identity propagation;
- identical source content mapping to identical ViewModels.
Relevant existing tests include:
DynamicPageOrchestratorTests;DynamicListFactoryTests;PilotCardFactoryTests;CommitteeListFactoryTests;CommitteeMembersFactoryTests;FavoritesViewModelTests;ItemComponentFactoryTests;RazorComponentResolutionTests.UI validation
Run UI tests after:
- changing route ownership;
- changing
AutomationIdvalues;- migrating
MecCommitteesPageor other visible pages;- changing favorite interactions;
- changing search result rendering.
Validate both supported layout widths for structural changes. Do not claim device validation unless the app was deployed and the rendered behavior was inspected on the device.
7. Validation Gates
After each phase:
dotnet build ALPAMobile.sln dotnet test UnitTest/UnitTest.csproj --logger "trx;LogFileName=test.trx"For route or UI migrations, also run the applicable platform UI suite:
./run-ui-tests.sh ios ./run-ui-tests.sh androidRead the generated test result before re-running a failed suite so the failing test identity is preserved.
7.1 PR sequencing — superseded 2026-09-24, see the status banner above
The plan below (one PR per phase, each stacked on the previous phase's branch) was the original intent and is preserved here for the historical record of how each phase was actually built and individually gated. It stopped being how the work reaches
imp/blazor-hybridonce PR #2543 (Service Layer Decoupling) landed there first: reconciling nine already-built, pre-#2543 phase branches one PR at a time would have meant resolving the sameIAuthenticationfallout repeatedly. Instead every phase branch below was merged, once each, into a new consolidated worktree/branchAB2845-DynFeedIntegration01/chore/AB2845-dyn-feed-integration-01, created from latestimp/blazor-hybrid(already carrying #2543). That branch is the current source of truth and will go toimp/blazor-hybridas one PR.Merge log (
chore/AB2845-dyn-feed-integration-01, 2026-09-24, in order — each step built, tested full-suite, and committed before the next):
# Branch merged Conflict? Resolution 1 docs/AB2846-dyn-feed-baseline-01(Phase 0)Clean — 2 feat/AB2847-dyn-feed-loader-01(Phase 1)Real DynamicFeedOrchestratorPagehad diverged two ways since this branch was cut:imp/blazor-hybridgrew a load-coalescing gate and theIAuthentication→ISessionState/IAuthTokenProvidersplit, while this branch introducedIDynamicPageLoader. Kept the loader abstraction (this phase's actual goal) and preserved both newer behaviors — git's 3-way merge resolved the.csbody correctly on its own; only the.razor@injectlines needed a manual union.3 feat/AB2848-dyn-feed-host-01(Phase 2)Doc only Plan-doc add/add conflict — took the branch's completed Phase 2 section over HEAD's stale unchecked one. 4 feat/AB2849-dyn-feed-mec-committees-01(Phase 3)Real LocalizedDynamicRoutePageneeded both the coalescing-gate fields (still used by the body) and the branch's new generalization parameters; dropped aFavoriteToggleContextinjection made redundant byDynamicContentHostnow owning it. Also caught a staleIAuthenticationreference that had slipped through with zero conflict markers in a cleanly-added new file (MecCommitteesPage.razor.cshad been renamed away;MecCommitteesPage.razorresolved by taking the branch's thin-shim version entirely).5 feat/AB2850-dyn-feed-lec-01(Phase 4)Real LecPage.razor— sameIAuthentication→ISessionState/IAuthTokenProvidersplit.6 feat/AB2851-dyn-feed-resolver-context-01(Phase 5)Clean — 7 feat/AB2852-dyn-feed-resolver-shared-01(Phase 6)Real CommitteeMembersFactory.cs— combined this phase's own shared render-type-resolution refactor (its actual purpose) with a newerDisplayNamefieldimp/blazor-hybridhad separately picked up from an unrelated contact-name-formatting fix.The recurring pattern: PR #2543 obsoleted the monolithic
IAuthenticationinterface mid-refactor, splitting it intoISessionState/IAuthTokenProvider/ICredentialStore/IAuthSession. Every phase branch predating that PR still targeted the old API in any file it touched — surfacing either as a direct conflict (rows 2, 4, 5 above) or, more subtly, as silent fallout in a whole new file that auto-merged cleanly with nothing to flag it (row 4'sMecCommitteesPage.razor.cs/LocalizedDynamicRoutePage.razorcase). Every merge step above was followed bygrep IAuthenticationacross its changed files regardless of whether git reported a conflict, specifically because of that second failure mode.Test results (through Phase 6):
dotnet build ALPAMobile.slnclean (0 errors) after every step. Full suite at the Phase 6 checkpoint: 3116/3116 passing, up from 3113 at the Phase 0 baseline. Two timing-sensitive tests —WatchForStuckContainers_GivesUpAStillLoadingContainerandSubscribe_UsesCallerProvidedNowOnEachTick— each flaked once under the full suite's parallel execution load but passed reliably in isolation; confirmed non-regressions, not real bugs (and per Phase 7's withdrawal note, Jose has since opened PR #2880 fixing the give-up-timer test's flakiness directly).Phase 7 withdrawn, 2026-09-24: rows 8–9 originally merged
feat/AB2853-dyn-feed-search-documents-01andfeat/AB2853-dyn-feed-search-links-01(both realIAuthentication-split conflicts, same pattern as above) on top of row 7, taking the full suite to 3125/3125. Both merge commits were then dropped —git reset --hardback to row 7's commit, since the branch had never been pushed — once it became clear Jose Castro's AB#2877/2878/2879 stack (PRs 2557–2565, Active againstimp/blazor-hybrid) covers the same search/favorites-alignment goal via a different, incompatible mechanism. PR #2550 (the documents pilot's own PR) was abandoned for the same reason. See Phase 7's section above for the full explanation and what's still salvageable (theDocumentItem.ParsedPublishDatebug fix).Staying current with
imp/blazor-hybrid, 2026-09-24–25: with Phase 7 dropped, the integration branch is kept up to date by periodically mergingorigin/imp/blazor-hybridback in — the same "pull latest, resolve, build, test" step, just in the opposite direction from the phase merges above. Three such merges so far, all clean (no conflicts):
- 2026-09-24, 27 commits, including PR #2530 (this plan's own Phase 0 baseline, merged directly into
imp/blazor-hybridon a separate track — see the historical per-phase table below) and PR #2566 (Jose's fix forWatchForStuckContainers_GivesUpAStillLoadingContainer's flakiness). Result: 3131/3131.- 2026-09-25, 33 commits, including PR #2580 — Jose's own consolidated squash of his whole AB#2682 stack (mirroring this plan's own worktree-consolidation move) — plus follow-ups #2582 (Search UI-thread freeze fix), #2585 (unrelated pilot-dashboard fix), and #2586 (review fixes). Confirmed by diff that #2580 does not touch
ContactCard/Committee/Rosteranywhere — no overlap with AB#2840/2885 (Eric Meewes' pending "moveMultiCommitteeRosteronto the content-resolver pattern" work, explicitly paused pending this plan; see below). One real fallout:imp/blazor-hybrid'sIThemeCoordinatorgained a newResolveThemeAsyncmember (so Search can resolve a feed card's site theme without applying it) that this branch's ownNoOpThemeCoordinatortest fake (Phase 1) didn't implement — same "interface evolved out from under a local test fake" shape as the repeatedIAuthenticationfallout, just a different interface. Fixed. Result: 3399/3399,WatchForStuckContainers_GivesUpAStillLoadingContainerstill an occasional flake under full-suite parallel load (confirmed passing in isolation three separate times) despite #2566 — apparently not fully resolved by that fix, or a distinct timing issue with the same symptom.- 2026-09-25, 6 commits, PR #2557 — Jose's "Search tabs are Offline and Online" work, the piece PR #2580's own description explicitly carved out ("Not included: PR ... (2557) is separate"), now landed on its own. Result: 3402/3402.
Downstream dependents worth tracking, not yet acted on:
- AB#2840 ("Move
MultiCommitteeRosteronto the content-resolver pattern", Eric Meewes, Task, New) — its own description says "Pause until done investigating refactors," a direct reference to this plan. It wantsMultiCommitteeRoster.razor(today: injectsIMECQueriesdirectly, maps volunteers inline, no role-ranking) moved onto the same shared mappingCommitteeMembersFactory(Phase 6, already merged) already has, "from one shared implementation" per its own acceptance criteria. Its child, AB#2885 ("Normalize sorting via helper class for all contact card lists", Issue, Active, created 2026-09-25), broadens this toLecRosterPagetoo. Genuinely downstream, not parallel — confirmed neither this plan's Phase 0–6 nor PR #2580 modifyContactCard/Committee/Rostercode, so this is a clean starting point for Eric once the consolidated PR lands, not a moving target.Work items: Feature AB#2845 "Dynamic Feed Orchestrator Refactor" (standalone — not parented to Epic AB#2081, which is already
Resolved), with one child Task per phase below.Original per-phase PR plan (historical — not how the work actually landed; see merge log above)
Each phase's exit criteria (§5) doubles as its PR boundary: land one phase, run the §7 gate, merge, then branch the next phase from an up-to-date
imp/blazor-hybrid. This mirrors the precedent already set by the sibling Service Layer Decoupling Plan, whose branch policy states "documentation can land independently; refactor implementation stays isolated until thoroughly tested" and which ships one branch per phase (codex/service-layer-phase0…phase7).Two phases are coarser than a good PR and should split further — migrate one page or one resolver per PR, not the whole phase at once, so a regression bisects to a single component:
- Phase 4 (migrate remaining page shells): one PR per page (
MecHomePage,AlpaIHomePage,LecPage/LecRosterPage, then any other CMS-driven page Phase 0 identifies).- Phase 6 (extract shared item resolution): one PR per candidate resolver (committee, committee-member, document, pilot, podcast, LEC link), after the shared-behavior extraction PR that precedes them.
- Phase 7 (unify search rendering): one PR per result type (contacts, documents, committees, links), per its own "migrate incrementally" instruction.
Phase PR scope Worktree / branch Depends on 0 Baseline inventory (docs/tests only, no runtime change) AB2846-DynFeedBaseline01/docs/AB2846-dyn-feed-baseline-01none — PR #2530 open against imp/blazor-hybrid1 IDynamicPageLoader+DynamicPageLoadResult(sync and progressive),LocalizedDynamicPageLoaderdelegationAB2847-DynFeedLoader01/feat/AB2847-dyn-feed-loader-01Phase 0 for context; compiles only against feat/ProgressiveContainerLoading(§2.1) — PR #2531 open against that branch, notimp/blazor-hybrid; retarget once it merges2 Shared dynamic page host AB2848-DynFeedHost01/feat/AB2848-dyn-feed-host-01Phase 1 — PR #2532 open against feat/AB2847-dyn-feed-loader-013 Migrate MecCommitteesPageto the generic hostAB2849-DynFeedMecCommittees01/feat/AB2849-dyn-feed-mec-committees-01Phase 2 4 One PR per page — see split above Done. MecHomePage: no code change, Phase 2 already covered it.LecPage: PR #2535.AlpaIHomePage/LecRosterPage: assessed and retained/excluded, doc-only, folded into PR #2535.Phase 3 5 ContentResolutionContextAB2851-DynFeedResolverContext01/feat/AB2851-dyn-feed-resolver-context-01Phase 4 (built on IAuthentication, not blocked on theISessionStateport — see §2's Phase 5 sequencing note)6 Shared item-resolution extraction, then one PR per resolver — see split above AB2852-DynFeedResolverShared01,AB2852-DynFeedResolverCommittee01, …Phase 5 7 One PR per search result type — see split above AB2853-DynFeedSearchContacts01,AB2853-DynFeedSearchDocuments01, …Phase 6 8 Unify favorites rendering AB2854-DynFeedFavorites01/feat/AB2854-dyn-feed-favorites-01Phase 7 9 Remove obsolete code, update docs AB2855-DynFeedCleanup01/chore/AB2855-dyn-feed-cleanup-01Phases 1–8 merged 10 CMS component catalog package (mobile-side capability advertisement; see Dynamic Content Admin and Mobile Architecture) AB2856-DynFeedCatalog01/feat/AB2856-dyn-feed-catalog-01Phases 1–9 substantially complete — the companion doc's runtime pipeline assumes IDynamicPageLoaderalready existsCurrent state (2026-09-29): phases 0 through 6, built on
chore/AB2845-dyn-feed-integration-01(see merge log above), landed inimp/blazor-hybridvia PR #2640, merged 2026-09-29. Phase 7 is withdrawn (handed to Jose — see Phase 7's own section). Phase 8 is on hold pending a check-in with Jose. AB#2840/2885 (Eric'sMultiCommitteeRoster/contact-list sorting work) is downstream and paused on this plan, not yet started. Phase 9 (obsolete-code removal, doc updates, component-resolution test coverage) is now underway, branched fromimp/blazor-hybriddirectly per this note's own prior guidance — worktreeAB2855-DynFeedCleanup01/ branchchore/AB2855-dyn-feed-cleanup-01. Phase 10 has not started.8. Risks and Mitigations
Risk Mitigation Generic loader loses localized fallback behavior Keep localization in a delegating loader and preserve dedicated tests before migration. Stable routes break during page removal Add route-resolution tests and migrate one route at a time. Feature-specific favorite identity is lost Treat identity as explicit resolver output and test it independently of visual component type. Search/favorites expose different fields than feed items Use an incremental common contract and retain source-specific payload data until all consumers migrate. RCL component dispatch silently fails Maintain RazorComponentResolutionTestsand add coverage for every new ViewModel mapping.A base orchestrator becomes a large inheritance hierarchy Use composition for item resolution and inheritance only for page policy. DynamicList remains unavailable Track IItemsQueriesimplementation and registration as a separate prerequisite.CMS authors content the mobile runtime cannot render Publish a catalog version with runtime capabilities and reject unsupported content before publication. CMS adapter leaks member-specific data or credentials Run authoring adapters behind an admin backend, classify capabilities by execution owner, and prohibit real member context in preview. CMS preview uses a different response shape than mobile Share normalized schemas and fixtures while keeping CMS and mobile adapter implementations separate. Admin styling bypasses the design system Expose approved theme tokens and assets, not arbitrary CSS or implementation selectors. Preview differs from mobile rendering Use shared payload schemas, fixtures, and compatibility tests for CMS preview and mobile runtime. UI regressions are hidden by successful builds Validate migrated routes with UI tests and compare narrow and wide layouts. 9. Completion Criteria
The refactor is complete when:
- ordinary dynamic pages use
DynamicPageLoader;- only pages requiring language selection use
LocalizedDynamicPageLoader;- CMS-driven pages no longer require dedicated Razor page components;
/mec/committeesremains a valid route but is hosted generically;- feed, search, and favorites can produce the same presentation model for the same content;
- shared content renders through
ItemComponentFactoryandComponentView;- feature resolvers contain source-specific policy but no page shell or Razor markup;
- the CMS consumes a versioned component catalog rather than internal adapters or RCL assemblies;
- published JSON validates against catalog schemas before reaching mobile clients;
- component, resolver, style-token, and asset compatibility is explicit and testable;
- the mobile runtime can report unsupported catalog capabilities and use documented fallbacks;
- favorite identity and navigation behavior remain correct;
- existing deep links continue to work;
- the solution build and unit suite pass;
- affected UI routes pass the appropriate platform validation.