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>
This commit is contained in:
2026-03-01 21:50:15 -05:00
parent e04e9573ea
commit 13b1f14344

View File

@@ -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:
- `<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**
```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<WorkTask> 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<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.
```html
@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:
```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 `<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:
```html
<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**
```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 `<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:
```css
.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**
```bash
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
```