diff --git a/backend/src/Taskdeck.Api/Controllers/TodayController.cs b/backend/src/Taskdeck.Api/Controllers/TodayController.cs
index 4cde80f83..623682653 100644
--- a/backend/src/Taskdeck.Api/Controllers/TodayController.cs
+++ b/backend/src/Taskdeck.Api/Controllers/TodayController.cs
@@ -9,7 +9,7 @@
namespace Taskdeck.Api.Controllers;
///
-/// Today-view endpoints: streak data, cadence aggregation, and daily dossier data.
+/// Today-view endpoints: streak data, cadence aggregation, daily dossier data, tomorrow notes, and day-seal operations.
///
[ApiController]
[Authorize]
@@ -19,17 +19,20 @@ public class TodayController : AuthenticatedControllerBase
{
private readonly IStreakService _streakService;
private readonly ICadenceService _cadenceService;
+ private readonly IDailySealService _dailySealService;
private readonly ITomorrowNoteService _tomorrowNoteService;
public TodayController(
IStreakService streakService,
ICadenceService cadenceService,
+ IDailySealService dailySealService,
ITomorrowNoteService tomorrowNoteService,
IUserContext userContext)
: base(userContext)
{
_streakService = streakService;
_cadenceService = cadenceService;
+ _dailySealService = dailySealService;
_tomorrowNoteService = tomorrowNoteService;
}
@@ -106,6 +109,26 @@ public async Task GetCadence(
return Ok(response);
}
+ [HttpPost("seal")]
+ public async Task SealDay([FromBody] SealDayRequest request, CancellationToken cancellationToken)
+ {
+ if (!TryGetCurrentUserId(out var userId, out var errorResult))
+ return errorResult!;
+
+ var result = await _dailySealService.SealDayAsync(userId, request.Date, cancellationToken);
+ return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
+ }
+
+ [HttpGet("seal")]
+ public async Task GetSealStatus([FromQuery] DateOnly date, CancellationToken cancellationToken)
+ {
+ if (!TryGetCurrentUserId(out var userId, out var errorResult))
+ return errorResult!;
+
+ var result = await _dailySealService.GetSealStatusAsync(userId, date, cancellationToken);
+ return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
+ }
+
///
/// Gets the tomorrow note for the given date.
/// The note was written the previous day and is displayed on the specified date's morning open.
@@ -153,3 +176,5 @@ public async Task SaveTomorrowNote(
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}
}
+
+public sealed record SealDayRequest(DateOnly Date);
diff --git a/backend/src/Taskdeck.Api/Extensions/ApplicationServiceRegistration.cs b/backend/src/Taskdeck.Api/Extensions/ApplicationServiceRegistration.cs
index 805df4461..c62361aa7 100644
--- a/backend/src/Taskdeck.Api/Extensions/ApplicationServiceRegistration.cs
+++ b/backend/src/Taskdeck.Api/Extensions/ApplicationServiceRegistration.cs
@@ -124,6 +124,8 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
+
services.AddScoped(sp =>
new ToolExecutorRegistry(sp.GetServices()));
services.AddScoped();
diff --git a/backend/src/Taskdeck.Application/Interfaces/IDailySnapshotRepository.cs b/backend/src/Taskdeck.Application/Interfaces/IDailySnapshotRepository.cs
new file mode 100644
index 000000000..825458242
--- /dev/null
+++ b/backend/src/Taskdeck.Application/Interfaces/IDailySnapshotRepository.cs
@@ -0,0 +1,9 @@
+using Taskdeck.Domain.Entities;
+
+namespace Taskdeck.Application.Interfaces;
+
+public interface IDailySnapshotRepository : IRepository
+{
+ Task GetByUserAndDateAsync(Guid userId, DateOnly date, CancellationToken cancellationToken = default);
+ Task> GetSealedDaysAsync(Guid userId, DateOnly from, DateOnly to, CancellationToken cancellationToken = default);
+}
diff --git a/backend/src/Taskdeck.Application/Interfaces/IUnitOfWork.cs b/backend/src/Taskdeck.Application/Interfaces/IUnitOfWork.cs
index 1f1552840..433c86768 100644
--- a/backend/src/Taskdeck.Application/Interfaces/IUnitOfWork.cs
+++ b/backend/src/Taskdeck.Application/Interfaces/IUnitOfWork.cs
@@ -34,6 +34,7 @@ public interface IUnitOfWork
IConnectorEventRepository ConnectorEvents { get; }
IConnectorCredentialRepository ConnectorCredentials { get; }
IProposalRevisionRepository ProposalRevisions { get; }
+ IDailySnapshotRepository DailySnapshots { get; }
ITomorrowNoteRepository TomorrowNotes { get; }
Task SaveChangesAsync(CancellationToken cancellationToken = default);
diff --git a/backend/src/Taskdeck.Application/Services/DailySealService.cs b/backend/src/Taskdeck.Application/Services/DailySealService.cs
new file mode 100644
index 000000000..30e922334
--- /dev/null
+++ b/backend/src/Taskdeck.Application/Services/DailySealService.cs
@@ -0,0 +1,69 @@
+using Taskdeck.Application.Interfaces;
+using Taskdeck.Domain.Common;
+using Taskdeck.Domain.Entities;
+using Taskdeck.Domain.Exceptions;
+
+namespace Taskdeck.Application.Services;
+
+public class DailySealService : IDailySealService
+{
+ private readonly IUnitOfWork _unitOfWork;
+
+ public DailySealService(IUnitOfWork unitOfWork)
+ {
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task> SealDayAsync(Guid userId, DateOnly date, CancellationToken cancellationToken = default)
+ {
+ if (userId == Guid.Empty)
+ return Result.Failure(ErrorCodes.ValidationError, "UserId cannot be empty");
+
+ var now = DateTimeOffset.UtcNow;
+
+ if (date > DateOnly.FromDateTime(now.UtcDateTime))
+ return Result.Failure(ErrorCodes.ValidationError, "Cannot seal a future date");
+
+ var snapshot = await _unitOfWork.DailySnapshots.GetByUserAndDateAsync(userId, date, cancellationToken);
+
+ var wasAlreadySealed = false;
+
+ if (snapshot is null)
+ {
+ snapshot = new DailySnapshot(userId, date, now);
+ snapshot.Seal(now);
+ await _unitOfWork.DailySnapshots.AddAsync(snapshot, cancellationToken);
+ }
+ else
+ {
+ wasAlreadySealed = snapshot.IsSealed;
+ snapshot.Seal(now);
+ }
+
+ await _unitOfWork.SaveChangesAsync(cancellationToken);
+
+ // Re-fetch to ensure accurate response after potential concurrent seal resolution.
+ // If TryResolveDuplicateDailySnapshotConflicts detached our entity, the persisted
+ // snapshot will have a different Id — meaning another request won the race.
+ var persisted = await _unitOfWork.DailySnapshots.GetByUserAndDateAsync(userId, date, cancellationToken);
+ if (persisted != null && persisted.Id != snapshot.Id)
+ {
+ return Result.Success(new DailySealResponse(persisted.SealedAt!.Value, WasAlreadySealed: true));
+ }
+
+ return Result.Success(new DailySealResponse(snapshot.SealedAt!.Value, wasAlreadySealed));
+ }
+
+ public async Task> GetSealStatusAsync(Guid userId, DateOnly date, CancellationToken cancellationToken = default)
+ {
+ if (userId == Guid.Empty)
+ return Result.Failure(ErrorCodes.ValidationError, "UserId cannot be empty");
+
+ var snapshot = await _unitOfWork.DailySnapshots.GetByUserAndDateAsync(userId, date, cancellationToken);
+
+ if (snapshot is null)
+ return Result.Success(new DailySealStatusResponse(date, false, null));
+
+ return Result.Success(new DailySealStatusResponse(date, snapshot.IsSealed, snapshot.SealedAt));
+ }
+}
diff --git a/backend/src/Taskdeck.Application/Services/IDailySealService.cs b/backend/src/Taskdeck.Application/Services/IDailySealService.cs
new file mode 100644
index 000000000..3321b94f3
--- /dev/null
+++ b/backend/src/Taskdeck.Application/Services/IDailySealService.cs
@@ -0,0 +1,12 @@
+using Taskdeck.Domain.Common;
+
+namespace Taskdeck.Application.Services;
+
+public interface IDailySealService
+{
+ Task> SealDayAsync(Guid userId, DateOnly date, CancellationToken cancellationToken = default);
+ Task> GetSealStatusAsync(Guid userId, DateOnly date, CancellationToken cancellationToken = default);
+}
+
+public sealed record DailySealResponse(DateTimeOffset SealedAt, bool WasAlreadySealed);
+public sealed record DailySealStatusResponse(DateOnly Date, bool IsSealed, DateTimeOffset? SealedAt);
diff --git a/backend/src/Taskdeck.Domain/Entities/DailySnapshot.cs b/backend/src/Taskdeck.Domain/Entities/DailySnapshot.cs
new file mode 100644
index 000000000..d050985f6
--- /dev/null
+++ b/backend/src/Taskdeck.Domain/Entities/DailySnapshot.cs
@@ -0,0 +1,39 @@
+using Taskdeck.Domain.Common;
+using Taskdeck.Domain.Exceptions;
+
+namespace Taskdeck.Domain.Entities;
+
+public class DailySnapshot : Entity
+{
+ public Guid UserId { get; private set; }
+ public DateOnly Date { get; private set; }
+ public DateTimeOffset? SealedAt { get; private set; }
+
+ public bool IsSealed => SealedAt.HasValue;
+
+ private DailySnapshot() { } // EF Core
+
+ public DailySnapshot(Guid userId, DateOnly date, DateTimeOffset now)
+ {
+ if (userId == Guid.Empty)
+ throw new DomainException(ErrorCodes.ValidationError, "UserId cannot be empty");
+
+ if (date > DateOnly.FromDateTime(now.UtcDateTime))
+ throw new DomainException(ErrorCodes.ValidationError, "Date must not be in the future");
+
+ UserId = userId;
+ Date = date;
+ }
+
+ ///
+ /// Seals the day's snapshot. Idempotent: if already sealed, this is a no-op.
+ ///
+ public void Seal(DateTimeOffset now)
+ {
+ if (IsSealed)
+ return;
+
+ SealedAt = now;
+ Touch();
+ }
+}
diff --git a/backend/src/Taskdeck.Infrastructure/DependencyInjection.cs b/backend/src/Taskdeck.Infrastructure/DependencyInjection.cs
index a6e92d3f0..a0ca0e0de 100644
--- a/backend/src/Taskdeck.Infrastructure/DependencyInjection.cs
+++ b/backend/src/Taskdeck.Infrastructure/DependencyInjection.cs
@@ -75,6 +75,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
services.AddScoped(sp =>
sp.GetRequiredService());
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
// Vector index is local; hash-based in-memory embeddings are development/test
diff --git a/backend/src/Taskdeck.Infrastructure/Migrations/20260425203404_AddDailySnapshots.Designer.cs b/backend/src/Taskdeck.Infrastructure/Migrations/20260425203404_AddDailySnapshots.Designer.cs
new file mode 100644
index 000000000..6167c3d83
--- /dev/null
+++ b/backend/src/Taskdeck.Infrastructure/Migrations/20260425203404_AddDailySnapshots.Designer.cs
@@ -0,0 +1,2309 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Taskdeck.Infrastructure.Persistence;
+
+#nullable disable
+
+namespace Taskdeck.Infrastructure.Migrations
+{
+ [DbContext(typeof(TaskdeckDbContext))]
+ [Migration("20260425203404_AddDailySnapshots")]
+ partial class AddDailySnapshots
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.26");
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.AgentProfile", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("TEXT");
+
+ b.Property("IsEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("PolicyJson")
+ .IsRequired()
+ .HasMaxLength(8000)
+ .HasColumnType("TEXT");
+
+ b.Property("ScopeBoardId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScopeType")
+ .HasColumnType("INTEGER");
+
+ b.Property("TemplateKey")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TemplateKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AgentProfiles", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.AgentRun", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AgentProfileId")
+ .HasColumnType("TEXT");
+
+ b.Property("ApproxCostUsd")
+ .HasColumnType("decimal(10, 6)");
+
+ b.Property("BoardId")
+ .HasColumnType("TEXT");
+
+ b.Property("CompletedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("FailureReason")
+ .HasMaxLength(4000)
+ .HasColumnType("TEXT");
+
+ b.Property("Objective")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("TEXT");
+
+ b.Property("ProposalId")
+ .HasColumnType("TEXT");
+
+ b.Property("StartedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("StepsExecuted")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasMaxLength(4000)
+ .HasColumnType("TEXT");
+
+ b.Property("TokensUsed")
+ .HasColumnType("INTEGER");
+
+ b.Property("TriggerType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AgentProfileId");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AgentRuns", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.AgentRunEvent", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Payload")
+ .IsRequired()
+ .HasMaxLength(16000)
+ .HasColumnType("TEXT");
+
+ b.Property("RunId")
+ .HasColumnType("TEXT");
+
+ b.Property("SequenceNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("Timestamp")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RunId", "SequenceNumber")
+ .IsUnique();
+
+ b.ToTable("AgentRunEvents", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.ApiKey", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("TEXT");
+
+ b.Property("KeyHash")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("KeyPrefix_")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("TEXT")
+ .HasColumnName("KeyPrefix");
+
+ b.Property("LastUsedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("RevokedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("KeyHash")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ApiKeys", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.ArchiveItem", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("ArchivedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("ArchivedByUserId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("BoardId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EntityId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("Reason")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("RestoreStatus")
+ .HasColumnType("INTEGER");
+
+ b.Property("RestoredAt")
+ .HasColumnType("TEXT");
+
+ b.Property("RestoredByUserId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("SnapshotJson")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ArchivedAt");
+
+ b.HasIndex("ArchivedByUserId");
+
+ b.HasIndex("BoardId");
+
+ b.HasIndex("RestoreStatus");
+
+ b.HasIndex("EntityType", "EntityId");
+
+ b.ToTable("ArchiveItems", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.AuditLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Action")
+ .HasColumnType("INTEGER");
+
+ b.Property("Changes")
+ .HasMaxLength(4000)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EntityId")
+ .HasColumnType("TEXT");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Timestamp")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Timestamp");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("EntityId", "Timestamp")
+ .HasDatabaseName("IX_AuditLogs_EntityId_Timestamp");
+
+ b.HasIndex("EntityType", "EntityId");
+
+ b.HasIndex("UserId", "Timestamp")
+ .HasDatabaseName("IX_AuditLogs_UserId_Timestamp");
+
+ b.ToTable("AuditLogs", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.AutomationProposal", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AppliedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("BoardId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("CorrelationId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("DecidedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("DecidedByUserId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("DiffPreview")
+ .HasColumnType("TEXT");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("TEXT");
+
+ b.Property("FailureReason")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("RequestedByUserId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("RiskLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property("SourceReferenceId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("SourceType")
+ .HasColumnType("INTEGER");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("ValidationIssues")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BoardId");
+
+ b.HasIndex("CorrelationId");
+
+ b.HasIndex("ExpiresAt");
+
+ b.HasIndex("RequestedByUserId");
+
+ b.HasIndex("Status");
+
+ b.ToTable("AutomationProposals", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.AutomationProposalOperation", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("ActionType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("ExpectedVersion")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("IdempotencyKey")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Parameters")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ProposalId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Sequence")
+ .HasColumnType("INTEGER");
+
+ b.Property("TargetId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("TargetType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IdempotencyKey")
+ .IsUnique();
+
+ b.HasIndex("ProposalId");
+
+ b.HasIndex("ProposalId", "Sequence");
+
+ b.ToTable("AutomationProposalOperations", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.Board", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("IsArchived")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("Boards", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.BoardAccess", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("BoardId")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("GrantedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("GrantedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Role")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("BoardId", "UserId")
+ .IsUnique();
+
+ b.ToTable("BoardAccesses", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.Card", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("BlockReason")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("BoardId")
+ .HasColumnType("TEXT");
+
+ b.Property("ColumnId")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(4000)
+ .HasColumnType("TEXT");
+
+ b.Property("DueDate")
+ .HasColumnType("TEXT");
+
+ b.Property("IsBlocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Position")
+ .HasColumnType("INTEGER");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ColumnId");
+
+ b.HasIndex("BoardId", "ColumnId")
+ .HasDatabaseName("IX_Cards_BoardId_ColumnId");
+
+ b.ToTable("Cards", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.CardComment", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AuthorUserId")
+ .HasColumnType("TEXT");
+
+ b.Property("BoardId")
+ .HasColumnType("TEXT");
+
+ b.Property("CardId")
+ .HasColumnType("TEXT");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasMaxLength(4000)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("DeletedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EditedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("IsDeleted")
+ .HasColumnType("INTEGER");
+
+ b.Property("ParentCommentId")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorUserId");
+
+ b.HasIndex("BoardId");
+
+ b.HasIndex("CardId");
+
+ b.HasIndex("ParentCommentId");
+
+ b.HasIndex("CardId", "CreatedAt");
+
+ b.ToTable("CardComments", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.CardCommentMention", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("CardCommentId")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("MentionedUserId")
+ .HasColumnType("TEXT");
+
+ b.Property("MentionedUsername")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CardCommentId");
+
+ b.HasIndex("MentionedUserId");
+
+ b.HasIndex("CardCommentId", "MentionedUserId")
+ .IsUnique();
+
+ b.ToTable("CardCommentMentions", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.CardLabel", b =>
+ {
+ b.Property("CardId")
+ .HasColumnType("TEXT");
+
+ b.Property("LabelId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("CardId", "LabelId");
+
+ b.HasIndex("LabelId");
+
+ b.ToTable("CardLabels", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.ChatMessage", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("DegradedReason")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("MessageType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("ProposalId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Role")
+ .HasColumnType("INTEGER");
+
+ b.Property("SessionId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("TokenUsage")
+ .HasColumnType("INTEGER");
+
+ b.Property("ToolCallMetadataJson")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("ProposalId");
+
+ b.HasIndex("SessionId");
+
+ b.ToTable("ChatMessages", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.ChatSession", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("BoardId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BoardId");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ChatSessions", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.Column", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("BoardId")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Position")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("WipLimit")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BoardId", "Position")
+ .IsUnique();
+
+ b.ToTable("Columns", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.CommandRun", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("CompletedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CorrelationId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(2000)
+ .HasColumnType("TEXT");
+
+ b.Property("ExitCode")
+ .HasColumnType("INTEGER");
+
+ b.Property("OutputPreview")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("RequestedByUserId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("StartedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("TemplateName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Truncated")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CorrelationId");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("RequestedByUserId");
+
+ b.HasIndex("Status");
+
+ b.ToTable("CommandRuns", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.CommandRunLog", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("CommandRunId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Level")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Metadata")
+ .HasColumnType("TEXT");
+
+ b.Property("Source")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("Timestamp")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CommandRunId");
+
+ b.HasIndex("Level");
+
+ b.HasIndex("Timestamp");
+
+ b.ToTable("CommandRunLogs", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.ConnectorCredential", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AuthMethod")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("ConnectorId")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EncryptedValue")
+ .IsRequired()
+ .HasMaxLength(8000)
+ .HasColumnType("TEXT");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("TEXT");
+
+ b.Property("KeyVersion")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(1);
+
+ b.Property("Label")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("RotatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ConnectorId", "UserId")
+ .IsUnique();
+
+ b.ToTable("ConnectorCredentials", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.ConnectorEvent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("ConnectorId")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EventType")
+ .HasColumnType("INTEGER");
+
+ b.Property("Payload")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ConnectorId");
+
+ b.HasIndex("ConnectorId", "CreatedAt");
+
+ b.ToTable("ConnectorEvents", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.DailySnapshot", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Date")
+ .HasColumnType("TEXT");
+
+ b.Property("SealedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("UserId", "Date")
+ .IsUnique();
+
+ b.ToTable("DailySnapshots", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.ExternalLogin", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AvatarUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Provider")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderDisplayName")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderUserId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Provider", "ProviderUserId")
+ .IsUnique();
+
+ b.ToTable("ExternalLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Taskdeck.Domain.Entities.IntegrationConnector", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Configuration")
+ .HasMaxLength(4000)
+ .HasColumnType("TEXT");
+
+ b.Property("ConnectorType")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Direction")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property