Skip to content

Jobs 8/9: Saved searches with e-mail alerts (jobs and people) #935

Description

@wintermeyer

TL;DR Saved searches with e-mail alerts, symmetric for both sides of the market: candidates save a job search ("remote Elixir in Köln") and get new matches by mail; recruiters save a people search ("tag:elixir ort:köln, open to offers") and get new candidates by mail.

Order: 8/9 in the Jobs & Organizations milestone. Depends on #933 (job board filters) and #928 (status visibility, for the people side).
Where: new table saved_searches, save buttons on /search and /jobs, management page under /settings, a daily sweeper + Emailer mail.

Data model:

saved_searches: user_id, kind: people|jobs, query (string, 255 — the same
                syntax the search/board URL uses, so saving = capturing the
                current filters), notify: none|daily|weekly,
                last_run_at, last_notified_at
  • Cap per member (config, default 10). New-results detection via high-water mark: only entities created after last_notified_at count, so a mail never repeats old results (same pattern as the DM notification high-water mark).

Want:

  • A saved jobs search captures the whole board filter set (the query string is the board URL: q, tag, near + radius, country, salary_min, workplace/employment type), and the sweeper's matcher runs the very same board query — radius and salary alerts behave exactly like the live board, "neue Stellen im Umkreis von 50 km um Köln ab 60.000 €" just works. A member with a stored minimum-salary expectation (Jobs 1/9: Employment-status visibility + minimum-salary expectation #928) gets a one-tap "ab meiner Gehaltsvorstellung" option when saving; it is stored as an ordinary salary filter and the figure never appears in any mail.
  • People search gains a status: operator/filter (status:looking, status:open) in Vutuv.Search.parse/2, honoring Jobs 1/9: Employment-status visibility + minimum-salary expectation #928's visibility: only statuses visible to signed-in members are matchable, hidden never matches. Requires login; logged-out search ignores the operator. Combined with the existing ort:/stadt: city operator and tag: this covers the recruiter's real search ("Elixir-Leute in Köln, offen für Angebote"). People matching deliberately stays city-text over member-entered addresses — no radius on people; member addresses are display data, not geocoded — and only ever matches what the searcher could see logged-in.
  • One quiet "Suche speichern" button on /search and on /jobs when filters are active — members who never use it see one small button, nothing else. Saving asks only for the notify cadence (none/daily/weekly); the select defaults to none (harmless default: saving a search never silently subscribes anyone to e-mail).
  • Management block in /settings (joins the settings hub like every area): paginated list of saved searches with query, cadence, last-result info; edit cadence, run now (jump to results), delete.
  • Daily sweeper (Berlin day) batches per member: one e-mail per member per day at most, listing each saved search with up to 5 new results and a link to the full list; weekly cadence collects on a fixed weekday. Built on Emailer.base_email/0 + bulk_headers/1, with a per-search one-click disable link (the unsubscribe pattern), both HTML and text bodies via the e-mail framework.
  • Deleting a member cascades their saved searches (extend the deletion chokepoint test).

Legal/privacy: alert mails about people contain only what the recipient could see logged-in (name, handle, headline, status pill); nothing hidden leaks by e-mail, and a member's minimum-salary expectation never rides along in an alert mail, whatever its visibility. Respect blocks: blocked members never appear in each other's alerts.
SEO & GEO: none (all behind login); the job-alert mail links the public detail pages.
UX: mobile-first mail layout (single column); the settings block is invisible until the first search is saved.
Docs: extend docs/architecture/search.md + jobs.md; new config knobs in docs/ADMINS.md.
Smoke test (browser + mail): save a jobs search with near=Köln&radius=50&salary_min=60000 plus a tag filter, publish a matching Cologne posting above the figure from a second account, run the sweeper → exactly one mail in /sent_emails listing the new posting, deep link works; publish one below the figure or outside the radius → the next sweep stays silent about it; run the sweeper again → no second mail (high-water mark); save a people search with status:looking, flip a test member's status with visibility members → next sweep mails it; set that member to hidden → they never appear; disable the search via the mail's one-click link → no further mails; German render of mail + settings checked.
Out of scope: push/in-app notification channels for alerts, algorithmic recommendations ("jobs you might like"), a jobs scope chip on /search.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions