Files
job/scrapper/README.md
T
nikolaandClaude Opus 5.5 784c3c5220 Add --host flag, listen on 127.0.0.1 by default
The server previously bound to all interfaces with no auth. Default to
loopback so it sits behind a reverse proxy; --host (or JOB_BOARD_HOST)
overrides it. Plain HOST is ignored since some environments set it to
the machine hostname.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:43:34 +02:00

96 lines
3.9 KiB
Markdown

# Job Board
Scrapes job sites once a day, stores results in SQLite (`data/jobs.sqlite`), and shows them in a web UI where you can mark jobs as seen.
No runtime dependencies. It uses Bun's built-in SQLite, HTTP server and HTMLRewriter.
## Sources
| Source | How | Notes |
|---|---|---|
| slo-tech.com (Ljubljana) | HTML scrape | ~20 newest jobs |
| startup.si | HTML scrape (no public API or feed) | Site lists every posting since 2019 on one page, so only the last 90 days are kept (`startupSi(days)` in `src/scrapers/index.ts`) |
| adecco.com (Slovenia, IT) | Internal JSON API the site's job search uses (no public API) | Filtered by category; add more with `adecco("<CategoryFilter value>", "<label>")` |
| mojedelo.com (IT, Osrednjeslovenska) | Internal JSON API the site's search uses (no public API) | Takes a search URL copied from the site; add more with `mojeDelo("<label>", "<search URL>")` (don't rename the label later, it's part of the source ID) |
| optius.com (IT, Osrednjeslovenska) | Official RSS feed | Build a feed URL at https://www.optius.com/rss/ (its IDs differ from the search page's); add more with `optius("<label>", "<feed URL>")` (same label rule as MojeDelo) |
## Run
```sh
bun install
bun start # http://127.0.0.1:3000
bun start --port 8080 # or -p 8080
```
The server scrapes every 24h. On startup it waits out the rest of the 24h since the last scrape (or scrapes right away if none happened in the last 24h).
You can also click **Scrape now** in the UI, or run `bun run scrape` once (e.g. from cron).
Env vars: `PORT` (3000; `--port` overrides it), `JOB_BOARD_HOST` (127.0.0.1; `--host` overrides it), `SCRAPE_INTERVAL_HOURS` (24), `DB_PATH` (`data/jobs.sqlite`).
## Build a binary for a server
`bun build --compile` bundles the app and the Bun runtime into one executable (~90 MB), so the
server doesn't need Bun or `node_modules`:
```sh
bun run build # bin/job-board for this machine
bun run build:linux-x64 # bin/job-board-linux-x64
bun run build:linux-arm64 # bin/job-board-linux-arm64 (e.g. Raspberry Pi, ARM VPS)
```
On the server:
```sh
./job-board # http://127.0.0.1:3000, scrapes every 24h
./job-board --port 8080
./job-board --host 0.0.0.0 --port 8080 # listen on all interfaces
./job-board scrape # scrape once and exit (exit code 1 if a scraper failed)
```
It listens on loopback (`127.0.0.1`) only by default, so it's meant to sit behind a reverse proxy
(nginx, Caddy). There is no login: only use `--host 0.0.0.0` on a trusted network.
The database goes to `data/jobs.sqlite` next to the binary (override with `DB_PATH`).
The binaries target glibc Linux; for Alpine use `--target=bun-linux-x64-musl`, and for old CPUs
without AVX2 use `--target=bun-linux-x64-baseline`.
Example systemd unit (`/etc/systemd/system/job-board.service`):
```ini
[Unit]
Description=Job board
After=network-online.target
[Service]
ExecStart=/opt/job-board/job-board --host 127.0.0.1 --port 3000
Restart=on-failure
User=jobboard
[Install]
WantedBy=multi-user.target
```
## UI
- **New / Seen / All** tabs. Opening a job, or clicking *Mark seen*, moves it to Seen.
- **no longer listed** means the job was missing from that source's latest successful scrape.
- Search filters on title, company and tags. Recent scrape runs (and errors) are listed at the bottom.
## Adding a site
1. Create `src/scrapers/<site>.ts` exporting an object that implements `Scraper` (`src/scrapers/types.ts`):
```ts
export const mySite: Scraper = {
id: "my-site", // stable, stored in DB
name: "My Site",
async scrape() {
const html = await fetchHtml("https://..."); // helper in ./http.ts
return [/* { externalId, title, url, company?, location?, tags?, postedAt? } */];
},
};
```
2. Add it to the list in `src/scrapers/index.ts`.
`externalId` must stay stable for a job, because it is what decides whether a job is new.
For another slo-tech city, just add `sloTech("Maribor")` to the list.