Files
TaskTracker/docs/plans/2026-03-01-razor-pages-migration-plan.md
AJ Isaacs 13b1f14344 docs: add Razor Pages migration implementation plan
11-task plan covering project setup, CSS/JS assets, Board page with
Kanban drag-and-drop, detail panel, search modal, Analytics page with
Chart.js, Mappings page with inline CRUD, and React app removal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:50:15 -05:00

26 KiB

Razor Pages Migration Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Replace the React/npm web UI with Razor Pages + htmx served from the existing TaskTracker.Api project, eliminating the Node toolchain entirely.

Architecture: Razor Pages added to the existing TaskTracker.Api project. Pages call repositories directly via DI (no API round-trip). htmx handles partial updates, SortableJS handles drag-and-drop, Chart.js handles analytics charts. All JS vendored as static files in wwwroot — zero npm.

Tech Stack: ASP.NET Razor Pages, htmx 2.0, SortableJS, Chart.js 4, vanilla JS

Reference files:

  • Design doc: docs/plans/2026-03-01-razor-pages-migration-design.md
  • Current React source: TaskTracker.Web/src/ (reference for feature parity)
  • Current CSS/tokens: TaskTracker.Web/src/index.css
  • API controllers: TaskTracker.Api/Controllers/ (keep unchanged)
  • Entities: TaskTracker.Core/Entities/ (WorkTask, TaskNote, ContextEvent, AppMapping)
  • Repositories: TaskTracker.Core/Interfaces/ (ITaskRepository, IContextEventRepository, IAppMappingRepository)
  • Enums: TaskTracker.Core/Enums/ (WorkTaskStatus, NoteType)

Task 1: Project Setup — Add Razor Pages to TaskTracker.Api

Files:

  • Modify: TaskTracker.Api/Program.cs
  • Create: TaskTracker.Api/Pages/_ViewImports.cshtml
  • Create: TaskTracker.Api/Pages/_ViewStart.cshtml
  • Create: TaskTracker.Api/Pages/Shared/_Layout.cshtml

Step 1: Update Program.cs to register Razor Pages

Add builder.Services.AddRazorPages() after the existing service registrations. Add app.MapRazorPages() before app.MapControllers(). Remove app.UseDefaultFiles() (Razor Pages handle routing now). Keep app.UseStaticFiles() for wwwroot.

// In Program.cs, after builder.Services.AddCors(...)
builder.Services.AddRazorPages();

// After app.UseCors()
app.UseStaticFiles();
app.MapRazorPages();
app.MapControllers();
// Remove: app.UseDefaultFiles();

Step 2: Create _ViewImports.cshtml

@using TaskTracker.Core.Entities
@using TaskTracker.Core.Enums
@using TaskTracker.Core.DTOs
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Step 3: Create _ViewStart.cshtml

@{
    Layout = "_Layout";
}

Step 4: Create _Layout.cshtml — the app shell

This is the shared layout with navigation bar, search modal placeholder, and script tags. Port the exact nav structure from TaskTracker.Web/src/components/Layout.tsx. Include the three vendored JS libraries and app.js.

The layout should have:

  • <header> with logo, nav links (Board, Analytics, Mappings), search button (Ctrl+K hint), and "New Task" button
  • <main> with @RenderBody()
  • <div id="search-modal"> empty container for the search modal
  • <div id="detail-panel"> empty container for the task detail slide-in
  • Script tags for htmx, Sortable, Chart.js, and app.js

Nav links use <a> tags with asp-page tag helpers. Active state uses a CSS class toggled by checking ViewContext.RouteData.

Step 5: Build and verify the app starts

Run: dotnet build TaskTracker.Api Expected: Build succeeds with no errors.

Step 6: Commit

feat(web): add Razor Pages scaffolding to API project

Task 2: Static Assets — CSS and Vendored JS

Files:

  • Create: TaskTracker.Api/wwwroot/css/site.css
  • Create: TaskTracker.Api/wwwroot/js/app.js (empty placeholder)
  • Download: TaskTracker.Api/wwwroot/lib/htmx.min.js
  • Download: TaskTracker.Api/wwwroot/lib/Sortable.min.js
  • Download: TaskTracker.Api/wwwroot/lib/chart.umd.min.js

Step 1: Create site.css

Port the design tokens and animations from TaskTracker.Web/src/index.css. Convert Tailwind utility patterns used across all React components into reusable CSS classes. Key sections:

  • CSS custom properties (:root block with all --color-* tokens)
  • Reset / base styles (dark background, font, box-sizing)
  • Animations (pulse-glow, live-dot, card-glow)
  • Scrollbar styles
  • Selection color
  • Noise grain texture overlay
  • Layout utilities (.flex, .grid, .flex-col, .items-center, .gap-*, etc. — only the ones actually used)
  • Component classes: .nav-link, .nav-link--active, .btn, .btn--primary, .btn--danger, .btn--amber, .btn--emerald, .stat-card, .badge, .input, .select, etc.
  • Kanban-specific: .kanban-grid, .kanban-column, .task-card, .task-card--active
  • Detail panel: .detail-overlay, .detail-panel, slide-in transition classes
  • Table styles for Mappings page
  • Responsive: the app is desktop-first, minimal responsive needed

Reference: Read every React component's className strings to ensure complete coverage. The CSS file will be ~400-600 lines.

Step 2: Download vendored JS libraries

Use curl to download from CDNs:

  • htmx 2.0: https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js
  • SortableJS: https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js
  • Chart.js 4: https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js

Step 3: Create empty app.js placeholder

// TaskTracker app.js — command palette, keyboard shortcuts, drag-and-drop wiring
// Will be populated in later tasks

Step 4: Verify static files serve

Run the app, navigate to /css/site.css, /lib/htmx.min.js — verify 200 responses.

Step 5: Commit

feat(web): add CSS design system and vendored JS libraries

Task 3: Board Page — Kanban Columns (Server-Rendered)

Files:

  • Create: TaskTracker.Api/Pages/Board.cshtml
  • Create: TaskTracker.Api/Pages/Board.cshtml.cs
  • Create: TaskTracker.Api/Pages/Partials/_KanbanColumn.cshtml
  • Create: TaskTracker.Api/Pages/Partials/_TaskCard.cshtml
  • Create: TaskTracker.Api/Pages/Partials/_CreateTaskForm.cshtml
  • Create: TaskTracker.Api/Pages/Partials/_FilterBar.cshtml

Step 1: Create Board.cshtml.cs (PageModel)

The PageModel should:

  • Inject ITaskRepository
  • OnGetAsync(): Load all tasks with subtasks (GetAllAsync(includeSubTasks: true)), filter to top-level only (ParentTaskId == null), group by status into 4 column view models. Accept optional category and hasSubtasks query params for filtering.
  • OnGetColumnAsync(WorkTaskStatus status): Return a single column partial (for htmx swap after drag-and-drop).
  • OnPostCreateTaskAsync(string title, string? category): Create a task, return updated Pending column partial.
  • OnPutStartAsync(int id): Start task (pause current active), return updated board columns.
  • OnPutPauseAsync(int id): Pause task, return updated board columns.
  • OnPutResumeAsync(int id): Resume task (pause current active), return updated board columns.
  • OnPutCompleteAsync(int id): Complete task, return updated board columns.
  • OnDeleteAbandonAsync(int id): Abandon (delete) task, return updated board columns.

For htmx handlers, detect Request.Headers["HX-Request"] and return Partial("Partials/_KanbanColumn", columnModel) instead of the full page.

Define a ColumnViewModel record: record ColumnViewModel(WorkTaskStatus Status, string Label, string Color, List<WorkTask> Tasks).

Use the same column config as the React app:

static readonly ColumnViewModel[] Columns = [
    new(WorkTaskStatus.Pending, "Pending", "#64748b", []),
    new(WorkTaskStatus.Active, "Active", "#3b82f6", []),
    new(WorkTaskStatus.Paused, "Paused", "#eab308", []),
    new(WorkTaskStatus.Completed, "Completed", "#22c55e", []),
];

Category colors dictionary — same as CATEGORY_COLORS in constants.ts:

public static readonly Dictionary<string, string> CategoryColors = new()
{
    ["Development"] = "#6366f1",
    ["Research"] = "#06b6d4",
    ["Communication"] = "#8b5cf6",
    ["DevOps"] = "#f97316",
    ["Documentation"] = "#14b8a6",
    ["Design"] = "#ec4899",
    ["Testing"] = "#3b82f6",
    ["General"] = "#64748b",
    ["Email"] = "#f59e0b",
    ["Engineering"] = "#6366f1",
    ["LaserCutting"] = "#ef4444",
    ["Unknown"] = "#475569",
};

Step 2: Create Board.cshtml

Renders the filter bar partial and a 4-column grid. Each column rendered via _KanbanColumn partial.

@page
@model TaskTracker.Api.Pages.BoardModel

<div class="board-page">
    <partial name="Partials/_FilterBar" model="Model" />
    <div id="kanban-board" class="kanban-grid">
        @foreach (var col in Model.Columns)
        {
            <partial name="Partials/_KanbanColumn" model="col" />
        }
    </div>
</div>

Step 3: Create _KanbanColumn.cshtml

Each column has:

  • Column header with status label, colored dot, and task count
  • id="column-{Status}" for htmx targeting and SortableJS group
  • data-status="{Status}" for JS to read on drag-and-drop
  • List of _TaskCard partials
  • If Pending column: include _CreateTaskForm partial at the bottom

Step 4: Create _TaskCard.cshtml

Each card has:

  • id="task-{Id}" and data-task-id="{Id}" for SortableJS
  • Card glow class, active pulse class if status == Active
  • Live dot indicator if Active
  • Title, category dot, elapsed time
  • Subtask progress dots (green = completed, dim = incomplete) + count
  • hx-get="/board?handler=TaskDetail&id={Id}" hx-target="#detail-panel" on click

Reference: TaskTracker.Web/src/components/TaskCard.tsx for exact structure.

Elapsed time formatting — port formatElapsed to a C# helper method on the PageModel or a static helper:

public static string FormatElapsed(DateTime? startedAt, DateTime? completedAt)
{
    if (startedAt is null) return "--";
    var start = startedAt.Value;
    var end = completedAt ?? DateTime.UtcNow;
    var mins = (int)(end - start).TotalMinutes;
    if (mins < 60) return $"{mins}m";
    var hours = mins / 60;
    var remainder = mins % 60;
    if (hours < 24) return $"{hours}h {remainder}m";
    var days = hours / 24;
    return $"{days}d {hours % 24}h";
}

Step 5: Create _FilterBar.cshtml

Category filter chips. Each chip is an <a> with htmx attributes:

  • hx-get="/board?category={cat}" hx-target="#kanban-board" hx-swap="innerHTML"
  • Active chip styled with category color background
  • "All" chip to clear filter

Step 6: Create _CreateTaskForm.cshtml

Inline form at bottom of Pending column:

  • Text input for title
  • htmx POST: hx-post="/board?handler=CreateTask" hx-target="#column-Pending" hx-swap="outerHTML"
  • Form submits on Enter

Step 7: Build and manually test

Run: dotnet run --project TaskTracker.Api Navigate to /board. Verify 4 columns render with tasks from the database.

Step 8: Commit

feat(web): add Board page with Kanban columns and task cards

Task 4: Board Page — Task Detail Panel

Files:

  • Create: TaskTracker.Api/Pages/Partials/_TaskDetail.cshtml
  • Create: TaskTracker.Api/Pages/Partials/_SubtaskList.cshtml
  • Create: TaskTracker.Api/Pages/Partials/_NotesList.cshtml
  • Modify: TaskTracker.Api/Pages/Board.cshtml.cs (add handler methods)

Step 1: Add handler methods to Board.cshtml.cs

  • OnGetTaskDetailAsync(int id): Load task by ID (with subtasks, notes), return _TaskDetail partial.
  • OnPutUpdateTaskAsync(int id, string? title, string? description, string? category, int? estimatedMinutes): Update task fields, return updated _TaskDetail partial.
  • OnPostAddSubtaskAsync(int id, string title): Create subtask with parentTaskId = id, return updated _SubtaskList partial.
  • OnPutCompleteSubtaskAsync(int id): Complete a subtask, return updated _SubtaskList partial.
  • OnPostAddNoteAsync(int id, string content): Add a General note, return updated _NotesList partial.
  • OnGetSearchAsync(string q): Search tasks by title/description/category (case-insensitive contains), return _SearchResults partial.

Step 2: Create _TaskDetail.cshtml

Port the structure from TaskTracker.Web/src/components/TaskDetailPanel.tsx:

  • Close button (onclick calls JS to hide the panel)
  • Title: displayed as text, with hx-get to swap in an edit form on click (or use JS contenteditable with htmx hx-put on blur)
  • Status badge (colored pill)
  • Category: click-to-edit (same pattern as title)
  • Description section: click-to-edit textarea
  • Time section: elapsed vs estimate, progress bar
  • Estimate: click-to-edit number input
  • Subtask list partial
  • Notes list partial
  • Action buttons at bottom (status-dependent: Start/Pause/Resume/Complete/Abandon)

Inline editing approach: Each editable field has two states (display and edit). Use htmx hx-get to swap the display element with an edit form, and hx-put on the form to save and swap back to display. Or use a small JS helper that toggles visibility and fires htmx on blur.

Action buttons use htmx:

<button hx-put="/board?handler=Start&id=@task.Id"
        hx-target="#kanban-board" hx-swap="innerHTML"
        class="btn btn--primary">Start</button>

After a status-change action, the board columns should refresh AND the detail panel should update. Use hx-swap-oob (out-of-band swap) to update both targets in one response, or have the JS close the panel after the action completes.

Step 3: Create _SubtaskList.cshtml

  • List of subtasks with checkbox icons
  • Completed subtasks show line-through
  • Click non-completed → htmx PUT to complete, swap subtask list
  • Inline input to add new subtask → htmx POST

Step 4: Create _NotesList.cshtml

  • Notes sorted chronologically
  • Type badge (Pause=amber, Resume=blue, General=subtle)
  • Relative timestamps (port the JS formatRelativeTime logic to C#)
  • Inline input to add new note → htmx POST

Step 5: Build and manually test

Click a task card → detail panel should slide in. Test inline editing, subtask creation, note creation, and action buttons.

Step 6: Commit

feat(web): add task detail panel with inline editing, subtasks, and notes

Task 5: Board Page — Drag-and-Drop with SortableJS

Files:

  • Modify: TaskTracker.Api/wwwroot/js/app.js

Step 1: Implement SortableJS wiring in app.js

function initKanban() {
    document.querySelectorAll('.kanban-column-body').forEach(el => {
        new Sortable(el, {
            group: 'kanban',
            animation: 150,
            ghostClass: 'task-card--ghost',
            dragClass: 'task-card--dragging',
            onEnd: function(evt) {
                const taskId = evt.item.dataset.taskId;
                const fromStatus = evt.from.dataset.status;
                const toStatus = evt.to.dataset.status;
                if (fromStatus === toStatus) return;

                let handler = null;
                if (toStatus === 'Active' && fromStatus === 'Paused') handler = 'Resume';
                else if (toStatus === 'Active') handler = 'Start';
                else if (toStatus === 'Paused') handler = 'Pause';
                else if (toStatus === 'Completed') handler = 'Complete';
                else { evt.from.appendChild(evt.item); return; } // Revert if invalid

                htmx.ajax('PUT', `/board?handler=${handler}&id=${taskId}`, {
                    target: '#kanban-board',
                    swap: 'innerHTML'
                });
            }
        });
    });
}

Add ghost card CSS: .task-card--ghost gets rotation, scale, opacity matching the React DragOverlay.

Call initKanban() on DOMContentLoaded and after htmx swaps (listen for htmx:afterSwap event on #kanban-board).

Step 2: Add htmx:afterSwap listener to re-init Sortable after board updates

document.addEventListener('htmx:afterSwap', function(evt) {
    if (evt.detail.target.id === 'kanban-board' ||
        evt.detail.target.closest('#kanban-board')) {
        initKanban();
    }
});

Step 3: Manually test drag-and-drop

Drag a Pending task to Active → should fire Start API call and refresh board. Drag Active to Paused → Pause. Drag Paused to Active → Resume. Drag to Completed → Complete. Drag to Pending → should revert (snap back).

Step 4: Commit

feat(web): add drag-and-drop between Kanban columns via SortableJS

Task 6: Board Page — Search Modal (Ctrl+K)

Files:

  • Modify: TaskTracker.Api/wwwroot/js/app.js
  • Create: TaskTracker.Api/Pages/Partials/_SearchResults.cshtml
  • Modify: TaskTracker.Api/Pages/Board.cshtml.cs (add search handler)

Step 1: Add search handler to Board.cshtml.cs

OnGetSearchAsync(string? q):

  • If q is empty/null: return recent Active/Paused/Pending tasks (up to 8)
  • If q has value: search tasks where title, description, or category contains q (case-insensitive), limit 10
  • Return _SearchResults partial

Step 2: Create _SearchResults.cshtml

List of results, each with:

  • Status color dot
  • Title
  • Category badge
  • Each result is a link/button that navigates to /board?task={id} or fires JS to open the detail panel

Step 3: Implement search modal in app.js

~80 lines of vanilla JS:

  • Ctrl+K / Cmd+K opens the modal (toggle #search-modal visibility)
  • Escape closes
  • Input field with debounced htmx fetch: hx-get="/board?handler=Search&q={value}" hx-target="#search-results" hx-trigger="input changed delay:200ms"
  • Arrow key navigation: track selected index, move highlight, Enter to navigate
  • Backdrop click closes

The search modal HTML structure can be in _Layout.cshtml (hidden by default) with the results container inside it.

Step 4: Manually test

Press Ctrl+K → modal opens. Type a search term → results appear. Arrow keys move selection. Enter opens task. Escape closes.

Step 5: Commit

feat(web): add Ctrl+K command palette search modal

Task 7: Analytics Page

Files:

  • Create: TaskTracker.Api/Pages/Analytics.cshtml
  • Create: TaskTracker.Api/Pages/Analytics.cshtml.cs

Step 1: Create Analytics.cshtml.cs

Inject ITaskRepository, IContextEventRepository, IAppMappingRepository.

OnGetAsync(int minutes = 1440, int? taskId = null):

  • Load all tasks
  • Load context events for the time range
  • Load mappings
  • Compute stat cards: open tasks count, total active time, top category
  • Compute timeline data: bucket events by hour, resolve category via mappings, serialize as JSON for Chart.js
  • Compute category breakdown: group events by resolved category, count, serialize as JSON
  • Load activity feed (first 20 events, most recent first)

OnGetActivityFeedAsync(int minutes, int? taskId, int offset):

  • Return next batch of activity feed items as a partial (for htmx "Load more")

Step 2: Create Analytics.cshtml

Port structure from TaskTracker.Web/src/pages/Analytics.tsx:

  • Header with time range and task filter dropdowns (use <select> with htmx hx-get on change to reload the page with new params)
  • 3 stat cards (rendered server-side)
  • Timeline section: <canvas id="timeline-chart"> + inline <script> that reads JSON from a <script type="application/json"> tag and renders a Chart.js bar chart
  • Category breakdown: <canvas id="category-chart"> + Chart.js donut
  • Activity feed: server-rendered list with "Load more" button using htmx

Chart.js config should match the React Recharts appearance:

  • Timeline: vertical bar chart, bars colored by category, custom tooltip
  • Category: donut chart (cutout 60%), padding angle, legend on right with colored dots and percentages

The category color resolution logic (matching app names to categories via mappings) should be done server-side and the resolved data passed to Chart.js as JSON.

Step 3: Build and manually test

Navigate to /analytics. Verify stat cards, charts render, activity feed loads, "Load more" works, dropdowns filter data.

Step 4: Commit

feat(web): add Analytics page with Chart.js charts and activity feed

Task 8: Mappings Page

Files:

  • Create: TaskTracker.Api/Pages/Mappings.cshtml
  • Create: TaskTracker.Api/Pages/Mappings.cshtml.cs
  • Create: TaskTracker.Api/Pages/Partials/_MappingRow.cshtml
  • Create: TaskTracker.Api/Pages/Partials/_MappingEditRow.cshtml

Step 1: Create Mappings.cshtml.cs

Inject IAppMappingRepository.

Handlers:

  • OnGetAsync(): Load all mappings, render full page
  • OnGetEditRowAsync(int id): Load mapping by ID, return _MappingEditRow partial (for inline edit)
  • OnGetAddRowAsync(): Return empty _MappingEditRow partial (for adding new)
  • OnPostSaveAsync(int? id, string pattern, string matchType, string category, string? friendlyName): Create or update mapping. If id is null, create; otherwise update. Return _MappingRow partial for the saved row.
  • OnDeleteAsync(int id): Delete mapping, return empty response (htmx removes the row)

Step 2: Create Mappings.cshtml

Port structure from TaskTracker.Web/src/pages/Mappings.tsx:

  • Header with "App Mappings" title and "Add Rule" button
  • Table with columns: Pattern, Match Type, Category, Friendly Name, Actions
  • Each row rendered via _MappingRow partial
  • "Add Rule" button uses htmx to insert _MappingEditRow at the top of the tbody
  • Empty state when no mappings

Step 3: Create _MappingRow.cshtml

Display row with:

  • Pattern (monospace)
  • Match type badge (colored: ProcessName=indigo, TitleContains=cyan, UrlContains=orange)
  • Category with colored dot
  • Friendly name
  • Edit button: hx-get="/mappings?handler=EditRow&id={Id}" hx-target="closest tr" hx-swap="outerHTML"
  • Delete button: hx-delete="/mappings?handler=Delete&id={Id}" hx-target="closest tr" hx-swap="outerHTML" hx-confirm="Delete this mapping rule?"

Step 4: Create _MappingEditRow.cshtml

Inline edit row (replaces the display row) with:

  • Pattern text input (autofocus)
  • Match type select (ProcessName, TitleContains, UrlContains)
  • Category text input
  • Friendly name text input
  • Save button (check icon): hx-post="/mappings?handler=Save" with form values
  • Cancel button (x icon): hx-get="/mappings?handler=Row&id={Id}" to swap back to display (or for new rows, just remove the row)

Step 5: Build and manually test

Navigate to /mappings. Add a new mapping, edit it, delete it. Verify inline edit and table updates.

Step 6: Commit

feat(web): add Mappings page with inline CRUD table

Task 9: Detail Panel Slide-In Animation and Polish

Files:

  • Modify: TaskTracker.Api/wwwroot/css/site.css
  • Modify: TaskTracker.Api/wwwroot/js/app.js

Step 1: Implement slide-in animation in CSS

The detail panel uses CSS transitions instead of Framer Motion:

.detail-overlay {
    position: fixed;
    inset: 0;
    z-index: 40;
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(4px);
    opacity: 0;
    transition: opacity 0.2s ease;
    pointer-events: none;
}
.detail-overlay--open {
    opacity: 1;
    pointer-events: auto;
}

.detail-panel {
    position: fixed;
    top: 0;
    right: 0;
    height: 100%;
    width: 480px;
    z-index: 50;
    transform: translateX(100%);
    transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
    /* ... background, border, shadow */
}
.detail-panel--open {
    transform: translateX(0);
}

Step 2: Implement panel open/close logic in app.js

  • Opening: When htmx loads the detail partial into #detail-panel, add --open classes to overlay and panel
  • Closing: Remove --open classes, wait for transition end, then clear the panel content
  • Backdrop click closes
  • Escape key closes (unless editing inline)

Step 3: Implement inline editing helpers in app.js

Small helper functions for click-to-edit fields:

  • startEdit(fieldId): hide display element, show input, focus
  • cancelEdit(fieldId): hide input, show display element
  • saveEdit(fieldId): read input value, fire htmx request, on success update display

~30 lines of JS.

Step 4: Manually test

Click a task card → panel slides in smoothly. Click backdrop or press Escape → panel slides out. Inline edit title, description, category, estimate. Verify smooth transitions.

Step 5: Commit

feat(web): add detail panel slide-in animation and inline editing

Task 10: Remove React App and Clean Up

Files:

  • Delete: TaskTracker.Web/ directory (entire React app)
  • Modify: .gitignore (remove node_modules entry if no longer needed)

Step 1: Verify all features work

Before deleting, do a final pass:

  • Board: 4 columns render, drag-and-drop works, task creation works, filters work
  • Detail panel: slides in, inline edit works, subtasks/notes work, action buttons work
  • Search: Ctrl+K opens, search works, keyboard navigation works
  • Analytics: stat cards, charts, activity feed, filters all work
  • Mappings: CRUD table works with inline editing

Step 2: Delete the React app directory

rm -rf TaskTracker.Web/

Step 3: Update .gitignore

Remove any React/Node-specific entries. Keep entries relevant to .NET.

Step 4: Build the full solution

Run: dotnet build Expected: All projects build successfully. No references to TaskTracker.Web.

Step 5: Commit

feat(web): remove React app — migration to Razor Pages complete

Task 11: Final Integration Test

Step 1: Run the app and test end-to-end

Start the API: dotnet run --project TaskTracker.Api

Test checklist:

  • Navigate to /board — Kanban columns load with tasks
  • Create a new task via inline form
  • Drag task from Pending to Active
  • Drag task from Active to Paused
  • Drag task from Paused to Active (resume)
  • Drag task to Completed
  • Click task card — detail panel slides in
  • Edit title, description, category, estimate inline
  • Add a subtask, complete it
  • Add a note
  • Use action buttons (Start, Pause, Resume, Complete, Abandon)
  • Ctrl+K search — type query, arrow navigate, enter to select
  • Category filter chips on board
  • Navigate to /analytics — stat cards, charts, activity feed
  • Change time range and task filter dropdowns
  • Click "Load more" on activity feed
  • Navigate to /mappings — table renders
  • Add a new mapping rule
  • Edit an existing mapping
  • Delete a mapping
  • Verify API endpoints still work (/api/tasks, /api/mappings, /swagger)

Step 2: Verify external consumers still work

  • MCP server: uses /api/* endpoints — unchanged
  • WindowWatcher: uses /api/context — unchanged
  • Chrome extension: uses /api/* — unchanged
  • Swagger UI: /swagger — still accessible

Step 3: Commit any final fixes

fix(web): address integration test findings