Files
ClaudeDo/src/ClaudeDo.Data/Repositories/AppSettingsRepository.cs
T

120 lines
5.4 KiB
C#

using System.Text.Json;
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Data.Repositories;
public sealed class AppSettingsRepository
{
private readonly ClaudeDoDbContext _context;
public AppSettingsRepository(ClaudeDoDbContext context) => _context = context;
public async Task<AppSettingsEntity> GetAsync(CancellationToken ct = default)
{
var row = await _context.AppSettings.AsNoTracking()
.FirstOrDefaultAsync(s => s.Id == AppSettingsEntity.SingletonId, ct);
if (row is null)
{
row = new AppSettingsEntity { Id = AppSettingsEntity.SingletonId, ModelPresets = ModelPresets.SerializeDefaults() };
_context.AppSettings.Add(row);
try
{
await _context.SaveChangesAsync(ct);
_context.Entry(row).State = EntityState.Detached;
}
catch (DbUpdateException)
{
// Concurrent process already inserted the singleton — discard our attempt and re-read.
_context.Entry(row).State = EntityState.Detached;
row = await _context.AppSettings.AsNoTracking()
.FirstAsync(s => s.Id == AppSettingsEntity.SingletonId, ct);
}
return row;
}
// First read after upgrading from a null model_presets column: persist the shipping
// defaults so the Settings UI shows real, editable rows instead of a code-only fallback.
if (row.ModelPresets is null)
row.ModelPresets = await BackfillModelPresetsAsync(ct);
return row;
}
private async Task<string> BackfillModelPresetsAsync(CancellationToken ct)
{
var defaults = ModelPresets.SerializeDefaults();
var tracked = await GetOrCreateTrackedRowAsync(ct);
if (tracked.ModelPresets is null)
{
tracked.ModelPresets = defaults;
await _context.SaveChangesAsync(ct);
}
return defaults;
}
private async Task<AppSettingsEntity> GetOrCreateTrackedRowAsync(CancellationToken ct)
{
var row = await _context.AppSettings
.FirstOrDefaultAsync(s => s.Id == AppSettingsEntity.SingletonId, ct);
if (row is null)
{
row = new AppSettingsEntity { Id = AppSettingsEntity.SingletonId };
_context.AppSettings.Add(row);
}
return row;
}
public async Task UpdateAsync(AppSettingsEntity updated, CancellationToken ct = default)
{
var row = await GetOrCreateTrackedRowAsync(ct);
row.DefaultClaudeInstructions = updated.DefaultClaudeInstructions ?? string.Empty;
row.DefaultModel = string.IsNullOrWhiteSpace(updated.DefaultModel) ? "sonnet" : updated.DefaultModel;
row.DefaultMaxTurns = updated.DefaultMaxTurns;
row.MaxTurnsCeiling = updated.MaxTurnsCeiling < 1 ? 1 : updated.MaxTurnsCeiling;
row.DefaultPermissionMode = string.IsNullOrWhiteSpace(updated.DefaultPermissionMode)
? "auto" : updated.DefaultPermissionMode;
row.MaxParallelExecutions = updated.MaxParallelExecutions < 1 ? 1 : updated.MaxParallelExecutions;
row.WorktreeStrategy = string.IsNullOrWhiteSpace(updated.WorktreeStrategy) ? "sibling" : updated.WorktreeStrategy;
row.CentralWorktreeRoot = string.IsNullOrWhiteSpace(updated.CentralWorktreeRoot)
? null : updated.CentralWorktreeRoot;
row.WorktreeAutoCleanupEnabled = updated.WorktreeAutoCleanupEnabled;
row.WorktreeAutoCleanupDays = updated.WorktreeAutoCleanupDays;
row.ReportExcludedPaths = string.IsNullOrWhiteSpace(updated.ReportExcludedPaths)
? null : updated.ReportExcludedPaths;
row.StandupWeekday = updated.StandupWeekday;
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
row.SessionSkills = string.IsNullOrWhiteSpace(updated.SessionSkills) ? null : updated.SessionSkills;
row.UsageGateFiveHourPct = Math.Clamp(updated.UsageGateFiveHourPct, 0, 100);
row.UsageGateSevenDayPct = Math.Clamp(updated.UsageGateSevenDayPct, 0, 100);
row.UsageThrottleFiveHourSoftPct = Math.Clamp(updated.UsageThrottleFiveHourSoftPct, 0, 100);
row.UsageThrottleFiveHourHardPct = Math.Clamp(updated.UsageThrottleFiveHourHardPct, 0, 100);
row.UsageThrottleSevenDaySoftPct = Math.Clamp(updated.UsageThrottleSevenDaySoftPct, 0, 100);
row.UsageThrottleSevenDayHardPct = Math.Clamp(updated.UsageThrottleSevenDayHardPct, 0, 100);
await _context.SaveChangesAsync(ct);
}
public async Task<List<string>> GetRepoImportFoldersAsync(CancellationToken ct = default)
{
var json = await _context.AppSettings.AsNoTracking()
.Where(s => s.Id == AppSettingsEntity.SingletonId)
.Select(s => s.RepoImportFolders)
.FirstOrDefaultAsync(ct);
if (string.IsNullOrWhiteSpace(json)) return new List<string>();
try { return JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>(); }
catch (JsonException) { return new List<string>(); }
}
public async Task SetRepoImportFoldersAsync(IEnumerable<string> folders, CancellationToken ct = default)
{
var list = folders.ToList();
var row = await GetOrCreateTrackedRowAsync(ct);
row.RepoImportFolders = list.Count == 0 ? null : JsonSerializer.Serialize(list);
await _context.SaveChangesAsync(ct);
}
}