Jaypore CI

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

commit e342ba7781c898f7d0137105e5fb643b9641e9c8
parent 5d3b55975d2cfb3d17145e63147d47cb19a33a67
Author: Arjoonn@exe.dev <arjoonn+exe.dev@midpathsoftware.com>
Date:   Wed, 25 Feb 2026 06:12:34 +0000

Add status.txt for pipeline status tracking and light theme UI

- Added status.txt file in CI artifacts that tracks pipeline state:
  - 'running' when job starts
  - 'ok' when run.sh completes successfully
  - 'err' when run.sh fails

- Updated UI to light theme with clean styling
- Made push status more visible with 'pushed'/'local' badges
- Status shown as colored badges (green=ok, red=err, yellow=running)
- Fallback parsing of old results without status.txt for backwards compat

Co-authored-by: Shelley <shelley@exe.dev>

Diffstat:
Minternal/jci/run.go | 34+++++++++++++++++++++++++---------
Minternal/jci/web.go | 212++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
2 files changed, 165 insertions(+), 81 deletions(-)

diff --git a/internal/jci/run.go b/internal/jci/run.go @@ -42,10 +42,21 @@ func Run(args []string) error { return fmt.Errorf("failed to create output dir: %w", err) } + // Set initial status to "running" + statusFile := filepath.Join(outputDir, "status.txt") + os.WriteFile(statusFile, []byte("running"), 0644) + // Run CI err = runCI(repoRoot, outputDir, commit) // Continue even if CI fails - we still want to store the results + // Update status based on result + if err != nil { + os.WriteFile(statusFile, []byte("err"), 0644) + } else { + os.WriteFile(statusFile, []byte("ok"), 0644) + } + // Generate index.html with results if err := generateIndexHTML(outputDir, commit, err); err != nil { fmt.Printf("Warning: failed to generate index.html: %v\n", err) @@ -121,11 +132,13 @@ func runCI(repoRoot string, outputDir string, commit string) error { func generateIndexHTML(outputDir string, commit string, ciErr error) error { commitMsg, _ := git("log", "-1", "--format=%s", commit) - status := "success" statusIcon := "✓ PASSED" + statusColor := "#1a7f37" + statusBg := "#dafbe1" if ciErr != nil { - status = "failed" statusIcon = "✗ FAILED" + statusColor = "#cf222e" + statusBg = "#ffebe9" } // Read output for standalone view @@ -141,22 +154,25 @@ func generateIndexHTML(outputDir string, commit string, ciErr error) error { <meta charset="utf-8"> <title>%s %s</title> <style> - body { font-family: monospace; font-size: 12px; background: #1a1a1a; color: #e0e0e0; padding: 8px; } - .header { margin-bottom: 8px; } - .%s { color: %s; font-weight: bold; } - pre { white-space: pre-wrap; } + body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; font-size: 13px; background: #f5f5f5; color: #24292f; padding: 16px; } + .header { margin-bottom: 12px; padding: 12px; background: #fff; border-radius: 8px; border: 1px solid #d0d7de; } + .status { display: inline-block; padding: 4px 10px; border-radius: 16px; font-weight: 600; font-size: 12px; background: %s; color: %s; } + .commit-info { margin-top: 8px; color: #57606a; font-size: 12px; } + .commit-hash { color: #0969da; font-family: monospace; } + pre { white-space: pre-wrap; background: #fff; padding: 16px; border-radius: 8px; border: 1px solid #d0d7de; font-family: "Monaco", "Menlo", monospace; font-size: 12px; line-height: 1.5; } </style> </head> <body> <div class="header"> - <span class="%s">%s</span> %s %s + <span class="status">%s</span> + <div class="commit-info"><span class="commit-hash">%s</span> %s</div> </div> <pre>%s</pre> </body> </html> `, commit[:7], escapeHTML(commitMsg), - status, map[string]string{"success": "#3fb950", "failed": "#f85149"}[status], - status, statusIcon, commit[:7], escapeHTML(commitMsg), + statusBg, statusColor, + statusIcon, commit[:7], escapeHTML(commitMsg), escapeHTML(outputContent)) indexPath := filepath.Join(outputDir, "index.html") diff --git a/internal/jci/web.go b/internal/jci/web.go @@ -176,21 +176,36 @@ func getBranchCommits(branch string, limit int) ([]CommitInfo, error) { return commits, nil } -// getCIStatus returns "success" or "failed" based on CI results +// getCIStatus returns "success", "failed", or "running" based on status.txt func getCIStatus(commit string) string { - // Try to read the index.html and look for status ref := "refs/jci/" + commit - cmd := exec.Command("git", "show", ref+":index.html") + + // Try to read status.txt (new format) + cmd := exec.Command("git", "show", ref+":status.txt") out, err := cmd.Output() + if err == nil { + status := strings.TrimSpace(string(out)) + switch status { + case "ok": + return "success" + case "err": + return "failed" + case "running": + return "running" + } + } + + // Fallback: parse index.html for old results + cmd = exec.Command("git", "show", ref+":index.html") + out, err = cmd.Output() if err != nil { return "" } - content := string(out) - if strings.Contains(content, "class=\"status success\"") { + if strings.Contains(content, "PASSED") || strings.Contains(content, "SUCCESS") { return "success" } - if strings.Contains(content, "class=\"status failed\"") { + if strings.Contains(content, "FAILED") { return "failed" } return "" @@ -279,41 +294,41 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; font-size: 12px; - background: #1a1a1a; - color: #e0e0e0; + background: #f5f5f5; + color: #333; display: flex; height: 100vh; overflow: hidden; } - a { color: #58a6ff; text-decoration: none; } + a { color: #0969da; text-decoration: none; } a:hover { text-decoration: underline; } /* Left panel - commits */ .commits-panel { - width: 240px; - background: #1e1e1e; - border-right: 1px solid #333; + width: 280px; + background: #fff; + border-right: 1px solid #d0d7de; display: flex; flex-direction: column; flex-shrink: 0; } .panel-header { - padding: 6px 8px; - background: #252525; - border-bottom: 1px solid #333; + padding: 8px 10px; + background: #f6f8fa; + border-bottom: 1px solid #d0d7de; display: flex; align-items: center; - gap: 6px; + gap: 8px; } - .panel-header h1 { font-size: 12px; font-weight: 600; color: #888; } + .panel-header h1 { font-size: 13px; font-weight: 600; color: #24292f; } .branch-selector { flex: 1; - padding: 2px 4px; - font-size: 11px; - border: 1px solid #444; - border-radius: 3px; - background: #2a2a2a; - color: #fff; + padding: 4px 8px; + font-size: 12px; + border: 1px solid #d0d7de; + border-radius: 6px; + background: #fff; + color: #24292f; } .commit-list { list-style: none; @@ -321,62 +336,89 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { flex: 1; } .commit-item { - padding: 3px 6px; + padding: 6px 10px; cursor: pointer; display: flex; align-items: center; - gap: 4px; - border-bottom: 1px solid #252525; + gap: 8px; + border-bottom: 1px solid #eaeef2; } - .commit-item:hover { background: #2a2a2a; } - .commit-item.selected { background: #2d4a3e; } + .commit-item:hover { background: #f6f8fa; } + .commit-item.selected { background: #ddf4ff; } .commit-item.no-ci { opacity: 0.5; } - .ci-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; } - .ci-dot.success { background: #3fb950; } - .ci-dot.failed { background: #f85149; } - .ci-dot.none { background: #484f58; } - .commit-hash { font-size: 10px; color: #58a6ff; flex-shrink: 0; } - .commit-msg { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #888; font-size: 11px; } - .ci-push-badge { font-size: 8px; color: #666; } - .ci-push-badge.pushed { color: #3fb950; } + + /* Status indicator */ + .status-badge { + font-size: 10px; + font-weight: 600; + padding: 2px 6px; + border-radius: 12px; + flex-shrink: 0; + } + .status-badge.success { background: #dafbe1; color: #1a7f37; } + .status-badge.failed { background: #ffebe9; color: #cf222e; } + .status-badge.running { background: #fff8c5; color: #9a6700; } + .status-badge.none { background: #eaeef2; color: #656d76; } + + .commit-hash { font-size: 11px; color: #0969da; flex-shrink: 0; font-family: monospace; } + .commit-msg { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #57606a; font-size: 12px; } + + /* Push status badge */ + .push-badge { + font-size: 9px; + font-weight: 600; + padding: 1px 5px; + border-radius: 10px; + flex-shrink: 0; + } + .push-badge.pushed { background: #ddf4ff; color: #0969da; } + .push-badge.local { background: #fff8c5; color: #9a6700; } /* Middle panel - files */ .files-panel { - width: 180px; - background: #1e1e1e; - border-right: 1px solid #333; + width: 200px; + background: #fff; + border-right: 1px solid #d0d7de; display: flex; flex-direction: column; flex-shrink: 0; } .files-panel.hidden { display: none; } .commit-info { - padding: 6px 8px; - background: #252525; - border-bottom: 1px solid #333; - font-size: 11px; + padding: 10px 12px; + background: #f6f8fa; + border-bottom: 1px solid #d0d7de; + font-size: 12px; + } + .commit-info .status-line { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; } - .commit-info .status { font-weight: 600; } - .commit-info .status.success { color: #3fb950; } - .commit-info .status.failed { color: #f85149; } - .commit-info .hash { color: #58a6ff; } - .commit-info .meta { color: #666; margin-top: 2px; } + .commit-info .status-icon { font-size: 14px; font-weight: bold; } + .commit-info .status-icon.success { color: #1a7f37; } + .commit-info .status-icon.failed { color: #cf222e; } + .commit-info .status-icon.running { color: #9a6700; } + .commit-info .hash { color: #0969da; font-family: monospace; } + .commit-info .meta { color: #656d76; margin-top: 4px; font-size: 11px; } .file-list { list-style: none; overflow-y: auto; flex: 1; } .file-item { - padding: 3px 8px; + padding: 6px 12px; cursor: pointer; - border-bottom: 1px solid #252525; + border-bottom: 1px solid #eaeef2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - font-size: 11px; + font-size: 12px; + color: #24292f; } - .file-item:hover { background: #2a2a2a; } - .file-item.selected { background: #2d4a3e; } + .file-item:hover { background: #f6f8fa; } + .file-item.selected { background: #ddf4ff; } /* Right panel - content */ .content-panel { @@ -384,26 +426,29 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { display: flex; flex-direction: column; min-width: 0; - background: #1a1a1a; + background: #fff; } .content-header { - padding: 4px 8px; - background: #252525; - border-bottom: 1px solid #333; - font-size: 11px; - color: #888; + padding: 6px 12px; + background: #f6f8fa; + border-bottom: 1px solid #d0d7de; + font-size: 12px; + color: #57606a; + font-family: monospace; } .content-body { flex: 1; overflow: auto; + background: #fff; } .content-body pre { - padding: 8px; - font-family: "Monaco", "Menlo", monospace; - font-size: 11px; - line-height: 1.4; + padding: 12px; + font-family: "Monaco", "Menlo", "Consolas", monospace; + font-size: 12px; + line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; + color: #24292f; } .content-body iframe { width: 100%; @@ -416,12 +461,13 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { align-items: center; justify-content: center; height: 100%; - color: #666; + color: #656d76; + font-size: 13px; } @media (max-width: 700px) { - .commits-panel { width: 180px; } - .files-panel { width: 140px; } + .commits-panel { width: 200px; } + .files-panel { width: 150px; } } </style> </head> @@ -460,20 +506,35 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { if (m) selectCommitByHash(m[1]); } + function getStatusLabel(status) { + switch(status) { + case 'success': return '✓ ok'; + case 'failed': return '✗ err'; + case 'running': return '⋯ run'; + default: return '—'; + } + } + 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'; - const pushIcon = c.hasCI ? (c.ciPushed ? '↑' : '○') : ''; - const pushClass = c.ciPushed ? 'pushed' : ''; + 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="ci-dot ' + status + '"></span>' + + '<span class="status-badge ' + status + '">' + getStatusLabel(status) + '</span>' + '<span class="commit-hash">' + c.shortHash + '</span>' + '<span class="commit-msg">' + escapeHtml(c.message) + '</span>' + - '<span class="ci-push-badge ' + pushClass + '">' + pushIcon + '</span></li>'; + pushBadge + '</li>'; }).join(''); list.querySelectorAll('.commit-item').forEach(el => { el.onclick = () => selectCommit(el.dataset.hash, el.dataset.hasci === 'true'); @@ -514,8 +575,15 @@ func showMainPage(w http.ResponseWriter, r *http.Request) { const infoRes = await fetch('/api/commit/' + hash); const info = await infoRes.json(); + let statusIcon = '?'; + let statusClass = ''; + if (info.status === 'success') { statusIcon = '✓'; statusClass = 'success'; } + else if (info.status === 'failed') { statusIcon = '✗'; statusClass = 'failed'; } + else if (info.status === 'running') { statusIcon = '⋯'; statusClass = 'running'; } + document.getElementById('commitInfo').innerHTML = - '<div><span class="status ' + info.status + '">' + (info.status === 'success' ? '✓' : '✗') + '</span> ' + + '<div class="status-line">' + + '<span class="status-icon ' + statusClass + '">' + statusIcon + '</span>' + '<span class="hash">' + hash.slice(0,7) + '</span></div>' + '<div class="meta">' + escapeHtml(info.author) + ' · ' + escapeHtml(info.date) + '</div>';