From 8eab467d3a89a24f945fbe7d105b6d7341bc9371 Mon Sep 17 00:00:00 2001 From: Nithin Date: Sat, 29 Aug 2026 16:21:53 -0400 Subject: [PATCH] feat(explorer): add freshness sort controls --- src/fortune_intel/api.py | 5 +++- src/fortune_intel/storage/sqlite.py | 13 +++++++-- src/fortune_intel/web/api.html | 2 +- src/fortune_intel/web/app.js | 9 ++++++- src/fortune_intel/web/index.html | 11 ++++++-- src/fortune_intel/web/styles.css | 8 +++++- tests/test_public_api.py | 42 +++++++++++++++++++++++++++-- 7 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/fortune_intel/api.py b/src/fortune_intel/api.py index bc8461f..54e5286 100644 --- a/src/fortune_intel/api.py +++ b/src/fortune_intel/api.py @@ -180,6 +180,7 @@ def jobs( tier: str = Query(default="", pattern="^[A-Ea-e]?$"), opened_within_days: int = Query(default=0, ge=0, le=365), verified_within_hours: int = Query(default=0, ge=0, le=168), + sort: str = Query(default="newest", pattern="^(newest|verified)$"), status: str = Query(default="active", pattern="^(active|closed)$"), limit: int = Query(default=50, ge=1, le=50), offset: int = Query(default=0, ge=0, le=5000), @@ -191,15 +192,17 @@ def jobs( "tier": tier, "opened_within_days": opened_within_days, "verified_within_hours": verified_within_hours, + "sort": sort, "status": status, "include_synthetic": settings.show_synthetic, "us_eligibility": "eligible", } records = repository.list_jobs(**filters, limit=limit, offset=offset) + count_filters = {key: value for key, value in filters.items() if key != "sort"} return { "items": records, "count": len(records), - "total": repository.count_jobs(**filters), + "total": repository.count_jobs(**count_filters), "limit": limit, "offset": offset, "country_scope": COUNTRY_SCOPE, diff --git a/src/fortune_intel/storage/sqlite.py b/src/fortune_intel/storage/sqlite.py index 1a9a1d5..8ae5e05 100644 --- a/src/fortune_intel/storage/sqlite.py +++ b/src/fortune_intel/storage/sqlite.py @@ -241,6 +241,7 @@ def list_jobs( tier: str = "", opened_within_days: int = 0, verified_within_hours: int = 0, + sort: str = "newest", status: str = "active", limit: int = 50, offset: int = 0, @@ -282,6 +283,15 @@ def list_jobs( "j.last_seen_at IS NOT NULL AND datetime(j.last_seen_at) >= datetime('now', ?)" ) params.append(f"-{verified_within_hours} hours") + sort_order = { + "newest": "datetime(COALESCE(j.posted_at, j.first_seen_at)) DESC, j.title ASC", + "verified": ( + "datetime(j.last_seen_at) DESC, " + "datetime(COALESCE(j.posted_at, j.first_seen_at)) DESC, j.title ASC" + ), + }.get(sort) + if sort_order is None: + raise ValueError("sort must be newest or verified") params.extend([min(max(limit, 1), 200), max(offset, 0)]) with self.connect() as connection: rows = connection.execute( @@ -299,8 +309,7 @@ def list_jobs( c.name AS company_name, c.slug AS company_slug, c.ats_type FROM jobs j JOIN companies c ON c.id = j.company_id WHERE {" AND ".join(clauses)} - ORDER BY datetime(COALESCE(j.posted_at, j.first_seen_at)) DESC, - j.title ASC + ORDER BY {sort_order} LIMIT ? OFFSET ? """, params, diff --git a/src/fortune_intel/web/api.html b/src/fortune_intel/web/api.html index dd0e8e1..5f177b4 100644 --- a/src/fortune_intel/web/api.html +++ b/src/fortune_intel/web/api.html @@ -21,7 +21,7 @@

Build from evidence.

Search verified U.S. openings, inspect H-1B evidence tiers, and monitor source freshness. Responses are JSON, bounded, paginated, and rate limited.

Endpoints

-
GET /api/jobs

Search definite U.S. roles with q, company, location, tier, opened_within_days, verified_within_hours, status, limit, and offset. The opening-date filter uses employer-posted dates only; roles without a source date are excluded. The verification filter uses the most recent time OpenRole observed that individual role. Ambiguous and non-U.S. locations are never returned.

+
GET /api/jobs

Search definite U.S. roles with q, company, location, tier, opened_within_days, verified_within_hours, sort, status, limit, and offset. Use sort=newest for employer posting recency or sort=verified for OpenRole verification recency. The opening-date filter uses employer-posted dates only; roles without a source date are excluded. The verification filter uses the most recent time OpenRole observed that individual role. Ambiguous and non-U.S. locations are never returned.

GET /api/jobs/{id}

Read a privacy-limited description excerpt and the evidence used for one assessment.

GET /api/companies

Search public company identities with q, limit, and offset; inspect active-job coverage and freshness.

GET /api/coverage

Inspect the complete company denominator and auditable discovery, approval, and successful-source counts. Directory inclusion alone is not coverage.

diff --git a/src/fortune_intel/web/app.js b/src/fortune_intel/web/app.js index bde0351..e4bda86 100644 --- a/src/fortune_intel/web/app.js +++ b/src/fortune_intel/web/app.js @@ -119,6 +119,7 @@ function replaceSearchUrl(company, filters) { if (filters.verifiedWithin) { params.set("verified_within_hours", filters.verifiedWithin); } + if (filters.sort !== "newest") params.set("sort", filters.sort); const search = params.toString(); history.replaceState(null, "", search ? `/?${search}#jobs` : "/"); } @@ -139,6 +140,10 @@ function loadSearchInputs() { if (["", "6", "24", "72"].includes(verifiedWithin)) { document.querySelector("#verified-within").value = verifiedWithin; } + const sort = params.get("sort") || "newest"; + if (["newest", "verified"].includes(sort)) { + document.querySelector("#sort").value = sort; + } return params.get("company") || ""; } @@ -150,7 +155,8 @@ async function loadJobs(company = "") { const tier = document.querySelector("#tier").value; const openedWithin = document.querySelector("#opened-within").value; const verifiedWithin = document.querySelector("#verified-within").value; - const filters = { query, location, tier, openedWithin, verifiedWithin }; + const sort = document.querySelector("#sort").value; + const filters = { query, location, tier, openedWithin, verifiedWithin, sort }; replaceSearchUrl(company, filters); if (query) params.set("q", query); if (company) params.set("company", company); @@ -158,6 +164,7 @@ async function loadJobs(company = "") { if (tier) params.set("tier", tier); if (openedWithin) params.set("opened_within_days", openedWithin); if (verifiedWithin) params.set("verified_within_hours", verifiedWithin); + if (sort !== "newest") params.set("sort", sort); const list = document.querySelector("#job-list"); const empty = document.querySelector("#empty-state"); diff --git a/src/fortune_intel/web/index.html b/src/fortune_intel/web/index.html index 7d2a232..db1cfaf 100644 --- a/src/fortune_intel/web/index.html +++ b/src/fortune_intel/web/index.html @@ -6,7 +6,7 @@ OpenRole Intelligence - +