Skip to content

Commit 3cf1750

Browse files
fix(frtonend): pass
1 parent 1663e81 commit 3cf1750

2 files changed

Lines changed: 66 additions & 42 deletions

File tree

frontend/app.js

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -612,34 +612,38 @@ async function runQuery() {
612612
releasePrimary();
613613
if (!alive()) return;
614614

615-
setStatus("Fetching related hashtags…");
616-
const trending = await apiGet("hashtags", param({ limit: "50" }), ctrl.signal);
617-
if (!alive()) return;
618-
// Related hashtags = the OTHER tags on the same changesets; drop the exact searched tag(s).
619-
const searched = new Set(state.hashtags.map((h) => String(h).replace(/^#/, "").toLowerCase()));
620-
state.hashtagTrends = (trending || []).filter(
621-
(t) => !searched.has(String(t.hashtag).replace(/^#/, "").toLowerCase())
622-
);
623-
if (typeof renderHashtagPieChart === "function") renderHashtagPieChart();
624-
625-
setStatus("Fetching editors…");
626-
await fetchEditorStats();
627-
if (!alive()) return;
615+
// Each secondary section is isolated so one failure can't blank the others or the primary content.
616+
await runSection("related hashtags", ctrl, async () => {
617+
setStatus("Fetching related hashtags…");
618+
const trending = await apiGet("hashtags", param({ limit: "50" }), ctrl.signal);
619+
if (!alive()) return;
620+
// drop the exact searched tag(s); keep the co-occurring ones
621+
const searched = new Set(state.hashtags.map((h) => String(h).replace(/^#/, "").toLowerCase()));
622+
state.hashtagTrends = (trending || []).filter(
623+
(t) => !searched.has(String(t.hashtag).replace(/^#/, "").toLowerCase())
624+
);
625+
if (typeof renderHashtagPieChart === "function") renderHashtagPieChart();
626+
});
628627

629-
setStatus("Fetching tag breakdown…");
630-
const tags = await apiGet("tags", param({ limit: "200" }), ctrl.signal);
631-
if (!alive()) return;
632-
state.tagRows = tags;
633-
renderOverviewDetails();
628+
await runSection("editors", ctrl, async () => {
629+
setStatus("Fetching editors…");
630+
await fetchEditorStats();
631+
});
632+
633+
await runSection("tag breakdown", ctrl, async () => {
634+
setStatus("Fetching tag breakdown…");
635+
const tags = await apiGet("tags", param({ limit: "200" }), ctrl.signal);
636+
if (!alive()) return;
637+
state.tagRows = tags;
638+
renderOverviewDetails();
639+
});
634640
} catch (err) {
635-
if (err?.name !== "AbortError") {
641+
// Primary failure: state it in every primary section instead of leaving blank skeletons.
642+
if (err?.name !== "AbortError" && alive()) {
636643
console.warn("OSMSG query failed:", err);
637-
// Surface a busy shed even when the first (summary) call is the one rejected, so the user is not
638-
// left staring at loading skeletons.
639-
if (err?.busy && alive()) {
640-
showError(err);
641-
$("#ov-strip-totals").innerHTML = "";
642-
}
644+
showError(err);
645+
sectionMessage("#podium", busyOrErrorMsg(err));
646+
sectionMessage("#ov-strip-totals", busyOrErrorMsg(err));
643647
}
644648
} finally {
645649
releasePrimary();
@@ -705,6 +709,7 @@ async function loadLeaderboardPage(setPodium = false, forceFetch = false) {
705709
console.warn("OSMSG leaderboard fetch failed:", err);
706710
state.lastError = err;
707711
showError(err);
712+
if (setPodium) sectionMessage("#podium", busyOrErrorMsg(err));
708713
} finally {
709714
clearTimeout(timeout);
710715
if (state.lbInflight === ctrl) state.lbInflight = null;
@@ -717,6 +722,25 @@ function onSectionError(section, err, ctrl) {
717722
console.warn(`OSMSG ${section} fetch failed:`, err);
718723
}
719724

725+
// Run one secondary section in isolation so its failure can't abort the query or blank another section.
726+
async function runSection(name, ctrl, fn) {
727+
try {
728+
await fn();
729+
} catch (err) {
730+
onSectionError(name, err, ctrl);
731+
}
732+
}
733+
734+
function sectionMessage(sel, msg) {
735+
const el = $(sel);
736+
if (el) el.innerHTML = `<div class="section-msg">${escapeHtml(msg)}</div>`;
737+
}
738+
739+
function busyOrErrorMsg(err) {
740+
const busy = err?.busy || err?.status === 503 || err?.name === "AbortError" || state.summary;
741+
return busy ? "Server is busy, give it a moment and hit Extract again." : "Couldn't load these stats.";
742+
}
743+
720744
let toastTimer;
721745
function toast({ msg, icon = "info", err = false } = {}) {
722746
const t = $("#toast");
@@ -1428,24 +1452,17 @@ function showLoading() {
14281452
}
14291453
function showError(err) {
14301454
const tb = $("#lb-body");
1431-
if (err?.busy) {
1432-
tb.innerHTML = `<tr><td colspan="8"><div class="errbox">
1433-
<i data-lucide="hourglass"></i>
1434-
<h3>Server is busy right now</h3>
1435-
<p style="margin-top:8px;color:#717D78">Too many queries are running at once. Give it a moment, then hit Search again.</p>
1436-
</div></td></tr>`;
1437-
$("#pagination").hidden = true;
1438-
refreshIcons(tb);
1439-
return;
1440-
}
1441-
const msg = err?.message || "Network error";
1442-
const isAbort = err?.name === "AbortError";
1455+
// A loaded overview proves the API is reachable, so a failure here is a slow/busy query, not "unreachable".
1456+
const busy = err?.busy || err?.status === 503 || err?.name === "AbortError" || !!state.summary;
1457+
const body = busy
1458+
? `<p style="margin-top:8px;color:var(--muted)">This is a large query and the server is busy right now. Give it a moment, then hit Extract again.</p>`
1459+
: `<p style="margin-top:8px"><code>${escapeHtml(err?.message || "Network error")}</code></p>
1460+
<p style="margin-top:14px;color:var(--muted)">The API may be unreachable. Check your connection, then hit Extract to try again.</p>
1461+
<p style="margin-top:18px"><a href="${API_BASE}/docs/swagger" target="_blank" rel="noopener">Open the API docs <i data-lucide="external-link" class="ico-sm" style="vertical-align:-2px"></i></a></p>`;
14431462
tb.innerHTML = `<tr><td colspan="8"><div class="errbox">
1444-
<i data-lucide="cloud-off"></i>
1445-
<h3>${isAbort ? "Request timed out" : "Couldn't reach the OSMSG API"}</h3>
1446-
<p style="margin-top:8px"><code style="font-family:var(--mono);font-size:12px;background:#F4F0E6;padding:2px 6px;border-radius:4px;color:#3A4744">${escapeHtml(msg)}</code></p>
1447-
<p style="margin-top:14px;color:#717D78">If this is a CORS error and you're hosting this page off the API origin, the API needs to allow your origin. Hit Search to try again.</p>
1448-
<p style="margin-top:18px"><a href="${API_BASE}/docs/swagger" target="_blank" rel="noopener">Open the API docs <i data-lucide="external-link" class="ico-sm" style="vertical-align:-2px"></i></a></p>
1463+
<i data-lucide="${busy ? "hourglass" : "cloud-off"}"></i>
1464+
<h3>${busy ? "Server is busy" : "Couldn't reach the OSMSG API"}</h3>
1465+
${body}
14491466
</div></td></tr>`;
14501467
$("#pagination").hidden = true;
14511468
refreshIcons(tb);

frontend/style.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2829,6 +2829,13 @@ button.ht-tag-add:hover {
28292829
color: var(--ink-2);
28302830
}
28312831

2832+
.section-msg {
2833+
padding: 24px 16px;
2834+
text-align: center;
2835+
color: var(--muted);
2836+
font-size: 14px;
2837+
}
2838+
28322839
.fade-in {
28332840
animation: fadein 0.35s ease-out both;
28342841
}

0 commit comments

Comments
 (0)