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:
720
docs/plans/2026-03-01-razor-pages-migration-plan.md
Normal file
720
docs/plans/2026-03-01-razor-pages-migration-plan.md
Normal 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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user