summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/app.js26
-rw-r--r--web/style.css21
-rw-r--r--web/test/deployment-badge.test.mjs72
3 files changed, 119 insertions, 0 deletions
diff --git a/web/app.js b/web/app.js
index e1782dd..9708727 100644
--- a/web/app.js
+++ b/web/app.js
@@ -96,6 +96,26 @@ export function renderChangestatsBadge(stats, doc = (typeof document !== 'undefi
return span;
}
+// Returns a <span class="deployment-badge"> element indicating whether the
+// currently-deployed server includes the task's fix commits.
+// Returns null if status is null/undefined or doc is null.
+// Accepts an optional doc parameter for testability (defaults to document).
+export function renderDeploymentBadge(status, doc = (typeof document !== 'undefined' ? document : null)) {
+ if (status == null || doc == null) return null;
+ const span = doc.createElement('span');
+ if (status.includes_fix) {
+ span.className = 'deployment-badge deployment-badge--deployed';
+ span.textContent = '✓ Deployed';
+ } else {
+ span.className = 'deployment-badge deployment-badge--pending';
+ span.textContent = '⚠ Not deployed';
+ }
+ if (status.deployed_commit) {
+ span.title = `Deployed commit: ${status.deployed_commit.slice(0, 8)}`;
+ }
+ return span;
+}
+
function truncateToWordBoundary(text, maxLen = 120) {
if (!text || text.length <= maxLen) return text;
const cut = text.lastIndexOf(' ', maxLen);
@@ -153,6 +173,12 @@ function createTaskCard(task) {
if (csBadge) card.appendChild(csBadge);
}
+ // Deployment status badge for READY tasks
+ if (task.state === 'READY' && task.deployment_status != null) {
+ const depBadge = renderDeploymentBadge(task.deployment_status);
+ if (depBadge) card.appendChild(depBadge);
+ }
+
// Footer: action buttons based on state
// Interrupted states (CANCELLED, FAILED, BUDGET_EXCEEDED) show both Resume and Restart.
// TIMED_OUT shows Resume only. Others show a single action.
diff --git a/web/style.css b/web/style.css
index e7d1de4..1b6fb69 100644
--- a/web/style.css
+++ b/web/style.css
@@ -863,6 +863,27 @@ dialog label select:focus {
white-space: nowrap;
}
+.deployment-badge {
+ display: inline-block;
+ font-size: 0.72rem;
+ font-weight: 600;
+ padding: 0.15em 0.45em;
+ border-radius: 0.25rem;
+ white-space: nowrap;
+}
+
+.deployment-badge--deployed {
+ background: color-mix(in srgb, var(--success, #22c55e) 15%, transparent);
+ color: var(--success, #16a34a);
+ border: 1px solid color-mix(in srgb, var(--success, #22c55e) 35%, transparent);
+}
+
+.deployment-badge--pending {
+ background: color-mix(in srgb, var(--warn, #f59e0b) 15%, transparent);
+ color: var(--warn, #b45309);
+ border: 1px solid color-mix(in srgb, var(--warn, #f59e0b) 35%, transparent);
+}
+
.btn-view-logs {
font-size: 0.72rem;
font-weight: 600;
diff --git a/web/test/deployment-badge.test.mjs b/web/test/deployment-badge.test.mjs
new file mode 100644
index 0000000..afb4a59
--- /dev/null
+++ b/web/test/deployment-badge.test.mjs
@@ -0,0 +1,72 @@
+// deployment-badge.test.mjs — Unit tests for deployment status badge.
+//
+// Run with: node --test web/test/deployment-badge.test.mjs
+
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import { renderDeploymentBadge } from '../app.js';
+
+function makeDoc() {
+ return {
+ createElement(tag) {
+ const el = {
+ tag,
+ className: '',
+ textContent: '',
+ title: '',
+ children: [],
+ appendChild(child) { this.children.push(child); return child; },
+ };
+ return el;
+ },
+ };
+}
+
+describe('renderDeploymentBadge', () => {
+ it('returns null for null status', () => {
+ const el = renderDeploymentBadge(null, makeDoc());
+ assert.equal(el, null);
+ });
+
+ it('returns null for undefined status', () => {
+ const el = renderDeploymentBadge(undefined, makeDoc());
+ assert.equal(el, null);
+ });
+
+ it('returns element with deployment-badge class for valid status', () => {
+ const status = { deployed_commit: 'abc123', fix_commits: [], includes_fix: false };
+ const el = renderDeploymentBadge(status, makeDoc());
+ assert.ok(el, 'element should not be null');
+ assert.ok(el.className.includes('deployment-badge'), `className should include deployment-badge, got: ${el.className}`);
+ });
+
+ it('shows "Deployed" text when includes_fix is true', () => {
+ const status = { deployed_commit: 'abc123', fix_commits: [{ hash: 'aabbcc', message: 'fix' }], includes_fix: true };
+ const el = renderDeploymentBadge(status, makeDoc());
+ assert.ok(el.textContent.includes('Deployed'), `expected "Deployed" in "${el.textContent}"`);
+ });
+
+ it('shows "Not deployed" text when includes_fix is false', () => {
+ const status = { deployed_commit: 'abc123', fix_commits: [{ hash: 'aabbcc', message: 'fix' }], includes_fix: false };
+ const el = renderDeploymentBadge(status, makeDoc());
+ assert.ok(el.textContent.includes('Not deployed'), `expected "Not deployed" in "${el.textContent}"`);
+ });
+
+ it('applies deployed class when includes_fix is true', () => {
+ const status = { deployed_commit: 'abc123', fix_commits: [{ hash: 'aabbcc', message: 'fix' }], includes_fix: true };
+ const el = renderDeploymentBadge(status, makeDoc());
+ assert.ok(el.className.includes('deployment-badge--deployed'), `className: ${el.className}`);
+ });
+
+ it('applies pending class when includes_fix is false', () => {
+ const status = { deployed_commit: 'abc123', fix_commits: [{ hash: 'aabbcc', message: 'fix' }], includes_fix: false };
+ const el = renderDeploymentBadge(status, makeDoc());
+ assert.ok(el.className.includes('deployment-badge--pending'), `className: ${el.className}`);
+ });
+
+ it('returns null for doc=null', () => {
+ const status = { deployed_commit: 'abc', fix_commits: [], includes_fix: false };
+ const el = renderDeploymentBadge(status, null);
+ assert.equal(el, null);
+ });
+});