diff --git a/README.md b/README.md index 6259229..604ca20 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,31 @@ The WASM patcher performs several checks on each patched binary before including - **ELF header validation** — verifies the magic bytes (`\x7fELF`), 32-bit class, little-endian encoding, and ARM machine type (`0x28`) are intact after patching. - **Archive consistency check** — after building the output tar.gz, re-reads the entire archive and verifies the sum of entry sizes matches what was written. +## 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: + +```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 +``` + ## Credits Built on [kobopatch](https://github.com/pgaskin/kobopatch) and [NickelMenu](https://pgaskin.net/NickelMenu/) by pgaskin. Uses [JSZip](https://stuk.github.io/jszip/) for client-side ZIP handling and [esbuild](https://esbuild.github.io/) for bundling. Software patches and discussion on the [MobileRead forums](https://www.mobileread.com/forums/forumdisplay.php?f=247). diff --git a/web/serve.mjs b/web/serve.mjs index 52969ef..d9351ca 100644 --- a/web/serve.mjs +++ b/web/serve.mjs @@ -1,10 +1,36 @@ import { createServer } from 'node:http'; -import { createReadStream, statSync, existsSync } from 'node:fs'; +import { createReadStream, readFileSync, statSync, existsSync } from 'node:fs'; import { join, extname } from 'node:path'; const DIST = join(import.meta.dirname, 'dist'); const PORT = process.env.PORT || 8888; +const UMAMI_WEBSITE_ID = process.env.UMAMI_WEBSITE_ID || ''; +const UMAMI_SCRIPT_URL = process.env.UMAMI_SCRIPT_URL || ''; +const analyticsEnabled = !!(UMAMI_WEBSITE_ID && UMAMI_SCRIPT_URL); + +// Pre-build the analytics snippet (injected before in index.html) +let analyticsSnippet = ''; +if (analyticsEnabled) { + analyticsSnippet = + ` \n` + + ` \n`; +} + +// Cache the processed index.html at startup +let cachedIndexHtml = null; +function getIndexHtml() { + if (cachedIndexHtml) return cachedIndexHtml; + const indexPath = join(DIST, 'index.html'); + if (!existsSync(indexPath)) return null; + let html = readFileSync(indexPath, 'utf-8'); + if (analyticsSnippet) { + html = html.replace('', analyticsSnippet + ''); + } + cachedIndexHtml = html; + return cachedIndexHtml; +} + const MIME = { '.html': 'text/html', '.css': 'text/css', @@ -26,6 +52,16 @@ createServer((req, res) => { if (filePath.endsWith('/')) filePath = join(filePath, 'index.html'); if (!extname(filePath) && existsSync(filePath + '/index.html')) filePath += '/index.html'; + // Serve processed index.html with analytics injection + if (filePath.endsWith('index.html')) { + const html = getIndexHtml(); + if (html) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(html); + return; + } + } + try { const stat = statSync(filePath); if (!stat.isFile()) throw new Error(); @@ -36,5 +72,5 @@ createServer((req, res) => { res.end('Not found'); } }).listen(PORT, () => { - console.log(`Serving web/dist on http://localhost:${PORT}`); + console.log(`Serving web/dist on http://localhost:${PORT}` + (analyticsEnabled ? ' (analytics enabled)' : '')); }); diff --git a/web/src/css/style.css b/web/src/css/style.css index 195003e..f3b6c9c 100644 --- a/web/src/css/style.css +++ b/web/src/css/style.css @@ -1095,7 +1095,8 @@ button:focus-visible { margin-bottom: 0.75rem; } -.modal-body ol { +.modal-body ol, +.modal-body ul { margin: 0 0 0.75rem 1.25rem; } diff --git a/web/src/index.html b/web/src/index.html index d89759f..454c448 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -385,6 +385,8 @@