# 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. ```csharp // 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** ```html @using TaskTracker.Core.Entities @using TaskTracker.Core.Enums @using TaskTracker.Core.DTOs @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers ``` **Step 3: Create _ViewStart.cshtml** ```html @{ 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: - `
` with logo, nav links (Board, Analytics, Mappings), search button (Ctrl+K hint), and "New Task" button - `
` with `@RenderBody()` - `
` empty container for the search modal - `
` empty container for the task detail slide-in - Script tags for htmx, Sortable, Chart.js, and app.js Nav links use `` 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** ```javascript // 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 Tasks)`. Use the same column config as the React app: ```csharp 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`: ```csharp public static readonly Dictionary 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. ```html @page @model TaskTracker.Api.Pages.BoardModel
@foreach (var col in Model.Columns) { }
``` **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: ```csharp 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 `
` 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: ```html ``` 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** ```javascript 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** ```javascript 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 `