feat(storage): B2b — settings/profile schema + templates SQLite persistence

Settings: 8 new fields (lastLaunchAt, reentryFreshStartUntil,
lastActiveProfileId, microStepGranularity, nudgeMode, nudgeDigestTimes,
sparklineRangeDays, energyLabels) with sane defaults; nudgesEnabled
auto-maps to nudgeMode on first load (true→immediate, false→off).

Profile: optional energyLabelsOverride field on the localStorage
Profile shape — when present, overrides the global energyLabels;
absent/null falls through to settings.energyLabels via the new
resolveEnergyLabels helper.

Templates: migration v17 adds the templates table (id PK, name,
sections JSON, created_at, updated_at). 5 storage CRUD functions
plus import_templates with idempotent duplicate-id handling. 5 Tauri
commands (list/create/update/delete/import) wired and registered.
Frontend store rewired to read from SQLite via list_templates_cmd;
one-time migration imports kon_templates localStorage and clears it
on success. Fresh installs only seed the new {Meeting notes, Daily
check-in} defaults. Existing user templates survive the migration
verbatim.

Test coverage: 5 new Rust tests (CRUD, import idempotency, atomic
failure, updated_at bump, missing-id delete is no-op).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 19:16:31 +01:00
parent 0111fc9e08
commit e4cbf62a20
10 changed files with 943 additions and 37 deletions

View File

@@ -3,7 +3,7 @@
import { onMount, onDestroy } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { settings, saveSettings, profiles, saveProfiles, templates, saveTemplates, page, addProfileTaskList, removeProfileTaskList } from "$lib/stores/page.svelte.js";
import { settings, saveSettings, profiles, saveProfiles, templates, createTemplate as createTemplateRow, updateTemplate as updateTemplateRow, deleteTemplate as deleteTemplateRow, page, addProfileTaskList, removeProfileTaskList } from "$lib/stores/page.svelte.js";
import Card from "$lib/components/Card.svelte";
import Toggle from "$lib/components/Toggle.svelte";
import SegmentedButton from "$lib/components/SegmentedButton.svelte";
@@ -932,21 +932,35 @@
return words ? words.split("\n").filter((w) => w.trim()).length : 0;
}
// --- Template management ---
function createTemplate() {
// --- Template management (B2b: SQLite-backed via createTemplateRow /
// updateTemplateRow / deleteTemplateRow). The inline section editor
// calls updateTemplateRow directly with the parsed sections array;
// the store is the source of truth for the rendered list, so UI
// updates flow through the awaited mutators.
async function createTemplate() {
if (!newTemplateName.trim()) return;
templates.push({ name: newTemplateName.trim(), sections: ["Section 1", "Section 2", "Section 3"] });
saveTemplates();
await createTemplateRow(newTemplateName.trim(), ["Section 1", "Section 2", "Section 3"]);
newTemplateName = "";
showNewTemplate = false;
editingTemplate = templates.length - 1;
}
function deleteTemplate(index) {
templates.splice(index, 1);
saveTemplates();
async function deleteTemplate(index) {
const template = templates[index];
if (!template) return;
await deleteTemplateRow(template.id);
editingTemplate = -1;
}
function persistTemplateSections(template, value) {
const sections = value.split("\n").filter((s) => s.trim());
// Optimistic local mutation so the textarea stays in sync with what
// the user typed; updateTemplateRow then writes through to SQLite
// and rewrites this entry from the returned row (which includes the
// bumped updated_at).
template.sections = sections;
void updateTemplateRow(template.id, { sections });
}
</script>
<div class="flex flex-col h-full overflow-y-auto animate-fade-in">
@@ -2025,7 +2039,7 @@
focus:outline-none focus:border-accent"
placeholder="One section per line..."
value={template.sections.join("\n")}
oninput={(e) => { template.sections = e.target.value.split("\n").filter((s) => s.trim()); saveTemplates(); }}
oninput={(e) => persistTemplateSections(template, e.target.value)}
data-no-transition
></textarea>
</div>