Hugin is a job radar for the Norwegian developer job market: it watches public registries for new companies and job ads in your region and tracks your outreach pipeline — from the command line or a local web dashboard.
Named after Odin's raven of thought, who flies out each morning and returns with tidings — while his sibling Munin remembers.
C# / .NET 10, EF Core with SQLite, NUnit; the dashboard is ASP.NET Core with a React + TypeScript frontend (Vite, Vitest). Layered: Hugin.Core (pure domain, no I/O) · Hugin.Infrastructure (database, HTTP clients, config) · Hugin.Console and Hugin.Api (thin hosts) · Hugin.Tests.
Get it from the Releases page:
Hugin.exe— single self-contained file, dashboard only, no .NET install needed. Put it anywhere and double-click. The dashboard opens athttp://localhost:5111and asks which part of Norway to cover on first run. Ahugin.jsonbeside the exe (start fromhugin.json.example) is optional and overrides the defaults.- Zip —
hugin.exe(CLI) +hugin-api.exe(dashboard) sharing onehugin.json/hugin.db. Requires the .NET 10 runtime.
hugin.json and the hugin.db database are gitignored — the pipeline holds your own outreach history.
Browse active ads, the company inventory (one row per company, branches as tabs on the detail page), and the Applications view; track outreach through Active → Applied → Answered (an Active entry whose ads have all expired moves to Expired on its own, and back when a new ad appears), star the ones you want to apply to, link an ad by hand to the company you track when the registry chain does not join them, and download a data extract (.md/.txt/.json). Settings manages the link-out Sources plus language (English/bokmål) and dark/light theme — both default from your browser and remember your choice. Every view is a real URL, so back/forward, reload, and deep links work.
--port picks the port (default 5111), --config <path> points at a different hugin.json, --no-browser skips the launch. For development, run dotnet run --project Hugin.Api and cd hugin-web && npm run dev side by side.
--public --state <dir> runs the hosted demo mode: all interfaces, every write refused, config and seed file read from <dir>, database snapshot persisted there after each sync. It is for the demo server only — never run it on a machine holding a real pipeline, it serves everything in the state dir to anyone who reaches the port.
A read-only showcase runs at https://hugin-demo.azurewebsites.net on Azure App Service (free tier: the first visit after idle takes a few seconds). Real job ads and companies from NAV and Brreg for Innlandet, a seeded example pipeline, no personal data, no tracking. Built with publish-demo.ps1; deployment notes in docs/specs/2026-09-03-hugin-demo-deployment.md.
| Command | What it does |
|---|---|
hugin sync [--full] |
Pulls companies from Brreg and ads from NAV. --full walks the whole feed history — run once after setup; interrupted runs resume |
hugin new [--seen] |
Everything first seen since the last review; --seen advances the mark |
hugin track <orgnr> <status> |
Sets pipeline status: active, applied, answered. Options: --why, --note, --svar |
hugin list [--status <s>] |
Shows the pipeline; --companies / --ads (each with --kommune <nr>) browse the synced inventory |
hugin export |
Writes data to stdout. --format md|txt|json, --scope new|category|all (category needs --category <navn>), --include-active |
A fresh install has no coverage until it is chosen — in the dashboard, or by hand under municipalities in hugin.json; hugin sync prints how before it exits. The first sync then sets a baseline, so hugin new starts empty rather than listing the whole register — browse that initial inventory with hugin list --companies. --config <path> works here too; the database is created next to the config.
The dashboard host also exposes its data as plain HTTP/JSON over the same hugin.db — suitable for scripting or AI/tooling integration against your own data. It binds to loopback only, and every state-changing request requires an X-Hugin: 1 header (CSRF protection, not authentication — local processes are trusted).
- Read:
/api/status,/api/ads,/api/new,/api/companies,/api/companies/{orgnr},/api/pipeline,/api/extract,/api/sources,/api/sync/status,/api/kommuner,/api/config/discovery,/api/config/focus,/api/config/focus/recommended - Write:
PUT /api/pipeline/{orgnr}·POST|PUT|DELETE /api/sources(+/reorder) ·POST|DELETE /api/ads/{feedId}/hide·PUT|DELETE /api/ads/{feedId}/link·POST /api/seen·POST /api/sync(?full=1for a full backfill) ·PUT /api/config/discovery·PUT /api/config/focus·GET /api/config/focus/preview(side-effecting, needsX-Hugin: 1too) ·POST /api/reset·POST /api/first-run-dismissed
hugin.json sits beside the exe (start from hugin.json.example):
| Field | What it does |
|---|---|
municipalities |
{ "name", "number" } pairs — Brreg kommune numbers to watch |
fylker |
2-digit fylke codes — expands to every kommune in the fylke |
allOfNorway |
true watches every kommune in the register |
naeringskoder |
SN2025 industry codes; a prefix like "62" matches every sub-code |
keywords |
The ad-title recall net — can stay broad, categories narrows it |
categories |
NAV occupation categories (default ["IT"]) filter out keyword coincidences; uncategorized ads always pass |
linkouts |
{ "label", "url" } pairs — imported into the dashboard's Sources on first run only; manage them under Settings afterwards |
navToken |
null fetches NAV's rotating public token automatically; set a registered token instead |
municipalities, fylker and allOfNorway are editable from the dashboard (first-run dialog and Settings → Dekning); naeringskoder and keywords are editable from Settings → Fokus, with a per-code company-count preview from Brreg and a "Legg til anbefalte" button that adds the curated default set. categories, navToken and linkouts stay hand-edited. Saving from the UI rewrites only the changed keys and keeps everything else as it was, after backing the file up to hugin.json.bak.
Settings → Nullstilling resets in two levels: clearing the coverage (municipalities, fylker, allOfNorway) so the first-run dialog asks again, keeping the database — or deleting everything, which snapshots the database to hugin.db.reset-<timestamp>.bak first, then wipes the companies, ads, pipeline and review marks. To restore a snapshot, stop the app and rename hugin.db.reset-<timestamp>.bak to hugin.db, replacing the current file, before relaunching.
A full NAV backfill button in Settings → Fokus walks the entire feed history to pick up ads matching a widened keyword list. It takes minutes and recovers open ads only — NAV returns closed ads content-stripped.
Municipality numbers come from Brreg's kommune register.
- Enhetsregisteret (Brønnøysundregistrene) — open company data, no authentication. Reads both hovedenheter and underenheter, because regional branch offices are registered as underenheter of parents elsewhere.
- NAV stillingsfeed (arbeidsplassen.no) — job ads under the API terms: the required deep-link is stored, and an expired ad is never presented as active.
Ads posted only on finn.no are not in the NAV feed, and neither finn.no nor proff.no permits scraping — Hugin lists them (plus LinkedIn) as Sources: link-out reminders to check by hand.
Needs the .NET 10 SDK and Node:
git clone https://github.com/malinfossum/hugin.git
cd hugin
cp hugin.json.example hugin.json.\build.ps1This builds the frontend, publishes both exes into publish\, and a self-contained single-file Hugin.exe (frontend embedded) into publish-single\. Run publish\hugin-api.exe — or publish-single\Hugin.exe — for the dashboard.
dotnet test