A mobile-first Progressive Web App (PWA) that helps small-scale traders and informal vendors track cash flow, log transactions by voice, scan receipts, and visualise daily profit and loss — with full offline support.
Live demo: https://shiny-croquembouche-525f4b.netlify.app
- Features
- Tech Stack
- Project Structure
- Getting Started
- Feature Guide
- API Key Setup
- Offline Support
- Running Tests
- Deployment
- Security Notes
| Feature | Description |
|---|---|
| Voice-to-Ledger | Speak a transaction naturally — "Sold 2kg of tomatoes for 200 shillings" — and the app parses it into a structured record |
| AI Voice (Gemini) | Tap-to-record using Google Gemini for higher accuracy and multi-language support |
| Receipt Scanning | Photograph or upload a receipt; Tesseract.js OCR extracts line items and totals automatically |
| Financial Dashboard | Live summary cards (today's income, expenses, profit) plus a 7-day bar chart |
| Transaction Ledger | Full history filterable by type (income / expense) and month, grouped by date |
| Offline Resilience | All data stored in IndexedDB; service worker caches the app shell; works with no internet after first load |
| PWA Install | Installable to the home screen on Android and iOS |
- Frontend: Vanilla HTML, CSS, JavaScript — no framework, no bundler
- Storage: IndexedDB (via a custom wrapper in
lib/db.js) - Voice (primary): Web Speech API (Chrome / Edge)
- Voice (AI fallback): Google Gemini API via
MediaRecorder - OCR: Tesseract.js v4 (loaded from CDN, cached by service worker)
- Charts: Custom Canvas API — no chart library needed
- Offline: Service Worker with network-first strategy for app shell, cache-first for CDN assets
- Hosting: Netlify
- Tests: Jest + fake-indexeddb
trader-app/
│
├── index.html # App shell — all four views (Dashboard, Add, Ledger, Scan)
├── app.css # Mobile-first styles
├── app.js # Main application logic (DB, Voice, Gemini, OCR, Charts, UI)
├── sw.js # Service worker — offline caching
├── manifest.json # PWA manifest — enables "Add to Home Screen"
├── netlify.toml # Netlify deploy config with correct cache headers
├── favicon.svg # App icon (SVG)
├── icon-192.svg # PWA icon 192×192
├── icon-512.svg # PWA icon 512×512
│
├── lib/ # Pure, testable modules (UMD — work in browser + Node)
│ ├── helpers.js # todayISO, fmtDate, fmtAmount, esc, last7Days
│ ├── nlp.js # Voice NLP parser — extracts type, amount, category
│ ├── ocr-parser.js # Receipt text parser — finds totals and line items
│ ├── stats.js # computeStats — income / expense / profit from a row set
│ └── db.js # IndexedDB wrapper — open, add, getAll, remove, stats
│
├── tests/ # Jest test suite
│ ├── helpers.test.js
│ ├── nlp.test.js
│ ├── ocr-parser.test.js
│ ├── stats.test.js
│ └── db.test.js
│
├── serve.py # Minimal local dev server (Python 3, no dependencies)
├── package.json # Jest + fake-indexeddb dev dependencies
│
├── .env # Gemini API key — NOT committed
├── config.js # Exposes key to browser as window.DUKABOOKS_CONFIG — NOT committed
└── .gitignore # Excludes .env, config.js, node_modules
- Python 3 (for the local dev server)
- Node.js ≥ 18 (for running tests)
- A modern browser — Chrome or Edge recommended for voice features
# Clone or download the project
cd trader-app
# Start the dev server
python3 serve.py
# → http://localhost:8080To access from a phone on the same Wi-Fi network, use your machine's local IP:
hostname -I | awk '{print $1}'
# e.g. → 10.0.0.42 → open http://10.0.0.42:8080 on your phoneThe app's core features (ledger, manual entry, charts) also work when opened directly as
file://— only the service worker (offline caching + PWA install) requires HTTP.
npm installThe home screen shows three summary cards for today:
- Today's Profit — income minus expenses (green = positive, red = negative)
- Income — total money received today
- Expenses — total money spent today
Below the cards is a 7-day bar chart (green = income, red = expenses) and a list of the five most recent transactions.
Navigate to the Add tab. There are three ways to log a transaction:
Hold the 🎤 Hold to Speak button and say the transaction naturally.
Examples:
"Sold 2kg of tomatoes for 200 shillings"
"Bought 5 bags of sugar for 1500"
"Received payment of 3000 from customer"
"Paid rent 5000 shillings"
"Spent 250 on matatu fare"
The NLP parser extracts:
- Type — income or expense (income wins if both keywords appear)
- Amount — handles
200,1,500,99.50,KSh 500,500 bob - Category — auto-detected from keywords (Produce, Stock, Transport, Rent, Utilities, Equipment)
- Description — cleaned up version of the spoken text
A preview card appears. Tap Use this ✓ to load it into the form, then Save Transaction.
Note: The Web Speech API requires internet because Chrome sends audio to Google's servers for processing.
Tap the 🎙 AI Record button (teal). Recording starts immediately. Tap again to stop.
The audio is sent to Google Gemini, which:
- Supports Swahili, English, mixed language, and accents
- Works even if Web Speech returns a "network" error
- Returns a transcript that goes through the same NLP parser
First use will prompt for your Gemini API key (see API Key Setup).
Fill in the form directly:
- Toggle + Income or − Expense
- Enter amount, description, category, and date
- Tap Save Transaction
A confirmation sheet slides up showing the full details before saving.
The Ledger tab shows all transactions:
- Filter by All / Income / Expense using the tab buttons
- Filter by month using the month picker
- Transactions are grouped by date with a daily profit/loss summary
- Tap × on any row to delete it
- A small ● dot indicates a transaction saved while offline (pending sync)
Navigate to the Scan tab.
Option A — Camera:
- Tap 📷 Open Camera
- Point at the receipt
- Tap 📸 Capture Photo
Option B — Upload:
- Tap 📁 Upload from Gallery
- Select a photo of the receipt
The OCR engine (Tesseract.js) scans the image and:
- If it finds a Total / Grand Total / Amount Due line, it creates one expense entry for that amount
- Otherwise it lists all detected line items
Tap Add next to any item to load it into the Add form for review and saving.
First use: Tesseract downloads ~10 MB of language data from CDN. Subsequent uses work offline (cached by service worker).
The Gemini key is used for the AI voice feature. It is never committed to git.
Create config.js in the project root:
window.DUKABOOKS_CONFIG = {
geminiKey: 'AIzaSy...',
};This file is loaded by index.html and listed in .gitignore.
Because config.js is not deployed, the key is stored in the browser's localStorage on first use.
When you tap 🎙 AI Record for the first time, a prompt asks for your key. Paste it in and it is saved to localStorage on that device. You will not be prompted again on the same browser.
To update or remove the key, open the browser console and run:
localStorage.removeItem('dukabooks_gemini_key');DukaBooks is designed to work in areas with poor or no connectivity.
| Scenario | Behaviour |
|---|---|
| Full offline (after first load) | Dashboard, ledger, manual entry, and charts all work. Data saves to IndexedDB. |
| Offline at save time | Transaction is saved locally with a synced: false flag shown as ● in the ledger. |
| Come back online | The ● dot is informational — in a production deployment with a backend, unsynced records would be POSTed to the server here. |
| Tesseract OCR offline | Works after first load (language data is cached by the service worker). |
| Web Speech API offline | Fails with a friendly message. Use AI Record or manual entry instead. |
| Gemini AI offline | Cannot reach the Gemini API — use manual entry. |
The service worker uses:
- Network-first for the app shell (
index.html,app.js,app.css) so code updates are always fresh - Cache-first for CDN assets (Tesseract.js) since these rarely change
npm test148 tests across 5 suites:
| Suite | Tests | Coverage |
|---|---|---|
tests/helpers.test.js |
32 | todayISO, fmtDate, fmtAmount, HTML escaping, last7Days |
tests/nlp.test.js |
42 | Income/expense detection, amount parsing, 7 categories, edge cases |
tests/ocr-parser.test.js |
22 | Total detection, multi-item parsing, comma/decimal amounts |
tests/stats.test.js |
27 | Stats computation, edge cases, precision |
tests/db.test.js |
25 | Full CRUD, all filter types, stats queries (uses fake-indexeddb) |
# With coverage report
npm run test:coverage
# Watch mode during development
npm run test:watchThe app is deployed to Netlify. To redeploy after changes:
cd trader-app
netlify deploy --dir=. --prodnpm install -g netlify-cli
netlify login
cd trader-app
netlify deploy --dir=. --prodThe .netlifyignore file excludes:
.env
config.js ← API key, never deployed
node_modules/
tests/
serve.py
package*.json
netlify.toml sets Cache-Control: no-cache on all files so updates reach users immediately, and Service-Worker-Allowed: / so the PWA service worker can intercept requests at the root scope.
| Item | Status |
|---|---|
| Gemini API key | Stored in config.js (local only, git-ignored) or localStorage (per-device). Never in source code or deployed files. |
| IndexedDB data | Stored locally in the user's browser. No data leaves the device unless Gemini AI or Tesseract OCR is used. |
| HTML escaping | All user-supplied text is run through esc() before being inserted into the DOM, preventing XSS. |
| No backend | The app has no server component. There is no database, no user accounts, and no data transmission beyond the Gemini API and Tesseract CDN calls. |
| Browser | Voice (Web Speech) | AI Record (Gemini) | OCR | Offline / PWA |
|---|---|---|---|---|
| Chrome (Android) | ✅ | ✅ | ✅ | ✅ Full PWA |
| Edge | ✅ | ✅ | ✅ | ✅ |
| Safari (iOS) | ✅ (online only) | ✅ | ✅ | ✅ Add to Home Screen |
| Firefox | ❌ | ✅ | ✅ | ✅ |
MIT