docs: add Razor Pages migration design
Design for moving the TaskTracker web UI from React/npm to Razor Pages + htmx, eliminating the Node toolchain and unifying on a single .NET stack with one-binary deployment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
160
docs/plans/2026-03-01-razor-pages-migration-design.md
Normal file
160
docs/plans/2026-03-01-razor-pages-migration-design.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# TaskTracker Web UI: React to Razor Pages Migration
|
||||
|
||||
## Date: 2026-03-01
|
||||
|
||||
## Motivation
|
||||
|
||||
- **Single tech stack:** Eliminate the npm/Node toolchain; everything in C#/.NET
|
||||
- **Single-binary deployment:** UI bundled into the API project — one process, one port, no separate build step
|
||||
|
||||
## Approach: Razor Pages + htmx
|
||||
|
||||
Server-rendered HTML with Razor syntax. htmx handles partial page updates via AJAX. Vanilla JS for drag-and-drop (SortableJS), charts (Chart.js), and keyboard shortcuts.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Single Process
|
||||
|
||||
Razor Pages are added directly to the existing `TaskTracker.Api` project. No separate web project.
|
||||
|
||||
- One `Program.cs`, one binary, one port
|
||||
- Razor Pages call repositories directly (same DI container) — no API round-trips for the UI
|
||||
- API controllers remain at `/api/*` for external consumers (MCP, WindowWatcher, Chrome extension)
|
||||
- `wwwroot/` hosts static JS/CSS files
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
TaskTracker.Api/
|
||||
├── Pages/
|
||||
│ ├── _ViewImports.cshtml
|
||||
│ ├── _ViewStart.cshtml
|
||||
│ ├── Shared/
|
||||
│ │ └── _Layout.cshtml (shell: nav bar, search modal, script tags)
|
||||
│ ├── Board.cshtml + Board.cshtml.cs
|
||||
│ ├── Analytics.cshtml + Analytics.cshtml.cs
|
||||
│ └── Mappings.cshtml + Mappings.cshtml.cs
|
||||
├── Pages/Partials/
|
||||
│ ├── _KanbanColumn.cshtml (single column, htmx swappable)
|
||||
│ ├── _TaskCard.cshtml (single card)
|
||||
│ ├── _TaskDetail.cshtml (slide-in detail panel)
|
||||
│ ├── _FilterBar.cshtml (category chip bar)
|
||||
│ ├── _SubtaskList.cshtml
|
||||
│ ├── _NotesList.cshtml
|
||||
│ ├── _CreateTaskForm.cshtml
|
||||
│ ├── _MappingRow.cshtml (single table row, inline edit)
|
||||
│ └── _SearchResults.cshtml (command palette results)
|
||||
├── wwwroot/
|
||||
│ ├── css/site.css (dark theme, design tokens, animations)
|
||||
│ ├── lib/htmx.min.js (~14KB gzipped)
|
||||
│ ├── lib/Sortable.min.js (~40KB)
|
||||
│ ├── lib/chart.min.js (~65KB gzipped)
|
||||
│ └── js/app.js (command palette, keyboard shortcuts, DnD wiring)
|
||||
└── Program.cs (adds AddRazorPages + MapRazorPages)
|
||||
```
|
||||
|
||||
### Program.cs Changes
|
||||
|
||||
```csharp
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
// ... existing setup ...
|
||||
|
||||
app.MapRazorPages(); // Add alongside MapControllers()
|
||||
app.MapControllers(); // Keep for API consumers
|
||||
```
|
||||
|
||||
## Client-Side Libraries (all vendored, no npm)
|
||||
|
||||
| Library | Size (gzipped) | Purpose |
|
||||
|---------|---------------|---------|
|
||||
| htmx | ~14KB | Partial page updates via AJAX |
|
||||
| SortableJS | ~40KB | Drag-and-drop Kanban columns |
|
||||
| Chart.js | ~65KB | Timeline bar chart, category donut |
|
||||
| app.js | ~100 lines | Command palette, keyboard shortcuts, DnD wiring |
|
||||
|
||||
## Styling
|
||||
|
||||
Plain CSS (no Tailwind, no build step). Same dark design system ported from the React app:
|
||||
|
||||
- CSS custom properties for all design tokens (`--color-page`, `--color-surface`, `--color-accent`, etc.)
|
||||
- Same animations: `animate-pulse-glow`, `animate-live-dot`, `card-glow`
|
||||
- Custom scrollbars, selection highlight, grain texture
|
||||
|
||||
## Page-by-Page Design
|
||||
|
||||
### Board Page
|
||||
|
||||
- 4 Kanban columns rendered server-side as partials
|
||||
- **Drag-and-drop:** SortableJS on each column; `onEnd` callback reads task ID + target column status, fires htmx request:
|
||||
- Pending → Active: `PUT /board/tasks/{id}/start`
|
||||
- Active → Paused: `PUT /board/tasks/{id}/pause`
|
||||
- Paused → Active: `PUT /board/tasks/{id}/resume`
|
||||
- Any → Completed: `PUT /board/tasks/{id}/complete`
|
||||
- Server returns updated column partials
|
||||
- **Task detail panel:** `hx-get="/board/tasks/{id}/detail"` loads partial into right-side container; CSS transition handles slide-in
|
||||
- **Inline editing:** Click-to-edit fields use `hx-put` on blur/Enter
|
||||
- **Subtasks/notes:** Rendered as partials inside detail panel; htmx handles add/complete
|
||||
- **Filter bar:** htmx re-requests board with query params (`?category=Development&hasSubtasks=true`)
|
||||
- **Create task:** Inline form in Pending column, htmx POST
|
||||
|
||||
### Analytics Page
|
||||
|
||||
- Stat cards rendered server-side (open tasks count, active time, top category)
|
||||
- **Charts:** Chart.js renders from JSON data serialized into `<script>` tags by the server
|
||||
- Timeline: bar chart with events bucketed by hour, colored by category
|
||||
- Category breakdown: donut chart with legend
|
||||
- **Activity feed:** Initial batch server-rendered; "Load more" button uses htmx to append items
|
||||
|
||||
### Mappings Page
|
||||
|
||||
- Table rows rendered server-side
|
||||
- **Inline edit:** Click "Edit" → htmx swaps row with editable form row partial
|
||||
- **Add new:** htmx inserts empty form row at top
|
||||
- **Delete:** `hx-delete` with `hx-confirm` for confirmation
|
||||
|
||||
### Search Modal (Ctrl+K)
|
||||
|
||||
- `app.js` handles keyboard shortcut and modal open/close
|
||||
- On keystroke: htmx fetches `/board/search?q={query}`, swaps results list
|
||||
- Arrow key navigation + Enter-to-select in JS (~80 lines)
|
||||
- When empty: shows recent Active/Paused/Pending tasks
|
||||
|
||||
## htmx Endpoint Design
|
||||
|
||||
Razor Pages handler methods return HTML partials (not JSON). Examples:
|
||||
|
||||
| Endpoint | Method | Returns |
|
||||
|----------|--------|---------|
|
||||
| `/board` | GET | Full board page |
|
||||
| `/board?handler=Column&status=Active` | GET | Single column partial |
|
||||
| `/board?handler=TaskDetail&id=5` | GET | Detail panel partial |
|
||||
| `/board?handler=Search&q=auth` | GET | Search results partial |
|
||||
| `/board?handler=Start&id=5` | PUT | Updated board columns |
|
||||
| `/board?handler=Pause&id=5` | PUT | Updated board columns |
|
||||
| `/board?handler=Resume&id=5` | PUT | Updated board columns |
|
||||
| `/board?handler=Complete&id=5` | PUT | Updated board columns |
|
||||
| `/board?handler=CreateTask` | POST | Updated Pending column |
|
||||
| `/board?handler=UpdateTask&id=5` | PUT | Updated task card |
|
||||
| `/board?handler=AddSubtask&id=5` | POST | Updated subtask list |
|
||||
| `/board?handler=AddNote&id=5` | POST | Updated notes list |
|
||||
| `/analytics` | GET | Full analytics page |
|
||||
| `/analytics?handler=ActivityFeed&offset=20` | GET | More activity items |
|
||||
| `/mappings` | GET | Full mappings page |
|
||||
| `/mappings?handler=EditRow&id=3` | GET | Editable row partial |
|
||||
| `/mappings?handler=Save` | POST/PUT | Updated row partial |
|
||||
| `/mappings?handler=Delete&id=3` | DELETE | Empty (row removed) |
|
||||
|
||||
## What Gets Removed
|
||||
|
||||
- `TaskTracker.Web/` directory (React app: node_modules, package.json, src/, dist/, etc.)
|
||||
- `UseDefaultFiles()` in Program.cs (Razor Pages handle routing)
|
||||
- CORS configuration becomes optional (UI is same-origin)
|
||||
|
||||
## What Stays Unchanged
|
||||
|
||||
- All API controllers (`/api/*`) — used by MCP, WindowWatcher, Chrome extension
|
||||
- `TaskTracker.Core` — entities, DTOs, interfaces
|
||||
- `TaskTracker.Infrastructure` — EF Core, repositories, migrations
|
||||
- `TaskTracker.MCP` — MCP server
|
||||
- `WindowWatcher` — context watcher service
|
||||
Reference in New Issue
Block a user