chore: initial commit of TaskTracker project

Existing ASP.NET API with vanilla JS SPA, WindowWatcher, Chrome extension, and MCP server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 22:08:45 -05:00
commit e12f78c479
66 changed files with 5170 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore;
using TaskTracker.Core.Entities;
namespace TaskTracker.Infrastructure.Data;
public class TaskTrackerDbContext(DbContextOptions<TaskTrackerDbContext> options) : DbContext(options)
{
public DbSet<WorkTask> Tasks => Set<WorkTask>();
public DbSet<TaskNote> Notes => Set<TaskNote>();
public DbSet<ContextEvent> ContextEvents => Set<ContextEvent>();
public DbSet<AppMapping> AppMappings => Set<AppMapping>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<WorkTask>(e =>
{
e.HasKey(t => t.Id);
e.Property(t => t.Title).HasMaxLength(500).IsRequired();
e.Property(t => t.Status).HasConversion<string>().HasMaxLength(50);
e.Property(t => t.Category).HasMaxLength(100);
e.HasMany(t => t.SubTasks).WithOne(t => t.ParentTask).HasForeignKey(t => t.ParentTaskId).OnDelete(DeleteBehavior.Restrict);
e.HasIndex(t => t.ParentTaskId);
e.HasMany(t => t.Notes).WithOne(n => n.WorkTask).HasForeignKey(n => n.WorkTaskId);
e.HasMany(t => t.ContextEvents).WithOne(c => c.WorkTask).HasForeignKey(c => c.WorkTaskId);
});
modelBuilder.Entity<TaskNote>(e =>
{
e.HasKey(n => n.Id);
e.Property(n => n.Content).IsRequired();
e.Property(n => n.Type).HasConversion<string>().HasMaxLength(50);
});
modelBuilder.Entity<ContextEvent>(e =>
{
e.HasKey(c => c.Id);
e.Property(c => c.Source).HasMaxLength(100).IsRequired();
e.Property(c => c.AppName).HasMaxLength(200).IsRequired();
e.Property(c => c.WindowTitle).HasMaxLength(1000).IsRequired();
e.Property(c => c.Url).HasMaxLength(2000);
e.HasIndex(c => c.Timestamp);
});
modelBuilder.Entity<AppMapping>(e =>
{
e.HasKey(m => m.Id);
e.Property(m => m.Pattern).HasMaxLength(500).IsRequired();
e.Property(m => m.MatchType).HasMaxLength(50).IsRequired();
e.Property(m => m.Category).HasMaxLength(100).IsRequired();
e.Property(m => m.FriendlyName).HasMaxLength(200);
});
// Seed default app mappings
modelBuilder.Entity<AppMapping>().HasData(
new AppMapping { Id = 1, Pattern = "SLDWORKS", MatchType = "ProcessName", Category = "Engineering", FriendlyName = "SolidWorks" },
new AppMapping { Id = 2, Pattern = "OUTLOOK", MatchType = "ProcessName", Category = "Email", FriendlyName = "Outlook" },
new AppMapping { Id = 3, Pattern = "notepad", MatchType = "ProcessName", Category = "General", FriendlyName = "Notepad" },
new AppMapping { Id = 4, Pattern = "pep", MatchType = "UrlContains", Category = "LaserCutting", FriendlyName = "PEP System" },
new AppMapping { Id = 5, Pattern = "solidworks", MatchType = "TitleContains", Category = "Engineering", FriendlyName = "SolidWorks" }
);
}
}

View File

@@ -0,0 +1,242 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using TaskTracker.Infrastructure.Data;
#nullable disable
namespace TaskTracker.Infrastructure.Migrations
{
[DbContext(typeof(TaskTrackerDbContext))]
[Migration("20260226032729_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("TaskTracker.Core.Entities.AppMapping", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("FriendlyName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("MatchType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Pattern")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("AppMappings");
b.HasData(
new
{
Id = 1,
Category = "Engineering",
FriendlyName = "SolidWorks",
MatchType = "ProcessName",
Pattern = "SLDWORKS"
},
new
{
Id = 2,
Category = "Email",
FriendlyName = "Outlook",
MatchType = "ProcessName",
Pattern = "OUTLOOK"
},
new
{
Id = 3,
Category = "General",
FriendlyName = "Notepad",
MatchType = "ProcessName",
Pattern = "notepad"
},
new
{
Id = 4,
Category = "LaserCutting",
FriendlyName = "PEP System",
MatchType = "UrlContains",
Pattern = "pep"
},
new
{
Id = 5,
Category = "Engineering",
FriendlyName = "SolidWorks",
MatchType = "TitleContains",
Pattern = "solidworks"
});
});
modelBuilder.Entity("TaskTracker.Core.Entities.ContextEvent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.Property<string>("Url")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("WindowTitle")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<int?>("WorkTaskId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Timestamp");
b.HasIndex("WorkTaskId");
b.ToTable("ContextEvents");
});
modelBuilder.Entity("TaskTracker.Core.Entities.TaskNote", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int>("WorkTaskId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("WorkTaskId");
b.ToTable("Notes");
});
modelBuilder.Entity("TaskTracker.Core.Entities.WorkTask", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("StartedAt")
.HasColumnType("datetime2");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("Tasks");
});
modelBuilder.Entity("TaskTracker.Core.Entities.ContextEvent", b =>
{
b.HasOne("TaskTracker.Core.Entities.WorkTask", "WorkTask")
.WithMany("ContextEvents")
.HasForeignKey("WorkTaskId");
b.Navigation("WorkTask");
});
modelBuilder.Entity("TaskTracker.Core.Entities.TaskNote", b =>
{
b.HasOne("TaskTracker.Core.Entities.WorkTask", "WorkTask")
.WithMany("Notes")
.HasForeignKey("WorkTaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("WorkTask");
});
modelBuilder.Entity("TaskTracker.Core.Entities.WorkTask", b =>
{
b.Navigation("ContextEvents");
b.Navigation("Notes");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,140 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace TaskTracker.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AppMappings",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Pattern = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
MatchType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Category = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
FriendlyName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AppMappings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Tasks",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Category = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
StartedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Tasks", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ContextEvents",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
WorkTaskId = table.Column<int>(type: "int", nullable: true),
Source = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
AppName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
WindowTitle = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
Url = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
Timestamp = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ContextEvents", x => x.Id);
table.ForeignKey(
name: "FK_ContextEvents_Tasks_WorkTaskId",
column: x => x.WorkTaskId,
principalTable: "Tasks",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Notes",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
WorkTaskId = table.Column<int>(type: "int", nullable: false),
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
Type = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Notes", x => x.Id);
table.ForeignKey(
name: "FK_Notes_Tasks_WorkTaskId",
column: x => x.WorkTaskId,
principalTable: "Tasks",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
table: "AppMappings",
columns: new[] { "Id", "Category", "FriendlyName", "MatchType", "Pattern" },
values: new object[,]
{
{ 1, "Engineering", "SolidWorks", "ProcessName", "SLDWORKS" },
{ 2, "Email", "Outlook", "ProcessName", "OUTLOOK" },
{ 3, "General", "Notepad", "ProcessName", "notepad" },
{ 4, "LaserCutting", "PEP System", "UrlContains", "pep" },
{ 5, "Engineering", "SolidWorks", "TitleContains", "solidworks" }
});
migrationBuilder.CreateIndex(
name: "IX_ContextEvents_Timestamp",
table: "ContextEvents",
column: "Timestamp");
migrationBuilder.CreateIndex(
name: "IX_ContextEvents_WorkTaskId",
table: "ContextEvents",
column: "WorkTaskId");
migrationBuilder.CreateIndex(
name: "IX_Notes_WorkTaskId",
table: "Notes",
column: "WorkTaskId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AppMappings");
migrationBuilder.DropTable(
name: "ContextEvents");
migrationBuilder.DropTable(
name: "Notes");
migrationBuilder.DropTable(
name: "Tasks");
}
}
}

View File

@@ -0,0 +1,259 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using TaskTracker.Infrastructure.Data;
#nullable disable
namespace TaskTracker.Infrastructure.Migrations
{
[DbContext(typeof(TaskTrackerDbContext))]
[Migration("20260227013459_AddSubTasks")]
partial class AddSubTasks
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("TaskTracker.Core.Entities.AppMapping", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("FriendlyName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("MatchType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Pattern")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("AppMappings");
b.HasData(
new
{
Id = 1,
Category = "Engineering",
FriendlyName = "SolidWorks",
MatchType = "ProcessName",
Pattern = "SLDWORKS"
},
new
{
Id = 2,
Category = "Email",
FriendlyName = "Outlook",
MatchType = "ProcessName",
Pattern = "OUTLOOK"
},
new
{
Id = 3,
Category = "General",
FriendlyName = "Notepad",
MatchType = "ProcessName",
Pattern = "notepad"
},
new
{
Id = 4,
Category = "LaserCutting",
FriendlyName = "PEP System",
MatchType = "UrlContains",
Pattern = "pep"
},
new
{
Id = 5,
Category = "Engineering",
FriendlyName = "SolidWorks",
MatchType = "TitleContains",
Pattern = "solidworks"
});
});
modelBuilder.Entity("TaskTracker.Core.Entities.ContextEvent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.Property<string>("Url")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("WindowTitle")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<int?>("WorkTaskId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Timestamp");
b.HasIndex("WorkTaskId");
b.ToTable("ContextEvents");
});
modelBuilder.Entity("TaskTracker.Core.Entities.TaskNote", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int>("WorkTaskId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("WorkTaskId");
b.ToTable("Notes");
});
modelBuilder.Entity("TaskTracker.Core.Entities.WorkTask", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<int?>("ParentTaskId")
.HasColumnType("int");
b.Property<DateTime?>("StartedAt")
.HasColumnType("datetime2");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("ParentTaskId");
b.ToTable("Tasks");
});
modelBuilder.Entity("TaskTracker.Core.Entities.ContextEvent", b =>
{
b.HasOne("TaskTracker.Core.Entities.WorkTask", "WorkTask")
.WithMany("ContextEvents")
.HasForeignKey("WorkTaskId");
b.Navigation("WorkTask");
});
modelBuilder.Entity("TaskTracker.Core.Entities.TaskNote", b =>
{
b.HasOne("TaskTracker.Core.Entities.WorkTask", "WorkTask")
.WithMany("Notes")
.HasForeignKey("WorkTaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("WorkTask");
});
modelBuilder.Entity("TaskTracker.Core.Entities.WorkTask", b =>
{
b.HasOne("TaskTracker.Core.Entities.WorkTask", "ParentTask")
.WithMany("SubTasks")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("ParentTask");
});
modelBuilder.Entity("TaskTracker.Core.Entities.WorkTask", b =>
{
b.Navigation("ContextEvents");
b.Navigation("Notes");
b.Navigation("SubTasks");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TaskTracker.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddSubTasks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ParentTaskId",
table: "Tasks",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Tasks_ParentTaskId",
table: "Tasks",
column: "ParentTaskId");
migrationBuilder.AddForeignKey(
name: "FK_Tasks_Tasks_ParentTaskId",
table: "Tasks",
column: "ParentTaskId",
principalTable: "Tasks",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Tasks_Tasks_ParentTaskId",
table: "Tasks");
migrationBuilder.DropIndex(
name: "IX_Tasks_ParentTaskId",
table: "Tasks");
migrationBuilder.DropColumn(
name: "ParentTaskId",
table: "Tasks");
}
}
}

View File

@@ -0,0 +1,256 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using TaskTracker.Infrastructure.Data;
#nullable disable
namespace TaskTracker.Infrastructure.Migrations
{
[DbContext(typeof(TaskTrackerDbContext))]
partial class TaskTrackerDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("TaskTracker.Core.Entities.AppMapping", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("FriendlyName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("MatchType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Pattern")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("AppMappings");
b.HasData(
new
{
Id = 1,
Category = "Engineering",
FriendlyName = "SolidWorks",
MatchType = "ProcessName",
Pattern = "SLDWORKS"
},
new
{
Id = 2,
Category = "Email",
FriendlyName = "Outlook",
MatchType = "ProcessName",
Pattern = "OUTLOOK"
},
new
{
Id = 3,
Category = "General",
FriendlyName = "Notepad",
MatchType = "ProcessName",
Pattern = "notepad"
},
new
{
Id = 4,
Category = "LaserCutting",
FriendlyName = "PEP System",
MatchType = "UrlContains",
Pattern = "pep"
},
new
{
Id = 5,
Category = "Engineering",
FriendlyName = "SolidWorks",
MatchType = "TitleContains",
Pattern = "solidworks"
});
});
modelBuilder.Entity("TaskTracker.Core.Entities.ContextEvent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime2");
b.Property<string>("Url")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("WindowTitle")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<int?>("WorkTaskId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Timestamp");
b.HasIndex("WorkTaskId");
b.ToTable("ContextEvents");
});
modelBuilder.Entity("TaskTracker.Core.Entities.TaskNote", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int>("WorkTaskId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("WorkTaskId");
b.ToTable("Notes");
});
modelBuilder.Entity("TaskTracker.Core.Entities.WorkTask", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<int?>("ParentTaskId")
.HasColumnType("int");
b.Property<DateTime?>("StartedAt")
.HasColumnType("datetime2");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("ParentTaskId");
b.ToTable("Tasks");
});
modelBuilder.Entity("TaskTracker.Core.Entities.ContextEvent", b =>
{
b.HasOne("TaskTracker.Core.Entities.WorkTask", "WorkTask")
.WithMany("ContextEvents")
.HasForeignKey("WorkTaskId");
b.Navigation("WorkTask");
});
modelBuilder.Entity("TaskTracker.Core.Entities.TaskNote", b =>
{
b.HasOne("TaskTracker.Core.Entities.WorkTask", "WorkTask")
.WithMany("Notes")
.HasForeignKey("WorkTaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("WorkTask");
});
modelBuilder.Entity("TaskTracker.Core.Entities.WorkTask", b =>
{
b.HasOne("TaskTracker.Core.Entities.WorkTask", "ParentTask")
.WithMany("SubTasks")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("ParentTask");
});
modelBuilder.Entity("TaskTracker.Core.Entities.WorkTask", b =>
{
b.Navigation("ContextEvents");
b.Navigation("Notes");
b.Navigation("SubTasks");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore;
using TaskTracker.Core.Entities;
using TaskTracker.Core.Interfaces;
using TaskTracker.Infrastructure.Data;
namespace TaskTracker.Infrastructure.Repositories;
public class AppMappingRepository(TaskTrackerDbContext db) : IAppMappingRepository
{
public async Task<List<AppMapping>> GetAllAsync()
{
return await db.AppMappings.OrderBy(m => m.Category).ToListAsync();
}
public async Task<AppMapping?> GetByIdAsync(int id)
{
return await db.AppMappings.FindAsync(id);
}
public async Task<AppMapping> CreateAsync(AppMapping mapping)
{
db.AppMappings.Add(mapping);
await db.SaveChangesAsync();
return mapping;
}
public async Task UpdateAsync(AppMapping mapping)
{
db.AppMappings.Update(mapping);
await db.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var mapping = await db.AppMappings.FindAsync(id);
if (mapping is not null)
{
db.AppMappings.Remove(mapping);
await db.SaveChangesAsync();
}
}
public async Task<AppMapping?> FindMatchAsync(string appName, string windowTitle, string? url)
{
var mappings = await db.AppMappings.ToListAsync();
return mappings.FirstOrDefault(m => m.MatchType switch
{
"ProcessName" => appName.Contains(m.Pattern, StringComparison.OrdinalIgnoreCase),
"TitleContains" => windowTitle.Contains(m.Pattern, StringComparison.OrdinalIgnoreCase),
"UrlContains" => url?.Contains(m.Pattern, StringComparison.OrdinalIgnoreCase) == true,
_ => false
});
}
}

View File

@@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore;
using TaskTracker.Core.Entities;
using TaskTracker.Core.Interfaces;
using TaskTracker.Infrastructure.Data;
namespace TaskTracker.Infrastructure.Repositories;
public class ContextEventRepository(TaskTrackerDbContext db) : IContextEventRepository
{
public async Task<ContextEvent> CreateAsync(ContextEvent contextEvent)
{
contextEvent.Timestamp = DateTime.UtcNow;
db.ContextEvents.Add(contextEvent);
await db.SaveChangesAsync();
return contextEvent;
}
public async Task<List<ContextEvent>> GetRecentAsync(int minutes = 30)
{
var since = DateTime.UtcNow.AddMinutes(-minutes);
return await db.ContextEvents
.Where(c => c.Timestamp >= since)
.OrderByDescending(c => c.Timestamp)
.ToListAsync();
}
public async Task<List<ContextEvent>> GetByTaskIdAsync(int taskId)
{
return await db.ContextEvents
.Where(c => c.WorkTaskId == taskId)
.OrderByDescending(c => c.Timestamp)
.ToListAsync();
}
}

View File

@@ -0,0 +1,89 @@
using Microsoft.EntityFrameworkCore;
using TaskTracker.Core.Entities;
using TaskTracker.Core.Enums;
using TaskTracker.Core.Interfaces;
using TaskTracker.Infrastructure.Data;
namespace TaskTracker.Infrastructure.Repositories;
public class TaskRepository(TaskTrackerDbContext db) : ITaskRepository
{
public async Task<List<WorkTask>> GetAllAsync(WorkTaskStatus? status = null, int? parentId = null, bool includeSubTasks = false)
{
var query = db.Tasks.Include(t => t.Notes).Include(t => t.SubTasks).AsQueryable();
if (status.HasValue)
query = query.Where(t => t.Status == status.Value);
if (parentId.HasValue)
query = query.Where(t => t.ParentTaskId == parentId.Value);
else if (!includeSubTasks)
query = query.Where(t => t.ParentTaskId == null);
return await query.OrderByDescending(t => t.CreatedAt).ToListAsync();
}
public async Task<WorkTask?> GetByIdAsync(int id)
{
return await db.Tasks
.Include(t => t.Notes.OrderByDescending(n => n.CreatedAt))
.Include(t => t.ContextEvents.OrderByDescending(c => c.Timestamp).Take(20))
.Include(t => t.SubTasks)
.Include(t => t.ParentTask)
.FirstOrDefaultAsync(t => t.Id == id);
}
public async Task<WorkTask?> GetActiveTaskAsync()
{
return await db.Tasks
.Include(t => t.Notes.OrderByDescending(n => n.CreatedAt))
.Include(t => t.ContextEvents.OrderByDescending(c => c.Timestamp).Take(20))
.Include(t => t.SubTasks)
.Include(t => t.ParentTask)
.FirstOrDefaultAsync(t => t.Status == WorkTaskStatus.Active);
}
public async Task<List<WorkTask>> GetSubTasksAsync(int parentId)
{
return await db.Tasks
.Include(t => t.SubTasks)
.Where(t => t.ParentTaskId == parentId)
.OrderByDescending(t => t.CreatedAt)
.ToListAsync();
}
public async Task<WorkTask> CreateAsync(WorkTask task)
{
task.CreatedAt = DateTime.UtcNow;
db.Tasks.Add(task);
await db.SaveChangesAsync();
return task;
}
public async Task UpdateAsync(WorkTask task)
{
db.Tasks.Update(task);
await db.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var task = await db.Tasks.Include(t => t.SubTasks).FirstOrDefaultAsync(t => t.Id == id);
if (task is not null)
{
await AbandonDescendantsAsync(task);
task.Status = WorkTaskStatus.Abandoned;
await db.SaveChangesAsync();
}
}
private async System.Threading.Tasks.Task AbandonDescendantsAsync(WorkTask task)
{
var children = await db.Tasks.Include(t => t.SubTasks).Where(t => t.ParentTaskId == task.Id).ToListAsync();
foreach (var child in children)
{
await AbandonDescendantsAsync(child);
child.Status = WorkTaskStatus.Abandoned;
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\TaskTracker.Core\TaskTracker.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>