@@ -140,7 +140,7 @@ docker compose up -d # serves on http://localhost:8080
|
||||
|
||||
The image builds in three stages: Vite builds the frontend, `tsc` compiles the backend (then `npm prune --production`), and the runtime stage assembles `node:20-slim` + nginx + Chromium + supervisor.
|
||||
|
||||
Full Unraid install instructions are in [UNRAID.md](./UNRAID.md).
|
||||
Full Unraid install instructions are in [Deploy on Unraid](#deploy-on-unraid) below.
|
||||
|
||||
## CI/CD
|
||||
|
||||
@@ -167,7 +167,126 @@ Configure via **Settings** in the UI, then use **Test Alert** to verify.
|
||||
|
||||
> ⚠️ These are live credentials committed to the repo. Rotate the bot token (via @BotFather) if this repository is ever made public or shared.
|
||||
|
||||
## Further Docs
|
||||
## Deploy on Unraid
|
||||
|
||||
- [PROJECT.md](./PROJECT.md) — deeper design notes and development history.
|
||||
- [UNRAID.md](./UNRAID.md) — step-by-step Unraid install and troubleshooting.
|
||||
### 1. Clone and build the image (CLI)
|
||||
|
||||
SSH into your Unraid server and run:
|
||||
|
||||
```bash
|
||||
mkdir -p /mnt/user/appdata/ui-tracker
|
||||
cd /mnt/user/appdata/ui-tracker
|
||||
git clone https://git.alwisp.com/jason/ui-tracker.git .
|
||||
docker build -t ui-tracker .
|
||||
```
|
||||
|
||||
The first build takes a few minutes — it compiles the frontend, compiles the backend, and installs Chromium inside the image. The result is available locally as `ui-tracker`.
|
||||
|
||||
### 2. Add the container (Unraid GUI)
|
||||
|
||||
In the Unraid web UI go to **Docker → Add Container** and fill in each section:
|
||||
|
||||
**Basic**
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Name | `ui-tracker` |
|
||||
| Repository | `ui-tracker` |
|
||||
| Icon URL | *(leave blank)* |
|
||||
|
||||
**Network**
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Network Type | `br0` |
|
||||
| Fixed IP Address | Your chosen static LAN IP — e.g. `192.168.1.50` |
|
||||
|
||||
> With `br0`, the container gets its own IP on your LAN, so you reach the UI directly at `http://<fixed-ip>:8080` with no port conflict against the Unraid host.
|
||||
|
||||
**Port** — *Add another Path, Port, Variable, Label or Device → Port*
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Name | `Web UI` |
|
||||
| Container Port | `8080` |
|
||||
| Host Port | `8080` |
|
||||
| Protocol | `TCP` |
|
||||
|
||||
**Path (persistent data)** — *Add another Path, Port, Variable, Label or Device → Path*
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Name | `Data` |
|
||||
| Container Path | `/app/data` |
|
||||
| Host Path | `/mnt/user/appdata/ui-tracker/data` |
|
||||
| Access Mode | `Read/Write` |
|
||||
|
||||
> This is where the SQLite database lives — tracked items and Telegram settings persist here across restarts and rebuilds.
|
||||
|
||||
**Variables** — none required; all configuration is baked into the image.
|
||||
|
||||
Click **Apply** to create and start the container.
|
||||
|
||||
### 3. First-time setup
|
||||
|
||||
Open the UI at `http://<fixed-ip>:8080` and:
|
||||
|
||||
1. Click **Settings** (top right).
|
||||
2. Enter your **Bot Token** and **Chat ID** (see [Telegram Configuration](#telegram-configuration)).
|
||||
3. Click **Test Alert** — a Telegram message should arrive within seconds.
|
||||
4. Click **Save**.
|
||||
5. Click **Add Item**, paste a `store.ui.com` product URL, set the check interval, and click **Start Tracking**.
|
||||
|
||||
### Rebuilding after an update
|
||||
|
||||
```bash
|
||||
cd /mnt/user/appdata/ui-tracker
|
||||
git pull
|
||||
docker stop ui-tracker
|
||||
docker rm ui-tracker
|
||||
docker build -t ui-tracker .
|
||||
```
|
||||
|
||||
Then re-add the container from **Docker → Add Container** with the same settings, or start it from the Docker tab if Unraid retained the template.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**UI not loading**
|
||||
- Confirm the container is running in the Docker tab.
|
||||
- Check logs: click the container icon → **Logs**.
|
||||
- Make sure the fixed IP isn't already in use on the network.
|
||||
|
||||
**Telegram test fails**
|
||||
- Verify the bot token and chat ID in Settings.
|
||||
- Send `/start` to your bot in Telegram at least once to open the conversation.
|
||||
- Confirm Unraid has outbound HTTPS (port 443) access.
|
||||
|
||||
**Items stuck on "Unknown" status**
|
||||
- Open container logs and look for Puppeteer errors.
|
||||
- Restart the container — Chromium occasionally needs a clean start.
|
||||
|
||||
## Design Notes & Development History
|
||||
|
||||
### Stock detection rationale
|
||||
|
||||
The scraper classifies each product page using case- and whitespace-insensitive substring matching against the page's buttons. Two non-obvious details drove the matching rules:
|
||||
|
||||
**Auth-state difference.** The scraper runs logged out. On a sold-out item, Ubiquiti shows `"Login for Notifications"` to logged-out visitors instead of the logged-in `"Notify me when available"`. Both strings must be in the sold-out list, or logged-out checks would fall through to `unknown`.
|
||||
|
||||
**The `label` attribute exists only on the in-stock button.** The production DOM carries `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 (the attribute *and* the button text) while sold-out relies on text alone — this asymmetry is by design.
|
||||
|
||||
When neither signal matches, the scraper logs a `[Scraper] UNKNOWN debug` payload (page title, button labels, button texts) so a misclassification can be diagnosed from container logs without reproducing locally. To avoid reading the page before React finishes rendering, the scraper waits 2.5s plus an active wait for a `<button>` to appear (up to 8s) before evaluating.
|
||||
|
||||
### Fixes applied during development
|
||||
|
||||
**`npm ci` → `npm install` (Dockerfile).** `npm ci` requires a committed `package-lock.json`. Lockfiles were never committed, so the build failed immediately. Both build stages use `npm install` instead — which is why no lockfiles are present in the repo.
|
||||
|
||||
**TypeScript DOM lib missing (backend).** `scraper.ts` uses `page.evaluate()`, whose callback runs in the browser context. TypeScript flagged `document`, `navigator`, and `HTMLMetaElement` as unknown because the backend `tsconfig.json` only included `"lib": ["ES2020"]`. Fixed by adding the DOM libs:
|
||||
|
||||
```json
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"]
|
||||
```
|
||||
|
||||
`DOM.Iterable` was specifically required to allow `for...of` iteration over `NodeListOf<HTMLSpanElement>`.
|
||||
|
||||
**Two containers → one container.** The original design used separate `frontend` and `backend` Docker services in docker-compose. These were consolidated into a single container running nginx and Node side by side under supervisord, with nginx proxying `/api/` to `localhost:3001`. Removed `backend/Dockerfile`, `frontend/Dockerfile`, and `frontend/nginx.conf`; the root-level `Dockerfile`, `nginx.conf`, and `supervisord.conf` replaced them.
|
||||
|
||||
Reference in New Issue
Block a user