Enhance documentation, update release process, and improve UI elements

This commit is contained in:
2026-09-26 17:27:21 +02:00
parent c8809e56b2
commit 3f4fccdba2
10 changed files with 80 additions and 23 deletions
+1
View File
@@ -21,6 +21,7 @@ Faerro KB is a self-hosted voice-note inbox designed for an iPhone-installed web
- Keep changes scoped. Inspect the owning code path, add or update focused tests for behavior changes, and run the narrowest relevant checks available. - Keep changes scoped. Inspect the owning code path, add or update focused tests for behavior changes, and run the narrowest relevant checks available.
- Update README/UI documentation when behavior, deployment, security, backup, or platform limitations change. Do not claim a capability that iOS or browsers cannot guarantee. - Update README/UI documentation when behavior, deployment, security, backup, or platform limitations change. Do not claim a capability that iOS or browsers cannot guarantee.
- Preserve user data and existing local changes. Do not perform destructive migrations or remove stored recordings without explicit confirmation and a recovery path. - Preserve user data and existing local changes. Do not perform destructive migrations or remove stored recordings without explicit confirmation and a recovery path.
- At the end of each chat, check whether it introduced durable project requirements, decisions, workflows, or corrections missing from these instructions or a relevant skill. Update the narrowest relevant guidance file when the change is clearly reusable and within the user's authorized scope; otherwise note the gap and ask before expanding scope. Do not record transient task details or sensitive information.
## Releases ## Releases
+2 -2
View File
@@ -10,8 +10,8 @@ Use this skill when asked to tag or publish a Faerro KB image. The release proce
## Procedure ## Procedure
1. Check the worktree and identify the exact commit being released. Do not tag unrelated or unreviewed changes. 1. Check the worktree and identify the exact commit being released. Do not tag unrelated or unreviewed changes.
2. Use the release version explicitly supplied by the user. If no version was supplied and no approved version is established in the task, ask instead of guessing. Tags must be exact `MAJOR.MINOR.PATCH`, optionally prefixed with `v`. 2. The script selects the next patch version by default. Use `--minor` or `--major` only when the user requests that increment; lower components are reset. Tags must be exact `MAJOR.MINOR.PATCH`.
3. Check that the chosen tag is unused and attach it to the intended commit. 3. The script creates an unused local tag on the checked-out commit; do not push Git tags to a remote unless requested.
4. Confirm Docker is available. For non-interactive authentication, put `GITEA_USERNAME` and a package-write `GITEA_TOKEN` in the ignored `.env` file. The script parses only these keys and sends the token through Docker's `--password-stdin`; never put credentials in tracked files, command-line arguments, or chat. Without these settings it relies on Docker's existing login credentials. 4. Confirm Docker is available. For non-interactive authentication, put `GITEA_USERNAME` and a package-write `GITEA_TOKEN` in the ignored `.env` file. The script parses only these keys and sends the token through Docker's `--password-stdin`; never put credentials in tracked files, command-line arguments, or chat. Without these settings it relies on Docker's existing login credentials.
5. Run `./scripts/publish-image.sh` from the repository. It builds, logs in when `.env` credentials are set, and pushes both the versioned tag and `latest`. 5. Run `./scripts/publish-image.sh` from the repository. It builds, logs in when `.env` credentials are set, and pushes both the versioned tag and `latest`.
6. Report the tagged commit, image tags, and actual build/push result. A successful local build is not a successful release if either registry push fails. 6. Report the tagged commit, image tags, and actual build/push result. A successful local build is not a successful release if either registry push fails.
+2 -3
View File
@@ -10,16 +10,15 @@ docker compose up --build
## Releases ## Releases
Git tags are the source of truth for release versions. From the repository root, create a semantic version tag on the commit to release, then run the publish script: Git tags are the source of truth for release versions. From the repository root, run the publish script to create the next patch tag and publish it:
```sh ```sh
cp .env.example .env cp .env.example .env
# Set GITEA_USERNAME and GITEA_TOKEN in .env, using a Gitea package-write token. # Set GITEA_USERNAME and GITEA_TOKEN in .env, using a Gitea package-write token.
git tag 0.0.2
./scripts/publish-image.sh ./scripts/publish-image.sh
``` ```
The script requires Docker and Git, and publishes both the versioned image (for example, `gitea.faerro.it/andrea/faerro-kb:0.0.2`) and `latest`. Tags may be plain versions or prefixed with `v`. `.env` is excluded from Git; the script reads the Gitea credentials from it and passes the token to Docker through standard input. If those credentials are unset, the script uses Docker's existing login credentials. Pass `--minor` to increment the minor version and reset patch to zero, or `--major` to increment the major version and reset minor and patch to zero. The options cannot be combined. The script requires at least one existing `MAJOR.MINOR.PATCH` Git tag, creates the next local tag on the checked-out commit, and publishes both the versioned image and `latest`. `.env` is excluded from Git; the script reads the Gitea credentials from it and passes the token to Docker through standard input. If those credentials are unset, the script uses Docker's existing login credentials.
Open `http://localhost` for a desktop smoke test. Set `HTTP_PORT` to publish the app on a different host port (for example, `HTTP_PORT=8000 docker compose up`). For iPhone use, serve the app over HTTPS through your VPN/reverse proxy. iOS only grants microphone access in a secure context. Keep the service reachable only through your VPN and firewall; this first version has no login layer. Open `http://localhost` for a desktop smoke test. Set `HTTP_PORT` to publish the app on a different host port (for example, `HTTP_PORT=8000 docker compose up`). For iPhone use, serve the app over HTTPS through your VPN/reverse proxy. iOS only grants microphone access in a secure context. Keep the service reachable only through your VPN and firewall; this first version has no login layer.
+1 -2
View File
@@ -4,7 +4,7 @@ const MAX_RECORDING_MS = 30 * 60 * 1000;
const elements = Object.fromEntries([ const elements = Object.fromEntries([
'record-button', 'record-title', 'record-help', 'record-time', 'record-meter-fill', 'capture-message', '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', 'queue-list', 'queue-empty', 'queue-count', 'sync-button', 'archive-list', 'archive-empty',
'network-state', 'date-label', 'storage-status', 'network-state', 'storage-status',
].map((id) => [id, document.getElementById(id)])); ].map((id) => [id, document.getElementById(id)]));
let databasePromise; let databasePromise;
@@ -304,7 +304,6 @@ window.addEventListener('offline', updateNetwork);
window.addEventListener('focus', () => { updateNetwork(); syncQueue(); loadAwaitingNotes(); }); window.addEventListener('focus', () => { updateNetwork(); syncQueue(); loadAwaitingNotes(); });
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') syncQueue(); }); 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(); updateNetwork();
renderQueue().then(syncQueue).catch(() => setMessage('Browser storage is unavailable. Check Safari storage settings.', true)); renderQueue().then(syncQueue).catch(() => setMessage('Browser storage is unavailable. Check Safari storage settings.', true));
loadAwaitingNotes(); loadAwaitingNotes();
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="32" fill="#355a43" />
<text x="32" y="45" fill="#fffefa" font-family="Georgia, serif" font-size="40" text-anchor="middle">V</text>
</svg>

After

Width:  |  Height:  |  Size: 229 B

+3 -3
View File
@@ -7,24 +7,24 @@
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title>Faerro KB</title> <title>Faerro KB</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
<a class="brand" href="/" aria-label="Faerro KB"><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>Faerro <b>KB</b></span></a>
<span id="network-state" class="network-state"><i></i> Checking</span> <span id="network-state" class="network-state"><i></i> Checking</span>
</header> </header>
<main> <main>
<section class="capture" aria-labelledby="capture-title"> <section class="capture" aria-labelledby="capture-title">
<div class="section-kicker"><span>PERSONAL ARCHIVE</span><span id="date-label"></span></div>
<div class="recorder-row"> <div class="recorder-row">
<button id="record-button" class="record-button" type="button" aria-label="Start recording"> <button id="record-button" class="record-button" type="button" aria-label="Start recording">
<span class="record-dot"></span> <span class="record-dot"></span>
</button> </button>
<div class="record-copy"> <div class="record-copy">
<strong id="record-title">Ready when you are</strong> <strong id="record-title">Ready</strong>
<span id="record-help">Tap to record a voice note</span> <span id="record-help">Tap to record a voice note</span>
</div> </div>
<time id="record-time">00:00</time> <time id="record-time">00:00</time>
+8 -1
View File
@@ -5,5 +5,12 @@
"display": "standalone", "display": "standalone",
"background_color": "#f5f3ed", "background_color": "#f5f3ed",
"theme_color": "#f5f3ed", "theme_color": "#f5f3ed",
"icons": [] "icons": [
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
}
]
} }
+6 -6
View File
@@ -28,7 +28,7 @@ main { max-width: 1040px; margin: auto; padding: 0 24px; }
h1 { margin: 24px 0 11px; font: 500 clamp(42px, 7vw, 72px)/0.99 var(--serif); letter-spacing: 0; } h1 { margin: 24px 0 11px; font: 500 clamp(42px, 7vw, 72px)/0.99 var(--serif); letter-spacing: 0; }
h1 em { color: var(--moss); font-weight: 500; } h1 em { color: var(--moss); font-weight: 500; }
.intro { color: #606b63; margin: 0 0 28px; font-size: 14px; } .intro { color: #606b63; margin: 0 0 28px; font-size: 14px; }
.recorder-row { max-width: 590px; min-height: 78px; padding: 14px 18px 14px 14px; display: flex; align-items: center; gap: 16px; background: #fffefa; border: 1px solid var(--line); } .recorder-row { max-width: 590px; min-height: 78px; margin-inline: auto; padding: 14px 18px 14px 14px; display: flex; align-items: center; gap: 16px; background: #fffefa; border: 1px solid var(--line); }
.record-button { width: 50px; height: 50px; border: 0; border-radius: 50%; background: var(--coral); display: grid; place-items: center; cursor: pointer; flex: 0 0 auto; transition: background .2s ease, transform .2s ease; } .record-button { width: 50px; height: 50px; border: 0; border-radius: 50%; background: var(--coral); display: grid; place-items: center; cursor: pointer; flex: 0 0 auto; transition: background .2s ease, transform .2s ease; }
.record-button:hover { transform: scale(1.04); } .record-button:hover { transform: scale(1.04); }
.record-button.recording { background: var(--ink); } .record-button.recording { background: var(--ink); }
@@ -38,15 +38,15 @@ h1 em { color: var(--moss); font-weight: 500; }
.record-copy strong { font-size: 14px; font-weight: 600; } .record-copy strong { font-size: 14px; font-weight: 600; }
.record-copy span { color: var(--muted); font-size: 12px; } .record-copy span { color: var(--muted); font-size: 12px; }
#record-time { color: var(--muted); font: 12px var(--mono); font-variant-numeric: tabular-nums; } #record-time { color: var(--muted); font: 12px var(--mono); font-variant-numeric: tabular-nums; }
.record-meter { height: 2px; max-width: 590px; background: #e4e3dc; } .record-meter { height: 2px; max-width: 590px; margin-inline: auto; background: #e4e3dc; }
.record-meter span { display: block; height: 100%; width: 0; background: var(--coral); transition: width .25s linear; } .record-meter span { display: block; height: 100%; width: 0; background: var(--coral); transition: width .25s linear; }
.capture-message { height: 20px; max-width: 590px; padding-top: 6px; color: var(--muted); font-size: 11px; } .capture-message { height: 20px; max-width: 590px; margin-inline: auto; padding-top: 6px; color: var(--muted); font-size: 11px; text-align: center; }
.queue-section, .archive-section { padding: 30px 0 34px; border-bottom: 1px solid var(--line); } .queue-section, .archive-section { padding: 30px 0 34px; border-bottom: 1px solid var(--line); }
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 18px; } .section-heading { position: relative; display: flex; align-items: center; justify-content: center; gap: 18px; text-align: center; }
.section-heading .section-kicker { justify-content: flex-start; } .section-heading .section-kicker { justify-content: center; }
h2 { margin: 8px 0 0; font: 500 25px/1.1 var(--serif); } h2 { margin: 8px 0 0; font: 500 25px/1.1 var(--serif); }
#queue-count { display: inline-grid; place-items: center; width: 23px; height: 23px; margin-left: 5px; border-radius: 50%; background: #e6e7df; color: var(--moss-dark); font: 11px var(--mono); vertical-align: 3px; } #queue-count { display: inline-grid; place-items: center; width: 23px; height: 23px; margin-left: 5px; border-radius: 50%; background: #e6e7df; color: var(--moss-dark); font: 11px var(--mono); vertical-align: 3px; }
.icon-button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; color: var(--moss-dark); font-size: 22px; cursor: pointer; } .icon-button { position: absolute; right: 0; width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; color: var(--moss-dark); font-size: 22px; cursor: pointer; }
.icon-button:disabled { opacity: .4; cursor: wait; } .icon-button:disabled { opacity: .4; cursor: wait; }
.queue-list, .archive-list { list-style: none; margin: 16px 0 0; padding: 0; } .queue-list, .archive-list { list-style: none; margin: 16px 0 0; padding: 0; }
.queue-item, .archive-item { display: flex; align-items: flex-start; gap: 15px; padding: 14px 0; border-top: 1px solid #e3e4dd; animation: reveal .35s ease both; } .queue-item, .archive-item { display: flex; align-items: flex-start; gap: 15px; padding: 14px 0; border-top: 1px solid #e3e4dd; animation: reveal .35s ease both; }
+2 -2
View File
@@ -1,5 +1,5 @@
const CACHE_NAME = 'faerro-kb-shell-v1'; const CACHE_NAME = 'faerro-kb-shell-v2';
const APP_SHELL = ['/', '/styles.css', '/app.js', '/manifest.webmanifest']; const APP_SHELL = ['/', '/styles.css', '/app.js', '/manifest.webmanifest', '/favicon.svg'];
self.addEventListener('install', (event) => { self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))); event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)));
+51 -4
View File
@@ -3,6 +3,24 @@ set -euo pipefail
image="gitea.faerro.it/andrea/faerro-kb" image="gitea.faerro.it/andrea/faerro-kb"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
bump="patch"
while [[ $# -gt 0 ]]; do
case "$1" in
--major|--minor)
if [[ "$bump" != "patch" ]]; then
echo "Choose only one of --major or --minor." >&2
exit 1
fi
bump="${1#--}"
;;
*)
echo "Usage: $0 [--major | --minor]" >&2
exit 1
;;
esac
shift
done
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then
echo "Docker is required to build and publish the image." >&2 echo "Docker is required to build and publish the image." >&2
@@ -14,14 +32,43 @@ if ! command -v git >/dev/null 2>&1; then
exit 1 exit 1
fi fi
tag="$(git -C "$repo_root" describe --tags --exact-match HEAD 2>/dev/null || true)" latest_version=""
version="${tag#v}" while IFS= read -r candidate; do
if [[ "$candidate" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
latest_version="$candidate"
break
fi
done < <(git -C "$repo_root" tag --list --sort=-version:refname)
if [[ ! "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then if [[ -z "$latest_version" ]]; then
echo "The checked-out commit must have a SemVer tag (for example, 0.0.2 or v0.0.2)." >&2 echo "At least one MAJOR.MINOR.PATCH Git tag is required to determine the next version." >&2
exit 1 exit 1
fi fi
IFS=. read -r major minor patch <<< "$latest_version"
case "$bump" in
major)
major=$((10#$major + 1))
minor=0
patch=0
;;
minor)
minor=$((10#$minor + 1))
patch=0
;;
patch)
patch=$((10#$patch + 1))
;;
esac
version="$major.$minor.$patch"
if git -C "$repo_root" show-ref --verify --quiet "refs/tags/$version"; then
echo "Git tag $version already exists; refusing to move it." >&2
exit 1
fi
git -C "$repo_root" tag "$version"
cd "$repo_root" cd "$repo_root"
docker build --tag "$image:$version" --tag "$image:latest" . docker build --tag "$image:$version" --tag "$image:latest" .
registry_username="${GITEA_USERNAME:-${DOCKER_USERNAME:-}}" registry_username="${GITEA_USERNAME:-${DOCKER_USERNAME:-}}"