1
0

Clarify analytics is hosted only
All checks were successful
Build and test project / build-and-test (push) Successful in 1m29s

This commit is contained in:
2026-03-21 17:11:19 +01:00
parent 82f32460cb
commit 6f902090c1
7 changed files with 54 additions and 49 deletions

View File

@@ -231,27 +231,12 @@ The WASM patcher performs several checks on each patched binary before including
## Analytics (optional)
The app supports optional, privacy-focused analytics via [Umami](https://umami.is). Analytics are disabled by default and only activate when two environment variables are set on the server:
The hosted version at [kp.nicoverbruggen.be](https://kp.nicoverbruggen.be) uses optional, privacy-focused analytics via [Umami](https://umami.is) to understand how the tool is used. No personal identifiers are collected. See the "Privacy" link in the footer for details.
Analytics are disabled for local and self-hosted installs. They activate only when `UMAMI_WEBSITE_ID` and `UMAMI_SCRIPT_URL` environment variables are set on the server. To test the analytics UI locally without sending any data:
```bash
UMAMI_WEBSITE_ID=your-website-id
UMAMI_SCRIPT_URL=https://your-umami-instance/script.js
```
When enabled, the server injects the Umami tracking script into `index.html` at runtime. A "Privacy" link appears in the footer with a modal explaining what is tracked.
**What is tracked** (no personal identifiers):
- **Flow start** — whether the user connected a Kobo directly (`connect`) or chose manual download (`manual`)
- **NickelMenu option** — which option was selected (`sample`, `nickelmenu-only`, or `remove`)
- **Flow end** — how the process completed (`nm-write`, `nm-download`, `nm-remove`, `patches-write`, `patches-download`, `restore-write`, `restore-download`)
**What is not tracked**: device model, serial number, firmware version, IP address, browsing behaviour. Umami is cookie-free and GDPR/CCPA/PECR compliant.
For local installs via `./serve-locally.sh`, analytics is disabled unless the environment variables are set:
```bash
UMAMI_WEBSITE_ID=... UMAMI_SCRIPT_URL=... ./serve-locally.sh
./serve-locally.sh --fake-analytics
```
## Credits

View File

@@ -1,6 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "${1:-}" == "--fake-analytics" ]]; then
export UMAMI_WEBSITE_ID="fake"
export UMAMI_SCRIPT_URL="data:,"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WEB_DIR="$SCRIPT_DIR/web"
SRC_DIR="$WEB_DIR/src"

View File

@@ -35,7 +35,7 @@ test.describe('NickelMenu', () => {
await expect(page.locator('#btn-nm-next')).toBeDisabled();
// Select "Install NickelMenu and configure"
await page.click('input[name="nm-option"][value="sample"]');
await page.click('input[name="nm-option"][value="preset"]');
await expect(page.locator('#nm-config-options')).not.toBeHidden();
// Verify default checkbox states
@@ -106,7 +106,7 @@ test.describe('NickelMenu', () => {
// NickelMenu configure step — select "Install NickelMenu with preset"
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
await page.click('input[name="nm-option"][value="sample"]');
await page.click('input[name="nm-option"][value="preset"]');
await expect(page.locator('#nm-config-options')).not.toBeHidden();
// KOReader checkbox should be visible and unchecked by default
@@ -150,7 +150,7 @@ test.describe('NickelMenu', () => {
await page.click('#btn-mode-next');
// Select "Install NickelMenu with preset"
await page.click('input[name="nm-option"][value="sample"]');
await page.click('input[name="nm-option"][value="preset"]');
// Enable KOReader
await page.check('input[name="nm-cfg-koreader"]');
@@ -243,7 +243,7 @@ test.describe('NickelMenu', () => {
await expect(page.locator('#nm-option-remove')).toHaveClass(/nm-option-disabled/);
// Select "Install NickelMenu and configure"
await page.click('input[name="nm-option"][value="sample"]');
await page.click('input[name="nm-option"][value="preset"]');
await expect(page.locator('#nm-config-options')).not.toBeHidden();
// Enable all options for testing

View File

@@ -1016,6 +1016,22 @@ button:focus-visible {
text-decoration: underline;
}
.site-footer a.site-footer-link {
color: var(--primary);
}
.site-footer p {
margin-bottom: 0.75rem;
}
.site-footer p:last-child {
margin-bottom: 0;
}
.site-footer-attribution {
font-size: 0.7rem;
}
.site-footer a:hover {
color: var(--text);
}

View File

@@ -170,7 +170,7 @@
<p>Choose what to do with your Kobo.</p>
<div class="nm-options">
<label class="nm-option">
<input type="radio" name="nm-option" value="sample">
<input type="radio" name="nm-option" value="preset">
<div class="nm-option-body">
<div class="nm-option-title">Install NickelMenu with preset</div>
<div class="nm-option-desc">Installs NickelMenu with a curated set of menu options. You get to decide which optional features you'd like to enable.</div>
@@ -384,27 +384,27 @@
<footer class="site-footer">
<p>
<a href="#" id="btn-how-it-works">How does this work?</a>
This project is not affiliated with Rakuten Kobo Inc.
</p>
<p>
<a href="#" id="btn-how-it-works" class="site-footer-link">Disclaimer</a>
<span id="privacy-link-separator" hidden>&nbsp;&middot;&nbsp;</span>
<a href="#" id="btn-privacy" hidden>Privacy</a>
<a href="#" id="btn-privacy" class="site-footer-link" hidden>Privacy</a>
&nbsp;&middot;&nbsp;
<a id="commit-link" href="https://github.com/nicoverbruggen/kobopatch-webui" target="_blank">Version <span id="commit-hash"></span></a>
<br/>
<br/>
<a id="commit-link" class="site-footer-link" href="https://github.com/nicoverbruggen/kobopatch-webui" target="_blank"><span id="commit-hash"></span></a>
</p>
<p class="site-footer-attribution">
Created by <a href="https://nicoverbruggen.be" target="_blank">Nico Verbruggen</a>.
Readerly is part of my <a href="https://ebook-fonts.nicoverbruggen.be" target="_blank">curated font collection</a>.
<br/>
Built on <a href="https://github.com/pgaskin/kobopatch" target="_blank">kobopatch</a> and <a href="https://pgaskin.net/NickelMenu/" target="_blank">NickelMenu</a> by Patrick Gaskin.
</p>
<p>
This project is not affiliated with Rakuten Kobo Inc.
</p>
</footer>
<dialog id="how-it-works-dialog" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>How does this work?</h2>
<h2>Disclaimer</h2>
<button id="btn-close-dialog" class="modal-close" aria-label="Close">&times;</button>
</div>
<div class="modal-body">
@@ -472,9 +472,8 @@
</div>
<div class="modal-body">
<p>
This website uses <a href="https://umami.is" target="_blank">Umami</a>, a privacy-focused
analytics tool, to help improve the experience. Analytics are only enabled on the hosted
version of this site, not on local or self-hosted installs.
This hosted version of the website uses <a href="https://umami.is" target="_blank">Umami</a>, a privacy-focused
analytics tool, to help improve the experience. Data is only used to further improve this website, and no personal information is collected.
</p>
<h3>What is tracked</h3>
@@ -483,11 +482,11 @@
</p>
<ul>
<li><strong>How the flow was started</strong> &mdash; whether you connected a Kobo
directly or chose the manual download option.</li>
directly or chose the manual download option. This helps the author decide whether to keep the manual download option, or remove it (if it is barely used).</li>
<li><strong>Which NickelMenu option was selected</strong> &mdash; preset installation,
NickelMenu only, or removal.</li>
<li><strong>How the flow ended</strong> &mdash; whether files were written to the
device or downloaded as a ZIP.</li>
device or downloaded as a ZIP. This helps the author understand how many people have actually used the complete flow successfully.</li>
</ul>
<h3>What is not tracked</h3>

View File

@@ -22,7 +22,7 @@ import JSZip from 'jszip';
let isRestore = false;
let availablePatches = null;
let selectedMode = null; // 'nickelmenu' | 'patches'
let nickelMenuOption = null; // 'sample' | 'nickelmenu-only' | 'remove'
let nickelMenuOption = null; // 'preset' | 'nickelmenu-only' | 'remove'
// --- Helpers ---
@@ -477,7 +477,7 @@ import JSZip from 'jszip';
// Show/hide config checkboxes based on radio selection, enable Continue
for (const radio of $qa('input[name="nm-option"]', stepNickelMenu)) {
radio.addEventListener('change', () => {
nmConfigOptions.hidden = radio.value !== 'sample' || !radio.checked;
nmConfigOptions.hidden = radio.value !== 'preset' || !radio.checked;
btnNmNext.disabled = false;
});
}
@@ -523,7 +523,7 @@ import JSZip from 'jszip';
function goToNickelMenuConfig() {
checkNickelMenuInstalled();
const currentOption = $q('input[name="nm-option"]:checked', stepNickelMenu);
nmConfigOptions.hidden = !currentOption || currentOption.value !== 'sample';
nmConfigOptions.hidden = !currentOption || currentOption.value !== 'preset';
btnNmNext.disabled = !currentOption;
setNavStep(3);
showStep(stepNickelMenu);
@@ -613,7 +613,7 @@ import JSZip from 'jszip';
return;
}
const cfg = nickelMenuOption === 'sample' ? getNmConfig() : null;
const cfg = nickelMenuOption === 'preset' ? getNmConfig() : null;
if (writeToDevice && device.directoryHandle) {
await nmInstaller.installToDevice(device, nickelMenuOption, cfg, (msg) => {
@@ -653,7 +653,7 @@ import JSZip from 'jszip';
triggerDownload(resultNmZip, 'NickelMenu-install.zip', 'application/zip');
$('nm-download-instructions').hidden = false;
// Show eReader.conf + reboot steps only when sample config is included
const showConfStep = nickelMenuOption === 'sample';
const showConfStep = nickelMenuOption === 'preset';
$('nm-download-conf-step').hidden = !showConfStep;
$('nm-download-reboot-step').hidden = !showConfStep;
track('flow-end', { result: 'nm-download' });

View File

@@ -9,9 +9,9 @@ import JSZip from 'jszip';
*
* Options:
* 'nickelmenu-only' — just NickelMenu (KoboRoot.tgz)
* 'sample' — NickelMenu + config based on cfg flags
* 'preset' — NickelMenu + config based on cfg flags
*
* Config flags (when option is 'sample'):
* Config flags (when option is 'preset'):
* fonts: bool — include Readerly fonts
* screensaver: bool — include custom screensaver
* simplifyTabs: bool — comment out experimental tab items in config
@@ -162,8 +162,8 @@ class NickelMenuInstaller {
/**
* Install to a connected Kobo device via File System Access API.
* @param {KoboDevice} device
* @param {string} option - 'sample' or 'nickelmenu-only'
* @param {object|null} cfg - config flags (when option is 'sample')
* @param {string} option - 'preset' or 'nickelmenu-only'
* @param {object|null} cfg - config flags (when option is 'preset')
* @param {function} progressFn
*/
async installToDevice(device, option, cfg, progressFn) {
@@ -233,8 +233,8 @@ class NickelMenuInstaller {
/**
* Build a zip for manual download containing all files to copy to the Kobo.
* @param {string} option - 'sample' or 'nickelmenu-only'
* @param {object|null} cfg - config flags (when option is 'sample')
* @param {string} option - 'preset' or 'nickelmenu-only'
* @param {object|null} cfg - config flags (when option is 'preset')
* @param {function} progressFn
* @returns {Uint8Array} zip contents
*/