User problem
CV.ee's web search interface resets filters and view state on every navigation. Job seekers need a persistent, dense comparison view with stable sorting, filtering, and fit ranking across multiple sessions.
Job Dashboard
This Next.js web application discovers, searches, and filters Tallinn/Tartu IT jobs from CV.ee. It calculates explainable local fit scores based on your candidate profile and enables dense, persistent comparison of listings with CSV export.
A lightweight Next.js dashboard for job searching on CV.ee with no external authentication, database server, or hidden credentials. The job is to make local job discovery fast, persistent, and personalized via an editable candidate profile.
CV.ee's web search interface resets filters and view state on every navigation. Job seekers need a persistent, dense comparison view with stable sorting, filtering, and fit ranking across multiple sessions.
IT job seekers in Tallinn/Tartu interested in locally calculated fit scores based on their skills, experience level, and role preferences.
Download fresh CV.ee listings, search by role/employer/keyword, filter by location/status/salary, sort by fit/date/salary, and export the current result set as CSV for offline tracking.
The dashboard combines search, filtering, sorting, persistent local state, and explainable scoring in a dense, keyboard-navigable interface.
localStoragecandidate-profile.jsonFit score transparency: The displayed percentage is a local ranking estimate based on your editable candidate profile, listing keywords, and available description text. It is not an employer assessment and does not infer salary targets or hard exclusions.
The dashboard is a Next.js server-side page with a client-side interactive component. Snapshots are discovered at build time from the data directory.
File System
data/cv-ee-raw-*.json (newest by embedded fetchedAt, mtime fallback)
↓ (read when the server page renders)
Next.js Server Page (app/page.tsx)
↓ (normalize, dedupe, drop expired, score → newest snapshot)
Client-Side Dashboard (components/job-dashboard.tsx)
↓ (interactive search, filter, sort, export)
Browser (persistent state via localStorage)
lib/cv-ee.ts scans every data/cv-ee-raw-*.json
snapshot and picks the newest by the fetchedAt timestamp
embedded in the file, falling back to file mtime only if that field is
missing. Git checkouts and clones rewrite file mtimes, so mtime alone
cannot reliably identify the latest fetch; the content's own timestamp
can. A snapshot that fails to parse is skipped with a warning instead of
crashing the page.
The interactive dashboard component handles search, filtering, sorting, row actions (pipeline status, Hide/Restore), and CSV export. All state is managed via React hooks and persisted to browser localStorage.
Raw CV.ee JSON snapshots are timestamped and stored untouched in
data/; the fetch script only appends new files and never
rewrites existing ones. Normalization, ID-based deduplication, and
dropping listings past their expirationDate all happen in
lib/cv-ee.ts when the page is generated, keeping the raw
archive intact for audit or re-processing.
Lightweight frontend stack with no backend server, database, or external APIs in the app runtime. All dependencies are typed and production-ready.
All work flows through npm scripts. The dashboard is built and deployed as a static Next.js export.
| Command | Purpose | Example |
|---|---|---|
npm run fetch:cv-ee |
Fetch current Tallinn/Tartu IT listings from CV.ee and save as
timestamped JSON in data/
|
npm run fetch:cv-eenpm run fetch:cv-ee -- --out data/cv-ee-raw-manual.json
|
npm install |
Install dependencies | npm install |
npm run dev |
Start local Next.js dev server at http://localhost:3000 |
npm run dev |
npm run build |
Production Next.js build (.next/); the newest snapshot is
discovered and rendered into the page during this step
|
npm run build |
npm run start |
Start production server (for local testing before deploy) | npm run start |
npm run typecheck |
Run TypeScript type check without emitting files | npm run typecheck |
Workflow note: a fresh snapshot only reaches the deployed
dashboard after both a new data/cv-ee-raw-*.json file is
committed and Netlify rebuilds the app, since the newest snapshot is
selected while the page is generated. The nightly GitHub Actions workflow
(see Automation) handles the commit half; a deploy still has to run
afterward.
Data flows from CV.ee through the fetch script, into timestamped snapshots, discovered at build time, and passed to the browser as props.
| Stage | Description | Key files |
|---|---|---|
| 1. Fetch |
The fetch-cv-ee.mjs script hits the CV.ee job search API
with fixed parameters (500 results, IT category, Tallinn + Tartu,
relevance order, include hidden listings).
|
scripts/fetch-cv-ee.mjs |
| 2. Store |
The raw API response is persisted as a timestamped JSON file in the
data/ directory. Subsequent runs append new snapshots
without deleting old ones.
|
data/cv-ee-raw-*.json |
| 3. Discover |
app/page.tsx calls lib/cv-ee.ts, which reads
every snapshot in data/ and picks the newest by its
embedded fetchedAt field (mtime is only a fallback, since
git checkouts rewrite mtimes and would otherwise pick an arbitrary
file).
|
app/page.tsx, lib/cv-ee.ts |
| 4. Normalize & clean | Each raw vacancy is normalized into consistent fields (ID, title, employer, location, salary, posting/expiry dates, description), then the list is deduplicated by listing ID and filtered to drop anything past its published expiration date, so the dashboard only shows live, unique roles. | lib/cv-ee.ts |
| 5. Score | Each listing is scored locally using the candidate profile. The score is based on skill matches, role signals, and description keywords. | lib/job-fit.ts, config/candidate-profile.json |
| 6. Render | The page passes the scored listings to the client component as props. The client renders a dense table with search, filter, sort, and export. |
components/job-dashboard.tsxBrowser localStorage for state |
A scheduled GitHub Actions workflow keeps the snapshot archive current without manual intervention.
.github/workflows/fetch-cv-ee.yml runs on a cron trigger at
02:15 UTC (04:15/05:15 Tallinn time) and can also be run on demand via
workflow_dispatch from the Actions tab.
The job runs
npm run fetch:cv-ee -- --out data/cv-ee-raw-latest.json,
then commits and pushes that file to master as
github-actions[bot] only if the response actually changed
— a no-op diff is skipped rather than committed.
Still manual: the workflow only refreshes the committed snapshot. Because the newest snapshot is selected while the page is generated, a hosted deployment needs its own rebuild after the commit to actually show the refreshed listings.
The fit score is a local, client-editable calculation based on your candidate profile. It is intended for personal job-fit guidance and is not an employer assessment.
Edit config/candidate-profile.json to define your skills,
seniority level, role signals, and scoring weights. The profile is built
into the app at build time and is not sent to CV.ee.
lib/job-fit.ts normalizes the title, description, and
keywords, then matches them against weighted role and skill signals from
the profile. The score is a base value plus the (capped) sum of matched
role and skill signals, plus a seniority-title bonus, minus penalties for
any deprioritized role signals found in the title — clamped to a
fixed range. All labels, aliases, weights, and caps live in
config/candidate-profile.json, not in code.
Scores are deterministic, computed purely from text matching against the editable profile — never from CV.ee or the employer. Each listing carries its matched signals and any penalties alongside the score, and the dashboard surfaces them under a "Why this estimate?" disclosure so the ranking is auditable rather than a black box.
After editing the profile, run npm run build to rebuild the
app with new scoring weights. Scores are recalculated at build time, not
at runtime.
The project uses TypeScript type checking to ensure type safety. Manual testing is recommended before deployment to Netlify.
npm run typecheck # TypeScript check npm run dev # Local dev server npm run build # Full build
npm run fetch:cv-ee to get fresh datanpm run dev and test search, filter, sort in browserconfig/candidate-profile.json and rebuild to verify scoring updatesThe dashboard is deployed as a static Next.js export to Netlify. No environment variables or secrets are required.
npm run build (from netlify.toml).nextnetlify.toml)data/cv-ee-raw-*.json snapshot and redeploy for fresh listings
Updating listings: the nightly GitHub Actions workflow
commits a refreshed data/cv-ee-raw-latest.json, but a Netlify
deploy still has to run afterward for the hosted dashboard to pick it up,
since the newest snapshot is selected while the page is generated.
The dashboard is production-ready. Future improvements are scoped to user convenience and data freshness.