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>
This commit is contained in:
67
frontend/vite.config.ts
Normal file
67
frontend/vite.config.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
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);
|
||||
assets.push('/', '/manifest.webmanifest', '/icon-192.png', '/icon-512.png');
|
||||
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',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user