diff --git a/docs/plans/2026-03-01-razor-pages-migration-plan.md b/docs/plans/2026-03-01-razor-pages-migration-plan.md new file mode 100644 index 0000000..16a4b57 --- /dev/null +++ b/docs/plans/2026-03-01-razor-pages-migration-plan.md @@ -0,0 +1,720 @@ +# 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 `