118 lines
5.2 KiB
C#
118 lines
5.2 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.UsageThrottleSoftPct = Math.Clamp(updated.UsageThrottleSoftPct, 0, 100);
|
|
row.UsageThrottleHardPct = Math.Clamp(updated.UsageThrottleHardPct, 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);
|
|
}
|
|
}
|