Site-agnostic job-portal monitor with pluggable adapters and Telegram alerts
Project description
๐ก Job Sentinel
Site-agnostic job-portal monitor with pluggable adapters and instant Telegram alerts.
Job Sentinel monitors job-listing portals on a configurable interval. The moment a new posting appears, you get a rich Telegram alert โ with title, employer, location, deadline, keyword tags, and a direct link.
It ships with adapters for UTD 12twenty and Handshake. Adding a new portal takes one file and ~50 lines of Python.
Out of the box it watches UTD 12twenty's on-campus Student Employment tab.
The tab is chosen by the tab= parameter in PORTAL_JOBS_URL, so the same
setup later points at internships or full-time listings by switching that URL.
โจ Features
| Feature | Details |
|---|---|
| Pluggable adapters | One Python file per portal โ no core changes needed |
| Telegram bot | Rich alerts + commands (/jobs, /applied, /stats, /deadlines, โฆ) |
| Rรฉsumรฉ engine | Universal profile โ ATS-friendly LaTeX/PDF, tailored per posting |
| Local-LLM tailoring | Optional Ollama rephrasing โ no API key, nothing leaves your machine |
| Web UI | Next.js + Tailwind app: profile editor, rรฉsumรฉ studio, jobs board |
| Local API | FastAPI layer (job-sentinel serve) the UI consumes โ one source of truth |
| Email + Telegram alerts | Two notifier channels; email is optional SMTP |
| Deadline awareness | /deadlines flags postings closing within a configurable window |
| Status tracking | NEW โ SEEN โ APPLIED / IGNORED / CLOSED, persisted in SQLite |
| Closed detection | Marks postings that disappear from the portal |
| Production-grade | mypy --strict, ~82% tests, CI (lint/types/tests/secret/supply-chain), Docker |
๐ Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Job Sentinel โ
โ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ
โ โ Scheduler โโโโโถโ Adapter โโโโโถโ JobRepository โ โ
โ โ (APSchedulerโ โ (Playwrightโ โ (sqlite-utils) โ โ
โ โ background)โ โ + site โ โ โ โ
โ โโโโโโโโฌโโโโโโโโ โ plugin) โ โโโโโโโโโโโโโโโโโโโโ โ
โ โ โโโโโโโโโโโโโโโ โ โ
โ โ โ โ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Telegram โโโโโโโโโโโโโโโโโโโโโโ Bot Handlers โ โ
โ โ Notifier โ alerts + cmds โ (python-telegram- โ โ
โ โ (httpx) โ โ bot v21, async) โ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
See docs/design/HLD.md for the full High-Level Design and docs/design/LLD.md for Low-Level Design.
๐ Quick Start
1. Prerequisites
- Python 3.11+
- uv (install:
curl -LsSf https://astral.sh/uv/install.sh | sh) - A Telegram account
2. Clone & Install
git clone https://github.com/harshitwandhare/job-sentinel.git
cd job-sentinel
# Install all dependencies (creates .venv automatically)
uv sync
# Install Playwright's Chromium browser
uv run playwright install chromium
3. Create your Telegram bot
- Open Telegram and message @BotFather
- Send
/newbotand follow the prompts โ copy the token - Message your new bot once, then visit:
https://api.telegram.org/bot<TOKEN>/getUpdates - Copy your chat ID from the JSON response (
message.chat.id)
4. Configure
cp .env.example .env
# Edit .env and fill in:
# TELEGRAM_BOT_TOKEN=...
# TELEGRAM_CHAT_ID=...
# PORTAL_USERNAME=your_utd_netid
# PORTAL_PASSWORD=your_password
5. Test run (dry run โ no messages sent)
uv run job-sentinel scrape
6. Start the full bot
uv run job-sentinel run
That's it. Open Telegram and send /start to your bot.
โ๏ธ Configuration
All configuration is via environment variables in .env.
See .env.example for the full reference.
| Variable | Default | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN |
โ | Required. From @BotFather |
TELEGRAM_CHAT_ID |
โ | Required. Your Telegram user/chat ID |
PORTAL_USERNAME |
โ | Required. Portal login |
PORTAL_PASSWORD |
โ | Required. Portal password |
PORTAL_JOBS_URL |
UTD 12twenty URL | Full URL to the listings page |
SITE_ADAPTER |
12twenty |
Adapter to use |
POLL_INTERVAL_SECONDS |
900 |
Scrape interval (min: 60) |
KEYWORD_FILTERS |
(empty = all) | CSV: software,engineer,research |
HEADLESS |
true |
Run browser headless |
DRY_RUN |
false |
Scrape but don't send alerts |
LOG_LEVEL |
INFO |
DEBUG/INFO/WARNING/ERROR |
๐ค Bot Commands
| Command | Description |
|---|---|
/jobs |
Trigger a fresh scrape + show recent postings |
/recent |
Show last 10 jobs from the database |
/applied <id> |
Mark posting as applied |
/ignore <id> |
Dismiss a posting |
/status <id> |
Full details of a specific posting |
/stats |
Counts by status (new / seen / applied / ignored / closed) |
/deadlines |
Postings closing within DEADLINE_ALERT_DAYS |
/filters |
Show active keyword filters |
/adapters |
List available site adapters |
/ping |
Health check |
๐ Rรฉsumรฉ Generator
Job Sentinel keeps a universal profile โ your master CV data in one hand-editable YAML file โ and renders ATS-friendly PDFs from it. It's standalone: you don't need the Telegram bot configured to use it.
# 1. Scaffold a profile you can edit like an Overleaf source
uv run job-sentinel resume init # writes data/profile.yaml
# 2. Edit data/profile.yaml โ add education, experience, projects, skillsโฆ
# 3. Build an ATS-friendly PDF (also writes the .tex next to it)
uv run job-sentinel resume build -o data/resume.pdf
uv run job-sentinel resume show # summarise your profile
# 4. Tailor to a specific posting โ reorders content by relevance and
# reports ATS keyword coverage (matched vs missing terms)
uv run job-sentinel resume build --job-text "paste a job description here"
uv run job-sentinel resume build --job-id <posting_id> # a posting already scraped
# 5. Generate a tailored cover letter (deterministic, or --ai to polish locally)
uv run job-sentinel resume cover --job-text "โฆ" --role "Student Assistant" --company "UTD"
PDF rendering uses Tectonic (a self-contained LaTeX engine โ no full TeX install needed). Install it once:
winget install TectonicProject.Tectonic # Windows
brew install tectonic # macOS
cargo install tectonic # Linux (or your package manager)
If Tectonic isn't installed, resume build still writes the .tex so you can
compile it on Overleaf. The template is single-column with standard fonts and
real selectable text, so it parses cleanly through ATS.
Optional: local-LLM rephrasing (no API key)
Add --ai to rephrase your bullets toward a posting using a local model via
Ollama โ fully offline, no API key, your data never leaves
your machine. It only rephrases content already in your profile (it can't
invent facts), and falls back to keyword tailoring if the model isn't available.
job-sentinel resume doctor --pull # checks Ollama + pulls the model
job-sentinel resume build --ai --job-text "paste a job description"
๐ฅ Web UI
Prefer a UI? Job Sentinel ships a local web app (Next.js + Tailwind) over a FastAPI layer โ same engine, nicer surface. It's fully local: the API binds to localhost and the optional LLM stays on your machine.
# 1. Start the local API (needs the 'web' extra: uv sync --extra web)
job-sentinel serve # http://127.0.0.1:8000 (Swagger at /docs)
# 2. Start the web app
cd web && npm install && npm run dev # http://localhost:3000
Pages: an animated landing, a profile editor, a rรฉsumรฉ studio (paste a JD โ live ATS coverage โ download a tailored PDF, with a local-LLM toggle), and a jobs board with statuses and deadlines.
๐ Adding a New Portal
- Create
src/job_sentinel/adapters/sites/my_portal.py - Subclass
SiteAdapter, implementlogin()andscrape_page() - Set
SITE_ADAPTER=my_portalin your.env
Full guide: docs/design/adapter-authoring.md
๐ Development
# Install dev dependencies
uv sync --all-extras
# Install pre-commit hooks (runs ruff, mypy, secret scan on every commit)
uv run pre-commit install
# Run tests
uv run pytest
# Lint & format
uv run ruff check --fix .
uv run ruff format .
# Type check
uv run mypy src/
# All at once (same as CI)
uv run pre-commit run --all-files
๐ณ Deployment (always-on) & data persistence
Run it continuously with Docker โ the image is based on the official Playwright image (Chromium + system libs included):
docker compose up -d --build # start detached
docker compose logs -f # follow
Your data never vanishes on restart. ./data and ./logs are bind-mounted
from the host, so the SQLite database, captured login session, and your profile
live on disk โ surviving container restarts, rebuilds, and reboots.
12twenty's login is Cloudflare-gated, so capture a session on the host first with
job-sentinel login(it writesdata/session.json, which the container mounts and reuses). Re-runloginif the session expires.
Backups. Everything important is in data/. Back it up while the bot is
idle โ e.g. a WAL-safe SQLite copy:
sqlite3 data/jobs.db ".backup data/jobs.backup.db"
cp data/profile.yaml data/profile.backup.yaml
๐ Project Structure
job-sentinel/
โโโ src/job_sentinel/
โ โโโ adapters/
โ โ โโโ base.py # Abstract SiteAdapter interface
โ โ โโโ registry.py # Plugin registry (dynamic loading)
โ โ โโโ sites/
โ โ โโโ twelve_twenty.py # UTD 12twenty adapter
โ โ โโโ handshake.py # Handshake adapter
โ โโโ bot/
โ โ โโโ handlers.py # Telegram command handlers
โ โโโ config/
โ โ โโโ settings.py # pydantic-settings config
โ โ โโโ logging.py # loguru setup
โ โโโ core/
โ โ โโโ browser.py # Playwright lifecycle manager
โ โ โโโ models.py # JobPosting, ScrapeResult (Pydantic v2)
โ โ โโโ scheduler.py # APScheduler poll loop
โ โโโ db/
โ โ โโโ repository.py # sqlite-utils DB layer
โ โโโ notifiers/
โ โ โโโ telegram.py # MarkdownV2 formatting + delivery
โ โโโ __main__.py # Typer CLI entry-point
โโโ tests/
โ โโโ unit/ # Fast, no I/O
โ โโโ integration/ # Real DB, mocked network
โ โโโ e2e/ # Full stack (optional, requires .env)
โโโ docs/
โ โโโ adr/ # Architecture Decision Records
โ โโโ design/ # HLD, LLD, adapter authoring guide
โโโ scripts/ # Dev helper scripts
โโโ .github/workflows/ # CI/CD (GitHub Actions)
โโโ pyproject.toml # Single source of truth (uv + hatchling)
โโโ .env.example # Config template
โโโ .pre-commit-config.yaml # Ruff, mypy, gitleaks, conventional commits
๐ Roadmap
- Rรฉsumรฉ engine (universal profile โ ATS LaTeX/PDF) with per-posting tailoring
- Local-LLM rephrasing via Ollama (no API key)
- Web UI (Next.js) + local FastAPI layer
- Email notifier (optional SMTP) alongside Telegram
- Deadline-aware tracking (
/deadlines) - Docker / docker-compose with persistent data
- Cover-letter generation (local LLM)
- Semantic relevance ranking (local embeddings)
- More portal adapters (Greenhouse, Workday, public boards via JobSpy)
- Discord webhook notifier
- Packaged installers + PyPI publish
๐ค Contributing
Contributions are welcome! Please read CONTRIBUTING.md first.
All commits must follow Conventional Commits.
Run uv run pre-commit install to enforce this automatically.
๐ License
MIT ยฉ Harshit Wandhare โ see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file job_sentinel-0.3.1.tar.gz.
File metadata
- Download URL: job_sentinel-0.3.1.tar.gz
- Upload date:
- Size: 142.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3744d36b400495c0c66be00039a4c13a6180c395cca98ba70e31f5245e2b42b5
|
|
| MD5 |
31fc4763ce506134c57026fd089e25cc
|
|
| BLAKE2b-256 |
0fadcd03acc7e47a3b507bd15cae377885b1b98cbb3eaf048e12fd266346582c
|
File details
Details for the file job_sentinel-0.3.1-py3-none-any.whl.
File metadata
- Download URL: job_sentinel-0.3.1-py3-none-any.whl
- Upload date:
- Size: 75.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
168a243cc46ca12a34faefcd7f3a95eb881059560e405cd94de5389a33a15a20
|
|
| MD5 |
65bee18095ca1cc1e276564842a95128
|
|
| BLAKE2b-256 |
ada9f0b3b901f5cf4cc9007ae66e159e5b5cdc840585027a78ff02aa2a434b9a
|