UI Stock Tracker
Monitors product pages on store.ui.com for stock availability and fires a Telegram alert the moment a watched item comes back in stock. Runs as a single self-contained Docker container on Unraid with a web UI for managing tracked products.
Status: Complete and running.
How It Works
A Puppeteer scraper visits each watched product URL on a per-item timer, waits for the React page to hydrate, and classifies the page as in_stock, sold_out, or unknown by inspecting the Add-to-Cart / notify buttons. When an item transitions to in_stock, the backend sends a one-time Telegram alert. You re-arm the item in the UI to enable the next alert, so you get notified without being spammed.
Stock detection rules:
- A
<button>withlabel="Add to Cart", or whose text contains"add to cart"→in_stock. - A
<button>whose text contains"login for notifications","login for updates","notify me when available","notify me","sold out","out of stock","currently unavailable", or"coming soon"→sold_out. - Neither found →
unknown(logs a debug payload with page title and button texts).
The scraper runs logged out, so Ubiquiti shows "Login for Notifications" on sold-out items — that string is in the sold-out list by design. Product name comes from the og:title meta tag (store suffix stripped) and the thumbnail from og:image.
Stack
| Layer | Technology |
|---|---|
| Frontend | React 18 + TypeScript, Vite, Lucide icons |
| Backend | Node.js 20 + TypeScript, Express |
| Database | SQLite via better-sqlite3 (WAL mode) |
| Scraper | puppeteer-core + system Chromium |
| Alerts | Telegram Bot API (direct HTTP) |
| Container | Single Docker image — nginx + Node managed by supervisord |
Architecture
One container runs two processes under supervisord:
supervisord
├── nginx → serves the React build on :8080, proxies /api/ → 127.0.0.1:3001
└── node → Express API, SQLite, Puppeteer scheduler, Telegram sender (:3001)
The SQLite database lives on a mounted volume at /app/data/tracker.db, so it persists across rebuilds.
Backend runtime behavior:
- Scheduler — each active item runs its own in-memory
setInterval; timers start automatically on boot from the DB. Minimum interval is 30 seconds (enforced backend and frontend). Pause / Resume / Edit update both the timer map and the DB immediately. - Concurrency — a semaphore caps concurrent Puppeteer instances at 2 to avoid OOM on Unraid.
- Alert logic — an alert fires once when an item goes
in_stockwithalert_sent = 0, then setsalert_sent = 1. Re-arm in the UI resets it to0. - Frontend polling — the UI polls
GET /api/itemsevery 10 seconds.
Repository Layout
ui-tracker/
├── Dockerfile # 3-stage single-container build
├── docker-compose.yml # Single service, port 8080, ./data volume
├── nginx.conf # Serves frontend, proxies /api/ to Node
├── supervisord.conf # Keeps nginx + node alive
├── .gitea/workflows/ # CI: build + push image on push to main
├── data/ # SQLite DB (volume mount)
├── backend/
│ └── src/
│ ├── index.ts # Express entry + scheduler init
│ ├── database.ts # Schema, getDb(), WatchedItem type
│ ├── scraper.ts # Puppeteer stock check + semaphore
│ ├── scheduler.ts # Per-item timers + alert logic
│ ├── telegram.ts # Bot API HTTP calls
│ └── routes/ # items.ts, settings.ts
└── frontend/
└── src/
├── App.tsx # Root, 10s polling, connection indicator
├── api/client.ts # Typed fetch wrappers
├── types/index.ts
└── components/ # ItemCard, AddItemModal, EditItemModal, SettingsModal
API
| Method | Path | Description |
|---|---|---|
| GET | /api/items |
List all watched items |
| POST | /api/items |
Add new item (url, check_interval) |
| PUT | /api/items/:id |
Update interval or active state |
| DELETE | /api/items/:id |
Remove item |
| POST | /api/items/:id/pause |
Stop checking |
| POST | /api/items/:id/resume |
Resume checking |
| POST | /api/items/:id/reset |
Clear alert_sent (re-arm) |
| GET | /api/settings |
Get Telegram credentials |
| PUT | /api/settings |
Save Telegram credentials |
| POST | /api/settings/test-telegram |
Send a test Telegram message |
| GET | /api/health |
Health check |
Database Schema
watched_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL UNIQUE,
name TEXT, -- populated after first scrape
thumbnail_url TEXT, -- populated after first scrape
check_interval INTEGER NOT NULL DEFAULT 60, -- seconds, min 30
is_active INTEGER NOT NULL DEFAULT 1, -- 0 = paused
last_status TEXT NOT NULL DEFAULT 'unknown', -- in_stock | sold_out | unknown
alert_sent INTEGER NOT NULL DEFAULT 0, -- 1 = alert fired, awaiting re-arm
check_count INTEGER NOT NULL DEFAULT 0,
last_checked_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
settings (
key TEXT PRIMARY KEY, -- telegram_bot_token | telegram_chat_id
value TEXT NOT NULL DEFAULT ''
)
Local Development
The backend and frontend run independently in dev.
# Backend (http://localhost:3001)
cd backend
npm install
npm run dev # ts-node-dev, respawns on change
# Frontend (http://localhost:5173, Vite)
cd frontend
npm install
npm run dev
Backend env vars (set automatically in the container via supervisord.conf): PORT (default 3001), DATABASE_PATH (default /app/data/tracker.db), PUPPETEER_EXECUTABLE_PATH (default /usr/bin/chromium).
Lockfiles are not committed, so the Docker build uses
npm installrather thannpm ci.
Build & Run (Docker)
docker build -t ui-tracker .
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.
CI/CD
.gitea/workflows/docker-build.yml builds and pushes the image on every push to main:
- Checks out the repo.
- Logs in to the private registry at
registry.alwisp.com(secrets.REGISTRY_USER/secrets.REGISTRY_TOKEN). - Builds and pushes
registry.alwisp.com/{owner}/{repo}:latest.
Pull the updated image on Unraid afterward:
docker pull registry.alwisp.com/{owner}/{repo}:latest
Telegram Configuration
Configure via Settings in the UI, then use Test Alert to verify.
| Field | Value |
|---|---|
| Bot Token | 8769097441:AAFBqPlSTcTIi3I-F5ZIN9EEpwbNDzHg8hM |
| Chat ID | 8435449432 |
⚠️ 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
- PROJECT.md — deeper design notes and development history.
- UNRAID.md — step-by-step Unraid install and troubleshooting.