This commit is contained in:
@@ -1,47 +1,173 @@
|
||||
# Drop-In Agent Instruction Suite
|
||||
# UI Stock Tracker
|
||||
|
||||
This repository is a portable markdown instruction pack for coding agents.
|
||||
Monitors product pages on [store.ui.com](https://store.ui.com/us/en) 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.
|
||||
|
||||
Copy these files into another repository to give the agent:
|
||||
- a root `AGENTS.md` entrypoint,
|
||||
- a central skill index,
|
||||
- category hubs for routing,
|
||||
- specialized skill files for common software, docs, UX, marketing, and ideation tasks.
|
||||
**Status:** Complete and running.
|
||||
|
||||
## Structure
|
||||
## How It Works
|
||||
|
||||
- `AGENTS.md` - base instructions and routing rules
|
||||
- `DEPLOYMENT-PROFILE.md` - agent-readable prefilled deployment defaults
|
||||
- `INSTALL.md` - copy and customization guide for other repositories
|
||||
- `PROJECT-PROFILE-WORKBOOK.md` - one-time questionnaire for staging defaults
|
||||
- `SKILLS.md` - canonical skill index
|
||||
- `ROUTING-EXAMPLES.md` - representative prompt-to-skill routing examples
|
||||
- `hubs/` - category-level routing guides
|
||||
- `skills/` - specialized reusable skill files
|
||||
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.
|
||||
|
||||
## Design Goals
|
||||
Stock detection rules:
|
||||
|
||||
- Plain markdown only
|
||||
- Cross-agent portability
|
||||
- Implementation-first defaults
|
||||
- On-demand skill loading instead of loading everything every session
|
||||
- Context-efficient routing for large skill libraries
|
||||
- Prefilled deployment defaults without per-install questioning
|
||||
- Repo-local instructions take precedence over this bundle
|
||||
- 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).
|
||||
|
||||
## Intended Workflow
|
||||
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`.
|
||||
|
||||
1. The agent reads `AGENTS.md`.
|
||||
2. The agent reads `DEPLOYMENT-PROFILE.md` when it is filled in.
|
||||
3. The agent checks `SKILLS.md`.
|
||||
4. The agent opens only the relevant hub and skill files for the task.
|
||||
5. The agent combines multiple skills when the task spans several domains.
|
||||
## Stack
|
||||
|
||||
## Core Categories
|
||||
| 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 |
|
||||
|
||||
- Software development
|
||||
- Debugging
|
||||
- Documentation
|
||||
- UI/UX
|
||||
- Marketing
|
||||
- Brainstorming
|
||||
## 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
|
||||
|
||||
```sql
|
||||
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.
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
|
||||
```bash
|
||||
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](./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:
|
||||
|
||||
```bash
|
||||
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](./PROJECT.md) — deeper design notes and development history.
|
||||
- [UNRAID.md](./UNRAID.md) — step-by-step Unraid install and troubleshooting.
|
||||
|
||||
Reference in New Issue
Block a user