- {/* Header */}
-
- {/* Title row with close button inline */}
-
-
- {editingTitle ? (
- setTitleValue(e.target.value)}
- onBlur={saveTitle}
- onKeyDown={(e) => {
- if (e.key === 'Enter') saveTitle()
- if (e.key === 'Escape') setEditingTitle(false)
- }}
- className="w-full bg-[var(--color-page)] text-xl font-semibold text-[var(--color-text-primary)] px-3 py-2 rounded border border-[var(--color-accent)] outline-none"
- />
- ) : (
-
{
- setTitleValue(task.title)
- setEditingTitle(true)
- }}
- >
- {task.title}
-
- )}
-
-
-
-
- {/* Status badge + Category */}
-
- {statusConfig && (
-
- {statusConfig.label}
-
- )}
-
- {editingCategory ? (
- setCategoryValue(e.target.value)}
- onBlur={saveCategory}
- onKeyDown={(e) => {
- if (e.key === 'Enter') saveCategory()
- if (e.key === 'Escape') setEditingCategory(false)
- }}
- placeholder="Category..."
- className="bg-[var(--color-page)] text-xs text-[var(--color-text-primary)] px-2 py-1 rounded border border-[var(--color-accent)] outline-none w-28"
- />
- ) : (
- {
- setCategoryValue(task.category ?? '')
- setEditingCategory(true)
- }}
- >
- {task.category || 'Add category'}
-
- )}
-
-
-
-
-
- {/* Description */}
-
-
- Description
-
- {editingDesc ? (
-
-
-
-
- {/* Time */}
-
-
- Time
-
-
-
-
- Elapsed
-
- {task.startedAt ? formatElapsed(task.startedAt, task.completedAt) : '--'}
-
-
-
- Estimate
- {editingEstimate ? (
- setEstimateValue(e.target.value)}
- onBlur={saveEstimate}
- onKeyDown={(e) => {
- if (e.key === 'Enter') saveEstimate()
- if (e.key === 'Escape') setEditingEstimate(false)
- }}
- placeholder="min"
- className="w-full bg-[var(--color-page)] text-sm text-[var(--color-text-primary)] px-2 py-1 rounded border border-[var(--color-accent)] outline-none"
- />
- ) : (
- {
- setEstimateValue(task.estimatedMinutes?.toString() ?? '')
- setEditingEstimate(true)
- }}
- >
- {task.estimatedMinutes ? `${task.estimatedMinutes}m` : '--'}
-
- )}
-
-
-
- {/* Progress bar */}
- {progressPercent !== null && (
-
-
= 100
- ? 'bg-rose-500'
- : 'bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-end)]'
- }`}
- style={{ width: `${Math.min(progressPercent, 100)}%` }}
- />
-
- )}
-
-
-
-
- {/* Subtasks */}
-
-
-
-
-
-
- {/* Notes */}
-
-
-
-
-
- {/* Action buttons - fixed at bottom */}
- {task.status !== WorkTaskStatus.Completed && task.status !== WorkTaskStatus.Abandoned && (
-
- {task.status === WorkTaskStatus.Pending && (
- <>
-
-
- >
- )}
-
- {task.status === WorkTaskStatus.Active && (
- <>
-
-
-
- >
- )}
-
- {task.status === WorkTaskStatus.Paused && (
- <>
-
-
-
- >
- )}
-
- )}
- >
- )}
-
- >
- )
-}
diff --git a/TaskTracker.Web/src/components/analytics/ActivityFeed.tsx b/TaskTracker.Web/src/components/analytics/ActivityFeed.tsx
deleted file mode 100644
index 751a2a1..0000000
--- a/TaskTracker.Web/src/components/analytics/ActivityFeed.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import { useMemo, useState } from 'react'
-import { useRecentContext } from '../../api/context'
-import { useMappings } from '../../api/mappings'
-import { CATEGORY_COLORS } from '../../lib/constants'
-
-interface ActivityFeedProps {
- minutes: number
- taskId?: number
-}
-
-const PAGE_SIZE = 20
-
-function resolveCategory(
- appName: string,
- mappings: { pattern: string; matchType: string; category: string }[],
-): string {
- for (const m of mappings) {
- if (m.matchType === 'Exact' && m.pattern.toLowerCase() === appName.toLowerCase()) {
- return m.category
- }
- if (m.matchType === 'Contains' && appName.toLowerCase().includes(m.pattern.toLowerCase())) {
- return m.category
- }
- if (m.matchType === 'Regex') {
- try {
- if (new RegExp(m.pattern, 'i').test(appName)) return m.category
- } catch {
- // skip invalid regex
- }
- }
- }
- return 'Unknown'
-}
-
-function formatTimestamp(ts: string): string {
- const date = new Date(ts)
- const now = new Date()
- const diffMs = now.getTime() - date.getTime()
- const diffMin = Math.floor(diffMs / 60_000)
-
- if (diffMin < 1) return 'just now'
- if (diffMin < 60) return `${diffMin}m ago`
-
- // Show time for older events
- const h = date.getHours()
- const m = date.getMinutes()
- const ampm = h >= 12 ? 'pm' : 'am'
- const hour12 = h % 12 || 12
- const mins = m.toString().padStart(2, '0')
- return `${hour12}:${mins}${ampm}`
-}
-
-export default function ActivityFeed({ minutes, taskId }: ActivityFeedProps) {
- const { data: events, isLoading: eventsLoading } = useRecentContext(minutes)
- const { data: mappings, isLoading: mappingsLoading } = useMappings()
- const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
-
- const sortedEvents = useMemo(() => {
- if (!events) return []
-
- let filtered = events
- if (taskId) {
- filtered = events.filter((e) => e.workTaskId === taskId)
- }
-
- // Reverse chronological
- return [...filtered].sort(
- (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
- )
- }, [events, taskId])
-
- const visibleEvents = sortedEvents.slice(0, visibleCount)
- const hasMore = visibleCount < sortedEvents.length
-
- if (eventsLoading || mappingsLoading) {
- return (
-
- Loading activity...
-
- )
- }
-
- if (sortedEvents.length === 0) {
- return (
-
- No activity events for this time range.
-
- )
- }
-
- return (
-
-
- {visibleEvents.map((evt, idx) => {
- const category = mappings ? resolveCategory(evt.appName, mappings) : 'Unknown'
- const color = CATEGORY_COLORS[category] ?? CATEGORY_COLORS['Unknown']
- const detail = evt.url || evt.windowTitle || ''
- const isLast = idx === visibleEvents.length - 1
-
- return (
-
- {/* Timeline connector + dot */}
-
-
- {!isLast && (
-
- )}
-
-
- {/* Content */}
-
-
-
- {formatTimestamp(evt.timestamp)}
-
- {evt.appName}
-
- {detail && (
-
{detail}
- )}
-
-
- )
- })}
-
-
- {hasMore && (
-
- )}
-
- )
-}
diff --git a/TaskTracker.Web/src/components/analytics/CategoryBreakdown.tsx b/TaskTracker.Web/src/components/analytics/CategoryBreakdown.tsx
deleted file mode 100644
index 9c40f22..0000000
--- a/TaskTracker.Web/src/components/analytics/CategoryBreakdown.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { useMemo } from 'react'
-import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'
-import { useContextSummary } from '../../api/context'
-import { CATEGORY_COLORS } from '../../lib/constants'
-
-interface CategoryBreakdownProps {
- minutes: number
- taskId?: number
-}
-
-interface CategoryData {
- name: string
- count: number
- color: string
- percentage: number
-}
-
-export default function CategoryBreakdown({ minutes: _minutes, taskId: _taskId }: CategoryBreakdownProps) {
- const { data: summary, isLoading } = useContextSummary()
-
- const categories = useMemo(() => {
- if (!summary) return []
-
- // Aggregate by category
- const catMap = new Map
()
- for (const item of summary) {
- const cat = item.category || 'Unknown'
- catMap.set(cat, (catMap.get(cat) ?? 0) + item.eventCount)
- }
-
- const total = Array.from(catMap.values()).reduce((s, c) => s + c, 0)
-
- const result: CategoryData[] = Array.from(catMap.entries())
- .map(([name, count]) => ({
- name,
- count,
- color: CATEGORY_COLORS[name] ?? CATEGORY_COLORS['Unknown'],
- percentage: total > 0 ? Math.round((count / total) * 100) : 0,
- }))
- .sort((a, b) => b.count - a.count)
-
- return result
- }, [summary])
-
- if (isLoading) {
- return (
-
- Loading category breakdown...
-
- )
- }
-
- if (categories.length === 0) {
- return (
-
- No category data available.
-
- )
- }
-
- const totalEvents = categories.reduce((s, c) => s + c.count, 0)
-
- return (
-
- {/* Left: Donut chart */}
-
-
-
-
- {categories.map((entry, index) => (
- |
- ))}
-
- {
- if (!active || !payload || payload.length === 0) return null
- const d = payload[0].payload as CategoryData
- return (
-
-
{d.name}
-
- {d.count} events ({d.percentage}%)
-
-
- )
- }}
- />
-
-
-
-
- {/* Right: Legend list */}
-
- {categories.map((cat) => (
-
- {/* Colored dot */}
-
-
- {/* Name + bar + stats */}
-
-
- {cat.name}
-
- {cat.count} ({cat.percentage}%)
-
-
- {/* Progress bar */}
-
-
0 ? (cat.count / totalEvents) * 100 : 0}%`,
- backgroundColor: cat.color,
- }}
- />
-
-
-
- ))}
-
-
- )
-}
diff --git a/TaskTracker.Web/src/components/analytics/Timeline.tsx b/TaskTracker.Web/src/components/analytics/Timeline.tsx
deleted file mode 100644
index db4903d..0000000
--- a/TaskTracker.Web/src/components/analytics/Timeline.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-import { useMemo } from 'react'
-import {
- BarChart,
- Bar,
- XAxis,
- YAxis,
- Tooltip,
- ResponsiveContainer,
- Cell,
-} from 'recharts'
-import { useRecentContext } from '../../api/context'
-import { useMappings } from '../../api/mappings'
-import { CATEGORY_COLORS } from '../../lib/constants'
-
-interface TimelineProps {
- minutes: number
- taskId?: number
-}
-
-interface BucketData {
- label: string
- count: number
- category: string
- color: string
- appName: string
- timeRange: string
-}
-
-function resolveCategory(
- appName: string,
- mappings: { pattern: string; matchType: string; category: string }[],
-): string {
- for (const m of mappings) {
- if (m.matchType === 'Exact' && m.pattern.toLowerCase() === appName.toLowerCase()) {
- return m.category
- }
- if (m.matchType === 'Contains' && appName.toLowerCase().includes(m.pattern.toLowerCase())) {
- return m.category
- }
- if (m.matchType === 'Regex') {
- try {
- if (new RegExp(m.pattern, 'i').test(appName)) return m.category
- } catch {
- // skip invalid regex
- }
- }
- }
- return 'Unknown'
-}
-
-function formatHour(date: Date): string {
- const h = date.getHours()
- const ampm = h >= 12 ? 'pm' : 'am'
- const hour12 = h % 12 || 12
- return `${hour12}${ampm}`
-}
-
-function formatTimeRange(date: Date): string {
- const start = formatHour(date)
- const next = new Date(date)
- next.setHours(next.getHours() + 1)
- const end = formatHour(next)
- return `${start} - ${end}`
-}
-
-export default function Timeline({ minutes, taskId }: TimelineProps) {
- const { data: events, isLoading: eventsLoading } = useRecentContext(minutes)
- const { data: mappings, isLoading: mappingsLoading } = useMappings()
-
- const buckets = useMemo(() => {
- if (!events || !mappings) return []
-
- let filtered = events
- if (taskId) {
- filtered = events.filter((e) => e.workTaskId === taskId)
- }
-
- if (filtered.length === 0) return []
-
- // Group by hour bucket
- const hourMap = new Map<
- string,
- { date: Date; apps: Map
}
- >()
-
- for (const evt of filtered) {
- const ts = new Date(evt.timestamp)
- const bucketDate = new Date(ts)
- bucketDate.setMinutes(0, 0, 0)
- const key = bucketDate.toISOString()
-
- if (!hourMap.has(key)) {
- hourMap.set(key, { date: bucketDate, apps: new Map() })
- }
-
- const bucket = hourMap.get(key)!
- const category = resolveCategory(evt.appName, mappings)
- const appKey = `${evt.appName}|${category}`
-
- if (!bucket.apps.has(appKey)) {
- bucket.apps.set(appKey, { count: 0, category })
- }
- bucket.apps.get(appKey)!.count++
- }
-
- // Sort by time and determine dominant app per bucket
- const sorted = Array.from(hourMap.entries())
- .sort(([a], [b]) => a.localeCompare(b))
- .map(([, { date, apps }]): BucketData => {
- let dominantApp = ''
- let dominantCategory = 'Unknown'
- let maxCount = 0
-
- for (const [key, { count, category }] of apps) {
- if (count > maxCount) {
- maxCount = count
- dominantCategory = category
- dominantApp = key.split('|')[0]
- }
- }
-
- const totalCount = Array.from(apps.values()).reduce((s, a) => s + a.count, 0)
-
- return {
- label: formatHour(date),
- count: totalCount,
- category: dominantCategory,
- color: CATEGORY_COLORS[dominantCategory] ?? CATEGORY_COLORS['Unknown'],
- appName: dominantApp,
- timeRange: formatTimeRange(date),
- }
- })
-
- return sorted
- }, [events, mappings, taskId])
-
- if (eventsLoading || mappingsLoading) {
- return (
-
- Loading timeline...
-
- )
- }
-
- if (buckets.length === 0) {
- return (
-
- No activity data for this time range.
-
- )
- }
-
- return (
-
-
-
-
-
- {
- if (!active || !payload || payload.length === 0) return null
- const d = payload[0].payload as BucketData
- return (
-
-
{d.timeRange}
-
{d.appName}
-
- {d.count} events
-
-
- )
- }}
- />
-
- {buckets.map((entry, index) => (
- |
- ))}
-
-
-
-
- )
-}
diff --git a/TaskTracker.Web/src/index.css b/TaskTracker.Web/src/index.css
deleted file mode 100644
index a9661ed..0000000
--- a/TaskTracker.Web/src/index.css
+++ /dev/null
@@ -1,100 +0,0 @@
-@import "tailwindcss";
-
-@theme {
- --font-sans: 'Inter', system-ui, sans-serif;
- --color-page: #0a0a0f;
- --color-surface: #12131a;
- --color-elevated: #1a1b26;
- --color-border: rgba(255, 255, 255, 0.06);
- --color-border-hover: rgba(255, 255, 255, 0.12);
- --color-text-primary: #e2e8f0;
- --color-text-secondary: #64748b;
- --color-text-tertiary: #334155;
- --color-accent: #8b5cf6;
- --color-accent-end: #6366f1;
- --color-status-active: #3b82f6;
- --color-status-paused: #eab308;
- --color-status-completed: #22c55e;
- --color-status-pending: #64748b;
-}
-
-/* Noise grain texture */
-body::before {
- content: '';
- position: fixed;
- inset: 0;
- z-index: 9999;
- pointer-events: none;
- opacity: 0.03;
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
- background-repeat: repeat;
- background-size: 256px 256px;
-}
-
-/* Active task pulse */
-@keyframes pulse-glow {
- 0%, 100% {
- box-shadow: 0 0 6px rgba(59, 130, 246, 0.3);
- }
- 50% {
- box-shadow: 0 0 16px rgba(59, 130, 246, 0.5);
- }
-}
-
-.animate-pulse-glow {
- animation: pulse-glow 2.5s ease-in-out infinite;
-}
-
-/* Live dot pulse */
-@keyframes live-dot {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.4; }
-}
-
-.animate-live-dot {
- animation: live-dot 1.5s ease-in-out infinite;
-}
-
-/* Card hover glow border */
-.card-glow {
- position: relative;
-}
-
-.card-glow::before {
- content: '';
- position: absolute;
- inset: -1px;
- border-radius: inherit;
- background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(99, 102, 241, 0.1), transparent);
- opacity: 0;
- transition: opacity 0.2s ease;
- z-index: -1;
- pointer-events: none;
-}
-
-.card-glow:hover::before {
- opacity: 1;
-}
-
-/* Custom scrollbar */
-::-webkit-scrollbar {
- width: 6px;
-}
-
-::-webkit-scrollbar-track {
- background: transparent;
-}
-
-::-webkit-scrollbar-thumb {
- background: #1a1b26;
- border-radius: 3px;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: #2a2d37;
-}
-
-/* Selection color */
-::selection {
- background: rgba(139, 92, 246, 0.3);
-}
diff --git a/TaskTracker.Web/src/lib/constants.ts b/TaskTracker.Web/src/lib/constants.ts
deleted file mode 100644
index f2f1e57..0000000
--- a/TaskTracker.Web/src/lib/constants.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-export const COLUMN_CONFIG = [
- { status: 'Pending' as const, label: 'Pending', color: '#64748b' },
- { status: 'Active' as const, label: 'Active', color: '#3b82f6' },
- { status: 'Paused' as const, label: 'Paused', color: '#eab308' },
- { status: 'Completed' as const, label: 'Completed', color: '#22c55e' },
-] as const
-
-export const CATEGORY_COLORS: Record = {
- Development: '#6366f1',
- Research: '#06b6d4',
- Communication: '#8b5cf6',
- DevOps: '#f97316',
- Documentation: '#14b8a6',
- Design: '#ec4899',
- Testing: '#3b82f6',
- General: '#64748b',
- Email: '#f59e0b',
- Engineering: '#6366f1',
- LaserCutting: '#ef4444',
- Unknown: '#475569',
-}
diff --git a/TaskTracker.Web/src/main.tsx b/TaskTracker.Web/src/main.tsx
deleted file mode 100644
index de4801c..0000000
--- a/TaskTracker.Web/src/main.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import './index.css'
-import App from './App'
-
-const queryClient = new QueryClient({
- defaultOptions: { queries: { staleTime: 10_000, retry: 1 } },
-})
-
-createRoot(document.getElementById('root')!).render(
-
-
-
-
- ,
-)
diff --git a/TaskTracker.Web/src/pages/Analytics.tsx b/TaskTracker.Web/src/pages/Analytics.tsx
deleted file mode 100644
index 2458dc8..0000000
--- a/TaskTracker.Web/src/pages/Analytics.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-import { useState } from 'react'
-import { useTasks } from '../api/tasks'
-import Timeline from '../components/analytics/Timeline'
-import CategoryBreakdown from '../components/analytics/CategoryBreakdown'
-import ActivityFeed from '../components/analytics/ActivityFeed'
-
-const TIME_RANGES = [
- { label: 'Today', minutes: 1440 },
- { label: '7 days', minutes: 10080 },
- { label: '30 days', minutes: 43200 },
-] as const
-
-export default function Analytics() {
- const [minutes, setMinutes] = useState(1440)
- const [taskId, setTaskId] = useState(undefined)
- const { data: tasks } = useTasks()
-
- return (
-
- {/* Header + Filters */}
-
-
Analytics
-
-
- {/* Time range dropdown */}
-
-
- {/* Task filter dropdown */}
-
-
-
-
- {/* Stat cards */}
-
-
-
Open Tasks
-
- {tasks?.filter(t => t.status !== 'Completed' && t.status !== 'Abandoned').length ?? 0}
-
-
-
-
Active Time
-
- {(() => {
- const totalMins = tasks?.reduce((acc, t) => {
- if (!t.startedAt) return acc
- const start = new Date(t.startedAt).getTime()
- const end = t.completedAt ? new Date(t.completedAt).getTime() : (t.status === 'Active' ? Date.now() : start)
- return acc + (end - start) / 60000
- }, 0) ?? 0
- const hours = Math.floor(totalMins / 60)
- const mins = Math.floor(totalMins % 60)
- return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`
- })()}
-
-
-
-
Top Category
-
- {(() => {
- const counts: Record = {}
- tasks?.forEach(t => { counts[t.category ?? 'Unknown'] = (counts[t.category ?? 'Unknown'] || 0) + 1 })
- const top = Object.entries(counts).sort(([,a], [,b]) => b - a)[0]
- return top ? top[0] : '\u2014'
- })()}
-
-
-
-
- {/* Timeline */}
-
-
- Activity Timeline
-
-
-
-
-
-
- {/* Category Breakdown */}
-
-
- Category Breakdown
-
-
-
-
-
-
- {/* Activity Feed */}
-
-
- Recent Activity
-
-
-
-
- )
-}
diff --git a/TaskTracker.Web/src/pages/Board.tsx b/TaskTracker.Web/src/pages/Board.tsx
deleted file mode 100644
index fc5d07e..0000000
--- a/TaskTracker.Web/src/pages/Board.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useState, useEffect, useMemo } from 'react'
-import { useSearchParams } from 'react-router-dom'
-import { useTasks } from '../api/tasks.ts'
-import KanbanBoard from '../components/KanbanBoard.tsx'
-import TaskDetailPanel from '../components/TaskDetailPanel.tsx'
-import FilterBar, { applyFilters, EMPTY_FILTERS } from '../components/FilterBar.tsx'
-import type { Filters } from '../components/FilterBar.tsx'
-
-export default function Board() {
- const [selectedTaskId, setSelectedTaskId] = useState(null)
- const [searchParams, setSearchParams] = useSearchParams()
- const [filters, setFilters] = useState(EMPTY_FILTERS)
- const { data: tasks, isLoading } = useTasks()
-
- // Read ?task= search param on mount or when it changes
- useEffect(() => {
- const taskParam = searchParams.get('task')
- if (taskParam) {
- const id = Number(taskParam)
- if (!isNaN(id)) {
- setSelectedTaskId(id)
- }
- // Clean up the search param
- setSearchParams({}, { replace: true })
- }
- }, [searchParams, setSearchParams])
-
- // Apply filters to tasks
- const filteredTasks = useMemo(() => {
- if (!tasks) return []
- return applyFilters(tasks, filters)
- }, [tasks, filters])
-
- return (
-
-
-
- setSelectedTaskId(id)}
- />
-
- {selectedTaskId !== null && (
-
setSelectedTaskId(null)} />
- )}
-
- )
-}
diff --git a/TaskTracker.Web/src/pages/Mappings.tsx b/TaskTracker.Web/src/pages/Mappings.tsx
deleted file mode 100644
index 687b7f1..0000000
--- a/TaskTracker.Web/src/pages/Mappings.tsx
+++ /dev/null
@@ -1,289 +0,0 @@
-import { useState } from 'react'
-import { Pencil, Trash2, Check, X, Plus, Link } from 'lucide-react'
-import { useMappings, useCreateMapping, useUpdateMapping, useDeleteMapping } from '../api/mappings'
-import { CATEGORY_COLORS } from '../lib/constants'
-import type { AppMapping } from '../types'
-
-const MATCH_TYPES = ['ProcessName', 'TitleContains', 'UrlContains'] as const
-
-const MATCH_TYPE_COLORS: Record = {
- ProcessName: '#6366f1',
- TitleContains: '#06b6d4',
- UrlContains: '#f97316',
-}
-
-interface FormData {
- pattern: string
- matchType: string
- category: string
- friendlyName: string
-}
-
-const emptyForm: FormData = { pattern: '', matchType: 'ProcessName', category: '', friendlyName: '' }
-
-function formFromMapping(m: AppMapping): FormData {
- return {
- pattern: m.pattern,
- matchType: m.matchType,
- category: m.category,
- friendlyName: m.friendlyName ?? '',
- }
-}
-
-export default function Mappings() {
- const { data: mappings, isLoading } = useMappings()
- const createMapping = useCreateMapping()
- const updateMapping = useUpdateMapping()
- const deleteMapping = useDeleteMapping()
-
- const [addingNew, setAddingNew] = useState(false)
- const [newForm, setNewForm] = useState(emptyForm)
- const [editingId, setEditingId] = useState(null)
- const [editForm, setEditForm] = useState(emptyForm)
-
- function handleAddSave() {
- if (!newForm.pattern.trim() || !newForm.category.trim()) return
- createMapping.mutate(
- {
- pattern: newForm.pattern.trim(),
- matchType: newForm.matchType,
- category: newForm.category.trim(),
- friendlyName: newForm.friendlyName.trim() || undefined,
- },
- {
- onSuccess: () => {
- setAddingNew(false)
- setNewForm(emptyForm)
- },
- },
- )
- }
-
- function handleAddCancel() {
- setAddingNew(false)
- setNewForm(emptyForm)
- }
-
- function handleEditStart(mapping: AppMapping) {
- setEditingId(mapping.id)
- setEditForm(formFromMapping(mapping))
- // Cancel any add-new row when starting an edit
- setAddingNew(false)
- }
-
- function handleEditSave() {
- if (editingId === null) return
- if (!editForm.pattern.trim() || !editForm.category.trim()) return
- updateMapping.mutate(
- {
- id: editingId,
- pattern: editForm.pattern.trim(),
- matchType: editForm.matchType,
- category: editForm.category.trim(),
- friendlyName: editForm.friendlyName.trim() || undefined,
- },
- {
- onSuccess: () => {
- setEditingId(null)
- setEditForm(emptyForm)
- },
- },
- )
- }
-
- function handleEditCancel() {
- setEditingId(null)
- setEditForm(emptyForm)
- }
-
- function handleDelete(id: number) {
- if (!window.confirm('Delete this mapping rule?')) return
- deleteMapping.mutate(id)
- }
-
- const inputClass =
- 'bg-[var(--color-page)] text-[var(--color-text-primary)] text-sm rounded border border-[var(--color-border)] px-2 py-1.5 focus:outline-none focus:border-[var(--color-accent)] transition-colors w-full'
- const selectClass =
- 'bg-[var(--color-page)] text-[var(--color-text-primary)] text-sm rounded border border-[var(--color-border)] px-2 py-1.5 focus:outline-none focus:border-[var(--color-accent)] transition-colors appearance-none cursor-pointer w-full'
-
- function renderFormRow(form: FormData, setForm: (f: FormData) => void, onSave: () => void, onCancel: () => void, isSaving: boolean) {
- return (
-
- |
- setForm({ ...form, pattern: e.target.value })}
- className={inputClass}
- autoFocus
- />
- |
-
-
- |
-
- setForm({ ...form, category: e.target.value })}
- className={inputClass}
- />
- |
-
- setForm({ ...form, friendlyName: e.target.value })}
- className={inputClass}
- />
- |
-
-
-
-
-
- |
-
- )
- }
-
- return (
-
- {/* Header */}
-
-
App Mappings
-
-
-
- {/* Table */}
- {isLoading ? (
-
Loading mappings...
- ) : !mappings?.length && !addingNew ? (
-
-
-
No mappings configured
-
-
- ) : (
-
-
-
-
- | Pattern |
- Match Type |
- Category |
- Friendly Name |
- Actions |
-
-
-
- {/* Add-new row */}
- {addingNew &&
- renderFormRow(newForm, setNewForm, handleAddSave, handleAddCancel, createMapping.isPending)}
-
- {/* Data rows */}
- {mappings?.map((m) =>
- editingId === m.id ? (
- renderFormRow(editForm, setEditForm, handleEditSave, handleEditCancel, updateMapping.isPending)
- ) : (
-
- | {m.pattern} |
-
-
- {m.matchType}
-
- |
-
-
-
- {m.category}
-
- |
-
- {m.friendlyName ?? '\u2014'}
- |
-
-
-
-
-
- |
-
- ),
- )}
-
-
-
- )}
-
- )
-}
diff --git a/TaskTracker.Web/src/types/index.ts b/TaskTracker.Web/src/types/index.ts
deleted file mode 100644
index 23985c8..0000000
--- a/TaskTracker.Web/src/types/index.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-export const WorkTaskStatus = {
- Pending: 'Pending',
- Active: 'Active',
- Paused: 'Paused',
- Completed: 'Completed',
- Abandoned: 'Abandoned',
-} as const
-export type WorkTaskStatus = (typeof WorkTaskStatus)[keyof typeof WorkTaskStatus]
-
-export const NoteType = {
- PauseNote: 'PauseNote',
- ResumeNote: 'ResumeNote',
- General: 'General',
-} as const
-export type NoteType = (typeof NoteType)[keyof typeof NoteType]
-
-export interface WorkTask {
- id: number
- title: string
- description: string | null
- status: WorkTaskStatus
- category: string | null
- createdAt: string
- startedAt: string | null
- completedAt: string | null
- estimatedMinutes: number | null
- parentTaskId: number | null
- subTasks: WorkTask[]
- notes: TaskNote[]
- contextEvents: ContextEvent[]
-}
-
-export interface TaskNote {
- id: number
- workTaskId: number
- content: string
- type: NoteType
- createdAt: string
-}
-
-export interface ContextEvent {
- id: number
- workTaskId: number | null
- source: string
- appName: string
- windowTitle: string
- url: string | null
- timestamp: string
-}
-
-export interface AppMapping {
- id: number
- pattern: string
- matchType: string
- category: string
- friendlyName: string | null
-}
-
-export interface ContextSummaryItem {
- appName: string
- category: string
- eventCount: number
- firstSeen: string
- lastSeen: string
-}
-
-export interface ApiResponse {
- success: boolean
- data: T
- error: string | null
-}
diff --git a/TaskTracker.Web/tsconfig.app.json b/TaskTracker.Web/tsconfig.app.json
deleted file mode 100644
index a9b5a59..0000000
--- a/TaskTracker.Web/tsconfig.app.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2022",
- "useDefineForClassFields": true,
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "types": ["vite/client"],
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "verbatimModuleSyntax": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "erasableSyntaxOnly": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["src"]
-}
diff --git a/TaskTracker.Web/tsconfig.json b/TaskTracker.Web/tsconfig.json
deleted file mode 100644
index 1ffef60..0000000
--- a/TaskTracker.Web/tsconfig.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "files": [],
- "references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ]
-}
diff --git a/TaskTracker.Web/tsconfig.node.json b/TaskTracker.Web/tsconfig.node.json
deleted file mode 100644
index 8a67f62..0000000
--- a/TaskTracker.Web/tsconfig.node.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2023",
- "lib": ["ES2023"],
- "module": "ESNext",
- "types": ["node"],
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "verbatimModuleSyntax": true,
- "moduleDetection": "force",
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "erasableSyntaxOnly": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/TaskTracker.Web/vite.config.ts b/TaskTracker.Web/vite.config.ts
deleted file mode 100644
index 20281aa..0000000
--- a/TaskTracker.Web/vite.config.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-import tailwindcss from '@tailwindcss/vite'
-
-export default defineConfig({
- plugins: [react(), tailwindcss()],
- server: {
- port: 5173,
- proxy: {
- '/api': {
- target: 'http://localhost:5200',
- changeOrigin: true,
- },
- },
- },
-})