Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/fortune_intel/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def jobs(
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),
offset: int = Query(default=0, ge=0, le=250000),
) -> dict[str, object]:
filters = {
"query": q,
Expand Down
43 changes: 38 additions & 5 deletions src/fortune_intel/web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ async function loadStats() {
document.querySelector("#stat-fetching-companies").textContent = Number(
stats.companies_with_current_job_fetches,
).toLocaleString();
document.querySelector("#coverage-live").textContent = Number(
stats.companies_with_successful_job_fetches,
).toLocaleString();
document.querySelector("#coverage-directory").textContent = Number(stats.companies).toLocaleString();
document.querySelector("#stat-h1b").textContent = Number(stats.h1b_employers).toLocaleString();
document.querySelector("#stat-evidence").textContent = Number(stats.jobs_with_evidence).toLocaleString();
document.querySelector("#stat-freshness").textContent = stats.last_verified_at
Expand All @@ -107,7 +111,10 @@ async function loadStats() {
}
}

const jobPageSize = 50;
let latestJobsRequest = 0;
let nextJobOffset = 0;
let currentCompany = "";

function replaceSearchUrl(company, filters) {
const params = new URLSearchParams();
Expand Down Expand Up @@ -147,8 +154,12 @@ function loadSearchInputs() {
return params.get("company") || "";
}

async function loadJobs(company = "") {
async function loadJobs(company = "", { append = false } = {}) {
const requestId = ++latestJobsRequest;
if (!append) {
nextJobOffset = 0;
currentCompany = company;
}
const params = new URLSearchParams();
const query = document.querySelector("#query").value.trim();
const location = document.querySelector("#location").value.trim();
Expand All @@ -157,28 +168,42 @@ async function loadJobs(company = "") {
const verifiedWithin = document.querySelector("#verified-within").value;
const sort = document.querySelector("#sort").value;
const filters = { query, location, tier, openedWithin, verifiedWithin, sort };
replaceSearchUrl(company, filters);
if (!append) replaceSearchUrl(company, filters);
if (query) params.set("q", query);
if (company) params.set("company", company);
if (location) params.set("location", location);
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);
params.set("limit", String(jobPageSize));
params.set("offset", String(nextJobOffset));

const list = document.querySelector("#job-list");
const empty = document.querySelector("#empty-state");
const loadMore = document.querySelector("#load-more-jobs");
const loading = element("div", "empty");
loading.append(element("span", "", "Loading current openings…"));
empty.hidden = true;
list.replaceChildren(loading);
if (append) {
loadMore.disabled = true;
} else {
list.replaceChildren(loading);
}
const response = await fetch(`/api/jobs?${params}`);
if (!response.ok) throw new Error("Unable to load jobs");
const data = await response.json();
if (requestId !== latestJobsRequest) return;
document.querySelector("#result-count").textContent = `${data.count} role${data.count === 1 ? "" : "s"} shown`;
list.replaceChildren(...data.items.map(jobCard));
nextJobOffset += data.count;
document.querySelector("#result-count").textContent = `${nextJobOffset} of ${data.total} role${data.total === 1 ? "" : "s"} shown`;
if (append) {
list.append(...data.items.map(jobCard));
} else {
list.replaceChildren(...data.items.map(jobCard));
}
empty.hidden = data.items.length !== 0;
loadMore.hidden = nextJobOffset >= data.total;
loadMore.disabled = false;
}

document.querySelector("#search-form").addEventListener("submit", async (event) => {
Expand All @@ -197,6 +222,14 @@ document.querySelector("#clear-filters").addEventListener("click", async () => {
}
});

document.querySelector("#load-more-jobs").addEventListener("click", async () => {
try {
await loadJobs(currentCompany, { append: true });
} catch (error) {
document.querySelector("#result-count").textContent = error.message;
}
});

const initialCompany = loadSearchInputs();
Promise.all([loadStats(), loadJobs(initialCompany)]).catch((error) => {
document.querySelector("#result-count").textContent = error.message;
Expand Down
9 changes: 8 additions & 1 deletion src/fortune_intel/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ <h1>Find roles with a clearer<br /><em>sponsorship signal.</em></h1>
<article><strong id="stat-evidence">—</strong><span>Jobs with evidence</span></article>
<article><strong id="stat-freshness">—</strong><span>Last verified</span></article>
</section>
<p class="coverage-note">
<a href="/companies"><strong id="coverage-live">—</strong> companies have a successfully synchronized job source</a>
out of <strong id="coverage-directory">—</strong> in the public directory. We report uncovered companies explicitly.
</p>

<section id="jobs" class="explorer">
<div class="section-heading">
Expand All @@ -106,6 +110,9 @@ <h2>Current openings</h2>
<p id="result-count" aria-live="polite">Loading jobs…</p>
</div>
<div id="job-list" class="job-list" aria-live="polite"></div>
<div class="load-more-wrap">
<button id="load-more-jobs" class="load-more" type="button" hidden>Load more roles</button>
</div>
<div id="empty-state" class="empty" hidden>
<strong>No matching roles</strong>
<span>Try a broader search or another evidence tier.</span>
Expand Down Expand Up @@ -134,6 +141,6 @@ <h2>A signal you can inspect.</h2>
<span>Evidence-first job intelligence</span>
<span><a href="/trust">Trust &amp; methodology</a> · Built for transparency, freshness, and correction.</span>
</footer>
<script src="/assets/app.js?v=11" defer></script>
<script src="/assets/app.js?v=12" defer></script>
</body>
</html>
7 changes: 7 additions & 0 deletions src/fortune_intel/web/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,20 @@ h1 em { color: var(--green); font-style: normal; }
.stats article:last-child { border: 0; }
.stats strong { font-size: 26px; }
.stats span { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .07em; white-space: nowrap; }
.coverage-note { max-width: 1040px; margin: 0 auto; padding: 18px 24px 0; color: var(--muted); font-size: 13px; text-align: center; }
.coverage-note a { color: var(--green-dark); text-decoration: none; }
.coverage-note a:hover { text-decoration: underline; }

.explorer { max-width: 1160px; margin: 0 auto; padding: 78px 24px; }
.section-heading { display: flex; justify-content: space-between; align-items: end; margin-bottom: 24px; }
.kicker { display: block; margin-bottom: 7px; color: var(--green); font-size: 11px; text-transform: uppercase; letter-spacing: .12em; font-weight: 700; }
h2 { font-size: clamp(30px, 4vw, 44px); }
.section-heading p { color: var(--muted); font-size: 13px; }
.job-list { border-top: 1px solid var(--line); }
.load-more-wrap { display: flex; justify-content: center; padding-top: 26px; }
.load-more { border: 1px solid var(--green); border-radius: 8px; padding: 11px 18px; background: transparent; color: var(--green-dark); font-weight: 700; cursor: pointer; }
.load-more:hover:not(:disabled) { background: var(--mint); }
.load-more:disabled { cursor: wait; opacity: .65; }
.job-card { display: grid; grid-template-columns: 1fr auto; gap: 26px; padding: 26px 8px; border-bottom: 1px solid var(--line); transition: background .2s; }
.job-card:hover { background: rgba(255,254,249,.7); }
.job-card h3 { margin: 4px 0 9px; font-size: 20px; }
Expand Down
9 changes: 8 additions & 1 deletion tests/test_public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ def test_job_api_can_filter_by_employer_posted_date_without_using_first_seen(tmp
"/api/jobs",
params={"q": "Opening window", "opened_within_days": 7},
)
assert client.get("/api/jobs", params={"offset": 5001}).status_code == 200
assert client.get("/api/jobs", params={"offset": 250001}).status_code == 422

assert response.status_code == 200
assert response.json()["total"] == 1
Expand Down Expand Up @@ -297,12 +299,17 @@ def test_dashboard_hidden_empty_state_cannot_display_with_results(tmp_path):
assert 'id="verified-within"' in dashboard
assert 'id="sort"' in dashboard
assert 'id="clear-filters"' in dashboard
assert "/assets/app.js?v=11" in dashboard
assert 'id="load-more-jobs"' in dashboard
assert "/assets/app.js?v=12" in dashboard
assert 'params.set("opened_within_days", openedWithin);' in script
assert 'params.set("verified_within_hours", verifiedWithin);' in script
assert 'params.set("sort", sort);' in script
assert "function replaceSearchUrl(company, filters)" in script
assert "function loadSearchInputs()" in script
assert "const jobPageSize = 50;" in script
assert 'params.set("offset", String(nextJobOffset));' in script
assert 'id="coverage-live"' in dashboard
assert 'id="coverage-directory"' in dashboard


def test_source_status_does_not_leak_internal_urls_or_errors(tmp_path):
Expand Down
Loading