Job Hunt Dashboard
Desktop / Productivity | 2026
A local-first job tracker that never leaves my machine.
A tool I built for myself while job-hunting in spring 2026. Electron 29 desktop app that lives in the Windows tray, polls Gmail on a schedule with read-only OAuth, and uses a pluggable AI provider (Gemini / Claude / OpenAI) to classify whether each new email is job-related. Everything is stored on my disk; nothing leaves except the Gmail OAuth call itself and, if I opt in, the email subject + snippet sent to one chosen LLM for classification.
Deliberately frameworkless: zero bundler, zero React, zero Tailwind. Storage is a JSON file in app.getPath('userData'). Gmail is the official googleapis SDK on a read-only scope. AI providers are reached over raw HTTPS without any vendor SDK so the surface is the same for all three. The wallpaper integration talks to the renderer over a local WebSocket on ws://127.0.0.1:49152.
The app I open every morning during the job search. Daily-task list and long-term goals are editable from inside the app, the dashboard mirrors live to a Wallpaper Engine background so the status is on screen even when the app is collapsed to tray.
Local-first, BYO-AI, even live on the desktop wallpaper.
Three properties are the point. It stays on disk, no server, no cloud, no telemetry. It's BYOK across three LLMs, Gemini, Anthropic, or OpenAI, switchable from Settings. It paints itself onto the wallpaper via a Wallpaper Engine plugin that listens on a localhost socket and re-renders whenever the app state changes.
gemini-2.0-flash, claude-haiku-4-5, gpt-4o-mini) live side-by-side in Settings. Keys are stored locally; switching providers is one dropdown change, no rebuild.wallpaper/wallpaper.html connects to the local WebSocket and renders the same daily tasks + deadline + sync status as the app. Check off a task on the wallpaper and it propagates back to the app instantly.A job search is the worst kind of long-running unindexed inbox.
Six months of applying to roles turns Gmail into a haystack, acknowledgments, recruiter outreach, scheduling threads, take-home assignments, and rejections all mixed in with everything else. Generic trackers (Huntr, Teal) want me to upload everything to their cloud. I wanted the opposite: an offline app on my own machine, plus the option to spend a few cents on an LLM call only when I explicitly choose to.
“How are people tracking 100+ applications without losing their minds?”
“Free tier limits at ~20 active applications; Pro tier $20-30/mo with cloud sync.”
“Request the narrowest scope that still works. Read-only Gmail is gmail.readonly, don't ask for write access if you only need to read.”
“Web wallpapers can run arbitrary HTML + JS and connect to local services, the perfect transport for a tray-app status widget.”
A productivity tool I'd actually trust with my Gmail.
Three increments that each made the next one possible.
Electron shell + manual inbox.
First milestone: a tray-resident Electron 29 app with a single HTML page, a JSON storage adapter, and a manual inbox where I could paste subject lines into application records. No Gmail integration, no AI. Useful enough to start the search, ugly enough to know it needed more.
Gmail sync + AI classification.
Added googleapis OAuth and a node-cron scheduler that polls Gmail every N minutes. Built a small provider abstraction so the sameclassify(subject, snippet) call dispatches to Gemini, Anthropic, or OpenAI based on the active provider in Settings. The job-related ones land tagged in the dashboard; the rest get ignored.
Wallpaper Engine live integration.
Stood up a local WebSocket server on ws://127.0.0.1:49152 inside the Electron main process. The Wallpaper Engine wallpaper is just an HTML page with a WebSocket client that subscribes to state updates from the renderer and pushes task-toggle events back. The result: my daily tasks and sync status are on screen even when the app is collapsed to the tray.
Electron's setLoginItemSettings happily registers the app for auto-launch, but openAsHidden alone doesn't hide it on first boot after install, Windows shows the window once. Fix was checking process.argv for the --hidden flag Windows passes on login starts and calling win.hide() if present. The first launch shows the window so you know it's installed; every subsequent boot opens it straight into the tray.
One process, three surfaces, one JSON file.
The Electron main process owns everything mutable, the JSON store, the cron timer, the OAuth tokens, the WebSocket server. The renderer process is the operator UI. The wallpaper is a passive listener over the local socket. All three see the same state because they all read from the same in-process source of truth.
// providers/index.js
const PROVIDERS = {
gemini: require('./gemini'), // gemini-2.0-flash
anthropic: require('./anthropic'), // claude-haiku-4-5
openai: require('./openai'), // gpt-4o-mini
}
// one shape, three implementations
async function classify(subject, snippet, cfg) {
if (cfg.provider === 'none') {
return { jobRelated: null, manual: true }
}
const p = PROVIDERS[cfg.provider]
return await p.classify({
subject, snippet,
apiKey: cfg.keys[cfg.provider],
})
}
// all three speak raw HTTPS, no SDKs, no auth helpers,
// no shared dependency surface between them.// main process
const wss = new WebSocketServer({
host: '127.0.0.1',
port: 49152,
})
wss.on('connection', (ws) => {
ws.send(JSON.stringify({
type: 'state', payload: store.snapshot()
}))
ws.on('message', (raw) => {
const m = JSON.parse(raw)
if (m.type === 'task.toggle') {
store.toggleTask(m.id)
broadcast({ type: 'state', payload: store.snapshot() })
}
})
})
// wallpaper/wallpaper.html
const ws = new WebSocket('ws://127.0.0.1:49152')
ws.onmessage = (e) => renderState(JSON.parse(e.data))
checkbox.onchange = () => ws.send(...)What it looks like every morning at 8am.
The desktop app is intentionally dense, stat cards across the top, today's objectives on the left, this-week progress + long-term goals on the right, all editable in-app. The Wallpaper Engine view is a slimmer read-mostly mirror of the same state, painted onto the desktop and kept in sync over the local WebSocket.


The same state, painted onto the desktop. A Wallpaper Engine “Web” wallpaper connects to the local WebSocket on 127.0.0.1:49152 and renders the stats, today's objectives, this-week targets, long-term goals, and the deadline countdown, all without focusing the app.
It's two-way: checking a task on the wallpaper marks it done in the app, and vice versa. Both surfaces read from the same in-process JSON store, so they never drift.


Local-only · no public URLJob Hunt is a desktop app, not a hosted site, it lives in the Windows tray and reads your Gmail locally. The screenshots above are from the running Electron build on developer hardware; daily tasks and goals are fully editable in Settings (count tasks track a number, binary tasks are done / not-done).