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