From 178401b5c52af93e057f3d19224e600939252449 Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Thu, 18 Jun 2026 09:31:15 +0900 Subject: [PATCH 1/3] feat(site): add dataset history section --- site/src/pages/index.astro | 33 +++++++++++++- site/src/scripts/techapi.js | 55 +++++++++++++++++++++++- site/src/styles/techapi.css | 86 +++++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 3 deletions(-) diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 7895800f1da..9af43135f92 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -48,7 +48,8 @@ const endpoints = [ TTechAPI @@ -104,6 +105,34 @@ 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.

+
+ v1/index.json +
+ +
+
+
Current snapshot
+
loading
+
+
+
+
Recent syncs
+
    +
  1. Loading repository history...
  2. +
+
+
+
+
+
diff --git a/site/src/scripts/techapi.js b/site/src/scripts/techapi.js index f1f6af04d90..7f84280c60f 100644 --- a/site/src/scripts/techapi.js +++ b/site/src/scripts/techapi.js @@ -3,7 +3,12 @@ const raw = import.meta.env.BASE_URL; const base = raw.endsWith("/") ? raw : raw + "/"; const absUrl = (path) => new URL(path.replace(/^\//, ""), location.origin + base).href; -const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">"); +const esc = (s) => String(s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); /* ---- theme: follow OS preference until the user picks one (persisted) ---- */ const root = document.documentElement; @@ -180,6 +185,54 @@ function countUp(node, target) { })(performance.now()); } +/* ============================================================ + HISTORY + ============================================================ */ +(function history() { + const totalEl = document.getElementById("history-total"); + const countsEl = document.getElementById("history-counts"); + const listEl = document.getElementById("history-list"); + if (!totalEl || !countsEl || !listEl) return; + + const order = ["smartphones", "socs", "gpus", "cpus", "brands"]; + const label = { smartphones: "Phones", socs: "SoCs", gpus: "GPUs", cpus: "CPUs", brands: "Brands" }; + + getJSON("v1/index.json").then((manifest) => { + const rows = order + .map((key) => ({ key, count: manifest.collections?.[key]?.count })) + .filter((row) => row.count != null); + 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(""); + }).catch(() => { + totalEl.textContent = "sync unavailable"; + countsEl.innerHTML = '
Static dumpoffline
'; + }); + + 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
  • '; + }); +})(); + /* ============================================================ PLAYGROUND ============================================================ */ diff --git a/site/src/styles/techapi.css b/site/src/styles/techapi.css index 1e1e1f80988..894e8973aaa 100644 --- a/site/src/styles/techapi.css +++ b/site/src/styles/techapi.css @@ -247,6 +247,92 @@ code, .mono { font-family: var(--mono); } .hero { padding: 56px 0 40px; } } +/* ============================================================ + HISTORY + ============================================================ */ +.history { + display: grid; + grid-template-columns: .9fr 1.1fr; + gap: 16px; +} +.history-panel { + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + padding: 22px; +} +.history-label { + font-family: var(--mono); + font-size: 11px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: .12em; +} +.history-total { + margin-top: 12px; + font-family: var(--mono); + font-size: clamp(30px, 4vw, 48px); + font-weight: 700; + letter-spacing: -.04em; + line-height: 1; +} +.history-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + margin-top: 22px; +} +.history-count { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-2); + font-family: var(--mono); +} +.history-count span { color: var(--muted); font-size: 12px; } +.history-count b { color: var(--fg); font-size: 13px; } +.history-list { + list-style: none; + margin: 18px 0 0; + padding: 0; + display: grid; + gap: 14px; +} +.history-list li { + display: grid; + grid-template-columns: 12px 1fr; + gap: 12px; + align-items: start; +} +.history-dot { + width: 8px; + height: 8px; + margin-top: 7px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent); +} +.history-list a { + color: var(--fg); + font-weight: 600; +} +.history-list a:hover { color: var(--accent-text); } +.history-list small { + display: block; + margin-top: 2px; + font-family: var(--mono); + font-size: 11.5px; + color: var(--muted); +} +@media (max-width: 760px) { + .history { grid-template-columns: 1fr; } + .history-grid { grid-template-columns: 1fr; } +} + /* ============================================================ SECTION SHELL ============================================================ */ From f7945e54209ee866f3e3c9931e3d8779377c6ce9 Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Thu, 18 Jun 2026 09:36:43 +0900 Subject: [PATCH 2/3] ci(site): request engine validation for homepage PRs --- .github/workflows/request-engine-pr-validation.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/request-engine-pr-validation.yml b/.github/workflows/request-engine-pr-validation.yml index de3d8ce962a..0bec07247b5 100644 --- a/.github/workflows/request-engine-pr-validation.yml +++ b/.github/workflows/request-engine-pr-validation.yml @@ -1,9 +1,9 @@ name: request-engine-pr-validation -# Ask TechEngine to validate data PRs and report the result back as a PR -# comment. The ordinary PR check still runs locally; this gives curator-facing -# feedback from the engine repository without moving PR ownership away from the -# human author. +# Ask TechEngine to validate TechAPI PRs and report the result back as PR +# comments. Data changes get engine data checks; homepage changes get the +# TechAPI site build check without moving PR ownership away from the human +# author. on: pull_request: types: [opened, synchronize, reopened, ready_for_review] @@ -11,6 +11,11 @@ on: - "data/**" - "site/public/v1/**" - "site/public/openapi.json" + - "site/src/**" + - "site/public/**" + - "site/astro.config.*" + - "site/package.json" + - "site/package-lock.json" - "app/validate.py" workflow_dispatch: inputs: From a8614d1b1f7aeb23e19e786c82cc244e55577a53 Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Thu, 18 Jun 2026 10:09:17 +0900 Subject: [PATCH 3/3] ci: auto-fill PR metadata --- .github/workflows/pr-metadata.yml | 180 ++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 .github/workflows/pr-metadata.yml diff --git a/.github/workflows/pr-metadata.yml b/.github/workflows/pr-metadata.yml new file mode 100644 index 00000000000..0a5e894ccab --- /dev/null +++ b/.github/workflows/pr-metadata.yml @@ -0,0 +1,180 @@ +name: pr-metadata + +# Keep PR triage fields useful without requiring manual cleanup on every branch. +# This workflow only updates PR metadata; it never checks out or executes PR code. +on: + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review] + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + triage: + runs-on: ubuntu-latest + env: + PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN || secrets.ENGINE_TOKEN }} + TRIAGE_PROJECT_URLS: ${{ vars.TRIAGE_PROJECT_URLS }} + steps: + - name: Assign author and label by changed paths + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = pr.number; + + const assignees = ['TechEngineBot']; + if (!pr.user?.type?.endsWith('Bot')) { + assignees.push(pr.user.login); + } + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: issue_number, + per_page: 100, + }); + + const labels = new Set(); + const trackers = new Set(); + const title = pr.title.toLowerCase(); + for (const file of files) { + const path = file.filename; + const isDataDump = path.startsWith('site/public/v1/') || path === 'site/public/openapi.json'; + if (path.startsWith('site/') && !isDataDump) { + labels.add('site'); + trackers.add('#19'); + } + if (path.startsWith('data/') || isDataDump) { + labels.add('data'); + trackers.add('#1'); + } + if (path.startsWith('app/')) labels.add('app'); + if (path.startsWith('.github/workflows/')) labels.add('ci'); + if (path.startsWith('docs/') || path === 'README.md' || path.endsWith('.md')) labels.add('documentation'); + } + if (title.startsWith('feat') || labels.has('site') || labels.has('data') || labels.has('app')) { + labels.add('enhancement'); + } + if (title.startsWith('fix')) { + labels.add('bug'); + } + + await github.rest.issues.addAssignees({ + owner, + repo, + issue_number, + assignees: [...new Set(assignees)], + }); + + if (labels.size) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [...labels], + }); + } + + const milestoneByLabel = new Map([ + ['site', 'Homepage and site improvements'], + ['data', 'Massive dataset rebuild (1989-2026)'], + ]); + for (const [label, title] of milestoneByLabel) { + if (!labels.has(label)) continue; + const milestones = await github.paginate(github.rest.issues.listMilestones, { + owner, + repo, + state: 'open', + per_page: 100, + }); + const milestone = milestones.find((item) => item.title === title); + if (milestone) { + await github.rest.issues.update({ + owner, + repo, + issue_number, + milestone: milestone.number, + }); + } + break; + } + + if (trackers.size) { + const marker = ''; + const existing = pr.body || ''; + const refs = [...trackers].sort().map((ref) => `- Refs ${ref}`).join('\n'); + const block = `${marker}\n\n## Tracking\n${refs}`; + const body = existing.includes(marker) + ? existing.replace(new RegExp(`${marker}[\\s\\S]*$`), block) + : `${existing.trim()}\n\n${block}`.trim(); + await github.rest.pulls.update({ + owner, + repo, + pull_number: issue_number, + body, + }); + } + + const changedLines = files.reduce((sum, file) => sum + file.additions + file.deletions, 0); + let priority = 'Medium'; + if (labels.has('data') && (files.length >= 25 || changedLines >= 1000)) { + priority = 'High'; + } else if (labels.has('data') || labels.has('app') || title.startsWith('fix')) { + priority = 'High'; + } else if (labels.size === 1 && labels.has('documentation')) { + priority = 'Low'; + } else if (changedLines <= 25 && !labels.has('ci')) { + priority = 'Low'; + } + core.exportVariable('TRIAGE_PRIORITY', priority); + + - name: Add PR to configured projects + if: env.TRIAGE_PROJECT_URLS != '' && env.PROJECT_TOKEN != '' + env: + GH_TOKEN: ${{ env.PROJECT_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + shell: bash + run: | + set -euo pipefail + IFS=',' read -ra urls <<< "${TRIAGE_PROJECT_URLS}" + today="$(date -u +%F)" + for url in "${urls[@]}"; do + url="$(echo "$url" | xargs)" + [ -z "$url" ] && continue + if [[ "$url" =~ github.com/orgs/([^/]+)/projects/([0-9]+) ]]; then + owner="${BASH_REMATCH[1]}" + project_number="${BASH_REMATCH[2]}" + elif [[ "$url" =~ github.com/users/([^/]+)/projects/([0-9]+) ]]; then + owner="${BASH_REMATCH[1]}" + project_number="${BASH_REMATCH[2]}" + else + echo "::warning::Unsupported project URL: $url" + continue + fi + + 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')" + 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')" + priority_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq ".fields[] | select(.name == \"Priority\") | .options[] | select(.name == \"${TRIAGE_PRIORITY:-Medium}\") | .id")" + if [ -z "$priority_option_id" ]; then + 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 "$start_field_id" ]; then + gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$start_field_id" --date "$today" + fi + if [ -n "$item_id" ] && [ -n "$target_field_id" ]; then + gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$target_field_id" --date "$today" + fi + if [ -n "$item_id" ] && [ -n "$priority_field_id" ] && [ -n "$priority_option_id" ]; then + gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$priority_field_id" --single-select-option-id "$priority_option_id" + fi + done