Job Dashboard

A local job search dashboard for CV.ee listings.

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.

Purpose

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.

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.

Target user

IT job seekers in Tallinn/Tartu interested in locally calculated fit scores based on their skills, experience level, and role preferences.

Core outcome

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.

Features

The dashboard combines search, filtering, sorting, persistent local state, and explainable scoring in a dense, keyboard-navigable interface.

Search

  • Search by job title, employer name, or CV.ee keyword
  • Real-time filtering as you type

Filtering

  • Location (Tallinn, Tartu, remote/hybrid variants)
  • Application pipeline status (not started, saved, applied, interviewing, closed)
  • Activity (opened vs. not-yet-opened listings)
  • Salary disclosed only
  • Minimum profile-estimate threshold, plus hidden listings toggle

Sorting

  • By fit score (highest first)
  • By date posted (newest first)
  • By salary (highest/lowest)
  • By employer name

Persistent state

  • Per-listing pipeline status and Hide/Restore, saved to localStorage
  • Automatic "opened" tracking when a listing link is clicked
  • Quick-view shortcuts: strong matches, opened listings, reset filters
  • No server-side accounts or database required

Fit scoring

  • Explainable local ranking from candidate-profile.json
  • Based on skills, role signals, and listing keywords
  • Not an employer assessment; for personal guidance only

Export

  • Download current result set as CSV
  • Includes all columns: title, employer, location, salary, fit, date

Fit 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.

Architecture

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)

Server layer

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.

Client layer

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.

Data layer

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.

Tech Stack

Lightweight frontend stack with no backend server, database, or external APIs in the app runtime. All dependencies are typed and production-ready.

Frontend

  • Next.js 16.1.6
  • React 19.2.3
  • TypeScript 6.0.3
  • Browser APIs only (localStorage, JSON)

Tooling

  • Node 20 (Netlify build environment)
  • npm for dependencies
  • Netlify's Next.js runtime (auto-detected, no static export)
  • No build-time secrets required

Data fetching (script-only)

  • Node.js fetch API (built-in)
  • CV.ee job search API
  • No authentication; public endpoint
  • Timestamped JSON snapshots

Main Commands

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-ee
npm 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 Flow

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.tsx
Browser localStorage for state

Automated Refresh

A scheduled GitHub Actions workflow keeps the snapshot archive current without manual intervention.

Nightly schedule

.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.

Fetch and commit

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.

Fit Scoring

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.

Profile configuration

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.

Scoring logic

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.

Explainability

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.

Editing and rebuilding

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.

Verification

The project uses TypeScript type checking to ensure type safety. Manual testing is recommended before deployment to Netlify.

Local checks

npm run typecheck    # TypeScript check
npm run dev          # Local dev server
npm run build        # Full build

Manual verification

  • Run npm run fetch:cv-ee to get fresh data
  • Run npm run dev and test search, filter, sort in browser
  • Verify Apply and Hide/Unhide persist across page reload
  • Test CSV export and check column order and data accuracy
  • Edit config/candidate-profile.json and rebuild to verify scoring updates

Deployment

The dashboard is deployed as a static Next.js export to Netlify. No environment variables or secrets are required.

Netlify configuration

  • Build command: npm run build (from netlify.toml)
  • Publish directory: .next
  • Node version: 20 (pinned in netlify.toml)
  • No environment variables required

Build and deployment workflow

  • Push changes to the repository
  • Netlify detects the Next.js app and supplies its Next.js runtime automatically
  • The app is built and served through that runtime; there is no static-export step
  • Commit a new 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.

Next Work

The dashboard is production-ready. Future improvements are scoped to user convenience and data freshness.