feat(data): store ssh workspaces on app_settings
This commit is contained in:
@@ -43,6 +43,9 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
||||
builder.Property(s => s.RepoImportFolders)
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
builder.Property(s => s.SshWorkspaces)
|
||||
.HasColumnName("ssh_workspaces");
|
||||
|
||||
builder.Property(s => s.ReportExcludedPaths).HasColumnName("report_excluded_paths");
|
||||
builder.Property(s => s.StandupWeekday).HasColumnName("standup_weekday")
|
||||
.IsRequired().HasDefaultValue((int)DayOfWeek.Wednesday);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSshWorkspaces : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ssh_workspaces",
|
||||
table: "app_settings",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
column: "ssh_workspaces",
|
||||
value: null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ssh_workspaces",
|
||||
table: "app_settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SshWorkspaces")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ssh_workspaces");
|
||||
|
||||
b.Property<int>("StandupWeekday")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
|
||||
@@ -29,6 +29,8 @@ public sealed class AppSettingsEntity
|
||||
|
||||
// JSON array of parent folders remembered by the repo-import modal.
|
||||
public string? RepoImportFolders { get; set; }
|
||||
// JSON array of SshWorkspace rows offered as remote quick sessions in Mission Control.
|
||||
public string? SshWorkspaces { get; set; }
|
||||
// JSON array of path prefixes whose sessions are excluded from the weekly report.
|
||||
public string? ReportExcludedPaths { get; set; }
|
||||
// DayOfWeek the standup happens on; default Wednesday. Drives the report's default range.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ClaudeDo.Data.Models;
|
||||
|
||||
/// <summary>A server an interactive claude session can be started on via ssh; connection details live in the user's ssh config.</summary>
|
||||
public sealed record SshWorkspace(string Id, string Name, string Host, string RemotePath)
|
||||
{
|
||||
// A leading '-' would make ssh parse the alias as an option (-oProxyCommand=... runs local code).
|
||||
private static readonly Regex HostPattern = new(@"^[A-Za-z0-9._@][A-Za-z0-9._@-]*$", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>Returns the first invalid field ("name", "host", "path") or null when valid.</summary>
|
||||
public static string? Validate(SshWorkspace w)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(w.Name)) return "name";
|
||||
if (string.IsNullOrEmpty(w.Host) || !HostPattern.IsMatch(w.Host)) return "host";
|
||||
var p = w.RemotePath ?? "";
|
||||
if (!(p.StartsWith('/') || p.StartsWith('~')) || p.Contains('\'') || p.Contains('\n') || p.Contains('\r'))
|
||||
return "path";
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Single-quotes a validated remote path for a POSIX shell, leaving a leading <c>~</c> unquoted so it still expands.</summary>
|
||||
public static string QuoteRemotePath(string path)
|
||||
{
|
||||
if (path == "~") return "~";
|
||||
if (path.StartsWith("~/")) return "~/'" + path[2..] + "'";
|
||||
return "'" + path + "'";
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,28 @@ public sealed class AppSettingsRepository
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>Reads the configured SSH workspaces; missing or corrupt JSON yields an empty list.</summary>
|
||||
public async Task<List<SshWorkspace>> GetSshWorkspacesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var json = await _context.AppSettings.AsNoTracking()
|
||||
.Where(s => s.Id == AppSettingsEntity.SingletonId)
|
||||
.Select(s => s.SshWorkspaces)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(json)) return new List<SshWorkspace>();
|
||||
try { return JsonSerializer.Deserialize<List<SshWorkspace>>(json) ?? new List<SshWorkspace>(); }
|
||||
catch (JsonException) { return new List<SshWorkspace>(); }
|
||||
}
|
||||
|
||||
/// <summary>Writes only the SSH workspaces column; an empty list clears it.</summary>
|
||||
public async Task SetSshWorkspacesAsync(IEnumerable<SshWorkspace> workspaces, CancellationToken ct = default)
|
||||
{
|
||||
var list = workspaces.ToList();
|
||||
var row = await GetOrCreateTrackedRowAsync(ct);
|
||||
row.SshWorkspaces = list.Count == 0 ? null : JsonSerializer.Serialize(list);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>Writes only the ticket fallback list; blank clears it.</summary>
|
||||
public async Task SetTicketFallbackListIdAsync(string? listId, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user