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:
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>';