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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user