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 @@
Search verified U.S. openings, inspect H-1B evidence tiers, and monitor source freshness. Responses are JSON, bounded, paginated, and rate limited.
GET /api/jobsSearch 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/jobsSearch 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/companiesSearch public company identities with q, limit, and offset; inspect active-job coverage and freshness.
GET /api/coverageInspect the complete company denominator and auditable discovery, approval, and successful-source counts. Directory inclusion alone is not coverage.