-32
@@ -1,32 +0,0 @@
|
|||||||
# UI Stock Tracker — Design Notes & Development History
|
|
||||||
|
|
||||||
Supplementary notes that go beyond the [README](./README.md): the reasoning behind the trickier parts of the scraper and the notable fixes made during development. For setup, architecture, API, and schema, see the README; for deployment see [UNRAID.md](./UNRAID.md).
|
|
||||||
|
|
||||||
## Stock Detection Rationale
|
|
||||||
|
|
||||||
The scraper classifies each product page as `in_stock`, `sold_out`, or `unknown` 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, not an oversight.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
### 1. `npm ci` → `npm install` (Dockerfile)
|
|
||||||
`npm ci` requires a committed `package-lock.json`. Lockfiles were never committed, so the build failed immediately. Both the frontend and backend build stages use `npm install` instead. (This is why no lockfiles are present in the repo.)
|
|
||||||
|
|
||||||
### 2. 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>`.
|
|
||||||
|
|
||||||
### 3. 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.
|
|
||||||
@@ -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.
|
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
|
## 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.
|
> ⚠️ 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.
|
### 1. Clone and build the image (CLI)
|
||||||
- [UNRAID.md](./UNRAID.md) — step-by-step Unraid install and troubleshooting.
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
# Unraid Installation Guide — UI Stock Tracker
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1 — CLI: Clone and Build the Image
|
|
||||||
|
|
||||||
SSH into your Unraid server and run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /mnt/user/appdata/ui-tracker
|
|
||||||
cd /mnt/user/appdata/ui-tracker
|
|
||||||
git clone https://github.com/<your-username>/ui-tracker.git .
|
|
||||||
docker build -t ui-tracker .
|
|
||||||
```
|
|
||||||
|
|
||||||
The first build takes a few minutes — it compiles the React frontend, compiles the Node.js backend, and installs Chromium inside the image. Once complete, the image is available locally as `ui-tracker`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2 — GUI: Add the Container in Unraid
|
|
||||||
|
|
||||||
In the Unraid web UI go to **Docker → Add Container**.
|
|
||||||
|
|
||||||
Fill in each section as follows:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 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. You will access the UI directly at `http://<fixed-ip>:8080` — no port conflict with the Unraid host.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Port
|
|
||||||
|
|
||||||
Click **Add another Path, Port, Variable, Label or Device** → select **Port**.
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|---|---|
|
|
||||||
| Name | `Web UI` |
|
|
||||||
| Container Port | `8080` |
|
|
||||||
| Host Port | `8080` |
|
|
||||||
| Protocol | `TCP` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Path (Persistent Data)
|
|
||||||
|
|
||||||
Click **Add another Path, Port, Variable, Label or Device** → select **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. All tracked items and Telegram settings persist here across restarts and rebuilds.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Variables
|
|
||||||
|
|
||||||
No environment variables are required — all configuration is baked into the image.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Click **Apply** to create and start the container.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3 — First-Time Setup
|
|
||||||
|
|
||||||
Open the UI at `http://<fixed-ip>:8080` and:
|
|
||||||
|
|
||||||
1. Click **Settings** (top right)
|
|
||||||
2. Enter **Bot Token**: `8769097441:AAFBqPlSTcTIi3I-F5ZIN9EEpwbNDzHg8hM`
|
|
||||||
3. Enter **Chat ID**: `8435449432`
|
|
||||||
4. Click **Test Alert** — a Telegram message should arrive within seconds
|
|
||||||
5. Click **Save**
|
|
||||||
6. Click **Add Item**, paste a `store.ui.com` product URL, set your check interval, 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 go back to **Docker → Add Container** and re-add it 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 your 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 from the Docker tab — Chromium occasionally needs a clean start
|
|
||||||
Reference in New Issue
Block a user