From 8946f794b57464d22c81abf8cc288e3cabdfcc06 Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Thu, 18 Jun 2026 14:47:56 +0900 Subject: [PATCH 1/2] feat(site): visualize dataset growth history Refs #19 --- site/src/pages/index.astro | 9 ++- site/src/scripts/techapi.js | 135 +++++++++++++++++++++++++++++------- site/src/styles/techapi.css | 67 +++++++++++++++++- 3 files changed, 181 insertions(+), 30 deletions(-) diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 9af43135f92..30636394c3a 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -111,8 +111,8 @@ const endpoints = [
00 - History -

Dataset growth, synced live

-

Counts come from the published static dump, while recent syncs come from repository activity when GitHub is reachable.

+

Dataset growth over time

+

Recent dump commits are replayed into a small growth chart, showing how many records each sync added.

v1/index.json
@@ -124,7 +124,10 @@ const endpoints = [
-
Recent syncs
+
Growth timeline
+
+
Loading growth chart...
+
  1. Loading repository history...
diff --git a/site/src/scripts/techapi.js b/site/src/scripts/techapi.js index 7f84280c60f..ac788e21ac3 100644 --- a/site/src/scripts/techapi.js +++ b/site/src/scripts/techapi.js @@ -191,46 +191,129 @@ function countUp(node, target) { (function history() { const totalEl = document.getElementById("history-total"); const countsEl = document.getElementById("history-counts"); + const chartEl = document.getElementById("history-chart"); const listEl = document.getElementById("history-list"); - if (!totalEl || !countsEl || !listEl) return; + if (!totalEl || !countsEl || !chartEl || !listEl) return; const order = ["smartphones", "socs", "gpus", "cpus", "brands"]; const label = { smartphones: "Phones", socs: "SoCs", gpus: "GPUs", cpus: "CPUs", brands: "Brands" }; + const shortLabel = { smartphones: "phones", socs: "socs", gpus: "gpus", cpus: "cpus", brands: "brands" }; + const dumpPath = "site/public/v1/index.json"; + const countRows = (manifest) => order + .map((key) => ({ key, count: manifest.collections?.[key]?.count })) + .filter((row) => row.count != null); + const totalRecords = (manifest) => countRows(manifest).reduce((sum, row) => sum + row.count, 0); + const sumByKey = (rows) => rows.reduce((out, row) => { + out[row.key] = row.count; + return out; + }, {}); - getJSON("v1/index.json").then((manifest) => { - const rows = order - .map((key) => ({ key, count: manifest.collections?.[key]?.count })) - .filter((row) => row.count != null); + function renderSnapshot(manifest) { + const rows = countRows(manifest); const total = rows.reduce((sum, row) => sum + row.count, 0); totalEl.textContent = total.toLocaleString() + " records"; countsEl.innerHTML = rows.map((row) => `
${label[row.key]}${row.count.toLocaleString()}
` ).join(""); + } + + function largestChanges(prevRows, nextRows) { + if (!prevRows) return []; + const prev = sumByKey(prevRows); + return nextRows + .map((row) => ({ key: row.key, delta: row.count - (prev[row.key] || 0) })) + .filter((row) => row.delta > 0) + .sort((a, b) => b.delta - a.delta) + .slice(0, 2); + } + + function renderHistory(points) { + if (!points.length) throw new Error("empty history"); + const maxTotal = Math.max(...points.map((point) => point.total)); + const minTotal = Math.min(...points.map((point) => point.total)); + const range = Math.max(1, maxTotal - minTotal); + chartEl.innerHTML = points.map((point) => { + const pct = 18 + ((point.total - minTotal) / range) * 82; + const deltaText = point.delta > 0 ? "+" + point.delta.toLocaleString() : "baseline"; + return ` + + ${point.total.toLocaleString()} + ${esc(deltaText)} + `; + }).join(""); + + listEl.innerHTML = points.slice().reverse().map((point) => { + const changes = point.changes.length + ? point.changes.map((row) => `${shortLabel[row.key]} +${row.delta.toLocaleString()}`).join(", ") + : (point.delta > 0 ? `total +${point.delta.toLocaleString()}` : "baseline snapshot"); + return `
  • + ${esc(point.title)} + ${esc(point.when)} - ${esc(point.sha)} - ${esc(changes)} +
  • `; + }).join(""); + } + + async function loadCommitHistory(currentManifest) { + const commitsUrl = `https://api.github.com/repos/GetTechAPI/TechAPI/commits?path=${encodeURIComponent(dumpPath)}&per_page=8`; + const response = await fetch(commitsUrl); + if (!response.ok) throw new Error(response.statusText); + const commits = await response.json(); + const items = Array.isArray(commits) ? commits.slice(0, 7) : []; + const snapshots = await Promise.all(items.map(async (item) => { + const sha = String(item.sha || ""); + const rawUrl = `https://raw.githubusercontent.com/GetTechAPI/TechAPI/${sha}/${dumpPath}`; + const raw = await fetch(rawUrl); + if (!raw.ok) return null; + const manifest = await raw.json(); + const date = item.commit?.committer?.date ? new Date(item.commit.committer.date) : null; + return { + sha: sha.slice(0, 7), + dateValue: date ? date.getTime() : 0, + when: date ? date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "recent", + title: (item.commit?.message || "Dataset sync").split("\n")[0], + url: item.html_url || "https://github.com/GetTechAPI/TechAPI", + rows: countRows(manifest), + total: totalRecords(manifest), + }; + })); + + const points = snapshots.filter(Boolean).sort((a, b) => a.dateValue - b.dateValue); + if (!points.length) throw new Error("empty history"); + + const currentTotal = totalRecords(currentManifest); + const latest = points[points.length - 1]; + if (latest.total !== currentTotal) { + points.push({ + sha: "current", + dateValue: Date.now(), + when: "current", + title: "Current published snapshot", + url: base + "v1/index.json", + rows: countRows(currentManifest), + total: currentTotal, + }); + } + + for (let i = 0; i < points.length; i++) { + const prev = points[i - 1]; + points[i].delta = prev ? points[i].total - prev.total : 0; + points[i].changes = largestChanges(prev?.rows, points[i].rows); + } + renderHistory(points); + } + + getJSON("v1/index.json").then((manifest) => { + renderSnapshot(manifest); + return loadCommitHistory(manifest).catch(() => { + chartEl.innerHTML = '
    Growth chart unavailable
    '; + listEl.innerHTML = '
  • Current static dump is available; commit history could not be loaded.GitHub API unavailable
  • '; + }); }).catch(() => { totalEl.textContent = "sync unavailable"; countsEl.innerHTML = '
    Static dumpoffline
    '; + chartEl.innerHTML = '
    Growth chart unavailable
    '; + listEl.innerHTML = '
  • Current static dump could not be loaded.Build the public data first
  • '; }); - - fetch("https://api.github.com/repos/GetTechAPI/TechAPI/commits?path=site/public/v1&per_page=5") - .then((response) => { - if (!response.ok) throw new Error(response.statusText); - return response.json(); - }) - .then((commits) => { - const items = Array.isArray(commits) ? commits.slice(0, 4) : []; - if (!items.length) throw new Error("empty history"); - listEl.innerHTML = items.map((item) => { - const message = (item.commit?.message || "Dataset sync").split("\n")[0]; - const date = item.commit?.committer?.date ? new Date(item.commit.committer.date) : null; - const when = date ? date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "recent"; - const sha = String(item.sha || "").slice(0, 7); - const url = item.html_url || "https://github.com/GetTechAPI/TechAPI"; - return `
  • ${esc(message)}${esc(when)} ยท ${esc(sha)}
  • `; - }).join(""); - }) - .catch(() => { - listEl.innerHTML = '
  • Current static dump is available; repository history could not be loaded.GitHub API unavailable
  • '; - }); })(); /* ============================================================ diff --git a/site/src/styles/techapi.css b/site/src/styles/techapi.css index 894e8973aaa..edb6ae38f6d 100644 --- a/site/src/styles/techapi.css +++ b/site/src/styles/techapi.css @@ -297,11 +297,75 @@ code, .mono { font-family: var(--mono); } .history-count b { color: var(--fg); font-size: 13px; } .history-list { list-style: none; - margin: 18px 0 0; + margin: 20px 0 0; padding: 0; display: grid; gap: 14px; } +.history-chart { + min-height: 210px; + margin-top: 18px; + padding: 16px 12px 12px; + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(62px, 1fr); + align-items: end; + gap: 10px; + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius); + background: + linear-gradient(to top, var(--border) 1px, transparent 1px) 0 25% / 100% 25%, + var(--surface-2); +} +.history-empty { + align-self: center; + justify-self: center; + grid-column: 1 / -1; + font-family: var(--mono); + color: var(--muted); + font-size: 12px; +} +.history-bar { + min-width: 0; + height: 178px; + display: grid; + grid-template-rows: 1fr auto auto; + gap: 6px; + color: var(--fg); +} +.history-bar-fill { + align-self: end; + min-height: 10px; + height: var(--h); + border: 1px solid color-mix(in srgb, var(--accent) 60%, var(--border)); + border-radius: 4px 4px 2px 2px; + background: + linear-gradient(180deg, color-mix(in srgb, var(--accent) 92%, white 8%), var(--accent-deep)); + box-shadow: 0 0 26px -12px var(--accent); + transition: filter .16s, transform .16s; +} +.history-bar:hover .history-bar-fill { + filter: brightness(1.08); + transform: translateY(-2px); +} +.history-bar-total, +.history-bar-delta { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--mono); + text-align: center; +} +.history-bar-total { + font-size: 11px; + font-weight: 700; +} +.history-bar-delta { + font-size: 10px; + color: var(--accent-text); +} .history-list li { display: grid; grid-template-columns: 12px 1fr; @@ -331,6 +395,7 @@ code, .mono { font-family: var(--mono); } @media (max-width: 760px) { .history { grid-template-columns: 1fr; } .history-grid { grid-template-columns: 1fr; } + .history-chart { grid-auto-columns: 68px; } } /* ============================================================ From 61137335a8fa2e400d186c77e22834f9b7f66ec6 Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Thu, 18 Jun 2026 14:51:41 +0900 Subject: [PATCH 2/2] ci: keep persistent tracker issues open Related to #19 --- .github/workflows/pr-metadata.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-metadata.yml b/.github/workflows/pr-metadata.yml index 0a5e894ccab..59a6cff9bbf 100644 --- a/.github/workflows/pr-metadata.yml +++ b/.github/workflows/pr-metadata.yml @@ -107,7 +107,7 @@ jobs: if (trackers.size) { const marker = ''; const existing = pr.body || ''; - const refs = [...trackers].sort().map((ref) => `- Refs ${ref}`).join('\n'); + const refs = [...trackers].sort().map((ref) => `- Related to ${ref}`).join('\n'); const block = `${marker}\n\n## Tracking\n${refs}`; const body = existing.includes(marker) ? existing.replace(new RegExp(`${marker}[\\s\\S]*$`), block) @@ -160,6 +160,11 @@ jobs: project_json="$(gh project view "$project_number" --owner "$owner" --format json)" project_id="$(jq -r '.id' <<< "$project_json")" item_id="$(gh project item-add "$project_number" --owner "$owner" --url "$PR_URL" --format json --jq '.id')" + status_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Status") | .id')" + status_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Status") | .options[] | select(.name == "In Progress") | .id')" + if [ -z "$status_option_id" ]; then + status_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Status") | .options[] | select(.name == "Todo") | .id')" + fi start_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Start date") | .id')" target_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Target date") | .id')" priority_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Priority") | .id')" @@ -168,6 +173,9 @@ jobs: priority_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Priority") | .options[] | select(.name == "Medium") | .id')" fi + if [ -n "$item_id" ] && [ -n "$status_field_id" ] && [ -n "$status_option_id" ]; then + gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$status_field_id" --single-select-option-id "$status_option_id" + fi if [ -n "$item_id" ] && [ -n "$start_field_id" ]; then gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$start_field_id" --date "$today" fi