Files
TaskTracker/docs/plans/2026-03-01-razor-pages-migration-design.md
AJ Isaacs e04e9573ea 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>
2026-03-01 21:47:08 -05:00

6.8 KiB

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

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