Skip to content

feat: live CLI scan UX + dashboard refresh improvements#23

Merged
root-Manas merged 1 commit intomainfrom
pr/14-live-cli-ui
Mar 29, 2026
Merged

feat: live CLI scan UX + dashboard refresh improvements#23
root-Manas merged 1 commit intomainfrom
pr/14-live-cli-ui

Conversation

@root-Manas
Copy link
Copy Markdown
Owner

Summary

  • add live CLI renderer with colored stage lifecycle output
  • wire engine stage events through app/main for real-time scan visibility
  • improve dashboard clarity with refresh status, manual refresh button, and periodic auto-refresh

Validation

  • go test ./...
  • go build ./...
  • go run ./cmd/macaron -scn google.com -stg subdomains,http -mod fast -thr 10 -rte 30

Copilot AI review requested due to automatic review settings March 29, 2026 06:57
@root-Manas root-Manas merged commit 6ffa484 into main Mar 29, 2026
5 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds real-time scan progress reporting from the engine up to the CLI, and improves the browser dashboard’s scan refresh UX for better visibility into scan state.

Changes:

  • Introduces model.StageEvent and an Options.Progress callback to emit stage lifecycle/progress events during scans.
  • Adds a live CLI renderer with a spinner and colored stage/status output wired through cmd/macaron.
  • Improves the dashboard header/detail view with refresh status, a manual refresh button, and periodic auto-refresh.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
internal/ui/assets/index.html Adds refresh status pill, manual “refresh now”, and periodic auto-refresh logic.
internal/model/model.go Adds StageEventType/StageEvent models for scan progress events.
internal/engine/engine.go Emits stage/target lifecycle events via a new Options.Progress callback.
internal/cliui/live.go New live CLI renderer for stage lifecycle output + spinner UX.
internal/app/app.go Plumbs Progress callback from app-level args into engine options.
cmd/macaron/main.go Instantiates the renderer and hooks progress events into it when not --quiet.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +104 to +107
close(r.spinStop)
r.spinnerOn = false
fmt.Fprint(r.out, "\r\033[2K")
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ANSI clear-line sequences ("\r\033[2K") are written even when output is not a TTY or when NO_COLOR is set, which can pollute piped logs / redirected output (and may behave poorly on some terminals). Consider detecting terminal capability (e.g., using go-isatty which is already in go.mod) and disabling the spinner/clear-line behavior when not interactive.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +28
scanStart time.Time
lastPrinted time.Time
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastPrinted is written to but never read, which adds noise and makes it unclear whether throttling/printing logic is incomplete. Either remove lastPrinted or use it (e.g., to rate-limit spinner updates / avoid flicker).

Copilot uses AI. Check for mistakes.
Comment on lines 459 to +463
loadScans();
setInterval(async () => {
await loadScans();
if (activeId) await loadResult(activeId);
}, refreshMS);
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The periodic refresh uses setInterval with an async callback and no in-flight guard. If /api/scans or /api/results is slow, calls can overlap (including with the manual refresh button) and race the UI state. Consider using a single refresh loop with an "inFlight" flag (or chaining setTimeout after completion) to ensure only one refresh runs at a time.

Suggested change
loadScans();
setInterval(async () => {
await loadScans();
if (activeId) await loadResult(activeId);
}, refreshMS);
let refreshInFlight = false;
async function refreshOnce() {
if (refreshInFlight) {
return;
}
refreshInFlight = true;
try {
await loadScans();
if (activeId) {
await loadResult(activeId);
}
} finally {
refreshInFlight = false;
}
}
function startRefreshLoop() {
const loop = async () => {
await refreshOnce();
setTimeout(loop, refreshMS);
};
setTimeout(loop, refreshMS);
}
loadScans();
startRefreshLoop();

Copilot uses AI. Check for mistakes.
Comment on lines 441 to 450
async function loadScans() {
const res = await fetch('/api/scans');
if (!res.ok) {
document.getElementById('pulse').textContent = 'scan index unavailable';
return;
}
scans = await res.json();
document.getElementById('pulse').textContent = `${scans.length} indexed scans`;
document.getElementById('lastRefresh').textContent = `updated ${new Date().toLocaleTimeString()}`;
renderScanList(scans);
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadScans() does not handle fetch() exceptions (e.g., network errors). With the new async setInterval refresh, this can surface as unhandled promise rejections and stop subsequent refresh logic. Wrap the fetch/json path in try/catch (similar to loadHeat) and update both "pulse" and "lastRefresh" to reflect the failure state.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +98
func (r *LiveRenderer) startSpinnerLocked() {
if r.spinnerOn {
return
}
r.spinnerOn = true
r.spinStop = make(chan struct{})
go r.spin()
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spinner lifecycle has a concurrency bug: startSpinnerLocked overwrites r.spinStop and launches r.spin() which reads r.spinStop from the struct. If a new scan starts soon after a previous stop, the old goroutine can end up selecting on the new channel and never exit, resulting in multiple spinner goroutines writing to the same output. Fix by capturing the stop channel in a local variable and passing it into spin(stopCh) (and selecting on that channel), so each goroutine listens to the channel it was created with.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants