Monitors product pages on [store.ui.com](https://store.ui.com/us/en) for stock availability and sends a Telegram alert the moment a watched item comes back in stock. Runs as a single persistent Docker container on Unraid with a clean web UI for managing tracked products.
Puppeteer navigates to each product URL, waits for React hydration (2.5s + active wait for `<button>` up to 8s), then evaluates with case- and whitespace-insensitive substring matching:
- Any `<button>` with `label="Add to Cart"` attribute, OR whose text contains `"add to cart"` → `in_stock`
- Any `<button>` whose text contains one of: `"login for notifications"`, `"login for updates"`, `"notify me when available"`, `"notify me"`, `"sold out"`, `"out of stock"`, `"currently unavailable"`, `"coming soon"` → `sold_out`
- Neither found → `unknown` (logs a `[Scraper] UNKNOWN debug` payload with page title, button labels, and button texts for diagnosis)
**Two gotchas the matcher handles:**
- **Auth-state difference.** The scraper runs logged out, so Ubiquiti shows `"Login for Notifications"` on sold-out items instead of the logged-in `"Notify me when available"`. Both must be in the sold-out list.
- **The `label` attribute exists only on the in-stock button.** Production DOM does carry `label="Add to Cart"` on the primary Add-to-Cart button (confirmed in DevTools), but the sold-out "Login for Notifications" button has no `label` attribute. So in-stock has two redundant signals (attribute + text), sold-out only has text — by design.
# Then add container via Unraid Docker GUI — see UNRAID.md
# Rebuild after code changes
docker stop ui-tracker && docker rm ui-tracker
docker build -t ui-tracker .
# Re-add in Unraid GUI or: docker run ... (see UNRAID.md)
```
---
## Fixes Applied During Development
### 1. `npm ci` → `npm install` (Dockerfile)
`npm ci` requires a `package-lock.json` to exist. Since lockfiles were not committed, the build failed immediately. Switched both frontend and backend build stages to `npm install`.
### 2. TypeScript DOM lib missing (backend)
`scraper.ts` uses `page.evaluate()` which runs a callback in the browser context. TypeScript flagged `document`, `navigator`, and `HTMLMetaElement` as unknown because the backend `tsconfig.json` only included `"lib": ["ES2020"]`. Fixed by adding `"DOM"` and `"DOM.Iterable"`:
```json
"lib":["ES2020","DOM","DOM.Iterable"]
```
`DOM.Iterable` was specifically required to allow `for...of` iteration over `NodeListOf<HTMLSpanElement>`.
### 3. Two containers → one container
Original design used separate `frontend` and `backend` Docker services in docker-compose. Consolidated into a single container using **supervisord** to run nginx and Node.js side by side, with nginx proxying `/api/` to `localhost:3001`. Removed `backend/Dockerfile`, `frontend/Dockerfile`, and `frontend/nginx.conf`. Root-level `Dockerfile`, `nginx.conf`, and `supervisord.conf` replaced them.