# Car-parts price comparator (POC)

Meta-search for car parts across Latvian stores: type a part number
(e.g. `W 712/52` or `0986452041`), get live prices from ~10 stores with
links straight to each store's product page. No catalog is stored — every
search asks the stores directly (15-min operational cache only).

Specific offers can be saved to a backend-persistent **Pirkumu plāns**, compared as
alternatives, grouped into proposed store orders, and refreshed from their exact product pages.
The 20 most recent distinct part-number searches are kept in user-visible **Meklēšanas vēsture**;
the product keeps no separate search, result, or outbound-click analytics.

**Docs:** [FUNCTIONAL-SPEC.md](FUNCTIONAL-SPEC.md) · [SEARCH-HISTORY-FUNCTIONAL-SPEC.md](SEARCH-HISTORY-FUNCTIONAL-SPEC.md) · [PURCHASE-PLAN-FUNCTIONAL-SPEC.md](PURCHASE-PLAN-FUNCTIONAL-SPEC.md) · [PURCHASE-PLAN-IMPLEMENTATION-PLAN.md](PURCHASE-PLAN-IMPLEMENTATION-PLAN.md) · [TECHNICAL-SPEC.md](TECHNICAL-SPEC.md) ·
[WALLED-STORES.md](WALLED-STORES.md) (Cloudflare-walled stores & the resident browser) ·
[MONETIZATION.md](MONETIZATION.md) ·
[DISTRIBUTION.md](DISTRIBUTION.md) (open-sourcing & desktop/mobile distribution) ·
[MONETIZATION-PLAYBOOK.md](MONETIZATION-PLAYBOOK.md) (step-by-step monetization for the open-source model) ·
[CART-ASSEMBLY.md](CART-ASSEMBLY.md) (cross-store cart assembly design note) ·
[STANDALONE-APP-IMPLEMENTATION-PLAN.md](STANDALONE-APP-IMPLEMENTATION-PLAN.md) (desktop + Android delivery plan) ·
[ADAPTER-PACKS-PLAN.md](ADAPTER-PACKS-PLAN.md) (country adapter sets, bundled + auto-update)

## Quick start

```bash
npm install
npm --prefix web install
npm run dev          # builds web/ into public/, then serves on :3000
```

Open http://localhost:3000. The five plain-HTTP stores work out of the box.
The five walled stores (trodo, rd24, euautodalas, autodoc, ic24) need the
resident-browser container running (see WALLED-STORES.md); without it they
simply report an error and the rest keep working.

## Layout

| Path        | What                                                                  |
| ----------- | --------------------------------------------------------------------- |
| `shared/`   | The API contract (types + canonicalize) imported by backend AND web    |
| `src/core/` | config, HTTP gateway (politeness/breaker), browser hub, cache, search, persisted user documents |
| `src/adapters/` | One file per store: `fetchRaw()` (impure) + `parse()` (pure)       |
| `web/`      | Angular app (Spartan NG + Tailwind); builds into `public/`             |
| `tests/`    | Backend vitest: adapter fixture tests + unit tests, no network         |
| `fixtures/` | Recorded store responses the adapter tests parse                       |
| `scripts/`  | `record.ts` (re-record a fixture), `canary.ts` (live store smoke test) |

## Commands

```bash
npm run dev            # build web + serve with reload
npm test               # backend tests (fixtures only, CI-safe)
npm run test:web       # web unit tests (pure view logic)
npm run typecheck      # tsc over src/tests/scripts/shared
npm run lint           # eslint: backend + web
npm run record <store> [partNumber]   # re-record a fixture after a store changes markup
npm run canary         # live smoke test of the plain-HTTP stores (--walled to include all)
npm run package:android:debug  # production Android app, debug-signed APK
```

## Android app

The Android build runs the same engine and store adapters locally in Capacitor; it does
not start or contact the POC server. Store challenges open in a full-screen native
WebView, the **Pirkumu plāns** and search history are persisted with Android Preferences,
and store links open in Chrome Custom Tabs without tracking decoration.

Build the installable debug APK with `npm run package:android:debug`. The result is
`android/app/build/outputs/apk/debug/app-debug.apk` and can be installed with
`adb install -r android/app/build/outputs/apk/debug/app-debug.apk`.

Tagged GitHub releases add a signed `Car-Parts-Comparator-<version>-android.apk` beside
the desktop artifacts. The repository must define these Actions secrets before the
first tag:

- `ANDROID_KEYSTORE_BASE64` — base64 of the release `.jks` file
- `ANDROID_KEYSTORE_PASSWORD`
- `ANDROID_KEY_ALIAS`
- `ANDROID_KEY_PASSWORD`

Keep the keystore and passwords backed up permanently: Android will not accept an
update signed by a different key.

## Configuration (env)

Everything is read once in `src/core/config.ts`:

| Var | Default | Meaning |
| --- | --- | --- |
| `PORT` / `HOST` | `3000` / `0.0.0.0` | Listen address (`HOST=127.0.0.1` for local-only) |
| `BROWSER_URL` | `http://127.0.0.1:9222` | CDP endpoint of the resident headful Chrome |
| `NOVNC_URL` | `http://127.0.0.1:8080/vnc.html?autoconnect=1` | noVNC page for human challenge solves |
| `BROWSER_NAV_TIMEOUT_MS` | `30000` | Page navigation budget |
| `BROWSER_CHALLENGE_CLEAR_MS` | `12000` | Wait for Cloudflare's managed challenge to clear itself |
| `BROWSER_FORCE_SOLVE` | _(empty)_ | Test switch: store ids that always pretend to need a solve |

## Adding a store

1. `src/adapters/<store>.ts`: implement `StoreAdapter` (`fetchRaw` via
   `ctx.http` or, for walled stores, `ctx.browser`; keep `parse` pure).
2. Register it in `src/core/registry.ts`.
3. `npm run record <store>` to capture a fixture, then write
   `tests/<store>.test.ts` asserting the parse.
4. Check the store's `robots.txt` and note the verdict in the adapter header
   comment (see existing adapters for the format).
