Refactor project name from "Voice KB" to "Faerro KB" across all files and update related configurations

This commit is contained in:
2026-09-26 16:54:29 +02:00
parent 3034d76682
commit 13fcb70a27
10 changed files with 102 additions and 50 deletions
+50 -22
View File
@@ -1,9 +1,9 @@
const DB_NAME = 'voice-kb-local';
const DB_NAME = 'faerro-kb-local';
const STORE_NAME = 'recordings';
const MAX_RECORDING_MS = 30 * 60 * 1000;
const elements = Object.fromEntries([
'record-button', 'record-title', 'record-help', 'record-time', 'record-meter-fill', 'capture-message',
'queue-list', 'queue-empty', 'queue-count', 'sync-button', 'archive-list', 'archive-empty', 'search-input',
'queue-list', 'queue-empty', 'queue-count', 'sync-button', 'archive-list', 'archive-empty',
'network-state', 'date-label', 'storage-status',
].map((id) => [id, document.getElementById(id)]));
@@ -13,7 +13,6 @@ let activeStream;
let recordingChunks = [];
let recordingStartedAt = 0;
let recordingTimer;
let searchTimer;
let syncing = false;
function openDatabase() {
@@ -138,6 +137,7 @@ async function syncQueue() {
if (syncing || !navigator.onLine) return;
syncing = true;
elements['sync-button'].disabled = true;
elements['queue-list'].querySelectorAll('.delete-note').forEach((button) => { button.disabled = true; });
try {
const recordings = await getQueued();
for (const recording of recordings.sort((a, b) => a.createdAtMs - b.createdAtMs)) {
@@ -159,7 +159,7 @@ async function syncQueue() {
syncing = false;
elements['sync-button'].disabled = false;
await renderQueue();
await loadArchive(elements['search-input'].value.trim());
await loadAwaitingNotes();
}
}
@@ -191,18 +191,41 @@ async function renderQueue() {
const meta = document.createElement('div');
meta.className = 'item-meta';
meta.textContent = `${prettyDate(recording.createdAt)} · ${formatDuration(recording.durationMs)}`;
const remove = createDeleteButton(`Delete queued note ${recording.filename}`);
remove.disabled = syncing;
remove.addEventListener('click', async () => {
if (syncing) return;
remove.disabled = true;
try {
await deleteQueued(recording.id);
await renderQueue();
} catch {
setMessage('Could not remove the queued recording.', true);
remove.disabled = false;
}
});
top.append(name, status);
body.append(top, meta);
item.append(icon, body);
item.append(icon, body, remove);
elements['queue-list'].append(item);
}
}
function createDeleteButton(label) {
const button = document.createElement('button');
button.className = 'delete-note';
button.type = 'button';
button.textContent = '×';
button.title = label;
button.setAttribute('aria-label', label);
return button;
}
function renderArchive(notes) {
elements['archive-list'].replaceChildren();
elements['archive-empty'].hidden = notes.length > 0;
if (!navigator.onLine) elements['archive-empty'].textContent = 'Reconnect to browse or search stored notes.';
else elements['archive-empty'].textContent = elements['search-input'].value.trim() ? 'No matching transcripts.' : 'Your stored notes will appear here.';
if (!navigator.onLine) elements['archive-empty'].textContent = 'Reconnect to view notes awaiting transcription.';
else elements['archive-empty'].textContent = 'No notes awaiting transcription.';
for (const note of notes) {
const item = document.createElement('article');
item.className = 'archive-item';
@@ -230,8 +253,7 @@ function renderArchive(notes) {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transcript: transcript.value }),
});
if (!response.ok) throw new Error('Save failed');
save.textContent = 'SAVED';
await loadArchive(elements['search-input'].value.trim());
await loadAwaitingNotes();
} catch {
save.textContent = 'RETRY SAVE';
} finally {
@@ -240,22 +262,32 @@ function renderArchive(notes) {
});
const status = document.createElement('div');
status.className = 'archive-status';
status.textContent = note.status.replaceAll('_', ' ').toUpperCase();
status.textContent = 'AWAITING TRANSCRIPTION';
const remove = createDeleteButton(`Delete note ${note.filename}`);
remove.addEventListener('click', async () => {
remove.disabled = true;
try {
const response = await fetch(`/api/notes/${encodeURIComponent(note.id)}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Delete failed');
await loadAwaitingNotes();
} catch {
remove.disabled = false;
setMessage('Could not delete this note. Please try again.', true);
}
});
body.append(name, transcript, save, status);
item.append(date, body);
item.append(date, body, remove);
elements['archive-list'].append(item);
}
}
async function loadArchive(query = '') {
async function loadAwaitingNotes() {
if (!navigator.onLine) {
renderArchive([]);
return;
}
try {
const url = new URL('/api/notes', window.location.origin);
if (query) url.searchParams.set('q', query);
const response = await fetch(url);
const response = await fetch('/api/notes?status=awaiting_transcription');
if (!response.ok) throw new Error('Archive unavailable');
renderArchive(await response.json());
} catch {
@@ -267,17 +299,13 @@ async function loadArchive(query = '') {
elements['record-button'].addEventListener('click', () => mediaRecorder?.state === 'recording' ? stopRecording() : startRecording());
elements['sync-button'].addEventListener('click', syncQueue);
elements['search-input'].addEventListener('input', () => {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => loadArchive(elements['search-input'].value.trim()), 250);
});
window.addEventListener('online', () => { updateNetwork(); syncQueue(); loadArchive(elements['search-input'].value.trim()); });
window.addEventListener('online', () => { updateNetwork(); syncQueue(); loadAwaitingNotes(); });
window.addEventListener('offline', updateNetwork);
window.addEventListener('focus', () => { updateNetwork(); syncQueue(); loadArchive(elements['search-input'].value.trim()); });
window.addEventListener('focus', () => { updateNetwork(); syncQueue(); loadAwaitingNotes(); });
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') syncQueue(); });
elements['date-label'].textContent = new Intl.DateTimeFormat(undefined, { weekday: 'short', month: 'short', day: 'numeric' }).format(new Date()).toUpperCase();
updateNetwork();
renderQueue().then(syncQueue).catch(() => setMessage('Browser storage is unavailable. Check Safari storage settings.', true));
loadArchive();
loadAwaitingNotes();
if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js').catch(() => {});
+5 -8
View File
@@ -6,21 +6,19 @@
<meta name="theme-color" content="#f5f3ed" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title>Voice KB</title>
<title>Faerro KB</title>
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<header class="topbar">
<a class="brand" href="/" aria-label="Voice KB home"><span class="brand-mark">V</span><span>voice <b>kb</b></span></a>
<a class="brand" href="/" aria-label="Faerro KB"><span class="brand-mark">V</span><span>voice <b>kb</b></span></a>
<span id="network-state" class="network-state"><i></i> Checking</span>
</header>
<main>
<section class="capture" aria-labelledby="capture-title">
<div class="section-kicker"><span>PERSONAL ARCHIVE</span><span id="date-label"></span></div>
<h1 id="capture-title">Catch the thought<br /><em>before it goes.</em></h1>
<p class="intro">Private voice notes, kept on your own server.</p>
<div class="recorder-row">
<button id="record-button" class="record-button" type="button" aria-label="Start recording">
<span class="record-dot"></span>
@@ -46,15 +44,14 @@
<section class="archive-section" aria-labelledby="archive-title">
<div class="section-heading archive-heading">
<div><span class="section-kicker">YOUR LIBRARY</span><h2 id="archive-title">Recent notes</h2></div>
<label class="search-box"><span aria-hidden="true">⌕</span><input id="search-input" type="search" placeholder="Search transcripts" autocomplete="off" /></label>
<div><span class="section-kicker">SERVER STORED</span><h2 id="archive-title">Awaiting transcription</h2></div>
</div>
<div id="archive-list" class="archive-list"></div>
<p id="archive-empty" class="empty-state">Your stored notes will appear here.</p>
<p id="archive-empty" class="empty-state">Notes awaiting transcription will appear here.</p>
</section>
</main>
<footer><span>LOCAL FIRST</span><span id="storage-status">Audio stays on this device until uploaded.</span></footer>
<footer><p>Faerro KB - Copyright Andrea Ferronato © 2026</p></footer>
<script src="/app.js" defer></script>
</body>
</html>
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "Voice KB",
"short_name": "Voice KB",
"name": "Faerro KB",
"short_name": "Faerro KB",
"start_url": "/",
"display": "standalone",
"background_color": "#f5f3ed",
+2 -4
View File
@@ -56,12 +56,10 @@ h2 { margin: 8px 0 0; font: 500 25px/1.1 var(--serif); }
.item-name { font-size: 13px; font-weight: 600; overflow-wrap: anywhere; }
.item-meta { margin-top: 5px; color: var(--muted); font: 10px var(--mono); }
.item-status { color: var(--moss-dark); font: 10px var(--mono); white-space: nowrap; }
.delete-note { width: 32px; height: 32px; flex: 0 0 auto; border: 1px solid var(--line); background: transparent; color: var(--coral); font: 20px/1 var(--sans); cursor: pointer; }
.delete-note:disabled { opacity: .45; cursor: wait; }
.empty-state { margin: 18px 0 0; color: var(--muted); font-size: 12px; }
.archive-heading { align-items: end; }
.search-box { width: min(280px, 52vw); height: 38px; display: flex; align-items: center; gap: 9px; padding: 0 11px; border: 1px solid var(--line); background: #fffefa; color: var(--muted); }
.search-box span { font-size: 22px; line-height: 1; }
.search-box input { min-width: 0; width: 100%; border: 0; outline: 0; background: transparent; color: var(--ink); font: 12px var(--sans); }
.search-box input::placeholder { color: #899088; }
.archive-item { padding: 16px 0; }
.archive-date { width: 72px; flex: 0 0 auto; color: var(--muted); font: 10px/1.5 var(--mono); }
.archive-transcript { width: 100%; min-height: 56px; resize: vertical; border: 0; border-bottom: 1px solid #e2e3dc; outline: 0; padding: 0 0 9px; background: transparent; color: var(--ink); font: 13px/1.55 var(--sans); }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'voice-kb-shell-v1';
const CACHE_NAME = 'faerro-kb-shell-v1';
const APP_SHELL = ['/', '/styles.css', '/app.js', '/manifest.webmanifest'];
self.addEventListener('install', (event) => {