Implement Tefter per SPEC.md: NV-style notes with server, sync, PWA, desktop
- frontend/: Svelte 4 + TS + Vite app — omnibar (search-and-create), ranked
diacritic-insensitive filtering, CodeMirror 6 markdown editor with 400ms
autosave, marked+DOMPurify preview with [[wiki-links]], tag filter, undo
toast, light/dark, narrow-screen stacked layout, IndexedDB store, pull-then-
push sync engine with conflict handling, versioned cache-first service
worker + manifest (installable PWA)
- server/: tefterd — Go stdlib HTTP + modernc.org/sqlite, /api/v1 sync API
(changes/batch/health/import), SHA-256 hashed bearer token, LWW-with-
conflict-copies push rules, subcommands: init, token rotate, import
simplenote, compact, backup (VACUUM INTO); embeds the frontend bundle
- desktop/: Wails v2 shell — single instance, hide-to-tray (fyne systray),
global Ctrl+Shift+Space hotkey, quit-on-close flag
- Simplenote import (CLI + web upload), idempotent via source_id dedupe;
verified against a real 242-note export
- Makefile (frontend/server/server-all/desktop/test), GitHub release workflow,
README with systemd/Caddy/backup docs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 07:16:01 +02:00
|
|
|
import { defineConfig, type Plugin } from 'vite';
|
|
|
|
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
|
|
|
import { createHash } from 'node:crypto';
|
|
|
|
|
|
|
|
|
|
const version = process.env.TEFTER_VERSION || 'dev';
|
|
|
|
|
|
|
|
|
|
// Emits sw.js at build time with the precache list of all emitted assets,
|
|
|
|
|
// versioned by a hash of the file names (which themselves carry content hashes).
|
|
|
|
|
function serviceWorker(): Plugin {
|
|
|
|
|
return {
|
|
|
|
|
name: 'tefter-sw',
|
|
|
|
|
apply: 'build',
|
|
|
|
|
generateBundle(_opts, bundle) {
|
|
|
|
|
const assets = Object.keys(bundle)
|
|
|
|
|
.filter((f) => !f.endsWith('.map'))
|
|
|
|
|
.map((f) => '/' + f);
|
2026-07-12 07:24:39 +02:00
|
|
|
assets.push('/', '/manifest.webmanifest', '/icon.svg', '/icon-192.png', '/icon-512.png', '/icon-512-maskable.png');
|
Implement Tefter per SPEC.md: NV-style notes with server, sync, PWA, desktop
- frontend/: Svelte 4 + TS + Vite app — omnibar (search-and-create), ranked
diacritic-insensitive filtering, CodeMirror 6 markdown editor with 400ms
autosave, marked+DOMPurify preview with [[wiki-links]], tag filter, undo
toast, light/dark, narrow-screen stacked layout, IndexedDB store, pull-then-
push sync engine with conflict handling, versioned cache-first service
worker + manifest (installable PWA)
- server/: tefterd — Go stdlib HTTP + modernc.org/sqlite, /api/v1 sync API
(changes/batch/health/import), SHA-256 hashed bearer token, LWW-with-
conflict-copies push rules, subcommands: init, token rotate, import
simplenote, compact, backup (VACUUM INTO); embeds the frontend bundle
- desktop/: Wails v2 shell — single instance, hide-to-tray (fyne systray),
global Ctrl+Shift+Space hotkey, quit-on-close flag
- Simplenote import (CLI + web upload), idempotent via source_id dedupe;
verified against a real 242-note export
- Makefile (frontend/server/server-all/desktop/test), GitHub release workflow,
README with systemd/Caddy/backup docs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 07:16:01 +02:00
|
|
|
const hash = createHash('sha256').update(assets.join(',') + version).digest('hex').slice(0, 12);
|
|
|
|
|
const sw = `// generated by vite.config.ts — do not edit
|
|
|
|
|
const CACHE = 'tefter-${'${'}HASH}';
|
|
|
|
|
const ASSETS = ${'${'}ASSETS};
|
|
|
|
|
|
|
|
|
|
self.addEventListener('install', (e) => {
|
|
|
|
|
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(ASSETS)).then(() => self.skipWaiting()));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
self.addEventListener('activate', (e) => {
|
|
|
|
|
e.waitUntil(
|
|
|
|
|
caches.keys()
|
|
|
|
|
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
|
|
|
|
.then(() => self.clients.claim())
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
self.addEventListener('fetch', (e) => {
|
|
|
|
|
const url = new URL(e.request.url);
|
|
|
|
|
if (e.request.method !== 'GET' || url.origin !== location.origin) return;
|
|
|
|
|
if (url.pathname.startsWith('/api/')) return; // network-only; sync engine handles offline
|
|
|
|
|
e.respondWith(
|
|
|
|
|
caches.match(url.pathname === '/' || url.pathname === '/index.html' ? '/' : e.request).then(
|
|
|
|
|
(hit) => hit || fetch(e.request)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
`
|
|
|
|
|
.replace('${HASH}', hash)
|
|
|
|
|
.replace('${ASSETS}', JSON.stringify(assets));
|
|
|
|
|
this.emitFile({ type: 'asset', fileName: 'sw.js', source: sw });
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default defineConfig({
|
|
|
|
|
plugins: [svelte(), serviceWorker()],
|
|
|
|
|
define: {
|
|
|
|
|
__TEFTER_VERSION__: JSON.stringify(version),
|
|
|
|
|
},
|
|
|
|
|
build: {
|
|
|
|
|
target: 'es2020',
|
|
|
|
|
sourcemap: false,
|
|
|
|
|
},
|
|
|
|
|
server: {
|
|
|
|
|
proxy: {
|
|
|
|
|
'/api': 'http://127.0.0.1:8420',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|