⚠ Your environment isn\'t ready — a required tool is missing. ' + btn("Open Readiness →", "goto:readiness") + "
";
} else if (d && d.overall === "caution") {
html += '
Some environment items need a look before you go further. ' + btn("Open Readiness →", "goto:readiness") + "
";
}
// Journey stepper — always shows where you are in the workflow.
html += '
Your modernization journey
' + journeyHtml(s) + "
";
// One adaptive "do this next" hero so the next click is never ambiguous.
const hero = heroNext(s);
html +=
'
' + esc(hero.eyebrow) + "
" +
'
' + esc(hero.title) + "
" +
'
' + hero.body + "
" +
'
' + hero.actions + "
";
// Autopilot nudge — once there's a checklist with work left, offer to run it
// hands-free. Hidden while a run is already active (the strip covers that).
const planSteps = (s.progress && s.progress.steps.length ? s.progress.steps : (s.plan ? s.plan.steps : [])) || [];
const pendingSteps = planSteps.filter((x) => x.status !== "done").length;
const envBlocked = d && d.overall === "blocked";
if (pendingSteps > 0 && !(s.autopilot && s.autopilot.running)) {
const cta = envBlocked
? btn("Open Readiness →", "goto:readiness")
: btn("▶ Run on autopilot", "autopilot_start", { scope: "phase" }, { primary: true }) + btn("See in Plan →", "goto:plan");
html +=
'
⚡ Run on autopilot beta
' +
'
Instead of clicking each step, let Copilot work the ' + pendingSteps +
" remaining step(s) in order and update this dashboard live. It pauses at the end of the phase and does not commit.
" +
'
' + cta + "
";
}
// Findings snapshot (only once an assessment has been run).
const rep = s.report;
if (rep && rep.findings && rep.findings.length) {
const by = sevCounts(rep.findings);
html += '
";
const rel = (s.tasks || []).filter((t) => t.relevant);
if (rel.length) {
html += '
Detected in this repo ' + rel.length + "
";
rel.forEach((t) => (html += taskRow(t)));
html += "
";
}
return html;
}
function assessmentTab(s) {
const rep = s.report;
if (!rep || !(rep.findings && rep.findings.length)) {
return emptyState(
"No assessment yet",
"Run an assessment to scan this repo for its Java runtime, dependencies, vulnerabilities, and Azure cloud-readiness gaps. Findings land here as a prioritized, clickable checklist — nothing in your code changes.",
btn("Run assessment", "start_assessment", {}, { primary: true })
);
}
const by = sevCounts(rep.findings);
let html = '
Assessment results';
if (rep.generatedAt) html += ' ' + esc(fmtDate(rep.generatedAt)) + "";
html += '' + btn("Re-run", "start_assessment") + "
";
if (rep.headline) html += '
' + esc(rep.headline) + "
";
if (rep.summary) html += '
' + esc(rep.summary) + "
";
if (rep.stack) {
const st = rep.stack;
html += '
";
items.forEach((x) => (html += findingCard(x, s.ordering)));
});
if (rep.strengths && rep.strengths.length) {
html += '
Already done well ' + rep.strengths.length + "
";
rep.strengths.forEach((x) => (html += '
' + esc(x) + "
"));
html += "
";
}
return html;
}
// ---- assessment helpers --------------------------------------------------
function journeyHtml(s) {
const labels = ["Assess", "Remediate", "Validate", "Ship"];
const hasPlan = !!((s.report && s.report.findings) || (s.plan && s.plan.exists) || (s.progress && s.progress.exists));
const gates = s.gates || {};
const anyGate = Object.keys(gates).some((k) => gates[k] && gates[k] !== "not_run");
const done = s.status === "completed";
let cur;
if (done) cur = 4;
else if (anyGate) cur = 2;
else if (hasPlan) cur = 1;
else cur = 0;
let h = '
';
labels.forEach((label, i) => {
if (i > 0) h += '';
const cls = i < cur ? "done" : i === cur ? "current" : "";
const mark = i < cur ? "✓" : String(i + 1);
h += '
' + mark + '' + esc(label) + "
";
});
return h + "
";
}
function heroNext(s) {
const rep = s.report;
const findings = rep && rep.findings ? rep.findings : [];
const hasReport = findings.length > 0;
const p0 = findings.filter((f) => f.severity === "P0");
const p1 = findings.filter((f) => f.severity === "P1");
const gates = s.gates || {};
const anyGate = Object.keys(gates).some((k) => gates[k] && gates[k] !== "not_run");
const hasPlan = !!((s.plan && s.plan.exists) || (s.progress && s.progress.exists));
if (s.status === "completed") {
return {
eyebrow: "Final step",
title: "Ship your changes",
body: "Validations are complete. Open a pull request to hand off the modernized service.",
actions: btn("Open a pull request", "open_pr", {}, { primary: true }) + btn("View summary", "goto:summary"),
};
}
if (!hasReport && !hasPlan) {
return {
eyebrow: "Start here",
title: "Assess the project",
body: "Scan the repo for its Java runtime, dependencies, known CVEs, and Azure cloud-readiness gaps. Nothing changes in your code — you get a prioritized list of findings to work through.",
actions: btn("Run assessment", "start_assessment", {}, { primary: true }),
};
}
if (p0.length) {
return {
eyebrow: "Blocker — fix this first",
title: p0[0].title,
body: esc(p0[0].detail || "A P0 issue is blocking modernization. Resolve it before moving on."),
actions: heroAction(p0[0]) + btn("See all findings", "goto:assessment"),
};
}
if (hasReport && !anyGate) {
if (p1.length) {
return {
eyebrow: "Recommended next",
title: "Start remediation: " + p1[0].title,
body: esc(p1[0].detail || "Work through the high-priority findings first."),
actions: heroAction(p1[0]) + btn("See all findings", "goto:assessment"),
};
}
return {
eyebrow: "Recommended next",
title: "Work through the findings",
body: "Open the Assessment tab and resolve findings by priority. Each one has a button that hands the fix to the agent.",
actions: btn("Open Assessment", "goto:assessment", {}, { primary: true }),
};
}
return {
eyebrow: "Recommended next",
title: "Validate your changes",
body: "Build the project, run unit tests, and re-check CVEs to confirm the migration holds.",
actions: btn("Build & run tests", "run_build_tests", {}, { primary: true }) + btn("Open Validation", "goto:validation"),
};
}
function heroAction(f) {
const act = f.action;
if (act && act.kind && act.kind !== "fix_finding") {
return btn(act.label || "Run this step", act.kind, act.payload || {}, { primary: true });
}
return btn((act && act.label) || "Help me fix this", "fix_finding", findingCtx(f), { primary: true });
}
function findingCard(x, ord) {
const locked = isFindingLocked(x, ord);
let h = '
" + autopilotLog(a)
);
}
const labels = {
completed: "finished — every eligible step is done",
phase_done: "finished this phase",
stuck: "paused — a step needs your input",
cancelled: "stopped",
capped: "reached its step limit",
error: "stopped on an error",
};
const label = labels[a.status] || "finished";
const bad = a.status === "error" || a.status === "stuck";
let note = "Review the changes (nothing was committed), then continue.";
if (a.stuck) note = "Stuck on: " + esc(a.stuck) + ". Open it in Plan & Progress to finish it yourself, then resume.";
if (a.error) note = esc(a.error);
const more =
btn("▶ Continue (next phase)", "autopilot_start", { scope: "phase" }, { primary: true }) +
btn("Dismiss", "autopilot_dismiss", {});
return (
'
";
}
// Autopilot launch card on the Plan tab.
function autopilotCard(s) {
const running = !!(s.autopilot && s.autopilot.running);
const blocked = s.doctor && s.doctor.overall === "blocked";
let body =
'
Let Copilot work the checklist for you. Autopilot runs each eligible step in order, ' +
"updates progress.md, and streams results here live. It pauses at the end of the phase, stops if a step needs a decision, " +
"and does not commit — so you stay in control and can review the diff.
";
let actions;
if (running) {
actions = btn("■ Stop autopilot", "autopilot_stop", {});
} else if (blocked) {
actions = 'Environment is not ready. ' + btn("Open Readiness →", "goto:readiness");
} else {
actions =
btn("▶ Run this phase", "autopilot_start", { scope: "phase" }, { primary: true }) +
btn("Run all phases", "autopilot_start", { scope: "all" });
}
return '
⚡ Autopilot beta
' + body + '
' + actions + "
";
}
function planTab(s) {
const usingProgress = !!(s.progress && s.progress.steps.length);
const steps = (usingProgress ? s.progress.steps : (s.plan ? s.plan.steps : [])) || [];
if (!steps.length) {
return emptyState(
"No plan yet",
"Run an assessment or generate an upgrade plan. App Modernization writes plan.md and a progress.md checklist, and your steps land here — each unchecked one becomes a button that hands that exact step to the agent.",
btn("Run assessment", "start_assessment", {}, { primary: true }) + btn("Generate upgrade plan", "generate_plan", { targetJava: 21 })
);
}
const src = usingProgress ? "progress.md" : "plan.md";
const total = steps.length;
const done = steps.filter((x) => x.status === "done").length;
const ord = s.ordering || { activeRank: null, activePhase: null };
// Intro + overall progress + legend.
let html = '
Your live modernization checklist. Each unchecked step has a Work on this button that hands just that step to the agent in this session — it checks the box here when the step is done.
";
// Recommended-order guide so users don't work steps out of sequence.
html += approachGuide(ord);
// Autopilot: hand the whole phase to the agent instead of clicking each step.
html += autopilotCard(s);
// "Continue here" — the recommended next step: first not-done step in the
// active phase (falls back to the first not-done step overall).
const next = steps.find((x) => x.status !== "done" && (ord.activeRank == null || x.rank === ord.activeRank)) || steps.find((x) => x.status !== "done");
if (next) {
html +=
'
";
});
return html;
}
function approachGuide(ord) {
const order = ["Assessment", "P0 Build", "P1 Security", "P2 Runtime", "P3 Observability", "Validate"];
const activeIdx = ord && ord.activeRank != null ? ord.activeRank : -1;
const flow = order
.map((name, i) => '' + esc(name) + "")
.join('›');
let body = "Work top-down and finish each phase before the next — e.g. don't upgrade the Java runtime (P2) while the build is still broken (P0). Steps in later phases are marked 🔒 until the current phase is done; Continue here always points to the safe next step.";
if (ord && ord.activePhase) body += " You're in " + esc(ord.activePhase) + ".";
return '
Recommended approach
' + flow + "
" + body + "
";
}
function shortPhase(name) {
if (!name) return "the current phase";
const m = String(name).match(/\bP[0-3]\b/i);
return m ? m[0].toUpperCase() : name;
}
function stepAction(st) {
const label = st.status === "in_progress" ? "Resume this step" : "Work on this step";
return btn(label, "work_step", { title: st.title, section: st.section }, { primary: true });
}
function stepRowAction(st, isValidation, locked, ord) {
if (st.status === "done") return '✓ Done';
if (isValidation) return 'run from Validation →';
if (locked) {
return '🔒 after ' + esc(shortPhase(ord && ord.activePhase)) + "" + btn("Do anyway", "work_step", { title: st.title, section: st.section });
}
const label = st.status === "in_progress" ? "Resume" : "Work on this";
return btn(label, "work_step", { title: st.title, section: st.section });
}
function validationTab(s) {
const gates = s.gates || {};
const labels = { build: "Build", tests: "Unit Tests", cve: "CVE Check", consistency: "Consistency", completeness: "Completeness" };
let html = '
CVE scan uses #appmod-validate-cves-for-java; test generation uses #appmod-generate-tests-for-java.
";
return html;
}
function doctorTab(s) {
const d = s.doctor;
if (!d) {
return emptyState(
"Environment readiness",
"Check that your machine has the tools the App Modernization workflow needs — a JDK, your build tool, git, and (optionally) Docker and the Azure CLI. This only reads version numbers; nothing is installed or changed.",
btn("Check my environment", "recheck_env", {}, { primary: true })
);
}
const tone = d.overall === "ready" ? "b-green" : d.overall === "caution" ? "b-amber" : "b-red";
const head =
d.overall === "ready" ? "Your environment is ready" :
d.overall === "caution" ? "Almost ready — a couple of things to check" :
"Not ready yet — resolve the blockers below";
const sub =
d.overall === "ready" ? "All required tools are installed. You're clear to start modernizing." :
d.overall === "caution" ? "The required tools are present; some optional or recommended items need attention." :
"One or more required tools are missing. Fix these before building the project or running tasks.";
const notAssessed = d.groups.some((g) => g.checks.some((c) => c.id === "assessed" && c.status !== "ok"));
let heroActions = btn("Re-check environment", "recheck_env", {}, { primary: true });
if (notAssessed) heroActions += btn("Run assessment", "start_assessment", {});
let html =
'
"
);
}
function emptyState(big, sub, action) {
return '
' + esc(big) + "
" + sub + "
" + (action ? '
' + action + "
" : "") + "
";
}
function miniMd(md) {
const lines = esc(md).split(/\r?\n/);
let out = "";
let inList = false;
for (let line of lines) {
if (/^\s*[-*]\s+/.test(line)) {
if (!inList) { out += "
"; inList = true; }
out += "
" + line.replace(/^\s*[-*]\s+/, "") + "
";
continue;
}
if (inList) { out += "
"; inList = false; }
const h = line.match(/^(#{1,3})\s+(.*)/);
if (h) { out += "" + h[2] + ""; continue; }
if (line.trim() === "") { out += " "; continue; }
out += "
" + line + "
";
}
if (inList) out += "";
return out.replace(/`([^`]+)`/g, "$1");
}
// ---- shell ---------------------------------------------------------------
function render() {
if (!state) {
$("#app").innerHTML = '
Loading…
';
return;
}
if (!state.ok) {
$("#app").innerHTML = emptyState("Repo not available", esc(state.error || "Could not read the repository."), btn("Retry", "refresh", {}, { primary: true }));
renderHeader();
return;
}
renderHeader();
autopilotRunning = !!(state.autopilot && state.autopilot.running);
const tabs = { overview: overviewTab, readiness: doctorTab, assessment: assessmentTab, plan: planTab, validation: validationTab, tasks: tasksTab, summary: summaryTab };
let body = "";
try {
body = autopilotStrip(state);
body += pending ? '
Sent to the agent: ' + esc(pending) + ". Watch the chat; this view refreshes when the turn finishes.
" : "";
body += (tabs[activeTab] || overviewTab)(state);
} catch (e) {
// A tab renderer threw — show the error instead of silently leaving the
// previous tab's content on screen (which looks like a dead click).
body = '
This view hit an error: ' + esc(e && e.message ? e.message : String(e)) + " " + btn("Reload", "refresh", {}, { primary: true }) + "
";
}
$("#app").innerHTML = body;
}
function renderHeader() {
const s = state || {};
const a = s.assessment || {};
$("#status").innerHTML = s.status ? statusBadge(s.status) : "";
$("#repo").textContent = s.repoPath ? s.repoPath.split(/[\\/]/).slice(-2).join("/") : "";
const chips = $("#chips");
if (!s.ok) { chips.innerHTML = ""; }
else {
chips.innerHTML = [
a.buildTool ? '' + esc(a.buildTool) + "" : "",
a.javaVersion ? 'Java ' + esc(a.javaVersion) + "" : "",
a.springBoot ? 'Spring Boot' : "",
a.hasDockerfile ? 'Docker' : "",
s.git && s.git.branch ? '⎇ ' + esc(s.git.branch) + "" : "",
].join("");
}
const counts = {
assessment: s.report && s.report.findings ? s.report.findings.length : 0,
plan: s.progress && s.progress.steps.length ? s.progress.steps.length : (s.plan ? s.plan.steps.length : 0),
tasks: (s.tasks ? s.tasks.length : 0) + (s.skills ? s.skills.length : 0),
};
document.querySelectorAll("nav.tabs button").forEach((b) => {
b.classList.toggle("active", b.dataset.tab === activeTab);
const c = b.querySelector(".count");
if (!c) return;
if (b.dataset.tab === "readiness") {
const ov = s.doctor && s.doctor.overall;
c.textContent = ov === "blocked" ? "⚠" : ov === "caution" ? "!" : "";
c.className = "count " + (ov === "blocked" ? "c-red" : ov === "caution" ? "c-amber" : "");
} else {
c.textContent = counts[b.dataset.tab] ? "(" + counts[b.dataset.tab] + ")" : "";
}
});
}
async function loadState() {
try {
const r = await fetch("/state" + tokenQuery);
state = await r.json();
render();
} catch (e) {
toast("Failed to load state");
}
}
async function doAction(kind, payload) {
if (kind === "refresh") { toast("Refreshing…"); await loadState(); return; }
if (kind.indexOf("goto:") === 0) { activeTab = kind.slice(5); render(); window.scrollTo(0, 0); return; }
// Autopilot controls stream their results over SSE, so they must not set the
// blocking "pending" banner that freezes the whole view for a one-shot send.
const isAutopilot = kind.indexOf("autopilot_") === 0;
try {
const r = await fetch("/action" + tokenQuery, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind, payload }),
});
const res = await r.json();
if (res.ok) {
if (res.state) {
// Local, synchronous actions (refresh, recheck_env) hand back a
// fresh snapshot inline. Apply it now and clear any spinner instead
// of waiting on an SSE broadcast that can be missed if the provider
// restarts or the connection drops — which would spin forever.
state = res.state;
pending = null;
} else if (!isAutopilot) {
pending = res.label || kind;
}
toast(res.message || "Sent to the agent");
render();
} else {
toast(res.error || "Action failed");
}
} catch (e) {
toast("Action failed");
}
}
// event delegation for all action buttons + tabs
document.addEventListener("click", (e) => {
const tabBtn = e.target.closest("nav.tabs button");
if (tabBtn) { activeTab = tabBtn.dataset.tab; render(); return; }
const b = e.target.closest("[data-kind]");
if (b && !b.disabled) {
let payload = {};
try { payload = JSON.parse(b.dataset.payload || "{}"); } catch {}
doAction(b.dataset.kind, payload);
}
});
// live updates: server pushes a fresh snapshot when the agent finishes a turn
try {
const ev = new EventSource("/events" + tokenQuery);
// Resync whenever the stream (re)connects. If the provider restarted while an
// action was in flight, its "done" broadcast was lost; re-fetching state here
// clears any stuck spinner and shows the real result.
ev.onopen = () => { loadState(); };
ev.onmessage = (m) => {
try {
const data = JSON.parse(m.data);
if (data && data.type === "state") { state = data.state; pending = null; render(); }
else if (data && data.type === "autopilot" && state) { state.autopilot = data.autopilot; render(); }
} catch {}
};
} catch {}
render();
loadState();
}
export function renderHtml({ instanceId, initialTab, token } = {}) {
// Escape `<` so a stray "" in any field can't break out of the inline
// " +
"