Manager MVC → AngularJS SPA migration plan¶
Status: Proposed — pending review (Zafer) Created: 2026-06-13 Goal: Eliminate the old MVC Razor view layer in the Manager admin app.
1. The real situation: a hybrid app, ~70% already migrated¶
The Manager admin runs two apps side by side:
- OLD — ASP.NET MVC Razor pages at
/Manager/{Controller}/{Action}, inApplication/Areas/Manager/Views/{Folder}/*.cshtmlplus controllers inApplication/Areas/Manager/Controllers/*.cs. - NEW — AngularJS (ui-router) SPA at
/ng#/manager/{state}, inRaklet.Backend/Content/scripts/core/manager/{module}/. Each module declares$stateProvider.state('manager.{area}...')in its*.module.js.
The page that triggered this review (/Manager/Account/DonationCampaignCreate) is a
legacy leftover: the SPA already has manager.fundraising.campaigns.create, and
even the old nav header (Views/Shared/ContentHeader/_Fundraising.cshtml) links to
/ng#/manager/fundraising/campaigns/. The old URL is reachable by direct link but is
no longer the wired path. Most "old pages" are in this state.
2. Migration status of all 14 Manager controllers¶
Coverage = full (SPA has equivalent states): Home → dashboard · CorporateMembers / FastAccess → contacts · CustomMemberTypes → membership · Tags → settings/tags · JobPostings → jobs
Coverage = partial (SPA covers main screens; sub-pages still on Razor):
| Controller | In SPA | Still on Razor |
|---|---|---|
| Account (fundraising+payments) | campaigns, donations, scheduled | payment edit/delete, campaign image+thumbnail upload/crop, scheduled-failed payments/donations |
| EventV3 | list, create, edit, details, settings | manual ticket add/register/delete, ticket payment details, manage custom fields, mail/SMS to participants |
| MemberLists | contacts segments | filter add/remove builder, export |
| Members | contacts | family members (view/create/edit/remove) |
| Reports | overview, payments, refunds, logs | name changes, email activities, phone activities |
| Settings | general, webhooks, fields | payment-account OAuth/activation, Stripe bank account, Iyzico setup |
Coverage = none:
- OfferedServices: Index, Create, Edit, Delete, Preview, ServicesAwaitingApproval (no SPA module; only a nav reference in
settings-navigation.controller.js) - Announcement: redirects out, but the SPA target is unconfirmed
3. Three findings that correct the goal¶
- Redirect shims are load-bearing.
TagsController.csis a 301RedirectPermanentto the SPA. It has no view — it keeps old bookmarks, email links, and external deep-links alive. Deleting it 404s those URLs. The thin redirect layer must survive. - Payment-account flows can't fully leave the server.
SettingsController.cshas Stripe OAuth authorize/callback handlers. Providers redirect back to a server URL, so those callback endpoints stay as MVC actions. The UI can move to the SPA; the controller can't be deleted. - Coverage ≠ safe-to-delete.
JobPostingsController.Create()stillreturn View(...)even thoughmanager.job.createexists. A Razor view is only deletable when the SPA has parity AND nothing links to the old action. The sweep is a per-page verification exercise, not a trust-the-classification delete.
4. Premises¶
- End state = zero
.cshtmlviews underApplication/Areas/Manager/Views/, not deleting the area. A thin controller layer survives for (a) 301 redirects and (b) provider OAuth/callback endpoints. - "Migrate" splits three ways — delete (dead duplicates + dead features), finish (partial areas), build (net-new modules). Not "rewrite 50 pages."
- The expensive 20% (payment-account OAuth UI, OfferedServices) must not gate the cheap, high-value cleanup of the already-migrated 70%.
4a. Production usage data (App Insights, 90d) — corrects the strategy¶
Before committing to "cleanup first," we pulled real traffic for every /Manager/*
path, splitting each hit into rendered-200 (old Razor page actually showed) vs
redirect-3xx (bounced to the SPA — already a shim). Reproduce with
scripts/dev/manager-page-usage.ps1. (Counts are floors due to App Insights
sampling; trust the relative distinct-org breadth.)
The data flips the priority:
- The value is in EventV3 ticketing, not cleanup. Highest traffic + broadest org usage is on not-migrated EventV3 flows, all still rendering Razor: ticketpaymentdetails (880 renders / 34 orgs), addmanuallyticket (382 / 51 orgs), createticketoptions (51 / 27), managefields (24 / 5), deleteticketoption (16 / 7).
- "Fully migrated" fundraising isn't cut over. donationcampaigncreate still renders for 21 orgs and donationcampaignedit for 17, despite SPA equivalents existing — the cutover is incomplete, not a dead leftover.
- Already-migrated shims are working (0 renders, all 3xx): members/edit (35 orgs), reports/loginactivities (41 orgs), eventv3 create/edit/details, account/payments, corporatemembers/edit, custommembertypes/edit, fastaccess, Settings OAuth-return pages. Leave them.
- Genuine cleanup is a short, low-traffic tail (≤2-5 orgs each): memberlists/index, reports name/email/phone activities (no SPA equivalent — build-vs-kill), scheduledfaileddonations, deletemanualticket, deletestripepaymentaccount.
5. Strategy: usage-led — finish EventV3 first, cleanup is the tail¶
Originally "cleanup sweep first for the biggest visible win." The usage data doesn't support that — pure cleanup touches ~6 low-traffic pages. Re-sequenced by real org breadth on Razor:
- EventV3 ticketing → SPA (ticketpaymentdetails, add/create/delete manual ticket, managefields). Highest org count still on Razor; biggest UX win.
- Fundraising create/edit cutover — verify SPA parity, repoint entry points, 301 the old pages (21 + 17 orgs affected).
- Settings payment-account UI (addorupdate, paymentedit) — keep OAuth callbacks server-side.
- Trivial tail — retire the low-traffic list; for reports activity pages with no SPA equivalent, decide build-vs-kill (2-5 orgs each).
The dead-view sweep below still runs, but as the tail (step 4), not the lead.
Phase 1 — Dead-view sweep (one PR, low risk)¶
Gated on a per-page inbound-link audit. For every old Manager action, grep the whole solution for inbound references before deleting its view:
Url.Action("Action", "Controller"),@Html.ActionLink,@Url.ActionRedirectToAction("Action", "Controller")- hardcoded
href="/Manager/Controller/Action" - SPA nav templates (
manager-navigation,ContentHeader/*.cshtml) - emails/templates under
Services/,Raklet.Webjob/, public-sideApplication/Controllers/
Decision per page:
- Zero inbound links + SPA parity → delete the
.cshtml, collapse the GET action into aRedirectPermanentto the SPA state (preserves the URL). - Has inbound links → repoint the link at the SPA first, then delete.
- POST/callback only → leave the action; it's an API/callback endpoint, not a page.
Strong delete candidates (each still requires the audit): the full-coverage set — Tags (done), CorporateMembers, FastAccess, CustomMemberTypes shims; JobPostings Create/Edit/Delete/Preview views; the DonationCampaign* Account views whose SPA states already exist.
Deliverable: a one-time audit script (scripts/dev/manager-deadview-audit.ps1)
that emits, per old action: inbound-link count, SPA-parity Y/N, recommendation. PR #1
deletes everything it marks zero-inbound + parity.
Phase 2+ — Gaps become ranked backlog items¶
Not part of the sweep. Each is a normal product card, ranked by real usage (App Insights page-hit telemetry), not by guess:
- Finish partials (SPA module exists, add states): Account payment/image/scheduled, EventV3 ticket+participant flows, MemberLists export/filter, Members family, Reports activity reports, Settings payment-account UI.
- Build net-new: OfferedServices — but first pull usage; if it's a dead feature, delete it instead of migrating. Confirm Announcement's SPA target.
- Keep server-side (never deleted, may get SPA UI wrappers): Stripe/Iyzico OAuth authorize+callback, AddBankAccountToStripe, ActivateAccount.
6. Risks¶
- Silent 404s from deleting a view that still has one inbound link. Mitigated by the audit script + 301 collapse.
- Killing an OAuth callback breaks live payment onboarding. Mitigated by the "POST/callback only → leave it" rule.
- Assumed SPA parity that isn't real (SPA create page missing a field the Razor one has). Each "finish partial" card needs a parity check before the old view dies.
- Scope creep: the partial/none piles are tempting to over-build. Rank by usage; delete unused features rather than migrating them.
7. First action¶
Write scripts/dev/manager-deadview-audit.ps1: for each public GET action across the
14 Manager controllers, print {controller, action, inbound_link_count, spa_state_exists,
recommendation}. Run it, eyeball the zero-inbound + parity rows, and ship PR #1
deleting that confirmed-dead set with GET actions collapsed to RedirectPermanent.
That single PR proves the pattern and removes the largest, safest chunk of Razor in one
move — before any feature-build work starts.