Jaypore CI

> Jaypore CI: Minimal, Offline, Local CI system.
Log | Files | Refs | README | LICENSE

commit 15bd7abba3f5f45cf305509c988c4db9d1017c4e
parent dd2990ee488d38e5747b9c42bd90f2254385cf4b
Author: Arjoonn Sharma <arjoonn@midpathsoftware.com>
Date:   Sat, 28 Feb 2026 13:35:52 +0530

x

Diffstat:
Minternal/jci/web.go | 192++++++++++++++++++++++++++++++++++---------------------------------------------
1 file changed, 83 insertions(+), 109 deletions(-)

diff --git a/internal/jci/web.go b/internal/jci/web.go @@ -119,87 +119,7 @@ func getLocalBranches() ([]string, error) { // getRemoteJCIRefs returns a set of commits that have CI refs pushed to remote -// getBranchCommits returns recent commits for a branch -func getBranchCommits(branch string, limit int) ([]CommitInfo, error) { - // Get commit hash and message - out, err := git("log", branch, fmt.Sprintf("--max-count=%d", limit), "--format=%H|%s") - if err != nil { - return nil, err - } - if out == "" { - return nil, nil - } - - // Get local JCI refs (single-run) - jciRefs, _ := ListJCIRefs() - jciSet := make(map[string]bool) - for _, ref := range jciRefs { - commit := strings.TrimPrefix(ref, "jci/") - jciSet[commit] = true - } - - // Get local JCI run refs (multi-run) - // Map: commit -> list of run refs - jciRuns := make(map[string][]string) - runRefs, _ := ListJCIRunRefs() - for _, ref := range runRefs { - // ref is refs/jci-runs/<commit>/<runid> - parts := strings.Split(strings.TrimPrefix(ref, "refs/jci-runs/"), "/") - if len(parts) >= 2 { - commit := parts[0] - jciRuns[commit] = append(jciRuns[commit], ref) - } - } - - // Get remote JCI refs for CI push status - remoteCI := getRemoteJCIRefs("origin") - - var commits []CommitInfo - for _, line := range strings.Split(out, "\n") { - parts := strings.SplitN(line, "|", 2) - if len(parts) != 2 { - continue - } - hash := parts[0] - msg := parts[1] - - hasCI := jciSet[hash] || len(jciRuns[hash]) > 0 - commit := CommitInfo{ - Hash: hash, - ShortHash: hash[:7], - Message: msg, - HasCI: hasCI, - CIPushed: remoteCI["refs/jci/"+hash], - } - - if jciSet[hash] { - commit.CIStatus = getCIStatus(hash) - } - // Add multi-run info - for _, runRef := range jciRuns[hash] { - parts := strings.Split(strings.TrimPrefix(runRef, "refs/jci-runs/"), "/") - if len(parts) >= 2 { - runID := parts[1] - status := getCIStatusFromRef(runRef) - commit.Runs = append(commit.Runs, RunInfo{ - RunID: runID, - Status: status, - Ref: runRef, - }) - } - } - - // If no single-run status but has runs, use latest run status - if commit.CIStatus == "" && len(commit.Runs) > 0 { - commit.CIStatus = commit.Runs[len(commit.Runs)-1].Status - } - - commits = append(commits, commit) - } - - return commits, nil -} // getCIStatus returns "success", "failed", or "running" based on status.txt func getCIStatus(commit string) string { @@ -725,6 +645,7 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { <script> let branches = [], currentCommit = null, currentFiles = [], currentFile = null, currentRuns = [], currentRunId = null; + let currentBranch = null, currentPage = 0, loadedCommits = [], hasMoreCommits = false; async function loadBranches() { const res = await fetch('/api/branches'); @@ -732,8 +653,8 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { const select = document.getElementById('branchSelect'); select.innerHTML = branches.map(b => '<option value="' + b.name + '">' + b.name + '</option>').join(''); const def = branches.find(b => b.name === 'main') || branches[0]; - if (def) { select.value = def.name; showBranch(def.name); } - + if (def) { select.value = def.name; await showBranch(def.name); } + // Check URL for initial commit and file const m = location.pathname.match(/^\/jci\/([a-f0-9]+)/); if (m) selectCommitByHash(m[1]); @@ -748,40 +669,92 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { } } - function showBranch(name) { - const branch = branches.find(b => b.name === name); - if (!branch) return; - const list = document.getElementById('commitList'); - list.innerHTML = (branch.commits || []).map(c => { - const status = c.hasCI ? (c.ciStatus || 'none') : 'none'; - const noCiClass = c.hasCI ? '' : 'no-ci'; - let pushBadge = ''; - if (c.hasCI) { - if (c.ciPushed) { - pushBadge = '<span class="push-badge pushed">pushed</span>'; - } else { - pushBadge = '<span class="push-badge local">local</span>'; - } - } - return '<li class="commit-item ' + noCiClass + '" data-hash="' + c.hash + '" data-hasci="' + c.hasCI + '">' + - '<span class="status-badge ' + status + '">' + getStatusLabel(status) + '</span>' + - '<span class="commit-hash">' + c.shortHash + '</span>' + - '<span class="commit-msg">' + escapeHtml(c.message) + '</span>' + - pushBadge + '</li>'; - }).join(''); - list.querySelectorAll('.commit-item').forEach(el => { + function renderCommitItem(c) { + const status = c.hasCI ? (c.ciStatus || 'none') : 'none'; + const noCiClass = c.hasCI ? '' : 'no-ci'; + let pushBadge = ''; + if (c.hasCI) { + pushBadge = c.ciPushed + ? '<span class="push-badge pushed">pushed</span>' + : '<span class="push-badge local">local</span>'; + } + return '<li class="commit-item ' + noCiClass + '" data-hash="' + c.hash + '" data-hasci="' + c.hasCI + '">' + + '<span class="status-badge ' + status + '">' + getStatusLabel(status) + '</span>' + + '<span class="commit-hash">' + c.shortHash + '</span>' + + '<span class="commit-msg">' + escapeHtml(c.message) + '</span>' + + pushBadge + '</li>'; + } + + function attachCommitClickHandlers() { + document.querySelectorAll('.commit-item').forEach(el => { el.onclick = () => selectCommit(el.dataset.hash, el.dataset.hasci === 'true'); }); } - function selectCommitByHash(hash) { - // Find full hash from branches - for (const b of branches) { - const c = (b.commits || []).find(c => c.hash.startsWith(hash)); - if (c) { selectCommit(c.hash, c.hasCI); return; } + async function showBranch(name) { + currentBranch = name; + currentPage = 0; + loadedCommits = []; + hasMoreCommits = false; + const list = document.getElementById('commitList'); + list.innerHTML = '<li style="padding:8px 10px;color:#656d76;">Loading…</li>'; + await loadMoreCommits(); + } + + async function loadMoreCommits() { + const res = await fetch('/api/commits?branch=' + encodeURIComponent(currentBranch) + '&page=' + currentPage); + const data = await res.json(); + loadedCommits = loadedCommits.concat(data.commits || []); + hasMoreCommits = data.hasMore || false; + + const list = document.getElementById('commitList'); + // Remove existing load-more button if present + const oldBtn = document.getElementById('loadMoreBtn'); + if (oldBtn) oldBtn.remove(); + + if (currentPage === 0) { + list.innerHTML = loadedCommits.map(renderCommitItem).join(''); + } else { + // Append newly loaded commits (remove loading placeholder first) + const placeholder = document.getElementById('loadMorePlaceholder'); + if (placeholder) placeholder.remove(); + const frag = document.createDocumentFragment(); + const tmp = document.createElement('ul'); + tmp.innerHTML = (data.commits || []).map(renderCommitItem).join(''); + while (tmp.firstChild) frag.appendChild(tmp.firstChild); + list.appendChild(frag); + } + + attachCommitClickHandlers(); + + if (hasMoreCommits) { + const btn = document.createElement('li'); + btn.id = 'loadMoreBtn'; + btn.style.cssText = 'padding:8px 10px;text-align:center;cursor:pointer;color:#0969da;border-top:1px solid #eaeef2;'; + btn.textContent = 'Load more commits…'; + btn.onclick = async () => { + btn.textContent = 'Loading…'; + btn.onclick = null; + currentPage++; + await loadMoreCommits(); + }; + list.appendChild(btn); + } + + // Re-highlight selected commit if any + if (currentCommit) { + document.querySelectorAll('.commit-item').forEach(el => + el.classList.toggle('selected', el.dataset.hash === currentCommit) + ); } } + function selectCommitByHash(hash) { + // Search already-loaded commits + const c = loadedCommits.find(c => c.hash.startsWith(hash)); + if (c) { selectCommit(c.hash, c.hasCI); return; } + } + async function selectCommit(hash, hasCI) { currentCommit = hash; currentFile = null; @@ -1044,6 +1017,7 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { document.getElementById('branchSelect').onchange = e => showBranch(e.target.value); loadBranches(); + </script> </body> </html>