Add YAML-based store configs with bidirectional sync

Stores can now be defined as YAML files in the stores/ directory.
On startup, YAML files are synced into the database. Changes made
via the admin UI are written back to YAML files automatically.

- Add store-sync service (load from files, export to files, write-back)
- Add /api/stores/sync and /api/stores/export endpoints
- Add Sync/Export buttons to admin UI
- Mount stores/ volume in Docker
- Include example store config template

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
mariosemes
2026-03-26 21:06:29 +01:00
parent 8ce5ba62dc
commit 26467a6368
11 changed files with 374 additions and 10 deletions

View File

@@ -42,6 +42,14 @@ export function deleteStore(id: number) {
return api<void>(`/api/stores/${id}`, { method: 'DELETE' });
}
export function exportStores() {
return api<{ exported: number; directory: string }>('/api/stores/export', { method: 'POST' });
}
export function syncStores() {
return api<{ created: number; updated: number; errors: string[] }>('/api/stores/sync', { method: 'POST' });
}
export function getCategories() {
return api<any[]>('/api/categories');
}

View File

@@ -1,9 +1,10 @@
<script>
import { getStores, toggleStore, deleteStore } from '$lib/api';
import { getStores, toggleStore, deleteStore, exportStores, syncStores } from '$lib/api';
import { onMount } from 'svelte';
let stores = $state([]);
let loading = $state(true);
let syncMessage = $state('');
onMount(async () => {
stores = await getStores();
@@ -20,19 +21,56 @@
await deleteStore(id);
stores = stores.filter((s) => s.id !== id);
}
async function handleSync() {
syncMessage = '';
const result = await syncStores();
stores = await getStores();
const parts = [];
if (result.created) parts.push(`${result.created} created`);
if (result.updated) parts.push(`${result.updated} updated`);
if (result.errors.length) parts.push(`${result.errors.length} errors`);
syncMessage = parts.length ? `Sync: ${parts.join(', ')}` : 'No changes found';
if (result.errors.length) syncMessage += ' — ' + result.errors.join('; ');
setTimeout(() => syncMessage = '', 5000);
}
async function handleExport() {
const result = await exportStores();
syncMessage = `Exported ${result.exported} stores to ${result.directory}/`;
setTimeout(() => syncMessage = '', 5000);
}
</script>
<div class="max-w-7xl mx-auto px-4 py-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Store Management</h1>
<a
href="/admin/stores/new"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
Add Store
</a>
<div class="flex gap-2">
<button onclick={handleSync}
class="border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
title="Reload store configs from YAML files in the stores/ directory">
Sync from Files
</button>
<button onclick={handleExport}
class="border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
title="Export all store configs to YAML files">
Export to Files
</button>
<a
href="/admin/stores/new"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
Add Store
</a>
</div>
</div>
{#if syncMessage}
<div class="mb-4 px-4 py-2.5 rounded-lg bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 text-sm">
{syncMessage}
</div>
{/if}
{#if loading}
<div class="animate-pulse space-y-3">
{#each Array(5) as _}