Jason Stedwell c0a44e7c04
Build and Push Docker Image / build (push) Failing after 15s
doc cleanup
2026-06-22 19:51:24 -05:00
2026-06-06 14:23:43 -05:00
2026-03-27 23:33:31 -05:00
2026-03-27 23:42:39 -05:00
2026-03-27 23:33:31 -05:00
2026-03-27 23:42:39 -05:00
2026-03-27 23:50:18 -05:00
2026-03-27 23:42:39 -05:00
2026-06-22 19:51:24 -05:00
2026-06-22 19:51:24 -05:00
2026-03-27 23:42:39 -05:00
2026-03-27 23:47:39 -05:00

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> with label="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_stock with alert_sent = 0, then sets alert_sent = 1. Re-arm in the UI resets it to 0.
  • Frontend polling — the UI polls GET /api/items every 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 install rather than npm 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:

  1. Checks out the repo.
  2. Logs in to the private registry at registry.alwisp.com (secrets.REGISTRY_USER / secrets.REGISTRY_TOKEN).
  3. 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.
S
Description
Unifi Store stock monitor — watches store.ui.com for product restocks and sends Telegram alerts; single Docker container for Unraid with a React admin UI
Readme 180 KiB
Languages
TypeScript 70.5%
CSS 25.6%
Dockerfile 3.1%
HTML 0.8%