diff --git a/web/static/js/issues.js b/web/static/js/issues.js
new file mode 100644
index 0000000..c5a301b
--- /dev/null
+++ b/web/static/js/issues.js
@@ -0,0 +1,124 @@
+// Issues & Warnings view â surfaces all validation problems before migration
+
+import { state } from './state.js';
+import { escHtml, formatDate } from './utils.js';
+import { navigate } from './router.js';
+
+export function renderIssues() {
+ const outlet = document.getElementById('router-outlet');
+ const templates = state.templates || [];
+
+ const blocked = templates.filter(t => t.blockers && t.blockers.length > 0);
+ const warnings = templates.filter(t =>
+ (!t.blockers || t.blockers.length === 0) && t.warnings && t.warnings.length > 0
+ );
+
+ if (!state.auth.adobe || !state.auth.docusign) {
+ outlet.innerHTML = `
+
+ âšī¸Connect both platforms to see validation results.
`;
+ return;
+ }
+
+ if (!blocked.length && !warnings.length) {
+ outlet.innerHTML = `
+
+
+
đ
+
+
All templates are ready!
+
No blockers or warnings found across ${templates.length} template${templates.length !== 1 ? 's' : ''}.
+
+
`;
+ return;
+ }
+
+ outlet.innerHTML = `
+
+
+ ${blocked.length ? `
+
+
+ đĢ Blockers â ${blocked.length} template${blocked.length > 1 ? 's' : ''} will fail migration
+
+
+ ${blocked.map(t => _blockerItem(t)).join('')}
+
+
` : ''}
+
+ ${warnings.length ? `
+
+
+ â Warnings â ${warnings.length} template${warnings.length > 1 ? 's' : ''} will migrate with caveats
+
+
+ ${warnings.map(t => _warningItem(t)).join('')}
+
+
` : ''}
+ `;
+
+ // Migrate Anyway buttons
+ document.querySelectorAll('.btn-migrate-anyway').forEach(btn => {
+ btn.addEventListener('click', () => {
+ import('./migration.js').then(m => m.showOptionsModal([btn.dataset.id]));
+ });
+ });
+
+ // View Template links
+ document.querySelectorAll('.btn-view-template').forEach(btn => {
+ btn.addEventListener('click', () => navigate(`#/templates/${btn.dataset.id}`));
+ });
+}
+
+function _blockerItem(t) {
+ const blockers = t.blockers || [];
+ return `
+
+
đĢ
+
+
${escHtml(t.name)}
+ ${blockers.map(b => `
âĸ ${escHtml(b)}
`).join('')}
+
Modified ${formatDate(t.adobe_modified)}
+
+
+
+
+
+ `;
+}
+
+function _warningItem(t) {
+ const warnings = t.warnings || [];
+ return `
+
+
â ī¸
+
+
${escHtml(t.name)}
+ ${warnings.slice(0, 3).map(w => `
âĸ ${escHtml(w)}
`).join('')}
+ ${warnings.length > 3 ? `
âĻ +${warnings.length - 3} more
` : ''}
+
Modified ${formatDate(t.adobe_modified)}
+
+
+
+
+
+
+ `;
+}