Monorepo with centralized build props, npm workspaces, LlamaSharp AI, SQLite/SQLCipher storage, Svelte frontend, and unified smoke tests. Co-Authored-By: Oz <oz-agent@warp.dev>
576 lines
29 KiB
C#
576 lines
29 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using System.Globalization;
|
|
using System.Text.Json;
|
|
using Journal.Core.Dtos;
|
|
using Journal.Core.Models;
|
|
using Journal.Core.Services.Ai;
|
|
using Journal.Core.Services.Config;
|
|
using Journal.Core.Services.Database;
|
|
using Journal.Core.Services.Entries;
|
|
using Journal.Core.Services.Fragments;
|
|
using Journal.Core.Services.Lists;
|
|
using Journal.Core.Services.Conversations;
|
|
using Journal.Core.Services.Logging;
|
|
using Journal.Core.Services.Speech;
|
|
using Journal.Core.Services.Todos;
|
|
using Journal.Core.Services.Vault;
|
|
|
|
namespace Journal.Core;
|
|
|
|
public class Entry(
|
|
IFragmentService fragments,
|
|
IEntrySearchService entrySearch,
|
|
IVaultStorageService vaultStorage,
|
|
IJournalDatabaseService database,
|
|
IDatabaseSessionService databaseSession,
|
|
IJournalConfigService config,
|
|
IAiService ai,
|
|
ISpeechBridgeService speech,
|
|
IS2TService liveSpeech,
|
|
IEntryFileService entryFiles,
|
|
IListService lists,
|
|
ITodoService todos,
|
|
ICoachService coach,
|
|
IConversationService conversations,
|
|
CommandLogger logger)
|
|
{
|
|
private readonly IFragmentService _fragments = fragments;
|
|
private readonly IEntrySearchService _entrySearch = entrySearch;
|
|
private readonly IVaultStorageService _vaultStorage = vaultStorage;
|
|
private readonly IJournalDatabaseService _database = database;
|
|
private readonly IDatabaseSessionService _databaseSession = databaseSession;
|
|
private readonly IJournalConfigService _config = config;
|
|
private readonly IAiService _ai = ai;
|
|
private readonly ISpeechBridgeService _speech = speech;
|
|
private readonly IS2TService _liveSpeech = liveSpeech;
|
|
private readonly IEntryFileService _entryFiles = entryFiles;
|
|
private readonly IListService _lists = lists;
|
|
private readonly ITodoService _todos = todos;
|
|
private readonly ICoachService _coach = coach;
|
|
private readonly IConversationService _conversations = conversations;
|
|
private readonly CommandLogger _logger = logger;
|
|
private static readonly HashSet<string> VaultSyncActions = new(StringComparer.Ordinal)
|
|
{
|
|
"entries.save",
|
|
"entries.delete",
|
|
"templates.save",
|
|
"templates.delete",
|
|
"fragments.create",
|
|
"fragments.update",
|
|
"fragments.delete",
|
|
"lists.create",
|
|
"lists.update",
|
|
"lists.delete",
|
|
"todos.create",
|
|
"todos.update",
|
|
"todos.delete",
|
|
"todos.items.create",
|
|
"todos.items.update",
|
|
"todos.items.delete",
|
|
"conversations.create",
|
|
"conversations.update",
|
|
"conversations.delete",
|
|
"conversations.chat"
|
|
};
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
public async Task RunAsync()
|
|
{
|
|
string? line;
|
|
while ((line = Console.ReadLine()) is not null)
|
|
{
|
|
var response = await HandleCommandAsync(line);
|
|
Console.WriteLine(response);
|
|
}
|
|
}
|
|
|
|
public async Task<string> HandleCommandAsync(string json)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
return Error("Invalid command");
|
|
|
|
Command? cmd;
|
|
try
|
|
{
|
|
cmd = JsonSerializer.Deserialize<Command>(json, JsonOptions);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return Error("Invalid command JSON");
|
|
}
|
|
|
|
if (cmd is null || string.IsNullOrWhiteSpace(cmd.Action))
|
|
return Error("Invalid command");
|
|
|
|
var action = cmd.Action.Trim();
|
|
var correlationId = string.IsNullOrWhiteSpace(cmd.CorrelationId)
|
|
? Guid.NewGuid().ToString("N")
|
|
: cmd.CorrelationId.Trim();
|
|
CommandLogger.LogStart(action, correlationId, cmd.Payload);
|
|
object? result;
|
|
|
|
try
|
|
{
|
|
switch (action)
|
|
{
|
|
case "fragments.list":
|
|
result = _fragments.GetAll();
|
|
break;
|
|
case "fragments.get":
|
|
if (!Guid.TryParse(cmd.Id, out var getId))
|
|
return Error("Invalid or missing id");
|
|
result = _fragments.GetById(getId);
|
|
break;
|
|
case "fragments.create":
|
|
var createDto = DeserializePayload<CreateFragmentDto>(cmd.Payload);
|
|
if (createDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _fragments.Create(createDto);
|
|
break;
|
|
case "fragments.update":
|
|
if (!Guid.TryParse(cmd.Id, out var updateId))
|
|
return Error("Invalid or missing id");
|
|
var updateDto = DeserializePayload<UpdateFragmentDto>(cmd.Payload);
|
|
if (updateDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _fragments.Update(updateId, updateDto);
|
|
break;
|
|
case "fragments.delete":
|
|
if (!Guid.TryParse(cmd.Id, out var deleteId))
|
|
return Error("Invalid or missing id");
|
|
result = _fragments.Remove(deleteId);
|
|
break;
|
|
case "fragments.search":
|
|
result = _fragments.Search(cmd.Type, cmd.Tag);
|
|
break;
|
|
|
|
// ── Lists ────────────────────────────────────────
|
|
case "lists.list":
|
|
result = _lists.GetAll();
|
|
break;
|
|
case "lists.get":
|
|
if (!Guid.TryParse(cmd.Id, out var getListId))
|
|
return Error("Invalid or missing id");
|
|
result = _lists.GetById(getListId);
|
|
break;
|
|
case "lists.create":
|
|
var createListDto = DeserializePayload<CreateListDto>(cmd.Payload);
|
|
if (createListDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _lists.Create(createListDto);
|
|
break;
|
|
case "lists.update":
|
|
if (!Guid.TryParse(cmd.Id, out var updateListId))
|
|
return Error("Invalid or missing id");
|
|
var updateListDto = DeserializePayload<UpdateListDto>(cmd.Payload);
|
|
if (updateListDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _lists.Update(updateListId, updateListDto);
|
|
break;
|
|
case "lists.delete":
|
|
if (!Guid.TryParse(cmd.Id, out var deleteListId))
|
|
return Error("Invalid or missing id");
|
|
result = _lists.Remove(deleteListId);
|
|
break;
|
|
|
|
// ── Todos ────────────────────────────────────────
|
|
case "todos.list":
|
|
result = _todos.GetAllLists();
|
|
break;
|
|
case "todos.get":
|
|
if (!Guid.TryParse(cmd.Id, out var getTodoListId))
|
|
return Error("Invalid or missing id");
|
|
result = _todos.GetListById(getTodoListId);
|
|
break;
|
|
case "todos.create":
|
|
var createTodoListDto = DeserializePayload<CreateTodoListDto>(cmd.Payload);
|
|
if (createTodoListDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _todos.CreateList(createTodoListDto);
|
|
break;
|
|
case "todos.update":
|
|
if (!Guid.TryParse(cmd.Id, out var updateTodoListId))
|
|
return Error("Invalid or missing id");
|
|
var updateTodoListDto = DeserializePayload<UpdateTodoListDto>(cmd.Payload);
|
|
if (updateTodoListDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _todos.UpdateList(updateTodoListId, updateTodoListDto);
|
|
break;
|
|
case "todos.delete":
|
|
if (!Guid.TryParse(cmd.Id, out var deleteTodoListId))
|
|
return Error("Invalid or missing id");
|
|
result = _todos.RemoveList(deleteTodoListId);
|
|
break;
|
|
case "todos.items.create":
|
|
var createItemDto = DeserializePayload<CreateTodoItemDto>(cmd.Payload);
|
|
if (createItemDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _todos.CreateItem(createItemDto);
|
|
break;
|
|
case "todos.items.update":
|
|
if (!Guid.TryParse(cmd.Id, out var updateItemId))
|
|
return Error("Invalid or missing id");
|
|
var updateItemDto = DeserializePayload<UpdateTodoItemDto>(cmd.Payload);
|
|
if (updateItemDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _todos.UpdateItem(updateItemId, updateItemDto);
|
|
break;
|
|
case "todos.items.delete":
|
|
if (!Guid.TryParse(cmd.Id, out var deleteItemId))
|
|
return Error("Invalid or missing id");
|
|
result = _todos.RemoveItem(deleteItemId);
|
|
break;
|
|
case "search.entries":
|
|
var searchPayload = DeserializePayload<SearchEntriesPayload>(cmd.Payload);
|
|
if (searchPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
var searchRequest = new EntrySearchRequestDto(
|
|
Query: searchPayload.Query,
|
|
Section: searchPayload.Section,
|
|
StartDate: searchPayload.StartDate,
|
|
EndDate: searchPayload.EndDate,
|
|
Tags: searchPayload.Tags,
|
|
Types: searchPayload.Types,
|
|
Checked: searchPayload.Checked,
|
|
Unchecked: searchPayload.Unchecked);
|
|
result = await _entrySearch.SearchEntriesAsync(searchRequest);
|
|
break;
|
|
case "entries.list":
|
|
_ = DeserializePayload<EntryListPayload>(cmd.Payload);
|
|
result = _entryFiles.ListEntries();
|
|
break;
|
|
case "templates.list":
|
|
_ = DeserializePayload<EntryTemplateListPayload>(cmd.Payload);
|
|
result = _entryFiles.ListTemplates();
|
|
break;
|
|
case "entries.load":
|
|
var loadEntryPayload = DeserializePayload<EntryLoadPayload>(cmd.Payload);
|
|
if (loadEntryPayload is null || string.IsNullOrWhiteSpace(loadEntryPayload.FilePath))
|
|
return Error("Missing or invalid payload");
|
|
result = _entryFiles.LoadEntry(loadEntryPayload.FilePath);
|
|
break;
|
|
case "templates.load":
|
|
var loadTemplatePayload = DeserializePayload<EntryTemplateLoadPayload>(cmd.Payload);
|
|
if (loadTemplatePayload is null || string.IsNullOrWhiteSpace(loadTemplatePayload.FilePath))
|
|
return Error("Missing or invalid payload");
|
|
result = _entryFiles.LoadTemplate(loadTemplatePayload.FilePath);
|
|
break;
|
|
case "entries.save":
|
|
var saveEntryPayload = DeserializePayload<EntrySavePayload>(cmd.Payload);
|
|
if (saveEntryPayload is null || string.IsNullOrWhiteSpace(saveEntryPayload.Content))
|
|
return Error("Missing or invalid payload");
|
|
result = _entryFiles.SaveEntry(saveEntryPayload);
|
|
break;
|
|
case "templates.save":
|
|
var saveTemplatePayload = DeserializePayload<EntryTemplateSavePayload>(cmd.Payload);
|
|
if (saveTemplatePayload is null || string.IsNullOrWhiteSpace(saveTemplatePayload.Name))
|
|
return Error("Missing or invalid payload");
|
|
result = _entryFiles.SaveTemplate(saveTemplatePayload);
|
|
break;
|
|
case "entries.delete":
|
|
var deleteEntryPayload = DeserializePayload<EntryDeletePayload>(cmd.Payload);
|
|
if (deleteEntryPayload is null || string.IsNullOrWhiteSpace(deleteEntryPayload.FilePath))
|
|
return Error("Missing or invalid payload");
|
|
result = _entryFiles.DeleteEntry(deleteEntryPayload.FilePath);
|
|
break;
|
|
case "templates.delete":
|
|
var deleteTemplatePayload = DeserializePayload<EntryTemplateDeletePayload>(cmd.Payload);
|
|
if (deleteTemplatePayload is null || string.IsNullOrWhiteSpace(deleteTemplatePayload.FilePath))
|
|
return Error("Missing or invalid payload");
|
|
result = _entryFiles.DeleteTemplate(deleteTemplatePayload.FilePath);
|
|
break;
|
|
case "config.get":
|
|
result = _config.Current;
|
|
break;
|
|
case "ai.health":
|
|
result = await _ai.HealthAsync();
|
|
break;
|
|
case "ai.summarize_entry":
|
|
var summarizeEntryPayload = DeserializePayload<AiSummarizeEntryPayload>(cmd.Payload);
|
|
if (summarizeEntryPayload is null || string.IsNullOrWhiteSpace(summarizeEntryPayload.Content))
|
|
return Error("Missing or invalid payload");
|
|
result = await _ai.SummarizeEntryAsync(summarizeEntryPayload.Content, summarizeEntryPayload.FileStem);
|
|
break;
|
|
case "ai.summarize_all":
|
|
var summarizeAllPayload = DeserializePayload<AiSummarizeAllPayload>(cmd.Payload);
|
|
if (summarizeAllPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
result = await _ai.SummarizeAllAsync(summarizeAllPayload.Entries ?? []);
|
|
break;
|
|
case "ai.chat":
|
|
var chatPayload = DeserializePayload<AiChatPayload>(cmd.Payload);
|
|
if (chatPayload is null || string.IsNullOrWhiteSpace(chatPayload.Prompt))
|
|
return Error("Missing or invalid payload");
|
|
result = await _ai.ChatAsync(chatPayload.Prompt);
|
|
break;
|
|
case "ai.embed":
|
|
var embedPayload = DeserializePayload<AiEmbedPayload>(cmd.Payload);
|
|
if (embedPayload is null || string.IsNullOrWhiteSpace(embedPayload.Content))
|
|
return Error("Missing or invalid payload");
|
|
result = await _ai.EmbedAsync(embedPayload.Content);
|
|
break;
|
|
|
|
// ── Coach ─────────────────────────────────────────
|
|
case "ai.coach.daily":
|
|
var coachDailyPayload = DeserializePayload<CoachDailyPayload>(cmd.Payload);
|
|
result = await _coach.DailyCheckInAsync(new CoachContextDto(
|
|
DateLocal: coachDailyPayload?.DateLocal ?? DateTime.Now.ToString("yyyy-MM-dd"),
|
|
RecentEntries: coachDailyPayload?.RecentEntries,
|
|
RecentFragments: coachDailyPayload?.RecentFragments,
|
|
Preferences: coachDailyPayload?.Preferences));
|
|
break;
|
|
case "ai.coach.evening":
|
|
var coachEveningPayload = DeserializePayload<CoachEveningPayload>(cmd.Payload);
|
|
result = await _coach.EveningReviewAsync(new CoachContextDto(
|
|
DateLocal: coachEveningPayload?.DateLocal ?? DateTime.Now.ToString("yyyy-MM-dd"),
|
|
RecentEntries: coachEveningPayload?.RecentEntries,
|
|
RecentFragments: coachEveningPayload?.RecentFragments,
|
|
Preferences: coachEveningPayload?.Preferences));
|
|
break;
|
|
case "ai.coach.weekly":
|
|
var coachWeeklyPayload = DeserializePayload<CoachWeeklyPayload>(cmd.Payload);
|
|
var now = DateTime.Now;
|
|
var weekStart = now.AddDays(-(int)now.DayOfWeek + (int)DayOfWeek.Monday);
|
|
result = await _coach.WeeklyReviewAsync(new CoachContextDto(
|
|
DateLocal: now.ToString("yyyy-MM-dd"),
|
|
WeekStartLocal: coachWeeklyPayload?.WeekStartLocal ?? weekStart.ToString("yyyy-MM-dd"),
|
|
WeekEndLocal: coachWeeklyPayload?.WeekEndLocal ?? weekStart.AddDays(6).ToString("yyyy-MM-dd"),
|
|
RecentEntries: coachWeeklyPayload?.RecentEntries,
|
|
RecentFragments: coachWeeklyPayload?.RecentFragments,
|
|
Preferences: coachWeeklyPayload?.Preferences));
|
|
break;
|
|
|
|
// ── Conversations ──────────────────────────────────
|
|
case "conversations.list":
|
|
result = _conversations.GetAll();
|
|
break;
|
|
case "conversations.get":
|
|
if (!Guid.TryParse(cmd.Id, out var getConvId))
|
|
return Error("Invalid or missing id");
|
|
result = _conversations.GetById(getConvId);
|
|
break;
|
|
case "conversations.create":
|
|
var convCreatePayload = DeserializePayload<ConversationCreatePayload>(cmd.Payload);
|
|
if (convCreatePayload is null || string.IsNullOrWhiteSpace(convCreatePayload.Title))
|
|
return Error("Missing or invalid payload");
|
|
result = _conversations.Create(new CreateConversationDto(convCreatePayload.Title));
|
|
break;
|
|
case "conversations.update":
|
|
if (!Guid.TryParse(cmd.Id, out var updateConvId))
|
|
return Error("Invalid or missing id");
|
|
var convUpdatePayload = DeserializePayload<ConversationUpdatePayload>(cmd.Payload);
|
|
if (convUpdatePayload is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _conversations.Update(updateConvId, new UpdateConversationDto(convUpdatePayload.Title));
|
|
break;
|
|
case "conversations.delete":
|
|
if (!Guid.TryParse(cmd.Id, out var deleteConvId))
|
|
return Error("Invalid or missing id");
|
|
result = _conversations.Remove(deleteConvId);
|
|
break;
|
|
case "conversations.chat":
|
|
var convChatPayload = DeserializePayload<ConversationChatPayload>(cmd.Payload);
|
|
if (convChatPayload is null || string.IsNullOrWhiteSpace(convChatPayload.Prompt)
|
|
|| !Guid.TryParse(convChatPayload.ConversationId, out var chatConvId))
|
|
return Error("Missing or invalid payload");
|
|
// Save user message
|
|
var userMsg = _conversations.AddMessage(chatConvId, "user", convChatPayload.Prompt);
|
|
// Build history from existing messages
|
|
var history = _conversations.GetMessages(chatConvId)
|
|
.Where(m => m.Id != userMsg.Id)
|
|
.Select(m => (m.Role, m.Text))
|
|
.ToList();
|
|
// Get AI response with full conversation context
|
|
var aiResponse = await _ai.ChatWithHistoryAsync(history, convChatPayload.Prompt);
|
|
// Save AI response
|
|
var assistantMsg = _conversations.AddMessage(chatConvId, "assistant", aiResponse);
|
|
result = new { userMessage = userMsg, assistantMessage = assistantMsg };
|
|
break;
|
|
|
|
case "speech.devices.list":
|
|
result = await _speech.ListDevicesAsync();
|
|
break;
|
|
case "speech.transcribe":
|
|
var speechPayload = DeserializePayload<SpeechTranscribePayload>(cmd.Payload);
|
|
if (speechPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
var audioBase64 = !string.IsNullOrWhiteSpace(speechPayload.AudioBase64)
|
|
? speechPayload.AudioBase64
|
|
: speechPayload.Audio_Base64;
|
|
var text = speechPayload.Text;
|
|
var whisperModel = !string.IsNullOrWhiteSpace(speechPayload.WhisperModel)
|
|
? speechPayload.WhisperModel
|
|
: speechPayload.Whisper_Model;
|
|
var simulateDelayMs = speechPayload.SimulateDelayMs ?? speechPayload.Simulate_Delay_Ms;
|
|
if (string.IsNullOrWhiteSpace(audioBase64) && string.IsNullOrWhiteSpace(text))
|
|
return Error("Missing or invalid payload");
|
|
result = await _speech.TranscribeAsync(new SpeechTranscribeRequestDto(
|
|
AudioBase64: audioBase64,
|
|
Engine: speechPayload.Engine,
|
|
WhisperModel: whisperModel,
|
|
Text: text,
|
|
SimulateDelayMs: simulateDelayMs));
|
|
break;
|
|
case "speech.live.start":
|
|
result = await _liveSpeech.StartAsync();
|
|
break;
|
|
case "speech.live.stop":
|
|
result = await _liveSpeech.StopAsync();
|
|
break;
|
|
case "speech.live.poll":
|
|
var livePollPayload = DeserializePayload<S2TPollPayload>(cmd.Payload);
|
|
var maxItems = livePollPayload?.MaxItems ?? 8;
|
|
if (maxItems <= 0)
|
|
maxItems = 1;
|
|
if (maxItems > 64)
|
|
maxItems = 64;
|
|
result = await _liveSpeech.PollAsync(maxItems);
|
|
break;
|
|
case "vault.initialize":
|
|
var initPayload = DeserializePayload<VaultInitializePayload>(cmd.Payload);
|
|
if (initPayload is null || string.IsNullOrWhiteSpace(initPayload.Password) || string.IsNullOrWhiteSpace(initPayload.VaultDirectory))
|
|
return Error("Missing or invalid payload");
|
|
Directory.CreateDirectory(initPayload.VaultDirectory);
|
|
result = true;
|
|
break;
|
|
case "vault.load_all":
|
|
var loadPayload = DeserializePayload<VaultPayload>(cmd.Payload);
|
|
if (loadPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
var vaultStorageDirectory = ResolveVaultStorageDirectory();
|
|
var loaded = _vaultStorage.LoadAllVaults(loadPayload.Password, loadPayload.VaultDirectory, vaultStorageDirectory);
|
|
if (loaded)
|
|
_databaseSession.SetPassword(loadPayload.Password);
|
|
result = loaded;
|
|
break;
|
|
case "vault.rebuild_all":
|
|
var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload);
|
|
if (rebuildPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
_databaseSession.CloseConnection();
|
|
_vaultStorage.RebuildAllVaults(rebuildPayload.Password, rebuildPayload.VaultDirectory, ResolveVaultStorageDirectory());
|
|
result = true;
|
|
break;
|
|
case "vault.clear_data_directory":
|
|
var clearPayload = DeserializePayload<ClearDataPayload>(cmd.Payload);
|
|
if (clearPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
if (_databaseSession is IDisposable disposableSession)
|
|
disposableSession.Dispose();
|
|
_vaultStorage.ClearDataDirectory(ResolveVaultStorageDirectory());
|
|
result = true;
|
|
break;
|
|
case "db.status":
|
|
var dbStatusPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
|
if (dbStatusPayload is null || string.IsNullOrWhiteSpace(dbStatusPayload.Password))
|
|
return Error("Missing or invalid payload");
|
|
result = _database.GetStatus(dbStatusPayload.Password);
|
|
break;
|
|
case "db.initialize_schema":
|
|
var dbInitPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
|
if (dbInitPayload is null || string.IsNullOrWhiteSpace(dbInitPayload.Password))
|
|
return Error("Missing or invalid payload");
|
|
var initResult = _database.HydrateWorkspace(dbInitPayload.Password);
|
|
result = new
|
|
{
|
|
initialized = initResult.RuntimeReady,
|
|
databasePath = initResult.DatabasePath,
|
|
initResult.Message
|
|
};
|
|
break;
|
|
case "db.hydrate_workspace":
|
|
var dbHydratePayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
|
if (dbHydratePayload is null || string.IsNullOrWhiteSpace(dbHydratePayload.Password))
|
|
return Error("Missing or invalid payload");
|
|
result = _database.HydrateWorkspace(dbHydratePayload.Password);
|
|
_databaseSession.SetPassword(dbHydratePayload.Password);
|
|
break;
|
|
default:
|
|
CommandLogger.LogFailure(action, correlationId, "unknown_action");
|
|
return Error($"Unknown action: {action}");
|
|
}
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
CommandLogger.LogFailure(action, correlationId, "invalid_payload_json");
|
|
return Error("Missing or invalid payload");
|
|
}
|
|
catch (ValidationException ex)
|
|
{
|
|
CommandLogger.LogFailure(action, correlationId, "validation", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
CommandLogger.LogFailure(action, correlationId, "argument", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch (TimeoutException ex)
|
|
{
|
|
CommandLogger.LogFailure(action, correlationId, "timeout", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
CommandLogger.LogFailure(action, correlationId, "invalid_operation", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch (FileNotFoundException ex)
|
|
{
|
|
CommandLogger.LogFailure(action, correlationId, "not_found", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch
|
|
{
|
|
CommandLogger.LogFailure(action, correlationId, "internal_error");
|
|
return Error("Internal error");
|
|
}
|
|
|
|
TryAutoSyncVault(action, correlationId);
|
|
CommandLogger.LogSuccess(action, correlationId);
|
|
return JsonSerializer.Serialize(new { ok = true, data = result });
|
|
}
|
|
|
|
private static string Error(string message)
|
|
=> JsonSerializer.Serialize(new { ok = false, error = message });
|
|
|
|
private static T? DeserializePayload<T>(JsonElement? payload)
|
|
{
|
|
if (payload is null)
|
|
return default;
|
|
return payload.Value.Deserialize<T>(JsonOptions);
|
|
}
|
|
|
|
private void TryAutoSyncVault(string action, string correlationId)
|
|
{
|
|
if (!VaultSyncActions.Contains(action))
|
|
return;
|
|
|
|
if (!_databaseSession.TryGetSession(out var password))
|
|
return;
|
|
|
|
try
|
|
{
|
|
var config = _config.Current;
|
|
_databaseSession.CloseConnection();
|
|
_vaultStorage.RebuildAllVaults(password, config.VaultDirectory, ResolveVaultStorageDirectory());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CommandLogger.LogFailure(action, correlationId, "vault_auto_sync_failed", ex.Message);
|
|
}
|
|
}
|
|
|
|
private string ResolveVaultStorageDirectory()
|
|
{
|
|
var dbPath = _database.GetDatabasePath();
|
|
var directory = Path.GetDirectoryName(dbPath);
|
|
return string.IsNullOrWhiteSpace(directory)
|
|
? Path.GetFullPath(".")
|
|
: Path.GetFullPath(directory);
|
|
}
|
|
}
|