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) ## 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 ```bash
UMAMI_WEBSITE_ID=your-website-id ./serve-locally.sh --fake-analytics
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
``` ```
## Credits ## Credits

View File

@@ -1,6 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail 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)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WEB_DIR="$SCRIPT_DIR/web" WEB_DIR="$SCRIPT_DIR/web"
SRC_DIR="$WEB_DIR/src" SRC_DIR="$WEB_DIR/src"

View File

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

View File

@@ -1016,6 +1016,22 @@ button:focus-visible {
text-decoration: underline; 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 { .site-footer a:hover {
color: var(--text); color: var(--text);
} }

View File

@@ -170,7 +170,7 @@
<p>Choose what to do with your Kobo.</p> <p>Choose what to do with your Kobo.</p>
<div class="nm-options"> <div class="nm-options">
<label class="nm-option"> <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-body">
<div class="nm-option-title">Install NickelMenu with preset</div> <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> <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"> <footer class="site-footer">
<p> <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> <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; &nbsp;&middot;&nbsp;
<a id="commit-link" href="https://github.com/nicoverbruggen/kobopatch-webui" target="_blank">Version <span id="commit-hash"></span></a> <a id="commit-link" class="site-footer-link" href="https://github.com/nicoverbruggen/kobopatch-webui" target="_blank"><span id="commit-hash"></span></a>
<br/> </p>
<br/> <p class="site-footer-attribution">
Created by <a href="https://nicoverbruggen.be" target="_blank">Nico Verbruggen</a>. 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>. Readerly is part of my <a href="https://ebook-fonts.nicoverbruggen.be" target="_blank">curated font collection</a>.
<br/> <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. 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>
<p>
This project is not affiliated with Rakuten Kobo Inc.
</p>
</footer> </footer>
<dialog id="how-it-works-dialog" class="modal"> <dialog id="how-it-works-dialog" class="modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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> <button id="btn-close-dialog" class="modal-close" aria-label="Close">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -472,9 +472,8 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p> <p>
This website uses <a href="https://umami.is" target="_blank">Umami</a>, a privacy-focused 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. Analytics are only enabled on the hosted analytics tool, to help improve the experience. Data is only used to further improve this website, and no personal information is collected.
version of this site, not on local or self-hosted installs.
</p> </p>
<h3>What is tracked</h3> <h3>What is tracked</h3>
@@ -483,11 +482,11 @@
</p> </p>
<ul> <ul>
<li><strong>How the flow was started</strong> &mdash; whether you connected a Kobo <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, <li><strong>Which NickelMenu option was selected</strong> &mdash; preset installation,
NickelMenu only, or removal.</li> NickelMenu only, or removal.</li>
<li><strong>How the flow ended</strong> &mdash; whether files were written to the <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> </ul>
<h3>What is not tracked</h3> <h3>What is not tracked</h3>

View File

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

View File

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