Clarify analytics is hosted only
All checks were successful
Build and test project / build-and-test (push) Successful in 1m29s
All checks were successful
Build and test project / build-and-test (push) Successful in 1m29s
This commit is contained in:
23
README.md
23
README.md
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> · </span>
|
<span id="privacy-link-separator" hidden> · </span>
|
||||||
<a href="#" id="btn-privacy" hidden>Privacy</a>
|
<a href="#" id="btn-privacy" class="site-footer-link" hidden>Privacy</a>
|
||||||
·
|
·
|
||||||
<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">×</button>
|
<button id="btn-close-dialog" class="modal-close" aria-label="Close">×</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> — whether you connected a Kobo
|
<li><strong>How the flow was started</strong> — 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> — preset installation,
|
<li><strong>Which NickelMenu option was selected</strong> — preset installation,
|
||||||
NickelMenu only, or removal.</li>
|
NickelMenu only, or removal.</li>
|
||||||
<li><strong>How the flow ended</strong> — whether files were written to the
|
<li><strong>How the flow ended</strong> — 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>
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user