chore(data): remove raw ADO.NET infrastructure, add EF migration and design-time factory
Delete SqliteConnectionFactory, SchemaInitializer, and schema.sql. Fix ValueConverter lambdas in entity configurations (no throw-expressions in expression trees). Add IDesignTimeDbContextFactory for dotnet-ef tooling. Generate InitialCreate migration with seed data for agent/manual tags. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,100 +0,0 @@
|
||||
-- ClaudeDo SQLite schema (single source of truth, 3NF)
|
||||
-- Applied by Worker on first startup. WAL mode is set via PRAGMA after open.
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lists (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
working_dir TEXT NULL,
|
||||
default_commit_type TEXT NOT NULL DEFAULT 'chore'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('manual','queued','running','done','failed')),
|
||||
scheduled_for TIMESTAMP NULL,
|
||||
result TEXT NULL,
|
||||
log_path TEXT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
started_at TIMESTAMP NULL,
|
||||
finished_at TIMESTAMP NULL,
|
||||
commit_type TEXT NOT NULL DEFAULT 'chore'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_list_id ON tasks(list_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS list_tags (
|
||||
list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (list_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_tags (
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (task_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS list_config (
|
||||
list_id TEXT PRIMARY KEY REFERENCES lists(id) ON DELETE CASCADE,
|
||||
model TEXT NULL,
|
||||
system_prompt TEXT NULL,
|
||||
agent_path TEXT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS worktrees (
|
||||
task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
path TEXT NOT NULL,
|
||||
branch_name TEXT NOT NULL,
|
||||
base_commit TEXT NOT NULL,
|
||||
head_commit TEXT NULL,
|
||||
diff_stat TEXT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active','merged','discarded','kept')),
|
||||
created_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
run_number INTEGER NOT NULL,
|
||||
session_id TEXT NULL,
|
||||
is_retry INTEGER NOT NULL DEFAULT 0,
|
||||
prompt TEXT NOT NULL,
|
||||
result_markdown TEXT NULL,
|
||||
structured_output TEXT NULL,
|
||||
error_markdown TEXT NULL,
|
||||
exit_code INTEGER NULL,
|
||||
turn_count INTEGER NULL,
|
||||
tokens_in INTEGER NULL,
|
||||
tokens_out INTEGER NULL,
|
||||
log_path TEXT NULL,
|
||||
started_at TIMESTAMP NULL,
|
||||
finished_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_task_id ON task_runs(task_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subtasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0,
|
||||
order_num INTEGER NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_subtasks_task_id ON subtasks(task_id);
|
||||
|
||||
-- Seed: minimal tag set (ignored if already present)
|
||||
INSERT OR IGNORE INTO tags (name) VALUES ('agent');
|
||||
INSERT OR IGNORE INTO tags (name) VALUES ('manual');
|
||||
15
src/ClaudeDo.Data/ClaudeDoDbContextFactory.cs
Normal file
15
src/ClaudeDo.Data/ClaudeDoDbContextFactory.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace ClaudeDo.Data;
|
||||
|
||||
public sealed class ClaudeDoDbContextFactory : IDesignTimeDbContextFactory<ClaudeDoDbContext>
|
||||
{
|
||||
public ClaudeDoDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite("Data Source=design-time.db")
|
||||
.Options;
|
||||
return new ClaudeDoDbContext(options);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,32 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Data.Configuration;
|
||||
|
||||
public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
||||
{
|
||||
private static string StatusToString(TaskStatus v)
|
||||
=> v == TaskStatus.Manual ? "manual"
|
||||
: v == TaskStatus.Queued ? "queued"
|
||||
: v == TaskStatus.Running ? "running"
|
||||
: v == TaskStatus.Done ? "done"
|
||||
: v == TaskStatus.Failed ? "failed"
|
||||
: throw new ArgumentOutOfRangeException(nameof(v));
|
||||
|
||||
private static TaskStatus StatusFromString(string v)
|
||||
=> v == "manual" ? TaskStatus.Manual
|
||||
: v == "queued" ? TaskStatus.Queued
|
||||
: v == "running" ? TaskStatus.Running
|
||||
: v == "done" ? TaskStatus.Done
|
||||
: v == "failed" ? TaskStatus.Failed
|
||||
: throw new ArgumentOutOfRangeException(nameof(v));
|
||||
|
||||
private static readonly ValueConverter<TaskStatus, string> StatusConverter =
|
||||
new(v => StatusToString(v), v => StatusFromString(v));
|
||||
|
||||
public void Configure(EntityTypeBuilder<TaskEntity> builder)
|
||||
{
|
||||
builder.ToTable("tasks");
|
||||
@@ -17,25 +37,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
||||
builder.Property(t => t.Title).HasColumnName("title").IsRequired();
|
||||
builder.Property(t => t.Description).HasColumnName("description");
|
||||
builder.Property(t => t.Status).HasColumnName("status").IsRequired()
|
||||
.HasConversion(
|
||||
v => v switch
|
||||
{
|
||||
TaskStatus.Manual => "manual",
|
||||
TaskStatus.Queued => "queued",
|
||||
TaskStatus.Running => "running",
|
||||
TaskStatus.Done => "done",
|
||||
TaskStatus.Failed => "failed",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
},
|
||||
v => v switch
|
||||
{
|
||||
"manual" => TaskStatus.Manual,
|
||||
"queued" => TaskStatus.Queued,
|
||||
"running" => TaskStatus.Running,
|
||||
"done" => TaskStatus.Done,
|
||||
"failed" => TaskStatus.Failed,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
});
|
||||
.HasConversion(StatusConverter);
|
||||
builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for");
|
||||
builder.Property(t => t.Result).HasColumnName("result");
|
||||
builder.Property(t => t.LogPath).HasColumnName("log_path");
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace ClaudeDo.Data.Configuration;
|
||||
|
||||
public class WorktreeEntityConfiguration : IEntityTypeConfiguration<WorktreeEntity>
|
||||
{
|
||||
private static string StateToString(WorktreeState v)
|
||||
=> v == WorktreeState.Active ? "active"
|
||||
: v == WorktreeState.Merged ? "merged"
|
||||
: v == WorktreeState.Discarded ? "discarded"
|
||||
: v == WorktreeState.Kept ? "kept"
|
||||
: throw new ArgumentOutOfRangeException(nameof(v));
|
||||
|
||||
private static WorktreeState StateFromString(string v)
|
||||
=> v == "active" ? WorktreeState.Active
|
||||
: v == "merged" ? WorktreeState.Merged
|
||||
: v == "discarded" ? WorktreeState.Discarded
|
||||
: v == "kept" ? WorktreeState.Kept
|
||||
: throw new ArgumentOutOfRangeException(nameof(v));
|
||||
|
||||
private static readonly ValueConverter<WorktreeState, string> StateConverter =
|
||||
new(v => StateToString(v), v => StateFromString(v));
|
||||
|
||||
public void Configure(EntityTypeBuilder<WorktreeEntity> builder)
|
||||
{
|
||||
builder.ToTable("worktrees");
|
||||
@@ -19,23 +37,7 @@ public class WorktreeEntityConfiguration : IEntityTypeConfiguration<WorktreeEnti
|
||||
builder.Property(w => w.DiffStat).HasColumnName("diff_stat");
|
||||
builder.Property(w => w.State).HasColumnName("state").IsRequired()
|
||||
.HasDefaultValue(WorktreeState.Active)
|
||||
.HasConversion(
|
||||
v => v switch
|
||||
{
|
||||
WorktreeState.Active => "active",
|
||||
WorktreeState.Merged => "merged",
|
||||
WorktreeState.Discarded => "discarded",
|
||||
WorktreeState.Kept => "kept",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
},
|
||||
v => v switch
|
||||
{
|
||||
"active" => WorktreeState.Active,
|
||||
"merged" => WorktreeState.Merged,
|
||||
"discarded" => WorktreeState.Discarded,
|
||||
"kept" => WorktreeState.Kept,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
});
|
||||
.HasConversion(StateConverter);
|
||||
builder.Property(w => w.CreatedAt).HasColumnName("created_at").IsRequired();
|
||||
}
|
||||
}
|
||||
|
||||
298
src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.cs
Normal file
298
src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "lists",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
created_at = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
working_dir = table.Column<string>(type: "TEXT", nullable: true),
|
||||
default_commit_type = table.Column<string>(type: "TEXT", nullable: false, defaultValue: "chore")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_lists", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tags",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
name = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_tags", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "list_config",
|
||||
columns: table => new
|
||||
{
|
||||
list_id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
model = table.Column<string>(type: "TEXT", nullable: true),
|
||||
system_prompt = table.Column<string>(type: "TEXT", nullable: true),
|
||||
agent_path = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_list_config", x => x.list_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_list_config_lists_list_id",
|
||||
column: x => x.list_id,
|
||||
principalTable: "lists",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tasks",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
list_id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
title = table.Column<string>(type: "TEXT", nullable: false),
|
||||
description = table.Column<string>(type: "TEXT", nullable: true),
|
||||
status = table.Column<string>(type: "TEXT", nullable: false),
|
||||
scheduled_for = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
result = table.Column<string>(type: "TEXT", nullable: true),
|
||||
log_path = table.Column<string>(type: "TEXT", nullable: true),
|
||||
created_at = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
started_at = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
finished_at = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
commit_type = table.Column<string>(type: "TEXT", nullable: false, defaultValue: "chore"),
|
||||
model = table.Column<string>(type: "TEXT", nullable: true),
|
||||
system_prompt = table.Column<string>(type: "TEXT", nullable: true),
|
||||
agent_path = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_tasks", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_tasks_lists_list_id",
|
||||
column: x => x.list_id,
|
||||
principalTable: "lists",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "list_tags",
|
||||
columns: table => new
|
||||
{
|
||||
list_id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
tag_id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_list_tags", x => new { x.list_id, x.tag_id });
|
||||
table.ForeignKey(
|
||||
name: "FK_list_tags_lists_list_id",
|
||||
column: x => x.list_id,
|
||||
principalTable: "lists",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_list_tags_tags_tag_id",
|
||||
column: x => x.tag_id,
|
||||
principalTable: "tags",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "subtasks",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
task_id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
title = table.Column<string>(type: "TEXT", nullable: false),
|
||||
completed = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
||||
order_num = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
created_at = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_subtasks", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_subtasks_tasks_task_id",
|
||||
column: x => x.task_id,
|
||||
principalTable: "tasks",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "task_runs",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
task_id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
run_number = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
session_id = table.Column<string>(type: "TEXT", nullable: true),
|
||||
is_retry = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
||||
prompt = table.Column<string>(type: "TEXT", nullable: false),
|
||||
result_markdown = table.Column<string>(type: "TEXT", nullable: true),
|
||||
structured_output = table.Column<string>(type: "TEXT", nullable: true),
|
||||
error_markdown = table.Column<string>(type: "TEXT", nullable: true),
|
||||
exit_code = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
turn_count = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
tokens_in = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
tokens_out = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
log_path = table.Column<string>(type: "TEXT", nullable: true),
|
||||
started_at = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
finished_at = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_task_runs", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_task_runs_tasks_task_id",
|
||||
column: x => x.task_id,
|
||||
principalTable: "tasks",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "task_tags",
|
||||
columns: table => new
|
||||
{
|
||||
task_id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
tag_id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_task_tags", x => new { x.task_id, x.tag_id });
|
||||
table.ForeignKey(
|
||||
name: "FK_task_tags_tags_tag_id",
|
||||
column: x => x.tag_id,
|
||||
principalTable: "tags",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_task_tags_tasks_task_id",
|
||||
column: x => x.task_id,
|
||||
principalTable: "tasks",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "worktrees",
|
||||
columns: table => new
|
||||
{
|
||||
task_id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
path = table.Column<string>(type: "TEXT", nullable: false),
|
||||
branch_name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
base_commit = table.Column<string>(type: "TEXT", nullable: false),
|
||||
head_commit = table.Column<string>(type: "TEXT", nullable: true),
|
||||
diff_stat = table.Column<string>(type: "TEXT", nullable: true),
|
||||
state = table.Column<string>(type: "TEXT", nullable: false, defaultValue: "active"),
|
||||
created_at = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_worktrees", x => x.task_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_worktrees_tasks_task_id",
|
||||
column: x => x.task_id,
|
||||
principalTable: "tasks",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "tags",
|
||||
columns: new[] { "id", "name" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1L, "agent" },
|
||||
{ 2L, "manual" }
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_list_tags_tag_id",
|
||||
table: "list_tags",
|
||||
column: "tag_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "idx_subtasks_task_id",
|
||||
table: "subtasks",
|
||||
column: "task_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_tags_name",
|
||||
table: "tags",
|
||||
column: "name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "idx_task_runs_task_id",
|
||||
table: "task_runs",
|
||||
column: "task_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_task_tags_tag_id",
|
||||
table: "task_tags",
|
||||
column: "tag_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "idx_tasks_list_id",
|
||||
table: "tasks",
|
||||
column: "list_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "idx_tasks_status",
|
||||
table: "tasks",
|
||||
column: "status");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "list_config");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "list_tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "subtasks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "task_runs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "task_tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "worktrees");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tasks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "lists");
|
||||
}
|
||||
}
|
||||
}
|
||||
479
src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs
Normal file
479
src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs
Normal file
@@ -0,0 +1,479 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
partial class ClaudeDoDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("tags", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Name = "agent"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
Name = "manual"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.Property<string>("list_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("list_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("list_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.Property<string>("task_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("task_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("task_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("list_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("task_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace ClaudeDo.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Applies the embedded schema.sql script. Safe to call on every start — the script uses
|
||||
/// IF NOT EXISTS / INSERT OR IGNORE.
|
||||
/// </summary>
|
||||
public static class SchemaInitializer
|
||||
{
|
||||
private const string ResourceName = "ClaudeDo.Data.schema.sql";
|
||||
|
||||
public static void Apply(SqliteConnectionFactory factory)
|
||||
{
|
||||
using var conn = factory.Open();
|
||||
ApplyTo(conn);
|
||||
}
|
||||
|
||||
public static void ApplyTo(SqliteConnection conn)
|
||||
{
|
||||
var sql = LoadScript();
|
||||
using var tx = conn.BeginTransaction();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.Transaction = tx;
|
||||
cmd.CommandText = sql;
|
||||
cmd.ExecuteNonQuery();
|
||||
tx.Commit();
|
||||
|
||||
ApplyMigrations(conn);
|
||||
}
|
||||
|
||||
private static void ApplyMigrations(SqliteConnection conn)
|
||||
{
|
||||
string[] alterStatements =
|
||||
[
|
||||
"ALTER TABLE tasks ADD COLUMN model TEXT NULL",
|
||||
"ALTER TABLE tasks ADD COLUMN system_prompt TEXT NULL",
|
||||
"ALTER TABLE tasks ADD COLUMN agent_path TEXT NULL",
|
||||
];
|
||||
|
||||
foreach (var sql in alterStatements)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
catch (SqliteException ex) when (ex.SqliteErrorCode == 1)
|
||||
{
|
||||
// Column already exists — safe to ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string LoadScript()
|
||||
{
|
||||
var asm = typeof(SchemaInitializer).Assembly;
|
||||
using var stream = asm.GetManifestResourceStream(ResourceName)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Embedded resource '{ResourceName}' not found in {asm.GetName().Name}. " +
|
||||
$"Available: {string.Join(", ", asm.GetManifestResourceNames())}");
|
||||
using var reader = new StreamReader(stream);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace ClaudeDo.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Opens <see cref="SqliteConnection"/> instances pointed at <see cref="DbPath"/>.
|
||||
/// First call ensures the parent directory exists, enables WAL and foreign keys.
|
||||
/// </summary>
|
||||
public sealed class SqliteConnectionFactory
|
||||
{
|
||||
public string DbPath { get; }
|
||||
private readonly string _connectionString;
|
||||
private int _walApplied;
|
||||
|
||||
public SqliteConnectionFactory(string dbPath)
|
||||
{
|
||||
DbPath = Paths.Expand(dbPath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(DbPath)!);
|
||||
|
||||
_connectionString = new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = DbPath,
|
||||
Mode = SqliteOpenMode.ReadWriteCreate,
|
||||
Cache = SqliteCacheMode.Shared,
|
||||
}.ToString();
|
||||
}
|
||||
|
||||
public SqliteConnection Open()
|
||||
{
|
||||
var conn = new SqliteConnection(_connectionString);
|
||||
conn.Open();
|
||||
|
||||
// WAL is a persistent DB-level setting; applying it once per process is enough,
|
||||
// but idempotent so we do it defensively on the first connection we hand out.
|
||||
if (Interlocked.Exchange(ref _walApplied, 1) == 0)
|
||||
{
|
||||
using var pragma = conn.CreateCommand();
|
||||
pragma.CommandText = "PRAGMA journal_mode=WAL;";
|
||||
pragma.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using var fk = conn.CreateCommand();
|
||||
fk.CommandText = "PRAGMA foreign_keys=ON;";
|
||||
fk.ExecuteNonQuery();
|
||||
|
||||
return conn;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user