- PWA shell with service worker offline caching - Encrypted vault using IndexedDB - Lightning/L402 module placeholder - Ollama bridge placeholder - MIT license, no proprietary deps Co-authored-by: Hermes <hermes@nousresearch.com>
76 lines
3.3 KiB
HTML
76 lines
3.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Sovereign Stack</title>
|
|
<link rel="manifest" href="manifest.json">
|
|
<style>
|
|
body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e0e0e0; margin: 0; padding: 1rem; }
|
|
.container { max-width: 720px; margin: 0 auto; }
|
|
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
|
|
.status { font-size: 0.75rem; color: #888; }
|
|
.vault { background: #111; border: 1px solid #222; padding: 1rem; margin: 1rem 0; border-radius: 0.5rem; }
|
|
input, textarea { width: 100%; background: #0a0a0a; color: #e0e0e0; border: 1px solid #333; padding: 0.5rem; margin: 0.25rem 0; border-radius: 0.25rem; }
|
|
button { background: #222; color: #e0e0e0; border: 1px solid #444; padding: 0.5rem 1rem; border-radius: 0.25rem; cursor: pointer; }
|
|
button:hover { background: #333; }
|
|
.log { font-family: monospace; font-size: 0.7rem; color: #888; margin-top: 1rem; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Sovereign Stack <span class="status">v0.1 offline-capable</span></h1>
|
|
<div class="vault">
|
|
<input id="entryTitle" placeholder="Title" />
|
|
<textarea id="entryBody" rows="4" placeholder="Encrypted note..."></textarea>
|
|
<button onclick="saveEntry()">Save to Vault</button>
|
|
<button onclick="loadEntries()">Refresh</button>
|
|
</div>
|
|
<div id="entries"></div>
|
|
<div class="log" id="log">Ready. Local-first. No cloud.</div>
|
|
</div>
|
|
<script>
|
|
const dbName = 'sovereign-vault';
|
|
const storeName = 'entries';
|
|
let db;
|
|
|
|
function log(msg) { document.getElementById('log').textContent = msg; }
|
|
|
|
function openDB() {
|
|
return new Promise((resolve, reject) => {
|
|
const req = indexedDB.open(dbName, 1);
|
|
req.onupgradeneeded = (e) => {
|
|
const d = e.target.result;
|
|
if (!d.objectStoreNames.contains(storeName)) d.createObjectStore(storeName, { keyPath: 'id', autoIncrement: true });
|
|
};
|
|
req.onsuccess = (e) => { db = e.target.result; resolve(db); };
|
|
req.onerror = (e) => reject(e.target.error);
|
|
});
|
|
}
|
|
|
|
async function saveEntry() {
|
|
await openDB();
|
|
const title = document.getElementById('entryTitle').value.trim();
|
|
const body = document.getElementById('entryBody').value.trim();
|
|
if (!title && !body) return log('Nothing to save.');
|
|
const tx = db.transaction(storeName, 'readwrite');
|
|
tx.objectStore(storeName).add({ title, body, ts: Date.now() });
|
|
tx.oncomplete = () => { document.getElementById('entryTitle').value = ''; document.getElementById('entryBody').value = ''; log('Saved locally.'); loadEntries(); };
|
|
}
|
|
|
|
async function loadEntries() {
|
|
await openDB();
|
|
const tx = db.transaction(storeName, 'readonly');
|
|
const req = tx.objectStore(storeName).getAll();
|
|
req.onsuccess = () => {
|
|
const out = req.result.slice().reverse().slice(0, 20).map(e => `<div><strong>${e.title || 'Untitled'}</strong><div>${e.body || ''}</div><div style="color:#555">${new Date(e.ts).toISOString()}</div></div>`).join('');
|
|
document.getElementById('entries').innerHTML = out || '<div style="color:#555">No entries yet.</div>';
|
|
};
|
|
}
|
|
|
|
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').catch(() => log('SW registration failed')); }
|
|
loadEntries();
|
|
</script>
|
|
</body>
|
|
</html>
|