diff --git a/Journal.App/src/lib/backend/templates.ts b/Journal.App/src/lib/backend/templates.ts new file mode 100644 index 0000000..79bf828 --- /dev/null +++ b/Journal.App/src/lib/backend/templates.ts @@ -0,0 +1,90 @@ +import { sendCommand } from "./client"; +import { pickCase } from "./normalize"; + +export type EntryTemplateItemDto = { + fileName: string; + filePath: string; +}; + +export type EntryTemplateLoadResultDto = { + fileName: string; + filePath: string; + content: string; +}; + +export type EntryTemplateSaveResultDto = { + filePath: string; +}; + +type EntryTemplateItemDtoRaw = { + fileName?: string; + filePath?: string; + FileName?: string; + FilePath?: string; +}; + +type EntryTemplateLoadResultDtoRaw = { + fileName?: string; + filePath?: string; + content?: string; + FileName?: string; + FilePath?: string; + Content?: string; +}; + +type EntryTemplateSaveResultDtoRaw = { + filePath?: string; + FilePath?: string; +}; + +function normalizeTemplateItem(raw: EntryTemplateItemDtoRaw): EntryTemplateItemDto { + return { + fileName: pickCase(raw, "fileName", "FileName", ""), + filePath: pickCase(raw, "filePath", "FilePath", "") + }; +} + +export async function listEntryTemplates(dataDirectory?: string): Promise { + const data = await sendCommand({ + action: "templates.list", + payload: { dataDirectory } + }); + + return data.map(normalizeTemplateItem).filter((item) => Boolean(item.filePath)); +} + +export async function loadEntryTemplate(filePath: string): Promise { + const data = await sendCommand({ + action: "templates.load", + payload: { filePath } + }); + + return { + fileName: pickCase(data, "fileName", "FileName", ""), + filePath: pickCase(data, "filePath", "FilePath", ""), + content: pickCase(data, "content", "Content", "") + }; +} + +export async function saveEntryTemplate(payload: { + name: string; + content: string; + filePath?: string; + dataDirectory?: string; +}): Promise { + const data = await sendCommand({ + action: "templates.save", + payload + }); + + return { + filePath: pickCase(data, "filePath", "FilePath", "") + }; +} + +export async function deleteEntryTemplate(filePath: string): Promise { + return sendCommand({ + action: "templates.delete", + payload: { filePath } + }); +} diff --git a/Journal.App/src/lib/components/EditorPanel.svelte b/Journal.App/src/lib/components/EditorPanel.svelte index 00dcce7..b6b536e 100644 --- a/Journal.App/src/lib/components/EditorPanel.svelte +++ b/Journal.App/src/lib/components/EditorPanel.svelte @@ -54,6 +54,8 @@ display: flex; flex-direction: column; gap: 16px; + min-height: 0; + overflow: hidden; } .editor-empty { diff --git a/Journal.App/src/lib/components/SidePanel.svelte b/Journal.App/src/lib/components/SidePanel.svelte index 269f4b0..28d36f5 100644 --- a/Journal.App/src/lib/components/SidePanel.svelte +++ b/Journal.App/src/lib/components/SidePanel.svelte @@ -322,6 +322,8 @@ display: flex; flex-direction: column; gap: 12px; + min-height: 0; + overflow: hidden; } .panel-header { @@ -388,6 +390,8 @@ display: flex; flex-direction: column; gap: 4px; + min-height: 0; + overflow: auto; li { display: flex; diff --git a/Journal.App/src/lib/components/editor/MarkdownEditor.svelte b/Journal.App/src/lib/components/editor/MarkdownEditor.svelte index 2713b59..ccb5774 100644 --- a/Journal.App/src/lib/components/editor/MarkdownEditor.svelte +++ b/Journal.App/src/lib/components/editor/MarkdownEditor.svelte @@ -1,5 +1,7 @@ @@ -94,6 +158,26 @@ + {#if isEntryDocument} + + {/if} @@ -101,6 +185,9 @@ + {#if templateError} +

{templateError}

+ {/if} {/if}
@@ -190,6 +277,12 @@ color: var(--text-primary); } + .template-error { + color: #e74c3c; + font-size: 0.78rem; + margin: -2px 0 0; + } + .editor-workspace { min-height: 0; height: 100%; diff --git a/Journal.App/src/routes/+page.svelte b/Journal.App/src/routes/+page.svelte index b34f81f..27f0b88 100644 --- a/Journal.App/src/routes/+page.svelte +++ b/Journal.App/src/routes/+page.svelte @@ -390,4 +390,16 @@ onCancel={handleModalCancel} /> + diff --git a/Journal.App/src/routes/settings/+page.svelte b/Journal.App/src/routes/settings/+page.svelte index 985ddd0..10a4455 100644 --- a/Journal.App/src/routes/settings/+page.svelte +++ b/Journal.App/src/routes/settings/+page.svelte @@ -2,6 +2,13 @@ import { goto } from "$app/navigation"; import AppModal from "$lib/components/AppModal.svelte"; import Navbar from "$lib/components/Navbar.svelte"; + import { + deleteEntryTemplate, + listEntryTemplates, + loadEntryTemplate, + saveEntryTemplate, + type EntryTemplateItemDto + } from "$lib/backend/templates"; import { addFragmentType, addSettingsTag, @@ -34,6 +41,12 @@ let sidecarRoot = ""; let sidecarRootIsCustom = false; let sidecarRootError = ""; + let templates: EntryTemplateItemDto[] = []; + let templateName = ""; + let templateContent = ""; + let templateFilePath: string | null = null; + let templateError = ""; + let templatesBusy = false; onMount(async () => { try { @@ -43,6 +56,8 @@ } catch (e) { sidecarRootError = String(e); } + + await refreshTemplates(); }); async function saveSidecarRoot() { @@ -192,6 +207,84 @@ cancelEditFragmentType(); } } + + async function refreshTemplates() { + templatesBusy = true; + templateError = ""; + try { + templates = await listEntryTemplates(); + } catch (error) { + templateError = String(error); + } finally { + templatesBusy = false; + } + } + + function resetTemplateEditor() { + templateName = ""; + templateContent = ""; + templateFilePath = null; + } + + async function editTemplate(item: EntryTemplateItemDto) { + templateError = ""; + try { + const loaded = await loadEntryTemplate(item.filePath); + templateFilePath = loaded.filePath; + templateName = loaded.fileName.replace(/\.template\.md$/i, ""); + templateContent = loaded.content; + } catch (error) { + templateError = String(error); + } + } + + async function saveTemplate() { + templateError = ""; + if (!templateName.trim()) { + templateError = "Template name is required."; + return; + } + if (!templateContent.trim()) { + templateError = "Template content is required."; + return; + } + + try { + const wasCreate = !templateFilePath; + const saved = await saveEntryTemplate({ + name: templateName, + content: templateContent, + filePath: templateFilePath ?? undefined + }); + + if (wasCreate) { + resetTemplateEditor(); + } else { + templateFilePath = saved.filePath; + } + await refreshTemplates(); + } catch (error) { + templateError = String(error); + } + } + + async function removeTemplate(item: EntryTemplateItemDto) { + templateError = ""; + try { + const ok = await deleteEntryTemplate(item.filePath); + if (!ok) { + templateError = "Failed to delete template."; + return; + } + + if (templateFilePath === item.filePath) { + resetTemplateEditor(); + } + await refreshTemplates(); + } catch (error) { + templateError = String(error); + } + }
@@ -205,16 +298,6 @@
- - - -
+ +
+ {#if templatesBusy} +

Loading templates...

+ {:else if templates.length === 0} +

No templates found.

+ {:else} +
    + {#each templates as item} +
  • + {item.fileName.replace(/\.template\.md$/i, "")} +
    + + +
    +
  • + {/each} +
+ {/if} +
+
+ + {#if templateError} +

{templateError}

+ {/if} + +

Sidecar

Root directory containing the Journal.Sidecar project.

@@ -384,14 +518,6 @@ margin-bottom: 16px; } - .toggle-row { - display: flex; - align-items: center; - gap: 8px; - color: var(--text-muted); - font-size: 0.88rem; - } - .route-card label { display: flex; flex-direction: column; @@ -499,6 +625,31 @@ color: #e74c3c; font-size: 0.82rem; } + + .template-grid { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + } + + .template-editor { + display: flex; + flex-direction: column; + gap: 8px; + } + + .template-editor textarea { + border: 1px solid var(--border-soft); + border-radius: 7px; + background: var(--bg-app); + color: var(--text-primary); + padding: 8px 9px; + resize: vertical; + } + + @media (min-width: 980px) { + .template-grid { + grid-template-columns: 1.2fr 1fr; + } + } - - diff --git a/Journal.Core/Dtos/CommandDtos.cs b/Journal.Core/Dtos/CommandDtos.cs index 9509ccd..e84f832 100644 --- a/Journal.Core/Dtos/CommandDtos.cs +++ b/Journal.Core/Dtos/CommandDtos.cs @@ -10,6 +10,11 @@ public sealed record EntryListItem(string FileName, string FilePath); public sealed record EntryLoadResult(string FileName, string FilePath, JournalEntryDto Entry); public sealed record EntrySaveResult(string FilePath); internal sealed record EntryDeletePayload(string FilePath); +internal sealed record EntryTemplateListPayload(string? DataDirectory = null); +internal sealed record EntryTemplateLoadPayload(string FilePath); +internal sealed record EntryTemplateDeletePayload(string FilePath); +public sealed record EntryTemplateLoadResult(string FileName, string FilePath, string Content); +public sealed record EntryTemplateSavePayload(string Name, string Content, string? FilePath = null, string? DataDirectory = null); internal sealed record DatabasePayload(string Password, string? DataDirectory = null); internal sealed record AiSummarizeEntryPayload(string Content, string? FileStem = null); internal sealed record AiSummarizeAllPayload(List? Entries); diff --git a/Journal.Core/Entry.cs b/Journal.Core/Entry.cs index 9bf5d1b..06b57dc 100644 --- a/Journal.Core/Entry.cs +++ b/Journal.Core/Entry.cs @@ -216,24 +216,49 @@ public class Entry( : _config.Current.DataDirectory; result = _entryFiles.ListEntries(listDataDirectory); break; + case "templates.list": + var templateListPayload = DeserializePayload(cmd.Payload); + var templateListDirectory = !string.IsNullOrWhiteSpace(templateListPayload?.DataDirectory) + ? templateListPayload.DataDirectory + : _config.Current.DataDirectory; + result = _entryFiles.ListTemplates(templateListDirectory); + break; case "entries.load": var loadEntryPayload = DeserializePayload(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(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(cmd.Payload); if (saveEntryPayload is null || string.IsNullOrWhiteSpace(saveEntryPayload.Content)) return Error("Missing or invalid payload"); result = _entryFiles.SaveEntry(saveEntryPayload, _config.Current.DataDirectory); break; + case "templates.save": + var saveTemplatePayload = DeserializePayload(cmd.Payload); + if (saveTemplatePayload is null || string.IsNullOrWhiteSpace(saveTemplatePayload.Name)) + return Error("Missing or invalid payload"); + result = _entryFiles.SaveTemplate(saveTemplatePayload, _config.Current.DataDirectory); + break; case "entries.delete": var deleteEntryPayload = DeserializePayload(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(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; diff --git a/Journal.Core/Services/Entries/EntryFileNaming.cs b/Journal.Core/Services/Entries/EntryFileNaming.cs new file mode 100644 index 0000000..7b800dd --- /dev/null +++ b/Journal.Core/Services/Entries/EntryFileNaming.cs @@ -0,0 +1,9 @@ +namespace Journal.Core.Services.Entries; + +internal static class EntryFileNaming +{ + internal const string TemplateSuffix = ".template.md"; + + internal static bool IsTemplateFileName(string fileName) + => fileName.EndsWith(TemplateSuffix, StringComparison.OrdinalIgnoreCase); +} diff --git a/Journal.Core/Services/Entries/EntryFileService.cs b/Journal.Core/Services/Entries/EntryFileService.cs index 4bbfb0a..450e859 100644 --- a/Journal.Core/Services/Entries/EntryFileService.cs +++ b/Journal.Core/Services/Entries/EntryFileService.cs @@ -10,6 +10,16 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ public IReadOnlyList ListEntries(string dataDirectory) { return [.. _repo.ListMarkdownFiles(dataDirectory) + .Where(path => !EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path))) + .Select(path => new EntryListItem( + FileName: _repo.GetFileName(path), + FilePath: _repo.GetFullPath(path)))]; + } + + public IReadOnlyList ListTemplates(string dataDirectory) + { + return [.. _repo.ListMarkdownFiles(dataDirectory) + .Where(path => EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path))) .Select(path => new EntryListItem( FileName: _repo.GetFileName(path), FilePath: _repo.GetFullPath(path)))]; @@ -31,6 +41,20 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ Entry: entry.ToDto()); } + public EntryTemplateLoadResult LoadTemplate(string filePath) + { + var normalizedPath = _repo.GetFullPath(filePath); + if (!_repo.FileExists(normalizedPath)) + throw new FileNotFoundException($"Template file not found: {normalizedPath}"); + + var fileName = _repo.GetFileName(normalizedPath); + if (!EntryFileNaming.IsTemplateFileName(fileName)) + throw new ArgumentException("Template file name must end with .template.md."); + + var rawContent = HtmlSanitizer.StripRichHtml(_repo.ReadFile(normalizedPath)); + return new EntryTemplateLoadResult(fileName, normalizedPath, rawContent); + } + public EntrySaveResult SaveEntry(EntrySavePayload payload, string defaultDataDirectory) { var targetPath = ResolveTargetPath(payload.FilePath, payload.FileName, defaultDataDirectory); @@ -69,6 +93,26 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ return new EntrySaveResult(targetPath); } + public EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload, string defaultDataDirectory) + { + ArgumentNullException.ThrowIfNull(payload); + if (string.IsNullOrWhiteSpace(payload.Name)) + throw new ArgumentException("Template name is required."); + + var directory = string.IsNullOrWhiteSpace(payload.DataDirectory) + ? defaultDataDirectory + : payload.DataDirectory; + var targetPath = ResolveTemplatePath(payload.FilePath, payload.Name, directory); + var fileName = _repo.GetFileName(targetPath); + if (!EntryFileNaming.IsTemplateFileName(fileName)) + throw new ArgumentException("Template file name must end with .template.md."); + + var sanitizedContent = HtmlSanitizer.StripRichHtml(payload.Content ?? ""); + _repo.EnsureDirectory(targetPath); + _repo.WriteFile(targetPath, sanitizedContent); + return new EntrySaveResult(targetPath); + } + public bool DeleteEntry(string filePath) { var normalizedPath = _repo.GetFullPath(filePath); @@ -78,6 +122,20 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ return true; } + public bool DeleteTemplate(string filePath) + { + var normalizedPath = _repo.GetFullPath(filePath); + if (!_repo.FileExists(normalizedPath)) + return false; + + var fileName = _repo.GetFileName(normalizedPath); + if (!EntryFileNaming.IsTemplateFileName(fileName)) + return false; + + _repo.DeleteFile(normalizedPath); + return true; + } + private string ResolveTargetPath(string? filePath, string? fileName, string defaultDataDirectory) { if (!string.IsNullOrWhiteSpace(filePath)) @@ -90,6 +148,15 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ return _repo.GetFullPath(Path.Combine(defaultDataDirectory, $"{name}.md")); } + private string ResolveTemplatePath(string? filePath, string templateName, string defaultDataDirectory) + { + if (!string.IsNullOrWhiteSpace(filePath)) + return _repo.GetFullPath(filePath); + + var name = SanitizeFileName(templateName); + return _repo.GetFullPath(Path.Combine(defaultDataDirectory, $"{name}{EntryFileNaming.TemplateSuffix}")); + } + private static string SanitizeFileName(string name) { var trimmed = name.Trim(); diff --git a/Journal.Core/Services/Entries/EntrySearchService.cs b/Journal.Core/Services/Entries/EntrySearchService.cs index a623512..b1df327 100644 --- a/Journal.Core/Services/Entries/EntrySearchService.cs +++ b/Journal.Core/Services/Entries/EntrySearchService.cs @@ -36,6 +36,9 @@ public class EntrySearchService : IEntrySearchService .OrderBy(Path.GetFileName, StringComparer.Ordinal)) { var fileName = Path.GetFileName(filePath); + if (EntryFileNaming.IsTemplateFileName(fileName)) + continue; + var fileStem = Path.GetFileNameWithoutExtension(filePath); var rawContent = File.ReadAllText(filePath); var entry = JournalParser.ParseJournalContent(rawContent, fileStem); diff --git a/Journal.Core/Services/Entries/IEntryFileService.cs b/Journal.Core/Services/Entries/IEntryFileService.cs index a8b8223..3983272 100644 --- a/Journal.Core/Services/Entries/IEntryFileService.cs +++ b/Journal.Core/Services/Entries/IEntryFileService.cs @@ -5,7 +5,11 @@ namespace Journal.Core.Services.Entries; public interface IEntryFileService { IReadOnlyList ListEntries(string dataDirectory); + IReadOnlyList ListTemplates(string dataDirectory); EntryLoadResult LoadEntry(string filePath); + EntryTemplateLoadResult LoadTemplate(string filePath); EntrySaveResult SaveEntry(EntrySavePayload payload, string defaultDataDirectory); + EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload, string defaultDataDirectory); bool DeleteEntry(string filePath); + bool DeleteTemplate(string filePath); } diff --git a/Journal.Core/Services/Sidecar/SidecarCli.cs b/Journal.Core/Services/Sidecar/SidecarCli.cs index 157e0dc..f7d82fd 100644 --- a/Journal.Core/Services/Sidecar/SidecarCli.cs +++ b/Journal.Core/Services/Sidecar/SidecarCli.cs @@ -118,7 +118,11 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe } var (_, dataDirectory) = ResolveDirectories(vaultOverride: null, options.DataDirectory); - if (!Directory.Exists(dataDirectory) || Directory.GetFiles(dataDirectory, "*.md").Length == 0) + var entryCount = Directory.Exists(dataDirectory) + ? Directory.GetFiles(dataDirectory, "*.md") + .Count(path => !EntryFileNaming.IsTemplateFileName(Path.GetFileName(path))) + : 0; + if (entryCount == 0) { Console.WriteLine("No decrypted journal entries found. Please load the vault first: journal vault load"); return 0; diff --git a/Journal.SmokeTests/Program.EntryTests.cs b/Journal.SmokeTests/Program.EntryTests.cs index 974f2cb..ad8da7d 100644 --- a/Journal.SmokeTests/Program.EntryTests.cs +++ b/Journal.SmokeTests/Program.EntryTests.cs @@ -190,6 +190,98 @@ hello world } } + static async Task TestEntryTemplatesCrudExcludesFromEntriesListAsync() + { + var root = Path.Combine(Path.GetTempPath(), "journal-template-smoke", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + try + { + File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "daily entry"); + + var entry = NewEntry(); + var saveRequest = JsonSerializer.Serialize(new + { + action = "templates.save", + payload = new + { + name = "Weekly Review", + content = "# Weekly Review\n\n## Wins\n- one", + dataDirectory = root + } + }); + var saveResponse = await entry.HandleCommandAsync(saveRequest); + using var saveDoc = JsonDocument.Parse(saveResponse); + Assert(saveDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.save."); + var templatePath = saveDoc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? ""; + Assert(templatePath.EndsWith(".template.md", StringComparison.OrdinalIgnoreCase), "Template file should end with .template.md."); + Assert(File.Exists(templatePath), "Template file should exist."); + + var listTemplatesRequest = JsonSerializer.Serialize(new + { + action = "templates.list", + payload = new + { + dataDirectory = root + } + }); + var listTemplatesResponse = await entry.HandleCommandAsync(listTemplatesRequest); + using var listTemplatesDoc = JsonDocument.Parse(listTemplatesResponse); + Assert(listTemplatesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.list."); + var templateItems = listTemplatesDoc.RootElement.GetProperty("data"); + Assert(templateItems.GetArrayLength() == 1, "Expected one template in templates.list."); + Assert(templateItems[0].GetProperty("FileName").GetString() == "Weekly Review.template.md", "Template file name mismatch."); + + var listEntriesRequest = JsonSerializer.Serialize(new + { + action = "entries.list", + payload = new + { + dataDirectory = root + } + }); + var listEntriesResponse = await entry.HandleCommandAsync(listEntriesRequest); + using var listEntriesDoc = JsonDocument.Parse(listEntriesResponse); + Assert(listEntriesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list."); + var entryItems = listEntriesDoc.RootElement.GetProperty("data"); + Assert(entryItems.GetArrayLength() == 1, "Expected entries.list to exclude template files."); + Assert(entryItems[0].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected only daily entry file in entries.list."); + + var loadTemplateRequest = JsonSerializer.Serialize(new + { + action = "templates.load", + payload = new + { + filePath = templatePath + } + }); + var loadTemplateResponse = await entry.HandleCommandAsync(loadTemplateRequest); + using var loadTemplateDoc = JsonDocument.Parse(loadTemplateResponse); + Assert(loadTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.load."); + var content = loadTemplateDoc.RootElement.GetProperty("data").GetProperty("Content").GetString() ?? ""; + Assert(content.Contains("## Wins", StringComparison.Ordinal), "Expected template content in templates.load result."); + + var deleteTemplateRequest = JsonSerializer.Serialize(new + { + action = "templates.delete", + payload = new + { + filePath = templatePath + } + }); + var deleteTemplateResponse = await entry.HandleCommandAsync(deleteTemplateRequest); + using var deleteTemplateDoc = JsonDocument.Parse(deleteTemplateResponse); + Assert(deleteTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.delete."); + Assert(deleteTemplateDoc.RootElement.GetProperty("data").GetBoolean(), "Expected templates.delete to return true."); + Assert(!File.Exists(templatePath), "Template file should be deleted."); + } + finally + { + if (Directory.Exists(root)) + Directory.Delete(root, recursive: true); + } + } + static async Task TestEntrySearchEntriesMatchesRawContentAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); diff --git a/Journal.SmokeTests/Program.cs b/Journal.SmokeTests/Program.cs index 2e1efc6..41a32bb 100644 --- a/Journal.SmokeTests/Program.cs +++ b/Journal.SmokeTests/Program.cs @@ -39,6 +39,7 @@ internal static partial class Program ("Entry entries.save writes and merges content", TestEntryEntriesSaveMergeAsync), ("Entry entries.load returns raw content payload", TestEntryEntriesLoadAsync), ("Entry entries.list returns markdown files", TestEntryEntriesListAsync), + ("Entry templates CRUD stores .template.md and entries.list excludes templates", TestEntryTemplatesCrudExcludesFromEntriesListAsync), ("Entry search.entries matches query against full raw content", TestEntrySearchEntriesMatchesRawContentAsync), ("Entry search.entries without query returns all markdown entries", TestEntrySearchEntriesWithoutQueryReturnsAllAsync), ("Entry search.entries applies date range filter", TestEntrySearchEntriesDateRangeFilterAsync),