- Go 96%
- HTML 3.5%
- Dockerfile 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| cmd/jobwire | ||
| deploy | ||
| docs/specs | ||
| internal | ||
| migrations | ||
| scripts | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| compose.yaml | ||
| config.container.example.yaml | ||
| config.example.yaml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
jobwire — Job Aggregator MVP
Self-hosted агрегатор вакансий на Go + SQLite: discover now → use now → enrich later.
Источники: Amazon / AWS (amazon.jobs), Greenhouse, Lever и Workday.
jobwire sync получает дешёвые метаданные и публикует их. jobwire enrich независимо обрабатывает постоянную очередь описаний. jobwire serve предоставляет JSON API, server-rendered HTML и RSS. Discovery никогда не вызывает массовую загрузку деталей.
Запуск
Нужен Go 1.25+. SQLite использует pure-Go драйвер modernc.org/sqlite, без CGO или отдельного сервера БД.
Локальные бинарники собираются в bin/, релизные архивы — в dist/. Обе директории исключены из Git.
Всегда указывайте -o при сборке: go build ./cmd/jobwire без этого параметра создаст бинарник в корне проекта.
go build -o bin/jobwire.exe ./cmd/jobwire
Copy-Item config.example.yaml config.yaml
# Добавьте targets в config.yaml.
.\bin\jobwire.exe sync
.\bin\jobwire.exe serve
# В другом терминале:
.\bin\jobwire.exe enrich
Linux:
go build -o bin/jobwire ./cmd/jobwire
cp config.example.yaml config.yaml
./bin/jobwire sync
./bin/jobwire serve
# В другом терминале:
./bin/jobwire enrich
Шаблоны и миграции встроены в один бинарник. БД и её родительская директория создаются автоматически. Явно заданные относительные пути отсчитываются от рабочей директории процесса, а не файла config.
Версия
jobwire --version и jobwire version выводят встроенную в бинарник версию, например jobwire v0.1.0.
Команда не требует конфигурации, Git или доступа к базе данных.
Обычная локальная сборка показывает dev, CI-сборка — snapshot, релизная — git-тег.
scripts/release.go передаёт версию через Go linker flag -X main.version=....
Для сборки контейнера с выбранной версией:
docker build --build-arg VERSION=v0.1.0 -t jobwire:v0.1.0 .
docker run --rm jobwire:v0.1.0 --version
Без VERSION контейнерная сборка также показывает dev.
Расположение базы данных
| Платформа | База по умолчанию |
|---|---|
| Linux / другие Unix | $XDG_DATA_HOME/jobwire/jobwire.db или ~/.local/share/jobwire/jobwire.db |
| macOS | ~/Library/Application Support/jobwire/jobwire.db |
| Windows | %LOCALAPPDATA%\jobwire\jobwire.db |
Относительный XDG_DATA_HOME игнорируется. На Windows при отсутствии LOCALAPPDATA
используется ~/AppData/Local/jobwire/jobwire.db. На macOS XDG_DATA_HOME не используется.
Путь можно переопределить через JOBWIRE_DB или глобальный флаг --db:
jobwire --db ./sample.db sync
jobwire --db ./sample.db serve
Приоритет: --db → JOBWIRE_DB → поле database в YAML → системный путь.
Поле YAML сохранено для совместимости; если оно отсутствует или пустое, используется системный путь
(при отсутствии флага и environment override). Флаг --db также принимается после команды.
Все процессы serve/sync/enrich должны использовать один и тот же путь БД.
Старая ./jobs.db автоматически не импортируется. При обновлении существующей установки остановите
все её процессы, сделайте резервную копию, выполните SQLite PRAGMA wal_checkpoint(TRUNCATE)
и убедитесь, что checkpoint завершился без busy. Закройте SQLite перед переносом базы.
Перенесите jobs.db в новый каталог под именем jobwire.db, затем удалите старое
database: ./jobs.db из конфига. Не отбрасывайте WAL без успешного checkpoint:
он может содержать ещё не записанные в основной файл данные.
В качестве альтернативы сохраните существующую базу на месте и задайте её путь явно.
Команды
| Команда | Поведение |
|---|---|
jobwire sync |
Один discovery всех настроенных targets; без Workday detail requests |
jobwire enrich |
Постоянные workers, ожидание новых и отложенных задач |
jobwire enrich -once |
Обработать доступные сейчас задачи в пределах бюджета и выйти |
jobwire serve |
HTTP API, HTML и RSS |
jobwire providers |
Названия поддерживаемых провайдеров |
Для каждой команды доступен -config path.yaml; флаги задаются после команды.
enrich -once не ждёт будущего next_attempt_at и не опустошает очередь сверх бюджета. Exit code 1 означает ошибки обработанных попыток, остальные задачи при этом продолжают обрабатываться.
В режиме service отдельная ошибка вакансии логируется и не останавливает workers; ошибка самого хранилища завершает процесс.
Ctrl+C отменяет сетевые запросы и возвращает незавершённые задачи в очередь. После аварийного завершения задачи становятся доступны по истечении двухминутной аренды.
Конфигурация
Сохранена совместимость с прежней формой sources: provider: [targets]. Настройки ограничений вынесены в providers.
# database: /custom/path/jobwire.db # optional; otherwise use the platform default
server:
listen: 127.0.0.1:8080
base_url: https://jobs.yarikus.com
enrichment:
workers: 8
refresh_after: 72h
max_attempts: 5
providers:
workday:
concurrency: 2
requests_per_second: 2
max_detail_requests_per_run: 200
budget_window: 1h
greenhouse:
concurrency: 8
lever:
concurrency: 8
amazon:
requests_per_second: 2
sources:
amazon:
- name: amazon-austin-tx
company: Amazon / AWS
max_pages: 20
amazon:
normalized_country_code: [USA]
normalized_state_name: [Texas]
normalized_city_name: [Austin]
# base_query: <нужные ключевые слова>
greenhouse:
- name: your-board-token
company: Example Company
lever:
- name: your-site-name
company: Another Company
region: global # либо eu
workday:
- name: nvidia-external
company: NVIDIA
url: https://nvidia.wd5.myworkdayjobs.com
tenant: nvidia
site: NVIDIAExternalCareerSite
В config.example.yaml targets пустые: добавьте нужные boards явно. Для Greenhouse/Lever доступна короткая форма greenhouse: [board-a, board-b]; без company отображается slug.
- Greenhouse:
name— board token. - Lever:
name— site name; регион по умолчанию global. - Workday: обязательны стабильное имя target, HTTPS origin
*.myworkdayjobs.com, tenant и site. Workday data-center hostname нельзя надёжно вывести из названия компании. - Amazon:
name— стабильное имя отфильтрованного поиска; параметры поиска задаются в блокеamazon, подробнее ниже. base_url— origin для RSS channel link; не берётся из HTTP Host.- По умолчанию: 8 общих workers, 72h TTL, 5 попыток. Workday и Amazon: concurrency 2 и 2 запроса/сек; остальные providers: concurrency 8 и 4 запроса/сек. Amazon выполняет listing последовательно и не использует detail workers.
- Бюджет по умолчанию — 200 detail requests на provider за запуск
-once. В постоянном процессе бюджет возобновляется раз вbudget_window, по умолчанию 1h. - Бюджет учитывает попытки, включая ошибочные запросы. Он локален процессу и сбрасывается после рестарта. Запускайте один
enrichservice; несколько процессов умножат бюджеты и лимиты конкурентности. - Нулевые значения provider limits означают defaults, а не unlimited.
enrichment.workers,refresh_afterиmax_attemptsдолжны быть положительными. - Конфигурация читается при запуске; после изменения sources или limits перезапустите соответствующие процессы.
Публичные endpoints не требуют secrets. Текущая YAML-схема не поддерживает secrets и environment interpolation; для будущих авторизованных providers secrets должны поступать через environment.
Amazon / AWS
Provider использует публичный amazon.jobs search JSON endpoint. Он обслуживает глобальную доску Amazon, поэтому каждый target должен сужать поиск через base_query или facet array: normalized_country_code, normalized_state_name, normalized_city_name, category либо business_category.
В примерах Amazon настроен на Austin, Texas, США: amazon-austin-tx. Страна, штат и город заданы отдельными facet arrays. Ограничение по специальности не задано.
Вложенный блок amazon передаёт scalar values как обычные query parameters, массивы — как повторяющиеся key[]=value. Например, normalized_country_code: [USA, CAN] становится normalized_country_code[]=USA&normalized_country_code[]=CAN. Ключ с уже указанными [] тоже поддерживается. Объекты и вложенные массивы не допускаются; null пропускается.
loc_query сам по себе ненадёжен: endpoint может проигнорировать текстовое ограничение страны. Его можно передать дополнительно, но он не считается достаточным ограничением target. Для страны используйте массив трёхбуквенных кодов, например normalized_country_code: [USA].
sort по умолчанию recent. Provider использует обычную пагинацию: result_limit=100 и offset. Эти параметры задаются самим provider; переопределять их в конфигурации нельзя. max_pages ограничивает весь target: по умолчанию 20, максимум 100. Автоматического разбиения поиска на дополнительные запросы по категориям или локациям нет.
Если результат превышает max_pages или hits достигает обрезанного API значения 10 000, нужно сузить target. Повторяющиеся postings, изменение counts и неполные страницы завершают target с ошибкой: частичный snapshot не применяется и старые вакансии не деактивируются.
При 429 сохраняется HTTP error с Retry-After, текущий target завершается без немедленного retry; повтор выполняется следующим запуском sync.
source_id и requisition_id — числовой posting ID из job_path, сверяемый с id_icims, если он присутствует. company_name предпочтительнее fallback company из конфигурации. Сохраняются canonical URL, location, posted date, schedule и исходный JSON.
Listing уже может содержать полные description, basic_qualifications и preferred_qualifications. Они сразу сохраняются как plain text с detail_status=complete без дополнительных HTTP requests. Если полного description нет, вакансия публикуется с description=null и detail_status=not_fetched; отдельная задача enrichment для Amazon не создаётся. description_short не выдаётся за полный текст. Данные обновятся при следующем успешном discovery.
Разные имена Amazon targets создают разные области identity. При пересекающихся фильтрах одна Amazon-вакансия может появиться в нескольких targets; cross-target дедупликация не выполняется. После изменения фильтра исчезновение означает выход из результата поиска этого target, а не обязательно закрытие на всей доске Amazon.
Discovery и identity
Один target сначала полностью получает listing, затем одной SQLite-транзакцией:
- создаёт и обновляет metadata;
- сохраняет первоначальный
idиdiscovered_at; - ставит новые detail-задачи в очередь;
- помечает исчезнувшие записи inactive.
После этой транзакции новые вакансии доступны API, HTML и RSS, даже если description = null. Discovery не ждёт workers.
Unique key — (source, target, source_id). Одинаковые IDs в разных targets создают независимые записи. target — настроенное name; переименование target меняет область identity, поэтому выбирайте имя надолго.
Workday source_id берётся из posting identifier в конце externalPath, включая суффиксы наподобие JR123-1. requisition_id хранится отдельно из listing, когда доступен. Это позволяет сохранять разные публикации одной requisition. Tenant больше не включается в source_id: namespace задаётся target.
Исчезнувшие вакансии не удаляются. Ошибка listing не применяет частичный снимок и не деактивирует старые вакансии. Успешный пустой listing может деактивировать весь target. Ошибка одного target не останавливает остальные; sync завершится с кодом 1, если были ошибки.
Удаление target из YAML не деактивирует его вакансии: без успешного listing нельзя утверждать, что они исчезли. Их enrichment прекращается, поскольку workers выбирают только настроенные targets.
updated_at — время изменения агрегатором. Реактивация сохраняет ID и дату первого обнаружения. Снимок более старого перекрывающегося sync одного target отклоняется.
Enrichment и очередь
enrichment_tasks в SQLite содержит job_id, числовой priority, status, attempts, next_attempt_at, даты создания/обновления, last_error и поля аренды.
- Новые вакансии имеют normal priority
0; TTL refresh — low priority-10. - Очередь сортируется по priority DESC, затем сроку выполнения и времени создания.
Store.Prioritizeпозволяет повысить приоритет для будущего on-demand API. Сам endpoint пока не реализован.- Задача захватывается атомарно с уникальным lease token на 2 минуты. Поздний ответ старого worker не может затереть результат новой попытки.
- Один detail HTTP request имеет таймаут 45 секунд. Ответ ограничен 32 MiB.
- 429, 502, 503, 504, временные сетевые ошибки, timeout и connection reset переносятся на
next_attempt_at. - Exponential backoff: база 30 секунд для 429, 5 секунд для остальных временных ошибок, рост до 1h плюс jitter.
Retry-Afterзадаёт минимальную паузу и может быть больше 1h. - При 429 provider cooldown сохраняется в БД; пока он действует, новые задачи этого provider не захватываются. Уже выполняющиеся запросы могут завершиться.
- Invalid ref, invalid payload и HTTP 404 завершают задачу с постоянной ошибкой. После
max_attemptsвременная ошибка также становится постоянной; discovery не сбрасывает failed-задачи при каждом запуске. - Неполное enrichment не скрывает вакансию и не меняет active/inactive.
- Исчезнувшие вакансии отменяют pending/fetching задачи. Смена detail reference аннулирует старую попытку и ставит новую.
- TTL проверяется при sync, при старте enrich и раз в минуту в постоянном enrich. Старое описание остаётся доступным во время refresh.
| detail_status | Значение |
|---|---|
| not_fetched | Детали отсутствуют и ещё не поставлены в очередь |
| pending | Первичная загрузка поставлена в очередь |
| fetching | Worker захватил задачу |
| complete | Детали доступны |
| error | Последняя попытка не удалась; повтор может быть запланирован |
| stale | Старые детали доступны, refresh ожидает выполнения |
В таблице задач используются pending, fetching, done, failed, canceled.
detail_attempted_at записывается при захвате задачи, detail_fetched_at — после успешного получения данных.
Сохранение результата, состояние задачи и изменение detail_status выполняются атомарно.
Providers
- Greenhouse Job Board API:
content=trueуже возвращает описание в listing. Эти дешёвые данные сохраняются сразу, отдельная detail-очередь не нужна. - Lever Postings API: listing содержит description, lists и дополнительные части текста. Provider объединяет их без дополнительных detail-запросов.
- Workday CXS:
POST /wday/cxs/{tenant}/{site}/jobsвозвращает metadata;GET /wday/cxs/{tenant}/{site}{externalPath}вызывается только enrichment worker.
Workday иногда возвращает total: 0 на последующих непустых страницах и ограничивает выдачу 2000 записями; запросы за этим пределом могут повторять первую страницу. При capped total provider разделяет listing по flat facets из ответа самого API, рекурсивно уточняет слишком большие части и объединяет результаты с удалением точных дублей между категориями. Если безопасное разбиение недоступно, discovery завершается ошибкой вместо публикации обрезанного списка. Повторяющиеся IDs внутри страницы/раздела, конфликты ID, изменившийся положительный total и неполная выдача вызывают ошибку всего listing. Пределы: 100 000 вакансий и 256 разделов.
CXS — API публичного карьерного сайта без гарантированного стабильного контракта. Отдельные tenants могут отличаться. Ошибки listing откладываются до следующего запуска sync; persistent retries применяются к enrichment.
Descriptions преобразуются в plain text; относительные даты вроде Posted Today не выдумываются в точные timestamps. Неизвестные posted_at остаются null, employment_type — unknown.
Исходный listing хранится в jobs.raw, отдельный detail — в jobs.raw_detail. Они не публикуются в API/UI/RSS.
HTTP, HTML и RSS
| Путь | Назначение |
|---|---|
GET / |
303 redirect на /jobs |
GET /jobs |
HTML список и фильтры, включая отображение detail_status |
GET /jobs/{id} |
Metadata, detail_status, описание либо Details are not loaded yet, ссылка Open original |
GET /api/health |
Проверка БД, ответ {"status":"ok"} |
GET /api/jobs |
Metadata без description/raw |
GET /api/jobs/{id} |
Нормализованная вакансия, nullable description и detail timestamps |
GET /feed.xml |
Последние 50 active вакансий, включая ещё не enriched |
Общие фильтры HTML и API:
| Параметр | Семантика |
|---|---|
| q | Подстрока в title, company, location или description |
| company, location | Подстрока соответствующего поля |
| source | Точное имя провайдера без учёта регистра |
| status | active по умолчанию, inactive или all |
| limit | 50 по умолчанию, максимум 200; 0 означает default |
| offset | 0 по умолчанию |
Фильтры соединяются через AND. Поиск подстроки нечувствителен к регистру Unicode, без stemming/нормализации Unicode; % и _ — обычные символы. Отсутствие description не мешает совпадениям metadata.
Порядок — discovered_at DESC, id DESC. Offset pagination может сдвигаться между sync.
Ошибочные параметры — 400, неизвестный ID — 404.
{
"id": "local-stable-id",
"source": "workday",
"source_id": "JR123",
"target": "nvidia-external",
"company": "NVIDIA",
"title": "Engineer",
"description": null,
"detail_status": "pending",
"detail_fetched_at": null
}
Пример выше — фрагмент detail response. Полный ответ также содержит location, url, employment_type, requisition_id, status, posted_at, discovered_at, updated_at и detail_attempted_at. Даты — RFC3339 UTC.
Список имеет форму {"jobs":[...],"limit":50,"offset":0}; пустая выдача — jobs: [].
RSS 2.0 использует тот же search/storage layer: title — title — company, description — company/location, link — оригинальная вакансия, pubDate — discovered_at, GUID — локальный ID с isPermaLink="false". Enrichment не создаёт новый RSS item и не меняет discovery order. Query parameters RSS пока игнорируются.
Миграция существующей установки
Перед обновлением остановите все старые serve/sync/enrich процессы, сделайте backup и замените бинарник. Новые процессы автоматически применяют 002_enrichment.sql; старый бинарник с новой схемой несовместим.
Миграция сохраняет существующие описания и timestamps, выделяет target-local identities и сохраняет исходный локальный ID для первого target. Если старую запись разделяли несколько targets, для остальных создаются новые IDs. Старый Workday префикс tenant: снимается.
У исторических inactive записей старая схема могла уже потерять target membership. При единственном известном target данного source используется он; при неоднозначности запись сохраняется с target = legacy-unknown. Данные не удаляются, но такие записи требуют ручного сопоставления, если их нужно вернуть в discovery. Старые requisition IDs и новые posting IDs с суффиксом могут представлять разные публикации и не объединяются fuzzy matching.
Схема версионируется через schema_migrations. Не редактируйте применённые SQL; добавляйте следующую миграцию. Неизвестная версия БД отклоняется.
Релизы в Forgejo CI
Workflows находятся в .forgejo/workflows/: ci.yml проверяет push веток и pull requests,
release.yml проверяет код и публикует релиз при push тега v*.
После успешных проверок обычный CI загружает артефакт jobwire-<commit SHA>:
архивы версии snapshot для Linux amd64/arm64 и Windows amd64 плюс SHA256SUMS. Их можно скачать в разделе
Artifacts на странице запуска Actions без создания релиза. Срок хранения — 14 дней
(в пределах политики сервера). Tar сохраняет право исполнения бинарника.
Используется совместимый с Forgejo upload-artifact@v3, согласно
документации артефактов.
Поддерживаются версии вида v0.1.0 и v0.1.0-rc.1; версии с суффиксом публикуются как prerelease.
Один раз включите Actions в настройках репозитория Forgejo и подключите Linux runner
с меткой docker. Оба workflow явно запускаются в node:22-bookworm.
Go устанавливается action по версии из go.mod. Сборку бинарников, tar.gz/zip и checksums
выполняет go run ./scripts/release.go "$RELEASE_VERSION" стандартной библиотекой Go;
отдельная установка утилит упаковки не нужна. Для action публикации релиза предполагается,
что jq уже доступен в окружении выполнения.
Runner должен иметь доступ к Docker Hub, Forgejo,
github.com, code.forgejo.org и загрузкам Go/modules.
Используется встроенный token задания Forgejo; отдельный PAT обычно не нужен.
Настройка runner описана в Forgejo Actions quick start.
После отправки workflow и кода в репозиторий новый релиз запускается двумя командами (выберите ещё не использованную версию):
git tag v0.1.0
git push origin v0.1.0
В разделе Releases появятся:
jobwire_v0.1.0_linux_amd64.tar.gz
jobwire_v0.1.0_linux_arm64.tar.gz
jobwire_v0.1.0_windows_amd64.zip
SHA256SUMS
Архив содержит бинарник jobwire (jobwire.exe для Windows), README и пример конфига.
Linux-архивы также содержат systemd units в deploy/. Локальные config.yaml и базы не упаковываются.
Ubuntu Server на Intel/AMD использует linux_amd64, на ARM — linux_arm64.
После скачивания архива и SHA256SUMS в одну папку проверьте checksum и распакуйте:
sha256sum --ignore-missing -c SHA256SUMS
mkdir -p dist/unpacked
tar -xzf jobwire_v0.1.0_linux_amd64.tar.gz -C dist/unpacked
CI выполняет gofmt check, go test ./... и go vet ./... перед сборкой.
Релиз публикуется после загрузки всех файлов; существующий релиз автоматически не перезаписывается.
При ошибке загрузки может остаться draft: удалите незавершённый draft перед повторным запуском задания.
Сборка сама по себе не обновляет запущенный сервер.
Для такой же сборки локально на Windows, Linux или macOS: go run ./scripts/release.go v0.1.0.
Для сборки без тега используйте go run ./scripts/release.go snapshot.
Запускайте из корня репозитория. Результат сохраняется в dist/release/; папка создаётся
скриптом и не должна существовать перед запуском. Перед повторной сборкой переместите
предыдущую папку с результатами. Временные бинарники создаются в системном temp, а не в корне проекта.
Запуск в контейнерах
Нужны Docker Engine и Docker Compose v2. Из корня проекта один раз скопируйте
config.container.example.yaml в config.container.yaml и добавьте targets по примерам
из config.example.yaml. На PowerShell используйте Copy-Item вместо cp.
cp config.container.example.yaml config.container.yaml
# Отредактируйте targets и server.base_url в config.container.yaml.
docker compose up -d --build
docker compose run --rm sync
up запускает serve и enrich; sync запускается отдельно по имени сервиса.
UI/API/RSS доступны на localhost:8080.
Внутри контейнера server.listen должен оставаться 0.0.0.0:8080.
Порт хоста привязан к 127.0.0.1, чтобы использовать reverse proxy на этом же сервере.
Для другого порта задайте JOBWIRE_PORT перед запуском Compose; для другого конфига — JOBWIRE_CONFIG.
При смене публичного URL обновите также server.base_url.
Образ собирается из исходников в два этапа; в финальном образе находятся бинарник,
CA certificates для HTTPS и пустой пример конфига. Процесс работает с UID/GID 10001:10001.
.dockerignore разрешает только входные файлы сборки: локальные конфиги и БД в образ не попадают.
Все три команды используют /data/jobwire.db через JOBWIRE_DB, заданный в образе.
Compose хранит /data в общем named volume jobwire_jobwire-data, включая WAL и очередь enrichment.
Это отдельная БД: существующая база из каталога данных пользователя автоматически в volume не копируется.
Volume сохраняется после перезапуска, пересборки и docker compose down.
docker compose down -v удаляет volume вместе с БД.
Для bind mount вместо named volume каталог должен быть доступен на запись UID/GID 10001:10001.
docker compose logs -f serve enrich
docker compose stop
# После обновления исходников:
docker compose up -d --build
Discovery планируется снаружи контейнеров. Например, cron на Ubuntu каждые 30 минут
(замените /opt/jobwire на путь checkout; пользователь cron должен иметь доступ к Docker):
*/30 * * * * cd /opt/jobwire && flock -n /opt/jobwire/.cache/container-sync.lock docker compose run --rm sync
Перед включением этой записи создайте каталог /opt/jobwire/.cache командой mkdir -p.
Lock предотвращает наложение запусков этого cron; ручной sync не запускайте параллельно с ним.
Для остановки sync при обновлении дождитесь его завершения и временно отключите cron.
Перед миграцией схемы остановите все три процесса и сделайте backup SQLite по инструкции ниже.
Для запуска без Compose:
docker build -t jobwire:local .
docker run --rm -p 127.0.0.1:8080:8080 --mount source=jobwire-data,target=/data jobwire:local
Этот пример запускает serve с пустыми sources из образа; для своих targets смонтируйте
конфиг в /etc/jobwire/config.yaml read-only. Схемы хранения volumes и
запуска сервисов по profiles описаны в документации Docker
и Compose profiles.
Deployment
reverse proxy → jobwire serve → HTML / API / RSS
↘
SQLite ← jobwire sync (timer)
↕
jobwire enrich (service)
Рекомендуемый layout:
/usr/local/bin/jobwire
/etc/jobwire/config.yaml
/var/lib/jobwire/jobwire.db
Systemd units задают XDG_DATA_HOME=/var/lib, поэтому default database — /var/lib/jobwire/jobwire.db.
Явный путь в YAML, JOBWIRE_DB или --db сохраняет свой приоритет. Для существующей установки
с /var/lib/jobwire/jobs.db оставьте этот путь в конфиге либо перенесите БД по инструкции выше.
Используйте отдельного пользователя jobwire с правом записи в /var/lib/jobwire,
listen 127.0.0.1:8080 и публичный base_url https://jobs.yarikus.com.
Примеры unit files находятся в deploy/. После подготовки пользователя, папок, binary и config:
sudo systemctl daemon-reload
sudo systemctl enable --now jobwire.service jobwire-enrich.service jobwire-sync.timer
journalctl -u jobwire-sync.service -u jobwire-enrich.service
Sync timer работает раз в 30 минут. Внутри приложения нет scheduler для discovery. Enrichment service самостоятельно ожидает очередь и сроки retry. Не запускайте несколько независимых sync одного target одновременно.
Structured discovery logs содержат provider, target, fetched, inserted, updated, deactivated, enqueued, duration, error. Во время загрузки выводится progress раз в 15 секунд. Enrichment logs содержат provider, target, job_id, attempt, status, duration, error.
SQLite использует WAL, foreign keys и busy timeout 10 секунд. Для backup работающей БД используйте SQLite online backup; не копируйте только .db при активном WAL. Перед обновлением схемы проще остановить все процессы и сделать backup. TLS/reverse proxy находятся вне scope, authentication отсутствует.
Разработка и проверки
gofmt -w cmd internal migrations scripts
go test ./...
go vet ./...
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/jobwire-linux-amd64 ./cmd/jobwire
Тесты не обращаются к внешним API. Используются временные SQLite databases и mock HTTP servers.
Покрыты: миграция заполненной старой БД, metadata upsert/target identity, lifecycle, rollback, nullable descriptions, публикация до enrichment, queue priority/retries/TTL, budget, bounded concurrency, lease recovery, конкурирующие claims, late responses, provider parsing/pagination, отсутствие Workday detail requests в discovery, 429/Retry-After и API/HTML/RSS.
cmd/jobwire/
internal/
config/
jobs/
providers/{amazon,greenhouse,lever,workday}/
discovery/
enrichment/
storage/
httpserver/templates/
migrations/
deploy/
Provider реализует List(ctx, target) → []jobs.Stub. Необязательный DetailProvider добавляет GetDetail(ctx, target, ref) → jobs.Detail. Provider не знает о SQLite, очереди, API или пользователе.
Career agent работает только через HTTP API: metadata filtering → shortlist → detail evaluation. On-demand enrichment endpoint, AI ranking, application tracking, agent skills, дополнительные ATS и federation в этот MVP не входят. Локальный ID не является глобальным идентификатором.
Предложение для будущего обмена между экземплярами: JOBWIRE Federation Protocol — Draft 0.1. Документ описывает metadata sync, origin identity, отдельную загрузку details и необязательный trusted relay; протокол пока не реализован.